From 40eefce085582acc4fd7a57042983d9cf7bb3e1e Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 19 Aug 2025 14:22:44 +0200 Subject: [PATCH 001/329] chore: mark 1.56.0-next (#37088) --- package-lock.json | 66 +++++++++---------- package.json | 2 +- .../playwright-browser-chromium/package.json | 4 +- .../playwright-browser-firefox/package.json | 4 +- .../playwright-browser-webkit/package.json | 4 +- packages/playwright-chromium/package.json | 4 +- packages/playwright-client/package.json | 2 +- packages/playwright-core/package.json | 2 +- packages/playwright-ct-core/package.json | 6 +- packages/playwright-ct-react/package.json | 4 +- packages/playwright-ct-react17/package.json | 4 +- packages/playwright-ct-svelte/package.json | 4 +- packages/playwright-ct-vue/package.json | 4 +- packages/playwright-firefox/package.json | 4 +- packages/playwright-test-mcp/package.json | 4 +- packages/playwright-test/package.json | 4 +- packages/playwright-webkit/package.json | 4 +- packages/playwright/package.json | 4 +- 18 files changed, 65 insertions(+), 65 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00229fb661a86..4d1c0cbc0ca23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "playwright-internal", - "version": "1.55.0-next", + "version": "1.56.0-next", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "playwright-internal", - "version": "1.55.0-next", + "version": "1.56.0-next", "license": "Apache-2.0", "workspaces": [ "packages/*" @@ -8616,10 +8616,10 @@ "version": "0.0.0" }, "packages/playwright": { - "version": "1.55.0-next", + "version": "1.56.0-next", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" }, "bin": { "playwright": "cli.js" @@ -8633,11 +8633,11 @@ }, "packages/playwright-browser-chromium": { "name": "@playwright/browser-chromium", - "version": "1.55.0-next", + "version": "1.56.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" }, "engines": { "node": ">=18" @@ -8645,11 +8645,11 @@ }, "packages/playwright-browser-firefox": { "name": "@playwright/browser-firefox", - "version": "1.55.0-next", + "version": "1.56.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" }, "engines": { "node": ">=18" @@ -8657,22 +8657,22 @@ }, "packages/playwright-browser-webkit": { "name": "@playwright/browser-webkit", - "version": "1.55.0-next", + "version": "1.56.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" }, "engines": { "node": ">=18" } }, "packages/playwright-chromium": { - "version": "1.55.0-next", + "version": "1.56.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" }, "bin": { "playwright": "cli.js" @@ -8686,14 +8686,14 @@ "version": "0.0.0", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" }, "engines": { "node": ">=18" } }, "packages/playwright-core": { - "version": "1.55.0-next", + "version": "1.56.0-next", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -8704,11 +8704,11 @@ }, "packages/playwright-ct-core": { "name": "@playwright/experimental-ct-core", - "version": "1.55.0-next", + "version": "1.56.0-next", "license": "Apache-2.0", "dependencies": { - "playwright": "1.55.0-next", - "playwright-core": "1.55.0-next", + "playwright": "1.56.0-next", + "playwright-core": "1.56.0-next", "vite": "^6.3.4" }, "engines": { @@ -8717,10 +8717,10 @@ }, "packages/playwright-ct-react": { "name": "@playwright/experimental-ct-react", - "version": "1.55.0-next", + "version": "1.56.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.55.0-next", + "@playwright/experimental-ct-core": "1.56.0-next", "@vitejs/plugin-react": "^4.2.1" }, "bin": { @@ -8732,10 +8732,10 @@ }, "packages/playwright-ct-react17": { "name": "@playwright/experimental-ct-react17", - "version": "1.55.0-next", + "version": "1.56.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.55.0-next", + "@playwright/experimental-ct-core": "1.56.0-next", "@vitejs/plugin-react": "^4.2.1" }, "bin": { @@ -8747,10 +8747,10 @@ }, "packages/playwright-ct-svelte": { "name": "@playwright/experimental-ct-svelte", - "version": "1.55.0-next", + "version": "1.56.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.55.0-next", + "@playwright/experimental-ct-core": "1.56.0-next", "@sveltejs/vite-plugin-svelte": "^5.1.0" }, "bin": { @@ -8803,10 +8803,10 @@ }, "packages/playwright-ct-vue": { "name": "@playwright/experimental-ct-vue", - "version": "1.55.0-next", + "version": "1.56.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.55.0-next", + "@playwright/experimental-ct-core": "1.56.0-next", "@vitejs/plugin-vue": "^5.2.0" }, "bin": { @@ -8817,11 +8817,11 @@ } }, "packages/playwright-firefox": { - "version": "1.55.0-next", + "version": "1.56.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" }, "bin": { "playwright": "cli.js" @@ -8877,10 +8877,10 @@ }, "packages/playwright-test": { "name": "@playwright/test", - "version": "1.55.0-next", + "version": "1.56.0-next", "license": "Apache-2.0", "dependencies": { - "playwright": "1.55.0-next" + "playwright": "1.56.0-next" }, "bin": { "playwright": "cli.js" @@ -8895,8 +8895,8 @@ "license": "Apache-2.0", "dependencies": { "commander": "^13.1.0", - "playwright": "1.55.0-next", - "playwright-core": "1.55.0-next" + "playwright": "1.56.0-next", + "playwright-core": "1.56.0-next" }, "bin": { "mcp-server-playwright-test": "cli.js" @@ -8923,11 +8923,11 @@ } }, "packages/playwright-webkit": { - "version": "1.55.0-next", + "version": "1.56.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" }, "bin": { "playwright": "cli.js" diff --git a/package.json b/package.json index 0677532561a88..969d856fbe815 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "playwright-internal", "private": true, - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "A high-level API to automate web browsers", "repository": { "type": "git", diff --git a/packages/playwright-browser-chromium/package.json b/packages/playwright-browser-chromium/package.json index 32b2f075a720c..e768e2496707e 100644 --- a/packages/playwright-browser-chromium/package.json +++ b/packages/playwright-browser-chromium/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/browser-chromium", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "Playwright package that automatically installs Chromium", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" } } diff --git a/packages/playwright-browser-firefox/package.json b/packages/playwright-browser-firefox/package.json index df7b8fea635f6..39702a88f0406 100644 --- a/packages/playwright-browser-firefox/package.json +++ b/packages/playwright-browser-firefox/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/browser-firefox", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "Playwright package that automatically installs Firefox", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" } } diff --git a/packages/playwright-browser-webkit/package.json b/packages/playwright-browser-webkit/package.json index bb35606e27006..7e7a32e669753 100644 --- a/packages/playwright-browser-webkit/package.json +++ b/packages/playwright-browser-webkit/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/browser-webkit", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "Playwright package that automatically installs WebKit", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" } } diff --git a/packages/playwright-chromium/package.json b/packages/playwright-chromium/package.json index 5290fcfbd247a..61f3fde050267 100644 --- a/packages/playwright-chromium/package.json +++ b/packages/playwright-chromium/package.json @@ -1,6 +1,6 @@ { "name": "playwright-chromium", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "A high-level API to automate Chromium", "repository": { "type": "git", @@ -30,6 +30,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" } } diff --git a/packages/playwright-client/package.json b/packages/playwright-client/package.json index 854df0d17903f..0058240e1ad0c 100644 --- a/packages/playwright-client/package.json +++ b/packages/playwright-client/package.json @@ -30,6 +30,6 @@ "watch": "npm run esbuild -- --watch" }, "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" } } diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json index 9e62d8396ffc7..ece81c449d6a4 100644 --- a/packages/playwright-core/package.json +++ b/packages/playwright-core/package.json @@ -1,6 +1,6 @@ { "name": "playwright-core", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "A high-level API to automate web browsers", "repository": { "type": "git", diff --git a/packages/playwright-ct-core/package.json b/packages/playwright-ct-core/package.json index b2875bb7e5c3c..330719a4d6255 100644 --- a/packages/playwright-ct-core/package.json +++ b/packages/playwright-ct-core/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/experimental-ct-core", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "Playwright Component Testing Helpers", "repository": { "type": "git", @@ -26,8 +26,8 @@ } }, "dependencies": { - "playwright-core": "1.55.0-next", + "playwright-core": "1.56.0-next", "vite": "^6.3.4", - "playwright": "1.55.0-next" + "playwright": "1.56.0-next" } } diff --git a/packages/playwright-ct-react/package.json b/packages/playwright-ct-react/package.json index 6068fb5b71835..27098cfbafc3c 100644 --- a/packages/playwright-ct-react/package.json +++ b/packages/playwright-ct-react/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/experimental-ct-react", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "Playwright Component Testing for React", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.55.0-next", + "@playwright/experimental-ct-core": "1.56.0-next", "@vitejs/plugin-react": "^4.2.1" }, "bin": { diff --git a/packages/playwright-ct-react17/package.json b/packages/playwright-ct-react17/package.json index d4b196a36ad34..3754c8583958f 100644 --- a/packages/playwright-ct-react17/package.json +++ b/packages/playwright-ct-react17/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/experimental-ct-react17", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "Playwright Component Testing for React", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.55.0-next", + "@playwright/experimental-ct-core": "1.56.0-next", "@vitejs/plugin-react": "^4.2.1" }, "bin": { diff --git a/packages/playwright-ct-svelte/package.json b/packages/playwright-ct-svelte/package.json index edc0b63bcacf3..adfca41c1ccdd 100644 --- a/packages/playwright-ct-svelte/package.json +++ b/packages/playwright-ct-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/experimental-ct-svelte", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "Playwright Component Testing for Svelte", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.55.0-next", + "@playwright/experimental-ct-core": "1.56.0-next", "@sveltejs/vite-plugin-svelte": "^5.1.0" }, "devDependencies": { diff --git a/packages/playwright-ct-vue/package.json b/packages/playwright-ct-vue/package.json index cb86cd2dce7fd..6beff5097dabb 100644 --- a/packages/playwright-ct-vue/package.json +++ b/packages/playwright-ct-vue/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/experimental-ct-vue", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "Playwright Component Testing for Vue", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.55.0-next", + "@playwright/experimental-ct-core": "1.56.0-next", "@vitejs/plugin-vue": "^5.2.0" }, "bin": { diff --git a/packages/playwright-firefox/package.json b/packages/playwright-firefox/package.json index a9b59ed7b68b8..8226a820dc6a4 100644 --- a/packages/playwright-firefox/package.json +++ b/packages/playwright-firefox/package.json @@ -1,6 +1,6 @@ { "name": "playwright-firefox", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "A high-level API to automate Firefox", "repository": { "type": "git", @@ -30,6 +30,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" } } diff --git a/packages/playwright-test-mcp/package.json b/packages/playwright-test-mcp/package.json index 45d0dfb47b8f8..b87d2b60c1c0c 100644 --- a/packages/playwright-test-mcp/package.json +++ b/packages/playwright-test-mcp/package.json @@ -32,7 +32,7 @@ "scripts": {}, "dependencies": { "commander": "^13.1.0", - "playwright": "1.55.0-next", - "playwright-core": "1.55.0-next" + "playwright": "1.56.0-next", + "playwright-core": "1.56.0-next" } } diff --git a/packages/playwright-test/package.json b/packages/playwright-test/package.json index 660d4fadbaf6f..cbcef86b234a6 100644 --- a/packages/playwright-test/package.json +++ b/packages/playwright-test/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/test", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "A high-level API to automate web browsers", "repository": { "type": "git", @@ -30,6 +30,6 @@ }, "scripts": {}, "dependencies": { - "playwright": "1.55.0-next" + "playwright": "1.56.0-next" } } diff --git a/packages/playwright-webkit/package.json b/packages/playwright-webkit/package.json index 5be4b5379f5a4..8a95b554f728f 100644 --- a/packages/playwright-webkit/package.json +++ b/packages/playwright-webkit/package.json @@ -1,6 +1,6 @@ { "name": "playwright-webkit", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "A high-level API to automate WebKit", "repository": { "type": "git", @@ -30,6 +30,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" } } diff --git a/packages/playwright/package.json b/packages/playwright/package.json index 60aac8e5f3d1a..d4379d66e4cd6 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -1,6 +1,6 @@ { "name": "playwright", - "version": "1.55.0-next", + "version": "1.56.0-next", "description": "A high-level API to automate web browsers", "repository": { "type": "git", @@ -60,7 +60,7 @@ }, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0-next" + "playwright-core": "1.56.0-next" }, "optionalDependencies": { "fsevents": "2.3.2" From a4e6e80cff30a53a817b7befa4b790b2a56159f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:26:23 +0200 Subject: [PATCH 002/329] chore(deps): bump actions/checkout from 4 to 5 in the actions group (#37108) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../cherry_pick_into_release_branch.yml | 2 +- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/create_test_report.yml | 2 +- .github/workflows/infra.yml | 4 +-- .../pr_check_client_side_changes.yml | 2 +- .github/workflows/publish_canary.yml | 4 +-- .github/workflows/publish_release_docker.yml | 2 +- .github/workflows/publish_release_driver.yml | 2 +- .github/workflows/publish_release_npm.yml | 2 +- .../workflows/publish_release_traceviewer.yml | 2 +- .../roll_browser_into_playwright.yml | 2 +- .github/workflows/roll_driver_nodejs.yml | 2 +- .github/workflows/roll_stable_test_runner.yml | 2 +- .github/workflows/tests_bidi.yml | 4 +-- .github/workflows/tests_components.yml | 2 +- .github/workflows/tests_others.yml | 10 +++---- .github/workflows/tests_primary.yml | 12 ++++----- .github/workflows/tests_secondary.yml | 26 +++++++++---------- .github/workflows/tests_video.yml | 2 +- 19 files changed, 43 insertions(+), 43 deletions(-) diff --git a/.github/workflows/cherry_pick_into_release_branch.yml b/.github/workflows/cherry_pick_into_release_branch.yml index b0f635a69b199..88d86e8369c89 100644 --- a/.github/workflows/cherry_pick_into_release_branch.yml +++ b/.github/workflows/cherry_pick_into_release_branch.yml @@ -26,7 +26,7 @@ jobs: echo "Version is not a two digit semver version" exit 1 fi - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: release-${{ github.event.inputs.version }} fetch-depth: 0 diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 54cf652ae21b6..563f3608f96d2 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -8,7 +8,7 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: "22" diff --git a/.github/workflows/create_test_report.yml b/.github/workflows/create_test_report.yml index 6758556deff7e..9e2d5ffb84d78 100644 --- a/.github/workflows/create_test_report.yml +++ b/.github/workflows/create_test_report.yml @@ -14,7 +14,7 @@ jobs: if: ${{ github.event.workflow_run.event == 'pull_request' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/infra.yml b/.github/workflows/infra.yml index 0a03f10f1338a..af92b588694a8 100644 --- a/.github/workflows/infra.yml +++ b/.github/workflows/infra.yml @@ -18,7 +18,7 @@ jobs: name: "docs & lint" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 @@ -40,7 +40,7 @@ jobs: name: "Lint snippets" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/pr_check_client_side_changes.yml b/.github/workflows/pr_check_client_side_changes.yml index 12b7e5dff9131..f3c26e6ece196 100644 --- a/.github/workflows/pr_check_client_side_changes.yml +++ b/.github/workflows/pr_check_client_side_changes.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-24.04 if: github.repository == 'microsoft/playwright' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/create-github-app-token@v2 id: app-token with: diff --git a/.github/workflows/publish_canary.yml b/.github/workflows/publish_canary.yml index 87ebcf27b3a5e..db7b62c8e14c2 100644 --- a/.github/workflows/publish_canary.yml +++ b/.github/workflows/publish_canary.yml @@ -21,7 +21,7 @@ jobs: contents: read # This is required for actions/checkout to succeed environment: allow-publish-driver-to-cdn # This is required for OIDC login (azure/login) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 @@ -67,7 +67,7 @@ jobs: runs-on: ubuntu-24.04 if: github.repository == 'microsoft/playwright' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/publish_release_docker.yml b/.github/workflows/publish_release_docker.yml index c9603bffcdf21..4f4b916df514a 100644 --- a/.github/workflows/publish_release_docker.yml +++ b/.github/workflows/publish_release_docker.yml @@ -18,7 +18,7 @@ jobs: if: github.repository == 'microsoft/playwright' environment: allow-publishing-docker-to-acr steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/publish_release_driver.yml b/.github/workflows/publish_release_driver.yml index 328deaf01cb7a..658a11727af0c 100644 --- a/.github/workflows/publish_release_driver.yml +++ b/.github/workflows/publish_release_driver.yml @@ -17,7 +17,7 @@ jobs: contents: read # This is required for actions/checkout to succeed environment: allow-publish-driver-to-cdn # This is required for OIDC login (azure/login) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/publish_release_npm.yml b/.github/workflows/publish_release_npm.yml index cab5e13596467..65f4d022b8865 100644 --- a/.github/workflows/publish_release_npm.yml +++ b/.github/workflows/publish_release_npm.yml @@ -16,7 +16,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/publish_release_traceviewer.yml b/.github/workflows/publish_release_traceviewer.yml index b497f5290cdcc..0428edd4307ab 100644 --- a/.github/workflows/publish_release_traceviewer.yml +++ b/.github/workflows/publish_release_traceviewer.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-24.04 if: github.repository == 'microsoft/playwright' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/roll_browser_into_playwright.yml b/.github/workflows/roll_browser_into_playwright.yml index 3dc822638e361..697f0b85d3dcb 100644 --- a/.github/workflows/roll_browser_into_playwright.yml +++ b/.github/workflows/roll_browser_into_playwright.yml @@ -19,7 +19,7 @@ jobs: roll: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/roll_driver_nodejs.yml b/.github/workflows/roll_driver_nodejs.yml index ee993f980f12d..7ba008daaa664 100644 --- a/.github/workflows/roll_driver_nodejs.yml +++ b/.github/workflows/roll_driver_nodejs.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 diff --git a/.github/workflows/roll_stable_test_runner.yml b/.github/workflows/roll_stable_test_runner.yml index 2c908520444d8..6580420957ef0 100644 --- a/.github/workflows/roll_stable_test_runner.yml +++ b/.github/workflows/roll_stable_test_runner.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 22 diff --git a/.github/workflows/tests_bidi.yml b/.github/workflows/tests_bidi.yml index 4d6dc437325ce..031a6ecd07e73 100644 --- a/.github/workflows/tests_bidi.yml +++ b/.github/workflows/tests_bidi.yml @@ -34,9 +34,9 @@ jobs: matrix: channel: [bidi-chromium, moz-firefox-nightly] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 if: github.event_name != 'workflow_dispatch' - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 if: github.event_name == 'workflow_dispatch' with: ref: ${{ github.event.inputs.ref }} diff --git a/.github/workflows/tests_components.yml b/.github/workflows/tests_components.yml index 093d5de0c406d..39f33c9455fd0 100644 --- a/.github/workflows/tests_components.yml +++ b/.github/workflows/tests_components.yml @@ -32,7 +32,7 @@ jobs: node-version: 22 runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/tests_others.yml b/.github/workflows/tests_others.yml index ed18e1541c635..68460879128e6 100644 --- a/.github/workflows/tests_others.yml +++ b/.github/workflows/tests_others.yml @@ -27,7 +27,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 20 @@ -63,7 +63,7 @@ jobs: id-token: write # This is required for OIDC login (azure/login) to succeed contents: read # This is required for actions/checkout to succeed steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' @@ -97,7 +97,7 @@ jobs: clock: [frozen, realtime] runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: node-version: 20 @@ -122,7 +122,7 @@ jobs: matrix: clock: [frozen, realtime] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: node-version: 20 @@ -146,7 +146,7 @@ jobs: contents: read # This is required for actions/checkout to succeed runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed if: ${{ runner.os == 'Linux' }} run: | diff --git a/.github/workflows/tests_primary.yml b/.github/workflows/tests_primary.yml index ec575d808e798..5176a7335bdb9 100644 --- a/.github/workflows/tests_primary.yml +++ b/.github/workflows/tests_primary.yml @@ -50,7 +50,7 @@ jobs: id-token: write # This is required for OIDC login (azure/login) to succeed contents: read # This is required for actions/checkout to succeed steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: node-version: ${{ matrix.node-version }} @@ -73,7 +73,7 @@ jobs: id-token: write # This is required for OIDC login (azure/login) to succeed contents: read # This is required for actions/checkout to succeed steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: chromium-tip-of-tree @@ -125,7 +125,7 @@ jobs: id-token: write # This is required for OIDC login (azure/login) to succeed contents: read # This is required for actions/checkout to succeed steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: node-version: ${{matrix.node-version}} @@ -141,7 +141,7 @@ jobs: name: Web Components runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 @@ -177,7 +177,7 @@ jobs: PWTEST_BOT_NAME: "vscode-extension" DEBUG_GIT_COMMIT_INFO: "" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 @@ -223,7 +223,7 @@ jobs: id-token: write # This is required for OIDC login (azure/login) to succeed contents: read # This is required for actions/checkout to succeed steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - run: npm install -g yarn@1 - run: npm install -g pnpm@8 - name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed diff --git a/.github/workflows/tests_secondary.yml b/.github/workflows/tests_secondary.yml index eafe68cd60f3e..04c4d67a10e98 100644 --- a/.github/workflows/tests_secondary.yml +++ b/.github/workflows/tests_secondary.yml @@ -34,7 +34,7 @@ jobs: os: [ubuntu-24.04] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: ${{ matrix.browser }} chromium @@ -61,7 +61,7 @@ jobs: browser: webkit runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: ${{ matrix.browser }} chromium @@ -80,7 +80,7 @@ jobs: browser: [chromium, firefox, webkit] runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: ${{ matrix.browser }} chromium @@ -106,7 +106,7 @@ jobs: node_version: 24 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - run: npm install -g yarn@1 - run: npm install -g pnpm@8 - name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed @@ -139,7 +139,7 @@ jobs: os: ubuntu-22.04 runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: ${{ matrix.browser }} chromium @@ -158,7 +158,7 @@ jobs: mode: [driver, service] runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: chromium @@ -189,7 +189,7 @@ jobs: channel: chromium-tip-of-tree runs-on: ${{ matrix.runs-on }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: ${{ matrix.browser }} chromium ${{ matrix.channel }} @@ -212,7 +212,7 @@ jobs: channel: [chrome, chrome-beta, msedge, msedge-beta, msedge-dev] runs-on: [ubuntu-22.04, macos-latest, windows-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: ${{ matrix.channel }} @@ -238,7 +238,7 @@ jobs: - os: ubuntu-22.04 headed: '' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: chromium-tip-of-tree @@ -259,7 +259,7 @@ jobs: matrix: os: [ubuntu-22.04] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: chromium-tip-of-tree-headless-shell @@ -280,7 +280,7 @@ jobs: matrix: os: [ubuntu-22.04, windows-latest, macos-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: firefox-beta chromium @@ -296,7 +296,7 @@ jobs: name: "build-playwright-driver" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 18 @@ -313,7 +313,7 @@ jobs: runs-on: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.runs-on }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: # TODO: this should pass --no-shell. diff --git a/.github/workflows/tests_video.yml b/.github/workflows/tests_video.yml index cc303d79c5aae..1095e293eb79d 100644 --- a/.github/workflows/tests_video.yml +++ b/.github/workflows/tests_video.yml @@ -25,7 +25,7 @@ jobs: contents: read # This is required for actions/checkout to succeed runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: ./.github/actions/run-test with: browsers-to-install: ${{ matrix.browser }} chromium From a5daba92ac34a552216508d1eac83aa9f0677cee Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 19 Aug 2025 14:33:51 +0200 Subject: [PATCH 003/329] chore: update browser_patches to 63a0173c4 (#37111) --- browser_patches/firefox/UPSTREAM_CONFIG.sh | 2 +- .../firefox/juggler/JugglerFrameParent.jsm | 2 +- .../firefox/juggler/TargetRegistry.js | 5 +- .../firefox/juggler/components/Juggler.js | 2 +- .../firefox/juggler/content/main.js | 24 +- .../juggler/protocol/BrowserHandler.js | 9 +- .../screencast/nsScreencastService.cpp | 1 + .../firefox/patches/bootstrap.diff | 594 +++--- browser_patches/webkit/UPSTREAM_CONFIG.sh | 2 +- .../embedder/Playwright/mac/AppDelegate.m | 13 +- browser_patches/webkit/patches/bootstrap.diff | 1614 ++++++++--------- 11 files changed, 1028 insertions(+), 1240 deletions(-) diff --git a/browser_patches/firefox/UPSTREAM_CONFIG.sh b/browser_patches/firefox/UPSTREAM_CONFIG.sh index 0513b8920832a..9d6ff4f566b95 100644 --- a/browser_patches/firefox/UPSTREAM_CONFIG.sh +++ b/browser_patches/firefox/UPSTREAM_CONFIG.sh @@ -1,3 +1,3 @@ REMOTE_URL="https://github.com/mozilla-firefox/firefox" BASE_BRANCH="release" -BASE_REVISION="00656c9425c51ee035578ca6ebebe13c755b0375" +BASE_REVISION="361373160356d92cb5cd4d67783a3806c776ee78" diff --git a/browser_patches/firefox/juggler/JugglerFrameParent.jsm b/browser_patches/firefox/juggler/JugglerFrameParent.jsm index 9c3b58898955e..9d41842a3b896 100644 --- a/browser_patches/firefox/juggler/JugglerFrameParent.jsm +++ b/browser_patches/firefox/juggler/JugglerFrameParent.jsm @@ -16,7 +16,7 @@ export class JugglerFrameParent extends JSWindowActorParent { // Actors are registered per the WindowGlobalParent / WindowGlobalChild pair. We are only // interested in those WindowGlobalParent actors that are matching current browsingContext // window global. - // See https://github.com/mozilla/gecko-dev/blob/cd2121e7d83af1b421c95e8c923db70e692dab5f/testing/mochitest/BrowserTestUtils/BrowserTestUtilsParent.sys.mjs#L15 + // See https://github.com/mozilla-firefox/firefox/blob/35e22180b0b61413dd8eccf6c00b1c6fac073eee/testing/mochitest/BrowserTestUtils/BrowserTestUtilsParent.sys.mjs#L15 if (!this.manager?.isCurrentGlobal) return; diff --git a/browser_patches/firefox/juggler/TargetRegistry.js b/browser_patches/firefox/juggler/TargetRegistry.js index 4bdf162ab64f7..5cddac6f64388 100644 --- a/browser_patches/firefox/juggler/TargetRegistry.js +++ b/browser_patches/firefox/juggler/TargetRegistry.js @@ -1023,9 +1023,9 @@ class BrowserContext { if (ignoreHTTPSErrors) { Preferences.set("network.stricttransportsecurity.preloadlist", false); Preferences.set("security.cert_pinning.enforcement_level", 0); - certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(true, this.userContextId); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyDataForUserContext(this.userContextId, true); } else { - certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(false, this.userContextId); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyDataForUserContext(this.userContextId, false); } } @@ -1167,6 +1167,7 @@ class BrowserContext { getCookies() { const result = []; const sameSiteToProtocol = { + [Ci.nsICookie.SAMESITE_UNSET]: 'None', [Ci.nsICookie.SAMESITE_NONE]: 'None', [Ci.nsICookie.SAMESITE_LAX]: 'Lax', [Ci.nsICookie.SAMESITE_STRICT]: 'Strict', diff --git a/browser_patches/firefox/juggler/components/Juggler.js b/browser_patches/firefox/juggler/components/Juggler.js index 6121c4d3928bc..7595958619381 100644 --- a/browser_patches/firefox/juggler/components/Juggler.js +++ b/browser_patches/firefox/juggler/components/Juggler.js @@ -65,7 +65,7 @@ export class Juggler { } // This flow is taken from Remote agent and Marionette. - // See https://github.com/mozilla/gecko-dev/blob/0c1b4921830e6af8bc951da01d7772de2fe60a08/remote/components/RemoteAgent.jsm#L302 + // See https://github.com/mozilla-firefox/firefox/blob/35e22180b0b61413dd8eccf6c00b1c6fac073eee/remote/components/RemoteAgent.sys.mjs#L417 async observe(subject, topic) { switch (topic) { case "profile-after-change": diff --git a/browser_patches/firefox/juggler/content/main.js b/browser_patches/firefox/juggler/content/main.js index 68fb364eadb4e..462566e626562 100644 --- a/browser_patches/firefox/juggler/content/main.js +++ b/browser_patches/firefox/juggler/content/main.js @@ -21,7 +21,7 @@ export function initialize(browsingContext, docShell) { const applySetting = { geolocation: (geolocation) => { if (geolocation) { - docShell.setGeolocationOverride({ + browsingContext.setGeolocationServiceOverride({ coords: { latitude: geolocation.latitude, longitude: geolocation.longitude, @@ -31,14 +31,12 @@ export function initialize(browsingContext, docShell) { heading: NaN, speed: NaN, }, - address: null, timestamp: Date.now() }); } else { - docShell.setGeolocationOverride(null); + browsingContext.setGeolocationServiceOverride(); } }, - bypassCSP: (bypassCSP) => { docShell.bypassCSPEnabled = bypassCSP; }, @@ -101,16 +99,20 @@ export function initialize(browsingContext, docShell) { }, async awaitViewportDimensions({width, height}) { - const win = docShell.domWindow; - if (win.innerWidth === width && win.innerHeight === height) - return; await new Promise(resolve => { - const listener = helper.addEventListener(win, 'resize', () => { - if (win.innerWidth === width && win.innerHeight === height) { - helper.removeListeners([listener]); + const listeners = []; + const check = () => { + helper.removeListeners(listeners); + if (docShell.domWindow.innerWidth === width && docShell.domWindow.innerHeight === height) { resolve(); + return; } - }); + // Note: "domWindow" listeners are often removed upon navigation, as specced. + // To survive viewport changes across navigations, re-install listeners upon commit. + listeners.push(helper.addEventListener(docShell.domWindow, 'resize', check)); + listeners.push(helper.addEventListener(data.frameTree, 'navigationcommitted', check)); + }; + check(); }); }, diff --git a/browser_patches/firefox/juggler/protocol/BrowserHandler.js b/browser_patches/firefox/juggler/protocol/BrowserHandler.js index 035a53bb688c9..970d8f8822867 100644 --- a/browser_patches/firefox/juggler/protocol/BrowserHandler.js +++ b/browser_patches/firefox/juggler/protocol/BrowserHandler.js @@ -5,6 +5,7 @@ "use strict"; const {AddonManager} = ChromeUtils.importESModule("resource://gre/modules/AddonManager.sys.mjs"); +const {XPIProvider} = ChromeUtils.importESModule("resource://gre/modules/addons/XPIProvider.sys.mjs"); const {TargetRegistry} = ChromeUtils.importESModule("chrome://juggler/content/TargetRegistry.js"); const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js'); const {PageHandler} = ChromeUtils.importESModule("chrome://juggler/content/protocol/PageHandler.js"); @@ -147,6 +148,10 @@ export class BrowserHandler { ]); } await this._startCompletePromise; + await Promise.all([ + ...XPIProvider.startupPromises, + ...XPIProvider.enabledAddonsStartupPromises, + ]); this._onclose(); Services.startup.quit(Ci.nsIAppStartup.eForceQuit); } @@ -166,7 +171,9 @@ export class BrowserHandler { ['Browser.clearCache']() { // Clearing only the context cache does not work: https://bugzilla.mozilla.org/show_bug.cgi?id=1819147 Services.cache2.clear(); - ChromeUtils.clearStyleSheetCache(); + ChromeUtils.clearResourceCache({ + types: ["stylesheet"], + }); } ['Browser.setHTTPCredentials']({browserContextId, credentials}) { diff --git a/browser_patches/firefox/juggler/screencast/nsScreencastService.cpp b/browser_patches/firefox/juggler/screencast/nsScreencastService.cpp index 7256c8c8ea416..fa9d1a965c570 100644 --- a/browser_patches/firefox/juggler/screencast/nsScreencastService.cpp +++ b/browser_patches/firefox/juggler/screencast/nsScreencastService.cpp @@ -4,6 +4,7 @@ #include "nsScreencastService.h" +#include "gfxPlatform.h" #include "ScreencastEncoder.h" #include "HeadlessWidget.h" #include "HeadlessWindowCapturer.h" diff --git a/browser_patches/firefox/patches/bootstrap.diff b/browser_patches/firefox/patches/bootstrap.diff index 6f70b0342c8ec..523cda6f45274 100644 --- a/browser_patches/firefox/patches/bootstrap.diff +++ b/browser_patches/firefox/patches/bootstrap.diff @@ -12,13 +12,13 @@ index 1bcd464d3bd6b8465f78c62b074b0d57dbc6a082..f878ac9db2d800542dabcc2f48e8ae47 virtual ~NotificationController(); diff --git a/accessible/interfaces/nsIAccessibleDocument.idl b/accessible/interfaces/nsIAccessibleDocument.idl -index 1886621c373fe1fd5ff54092afc4c64e9ca9a8fd..a0febf72885410b45227171c63e823eca118cb37 100644 +index 6c932c14a03e9cbf70723171dc9a1006bd2c017d..6312131c94c6d7e994a4a49dd64b2b4c5777251f 100644 --- a/accessible/interfaces/nsIAccessibleDocument.idl +++ b/accessible/interfaces/nsIAccessibleDocument.idl -@@ -67,4 +67,9 @@ interface nsIAccessibleDocument : nsISupports - * Return the child document accessible at the given index. +@@ -73,4 +73,9 @@ interface nsIAccessibleDocument : nsISupports + * The BrowsingContext of this document. */ - nsIAccessibleDocument getChildDocumentAt(in unsigned long index); + readonly attribute BrowsingContext browsingContext; + + /** + * Return whether it is updating. @@ -26,11 +26,11 @@ index 1886621c373fe1fd5ff54092afc4c64e9ca9a8fd..a0febf72885410b45227171c63e823ec + readonly attribute boolean isUpdatePendingForJugglerAccessibility; }; diff --git a/accessible/xpcom/xpcAccessibleDocument.cpp b/accessible/xpcom/xpcAccessibleDocument.cpp -index d616e476b2149de5703077563680905e40db0459..7a8a48d5e7303a298a3e2e9fdf64558b3cdbe654 100644 +index 3229ee415b93b9a47d7d40605ed7b509744c94ad..24a0837c508f7bc2709538fc28fdafe83ed524f8 100644 --- a/accessible/xpcom/xpcAccessibleDocument.cpp +++ b/accessible/xpcom/xpcAccessibleDocument.cpp -@@ -131,6 +131,13 @@ xpcAccessibleDocument::GetChildDocumentAt(uint32_t aIndex, - return *aDocument ? NS_OK : NS_ERROR_INVALID_ARG; +@@ -151,6 +151,13 @@ xpcAccessibleDocument::GetBrowsingContext( + return NS_OK; } +NS_IMETHODIMP @@ -44,12 +44,12 @@ index d616e476b2149de5703077563680905e40db0459..7a8a48d5e7303a298a3e2e9fdf64558b // xpcAccessibleDocument diff --git a/accessible/xpcom/xpcAccessibleDocument.h b/accessible/xpcom/xpcAccessibleDocument.h -index 8e9bf2b413585b5a3db9370eee5d57fb6c6716ed..5a3b194b54e3813c89989f13a214c989a409f0f6 100644 +index 9b6effc38fbecb50897a93b49c41d4453b10fb7f..939ff9615bd422eec186d8164e2ac7e643f39beb 100644 --- a/accessible/xpcom/xpcAccessibleDocument.h +++ b/accessible/xpcom/xpcAccessibleDocument.h -@@ -47,6 +47,8 @@ class xpcAccessibleDocument : public xpcAccessibleHyperText, - NS_IMETHOD GetChildDocumentAt(uint32_t aIndex, +@@ -48,6 +48,8 @@ class xpcAccessibleDocument : public xpcAccessibleHyperText, nsIAccessibleDocument** aDocument) final; + NS_IMETHOD GetBrowsingContext(dom::BrowsingContext** aBrowsingContext) final; + NS_IMETHOD GetIsUpdatePendingForJugglerAccessibility(bool* aUpdating) final; + @@ -89,10 +89,10 @@ index 8167d2b81c918e02ce757f7f448f22e07c29d140..3ae798880acfd8aa965ae08051f2f818 DWORD creationFlags = CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT; diff --git a/browser/installer/allowed-dupes.mn b/browser/installer/allowed-dupes.mn -index a097c2c56a665204ff7b5593c7faf836366801cf..235a4e224e08c22870c6913e335f0b6020b3e7da 100644 +index a315ebcb8b60875b4eddbb5d654764603e0e7bec..fa67db1ff5a331dda4eaff4457418c306869d011 100644 --- a/browser/installer/allowed-dupes.mn +++ b/browser/installer/allowed-dupes.mn -@@ -67,6 +67,12 @@ browser/features/webcompat@mozilla.org/shims/empty-shim.txt +@@ -59,6 +59,12 @@ browser/chrome/browser/builtin-addons/webcompat/shims/empty-shim.txt removed-files #endif @@ -106,10 +106,10 @@ index a097c2c56a665204ff7b5593c7faf836366801cf..235a4e224e08c22870c6913e335f0b60 browser/chrome/browser/content/activity-stream/data/content/tippytop/favicons/allegro-pl.ico browser/defaults/settings/main/search-config-icons/96327a73-c433-5eb4-a16d-b090cadfb80b diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in -index 89cff6b562a82bd3a9a5d268abd4373199b31fac..e58dbf0ab0b06e84f6dac64698d11c6268091204 100644 +index 6ebf2f62c47757f16841d04c164d2d86447fcbd0..cae3f3ac6f6ced0b59fe850643bbb90d619750d0 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in -@@ -196,6 +196,9 @@ +@@ -192,6 +192,9 @@ @RESPATH@/chrome/remote.manifest #endif @@ -118,7 +118,7 @@ index 89cff6b562a82bd3a9a5d268abd4373199b31fac..e58dbf0ab0b06e84f6dac64698d11c62 + ; [Extensions] @RESPATH@/components/extensions-toolkit.manifest - @RESPATH@/browser/components/extensions-browser.manifest + diff --git a/devtools/server/socket/websocket-server.js b/devtools/server/socket/websocket-server.js index d49c6fbf1bf83b832795fa674f6b41f223eef812..7ea3540947ff5f61b15f27fbf4b955649f8e9ff9 100644 --- a/devtools/server/socket/websocket-server.js @@ -167,10 +167,10 @@ index d49c6fbf1bf83b832795fa674f6b41f223eef812..7ea3540947ff5f61b15f27fbf4b95564 const transportProvider = { setListener(upgradeListener) { diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp -index 2425779a7767e9350ee2afc4aea111a090c7f909..393eb86bf2d9a8f778bfce560a9fb3bf528ba558 100644 +index 1a33afa481826b1a53c507d5ea596bcb629d8ac4..8f4f13ff1325b104af80e319cf092be9fd2baab0 100644 --- a/docshell/base/BrowsingContext.cpp +++ b/docshell/base/BrowsingContext.cpp -@@ -108,8 +108,15 @@ struct ParamTraits +@@ -109,8 +109,15 @@ struct ParamTraits template <> struct ParamTraits @@ -188,7 +188,7 @@ index 2425779a7767e9350ee2afc4aea111a090c7f909..393eb86bf2d9a8f778bfce560a9fb3bf template <> struct ParamTraits -@@ -2891,6 +2898,32 @@ void BrowsingContext::DidSet(FieldIndex, +@@ -2915,6 +2922,32 @@ void BrowsingContext::DidSet(FieldIndex, PresContextAffectingFieldChanged(); } @@ -222,10 +222,10 @@ index 2425779a7767e9350ee2afc4aea111a090c7f909..393eb86bf2d9a8f778bfce560a9fb3bf nsString&& aOldValue) { MOZ_ASSERT(IsTop()); diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h -index eb183cb5c0751e43ea674f9e52441a5a82f186e0..79c5d8110faa89779dd0c16ba00620e7e65d06f5 100644 +index 0bd3efa16dcae42ff02f42bddce378a7f0c7b621..c8cc1bce7d9901aced6032d0e2e2285cf9eb394b 100644 --- a/docshell/base/BrowsingContext.h +++ b/docshell/base/BrowsingContext.h -@@ -203,10 +203,10 @@ struct EmbedderColorSchemes { +@@ -205,10 +205,10 @@ struct EmbedderColorSchemes { FIELD(GVInaudibleAutoplayRequestStatus, GVAutoplayRequestStatus) \ /* ScreenOrientation-related APIs */ \ FIELD(CurrentOrientationAngle, float) \ @@ -238,7 +238,7 @@ index eb183cb5c0751e43ea674f9e52441a5a82f186e0..79c5d8110faa89779dd0c16ba00620e7 FIELD(EmbedderElementType, Maybe) \ FIELD(MessageManagerGroup, nsString) \ FIELD(MaxTouchPointsOverride, uint8_t) \ -@@ -246,6 +246,9 @@ struct EmbedderColorSchemes { +@@ -245,6 +245,9 @@ struct EmbedderColorSchemes { * embedder element. */ \ FIELD(EmbedderColorSchemes, EmbedderColorSchemes) \ FIELD(DisplayMode, dom::DisplayMode) \ @@ -248,7 +248,7 @@ index eb183cb5c0751e43ea674f9e52441a5a82f186e0..79c5d8110faa89779dd0c16ba00620e7 /* The number of entries added to the session history because of this \ * browsing context. */ \ FIELD(HistoryEntryCount, uint32_t) \ -@@ -950,6 +953,14 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { +@@ -967,6 +970,14 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { return GetForcedColorsOverride(); } @@ -263,7 +263,7 @@ index eb183cb5c0751e43ea674f9e52441a5a82f186e0..79c5d8110faa89779dd0c16ba00620e7 bool IsInBFCache() const; bool AllowJavascript() const { return GetAllowJavascript(); } -@@ -1112,6 +1123,11 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { +@@ -1131,6 +1142,11 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { return IsTop(); } @@ -275,7 +275,7 @@ index eb183cb5c0751e43ea674f9e52441a5a82f186e0..79c5d8110faa89779dd0c16ba00620e7 bool CanSet(FieldIndex, dom::ForcedColorsOverride, ContentParent*) { return IsTop(); -@@ -1130,10 +1146,22 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { +@@ -1149,10 +1165,22 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { void DidSet(FieldIndex, dom::ForcedColorsOverride aOldValue); @@ -299,13 +299,13 @@ index eb183cb5c0751e43ea674f9e52441a5a82f186e0..79c5d8110faa89779dd0c16ba00620e7 bool CanSet(FieldIndex, bool, ContentParent*) { diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp -index bef42c91d6f88922c8c101f3675325d828872aaf..8eb68c441fbef8ecbe5e90c118ccc00813564577 100644 +index 1c5c421d6ec153ab61d3aae29200f8abb02b1a33..68751ec0190f308c20aae6b6c58687dcf0fa225d 100644 --- a/docshell/base/CanonicalBrowsingContext.cpp +++ b/docshell/base/CanonicalBrowsingContext.cpp -@@ -324,6 +324,8 @@ void CanonicalBrowsingContext::ReplacedBy( - txn.SetShouldDelayMediaFromStart(GetShouldDelayMediaFromStart()); +@@ -325,6 +325,8 @@ void CanonicalBrowsingContext::ReplacedBy( txn.SetForceOffline(GetForceOffline()); txn.SetTopInnerSizeForRFP(GetTopInnerSizeForRFP()); + txn.SetIPAddressSpace(GetIPAddressSpace()); + txn.SetPrefersReducedMotionOverride(GetPrefersReducedMotionOverride()); + txn.SetForcedColorsOverride(GetForcedColorsOverride()); @@ -325,10 +325,10 @@ index bef42c91d6f88922c8c101f3675325d828872aaf..8eb68c441fbef8ecbe5e90c118ccc008 } diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp -index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0d41c43cb 100644 +index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4472cce2d 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp -@@ -15,6 +15,12 @@ +@@ -16,6 +16,12 @@ # include // for getpid() #endif @@ -341,7 +341,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 #include "mozilla/ArrayUtils.h" #include "mozilla/Attributes.h" #include "mozilla/AutoRestore.h" -@@ -66,6 +72,7 @@ +@@ -67,6 +73,7 @@ #include "mozilla/dom/DocGroup.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/FragmentDirective.h" @@ -349,7 +349,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 #include "mozilla/dom/HTMLAnchorElement.h" #include "mozilla/dom/HTMLIFrameElement.h" #include "mozilla/dom/Navigation.h" -@@ -94,6 +101,7 @@ +@@ -95,6 +102,7 @@ #include "mozilla/dom/DocumentBinding.h" #include "mozilla/glean/DocshellMetrics.h" #include "mozilla/ipc/ProtocolUtils.h" @@ -357,7 +357,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 #include "mozilla/net/DocumentChannel.h" #include "mozilla/net/DocumentChannelChild.h" #include "mozilla/net/ParentChannelWrapper.h" -@@ -117,6 +125,7 @@ +@@ -118,6 +126,7 @@ #include "nsIDocumentViewer.h" #include "mozilla/dom/Document.h" #include "nsHTMLDocument.h" @@ -365,7 +365,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 #include "nsIDocumentLoaderFactory.h" #include "nsIDOMWindow.h" #include "nsIEditingSession.h" -@@ -211,6 +220,7 @@ +@@ -212,6 +221,7 @@ #include "nsGlobalWindowInner.h" #include "nsGlobalWindowOuter.h" #include "nsJSEnvironment.h" @@ -373,7 +373,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 #include "nsNetCID.h" #include "nsNetUtil.h" #include "nsObjectLoadingContent.h" -@@ -352,6 +362,14 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext, +@@ -353,6 +363,14 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext, mAllowDNSPrefetch(true), mAllowWindowControl(true), mCSSErrorReportingEnabled(false), @@ -388,7 +388,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 mAllowAuth(mItemType == typeContent), mAllowKeywordFixup(false), mDisableMetaRefreshWhenInactive(false), -@@ -3024,6 +3042,232 @@ nsDocShell::GetMessageManager(ContentFrameMessageManager** aMessageManager) { +@@ -3025,6 +3043,232 @@ nsDocShell::GetMessageManager(ContentFrameMessageManager** aMessageManager) { return NS_OK; } @@ -621,7 +621,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 NS_IMETHODIMP nsDocShell::GetIsNavigating(bool* aOut) { *aOut = mIsNavigating; -@@ -4731,7 +4975,7 @@ nsDocShell::GetVisibility(bool* aVisibility) { +@@ -4790,7 +5034,7 @@ nsDocShell::GetVisibility(bool* aVisibility) { } void nsDocShell::ActivenessMaybeChanged() { @@ -630,7 +630,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 if (RefPtr presShell = GetPresShell()) { presShell->ActivenessMaybeChanged(); } -@@ -6658,6 +6902,10 @@ bool nsDocShell::CanSavePresentation(uint32_t aLoadType, +@@ -6723,6 +6967,10 @@ bool nsDocShell::CanSavePresentation(uint32_t aLoadType, return false; // no entry to save into } @@ -641,7 +641,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 MOZ_ASSERT(!mozilla::SessionHistoryInParent(), "mOSHE cannot be non-null with SHIP"); nsCOMPtr viewer = mOSHE->GetDocumentViewer(); -@@ -8399,6 +8647,12 @@ nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) { +@@ -8464,6 +8712,12 @@ nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) { true, // aForceNoOpener getter_AddRefs(newBC)); MOZ_ASSERT(!newBC); @@ -654,7 +654,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 return rv; } -@@ -9572,6 +9826,16 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState, +@@ -9754,6 +10008,16 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState, nsINetworkPredictor::PREDICT_LOAD, attrs, nullptr); nsCOMPtr req; @@ -671,7 +671,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 rv = DoURILoad(aLoadState, aCacheKey, getter_AddRefs(req)); if (NS_SUCCEEDED(rv)) { -@@ -12791,6 +13055,9 @@ class OnLinkClickEvent : public Runnable { +@@ -12970,6 +13234,9 @@ class OnLinkClickEvent : public Runnable { mHandler->OnLinkClickSync(mContent, mLoadState, mNoOpenerImplied, mTriggeringPrincipal); } @@ -681,7 +681,7 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 return NS_OK; } -@@ -12877,6 +13144,8 @@ nsresult nsDocShell::OnLinkClick( +@@ -13083,6 +13350,8 @@ nsresult nsDocShell::OnLinkClick( nsCOMPtr ev = new OnLinkClickEvent( this, aContent, loadState, noOpenerImplied, aTriggeringPrincipal); @@ -691,18 +691,18 @@ index 32c537d6be90247af8d778048c6d27f3800d4b02..b72196b0694828489f8ad27c209f49f0 } diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h -index f22a333733322ad17f097d7edd46af21a687906c..6bcf8ca9f9cd64dc9f5695d00e0a3e6a97978f02 100644 +index 32d1673ea09a2c9f6bb14f6dcd8a9c1c8f9831c4..a301ecd30fcad4d79d370ab316fa0d8e5e509795 100644 --- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h -@@ -15,6 +15,7 @@ - #include "mozilla/UniquePtr.h" +@@ -16,6 +16,7 @@ #include "mozilla/WeakPtr.h" #include "mozilla/dom/BrowsingContext.h" + #include "mozilla/dom/NavigationBinding.h" +#include "mozilla/dom/Element.h" #include "mozilla/dom/WindowProxyHolder.h" #include "nsCOMPtr.h" #include "nsCharsetSource.h" -@@ -77,6 +78,7 @@ class nsCommandManager; +@@ -78,6 +79,7 @@ class nsCommandManager; class nsDocShellEditorData; class nsDOMNavigationTiming; class nsDSURIContentListener; @@ -710,7 +710,7 @@ index f22a333733322ad17f097d7edd46af21a687906c..6bcf8ca9f9cd64dc9f5695d00e0a3e6a class nsGlobalWindowOuter; class FramingChecker; -@@ -403,6 +405,15 @@ class nsDocShell final : public nsDocLoader, +@@ -408,6 +410,15 @@ class nsDocShell final : public nsDocLoader, void SetWillChangeProcess() { mWillChangeProcess = true; } bool WillChangeProcess() { return mWillChangeProcess; } @@ -726,7 +726,7 @@ index f22a333733322ad17f097d7edd46af21a687906c..6bcf8ca9f9cd64dc9f5695d00e0a3e6a // Create a content viewer within this nsDocShell for the given // `WindowGlobalChild` actor. nsresult CreateDocumentViewerForActor( -@@ -1006,6 +1017,8 @@ class nsDocShell final : public nsDocLoader, +@@ -1013,6 +1024,8 @@ class nsDocShell final : public nsDocLoader, bool CSSErrorReportingEnabled() const { return mCSSErrorReportingEnabled; } @@ -735,7 +735,7 @@ index f22a333733322ad17f097d7edd46af21a687906c..6bcf8ca9f9cd64dc9f5695d00e0a3e6a // Handles retrieval of subframe session history for nsDocShell::LoadURI. If a // load is requested in a subframe of the current DocShell, the subframe // loadType may need to reflect the loadType of the parent document, or in -@@ -1285,6 +1298,17 @@ class nsDocShell final : public nsDocLoader, +@@ -1301,6 +1314,17 @@ class nsDocShell final : public nsDocLoader, bool mAllowDNSPrefetch : 1; bool mAllowWindowControl : 1; bool mCSSErrorReportingEnabled : 1; @@ -754,7 +754,7 @@ index f22a333733322ad17f097d7edd46af21a687906c..6bcf8ca9f9cd64dc9f5695d00e0a3e6a bool mAllowKeywordFixup : 1; bool mDisableMetaRefreshWhenInactive : 1; diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl -index 84e821e33e8164829dfee4f05340784e189b90ee..aa690eb747cb73bc6bff40a62546037c2e64c485 100644 +index 159daa1dd936e84f1bf3ce413643382e4d37f592..607224a5270995abccf439992ec577f11178e522 100644 --- a/docshell/base/nsIDocShell.idl +++ b/docshell/base/nsIDocShell.idl @@ -44,6 +44,7 @@ interface nsIURI; @@ -812,10 +812,10 @@ index 84e821e33e8164829dfee4f05340784e189b90ee..aa690eb747cb73bc6bff40a62546037c * This attempts to save any applicable layout history state (like * scroll position) in the nsISHEntry. This is normally done diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp -index fd2d0be5f7755e64fc134515ea138c4ed0d28daf..ae48159ddbfb98d03e538d077a33260c639a74ac 100644 +index a16bef739fcde0f14ba7e53e0acfa3aa2ee1dd3a..c4747478156c973307b7866f84c65520e4bff9d1 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp -@@ -3752,6 +3752,9 @@ void Document::SendToConsole(nsCOMArray& aMessages) { +@@ -3818,6 +3818,9 @@ void Document::SendToConsole(nsCOMArray& aMessages) { } void Document::ApplySettingsFromCSP(bool aSpeculative) { @@ -825,7 +825,7 @@ index fd2d0be5f7755e64fc134515ea138c4ed0d28daf..ae48159ddbfb98d03e538d077a33260c nsresult rv = NS_OK; if (!aSpeculative) { // 1) apply settings from regular CSP -@@ -3809,6 +3812,11 @@ nsresult Document::InitCSP(nsIChannel* aChannel) { +@@ -3875,6 +3878,11 @@ nsresult Document::InitCSP(nsIChannel* aChannel) { MOZ_ASSERT(!mScriptGlobalObject, "CSP must be initialized before mScriptGlobalObject is set!"); @@ -837,7 +837,7 @@ index fd2d0be5f7755e64fc134515ea138c4ed0d28daf..ae48159ddbfb98d03e538d077a33260c // If this is a data document - no need to set CSP. if (mLoadedAsData) { return NS_OK; -@@ -4617,6 +4625,10 @@ bool Document::HasFocus(ErrorResult& rv) const { +@@ -4699,6 +4707,10 @@ bool Document::HasFocus(ErrorResult& rv) const { return false; } @@ -848,7 +848,7 @@ index fd2d0be5f7755e64fc134515ea138c4ed0d28daf..ae48159ddbfb98d03e538d077a33260c if (!fm->IsInActiveWindow(bc)) { return false; } -@@ -19688,6 +19700,35 @@ ColorScheme Document::PreferredColorScheme(IgnoreRFP aIgnoreRFP) const { +@@ -20107,6 +20119,35 @@ ColorScheme Document::PreferredColorScheme(IgnoreRFP aIgnoreRFP) const { return PreferenceSheet::PrefsFor(*this).mColorScheme; } @@ -885,10 +885,10 @@ index fd2d0be5f7755e64fc134515ea138c4ed0d28daf..ae48159ddbfb98d03e538d077a33260c if (!sLoadingForegroundTopLevelContentDocument) { return false; diff --git a/dom/base/Document.h b/dom/base/Document.h -index 622f54e369d324a4cc2800dd4b331bd628339bed..2ef6ed20cf35febeff75b22dfa5bb2fbb4e295fe 100644 +index f9f8089ad484330a9d863fac7559b94aa34d80fd..75b6f3b03a6fe11511cc1e9ebdc6aa16936bbbcb 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h -@@ -4140,6 +4140,8 @@ class Document : public nsINode, +@@ -4224,6 +4224,8 @@ class Document : public nsINode, // color-scheme meta tag. ColorScheme DefaultColorScheme() const; @@ -898,10 +898,10 @@ index 622f54e369d324a4cc2800dd4b331bd628339bed..2ef6ed20cf35febeff75b22dfa5bb2fb static bool AutomaticStorageAccessPermissionCanBeGranted( diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp -index a13cae5b990fb2f750e62f5117ad63aa981d787f..bc0f2d674aadd8eba867f56e873595a8885d1798 100644 +index fac275953573368e91e99bc8a72a885fb1c75521..7c1bcfdba325f8310239fc69921aaa0f14255f33 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp -@@ -344,14 +344,18 @@ void Navigator::GetAppName(nsAString& aAppName) const { +@@ -347,14 +347,18 @@ void Navigator::GetAppName(nsAString& aAppName) const { * for more detail. */ /* static */ @@ -922,7 +922,7 @@ index a13cae5b990fb2f750e62f5117ad63aa981d787f..bc0f2d674aadd8eba867f56e873595a8 // Split values on commas. for (nsDependentSubstring lang : -@@ -403,7 +407,13 @@ void Navigator::GetLanguage(nsAString& aLanguage) { +@@ -406,7 +410,13 @@ void Navigator::GetLanguage(nsAString& aLanguage) { } void Navigator::GetLanguages(nsTArray& aLanguages) { @@ -937,7 +937,7 @@ index a13cae5b990fb2f750e62f5117ad63aa981d787f..bc0f2d674aadd8eba867f56e873595a8 // The returned value is cached by the binding code. The window listens to the // accept languages change and will clear the cache when needed. It has to -@@ -2298,7 +2308,8 @@ bool Navigator::Webdriver() { +@@ -2309,7 +2319,8 @@ bool Navigator::Webdriver() { } #endif @@ -948,10 +948,10 @@ index a13cae5b990fb2f750e62f5117ad63aa981d787f..bc0f2d674aadd8eba867f56e873595a8 AutoplayPolicy Navigator::GetAutoplayPolicy(AutoplayPolicyMediaType aType) { diff --git a/dom/base/Navigator.h b/dom/base/Navigator.h -index 6abf6cef230c97815f17f6b7abf9f1b1de274a6f..46ead1f32e0d710b5b32e61dff72a4f772d5421e 100644 +index 893947475fbb8688becb1c1495385e4048d4927d..dfb50acf689223fdab7ef6f42afbbd53341e1b0b 100644 --- a/dom/base/Navigator.h +++ b/dom/base/Navigator.h -@@ -218,7 +218,7 @@ class Navigator final : public nsISupports, public nsWrapperCache { +@@ -220,7 +220,7 @@ class Navigator final : public nsISupports, public nsWrapperCache { StorageManager* Storage(); @@ -961,10 +961,10 @@ index 6abf6cef230c97815f17f6b7abf9f1b1de274a6f..46ead1f32e0d710b5b32e61dff72a4f7 dom::MediaCapabilities* MediaCapabilities(); dom::MediaSession* MediaSession(); diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp -index f362a444a0f5ed247582646754dffd54d0b4540a..bbd72dab7ff4fbac8c247961e530764cb5c68d11 100644 +index 787d4c4e22715a72197e5d06831bd6d284129c2c..75fc73ce2863f994ce703b0f822acb924bee4f3d 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp -@@ -9151,11 +9151,13 @@ nsresult nsContentUtils::SendMouseEvent( +@@ -9432,11 +9432,13 @@ nsresult nsContentUtils::SendMouseEvent( int32_t aClickCount, int32_t aModifiers, bool aIgnoreRootScrollFrame, float aPressure, unsigned short aInputSourceArg, uint32_t aIdentifier, bool aToWindow, bool* aPreventDefault, bool aIsDOMEventSynthesized, @@ -979,7 +979,7 @@ index f362a444a0f5ed247582646754dffd54d0b4540a..bbd72dab7ff4fbac8c247961e530764c if (aType.EqualsLiteral("mousedown")) { msg = eMouseDown; } else if (aType.EqualsLiteral("mouseup")) { -@@ -9181,6 +9183,12 @@ nsresult nsContentUtils::SendMouseEvent( +@@ -9462,6 +9464,12 @@ nsresult nsContentUtils::SendMouseEvent( msg = eMouseHitTest; } else if (aType.EqualsLiteral("MozMouseExploreByTouch")) { msg = eMouseExploreByTouch; @@ -992,7 +992,7 @@ index f362a444a0f5ed247582646754dffd54d0b4540a..bbd72dab7ff4fbac8c247961e530764c } else { return NS_ERROR_FAILURE; } -@@ -9191,7 +9199,14 @@ nsresult nsContentUtils::SendMouseEvent( +@@ -9472,7 +9480,14 @@ nsresult nsContentUtils::SendMouseEvent( Maybe pointerEvent; Maybe mouseEvent; @@ -1008,7 +1008,7 @@ index f362a444a0f5ed247582646754dffd54d0b4540a..bbd72dab7ff4fbac8c247961e530764c MOZ_ASSERT(!aIsWidgetEventSynthesized, "The event shouldn't be dispatched as a synthesized event"); if (MOZ_UNLIKELY(aIsWidgetEventSynthesized)) { -@@ -9210,8 +9225,11 @@ nsresult nsContentUtils::SendMouseEvent( +@@ -9491,8 +9506,11 @@ nsresult nsContentUtils::SendMouseEvent( contextMenuKey ? WidgetMouseEvent::eContextMenuKey : WidgetMouseEvent::eNormal); } @@ -1020,7 +1020,7 @@ index f362a444a0f5ed247582646754dffd54d0b4540a..bbd72dab7ff4fbac8c247961e530764c mouseOrPointerEvent.pointerId = aIdentifier; mouseOrPointerEvent.mModifiers = GetWidgetModifiers(aModifiers); mouseOrPointerEvent.mButton = aButton; -@@ -9224,6 +9242,8 @@ nsresult nsContentUtils::SendMouseEvent( +@@ -9505,6 +9523,8 @@ nsresult nsContentUtils::SendMouseEvent( mouseOrPointerEvent.mClickCount = aClickCount; mouseOrPointerEvent.mFlags.mIsSynthesizedForTests = aIsDOMEventSynthesized; mouseOrPointerEvent.mExitFrom = exitFrom; @@ -1030,10 +1030,10 @@ index f362a444a0f5ed247582646754dffd54d0b4540a..bbd72dab7ff4fbac8c247961e530764c nsPresContext* presContext = aPresShell->GetPresContext(); if (!presContext) return NS_ERROR_FAILURE; diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h -index 779cd9e544bfb2d135f12c3558c0ca8164b37029..041eba4bcbf40f51fc40ce7609ea81408e6a788d 100644 +index 7d89626774660fb9e0f564808270e3059e4d7b3c..c7f748e6f33cbcd72b0c97d437b2abbcbe4242be 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h -@@ -3015,8 +3015,9 @@ class nsContentUtils { +@@ -3078,8 +3078,9 @@ class nsContentUtils { int32_t aButton, int32_t aButtons, int32_t aClickCount, int32_t aModifiers, bool aIgnoreRootScrollFrame, float aPressure, unsigned short aInputSourceArg, uint32_t aIdentifier, bool aToWindow, @@ -1046,10 +1046,10 @@ index 779cd9e544bfb2d135f12c3558c0ca8164b37029..041eba4bcbf40f51fc40ce7609ea8140 static void FirePageShowEventForFrameLoaderSwap( nsIDocShellTreeItem* aItem, diff --git a/dom/base/nsDOMWindowUtils.cpp b/dom/base/nsDOMWindowUtils.cpp -index 15fe1db8a28ed2592b61aaf2006ddaa656f12389..2642c18bebcdfdd467be557171ba4ee204fcabde 100644 +index e85140d5afebf57cf56bf16ef0c43c425c8d50c7..3062737c3319e35cdc4786c06750a1ac2a99565f 100644 --- a/dom/base/nsDOMWindowUtils.cpp +++ b/dom/base/nsDOMWindowUtils.cpp -@@ -710,6 +710,26 @@ nsDOMWindowUtils::GetPresShellId(uint32_t* aPresShellId) { +@@ -707,6 +707,26 @@ nsDOMWindowUtils::GetPresShellId(uint32_t* aPresShellId) { return NS_ERROR_FAILURE; } @@ -1076,7 +1076,7 @@ index 15fe1db8a28ed2592b61aaf2006ddaa656f12389..2642c18bebcdfdd467be557171ba4ee2 NS_IMETHODIMP nsDOMWindowUtils::SendMouseEvent( const nsAString& aType, float aX, float aY, int32_t aButton, -@@ -724,7 +744,7 @@ nsDOMWindowUtils::SendMouseEvent( +@@ -721,7 +741,7 @@ nsDOMWindowUtils::SendMouseEvent( aOptionalArgCount >= 7 ? aIdentifier : DEFAULT_MOUSE_POINTER_ID, false, aPreventDefault, aOptionalArgCount >= 4 ? aIsDOMEventSynthesized : true, aOptionalArgCount >= 5 ? aIsWidgetEventSynthesized : false, @@ -1085,7 +1085,7 @@ index 15fe1db8a28ed2592b61aaf2006ddaa656f12389..2642c18bebcdfdd467be557171ba4ee2 } NS_IMETHODIMP -@@ -742,7 +762,7 @@ nsDOMWindowUtils::SendMouseEventToWindow( +@@ -739,7 +759,7 @@ nsDOMWindowUtils::SendMouseEventToWindow( aOptionalArgCount >= 7 ? aIdentifier : DEFAULT_MOUSE_POINTER_ID, true, nullptr, aOptionalArgCount >= 4 ? aIsDOMEventSynthesized : true, aOptionalArgCount >= 5 ? aIsWidgetEventSynthesized : false, @@ -1094,7 +1094,7 @@ index 15fe1db8a28ed2592b61aaf2006ddaa656f12389..2642c18bebcdfdd467be557171ba4ee2 } NS_IMETHODIMP -@@ -751,7 +771,7 @@ nsDOMWindowUtils::SendMouseEventCommon( +@@ -748,7 +768,7 @@ nsDOMWindowUtils::SendMouseEventCommon( int32_t aClickCount, int32_t aModifiers, bool aIgnoreRootScrollFrame, float aPressure, unsigned short aInputSourceArg, uint32_t aPointerId, bool aToWindow, bool* aPreventDefault, bool aIsDOMEventSynthesized, @@ -1103,7 +1103,7 @@ index 15fe1db8a28ed2592b61aaf2006ddaa656f12389..2642c18bebcdfdd467be557171ba4ee2 RefPtr presShell = GetPresShell(); if (!presShell) { return NS_ERROR_FAILURE; -@@ -768,7 +788,7 @@ nsDOMWindowUtils::SendMouseEventCommon( +@@ -765,7 +785,7 @@ nsDOMWindowUtils::SendMouseEventCommon( presShell, widget, aType, refPoint, aButton, aButtons, aClickCount, aModifiers, aIgnoreRootScrollFrame, aPressure, aInputSourceArg, aPointerId, aToWindow, aPreventDefault, aIsDOMEventSynthesized, @@ -1126,10 +1126,10 @@ index a8a48d28fc4ef612f8234bc2490a41672f1704f5..f702b0c9a0783ec547a41bbefd68e18a MOZ_CAN_RUN_SCRIPT nsresult SendTouchEventCommon( diff --git a/dom/base/nsFocusManager.cpp b/dom/base/nsFocusManager.cpp -index 555a08b4b3fcd0d0c7986014d2e3516c6e5991c0..74a39e000b0e68042f1f51f6fafbc39ac9b42e51 100644 +index be89a1c984982ea005e9bf7f440f5c8a2e8bab55..f8154882b9af02c5e9d7181e7a15b78824be8df9 100644 --- a/dom/base/nsFocusManager.cpp +++ b/dom/base/nsFocusManager.cpp -@@ -1720,6 +1720,10 @@ Maybe nsFocusManager::SetFocusInner(Element* aNewContent, +@@ -1719,6 +1719,10 @@ Maybe nsFocusManager::SetFocusInner(Element* aNewContent, (GetActiveBrowsingContext() == newRootBrowsingContext); } @@ -1140,7 +1140,7 @@ index 555a08b4b3fcd0d0c7986014d2e3516c6e5991c0..74a39e000b0e68042f1f51f6fafbc39a // Exit fullscreen if a website focuses another window if (StaticPrefs::full_screen_api_exit_on_windowRaise() && !isElementInActiveWindow && (aFlags & FLAG_RAISE)) { -@@ -2306,6 +2310,7 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, +@@ -2305,6 +2309,7 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, bool aIsLeavingDocument, bool aAdjustWidget, bool aRemainActive, Element* aElementToFocus, uint64_t aActionId) { @@ -1148,7 +1148,7 @@ index 555a08b4b3fcd0d0c7986014d2e3516c6e5991c0..74a39e000b0e68042f1f51f6fafbc39a LOGFOCUS(("<>", aActionId)); // hold a reference to the focused content, which may be null -@@ -2352,6 +2357,11 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, +@@ -2351,6 +2356,11 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, return true; } @@ -1160,7 +1160,7 @@ index 555a08b4b3fcd0d0c7986014d2e3516c6e5991c0..74a39e000b0e68042f1f51f6fafbc39a // Keep a ref to presShell since dispatching the DOM event may cause // the document to be destroyed. RefPtr presShell = docShell->GetPresShell(); -@@ -3066,7 +3076,9 @@ void nsFocusManager::RaiseWindow(nsPIDOMWindowOuter* aWindow, +@@ -3061,7 +3071,9 @@ void nsFocusManager::RaiseWindow(nsPIDOMWindowOuter* aWindow, } } @@ -1172,10 +1172,10 @@ index 555a08b4b3fcd0d0c7986014d2e3516c6e5991c0..74a39e000b0e68042f1f51f6fafbc39a // care of lowering the present active window. This happens in // a separate runnable to avoid touching multiple windows in diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp -index 99a5049a3aff2efb208895d60622fd9c8d7f337a..9a9b039a46f1294a8b4af0613fb4f4173ac6a8a0 100644 +index 42ee50b53a666c05c6540a1ddcf3745694aaed2d..b15150f7d2e293c2e338f8cd3ada927c052b30b2 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp -@@ -2512,10 +2512,16 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument, +@@ -2511,10 +2511,16 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument, }(); if (!isContentAboutBlankInChromeDocshell) { @@ -1196,7 +1196,7 @@ index 99a5049a3aff2efb208895d60622fd9c8d7f337a..9a9b039a46f1294a8b4af0613fb4f417 } } -@@ -2635,6 +2641,19 @@ void nsGlobalWindowOuter::DispatchDOMWindowCreated() { +@@ -2634,6 +2640,19 @@ void nsGlobalWindowOuter::DispatchDOMWindowCreated() { } } @@ -1217,7 +1217,7 @@ index 99a5049a3aff2efb208895d60622fd9c8d7f337a..9a9b039a46f1294a8b4af0613fb4f417 void nsGlobalWindowOuter::SetDocShell(nsDocShell* aDocShell) { diff --git a/dom/base/nsGlobalWindowOuter.h b/dom/base/nsGlobalWindowOuter.h -index 0453a18ec10c1434d1692f10b1b4acee754e6b7e..ee7bad691515bb51f6b4345e88944b02ad173180 100644 +index a846e57a8786e77e055d17474c5d910a6844cd5f..02815da6a94e98d452e8b4781a998fc0d5ed1124 100644 --- a/dom/base/nsGlobalWindowOuter.h +++ b/dom/base/nsGlobalWindowOuter.h @@ -320,6 +320,7 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget, @@ -1229,10 +1229,10 @@ index 0453a18ec10c1434d1692f10b1b4acee754e6b7e..ee7bad691515bb51f6b4345e88944b02 // Outer windows only. virtual void EnsureSizeAndPositionUpToDate() override; diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp -index 8e2bbed21c13f23745e2eaad4ded831106ebd930..a17b0c7b9730737f178c05703b08d0f5f6d9ecd1 100644 +index 7d5e58f27b2ae07f8e8ac23c6d12bf90d2fdd8b7..2510572d9bfe8ea2909c48d9c3e86aa02c69e28e 100644 --- a/dom/base/nsINode.cpp +++ b/dom/base/nsINode.cpp -@@ -1449,6 +1449,61 @@ void nsINode::GetBoxQuadsFromWindowOrigin(const BoxQuadOptions& aOptions, +@@ -1498,6 +1498,61 @@ void nsINode::GetBoxQuadsFromWindowOrigin(const BoxQuadOptions& aOptions, mozilla::GetBoxQuadsFromWindowOrigin(this, aOptions, aResult, aRv); } @@ -1295,10 +1295,10 @@ index 8e2bbed21c13f23745e2eaad4ded831106ebd930..a17b0c7b9730737f178c05703b08d0f5 DOMQuad& aQuad, const GeometryNode& aFrom, const ConvertCoordinateOptions& aOptions, CallerType aCallerType, diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h -index eb75f281630f8ca1b901686207c9fc97336d675f..e607f0ae454d52fc2bfe19046b492352a84b4ebd 100644 +index 6a982b3f278bf810dd582b3d5ebc33967323047e..4e991ef317c572c4a79c053d468f14df6c8880b5 100644 --- a/dom/base/nsINode.h +++ b/dom/base/nsINode.h -@@ -2397,6 +2397,10 @@ class nsINode : public mozilla::dom::EventTarget { +@@ -2417,6 +2417,10 @@ class nsINode : public mozilla::dom::EventTarget { nsTArray>& aResult, ErrorResult& aRv); @@ -1338,10 +1338,10 @@ index f32e21752d5013bf143eb45391ab9218debab08e..83763d2354dade2f8d2b7930ba18ae91 static bool DumpEnabled(); diff --git a/dom/chrome-webidl/BrowsingContext.webidl b/dom/chrome-webidl/BrowsingContext.webidl -index 6ec88536141126c97c9b599e3237bb5670d42ce8..41c6cc56738bdaf711adf2cf5b00c7fad5d71ba8 100644 +index 7923fcfb3e70aabddf343ab3ec2f25313bbd227e..cf02ce032d11d85a13bcf91e93e98882eaa9f7c7 100644 --- a/dom/chrome-webidl/BrowsingContext.webidl +++ b/dom/chrome-webidl/BrowsingContext.webidl -@@ -61,6 +61,26 @@ enum ForcedColorsOverride { +@@ -62,6 +62,26 @@ enum ForcedColorsOverride { "active", }; @@ -1368,7 +1368,7 @@ index 6ec88536141126c97c9b599e3237bb5670d42ce8..41c6cc56738bdaf711adf2cf5b00c7fa /** * Allowed overrides of platform/pref default behaviour for touch events. */ -@@ -220,6 +240,12 @@ interface BrowsingContext { +@@ -226,6 +246,12 @@ interface BrowsingContext { // Forced-colors simulation, for DevTools [SetterThrows] attribute ForcedColorsOverride forcedColorsOverride; @@ -1382,10 +1382,10 @@ index 6ec88536141126c97c9b599e3237bb5670d42ce8..41c6cc56738bdaf711adf2cf5b00c7fa * A unique identifier for the browser element that is hosting this * BrowsingContext tree. Every BrowsingContext in the element's tree will diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp -index 0bd219694282347309680fc9b53b945e1fd0ad92..c5c2e2d32a380ec72379b05a8b84f187431f7309 100644 +index 2a29279a6d74770a2ec5cee80891bff9eadf1d13..bce59065615a33cf020aae4b20750ce8b1be66ce 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp -@@ -701,6 +701,12 @@ already_AddRefed FetchRequest(nsIGlobalObject* aGlobal, +@@ -702,6 +702,12 @@ already_AddRefed FetchRequest(nsIGlobalObject* aGlobal, ipcArgs.hasCSPEventListener() = false; ipcArgs.isWorkerRequest() = false; @@ -1399,7 +1399,7 @@ index 0bd219694282347309680fc9b53b945e1fd0ad92..c5c2e2d32a380ec72379b05a8b84f187 mozilla::glean::networking::fetch_keepalive_request_count.Get("main"_ns) diff --git a/dom/fetch/FetchService.cpp b/dom/fetch/FetchService.cpp -index b5e60bbd27fbb2f033d233e9ae2ebc728f442512..0adc568ece34d2c0f35eeacd81e2db9125b7c327 100644 +index fcdddf2f772af305c68cc169ba891386a1dba982..d8b19d35719fff3f9c55c199718f4dc1d15cdfe9 100644 --- a/dom/fetch/FetchService.cpp +++ b/dom/fetch/FetchService.cpp @@ -268,6 +268,14 @@ RefPtr FetchService::FetchInstance::Fetch() { @@ -1417,110 +1417,11 @@ index b5e60bbd27fbb2f033d233e9ae2ebc728f442512..0adc568ece34d2c0f35eeacd81e2db91 if (mArgsType == FetchArgsType::WorkerFetch) { auto& args = mArgs.as(); mFetchDriver->SetWorkerScript(args.mWorkerScript); -diff --git a/dom/geolocation/Geolocation.cpp b/dom/geolocation/Geolocation.cpp -index 7c653fe131dc34d35ffdc030950071adb31a9fc9..b23442a42ba8ee270e8e38930e59ae15c2a29039 100644 ---- a/dom/geolocation/Geolocation.cpp -+++ b/dom/geolocation/Geolocation.cpp -@@ -28,6 +28,7 @@ - #include "nsComponentManagerUtils.h" - #include "nsContentPermissionHelper.h" - #include "nsContentUtils.h" -+#include "nsDocShell.h" - #include "nsGlobalWindowInner.h" - #include "mozilla/dom/Document.h" - #include "nsINamed.h" -@@ -428,10 +429,8 @@ nsGeolocationRequest::Allow(JS::Handle aChoices) { - return NS_OK; - } - -- RefPtr gs = -- nsGeolocationService::GetGeolocationService(); -- -- bool canUseCache = false; -+ nsGeolocationService* gs = mLocator->GetGeolocationService(); -+ bool canUseCache = gs != nsGeolocationService::sService.get(); - CachedPositionAndAccuracy lastPosition = gs->GetCachedPosition(); - if (lastPosition.position) { - EpochTimeStamp cachedPositionTime_ms; -@@ -639,8 +638,7 @@ void nsGeolocationRequest::Shutdown() { - // If there are no other high accuracy requests, the geolocation service will - // notify the provider to switch to the default accuracy. - if (mOptions && mOptions->mEnableHighAccuracy) { -- RefPtr gs = -- nsGeolocationService::GetGeolocationService(); -+ nsGeolocationService* gs = mLocator ? mLocator->GetGeolocationService() : nullptr; - if (gs) { - gs->UpdateAccuracy(); - } -@@ -957,8 +955,14 @@ void nsGeolocationService::StopDevice() { - StaticRefPtr nsGeolocationService::sService; - - already_AddRefed --nsGeolocationService::GetGeolocationService() { -+nsGeolocationService::GetGeolocationService(nsDocShell* docShell) { - RefPtr result; -+ if (docShell) { -+ result = docShell->GetGeolocationServiceOverride(); -+ if (result) -+ return result.forget(); -+ } -+ - if (nsGeolocationService::sService) { - result = nsGeolocationService::sService; - -@@ -1050,7 +1054,9 @@ nsresult Geolocation::Init(nsPIDOMWindowInner* aContentDom) { - // If no aContentDom was passed into us, we are being used - // by chrome/c++ and have no mOwner, no mPrincipal, and no need - // to prompt. -- mService = nsGeolocationService::GetGeolocationService(); -+ nsCOMPtr doc = aContentDom ? aContentDom->GetDoc() : nullptr; -+ mService = nsGeolocationService::GetGeolocationService( -+ doc ? static_cast(doc->GetDocShell()) : nullptr); - if (mService) { - mService->AddLocator(this); - } -diff --git a/dom/geolocation/Geolocation.h b/dom/geolocation/Geolocation.h -index 992de29b5d2d09c19e55ebb2502215ec9d05a171..cdc20567b693283b0fd5a5923f7ea54210bd1712 100644 ---- a/dom/geolocation/Geolocation.h -+++ b/dom/geolocation/Geolocation.h -@@ -31,6 +31,7 @@ - - #include "nsIGeolocationProvider.h" - #include "mozilla/Attributes.h" -+#include "nsDocShell.h" - - class nsGeolocationService; - class nsGeolocationRequest; -@@ -51,13 +52,14 @@ struct CachedPositionAndAccuracy { - bool isHighAccuracy; - }; - -+ - /** - * Singleton that manages the geolocation provider - */ - class nsGeolocationService final : public nsIGeolocationUpdate, - public nsIObserver { - public: -- static already_AddRefed GetGeolocationService(); -+ static already_AddRefed GetGeolocationService(nsDocShell* docShell = nullptr); - static mozilla::StaticRefPtr sService; - - NS_DECL_THREADSAFE_ISUPPORTS -@@ -189,6 +191,8 @@ class Geolocation final : public nsIGeolocationUpdate, public nsWrapperCache { - BrowsingContext* aBrowsingContext, - geolocation::ParentRequestResolver&& aResolver); - -+ nsGeolocationService* GetGeolocationService() { return mService; }; -+ - private: - ~Geolocation(); - diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp -index c3e7de8f41e06e11155620b75c4d8a830d908b37..39eb9d31258693dce3a26c3227f28d92bccdb019 100644 +index 9382c73dc527f3fb676e8ea3457a30eee2d9415f..f1785121db1de071ce5aa1a0ad38d3d603c486f7 100644 --- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp -@@ -64,6 +64,7 @@ +@@ -63,6 +63,7 @@ #include "mozilla/dom/Document.h" #include "mozilla/dom/HTMLDataListElement.h" #include "mozilla/dom/HTMLOptionElement.h" @@ -1528,7 +1429,7 @@ index c3e7de8f41e06e11155620b75c4d8a830d908b37..39eb9d31258693dce3a26c3227f28d92 #include "nsIFrame.h" #include "nsRangeFrame.h" #include "nsError.h" -@@ -790,6 +791,13 @@ nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) { +@@ -816,6 +817,13 @@ nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) { return NS_ERROR_FAILURE; } @@ -1543,7 +1444,7 @@ index c3e7de8f41e06e11155620b75c4d8a830d908b37..39eb9d31258693dce3a26c3227f28d92 return NS_OK; } diff --git a/dom/interfaces/base/nsIDOMWindowUtils.idl b/dom/interfaces/base/nsIDOMWindowUtils.idl -index 5e417145c4f21d8f2aa65088611477b681c9c327..bc84c509659c7556077e69c652e5b19639eb88bb 100644 +index 4cdfcaafa779ed402d02411f69c02ab0eb5b4e09..e69c837f7ec340a11e8ae9485cd5b714a3ea9a88 100644 --- a/dom/interfaces/base/nsIDOMWindowUtils.idl +++ b/dom/interfaces/base/nsIDOMWindowUtils.idl @@ -374,6 +374,26 @@ interface nsIDOMWindowUtils : nsISupports { @@ -1574,10 +1475,10 @@ index 5e417145c4f21d8f2aa65088611477b681c9c327..bc84c509659c7556077e69c652e5b196 * touchstart, touchend, touchmove, and touchcancel * diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp -index 93f20a36acef74947d5377df5ff916546218d8b8..22b706b985d22a8c0c278a12ab4944e26ded51a4 100644 +index 310e22a130612c055f7642d813b2bc06538c8797..8528cd75252c6cf0bebe42ffbf83c0220551ba28 100644 --- a/dom/ipc/BrowserChild.cpp +++ b/dom/ipc/BrowserChild.cpp -@@ -1676,6 +1676,21 @@ void BrowserChild::HandleRealMouseButtonEvent(const WidgetMouseEvent& aEvent, +@@ -1760,6 +1760,21 @@ void BrowserChild::HandleRealMouseButtonEvent(const WidgetMouseEvent& aEvent, if (postLayerization) { postLayerization->Register(); } @@ -1600,24 +1501,24 @@ index 93f20a36acef74947d5377df5ff916546218d8b8..22b706b985d22a8c0c278a12ab4944e2 mozilla::ipc::IPCResult BrowserChild::RecvNormalPriorityRealMouseButtonEvent( diff --git a/dom/ipc/CoalescedMouseData.cpp b/dom/ipc/CoalescedMouseData.cpp -index 5aa445d2e0a6169e57c44569974d557b3baf7064..671f71979b407f0ca17c66f13805e851ba30479e 100644 +index 31d56e5d4ea1f21b4b827c763d6e4b34432eeb7d..8537ae198faeec5fd5cced71995bbce9dccf90c4 100644 --- a/dom/ipc/CoalescedMouseData.cpp +++ b/dom/ipc/CoalescedMouseData.cpp -@@ -67,6 +67,9 @@ bool CoalescedMouseData::CanCoalesce(const WidgetMouseEvent& aEvent, - mCoalescedInputEvent->pointerId == aEvent.pointerId && - mCoalescedInputEvent->mButton == aEvent.mButton && - mCoalescedInputEvent->mButtons == aEvent.mButtons && mGuid == aGuid && -+ // `mJugglerEventId` is 0 for non-juggler events and a unique number for -+ // juggler-emitted events. -+ mCoalescedInputEvent->mJugglerEventId == aEvent.mJugglerEventId && - mInputBlockId == aInputBlockId); - } - +@@ -71,6 +71,9 @@ bool CoalescedMouseData::CanCoalesce(const WidgetMouseEvent& aEvent, + mCoalescedInputEvent->pointerId != aEvent.pointerId || + mCoalescedInputEvent->mButton != aEvent.mButton || + mCoalescedInputEvent->mButtons != aEvent.mButtons || mGuid != aGuid || ++ // `mJugglerEventId` is 0 for non-juggler events and a unique number for ++ // juggler-emitted events. ++ mCoalescedInputEvent->mJugglerEventId != aEvent.mJugglerEventId || + mInputBlockId != aInputBlockId) { + return false; + } diff --git a/dom/media/systemservices/video_engine/desktop_capture_impl.cc b/dom/media/systemservices/video_engine/desktop_capture_impl.cc -index c43a1b3b245101c974742c5e50f54857e538bbfb..c07a568da3342cbf2af07471fa6959cb242b9a4e 100644 +index db777994b583efc2a3533e7d4534fff894c733bb..eb317c19b2422bd983d5e132c9ee4dac31687428 100644 --- a/dom/media/systemservices/video_engine/desktop_capture_impl.cc +++ b/dom/media/systemservices/video_engine/desktop_capture_impl.cc -@@ -52,9 +52,10 @@ namespace webrtc { +@@ -53,9 +53,10 @@ namespace webrtc { DesktopCaptureImpl* DesktopCaptureImpl::Create(const int32_t aModuleId, const char* aUniqueId, @@ -1630,7 +1531,7 @@ index c43a1b3b245101c974742c5e50f54857e538bbfb..c07a568da3342cbf2af07471fa6959cb } static DesktopCaptureOptions CreateDesktopCaptureOptions() { -@@ -155,8 +156,10 @@ static std::unique_ptr CreateTabCapturer( +@@ -156,8 +157,10 @@ static std::unique_ptr CreateTabCapturer( static std::unique_ptr CreateDesktopCapturerAndThread( CaptureDeviceType aDeviceType, DesktopCapturer::SourceId aSourceId, @@ -1642,7 +1543,7 @@ index c43a1b3b245101c974742c5e50f54857e538bbfb..c07a568da3342cbf2af07471fa6959cb auto ensureThread = [&]() { if (*aOutThread) { return *aOutThread; -@@ -253,7 +256,8 @@ static std::unique_ptr CreateDesktopCapturerAndThread( +@@ -254,7 +257,8 @@ static std::unique_ptr CreateDesktopCapturerAndThread( } DesktopCaptureImpl::DesktopCaptureImpl(const int32_t aId, const char* aUniqueId, @@ -1652,7 +1553,7 @@ index c43a1b3b245101c974742c5e50f54857e538bbfb..c07a568da3342cbf2af07471fa6959cb : mModuleId(aId), mTrackingId(mozilla::TrackingId(CaptureEngineToTrackingSourceStr([&] { switch (aType) { -@@ -270,6 +274,7 @@ DesktopCaptureImpl::DesktopCaptureImpl(const int32_t aId, const char* aUniqueId, +@@ -271,6 +275,7 @@ DesktopCaptureImpl::DesktopCaptureImpl(const int32_t aId, const char* aUniqueId, aId)), mDeviceUniqueId(aUniqueId), mDeviceType(aType), @@ -1660,7 +1561,7 @@ index c43a1b3b245101c974742c5e50f54857e538bbfb..c07a568da3342cbf2af07471fa6959cb mControlThread(mozilla::GetCurrentSerialEventTarget()), mNextFrameMinimumTime(Timestamp::Zero()), mCallbacks("DesktopCaptureImpl::mCallbacks") {} -@@ -294,6 +299,19 @@ void DesktopCaptureImpl::DeRegisterCaptureDataCallback( +@@ -295,6 +300,19 @@ void DesktopCaptureImpl::DeRegisterCaptureDataCallback( } } @@ -1680,16 +1581,16 @@ index c43a1b3b245101c974742c5e50f54857e538bbfb..c07a568da3342cbf2af07471fa6959cb int32_t DesktopCaptureImpl::StopCaptureIfAllClientsClose() { { auto callbacks = mCallbacks.Lock(); -@@ -333,7 +351,7 @@ int32_t DesktopCaptureImpl::StartCapture( - - DesktopCapturer::SourceId sourceId = std::stoi(mDeviceUniqueId); +@@ -350,7 +368,7 @@ int32_t DesktopCaptureImpl::StartCapture( + return -1; + } std::unique_ptr capturer = CreateDesktopCapturerAndThread( - mDeviceType, sourceId, getter_AddRefs(mCaptureThread)); + mDeviceType, sourceId, getter_AddRefs(mCaptureThread), capture_cursor_); MOZ_ASSERT(!capturer == !mCaptureThread); if (!capturer) { -@@ -441,6 +459,15 @@ void DesktopCaptureImpl::OnCaptureResult(DesktopCapturer::Result aResult, +@@ -458,6 +476,15 @@ void DesktopCaptureImpl::OnCaptureResult(DesktopCapturer::Result aResult, frameInfo.height = aFrame->size().height(); frameInfo.videoType = VideoType::kARGB; @@ -1803,7 +1704,7 @@ index a76b7de569db1cb42728a5514fb80e5c46e0344e..3d61ad8d3aa4a7eaf96957dcd680e1e1 const nsCOMPtr mControlThread; // Set in StartCapture. diff --git a/dom/script/ScriptSettings.cpp b/dom/script/ScriptSettings.cpp -index 3b39538e51840cd9b1685b2efd2ff2e9ec83608a..c7bf4f2d53b58bbacb22b3ebebf6f3fc9b5e445f 100644 +index d5964b27e9c99af76fe3f4076322592a9370c2d0..da217e2c1fbd9ca02e50ff74dde028189a216a6d 100644 --- a/dom/script/ScriptSettings.cpp +++ b/dom/script/ScriptSettings.cpp @@ -150,6 +150,30 @@ ScriptSettingsStackEntry::~ScriptSettingsStackEntry() { @@ -1847,10 +1748,10 @@ index 3b39538e51840cd9b1685b2efd2ff2e9ec83608a..c7bf4f2d53b58bbacb22b3ebebf6f3fc return aGlobalOrNull; diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp -index 5ec21c1c7f975a372399748e8bab2b21ce347f20..ed16831e549afa3d6623398d35eb61e26ab5f2b0 100644 +index 40844c900fc64415e39f4d2dead4c490d5ec301b..f63d0fd6a6df7ea932ca4d4bb70dfbd7562b2fa0 100644 --- a/dom/security/nsCSPUtils.cpp +++ b/dom/security/nsCSPUtils.cpp -@@ -23,6 +23,7 @@ +@@ -24,6 +24,7 @@ #include "nsSandboxFlags.h" #include "nsServiceManagerUtils.h" #include "nsWhitespaceTokenizer.h" @@ -1894,10 +1795,10 @@ index aee376e971ae01ac1e512c3920b115bfaf06afa8..1701311534bf77e6cd9bafc0e3a28361 * returned quads are further translated relative to the window * origin -- which is not the layout origin. Further translation diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp -index a23637c4a887b66a1b4c709a648762b84151bf01..d8da9063261482f1da3257e3f95a8a49d94325f8 100644 +index 76035deec8f20933800284211a07ff23e124c1b9..d46c22728c82c15ed6b5c9d3c57f8b8e2229212b 100644 --- a/dom/workers/RuntimeService.cpp +++ b/dom/workers/RuntimeService.cpp -@@ -1026,7 +1026,7 @@ void PrefLanguagesChanged(const char* /* aPrefName */, void* /* aClosure */) { +@@ -1079,7 +1079,7 @@ void PrefLanguagesChanged(const char* /* aPrefName */, void* /* aClosure */) { AssertIsOnMainThread(); nsTArray languages; @@ -1906,7 +1807,7 @@ index a23637c4a887b66a1b4c709a648762b84151bf01..d8da9063261482f1da3257e3f95a8a49 RuntimeService* runtime = RuntimeService::GetService(); if (runtime) { -@@ -1214,8 +1214,7 @@ bool RuntimeService::RegisterWorker(WorkerPrivate& aWorkerPrivate) { +@@ -1269,8 +1269,7 @@ bool RuntimeService::RegisterWorker(WorkerPrivate& aWorkerPrivate) { } // The navigator overridden properties should have already been read. @@ -1916,7 +1817,7 @@ index a23637c4a887b66a1b4c709a648762b84151bf01..d8da9063261482f1da3257e3f95a8a49 mNavigatorPropertiesLoaded = true; } -@@ -1836,6 +1835,13 @@ void RuntimeService::PropagateStorageAccessPermissionGranted( +@@ -1898,6 +1897,13 @@ void RuntimeService::PropagateStorageAccessPermissionGranted( } } @@ -1930,7 +1831,7 @@ index a23637c4a887b66a1b4c709a648762b84151bf01..d8da9063261482f1da3257e3f95a8a49 template void RuntimeService::BroadcastAllWorkers(const Func& aFunc) { AssertIsOnMainThread(); -@@ -2361,6 +2367,14 @@ void PropagateStorageAccessPermissionGrantedToWorkers( +@@ -2438,6 +2444,14 @@ void PropagateStorageAccessPermissionGrantedToWorkers( } } @@ -1946,7 +1847,7 @@ index a23637c4a887b66a1b4c709a648762b84151bf01..d8da9063261482f1da3257e3f95a8a49 MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(aCx); diff --git a/dom/workers/RuntimeService.h b/dom/workers/RuntimeService.h -index 534bbe9ec4f0261189eb3322c1229c1eb5d8802e..6aa99b64fdbbff3704602e944b129879fbdf8c15 100644 +index b17efeafac1df3653ed9659e506c3d582044e2d0..3dc739c3b72980daf8a99190123920d0096da00c 100644 --- a/dom/workers/RuntimeService.h +++ b/dom/workers/RuntimeService.h @@ -112,6 +112,8 @@ class RuntimeService final : public nsIObserver { @@ -1959,7 +1860,7 @@ index 534bbe9ec4f0261189eb3322c1229c1eb5d8802e..6aa99b64fdbbff3704602e944b129879 return mNavigatorProperties; } diff --git a/dom/workers/WorkerCommon.h b/dom/workers/WorkerCommon.h -index 58894a8361c7ef1dddd481ca5877a209a8b8ff5c..c481d40d79b6397b7f1d571bd9f6ae5c0a946217 100644 +index c928f8292a29ce7b53dd45aec4abc107263b1c8f..e11fdf6159623b2232e51160efbcbb6df5b6ed89 100644 --- a/dom/workers/WorkerCommon.h +++ b/dom/workers/WorkerCommon.h @@ -47,6 +47,8 @@ void ResumeWorkersForWindow(const nsPIDOMWindowInner& aWindow); @@ -1972,10 +1873,10 @@ index 58894a8361c7ef1dddd481ca5877a209a8b8ff5c..c481d40d79b6397b7f1d571bd9f6ae5c bool IsWorkerGlobal(JSObject* global); diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp -index 5d918a82708a26125f7322e43f6436d7eafaa812..b230baead02e05d87a211c276066ec7939ea8251 100644 +index e19f398e2672a6ffe07f53c820dd53c3210dd8b1..8dba789845e910986d3240f317a8749a37cd50b6 100644 --- a/dom/workers/WorkerPrivate.cpp +++ b/dom/workers/WorkerPrivate.cpp -@@ -736,6 +736,18 @@ class UpdateContextOptionsRunnable final : public WorkerControlRunnable { +@@ -755,6 +755,18 @@ class UpdateContextOptionsRunnable final : public WorkerControlRunnable { } }; @@ -1994,7 +1895,7 @@ index 5d918a82708a26125f7322e43f6436d7eafaa812..b230baead02e05d87a211c276066ec79 class UpdateLanguagesRunnable final : public WorkerThreadRunnable { nsTArray mLanguages; -@@ -2159,6 +2171,16 @@ void WorkerPrivate::UpdateContextOptions( +@@ -2401,6 +2413,16 @@ void WorkerPrivate::UpdateContextOptions( } } @@ -2011,7 +1912,7 @@ index 5d918a82708a26125f7322e43f6436d7eafaa812..b230baead02e05d87a211c276066ec79 void WorkerPrivate::UpdateLanguages(const nsTArray& aLanguages) { AssertIsOnParentThread(); -@@ -5946,6 +5968,15 @@ void WorkerPrivate::UpdateContextOptionsInternal( +@@ -6374,6 +6396,15 @@ void WorkerPrivate::UpdateContextOptionsInternal( } } @@ -2028,10 +1929,10 @@ index 5d918a82708a26125f7322e43f6436d7eafaa812..b230baead02e05d87a211c276066ec79 const nsTArray& aLanguages) { WorkerGlobalScope* globalScope = GlobalScope(); diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h -index 7bccdc8c0c2cd53f7aa7a6d9a74435344dd27980..86bba2128a7c0f4e5efa4bfbc939937aec146695 100644 +index d887a9ca510730aef61d71c6f8b2e58c3762e5df..7ffe86c8ec792558e3d83e5b7ca8fccd39147385 100644 --- a/dom/workers/WorkerPrivate.h +++ b/dom/workers/WorkerPrivate.h -@@ -443,6 +443,8 @@ class WorkerPrivate final +@@ -453,6 +453,8 @@ class WorkerPrivate final void UpdateContextOptionsInternal(JSContext* aCx, const JS::ContextOptions& aContextOptions); @@ -2040,7 +1941,7 @@ index 7bccdc8c0c2cd53f7aa7a6d9a74435344dd27980..86bba2128a7c0f4e5efa4bfbc939937a void UpdateLanguagesInternal(const nsTArray& aLanguages); void UpdateJSWorkerMemoryParameterInternal(JSContext* aCx, JSGCParamKey key, -@@ -1091,6 +1093,8 @@ class WorkerPrivate final +@@ -1099,6 +1101,8 @@ class WorkerPrivate final void UpdateContextOptions(const JS::ContextOptions& aContextOptions); @@ -2102,10 +2003,10 @@ index 523e84c8c93f4221701f90f2e8ee146ec8e1adbd..98d5b1176e5378431b859a2dbd4d4e77 inline ClippedTime TimeClip(double time); diff --git a/js/src/debugger/Object.cpp b/js/src/debugger/Object.cpp -index b7ea4b6f66d14db0324397cdc1b0ed8c5ea167e2..1c59e328079e7e43b65f7cb7bc31636a48a93263 100644 +index 2d177b5aade6d158e9ae62d317d5155b72cdcf84..28af5d41eb06d4b33cd5f86484dc29c923308835 100644 --- a/js/src/debugger/Object.cpp +++ b/js/src/debugger/Object.cpp -@@ -2484,7 +2484,11 @@ Maybe DebuggerObject::call(JSContext* cx, +@@ -2516,7 +2516,11 @@ Maybe DebuggerObject::call(JSContext* cx, invokeArgs[i].set(args2[i]); } @@ -2273,10 +2174,10 @@ index 4bfd336ddcbee8004ac538ca7b7d8216d04a61c3..cd22351c4aeacea8afc9828972222aca // No boxes to return return; diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp -index e8fb3a8304b27814e6e84355f24410c820667f9d..211c86fe55b8b650e40275a427b30a1ee8a9a3d7 100644 +index 1922382f924e74dde81501540d38699dcf3d9e57..c05b9ed071a9a4c99e2afb28eef0dba376777976 100644 --- a/layout/base/PresShell.cpp +++ b/layout/base/PresShell.cpp -@@ -11512,7 +11512,9 @@ bool PresShell::ComputeActiveness() const { +@@ -11881,7 +11881,9 @@ bool PresShell::ComputeActiveness() const { if (!browserChild->IsVisible()) { MOZ_LOG(gLog, LogLevel::Debug, (" > BrowserChild %p is not visible", browserChild)); @@ -2288,7 +2189,7 @@ index e8fb3a8304b27814e6e84355f24410c820667f9d..211c86fe55b8b650e40275a427b30a1e // If the browser is visible but just due to be preserving layers diff --git a/layout/base/nsLayoutUtils.cpp b/layout/base/nsLayoutUtils.cpp -index 315d532eab56dab13b6c8bc2380a5cda2a17ffc1..7c4552137149e8c7fc9ac08a09bd4242952b53b6 100644 +index bc5b7ab177070178d1a3c49b563df0f04860e7d3..393f694c766bec5e5443d91922a8bc3524ade676 100644 --- a/layout/base/nsLayoutUtils.cpp +++ b/layout/base/nsLayoutUtils.cpp @@ -708,6 +708,10 @@ bool nsLayoutUtils::AllowZoomingForDocument( @@ -2302,7 +2203,7 @@ index 315d532eab56dab13b6c8bc2380a5cda2a17ffc1..7c4552137149e8c7fc9ac08a09bd4242 // True if we allow zooming for all documents on this platform, or if we are // in RDM. BrowsingContext* bc = aDocument->GetBrowsingContext(); -@@ -9770,6 +9774,9 @@ void nsLayoutUtils::ComputeSystemFont(nsFont* aSystemFont, +@@ -9773,6 +9777,9 @@ void nsLayoutUtils::ComputeSystemFont(nsFont* aSystemFont, /* static */ bool nsLayoutUtils::ShouldHandleMetaViewport(const Document* aDocument) { @@ -2313,10 +2214,10 @@ index 315d532eab56dab13b6c8bc2380a5cda2a17ffc1..7c4552137149e8c7fc9ac08a09bd4242 return StaticPrefs::dom_meta_viewport_enabled() || (bc && bc->InRDMPane()); } diff --git a/layout/style/GeckoBindings.h b/layout/style/GeckoBindings.h -index c7cf59c2661c7e203384c9b82789879f756b44b7..21e32dab4e60112c073bdd5070a308da2b4e0373 100644 +index 4cdceba041ddc5af98982d27b6e622a0dd5d0e34..50f644a91923e529ff06a164256f5704e5c4d8ae 100644 --- a/layout/style/GeckoBindings.h +++ b/layout/style/GeckoBindings.h -@@ -596,6 +596,7 @@ float Gecko_MediaFeatures_GetResolution(const mozilla::dom::Document*); +@@ -594,6 +594,7 @@ float Gecko_MediaFeatures_GetResolution(const mozilla::dom::Document*); bool Gecko_MediaFeatures_PrefersReducedMotion(const mozilla::dom::Document*); bool Gecko_MediaFeatures_PrefersReducedTransparency( const mozilla::dom::Document*); @@ -2325,10 +2226,10 @@ index c7cf59c2661c7e203384c9b82789879f756b44b7..21e32dab4e60112c073bdd5070a308da const mozilla::dom::Document*); mozilla::StylePrefersColorScheme Gecko_MediaFeatures_PrefersColorScheme( diff --git a/layout/style/nsMediaFeatures.cpp b/layout/style/nsMediaFeatures.cpp -index ca382a3cfba8ce5839890d6e4cb3cf9789287e3b..5800fc23dc77ee5764beddd6fa48a7fd701d2939 100644 +index d843c5952e273c64703af6e9f2528798e3ada307..50dcfd6839777dd0ed9f806dfac3e1a4b0102efa 100644 --- a/layout/style/nsMediaFeatures.cpp +++ b/layout/style/nsMediaFeatures.cpp -@@ -264,11 +264,7 @@ bool Gecko_MediaFeatures_MatchesPlatform(StylePlatform aPlatform) { +@@ -268,11 +268,7 @@ bool Gecko_MediaFeatures_MatchesPlatform(StylePlatform aPlatform) { } bool Gecko_MediaFeatures_PrefersReducedMotion(const Document* aDocument) { @@ -2341,7 +2242,7 @@ index ca382a3cfba8ce5839890d6e4cb3cf9789287e3b..5800fc23dc77ee5764beddd6fa48a7fd } bool Gecko_MediaFeatures_PrefersReducedTransparency(const Document* aDocument) { -@@ -293,6 +289,20 @@ StylePrefersColorScheme Gecko_MediaFeatures_PrefersColorScheme( +@@ -297,6 +293,20 @@ StylePrefersColorScheme Gecko_MediaFeatures_PrefersColorScheme( // as a signal. StylePrefersContrast Gecko_MediaFeatures_PrefersContrast( const Document* aDocument) { @@ -2363,10 +2264,10 @@ index ca382a3cfba8ce5839890d6e4cb3cf9789287e3b..5800fc23dc77ee5764beddd6fa48a7fd return StylePrefersContrast::NoPreference; } diff --git a/netwerk/base/LoadInfo.cpp b/netwerk/base/LoadInfo.cpp -index 1ec2c64193206d31702e22e5c4783f084b1cff31..fb463eb12ee39cd1e448369f3b47fbcfbb2473b9 100644 +index c9d6cace8de545779ae9c4630973c5a920cd52c6..032a42b732179a951181f390199c2520fecd742b 100644 --- a/netwerk/base/LoadInfo.cpp +++ b/netwerk/base/LoadInfo.cpp -@@ -696,7 +696,8 @@ LoadInfo::LoadInfo(const LoadInfo& rhs) +@@ -792,7 +792,8 @@ LoadInfo::LoadInfo(const LoadInfo& rhs) rhs.mHasInjectedCookieForCookieBannerHandling), mSchemelessInput(rhs.mSchemelessInput), mHttpsUpgradeTelemetry(rhs.mHttpsUpgradeTelemetry), @@ -2376,8 +2277,8 @@ index 1ec2c64193206d31702e22e5c4783f084b1cff31..fb463eb12ee39cd1e448369f3b47fbcf } LoadInfo::LoadInfo( -@@ -2534,4 +2535,16 @@ LoadInfo::SetSkipHTTPSUpgrade(bool aSkipHTTPSUpgrade) { - return NS_OK; +@@ -2705,4 +2706,16 @@ void LoadInfo::UpdateParentAddressSpaceInfo() { + } } +NS_IMETHODIMP @@ -2394,23 +2295,23 @@ index 1ec2c64193206d31702e22e5c4783f084b1cff31..fb463eb12ee39cd1e448369f3b47fbcf + } // namespace mozilla::net diff --git a/netwerk/base/LoadInfo.h b/netwerk/base/LoadInfo.h -index 93cc8d3630f7029303240555ae72d41b68047375..8a09863af399e25ba3f01caff2f6b3af1260e8b8 100644 +index 7946dacf862aa13da58c65aba3c72deef575dbba..eb307b58b5fda79fa44e5df94f1bcb4dfaa0a8b6 100644 --- a/netwerk/base/LoadInfo.h +++ b/netwerk/base/LoadInfo.h -@@ -426,6 +426,8 @@ class LoadInfo final : public nsILoadInfo { +@@ -448,6 +448,8 @@ class LoadInfo final : public nsILoadInfo { bool mIsNewWindowTarget = false; bool mSkipHTTPSUpgrade = false; + + uint64_t mJugglerLoadIdentifier = 0; }; - // This is exposed solely for testing purposes and should not be used outside of + // LoadInfo diff --git a/netwerk/base/TRRLoadInfo.cpp b/netwerk/base/TRRLoadInfo.cpp -index d1650595f8cf28a704f94a99c1f6bfe1deb9cc77..2072a3990ff6f4496626dcebb277291ad845cac3 100644 +index dd8cc849fb3dd56d6e979cc3cbff96727f702d90..dd2d543977cd9543494c52e862552831d955fdb2 100644 --- a/netwerk/base/TRRLoadInfo.cpp +++ b/netwerk/base/TRRLoadInfo.cpp -@@ -950,5 +950,15 @@ TRRLoadInfo::GetFetchDestination(nsACString& aDestination) { +@@ -973,5 +973,15 @@ TRRLoadInfo::GetFetchDestination(nsACString& aDestination) { return NS_ERROR_NOT_IMPLEMENTED; } @@ -2427,10 +2328,10 @@ index d1650595f8cf28a704f94a99c1f6bfe1deb9cc77..2072a3990ff6f4496626dcebb277291a } // namespace net } // namespace mozilla diff --git a/netwerk/base/nsILoadInfo.idl b/netwerk/base/nsILoadInfo.idl -index 774ec045c0b18310e8cb86e8a9d6b1788d028435..cbf303a0ed872c27d580b4b6615f3dd9c76a8a19 100644 +index 1cfda408c33c16c75a74ea839ae3bde6142ac92b..b3951bc2df4d448e6c3b7e51bef2c0050e97a35d 100644 --- a/netwerk/base/nsILoadInfo.idl +++ b/netwerk/base/nsILoadInfo.idl -@@ -1626,4 +1626,6 @@ interface nsILoadInfo : nsISupports +@@ -1650,4 +1650,6 @@ interface nsILoadInfo : nsISupports return static_cast(userNavigationInvolvement); } %} @@ -2459,10 +2360,10 @@ index 7f91d2df6f8bb4020c75c132dc8f6bf26625fa1e..aaa5541a17039d6b13ad83ab176fdaaf * Set the status and reason for the forthcoming synthesized response. * Multiple calls overwrite existing values. diff --git a/netwerk/ipc/DocumentLoadListener.cpp b/netwerk/ipc/DocumentLoadListener.cpp -index 771ae1fbe3d54aa25443eea675cf3abd26a21ce9..a916cb49c16dc6c7809ccbb7c8d4172446a5ac07 100644 +index c486793f79e2c8c248e25f7963ba4e2c08f553d2..f1e625c59ec79c1104fe9594dbf86d39f3293438 100644 --- a/netwerk/ipc/DocumentLoadListener.cpp +++ b/netwerk/ipc/DocumentLoadListener.cpp -@@ -177,6 +177,7 @@ static auto CreateDocumentLoadInfo(CanonicalBrowsingContext* aBrowsingContext, +@@ -178,6 +178,7 @@ static auto CreateDocumentLoadInfo(CanonicalBrowsingContext* aBrowsingContext, loadInfo->SetTextDirectiveUserActivation( aLoadState->GetTextDirectiveUserActivation()); loadInfo->SetIsMetaRefresh(aLoadState->IsMetaRefresh()); @@ -2516,7 +2417,7 @@ index fbf4bdf1e24d1102df113984be6c8dc3a7d0d810..787bf014d3bf0b8537f99bf5eb4074e1 + // As they are intercepted by Playwright, they don't have + // serviceWorkerTainting as well. + // Thus these asserts are wrong for Playwright world. -+ // Note: these checks were added in https://github.com/mozilla/gecko-dev/commit/92e2cdde79c11510c3e4192e1b6264d00398ed95 ++ // Note: these checks were added in https://github.com/mozilla-firefox/firefox/commit/bb16ca6496682c3b0ddd452d0dd4c1dd46ff71f8 + /* MOZ_ASSERT_IF(!mLoadInfo->GetServiceWorkerTaintingSynthesized(), mLoadInfo->GetLoadingPrincipal()); @@ -2544,7 +2445,7 @@ index 704404c9f094640ad63b685d64bd5a396e733e4b..92bdc21b4d6a015cc2f2bb22781ec675 * InterceptionTimeStamps is used to record the time stamps of the * interception. diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp -index e4479400a4c574f652befbee7d83bd664aa2b840..2761dce9477f7f1622a82fe1da6472597ae82534 100644 +index af4c6482ad41ad67f41b93183c94d6f341fb4989..0a18827e53760b4132381fd0aff286ee69c460e7 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -688,11 +688,9 @@ nsresult nsHttpChannel::OnBeforeConnect() { @@ -2612,7 +2513,7 @@ index e4479400a4c574f652befbee7d83bd664aa2b840..2761dce9477f7f1622a82fe1da647259 return; } -@@ -4214,9 +4206,6 @@ nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { +@@ -4198,9 +4190,6 @@ nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { uint32_t cacheEntryOpenFlags; bool offline = gIOService->IsOffline(); @@ -2622,7 +2523,7 @@ index e4479400a4c574f652befbee7d83bd664aa2b840..2761dce9477f7f1622a82fe1da647259 bool maybeRCWN = false; nsAutoCString cacheControlRequestHeader; -@@ -4227,7 +4216,7 @@ nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { +@@ -4211,7 +4200,7 @@ nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { return NS_OK; } @@ -2631,7 +2532,7 @@ index e4479400a4c574f652befbee7d83bd664aa2b840..2761dce9477f7f1622a82fe1da647259 if (offline || (mLoadFlags & INHIBIT_CACHING) || forceOffline) { if (BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass()) && !offline && !forceOffline) { -@@ -7300,6 +7289,20 @@ void nsHttpChannel::MaybeStartDNSPrefetch() { +@@ -7315,6 +7304,20 @@ void nsHttpChannel::MaybeStartDNSPrefetch() { } } @@ -2653,10 +2554,10 @@ index e4479400a4c574f652befbee7d83bd664aa2b840..2761dce9477f7f1622a82fe1da647259 nsHttpChannel::GetEncodedBodySize(uint64_t* aEncodedBodySize) { if (mCacheEntry && !LoadCacheEntryIsWriteOnly()) { diff --git a/netwerk/protocol/http/nsHttpChannel.h b/netwerk/protocol/http/nsHttpChannel.h -index cb8b8b7406411edceb30aa53c9b9007a38058f84..ebdc5384ca20feda399b70532a3036174f1a7431 100644 +index fb16e050efea8d49cd315203bfc6a0779f34bb02..c7b5a9894cb993bd420a5407c1aefb4aacd35595 100644 --- a/netwerk/protocol/http/nsHttpChannel.h +++ b/netwerk/protocol/http/nsHttpChannel.h -@@ -307,6 +307,10 @@ class nsHttpChannel final : public HttpBaseChannel, +@@ -303,6 +303,10 @@ class nsHttpChannel final : public HttpBaseChannel, void MaybeResolveProxyAndBeginConnect(); void MaybeStartDNSPrefetch(); @@ -2668,10 +2569,10 @@ index cb8b8b7406411edceb30aa53c9b9007a38058f84..ebdc5384ca20feda399b70532a303617 // end server host name. ProxyDNSStrategy GetProxyDNSStrategy(); diff --git a/parser/html/nsHtml5TreeOpExecutor.cpp b/parser/html/nsHtml5TreeOpExecutor.cpp -index d3b44cc62d3df49bbf842356cbdb153c82c3163c..23cf9bc83fb1faaf1c7406331b78e522b307cbf0 100644 +index 324d71a1567ef2acbd5e9a008961e39a11445419..e76d57612f318b7ad325196684e2e57660008a94 100644 --- a/parser/html/nsHtml5TreeOpExecutor.cpp +++ b/parser/html/nsHtml5TreeOpExecutor.cpp -@@ -1349,6 +1349,10 @@ void nsHtml5TreeOpExecutor::UpdateReferrerInfoFromMeta( +@@ -1359,6 +1359,10 @@ void nsHtml5TreeOpExecutor::UpdateReferrerInfoFromMeta( void nsHtml5TreeOpExecutor::AddSpeculationCSP(const nsAString& aCSP) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); @@ -2683,80 +2584,20 @@ index d3b44cc62d3df49bbf842356cbdb153c82c3163c..23cf9bc83fb1faaf1c7406331b78e522 nsCOMPtr preloadCsp = mDocument->GetPreloadCsp(); if (!preloadCsp) { diff --git a/security/manager/ssl/nsCertOverrideService.cpp b/security/manager/ssl/nsCertOverrideService.cpp -index 8413eb5916f1f857e18972a14292d14f32684aee..66a3c7b01fdc56c29d789ff786aa91d8b0f02cd6 100644 +index 0ecf2c0df3aad52e004a69b4c7a22fea7532b4d8..f5982c2cf1d6ea0522ac7f7e1fb5b4ab8fa07cb4 100644 --- a/security/manager/ssl/nsCertOverrideService.cpp +++ b/security/manager/ssl/nsCertOverrideService.cpp -@@ -433,7 +433,12 @@ nsCertOverrideService::HasMatchingOverride( - bool disableAllSecurityCheck = false; - { - MutexAutoLock lock(mMutex); -- disableAllSecurityCheck = mDisableAllSecurityCheck; -+ if (aOriginAttributes.mUserContextId) { -+ disableAllSecurityCheck = mUserContextIdsWithDisabledSecurityChecks.has( -+ aOriginAttributes.mUserContextId); -+ } else { -+ disableAllSecurityCheck = mDisableAllSecurityCheck; -+ } - } - if (disableAllSecurityCheck) { - *aIsTemporary = false; -@@ -645,14 +650,24 @@ static bool IsDebugger() { - - NS_IMETHODIMP - nsCertOverrideService:: -- SetDisableAllSecurityChecksAndLetAttackersInterceptMyData(bool aDisable) { -- if (!(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || IsDebugger())) { -+ SetDisableAllSecurityChecksAndLetAttackersInterceptMyData( -+ bool aDisable, uint32_t aUserContextId) { -+ if (false /* juggler hacks */ && !(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || IsDebugger())) { - return NS_ERROR_NOT_AVAILABLE; - } - - { - MutexAutoLock lock(mMutex); -- mDisableAllSecurityCheck = aDisable; -+ if (aUserContextId) { -+ if (aDisable) { -+ mozilla::Unused << mUserContextIdsWithDisabledSecurityChecks.put(aUserContextId); -+ } else { -+ mUserContextIdsWithDisabledSecurityChecks.remove(aUserContextId); -+ } -+ return NS_OK; -+ } else { -+ mDisableAllSecurityCheck = aDisable; -+ } - } - - nsCOMPtr nss(do_GetService(PSM_COMPONENT_CONTRACTID)); -diff --git a/security/manager/ssl/nsCertOverrideService.h b/security/manager/ssl/nsCertOverrideService.h -index 21cff56300db6490cf9649aa62099cb5525749b3..ce9a7fc16c2d5980be166e0f4ab9a25df300ca2f 100644 ---- a/security/manager/ssl/nsCertOverrideService.h -+++ b/security/manager/ssl/nsCertOverrideService.h -@@ -118,6 +118,7 @@ class nsCertOverrideService final : public nsICertOverrideService, - - mozilla::Mutex mMutex; - bool mDisableAllSecurityCheck MOZ_GUARDED_BY(mMutex); -+ mozilla::HashSet mUserContextIdsWithDisabledSecurityChecks MOZ_GUARDED_BY(mMutex); - nsCOMPtr mSettingsFile MOZ_GUARDED_BY(mMutex); - nsTHashtable mSettingsTable MOZ_GUARDED_BY(mMutex); - -diff --git a/security/manager/ssl/nsICertOverrideService.idl b/security/manager/ssl/nsICertOverrideService.idl -index 6dfd07d6b676a99993408921de8dea9d561f201d..e3c6794363cd6336effbeac83a179f3796dd71b0 100644 ---- a/security/manager/ssl/nsICertOverrideService.idl -+++ b/security/manager/ssl/nsICertOverrideService.idl -@@ -137,7 +137,9 @@ interface nsICertOverrideService : nsISupports { - * @param aDisable If true, disable all security check and make - * hasMatchingOverride always return true. - */ -- void setDisableAllSecurityChecksAndLetAttackersInterceptMyData(in boolean aDisable); -+ void setDisableAllSecurityChecksAndLetAttackersInterceptMyData( -+ in boolean aDisable, -+ [optional] in uint32_t aUserContextId); +@@ -627,6 +627,8 @@ void nsCertOverrideService::CountPermanentOverrideTelemetry( + } - readonly attribute boolean securityCheckDisabled; - }; + static bool IsDebugger() { ++ // In playwright world, this is always enabled. ++ if (1 == 1) return true; + #ifdef ENABLE_WEBDRIVER + nsCOMPtr marionette = do_GetService(NS_MARIONETTE_CONTRACTID); + if (marionette) { diff --git a/services/settings/Utils.sys.mjs b/services/settings/Utils.sys.mjs -index d3643aedf21a26594268a47bc0f6ac53e3977f75..795c6f3b28278b9f65a596799d4e424880fcffa7 100644 +index cbdf07bcdae11de6f59e6c6c279833aca430dc70..6840c78350ccb52e99f23446b010abc2a1d92a55 100644 --- a/services/settings/Utils.sys.mjs +++ b/services/settings/Utils.sys.mjs @@ -97,7 +97,7 @@ const _cdnURLs = {}; @@ -2793,10 +2634,10 @@ index 75555352b8a15a50e4a21e34fc8ede4e9246c7cc..72855a404effa42b6c55cd0c2fcb8bdd // ignored for Linux. const unsigned long CHROME_SUPPRESS_ANIMATION = 1 << 24; diff --git a/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs -index 76fb919603e8d2b7864d351eb47be2a38e40e31e..9f1e880fe9027d1a2540ffeaa11fc0c4e1a36133 100644 +index 3d8837cdb38fb5b25abfb9e3e369ad6fe688706e..867ce2efaa10eaea87ffeeb2669cc5a7be0045b3 100644 --- a/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs +++ b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs -@@ -108,7 +108,9 @@ EnterprisePoliciesManager.prototype = { +@@ -106,7 +106,9 @@ EnterprisePoliciesManager.prototype = { Services.prefs.clearUserPref(PREF_POLICIES_APPLIED); } @@ -2807,7 +2648,7 @@ index 76fb919603e8d2b7864d351eb47be2a38e40e31e..9f1e880fe9027d1a2540ffeaa11fc0c4 if (provider.failed) { this.status = Ci.nsIEnterprisePolicies.FAILED; -@@ -631,6 +633,19 @@ class JSONPoliciesProvider { +@@ -606,6 +608,19 @@ class JSONPoliciesProvider { } } @@ -2828,10 +2669,10 @@ index 76fb919603e8d2b7864d351eb47be2a38e40e31e..9f1e880fe9027d1a2540ffeaa11fc0c4 constructor() { this._policies = null; diff --git a/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp b/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp -index 253171bed4dea54fc28bb4ddc9920823dbd9351c..6dc0e620b399ed9ee6b53f97bc080ec17ee4e1b5 100644 +index 7dc1cee243017e1647521b021424781532552d8c..47908d73a7a39a7f95ae8ea28362b4f29505508f 100644 --- a/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp +++ b/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp -@@ -490,7 +490,7 @@ void PopulateLanguages() { +@@ -587,7 +587,7 @@ void PopulateLanguages() { // sufficient to only collect this information as the other properties are // just reformats of Navigator::GetAcceptLanguages. nsTArray languages; @@ -2841,7 +2682,7 @@ index 253171bed4dea54fc28bb4ddc9920823dbd9351c..6dc0e620b399ed9ee6b53f97bc080ec1 for (const auto& language : languages) { diff --git a/toolkit/components/startup/nsAppStartup.cpp b/toolkit/components/startup/nsAppStartup.cpp -index dc0826f72134b91482e30d183ddf52e95146e12f..119a324e162b6965ddd3d6b2d53bd2856a174452 100644 +index 316a86d5fcadba99918254ba132f363fb462cc3f..9dcd1b208ffd2d42db6c8ff52476a22791149eb4 100644 --- a/toolkit/components/startup/nsAppStartup.cpp +++ b/toolkit/components/startup/nsAppStartup.cpp @@ -361,7 +361,7 @@ nsAppStartup::Quit(uint32_t aMode, int aExitCode, bool* aUserAllowedQuit) { @@ -2854,7 +2695,7 @@ index dc0826f72134b91482e30d183ddf52e95146e12f..119a324e162b6965ddd3d6b2d53bd285 if (windowEnumerator) { bool more; diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp -index 654903fadb709be976b72f36f155e23bc0622152..815b3dc24c9fda6b1db6c4666ac68904c87ac0ab 100644 +index 89f7233835b4d03e7a140ca2c75ed8db097d482d..ed7c92d30b3cd77c102467eb0f12c9f482a08fde 100644 --- a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp +++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp @@ -174,8 +174,8 @@ nsBrowserStatusFilter::OnStateChange(nsIWebProgress* aWebProgress, @@ -2886,10 +2727,10 @@ index 811fb16410e8cf900ad873797269e5fe715579a5..821f5b0c2af8e1dc8754cd023571d1d0 /** diff --git a/toolkit/mozapps/update/UpdateService.sys.mjs b/toolkit/mozapps/update/UpdateService.sys.mjs -index 40f04aeace0efd701e9454bb8dc6260dec90807e..5b70f65f3e78fc0889b15651ff203bb82e79d202 100644 +index 26d633f4f4fe9125df557ee7367809c8591d7211..6c558e8297114ac3f94ae3836c25e9edd51cc4d4 100644 --- a/toolkit/mozapps/update/UpdateService.sys.mjs +++ b/toolkit/mozapps/update/UpdateService.sys.mjs -@@ -3814,6 +3814,8 @@ export class UpdateService { +@@ -3873,6 +3873,8 @@ export class UpdateService { } get disabledForTesting() { @@ -2899,10 +2740,10 @@ index 40f04aeace0efd701e9454bb8dc6260dec90807e..5b70f65f3e78fc0889b15651ff203bb8 } diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild -index c50b7f3932e18da9fad4b673e353974a001e78c4..708e0d75594ddcd62276d4e08c4bd5c64d7f0698 100644 +index 304c39a11bd88e0b8cf681a3c3bc5dc11ed929ec..71e57ce2164e5436fac68d696b632d120a85d6f2 100644 --- a/toolkit/toolkit.mozbuild +++ b/toolkit/toolkit.mozbuild -@@ -152,6 +152,7 @@ if CONFIG["ENABLE_WEBDRIVER"]: +@@ -151,6 +151,7 @@ if CONFIG["ENABLE_WEBDRIVER"]: "/remote", "/testing/firefox-ui", "/testing/marionette", @@ -2946,10 +2787,10 @@ index 7eb9e1104682d4eb47060654f43a1efa8b2a6bb2..a8315d6decf654b5302bea5beeea3414 // Only run this code if LauncherProcessWin.h was included beforehand, thus // signalling that the hosting process should support launcher mode. diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp -index e5cc386651e192710b61858ab5625c97a02b92da..e560ad4fef232a26ce1e1b244f4ccea05f4aea71 100644 +index 444116840b68443c31d8df66699d47a582ce4622..9ae13a8462301a8a3024bffb10d9a20f9accff1d 100644 --- a/uriloader/base/nsDocLoader.cpp +++ b/uriloader/base/nsDocLoader.cpp -@@ -812,6 +812,12 @@ void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout, +@@ -861,6 +861,12 @@ void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout, ("DocLoader:%p: Firing load event for document.open\n", this)); @@ -2963,7 +2804,7 @@ index e5cc386651e192710b61858ab5625c97a02b92da..e560ad4fef232a26ce1e1b244f4ccea0 // nsDocumentViewer::LoadComplete that doesn't do various things // that are not relevant here because this wasn't an actual diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp -index e23df8e6f982ea71eb1f07dd677ed13109d2831b..d98f49d34a346113fd0ed5c242d5ef228ea0e0cd 100644 +index 7cd8c24d871465bc96fe7249eb9a41c999057eb5..02b3ce1fbaa8056219a139a3bc3f107343f9bb89 100644 --- a/uriloader/exthandler/nsExternalHelperAppService.cpp +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -112,6 +112,7 @@ @@ -3000,7 +2841,7 @@ index e23df8e6f982ea71eb1f07dd677ed13109d2831b..d98f49d34a346113fd0ed5c242d5ef22 mSaver = do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); -@@ -1672,7 +1684,36 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { +@@ -1671,7 +1683,36 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { return NS_OK; } @@ -3038,7 +2879,7 @@ index e23df8e6f982ea71eb1f07dd677ed13109d2831b..d98f49d34a346113fd0ed5c242d5ef22 if (NS_FAILED(rv)) { nsresult transferError = rv; -@@ -1733,6 +1774,9 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { +@@ -1732,6 +1773,9 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { bool alwaysAsk = true; mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk); @@ -3048,7 +2889,7 @@ index e23df8e6f982ea71eb1f07dd677ed13109d2831b..d98f49d34a346113fd0ed5c242d5ef22 if (alwaysAsk) { // But we *don't* ask if this mimeInfo didn't come from // our user configuration datastore and the user has said -@@ -2249,6 +2293,16 @@ nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver, +@@ -2248,6 +2292,16 @@ nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver, NotifyTransfer(aStatus); } @@ -3065,7 +2906,7 @@ index e23df8e6f982ea71eb1f07dd677ed13109d2831b..d98f49d34a346113fd0ed5c242d5ef22 return NS_OK; } -@@ -2732,6 +2786,15 @@ NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) { +@@ -2731,6 +2785,15 @@ NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) { } } @@ -3082,10 +2923,10 @@ index e23df8e6f982ea71eb1f07dd677ed13109d2831b..d98f49d34a346113fd0ed5c242d5ef22 // OnStartRequest) mDialog = nullptr; diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h -index 2dd4ff87bda3e0ba395cca168c42b37db1713ddf..83e8a3d328e325b3f50f593c9ea71692f9c7d401 100644 +index 68f240c9f9280c498900561e6cdb3bc4c4d45a38..395f54e8c47d56b0892c75a513e5828de7e4fceb 100644 --- a/uriloader/exthandler/nsExternalHelperAppService.h +++ b/uriloader/exthandler/nsExternalHelperAppService.h -@@ -258,6 +258,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService, +@@ -254,6 +254,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService, mozilla::dom::BrowsingContext* aContentContext, bool aForceSave, nsIInterfaceRequestor* aWindowContext, nsIStreamListener** aStreamListener); @@ -3094,7 +2935,7 @@ index 2dd4ff87bda3e0ba395cca168c42b37db1713ddf..83e8a3d328e325b3f50f593c9ea71692 }; /** -@@ -455,6 +457,9 @@ class nsExternalAppHandler final : public nsIStreamListener, +@@ -451,6 +453,9 @@ class nsExternalAppHandler final : public nsIStreamListener, * Upon successful return, both mTempFile and mSaver will be valid. */ nsresult SetUpTempFile(nsIChannel* aChannel); @@ -3148,7 +2989,7 @@ index 53ea934dd4876e4b491b724385c8fbf7d00ee6cd..0b7b88c853b21ce778d8e87fea0a2bfe /** diff --git a/widget/InProcessCompositorWidget.cpp b/widget/InProcessCompositorWidget.cpp -index 1c25e9d9a101233f71e92288a0f93125b81ac1c5..22cf67b0f6e3ddd2b3ed725a314ba6a9896abd1c 100644 +index 7098db301770ecb5b9a506d7caec89d5cf63384b..aff8d7562f8a3e595a077ce8e591008762479587 100644 --- a/widget/InProcessCompositorWidget.cpp +++ b/widget/InProcessCompositorWidget.cpp @@ -4,7 +4,10 @@ @@ -3176,10 +3017,10 @@ index 1c25e9d9a101233f71e92288a0f93125b81ac1c5..22cf67b0f6e3ddd2b3ed725a314ba6a9 } #endif diff --git a/widget/MouseEvents.h b/widget/MouseEvents.h -index 5ca1a6fa13233b1bd00ee0467732c5875c51d343..0d3b8ebe127e59516802e8819f4bbed961f0992b 100644 +index 8004d6fe2130246252e57198f2ea731f84d1968a..7a0774b8320741108dc209c3b7069f524b1360ca 100644 --- a/widget/MouseEvents.h +++ b/widget/MouseEvents.h -@@ -368,6 +368,9 @@ class WidgetMouseEvent : public WidgetMouseEventBase, +@@ -375,6 +375,9 @@ class WidgetMouseEvent : public WidgetMouseEventBase, // Otherwise, this must be 0. uint32_t mClickCount = 0; @@ -3189,7 +3030,7 @@ index 5ca1a6fa13233b1bd00ee0467732c5875c51d343..0d3b8ebe127e59516802e8819f4bbed9 // Whether the event should ignore scroll frame bounds during dispatch. bool mIgnoreRootScrollFrame = false; -@@ -391,6 +394,7 @@ class WidgetMouseEvent : public WidgetMouseEventBase, +@@ -398,6 +401,7 @@ class WidgetMouseEvent : public WidgetMouseEventBase, mContextMenuTrigger = aEvent.mContextMenuTrigger; mExitFrom = aEvent.mExitFrom; mClickCount = aEvent.mClickCount; @@ -3261,7 +3102,7 @@ index 24b70173c2e8bb9be9fd6255984a70efe3b14099..75ac367a1c4bb44d4b68b5f4ecc6adf5 } if (aEvent.IsMeta()) { diff --git a/widget/gtk/nsFilePicker.cpp b/widget/gtk/nsFilePicker.cpp -index f4bded345e95674c66e4d4ad56b50844fce0871b..321e22d334a8bbc6057ee78e77e139a2804b2403 100644 +index 02b7b185caf4a2352522c0ed4185d89b514c1738..912a270b6d87e1154d844bde2ffe6a3c2b8f3061 100644 --- a/widget/gtk/nsFilePicker.cpp +++ b/widget/gtk/nsFilePicker.cpp @@ -21,6 +21,7 @@ @@ -3273,7 +3114,7 @@ index f4bded345e95674c66e4d4ad56b50844fce0871b..321e22d334a8bbc6057ee78e77e139a2 #include "nsArrayEnumerator.h" #include "nsEnumeratorUtils.h" diff --git a/widget/headless/HeadlessCompositorWidget.cpp b/widget/headless/HeadlessCompositorWidget.cpp -index bb4ee9175e66dc40de1871a7f91368fe309494a3..747625e3869882300bfbc18b184db5151dd90c1a 100644 +index bb4ee9175e66dc40de1871a7f91368fe309494a3..6814ae85038d31f1a3b1f87ba341219ffbc1e6b3 100644 --- a/widget/headless/HeadlessCompositorWidget.cpp +++ b/widget/headless/HeadlessCompositorWidget.cpp @@ -3,6 +3,7 @@ @@ -3284,7 +3125,7 @@ index bb4ee9175e66dc40de1871a7f91368fe309494a3..747625e3869882300bfbc18b184db515 #include "mozilla/widget/PlatformWidgetTypes.h" #include "HeadlessCompositorWidget.h" #include "VsyncDispatcher.h" -@@ -15,9 +16,32 @@ HeadlessCompositorWidget::HeadlessCompositorWidget( +@@ -15,9 +16,30 @@ HeadlessCompositorWidget::HeadlessCompositorWidget( const layers::CompositorOptions& aOptions, HeadlessWidget* aWindow) : CompositorWidget(aOptions), mWidget(aWindow), @@ -3304,12 +3145,10 @@ index bb4ee9175e66dc40de1871a7f91368fe309494a3..747625e3869882300bfbc18b184db515 +} + +already_AddRefed HeadlessCompositorWidget::StartRemoteDrawingInRegion( -+ const LayoutDeviceIntRegion& aInvalidRegion, -+ layers::BufferMode* aBufferMode) { ++ const LayoutDeviceIntRegion& aInvalidRegion) { + if (!mDrawTarget) + return nullptr; + -+ *aBufferMode = layers::BufferMode::BUFFER_NONE; + RefPtr result = mDrawTarget; + return result.forget(); +} @@ -3317,7 +3156,7 @@ index bb4ee9175e66dc40de1871a7f91368fe309494a3..747625e3869882300bfbc18b184db515 void HeadlessCompositorWidget::ObserveVsync(VsyncObserver* aObserver) { if (RefPtr cvd = mWidget->GetCompositorVsyncDispatcher()) { -@@ -31,6 +55,59 @@ void HeadlessCompositorWidget::NotifyClientSizeChanged( +@@ -31,6 +53,59 @@ void HeadlessCompositorWidget::NotifyClientSizeChanged( const LayoutDeviceIntSize& aClientSize) { auto size = mClientSize.Lock(); *size = aClientSize; @@ -3378,7 +3217,7 @@ index bb4ee9175e66dc40de1871a7f91368fe309494a3..747625e3869882300bfbc18b184db515 LayoutDeviceIntSize HeadlessCompositorWidget::GetClientSize() { diff --git a/widget/headless/HeadlessCompositorWidget.h b/widget/headless/HeadlessCompositorWidget.h -index facd2bc65afab8ec1aa322faa20a67464964dfb9..d6dea95472bec6006411753c3dfdab2e3659171f 100644 +index facd2bc65afab8ec1aa322faa20a67464964dfb9..3c5751ad1b7f042bc7cd9a63895cebcd2942ccd3 100644 --- a/widget/headless/HeadlessCompositorWidget.h +++ b/widget/headless/HeadlessCompositorWidget.h @@ -6,6 +6,7 @@ @@ -3389,7 +3228,7 @@ index facd2bc65afab8ec1aa322faa20a67464964dfb9..d6dea95472bec6006411753c3dfdab2e #include "mozilla/widget/CompositorWidget.h" #include "HeadlessWidget.h" -@@ -23,8 +24,12 @@ class HeadlessCompositorWidget final : public CompositorWidget, +@@ -23,8 +24,11 @@ class HeadlessCompositorWidget final : public CompositorWidget, HeadlessWidget* aWindow); void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize); @@ -3397,12 +3236,11 @@ index facd2bc65afab8ec1aa322faa20a67464964dfb9..d6dea95472bec6006411753c3dfdab2e // CompositorWidget Overrides + already_AddRefed StartRemoteDrawingInRegion( -+ const LayoutDeviceIntRegion& aInvalidRegion, -+ layers::BufferMode* aBufferMode) override; ++ const LayoutDeviceIntRegion& aInvalidRegion) override; uintptr_t GetWidgetKey() override; -@@ -42,10 +47,18 @@ class HeadlessCompositorWidget final : public CompositorWidget, +@@ -42,10 +46,18 @@ class HeadlessCompositorWidget final : public CompositorWidget, } private: diff --git a/browser_patches/webkit/UPSTREAM_CONFIG.sh b/browser_patches/webkit/UPSTREAM_CONFIG.sh index 574e1f310aa7f..d6e587bc4a52b 100644 --- a/browser_patches/webkit/UPSTREAM_CONFIG.sh +++ b/browser_patches/webkit/UPSTREAM_CONFIG.sh @@ -1,3 +1,3 @@ REMOTE_URL="https://github.com/WebKit/WebKit.git" BASE_BRANCH="main" -BASE_REVISION="153da00a252619799ba4b32cd0ac6c5b8faf6a35" +BASE_REVISION="db897909f7c31d9b38793374c66427b6a4cb3dd3" diff --git a/browser_patches/webkit/embedder/Playwright/mac/AppDelegate.m b/browser_patches/webkit/embedder/Playwright/mac/AppDelegate.m index b4d08b700ac91..0d100edad05e4 100644 --- a/browser_patches/webkit/embedder/Playwright/mac/AppDelegate.m +++ b/browser_patches/webkit/embedder/Playwright/mac/AppDelegate.m @@ -236,16 +236,18 @@ - (WKWebViewConfiguration *)defaultConfiguration configuration.preferences._mediaDevicesEnabled = YES; configuration.preferences._mockCaptureDevicesEnabled = YES; // Enable WebM support. - configuration.preferences._alternateWebMPlayerEnabled = YES; configuration.preferences._hiddenPageDOMTimerThrottlingEnabled = NO; configuration.preferences._hiddenPageDOMTimerThrottlingAutoIncreases = NO; configuration.preferences._pageVisibilityBasedProcessSuppressionEnabled = NO; configuration.preferences._domTimersThrottlingEnabled = NO; // Do not auto play audio and video with sound. configuration.defaultWebpagePreferences._autoplayPolicy = _WKWebsiteAutoplayPolicyAllowWithoutSound; + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" _WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease]; processConfiguration.forceOverlayScrollbars = YES; configuration.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease]; + #pragma clang diagnostic pop } return configuration; } @@ -288,7 +290,10 @@ - (WKWebViewConfiguration *) sessionConfiguration:(uint64_t)sessionID continue; WKWebViewConfiguration *configuration = [[[self defaultConfiguration] copy] autorelease]; configuration.websiteDataStore = [browserContext dataStore]; + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" configuration.processPool = [browserContext processPool]; + #pragma clang diagnostic pop return configuration; } return [self defaultConfiguration]; @@ -347,8 +352,11 @@ - (WKWebView *)createHeadlessPage:(WKWebViewConfiguration *)configuration withUR - (_WKBrowserContext *)createBrowserContext:(NSString *)proxyServer WithBypassList:(NSString *) proxyBypassList { _WKBrowserContext *browserContext = [[_WKBrowserContext alloc] init]; + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" _WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease]; processConfiguration.forceOverlayScrollbars = YES; + #pragma clang diagnostic pop _WKWebsiteDataStoreConfiguration *dataStoreConfiguration = [[[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration] autorelease]; if (!proxyServer || ![proxyServer length]) proxyServer = _proxyServer; @@ -356,7 +364,10 @@ - (_WKBrowserContext *)createBrowserContext:(NSString *)proxyServer WithBypassLi proxyBypassList = _proxyBypassList; [dataStoreConfiguration setProxyConfiguration:[self proxyConfiguration:proxyServer WithBypassList:proxyBypassList]]; browserContext.dataStore = [[[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration] autorelease]; + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" browserContext.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease]; + #pragma clang diagnostic pop [_browserContexts addObject:browserContext]; return browserContext; } diff --git a/browser_patches/webkit/patches/bootstrap.diff b/browser_patches/webkit/patches/bootstrap.diff index 9d644885f063b..f713c6fa093a1 100644 --- a/browser_patches/webkit/patches/bootstrap.diff +++ b/browser_patches/webkit/patches/bootstrap.diff @@ -1,8 +1,8 @@ diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt -index 458e1d4118805169ee6f9139af942cc5c3f88f8e..238f2a33ba150fae735915244910eaa66f5fd267 100644 +index 1b7690be3268f54cc649a97206fc38fb27453e08..d14a541e585d2b43d6738c9bb5eb26642522b46b 100644 --- a/Source/JavaScriptCore/CMakeLists.txt +++ b/Source/JavaScriptCore/CMakeLists.txt -@@ -1411,21 +1411,26 @@ set(JavaScriptCore_INSPECTOR_DOMAINS +@@ -1418,21 +1418,26 @@ set(JavaScriptCore_INSPECTOR_DOMAINS ${JAVASCRIPTCORE_DIR}/inspector/protocol/CSS.json ${JAVASCRIPTCORE_DIR}/inspector/protocol/Canvas.json ${JAVASCRIPTCORE_DIR}/inspector/protocol/Console.json @@ -30,10 +30,10 @@ index 458e1d4118805169ee6f9139af942cc5c3f88f8e..238f2a33ba150fae735915244910eaa6 ${JAVASCRIPTCORE_DIR}/inspector/protocol/ServiceWorker.json ${JAVASCRIPTCORE_DIR}/inspector/protocol/Target.json diff --git a/Source/JavaScriptCore/DerivedSources-input.xcfilelist b/Source/JavaScriptCore/DerivedSources-input.xcfilelist -index 9fd3a4e6a53eb3339ced1e87f0b4cacd16f73167..a77d72ab052625c7b5a4d7c56a71befbad884871 100644 +index 53809cbc20c8fb942304a25899a2e5ecccc79ee3..63d93e08bcf8089e4e315e48d5d2093750588cb1 100644 --- a/Source/JavaScriptCore/DerivedSources-input.xcfilelist +++ b/Source/JavaScriptCore/DerivedSources-input.xcfilelist -@@ -98,20 +98,25 @@ $(PROJECT_DIR)/inspector/protocol/CPUProfiler.json +@@ -100,20 +100,25 @@ $(PROJECT_DIR)/inspector/protocol/CPUProfiler.json $(PROJECT_DIR)/inspector/protocol/CSS.json $(PROJECT_DIR)/inspector/protocol/Canvas.json $(PROJECT_DIR)/inspector/protocol/Console.json @@ -60,10 +60,10 @@ index 9fd3a4e6a53eb3339ced1e87f0b4cacd16f73167..a77d72ab052625c7b5a4d7c56a71befb $(PROJECT_DIR)/inspector/protocol/Security.json $(PROJECT_DIR)/inspector/protocol/ServiceWorker.json diff --git a/Source/JavaScriptCore/DerivedSources.make b/Source/JavaScriptCore/DerivedSources.make -index 3db6c2e250371dedc6a9f88bf1688112d467434a..a3832759ea36fb50bdbea6809b9fd13e6f66e46b 100644 +index cb42b0e750a8b6a64ebc58f1bfe65c50e7b4c99c..ed548bf0ac70da0273a20178a2786e232907ce68 100644 --- a/Source/JavaScriptCore/DerivedSources.make +++ b/Source/JavaScriptCore/DerivedSources.make -@@ -301,21 +301,26 @@ INSPECTOR_DOMAINS := \ +@@ -303,21 +303,26 @@ INSPECTOR_DOMAINS := \ $(JavaScriptCore)/inspector/protocol/CSS.json \ $(JavaScriptCore)/inspector/protocol/Canvas.json \ $(JavaScriptCore)/inspector/protocol/Console.json \ @@ -118,7 +118,7 @@ index 9bc5d1fd8e2a7e576be046b3c6ae1266696cf552..610f810db1dd6865c500c0796386a828 { return addPrefixToIdentifier(++s_lastUsedIdentifier); diff --git a/Source/JavaScriptCore/inspector/IdentifiersFactory.h b/Source/JavaScriptCore/inspector/IdentifiersFactory.h -index 4113ddb45d4a8d08379d4dc56c44cde56162accf..e00cb9cb01a7a241f3f25b1e4cdc2fcaee92b3ac 100644 +index b967783b83cc6da337ab0840ce0a0a8d480cbc4f..06a09811709e5e6cb1db24ad6fc4895481d39f65 100644 --- a/Source/JavaScriptCore/inspector/IdentifiersFactory.h +++ b/Source/JavaScriptCore/inspector/IdentifiersFactory.h @@ -31,6 +31,7 @@ namespace Inspector { @@ -146,7 +146,7 @@ index 8b39848154ecab9f7daa2d21c85562a319cd06d7..c8a1f44cb4516993899ffe1404b6c386 return nullptr; inspectorObject->setValue(name.string(), inspectorValue.releaseNonNull()); diff --git a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp -index 1f5d0adbf624bd24ef1e525967e6e82e8c37b4e5..4fe0f364b4ccd11774bf29f772e0a568549a4322 100644 +index 6f8cae4d995d3ae020cd09aa0ba0c9ccc1e78409..fc3d0d1e0803ea38a70fee82ea94624797c6e407 100644 --- a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp +++ b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp @@ -102,7 +102,7 @@ void BackendDispatcher::registerDispatcherForDomain(const String& domain, Supple @@ -169,7 +169,7 @@ index 1f5d0adbf624bd24ef1e525967e6e82e8c37b4e5..4fe0f364b4ccd11774bf29f772e0a568 // We could be called re-entrantly from a nested run loop, so restore the previous id. SetForScope scopedRequestId(m_currentRequestId, requestId); diff --git a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h -index 28f4cdacf6ebd7037a42a75872618436332d90ec..463f014be2bd29a75bee7b2113b6f929da13aca5 100644 +index c988668bd732434ec4eaa485c7bbc0a93e2e64cf..75da5ed8a0575d8fe444f3f251c1af99fed78c97 100644 --- a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h +++ b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h @@ -95,8 +95,11 @@ public: @@ -237,7 +237,7 @@ index b555c2e5a071d0a6a016061cc60755449557556d..d019346f0932296d15212c76a4a9b56b bool m_isPaused { false }; }; diff --git a/Source/JavaScriptCore/inspector/JSGlobalObjectConsoleClient.cpp b/Source/JavaScriptCore/inspector/JSGlobalObjectConsoleClient.cpp -index 13eb84b3cba851e14eae8894f8f7aeb242b6f9d4..30bfdf65a7b891420a25bbc54bfe7f03db694273 100644 +index 2b36937d913f237ca777f455ada0fbb9bf5f29df..1dd30c0c017dcce8b9019d5c2bf28005e6ea971e 100644 --- a/Source/JavaScriptCore/inspector/JSGlobalObjectConsoleClient.cpp +++ b/Source/JavaScriptCore/inspector/JSGlobalObjectConsoleClient.cpp @@ -222,6 +222,14 @@ void JSGlobalObjectConsoleClient::screenshot(JSGlobalObject*, Ref> getPreview(const Protocol::Runtime::RemoteObjectId&) final; Protocol::ErrorStringOr>, RefPtr>>> getProperties(const Protocol::Runtime::RemoteObjectId&, std::optional&& ownProperties, std::optional&& fetchStart, std::optional&& fetchCount, std::optional&& generatePreview) final; diff --git a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp -index e47c6ca59f37fbf18ca8a393df72e0472363fabd..b393465540595220561ae00afb85408279710864 100644 +index db75bf0f9edf28d3e23ab40740dfb86a6fd3b104..045caf300e3f9085e5d0ae7feb424f4bf3cf964f 100644 --- a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp +++ b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.cpp @@ -90,6 +90,34 @@ Protocol::ErrorStringOr InspectorTargetAgent::sendMessageToTarget(const St @@ -365,7 +365,7 @@ index e47c6ca59f37fbf18ca8a393df72e0472363fabd..b393465540595220561ae00afb854082 void InspectorTargetAgent::didCommitProvisionalTarget(const String& oldTargetID, const String& committedTargetID) diff --git a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h -index 04377b714a6ccb5294c65d592e74350621d470ba..b6de937bfa3e6185ce29f4e432d327a3cedee6df 100644 +index d1ebed23d1fbacd95e1543606b2591438826b0d2..8536f8325de2afa21e814c2d2ce4bef8189bda70 100644 --- a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h +++ b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h @@ -53,8 +53,11 @@ public: @@ -1679,33 +1679,39 @@ index 24891ad836086fd23024fcb4d08ca63f6974c812..29f4b6b1923383fec7a99d28a4e815dc private: enum ArgumentRequirement { ArgumentRequired, ArgumentNotRequired }; diff --git a/Source/ThirdParty/libwebrtc/CMakeLists.txt b/Source/ThirdParty/libwebrtc/CMakeLists.txt -index ca4f3508a44e3c6677a72fbe3d7c853714b4f2c6..ae117f5f402a7eb259e376ca9440e00062e22d9f 100644 +index a04e69b350a29f6b569a04d094a15872f0745870..6f2aa0dcb9fe06cdbc8184b80b13bf735729ad1f 100644 --- a/Source/ThirdParty/libwebrtc/CMakeLists.txt +++ b/Source/ThirdParty/libwebrtc/CMakeLists.txt -@@ -532,6 +532,11 @@ set(webrtc_SOURCES - Source/third_party/crc32c/src/src/crc32c.cc - Source/third_party/crc32c/src/src/crc32c_portable.cc - Source/third_party/crc32c/src/src/crc32c_sse42.cc +@@ -1807,6 +1807,14 @@ list(APPEND webrtc_SOURCES + Source/third_party/boringssl/src/util/fipstools/acvp/modulewrapper/modulewrapper.cc + ) + +# Playwright begin ++list(APPEND webrtc_SOURCES + Source/third_party/libwebm/mkvmuxer/mkvmuxer.cc + Source/third_party/libwebm/mkvmuxer/mkvmuxerutil.cc + Source/third_party/libwebm/mkvmuxer/mkvwriter.cc ++) +# Playwright end - Source/third_party/libyuv/source/compare.cc - Source/third_party/libyuv/source/compare_common.cc - Source/third_party/libyuv/source/compare_gcc.cc -@@ -2348,6 +2353,11 @@ set(webrtc_INCLUDE_DIRECTORIES PRIVATE - Source/third_party/libsrtp/config - Source/third_party/libsrtp/crypto/include - Source/third_party/libsrtp/include ++ + if (WTF_CPU_X86_64 OR WTF_CPU_X86) + list(APPEND webrtc_SOURCES + Source/webrtc/common_audio/fir_filter_sse.cc +@@ -2399,6 +2407,14 @@ set(webrtc_INCLUDE_DIRECTORIES PRIVATE + Source/webrtc/modules/video_coding/include + ) + +# Playwright begin ++list(APPEND webrtc_INCLUDE_DIRECTORIES + Source/third_party/libwebm + Source/third_party/libvpx/source/libvpx + Source/third_party/libvpx/source/libvpx/third_party/googletest/src/include ++) +# Playwright end - Source/third_party/libyuv/include - Source/third_party/opus/src/celt - Source/third_party/opus/src/include ++ + if (APPLE) + list(APPEND webrtc_INCLUDE_DIRECTORIES PRIVATE + Source/third_party/libvpx/source/libvpx diff --git a/Source/ThirdParty/libwebrtc/Configurations/Base-libwebrtc.xcconfig b/Source/ThirdParty/libwebrtc/Configurations/Base-libwebrtc.xcconfig index 0c5c8e689bdddec766f9de5bffd4444a5e068d77..330dd1f585e530722178c65c883641a2b8c0f1bd 100644 --- a/Source/ThirdParty/libwebrtc/Configurations/Base-libwebrtc.xcconfig @@ -1720,13 +1726,13 @@ index 0c5c8e689bdddec766f9de5bffd4444a5e068d77..330dd1f585e530722178c65c883641a2 // FIXME: Set WEBRTC_USE_BUILTIN_ISAC_FIX and WEBRTC_USE_BUILTIN_ISAC_FLOAT for iOS and Mac diff --git a/Source/ThirdParty/libwebrtc/Configurations/libwebrtc.exp b/Source/ThirdParty/libwebrtc/Configurations/libwebrtc.exp -index 13c5b5ea562fe808a3251c3ae789f8106632cd25..36b77e7d6bc78ba2e982cad5a2b4792775d3280d 100644 +index 30585bd097ec1e57d0c5f87180bf59a71f0192df..95b7a60b3cc66704c51fd6a6ce4e32582a089f9f 100644 --- a/Source/ThirdParty/libwebrtc/Configurations/libwebrtc.exp +++ b/Source/ThirdParty/libwebrtc/Configurations/libwebrtc.exp -@@ -434,3 +434,16 @@ __ZN6webrtc18VideoFrameMetadata7SetSsrcEj - __ZN6webrtc18VideoFrameMetadata8SetCsrcsENSt3__16vectorIjNS1_9allocatorIjEEEE - __ZN6webrtc18VideoFrameMetadata8SetWidthEt - __ZN6webrtc18VideoFrameMetadata9SetHeightEt +@@ -404,3 +404,16 @@ __ZTVN6webrtc14NetworkManagerE + __ZTVN6webrtc17AsyncPacketSocketE + __ZTVN6webrtc18NetworkManagerBaseE + __ZNK6webrtc9IPAddressneERKS0_ +__ZN8mkvmuxer11SegmentInfo4InitEv +__ZN8mkvmuxer9MkvWriterC1EP7__sFILE +_ARGBToI420 @@ -1741,7 +1747,7 @@ index 13c5b5ea562fe808a3251c3ae789f8106632cd25..36b77e7d6bc78ba2e982cad5a2b47927 +_vpx_codec_version_str +_vpx_codec_vp8_cx diff --git a/Source/ThirdParty/libwebrtc/libwebrtc.xcodeproj/project.pbxproj b/Source/ThirdParty/libwebrtc/libwebrtc.xcodeproj/project.pbxproj -index 7585b2e5e9bffdc8cabd888dd822313d53b30141..5909d33b9726cdc7d2d53b538f7da18bc221e30b 100644 +index df544586f202dfdcb53b0c16ecf7a51b60ca3767..81f39ca4766d0368109ae90b94beb3cbe458a06f 100644 --- a/Source/ThirdParty/libwebrtc/libwebrtc.xcodeproj/project.pbxproj +++ b/Source/ThirdParty/libwebrtc/libwebrtc.xcodeproj/project.pbxproj @@ -56,6 +56,20 @@ @@ -1765,7 +1771,7 @@ index 7585b2e5e9bffdc8cabd888dd822313d53b30141..5909d33b9726cdc7d2d53b538f7da18b /* Begin PBXBuildFile section */ 2D6BFF60280A93DF00A1A74F /* video_coding.h in Headers */ = {isa = PBXBuildFile; fileRef = 4131C45B234C81710028A615 /* video_coding.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2D6BFF61280A93EC00A1A74F /* video_codec_initializer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4131C45E234C81720028A615 /* video_codec_initializer.h */; settings = {ATTRIBUTES = (Public, ); }; }; -@@ -5786,6 +5800,13 @@ +@@ -5852,6 +5866,13 @@ remoteGlobalIDString = DDF30D0527C5C003006A526F; remoteInfo = absl; }; @@ -1779,7 +1785,7 @@ index 7585b2e5e9bffdc8cabd888dd822313d53b30141..5909d33b9726cdc7d2d53b538f7da18b /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ -@@ -24271,6 +24292,7 @@ +@@ -24570,6 +24591,7 @@ ); dependencies = ( 410B3827292B73E90003E515 /* PBXTargetDependency */, @@ -1787,7 +1793,7 @@ index 7585b2e5e9bffdc8cabd888dd822313d53b30141..5909d33b9726cdc7d2d53b538f7da18b DD2E76E827C6B69A00F2A74C /* PBXTargetDependency */, CDEBB4CC24C01AB400ADBD44 /* PBXTargetDependency */, 411ED040212E0811004320BA /* PBXTargetDependency */, -@@ -24364,6 +24386,7 @@ +@@ -24663,6 +24685,7 @@ 4460B8B92B155B6A00392062 /* vp9_qp_parser_fuzzer */, 444A6EF02AEADFC9005FE121 /* vp9_replay_fuzzer */, 44945C512B9BA1C300447FFD /* webm_fuzzer */, @@ -1795,7 +1801,7 @@ index 7585b2e5e9bffdc8cabd888dd822313d53b30141..5909d33b9726cdc7d2d53b538f7da18b ); }; /* End PBXProject section */ -@@ -24467,6 +24490,23 @@ +@@ -24766,6 +24789,23 @@ shellPath = /bin/sh; shellScript = "[ -z \"${WK_DERIVED_SDK_HEADERS_DIR}\" -o -d \"${WK_DERIVED_SDK_HEADERS_DIR}\" ] && touch \"${SCRIPT_OUTPUT_FILE_0}\"\n"; }; @@ -1819,7 +1825,7 @@ index 7585b2e5e9bffdc8cabd888dd822313d53b30141..5909d33b9726cdc7d2d53b538f7da18b /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ -@@ -27437,6 +27477,11 @@ +@@ -27789,6 +27829,11 @@ target = DDF30D0527C5C003006A526F /* absl */; targetProxy = DD2E76E727C6B69A00F2A74C /* PBXContainerItemProxy */; }; @@ -1831,7 +1837,7 @@ index 7585b2e5e9bffdc8cabd888dd822313d53b30141..5909d33b9726cdc7d2d53b538f7da18b /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ -@@ -28204,6 +28249,27 @@ +@@ -28556,6 +28601,27 @@ }; name = Production; }; @@ -1859,7 +1865,7 @@ index 7585b2e5e9bffdc8cabd888dd822313d53b30141..5909d33b9726cdc7d2d53b538f7da18b FB39D0711200ED9200088E69 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5D7C59C71208C68B001C873E /* DebugRelease.xcconfig */; -@@ -28586,6 +28652,16 @@ +@@ -28938,6 +29004,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Production; }; @@ -1877,7 +1883,7 @@ index 7585b2e5e9bffdc8cabd888dd822313d53b30141..5909d33b9726cdc7d2d53b538f7da18b isa = XCConfigurationList; buildConfigurations = ( diff --git a/Source/ThirdParty/skia/CMakeLists.txt b/Source/ThirdParty/skia/CMakeLists.txt -index 6bfc5cba986488f3d808ebd0583c476cd93da70e..f4c4222d17bce640355ba52e00152069d5b14432 100644 +index 8675218a87262162d91bf992d00a1eecaf83f289..7dfe83ab3f9dd97bb13721f7034b4963efce3a30 100644 --- a/Source/ThirdParty/skia/CMakeLists.txt +++ b/Source/ThirdParty/skia/CMakeLists.txt @@ -10,6 +10,8 @@ if (USE_SKIA_ENCODERS) @@ -1889,7 +1895,7 @@ index 6bfc5cba986488f3d808ebd0583c476cd93da70e..f4c4222d17bce640355ba52e00152069 if (ANDROID) find_package(EXPAT REQUIRED) endif () -@@ -948,6 +950,7 @@ endif () +@@ -958,6 +960,7 @@ endif () target_link_libraries(Skia PRIVATE JPEG::JPEG PNG::PNG @@ -1897,23 +1903,11 @@ index 6bfc5cba986488f3d808ebd0583c476cd93da70e..f4c4222d17bce640355ba52e00152069 ) WEBKIT_ADD_TARGET_CXX_FLAGS(Skia -diff --git a/Source/ThirdParty/skia/src/opts/SkOpts_SetTarget.h b/Source/ThirdParty/skia/src/opts/SkOpts_SetTarget.h -index 525cfcb862ae96bf8573d00b67dc9e5e23c10d22..f2debc0444cb8f5b80a0e99a2214bceaab3960c1 100644 ---- a/Source/ThirdParty/skia/src/opts/SkOpts_SetTarget.h -+++ b/Source/ThirdParty/skia/src/opts/SkOpts_SetTarget.h -@@ -65,6 +65,7 @@ - // Each of the specific intrinsic headers also checks to ensure that immintrin.h has been - // included, so do that here, first. - #if defined(__clang__) && defined(_MSC_VER) -+ #define __RTMINTRIN_H // Workaround for https://github.com/llvm/llvm-project/issues/95133 - #include - #endif - diff --git a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml -index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd7bf5a80f 100644 +index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb81574982ee7fd1 100644 --- a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml +++ b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml -@@ -588,6 +588,7 @@ ApplePayEnabled: +@@ -562,6 +562,7 @@ ApplePayEnabled: richJavaScript: true # FIXME: This is on by default in WebKit2 PLATFORM(COCOA). Perhaps we should consider turning it on for WebKitLegacy as well. @@ -1921,7 +1915,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd AsyncClipboardAPIEnabled: type: bool status: mature -@@ -598,7 +599,7 @@ AsyncClipboardAPIEnabled: +@@ -572,7 +573,7 @@ AsyncClipboardAPIEnabled: default: false WebKit: "PLATFORM(COCOA) || PLATFORM(GTK) || PLATFORM(WPE)" : true @@ -1930,7 +1924,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd WebCore: default: false -@@ -857,13 +858,10 @@ BlobFileAccessEnforcementEnabled: +@@ -832,13 +833,10 @@ BlobFileAccessEnforcementEnabled: sharedPreferenceForWebProcess: true defaultValue: WebKitLegacy: @@ -1944,7 +1938,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd default: false BlockFontServiceInWebContentSandbox: -@@ -2143,6 +2141,7 @@ CrossOriginEmbedderPolicyEnabled: +@@ -2089,6 +2087,7 @@ CrossOriginEmbedderPolicyEnabled: WebCore: default: false @@ -1952,7 +1946,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd CrossOriginOpenerPolicyEnabled: type: bool status: stable -@@ -2216,6 +2215,7 @@ DOMAudioSessionFullEnabled: +@@ -2162,6 +2161,7 @@ DOMAudioSessionFullEnabled: WebCore: default: false @@ -1960,7 +1954,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd DOMPasteAccessRequestsEnabled: type: bool status: internal -@@ -2227,7 +2227,7 @@ DOMPasteAccessRequestsEnabled: +@@ -2173,7 +2173,7 @@ DOMPasteAccessRequestsEnabled: default: false WebKit: "PLATFORM(IOS) || PLATFORM(MAC) || PLATFORM(GTK) || PLATFORM(WPE) || PLATFORM(VISION)": true @@ -1969,7 +1963,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd WebCore: default: false -@@ -2293,10 +2293,10 @@ DataListElementEnabled: +@@ -2239,10 +2239,10 @@ DataListElementEnabled: WebKitLegacy: default: false WebKit: @@ -1982,7 +1976,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd default: false sharedPreferenceForWebProcess: true -@@ -2309,7 +2309,7 @@ DataTransferItemsEnabled: +@@ -2255,7 +2255,7 @@ DataTransferItemsEnabled: WebKitLegacy: default: true WebKit: @@ -1991,7 +1985,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd default: false WebCore: default: false -@@ -2537,7 +2537,7 @@ DirectoryUploadEnabled: +@@ -2497,7 +2497,7 @@ DirectoryUploadEnabled: WebKitLegacy: default: false WebKit: @@ -2000,7 +1994,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd default: false WebCore: default: false -@@ -3020,10 +3020,10 @@ FullScreenEnabled: +@@ -3022,10 +3022,10 @@ FullScreenEnabled: WebKitLegacy: default: false WebKit: @@ -2013,7 +2007,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd default: false sharedPreferenceForWebProcess: true -@@ -3614,7 +3614,7 @@ InputTypeColorEnabled: +@@ -3632,7 +3632,7 @@ InputTypeColorEnabled: WebKitLegacy: default: false WebKit: @@ -2022,7 +2016,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd default: false WebCore: default: false -@@ -3647,7 +3647,7 @@ InputTypeDateEnabled: +@@ -3665,7 +3665,7 @@ InputTypeDateEnabled: "PLATFORM(IOS_FAMILY)": true default: false WebKit: @@ -2031,7 +2025,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd default: false WebCore: default: false -@@ -3663,7 +3663,7 @@ InputTypeDateTimeLocalEnabled: +@@ -3681,7 +3681,7 @@ InputTypeDateTimeLocalEnabled: "PLATFORM(IOS_FAMILY)": true default: false WebKit: @@ -2040,7 +2034,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd default: false WebCore: default: false -@@ -3695,7 +3695,7 @@ InputTypeTimeEnabled: +@@ -3713,7 +3713,7 @@ InputTypeTimeEnabled: "PLATFORM(IOS_FAMILY)": true default: false WebKit: @@ -2049,7 +2043,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd default: false WebCore: default: false -@@ -3756,6 +3756,7 @@ InspectorMaximumResourcesContentSize: +@@ -3774,6 +3774,7 @@ InspectorMaximumResourcesContentSize: "PLATFORM(WPE)": 50 default: 200 @@ -2057,7 +2051,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd InspectorStartsAttached: type: bool status: embedder -@@ -3763,7 +3764,7 @@ InspectorStartsAttached: +@@ -3781,7 +3782,7 @@ InspectorStartsAttached: exposed: [ WebKit ] defaultValue: WebKit: @@ -2066,7 +2060,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd InspectorSupportsShowingCertificate: type: bool -@@ -5683,7 +5684,7 @@ PermissionsAPIEnabled: +@@ -5701,7 +5702,7 @@ PermissionsAPIEnabled: WebKitLegacy: default: false WebKit: @@ -2075,7 +2069,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd default: false WebCore: default: false -@@ -5762,6 +5763,19 @@ PitchCorrectionAlgorithm: +@@ -5780,6 +5781,19 @@ PitchCorrectionAlgorithm: WebCore: default: MediaPlayerEnums::PitchCorrectionAlgorithm::BestAllAround @@ -2095,7 +2089,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd PointerLockOptionsEnabled: type: bool status: stable -@@ -6344,7 +6358,7 @@ ScreenOrientationAPIEnabled: +@@ -6364,7 +6378,7 @@ ScreenOrientationAPIEnabled: WebKitLegacy: default: false WebKit: @@ -2104,15 +2098,15 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd WebCore: default: false sharedPreferenceForWebProcess: true -@@ -7806,6 +7820,7 @@ UseCGDisplayListsForDOMRendering: - default: true - sharedPreferenceForWebProcess: true +@@ -7847,6 +7861,7 @@ UseDamagingInformationForCompositing: + WebCore: + default: false +# Playwright: force-disable on Windows. UseGPUProcessForCanvasRenderingEnabled: type: bool status: stable -@@ -7818,7 +7833,7 @@ UseGPUProcessForCanvasRenderingEnabled: +@@ -7859,7 +7874,7 @@ UseGPUProcessForCanvasRenderingEnabled: defaultValue: WebKit: "ENABLE(GPU_PROCESS_BY_DEFAULT)": true @@ -2121,7 +2115,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd default: false UseGPUProcessForDOMRenderingEnabled: -@@ -7863,6 +7878,7 @@ UseGPUProcessForMediaEnabled: +@@ -7904,6 +7919,7 @@ UseGPUProcessForMediaEnabled: sharedPreferenceForWebProcess: true mediaPlaybackRelated: true @@ -2129,7 +2123,7 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd UseGPUProcessForWebGLEnabled: type: bool status: internal -@@ -7874,7 +7890,7 @@ UseGPUProcessForWebGLEnabled: +@@ -7915,7 +7931,7 @@ UseGPUProcessForWebGLEnabled: default: false WebKit: "ENABLE(GPU_PROCESS_BY_DEFAULT) && ENABLE(GPU_PROCESS_WEBGL_BY_DEFAULT)": true @@ -2139,10 +2133,10 @@ index 3437d1ccf6e9a78bb9a42158e54ba99f4d9d25f9..b67f7d77bdf4090ec9c29abd221562cd WebCore: "ENABLE(GPU_PROCESS_BY_DEFAULT) && ENABLE(GPU_PROCESS_WEBGL_BY_DEFAULT)": true diff --git a/Source/WTF/wtf/PlatformEnable.h b/Source/WTF/wtf/PlatformEnable.h -index a36395e83c53617c2f894695846f8798fc277624..f0b8dd0fd12b4cde24975c20015cee30292abf9d 100644 +index c7bb871d6471f8ad9144db5063f39e37db20a396..0c09abedbc001572341f30deabefe588e4938e71 100644 --- a/Source/WTF/wtf/PlatformEnable.h +++ b/Source/WTF/wtf/PlatformEnable.h -@@ -385,7 +385,7 @@ +@@ -381,7 +381,7 @@ // ORIENTATION_EVENTS should never get enabled on Desktop, only Mobile. #if !defined(ENABLE_ORIENTATION_EVENTS) @@ -2151,7 +2145,7 @@ index a36395e83c53617c2f894695846f8798fc277624..f0b8dd0fd12b4cde24975c20015cee30 #endif #if !defined(ENABLE_OVERFLOW_SCROLLING_TOUCH) -@@ -502,7 +502,7 @@ +@@ -498,7 +498,7 @@ #endif #if !defined(ENABLE_TOUCH_EVENTS) @@ -2161,10 +2155,10 @@ index a36395e83c53617c2f894695846f8798fc277624..f0b8dd0fd12b4cde24975c20015cee30 #if !defined(ENABLE_TOUCH_ACTION_REGIONS) diff --git a/Source/WTF/wtf/PlatformEnableCocoa.h b/Source/WTF/wtf/PlatformEnableCocoa.h -index 8304147ff102789180b2682eb64d599791528c93..af8cb85981bda7b91edfa21b6cc321849d93b909 100644 +index ab4579dd256f98d3984882e2a2f26a527993416e..3a529e1864e88ccf3ce2df47176b0215427eb4bc 100644 --- a/Source/WTF/wtf/PlatformEnableCocoa.h +++ b/Source/WTF/wtf/PlatformEnableCocoa.h -@@ -808,7 +808,7 @@ +@@ -795,7 +795,7 @@ #endif #if !defined(ENABLE_SEC_ITEM_SHIM) @@ -2174,10 +2168,10 @@ index 8304147ff102789180b2682eb64d599791528c93..af8cb85981bda7b91edfa21b6cc32184 #if !defined(ENABLE_SERVER_PRECONNECT) diff --git a/Source/WTF/wtf/PlatformHave.h b/Source/WTF/wtf/PlatformHave.h -index 9e91a26aac61faea9634328f9a46421a4b4b7c38..b70776d97be4025435dc3c0364105cb17c429f6a 100644 +index bd819cb4b1bef3c4758353827ee0de710708848e..40c923882da567889b48730d3ec3b809aa040b1a 100644 --- a/Source/WTF/wtf/PlatformHave.h +++ b/Source/WTF/wtf/PlatformHave.h -@@ -1189,7 +1189,8 @@ +@@ -1059,7 +1059,8 @@ #endif #if PLATFORM(MAC) @@ -2204,10 +2198,10 @@ index 007b8fe3292f326504013be8198ae020f7aacf35..1c722c473732ffe05fdb61010fa4417e namespace Unicode { diff --git a/Source/WebCore/DerivedSources.make b/Source/WebCore/DerivedSources.make -index 0f0341624503ae9744b71d3675dc96545371456a..cc73c79374f07fbf1f83e7075e53a3d99da0705c 100644 +index 6d76e72ddae832186e6f3609146ac29b91e77a85..18a0976d7c065968d688fb1f585b0022541bac04 100644 --- a/Source/WebCore/DerivedSources.make +++ b/Source/WebCore/DerivedSources.make -@@ -1229,6 +1229,10 @@ JS_BINDING_IDLS := \ +@@ -1230,6 +1230,10 @@ JS_BINDING_IDLS := \ $(WebCore)/dom/SubscriberCallback.idl \ $(WebCore)/dom/SubscriptionObserver.idl \ $(WebCore)/dom/SubscriptionObserverCallback.idl \ @@ -2218,7 +2212,7 @@ index 0f0341624503ae9744b71d3675dc96545371456a..cc73c79374f07fbf1f83e7075e53a3d9 $(WebCore)/dom/Text.idl \ $(WebCore)/dom/TextDecoder.idl \ $(WebCore)/dom/TextDecoderStream.idl \ -@@ -1829,9 +1833,6 @@ JS_BINDING_IDLS := \ +@@ -1830,9 +1834,6 @@ JS_BINDING_IDLS := \ ADDITIONAL_BINDING_IDLS = \ DocumentTouch.idl \ GestureEvent.idl \ @@ -2229,10 +2223,10 @@ index 0f0341624503ae9744b71d3675dc96545371456a..cc73c79374f07fbf1f83e7075e53a3d9 vpath %.in $(WEBKITADDITIONS_HEADER_SEARCH_PATHS) diff --git a/Source/WebCore/Modules/geolocation/Geolocation.cpp b/Source/WebCore/Modules/geolocation/Geolocation.cpp -index 7d0ca9a308e7aeaf132dccfddeae129fc8c9e093..0eeac0eec23fbc8c3df6c56d63603acc46e8590c 100644 +index 51d4e0b4cbc69d2c5c5f76b7063b54865b6fc2d0..d23693e52b96d579f0d828dd825c5dab8572e7c7 100644 --- a/Source/WebCore/Modules/geolocation/Geolocation.cpp +++ b/Source/WebCore/Modules/geolocation/Geolocation.cpp -@@ -362,8 +362,9 @@ bool Geolocation::shouldBlockGeolocationRequests() +@@ -374,8 +374,9 @@ bool Geolocation::shouldBlockGeolocationRequests() bool isSecure = SecurityOrigin::isSecure(document->url()) || document->isSecureContext(); bool hasMixedContent = !document->foundMixedContent().isEmpty(); bool isLocalOrigin = securityOrigin()->isLocal(); @@ -2244,10 +2238,10 @@ index 7d0ca9a308e7aeaf132dccfddeae129fc8c9e093..0eeac0eec23fbc8c3df6c56d63603acc } diff --git a/Source/WebCore/Modules/speech/cocoa/WebSpeechRecognizerTask.mm b/Source/WebCore/Modules/speech/cocoa/WebSpeechRecognizerTask.mm -index b2b0391c120d527a9ab4bc6daf8bff7ea5d03cf7..d490a95f89f21536fce4f403b86399160abefc23 100644 +index ba3ef589f1930904ffbdae7158fd59db13961cde..e227e731c8a1b6e4c2f2d5501ec1cfe0a5174710 100644 --- a/Source/WebCore/Modules/speech/cocoa/WebSpeechRecognizerTask.mm +++ b/Source/WebCore/Modules/speech/cocoa/WebSpeechRecognizerTask.mm -@@ -195,6 +195,7 @@ - (void)sendEndIfNeeded +@@ -197,6 +197,7 @@ - (void)sendEndIfNeeded - (void)speechRecognizer:(SFSpeechRecognizer *)speechRecognizer availabilityDidChange:(BOOL)available { @@ -2255,7 +2249,7 @@ index b2b0391c120d527a9ab4bc6daf8bff7ea5d03cf7..d490a95f89f21536fce4f403b8639916 ASSERT(isMainThread()); if (available || !_task) -@@ -208,6 +209,7 @@ - (void)speechRecognizer:(SFSpeechRecognizer *)speechRecognizer availabilityDidC +@@ -210,6 +211,7 @@ - (void)speechRecognizer:(SFSpeechRecognizer *)speechRecognizer availabilityDidC - (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didHypothesizeTranscription:(SFTranscription *)transcription { @@ -2263,7 +2257,7 @@ index b2b0391c120d527a9ab4bc6daf8bff7ea5d03cf7..d490a95f89f21536fce4f403b8639916 ASSERT(isMainThread()); [self sendSpeechStartIfNeeded]; -@@ -216,6 +218,7 @@ - (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didHypothesizeTran +@@ -218,6 +220,7 @@ - (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didHypothesizeTran - (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didFinishRecognition:(SFSpeechRecognitionResult *)recognitionResult { @@ -2271,7 +2265,7 @@ index b2b0391c120d527a9ab4bc6daf8bff7ea5d03cf7..d490a95f89f21536fce4f403b8639916 ASSERT(isMainThread()); if (task.state == SFSpeechRecognitionTaskStateCanceling || (!_doMultipleRecognitions && task.state == SFSpeechRecognitionTaskStateCompleted)) -@@ -229,6 +232,7 @@ - (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didFinishRecogniti +@@ -231,6 +234,7 @@ - (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didFinishRecogniti - (void)speechRecognitionTaskWasCancelled:(SFSpeechRecognitionTask *)task { @@ -2279,11 +2273,23 @@ index b2b0391c120d527a9ab4bc6daf8bff7ea5d03cf7..d490a95f89f21536fce4f403b8639916 ASSERT(isMainThread()); [self sendSpeechEndIfNeeded]; +diff --git a/Source/WebCore/PlatformWin.cmake b/Source/WebCore/PlatformWin.cmake +index 72b2846f2c82818fc9a64fd90b7cba0c0601e15f..22277ab6c3233f040852d9daf9becf7ba81d12ca 100644 +--- a/Source/WebCore/PlatformWin.cmake ++++ b/Source/WebCore/PlatformWin.cmake +@@ -217,6 +217,7 @@ if (USE_CAIRO) + platform/graphics/win/cairo/MediaPlayerPrivateMediaFoundationCairo.cpp + + platform/win/cairo/DragImageWinCairo.cpp ++ platform/win/DragImageWin.cpp + ) + elseif (USE_SKIA) + list(APPEND WebCore_SOURCES diff --git a/Source/WebCore/SourcesCocoa.txt b/Source/WebCore/SourcesCocoa.txt -index 06a9accfc8e6c46493733663b5d76b07fc80db22..4946d012d166c84b25d4d954266c4dc528f7d8ad 100644 +index f09f2e709ee13060d25d4419c05affd987e6328f..16a6491a03dc6aeaf5bfdb4c5b75c3bd6dcd6944 100644 --- a/Source/WebCore/SourcesCocoa.txt +++ b/Source/WebCore/SourcesCocoa.txt -@@ -734,3 +734,9 @@ testing/cocoa/WebViewVisualIdentificationOverlay.mm +@@ -732,3 +732,9 @@ testing/cocoa/WebViewVisualIdentificationOverlay.mm platform/graphics/angle/GraphicsContextGLANGLE.cpp @no-unify platform/graphics/cocoa/GraphicsContextGLCocoa.mm @no-unify platform/graphics/cv/GraphicsContextGLCVCocoa.mm @no-unify @@ -2294,13 +2300,13 @@ index 06a9accfc8e6c46493733663b5d76b07fc80db22..4946d012d166c84b25d4d954266c4dc5 +JSTouchList.cpp +// Playwright end diff --git a/Source/WebCore/SourcesGTK.txt b/Source/WebCore/SourcesGTK.txt -index 2b22eb2071e32741cb1383601466e537dca917f2..4da3cb8c1f1247bbc9886b060cd2d53047ca6572 100644 +index 393fc726c204fc3037287d67cc6b7aa9a5a9c4c3..7e45bf78bb7f0688cf3e09449728ad32e2be3870 100644 --- a/Source/WebCore/SourcesGTK.txt +++ b/Source/WebCore/SourcesGTK.txt -@@ -112,3 +112,10 @@ platform/unix/LoggingUnix.cpp - platform/unix/SharedMemoryUnix.cpp - +@@ -113,3 +113,10 @@ platform/unix/SharedMemoryUnix.cpp platform/xdg/MIMETypeRegistryXdg.cpp + + platform/xr/openxr/PlatformXROpenXR.cpp + +// Playwright: begin. +JSSpeechSynthesisErrorCode.cpp @@ -2309,7 +2315,7 @@ index 2b22eb2071e32741cb1383601466e537dca917f2..4da3cb8c1f1247bbc9886b060cd2d530 +JSSpeechSynthesisEventInit.cpp +// Playwright: end. diff --git a/Source/WebCore/SourcesWPE.txt b/Source/WebCore/SourcesWPE.txt -index ce3cf51287e5891289bd23580084b8137ee4276b..c46e4e0c6faaca888e3ea62afd0e16d6f4cfda35 100644 +index f5fe901c57ba84d42c8ee3be47c404069ac6fed8..599a40b75fdb63306f706dbb7ff845b9e29a473a 100644 --- a/Source/WebCore/SourcesWPE.txt +++ b/Source/WebCore/SourcesWPE.txt @@ -48,6 +48,8 @@ editing/glib/WebContentReaderGLib.cpp @@ -2321,14 +2327,13 @@ index ce3cf51287e5891289bd23580084b8137ee4276b..c46e4e0c6faaca888e3ea62afd0e16d6 page/linux/ResourceUsageOverlayLinux.cpp page/linux/ResourceUsageThreadLinux.cpp -@@ -97,3 +99,13 @@ platform/wpe/PasteboardWPE.cpp - platform/wpe/PlatformScreenWPE.cpp - +@@ -99,3 +101,12 @@ platform/wpe/PlatformScreenWPE.cpp platform/xdg/MIMETypeRegistryXdg.cpp + + platform/xr/openxr/PlatformXROpenXR.cpp + +// Playwright: begin. +platform/wpe/DragDataWPE.cpp -+platform/wpe/DragImageWPE.cpp + +JSSpeechSynthesisErrorCode.cpp +JSSpeechSynthesisErrorEvent.cpp @@ -2336,12 +2341,12 @@ index ce3cf51287e5891289bd23580084b8137ee4276b..c46e4e0c6faaca888e3ea62afd0e16d6 +JSSpeechSynthesisEventInit.cpp +// Playwright: end. diff --git a/Source/WebCore/WebCore.xcodeproj/project.pbxproj b/Source/WebCore/WebCore.xcodeproj/project.pbxproj -index e522afd2d6f038d2a2c9804313d1d8e75c63e914..03f521d898bd7d80a94e7e3c0fc4c904d279a0ba 100644 +index 612c2492b160e840f942b1b5bef3faf995f411cf..221ffcfc32818d1d2839aa984eefc43f60d2ba31 100644 --- a/Source/WebCore/WebCore.xcodeproj/project.pbxproj +++ b/Source/WebCore/WebCore.xcodeproj/project.pbxproj -@@ -6452,6 +6452,13 @@ - EE0C7E042CE845CB0043DAF8 /* CSSPositionTryRule.h in Headers */ = {isa = PBXBuildFile; fileRef = EE0C7E002CE845CB0043DAF8 /* CSSPositionTryRule.h */; }; - EE0D3C492D2F4B4C00072978 /* StageModeOperations.h in Headers */ = {isa = PBXBuildFile; fileRef = EE0D3C482D2F4AE600072978 /* StageModeOperations.h */; settings = {ATTRIBUTES = (Private, ); }; }; +@@ -6503,6 +6503,13 @@ + EE62BD9D2DE12C1B006C9A05 /* ResolvedScopedName.h in Headers */ = {isa = PBXBuildFile; fileRef = EE62BD9B2DE12BD4006C9A05 /* ResolvedScopedName.h */; settings = {ATTRIBUTES = (Private, ); }; }; + EEE349082DE0061C00A7D4BB /* StyleScopeIdentifier.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE349072DE005FC00A7D4BB /* StyleScopeIdentifier.h */; settings = {ATTRIBUTES = (Private, ); }; }; EFCC6C8F20FE914400A2321B /* CanvasActivityRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = EFCC6C8D20FE914000A2321B /* CanvasActivityRecord.h */; settings = {ATTRIBUTES = (Private, ); }; }; + F050E16823AC9C080011CE47 /* PlatformTouchEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = F050E16623AC9C070011CE47 /* PlatformTouchEvent.h */; settings = {ATTRIBUTES = (Private, ); }; }; + F050E16A23AD660C0011CE47 /* Touch.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F050E16923AD660C0011CE47 /* Touch.cpp */; }; @@ -2353,8 +2358,8 @@ index e522afd2d6f038d2a2c9804313d1d8e75c63e914..03f521d898bd7d80a94e7e3c0fc4c904 F12171F616A8CF0B000053CA /* WebVTTElement.h in Headers */ = {isa = PBXBuildFile; fileRef = F12171F416A8BC63000053CA /* WebVTTElement.h */; }; F32BDCD92363AACA0073B6AE /* UserGestureEmulationScope.h in Headers */ = {isa = PBXBuildFile; fileRef = F32BDCD72363AACA0073B6AE /* UserGestureEmulationScope.h */; }; F344C7141125B82C00F26EEE /* InspectorFrontendClient.h in Headers */ = {isa = PBXBuildFile; fileRef = F344C7121125B82C00F26EEE /* InspectorFrontendClient.h */; settings = {ATTRIBUTES = (Private, ); }; }; -@@ -21141,6 +21148,14 @@ - EE7A169F2C607BFA0057B563 /* StartViewTransitionOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StartViewTransitionOptions.h; sourceTree = ""; }; +@@ -21257,6 +21264,14 @@ + EEE349072DE005FC00A7D4BB /* StyleScopeIdentifier.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StyleScopeIdentifier.h; sourceTree = ""; }; EFB7287B2124C73D005C2558 /* CanvasActivityRecord.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CanvasActivityRecord.cpp; sourceTree = ""; }; EFCC6C8D20FE914000A2321B /* CanvasActivityRecord.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CanvasActivityRecord.h; sourceTree = ""; }; + F050E16623AC9C070011CE47 /* PlatformTouchEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlatformTouchEvent.h; sourceTree = ""; }; @@ -2368,7 +2373,7 @@ index e522afd2d6f038d2a2c9804313d1d8e75c63e914..03f521d898bd7d80a94e7e3c0fc4c904 F12171F316A8BC63000053CA /* WebVTTElement.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = WebVTTElement.cpp; sourceTree = ""; }; F12171F416A8BC63000053CA /* WebVTTElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebVTTElement.h; sourceTree = ""; }; F32BDCD52363AAC90073B6AE /* UserGestureEmulationScope.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = UserGestureEmulationScope.cpp; sourceTree = ""; }; -@@ -28928,6 +28943,11 @@ +@@ -29046,6 +29061,11 @@ BC4A5324256055590028C592 /* TextDirectionSubmenuInclusionBehavior.h */, 2D4F96F11A1ECC240098BF88 /* TextIndicator.cpp */, 2D4F96F21A1ECC240098BF88 /* TextIndicator.h */, @@ -2380,7 +2385,7 @@ index e522afd2d6f038d2a2c9804313d1d8e75c63e914..03f521d898bd7d80a94e7e3c0fc4c904 F48570A42644C76D00C05F71 /* TranslationContextMenuInfo.h */, F4E1965F21F26E4E00285078 /* UndoItem.cpp */, 2ECDBAD521D8906300F00ECD /* UndoItem.h */, -@@ -35916,6 +35936,8 @@ +@@ -36194,6 +36214,8 @@ 29E4D8DF16B0940F00C84704 /* PlatformSpeechSynthesizer.h */, 1AD8F81A11CAB9E900E93E54 /* PlatformStrategies.cpp */, 1AD8F81911CAB9E900E93E54 /* PlatformStrategies.h */, @@ -2389,7 +2394,7 @@ index e522afd2d6f038d2a2c9804313d1d8e75c63e914..03f521d898bd7d80a94e7e3c0fc4c904 FE3DC9932D0C063C0021B6FC /* PlatformTZoneImpls.cpp */, 0FD7C21D23CE41E30096D102 /* PlatformWheelEvent.cpp */, 935C476A09AC4D4F00A6AAB4 /* PlatformWheelEvent.h */, -@@ -38777,6 +38799,7 @@ +@@ -39050,6 +39072,7 @@ AD6E71AB1668899D00320C13 /* DocumentSharedObjectPool.h */, 6BDB5DC1227BD3B800919770 /* DocumentStorageAccess.cpp */, 6BDB5DC0227BD3B800919770 /* DocumentStorageAccess.h */, @@ -2397,7 +2402,7 @@ index e522afd2d6f038d2a2c9804313d1d8e75c63e914..03f521d898bd7d80a94e7e3c0fc4c904 7CE7FA5B1EF882300060C9D6 /* DocumentTouch.cpp */, 7CE7FA591EF882300060C9D6 /* DocumentTouch.h */, A8185F3209765765005826D9 /* DocumentType.cpp */, -@@ -43729,6 +43752,8 @@ +@@ -44014,6 +44037,8 @@ F4E90A3C2B52038E002DA469 /* PlatformTextAlternatives.h in Headers */, 0F7D07331884C56C00B4AF86 /* PlatformTextTrack.h in Headers */, 074E82BB18A69F0E007EF54C /* PlatformTimeRanges.h in Headers */, @@ -2406,7 +2411,7 @@ index e522afd2d6f038d2a2c9804313d1d8e75c63e914..03f521d898bd7d80a94e7e3c0fc4c904 CDD08ABD277E542600EA3755 /* PlatformTrackConfiguration.h in Headers */, CD1F9B022700323D00617EB6 /* PlatformVideoColorPrimaries.h in Headers */, CD1F9B01270020B700617EB6 /* PlatformVideoColorSpace.h in Headers */, -@@ -45097,6 +45122,7 @@ +@@ -45423,6 +45448,7 @@ 0F54DD081881D5F5003EEDBB /* Touch.h in Headers */, 71B7EE0D21B5C6870031C1EF /* TouchAction.h in Headers */, 0F54DD091881D5F5003EEDBB /* TouchEvent.h in Headers */, @@ -2414,7 +2419,7 @@ index e522afd2d6f038d2a2c9804313d1d8e75c63e914..03f521d898bd7d80a94e7e3c0fc4c904 0F54DD0A1881D5F5003EEDBB /* TouchList.h in Headers */, 070334D71459FFD5008D8D45 /* TrackBase.h in Headers */, BE88E0C21715CE2600658D98 /* TrackListBase.h in Headers */, -@@ -46300,6 +46326,8 @@ +@@ -46627,6 +46653,8 @@ 2D22830323A8470700364B7E /* CursorMac.mm in Sources */, 5CBD59592280E926002B22AA /* CustomHeaderFields.cpp in Sources */, 07E4BDBF2A3A5FAB000D5509 /* DictationCaretAnimator.cpp in Sources */, @@ -2423,7 +2428,7 @@ index e522afd2d6f038d2a2c9804313d1d8e75c63e914..03f521d898bd7d80a94e7e3c0fc4c904 7CE6CBFD187F394900D46BF5 /* FormatConverter.cpp in Sources */, 4667EA3E2968D9DA00BAB1E2 /* GameControllerHapticEffect.mm in Sources */, 46FE73D32968E52000B8064C /* GameControllerHapticEngines.mm in Sources */, -@@ -46390,6 +46418,9 @@ +@@ -46718,6 +46746,9 @@ CE88EE262414467B007F29C2 /* TextAlternativeWithRange.mm in Sources */, BE39137129B267F500FA5D4F /* TextTransformCocoa.cpp in Sources */, 51DF6D800B92A18E00C2DC85 /* ThreadCheck.mm in Sources */, @@ -2434,7 +2439,7 @@ index e522afd2d6f038d2a2c9804313d1d8e75c63e914..03f521d898bd7d80a94e7e3c0fc4c904 538EC8021F96AF81004D22A8 /* UnifiedSource1.cpp in Sources */, 538EC8051F96AF81004D22A8 /* UnifiedSource2-mm.mm in Sources */, diff --git a/Source/WebCore/accessibility/AccessibilityObject.cpp b/Source/WebCore/accessibility/AccessibilityObject.cpp -index 3f19caf891dfa1cc70aa630f4e82c405825ab40c..62b2978c8d5e8af5e337d8b27c1d2ad938b773fe 100644 +index 9587b90007ab8ee8ab32f7f86219732f11d457bf..81e09081c42db3c905f1adb2f917b0bca32c24c2 100644 --- a/Source/WebCore/accessibility/AccessibilityObject.cpp +++ b/Source/WebCore/accessibility/AccessibilityObject.cpp @@ -72,6 +72,7 @@ @@ -2445,8 +2450,8 @@ index 3f19caf891dfa1cc70aa630f4e82c405825ab40c..62b2978c8d5e8af5e337d8b27c1d2ad9 #include "LocalFrame.h" #include "LocalizedStrings.h" #include "MathMLNames.h" -@@ -3931,7 +3932,12 @@ AccessibilityObjectInclusion AccessibilityObject::defaultObjectInclusion() const - if (roleValue() == AccessibilityRole::ApplicationDialog) +@@ -3998,7 +3999,12 @@ AccessibilityObjectInclusion AccessibilityObject::defaultObjectInclusion() const + if (role() == AccessibilityRole::ApplicationDialog) return AccessibilityObjectInclusion::IncludeObject; - return accessibilityPlatformIncludesObject(); @@ -2460,7 +2465,7 @@ index 3f19caf891dfa1cc70aa630f4e82c405825ab40c..62b2978c8d5e8af5e337d8b27c1d2ad9 bool AccessibilityObject::isWithinHiddenWebArea() const diff --git a/Source/WebCore/bindings/js/WebCoreBuiltinNames.h b/Source/WebCore/bindings/js/WebCoreBuiltinNames.h -index 158dc6af1464896ac4c4727c52581729ac132352..b8bd22a7d80d08d6dd56ea1bc10addbe3f839b6d 100644 +index c4b32aaefc75bdef3b52f2469786f9abe11fb8d4..6d84e95ff7f766a2e26c1414be92af89fa7d73cf 100644 --- a/Source/WebCore/bindings/js/WebCoreBuiltinNames.h +++ b/Source/WebCore/bindings/js/WebCoreBuiltinNames.h @@ -190,6 +190,8 @@ namespace WebCore { @@ -2473,10 +2478,10 @@ index 158dc6af1464896ac4c4727c52581729ac132352..b8bd22a7d80d08d6dd56ea1bc10addbe macro(DynamicsCompressorNode) \ macro(ElementInternals) \ diff --git a/Source/WebCore/css/query/MediaQueryFeatures.cpp b/Source/WebCore/css/query/MediaQueryFeatures.cpp -index aec43490053f95341ef979385b5e6c1daf03a090..4e44b7e341c8247f916f8597092248d8ec884518 100644 +index 5354042180f7c7cf5754142ffeeb9908166074bc..2b4d7423a3f4726c5e3ee93078184aa9b1d97649 100644 --- a/Source/WebCore/css/query/MediaQueryFeatures.cpp +++ b/Source/WebCore/css/query/MediaQueryFeatures.cpp -@@ -498,7 +498,11 @@ static const IdentifierSchema& forcedColorsFeatureSchema() +@@ -403,7 +403,11 @@ static const IdentifierSchema& forcedColorsFeatureSchema() "forced-colors"_s, FixedVector { CSSValueNone, CSSValueActive }, OptionSet(), @@ -2489,7 +2494,7 @@ index aec43490053f95341ef979385b5e6c1daf03a090..4e44b7e341c8247f916f8597092248d8 return MatchingIdentifiers { CSSValueNone }; } }; -@@ -686,6 +690,9 @@ static const IdentifierSchema& prefersReducedMotionFeatureSchema() +@@ -591,6 +595,9 @@ static const IdentifierSchema& prefersReducedMotionFeatureSchema() [](auto& context) { bool userPrefersReducedMotion = [&] { Ref frame = *context.document->frame(); @@ -2500,7 +2505,7 @@ index aec43490053f95341ef979385b5e6c1daf03a090..4e44b7e341c8247f916f8597092248d8 case ForcedAccessibilityValue::On: return true; diff --git a/Source/WebCore/dom/DataTransfer.cpp b/Source/WebCore/dom/DataTransfer.cpp -index c6f2dca0d4aede2bea015d1cca45dff434425938..e6f41af39befe69f47d0e0953f7d689c338ca184 100644 +index c0b9d058536120b4c368ab8094c16f19a5d2acba..37e7e016f5862bab19d750d06bd72b100c775353 100644 --- a/Source/WebCore/dom/DataTransfer.cpp +++ b/Source/WebCore/dom/DataTransfer.cpp @@ -523,6 +523,14 @@ Ref DataTransfer::createForDrag(const Document& document) @@ -2519,7 +2524,7 @@ index c6f2dca0d4aede2bea015d1cca45dff434425938..e6f41af39befe69f47d0e0953f7d689c { auto dataTransfer = adoptRef(*new DataTransfer(StoreMode::ReadWrite, makeUnique(), Type::DragAndDropData)); diff --git a/Source/WebCore/dom/DataTransfer.h b/Source/WebCore/dom/DataTransfer.h -index 34ef2f454a732d39acae04987584cab5638b8c60..5e7b788612718dffe3423c89d96141b5b53621fb 100644 +index 315635014cf133628d72942eb230df4e8cad036c..5424ad948028c192d9fc165cde0102e15ad65b56 100644 --- a/Source/WebCore/dom/DataTransfer.h +++ b/Source/WebCore/dom/DataTransfer.h @@ -92,6 +92,9 @@ public: @@ -2729,7 +2734,7 @@ index d0a3d5c048647b07772e1581c76c4eb60ecf41b0..bec324636991079264e620c0dfdaf984 #endif // USE(LIBWPE) diff --git a/Source/WebCore/html/FileInputType.cpp b/Source/WebCore/html/FileInputType.cpp -index d5234a678a77e24ce0a95fe08740416ab8acbadb..c9a42fd354e88c5a8c3ee53974442ec9d4cb1224 100644 +index 64137520fb575aac59fbb4a8fc8509cbdd8cc04e..5bdac5378686cefef1e05f4aa01a49729c3fd398 100644 --- a/Source/WebCore/html/FileInputType.cpp +++ b/Source/WebCore/html/FileInputType.cpp @@ -38,6 +38,7 @@ @@ -2753,7 +2758,7 @@ index d5234a678a77e24ce0a95fe08740416ab8acbadb..c9a42fd354e88c5a8c3ee53974442ec9 return; diff --git a/Source/WebCore/inspector/InspectorController.cpp b/Source/WebCore/inspector/InspectorController.cpp -index 48efc5356601313e907846283f753e44d6a0f983..f153fd7da52da98d033e4070d79c64d4a38ff197 100644 +index af0482d2d06db91a7964b7662dd3379804062fff..49ed24c301cecabeb21ac5f3d06d2777218cd538 100644 --- a/Source/WebCore/inspector/InspectorController.cpp +++ b/Source/WebCore/inspector/InspectorController.cpp @@ -296,6 +296,8 @@ void InspectorController::disconnectFrontend(FrontendChannel& frontendChannel) @@ -2821,7 +2826,7 @@ index 48efc5356601313e907846283f753e44d6a0f983..f153fd7da52da98d033e4070d79c64d4 + } // namespace WebCore diff --git a/Source/WebCore/inspector/InspectorController.h b/Source/WebCore/inspector/InspectorController.h -index 4f5c1e836876710a554455ec53733f72db63de58..774c4a66af84664a7a83ba0790d147f7bbb4236a 100644 +index d56afdb6e48840802c5a4f28b9a954695f09965d..7c3223c9f57bc45f4109325433ac4f5e745eb986 100644 --- a/Source/WebCore/inspector/InspectorController.h +++ b/Source/WebCore/inspector/InspectorController.h @@ -106,6 +106,12 @@ public: @@ -2844,7 +2849,7 @@ index 4f5c1e836876710a554455ec53733f72db63de58..774c4a66af84664a7a83ba0790d147f7 + void runLoopWhilePaused(); WeakRef m_page; - Ref m_instrumentingAgents; + const Ref m_instrumentingAgents; @@ -159,6 +166,7 @@ private: bool m_isAutomaticInspection { false }; bool m_pauseAfterInitialization = { false }; @@ -3374,7 +3379,7 @@ index c028341e84e59a6b1b16107fd74feb21f70b12ab..d385418ac34e8f315f201801a2c65226 + } diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp -index 94492280fc724f299655a5df34492f929d82c57c..af0aafa701a9c697650aad2d86ddcf6b3022dcc5 100644 +index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a93705e1a0 100644 --- a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp +++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp @@ -54,6 +54,7 @@ @@ -3743,7 +3748,7 @@ index 94492280fc724f299655a5df34492f929d82c57c..af0aafa701a9c697650aad2d86ddcf6b + } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.h b/Source/WebCore/inspector/agents/InspectorDOMAgent.h -index 3496e0037572430b993ea91704501a742dd15627..9eb0ce0094fdb2ab9ca94bb5cddb082f201bf4cf 100644 +index 185ba93a265317bf876cc5fedd5f3d5cf3a8757e..664fc4f47f0540e5b84d96ba4c6a530d44aacf25 100644 --- a/Source/WebCore/inspector/agents/InspectorDOMAgent.h +++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.h @@ -59,6 +59,7 @@ namespace WebCore { @@ -3816,7 +3821,7 @@ index 3496e0037572430b993ea91704501a742dd15627..9eb0ce0094fdb2ab9ca94bb5cddb082f void discardBindings(); diff --git a/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp b/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp -index f3a0bdf189aed455ceb84f4023c2dbfda8e5dc16..25d54fc33be2ccd42565cb7ddf879344ab8aa3f2 100644 +index dfb469de2c5dcaa39b03ef6c188eda34d869401c..4d1fb3b02d63c8b425b50b92c25f334334e8ac86 100644 --- a/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp +++ b/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp @@ -58,6 +58,7 @@ @@ -3933,7 +3938,7 @@ index f3a0bdf189aed455ceb84f4023c2dbfda8e5dc16..25d54fc33be2ccd42565cb7ddf879344 { return startsWithLettersIgnoringASCIICase(mimeType, "text/"_s) diff --git a/Source/WebCore/inspector/agents/InspectorNetworkAgent.h b/Source/WebCore/inspector/agents/InspectorNetworkAgent.h -index de6b2dd844943074c5a383c7b9b8ccba1c96419a..7a3404f2380b5e62f1c0523a70f8ef442014759d 100644 +index 66da07b880259dd1388703cb6c8c7b10d63c32d7..ff00bc16dd84f34056fe0b36896c0049d8fa9a49 100644 --- a/Source/WebCore/inspector/agents/InspectorNetworkAgent.h +++ b/Source/WebCore/inspector/agents/InspectorNetworkAgent.h @@ -35,6 +35,8 @@ @@ -3970,7 +3975,7 @@ index de6b2dd844943074c5a383c7b9b8ccba1c96419a..7a3404f2380b5e62f1c0523a70f8ef44 } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp -index cc649f41559f905102d7a8785190f3157d3e6dea..ee7fee8a4ac224d27dd40d04be0803ff89720232 100644 +index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e19a9056d 100644 --- a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp +++ b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp @@ -32,19 +32,27 @@ @@ -4053,7 +4058,7 @@ index cc649f41559f905102d7a8785190f3157d3e6dea..ee7fee8a4ac224d27dd40d04be0803ff { if (buffer.data()) { @@ -348,6 +375,7 @@ InspectorPageAgent::InspectorPageAgent(PageAgentContext& context, InspectorClien - , m_frontendDispatcher(makeUnique(context.frontendRouter)) + , m_frontendDispatcher(makeUniqueRef(context.frontendRouter)) , m_backendDispatcher(Inspector::PageBackendDispatcher::create(context.backendDispatcher, this)) , m_inspectedPage(context.inspectedPage) + , m_injectedScriptManager(context.injectedScriptManager) @@ -4457,7 +4462,7 @@ index cc649f41559f905102d7a8785190f3157d3e6dea..ee7fee8a4ac224d27dd40d04be0803ff +Protocol::ErrorStringOr InspectorPageAgent::insertText(const String& text) +{ + UserGestureIndicator indicator { IsProcessingUserGesture::Yes }; -+ RefPtr frame = m_inspectedPage->checkedFocusController()->focusedOrMainFrame(); ++ RefPtr frame = m_inspectedPage->focusController().focusedOrMainFrame(); + if (!frame) + return { }; + @@ -4475,7 +4480,7 @@ index cc649f41559f905102d7a8785190f3157d3e6dea..ee7fee8a4ac224d27dd40d04be0803ff + String computedRoleString = axObject->computedRoleString(); + if (!computedRoleString.isEmpty()) + return computedRoleString; -+ AccessibilityRole role = axObject->roleValue(); ++ AccessibilityRole role = axObject->role(); + switch(role) { + case AccessibilityRole::Application: + return "Application"_s; @@ -4547,8 +4552,6 @@ index cc649f41559f905102d7a8785190f3157d3e6dea..ee7fee8a4ac224d27dd40d04be0803ff + return "Feed"_s; + case AccessibilityRole::Figure: + return "Figure"_s; -+ case AccessibilityRole::Footer: -+ return "Footer"_s; + case AccessibilityRole::Footnote: + return "Footnote"_s; + case AccessibilityRole::Form: @@ -4669,6 +4672,10 @@ index cc649f41559f905102d7a8785190f3157d3e6dea..ee7fee8a4ac224d27dd40d04be0803ff + return "ScrollBar"_s; + case AccessibilityRole::SearchField: + return "SearchField"_s; ++ case AccessibilityRole::SectionFooter: ++ return "SectionFooter"_s; ++ case AccessibilityRole::SectionHeader: ++ return "SectionHeader"_s; + case AccessibilityRole::Slider: + return "Slider"_s; + case AccessibilityRole::SliderThumb: @@ -4743,8 +4750,6 @@ index cc649f41559f905102d7a8785190f3157d3e6dea..ee7fee8a4ac224d27dd40d04be0803ff + return "WebApplication"_s; + case AccessibilityRole::WebArea: + return "WebArea"_s; -+ case AccessibilityRole::WebCoreLink: -+ return "WebCoreLink"_s; + }; + return "Unknown"_s; +} @@ -4947,7 +4952,7 @@ index cc649f41559f905102d7a8785190f3157d3e6dea..ee7fee8a4ac224d27dd40d04be0803ff } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.h b/Source/WebCore/inspector/agents/InspectorPageAgent.h -index 7daa8d1d5c96afe1829aa21ccb8ed1b8ebcc3861..da57245795d7cb287daaaaf5d09a523c0f88fcc7 100644 +index 169a3023f68227a6ab6538dc6b1271882154deb9..b300be1ff6f278a79601613f76266c06c5ac92a8 100644 --- a/Source/WebCore/inspector/agents/InspectorPageAgent.h +++ b/Source/WebCore/inspector/agents/InspectorPageAgent.h @@ -32,8 +32,10 @@ @@ -5073,7 +5078,7 @@ index 7daa8d1d5c96afe1829aa21ccb8ed1b8ebcc3861..da57245795d7cb287daaaaf5d09a523c Ref protectedOverlay() const; @@ -173,17 +205,22 @@ private: - RefPtr m_backendDispatcher; + const Ref m_backendDispatcher; WeakRef m_inspectedPage; + Inspector::InjectedScriptManager& m_injectedScriptManager; @@ -5097,7 +5102,7 @@ index 7daa8d1d5c96afe1829aa21ccb8ed1b8ebcc3861..da57245795d7cb287daaaaf5d09a523c } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/page/PageRuntimeAgent.cpp b/Source/WebCore/inspector/agents/page/PageRuntimeAgent.cpp -index 0cc351959aaa96bd427ecbf75dd2ba51e730fc30..fb2b6c842a2e2b819ce746403e33550c0d4e2440 100644 +index 2f591841f3db6618a2936802d5e4e9604cd25963..639bf063a684a361e71b04c7a17c4dfc2541513a 100644 --- a/Source/WebCore/inspector/agents/page/PageRuntimeAgent.cpp +++ b/Source/WebCore/inspector/agents/page/PageRuntimeAgent.cpp @@ -34,6 +34,7 @@ @@ -5229,7 +5234,7 @@ index 0cc351959aaa96bd427ecbf75dd2ba51e730fc30..fb2b6c842a2e2b819ce746403e33550c // Always send the main world first. diff --git a/Source/WebCore/inspector/agents/page/PageRuntimeAgent.h b/Source/WebCore/inspector/agents/page/PageRuntimeAgent.h -index ab49ddd13fc6e1ed967cf501afb0230c1e795159..5ea038212cedc1a7b250588b616225edad6f643c 100644 +index 6544b61b15956b218e97814b0ceb7250809f8ed3..c6657031bcd27573533a34be62e0ea0d8441a7e4 100644 --- a/Source/WebCore/inspector/agents/page/PageRuntimeAgent.h +++ b/Source/WebCore/inspector/agents/page/PageRuntimeAgent.h @@ -38,6 +38,7 @@ @@ -5263,7 +5268,7 @@ index ab49ddd13fc6e1ed967cf501afb0230c1e795159..5ea038212cedc1a7b250588b616225ed } // namespace WebCore diff --git a/Source/WebCore/loader/CookieJar.h b/Source/WebCore/loader/CookieJar.h -index 8fb27c1045b8073d1487d5b61ccdec23a395bfd1..5008052f587ca4ba90da973c539188deb9551621 100644 +index 5dbca10d65d0426d90c743b789e18c3786025817..56b1aa222343649be7c28e104e6ae1dde598e59f 100644 --- a/Source/WebCore/loader/CookieJar.h +++ b/Source/WebCore/loader/CookieJar.h @@ -48,6 +48,7 @@ class NetworkStorageSession; @@ -5285,10 +5290,10 @@ index 8fb27c1045b8073d1487d5b61ccdec23a395bfd1..5008052f587ca4ba90da973c539188de protected: static SameSiteInfo sameSiteInfo(const Document&, IsForDOMCookieAccess = IsForDOMCookieAccess::No); diff --git a/Source/WebCore/loader/DocumentLoader.cpp b/Source/WebCore/loader/DocumentLoader.cpp -index 541963eb00a1e5700da683be152543ac57f177a1..bd0debb9cdf2fac02bcf0243571a4a668be9decc 100644 +index c63569ec7dbb402d261f16a5b5febdf428cfe749..b5cff81dcf8fbcc5a0a8cb6f05857f4382b090be 100644 --- a/Source/WebCore/loader/DocumentLoader.cpp +++ b/Source/WebCore/loader/DocumentLoader.cpp -@@ -774,8 +774,10 @@ void DocumentLoader::willSendRequest(ResourceRequest&& newRequest, const Resourc +@@ -775,8 +775,10 @@ void DocumentLoader::willSendRequest(ResourceRequest&& newRequest, const Resourc if (!didReceiveRedirectResponse) return completionHandler(WTFMove(newRequest)); @@ -5299,7 +5304,7 @@ index 541963eb00a1e5700da683be152543ac57f177a1..bd0debb9cdf2fac02bcf0243571a4a66 switch (navigationPolicyDecision) { case NavigationPolicyDecision::IgnoreLoad: case NavigationPolicyDecision::LoadWillContinueInAnotherProcess: -@@ -1572,11 +1574,17 @@ void DocumentLoader::detachFromFrame(LoadWillContinueInAnotherProcess loadWillCo +@@ -1592,11 +1594,17 @@ void DocumentLoader::detachFromFrame(LoadWillContinueInAnotherProcess loadWillCo if (auto navigationID = std::exchange(m_navigationID, { })) frame->loader().client().documentLoaderDetached(*navigationID, loadWillContinueInAnotherProcess); @@ -5320,10 +5325,10 @@ index 541963eb00a1e5700da683be152543ac57f177a1..bd0debb9cdf2fac02bcf0243571a4a66 { m_navigationID = navigationID; diff --git a/Source/WebCore/loader/DocumentLoader.h b/Source/WebCore/loader/DocumentLoader.h -index a3e5891cc44903ec74da16164904c12673498d97..9f8566a8c1cc20c1d2a495c9765f1c522890e993 100644 +index 061494ea45af99d0516cf16af3f11c25a180e970..eff67613ea81446d7f53ecf2e66d5b428b4c6bab 100644 --- a/Source/WebCore/loader/DocumentLoader.h +++ b/Source/WebCore/loader/DocumentLoader.h -@@ -218,6 +218,8 @@ public: +@@ -220,6 +220,8 @@ public: WEBCORE_EXPORT virtual void detachFromFrame(LoadWillContinueInAnotherProcess); @@ -5333,10 +5338,10 @@ index a3e5891cc44903ec74da16164904c12673498d97..9f8566a8c1cc20c1d2a495c9765f1c52 WEBCORE_EXPORT RefPtr protectedFrameLoader() const; WEBCORE_EXPORT SubresourceLoader* mainResourceLoader() const; diff --git a/Source/WebCore/loader/FrameLoader.cpp b/Source/WebCore/loader/FrameLoader.cpp -index 0974e98f02b59150df7054af753a3c3bb1d3b0e9..559a8532162d4c9e106c1059dfbf50fc63ee9326 100644 +index 1263d20c197bc21cb5a4098a07493943fd2830e4..fbcfaa000c4095c9ba9257dfb2229424229ee670 100644 --- a/Source/WebCore/loader/FrameLoader.cpp +++ b/Source/WebCore/loader/FrameLoader.cpp -@@ -1325,6 +1325,7 @@ void FrameLoader::loadInSameDocument(URL url, RefPtr stat +@@ -1332,6 +1332,7 @@ void FrameLoader::loadInSameDocument(URL url, RefPtr stat } m_client->dispatchDidNavigateWithinPage(); @@ -5344,7 +5349,7 @@ index 0974e98f02b59150df7054af753a3c3bb1d3b0e9..559a8532162d4c9e106c1059dfbf50fc document->statePopped(stateObject ? stateObject.releaseNonNull() : SerializedScriptValue::nullValue()); m_client->dispatchDidPopStateWithinPage(); -@@ -1867,6 +1868,7 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t +@@ -1874,6 +1875,7 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t const String& httpMethod = loader->request().httpMethod(); if (shouldPerformFragmentNavigation(isFormSubmission, httpMethod, policyChecker().loadType(), newURL)) { @@ -5352,7 +5357,7 @@ index 0974e98f02b59150df7054af753a3c3bb1d3b0e9..559a8532162d4c9e106c1059dfbf50fc RefPtr oldDocumentLoader = m_documentLoader; NavigationAction action { frame->protectedDocument().releaseNonNull(), loader->request(), InitiatedByMainFrame::Unknown, loader->isRequestFromClientOrUserInput(), policyChecker().loadType(), isFormSubmission }; -@@ -1905,7 +1907,9 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t +@@ -1912,7 +1914,9 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t auto policyDecisionMode = loader->triggeringAction().isFromNavigationAPI() ? PolicyDecisionMode::Synchronous : PolicyDecisionMode::Asynchronous; RELEASE_ASSERT(!isBackForwardLoadType(policyChecker().loadType()) || history().provisionalItem()); @@ -5362,7 +5367,7 @@ index 0974e98f02b59150df7054af753a3c3bb1d3b0e9..559a8532162d4c9e106c1059dfbf50fc continueLoadAfterNavigationPolicy(request, RefPtr { weakFormState.get() }.get(), navigationPolicyDecision, allowNavigationToInvalidURL); completionHandler(); }, policyDecisionMode); -@@ -3219,10 +3223,15 @@ String FrameLoader::userAgent(const URL& url) const +@@ -3238,10 +3242,15 @@ String FrameLoader::userAgent(const URL& url) const String FrameLoader::navigatorPlatform() const { @@ -5380,7 +5385,7 @@ index 0974e98f02b59150df7054af753a3c3bb1d3b0e9..559a8532162d4c9e106c1059dfbf50fc } void FrameLoader::dispatchOnloadEvents() -@@ -3682,6 +3691,8 @@ void FrameLoader::receivedMainResourceError(const ResourceError& error, LoadWill +@@ -3702,6 +3711,8 @@ void FrameLoader::receivedMainResourceError(const ResourceError& error, LoadWill checkCompleted(); if (frame->page()) checkLoadComplete(loadWillContinueInAnotherProcess); @@ -5389,7 +5394,7 @@ index 0974e98f02b59150df7054af753a3c3bb1d3b0e9..559a8532162d4c9e106c1059dfbf50fc } void FrameLoader::continueFragmentScrollAfterNavigationPolicy(const ResourceRequest& request, const SecurityOrigin* requesterOrigin, bool shouldContinue, NavigationHistoryBehavior historyHandling) -@@ -4576,9 +4587,6 @@ String FrameLoader::referrer() const +@@ -4591,9 +4602,6 @@ String FrameLoader::referrer() const void FrameLoader::dispatchDidClearWindowObjectsInAllWorlds() { @@ -5399,7 +5404,7 @@ index 0974e98f02b59150df7054af753a3c3bb1d3b0e9..559a8532162d4c9e106c1059dfbf50fc Vector> worlds; ScriptController::getAllWorlds(worlds); for (auto& world : worlds) -@@ -4588,13 +4596,12 @@ void FrameLoader::dispatchDidClearWindowObjectsInAllWorlds() +@@ -4603,13 +4611,12 @@ void FrameLoader::dispatchDidClearWindowObjectsInAllWorlds() void FrameLoader::dispatchDidClearWindowObjectInWorld(DOMWrapperWorld& world) { Ref frame = m_frame.get(); @@ -5420,7 +5425,7 @@ index 0974e98f02b59150df7054af753a3c3bb1d3b0e9..559a8532162d4c9e106c1059dfbf50fc InspectorInstrumentation::didClearWindowObjectInWorld(frame, world); } diff --git a/Source/WebCore/loader/LoaderStrategy.h b/Source/WebCore/loader/LoaderStrategy.h -index 1b34dfdd2a8e56beab49591a3517aba02c510ee6..768b895c132b73d935198cbfc2126a227a656b46 100644 +index a33d00c3ce3a861f8746e58b01ea51e5777cb2ea..9cf20e3c47c7a0bb9db72b9385431cc3df48df86 100644 --- a/Source/WebCore/loader/LoaderStrategy.h +++ b/Source/WebCore/loader/LoaderStrategy.h @@ -86,6 +86,7 @@ public: @@ -5432,10 +5437,10 @@ index 1b34dfdd2a8e56beab49591a3517aba02c510ee6..768b895c132b73d935198cbfc2126a22 virtual bool shouldPerformSecurityChecks() const { return false; } virtual bool havePerformedSecurityChecks(const ResourceResponse&) const { return false; } diff --git a/Source/WebCore/loader/NavigationScheduler.cpp b/Source/WebCore/loader/NavigationScheduler.cpp -index d1d57f421c8570468b8c1ad2dc3c081b50cc4f0a..17a75616518d81eb3ce1e8107ee1f3546b477e30 100644 +index 350f729ee5dc95ba3dd7018542db1dbfb00b382e..f8d31dfc86c0cbc10b474e1773ddbf682b3c9b66 100644 --- a/Source/WebCore/loader/NavigationScheduler.cpp +++ b/Source/WebCore/loader/NavigationScheduler.cpp -@@ -806,7 +806,7 @@ void NavigationScheduler::startTimer() +@@ -803,7 +803,7 @@ void NavigationScheduler::startTimer() Seconds delay = 1_s * m_redirect->delay(); m_timer.startOneShot(delay); @@ -5445,7 +5450,7 @@ index d1d57f421c8570468b8c1ad2dc3c081b50cc4f0a..17a75616518d81eb3ce1e8107ee1f354 } diff --git a/Source/WebCore/loader/ProgressTracker.cpp b/Source/WebCore/loader/ProgressTracker.cpp -index a1554c16a0b836bcf87fea0f4e7831d9e0d7baf0..354e4fef6de9c77b7cad96cc428056ae4e2ff067 100644 +index 1832114445abe51041c241f40df9731d6e0a2f8d..3213517c4cd0b235ff12c098e34389987e6ea090 100644 --- a/Source/WebCore/loader/ProgressTracker.cpp +++ b/Source/WebCore/loader/ProgressTracker.cpp @@ -163,6 +163,8 @@ void ProgressTracker::progressCompleted(LocalFrame& frame) @@ -5460,17 +5465,17 @@ index a1554c16a0b836bcf87fea0f4e7831d9e0d7baf0..354e4fef6de9c77b7cad96cc428056ae @@ -189,8 +191,6 @@ void ProgressTracker::finalProgressComplete() m_client->progressFinished(*frame); protectedPage()->progressFinished(*frame); - frame->protectedLoader()->loadProgressingStatusChanged(); + frame->loader().loadProgressingStatusChanged(); - - InspectorInstrumentation::frameStoppedLoading(*frame); } } diff --git a/Source/WebCore/loader/cache/CachedResourceLoader.cpp b/Source/WebCore/loader/cache/CachedResourceLoader.cpp -index c6a8c86e12ba98ad8b9aab74430523c3dd8cb42e..61c73db33d4c629803d14f243862acea15e762cb 100644 +index b78b91913be6fd14efc7621e251bb5adab05c315..b7f3252e4f201c2c44bca48997f20a3205301486 100644 --- a/Source/WebCore/loader/cache/CachedResourceLoader.cpp +++ b/Source/WebCore/loader/cache/CachedResourceLoader.cpp -@@ -1173,8 +1173,11 @@ ResourceErrorOr> CachedResourceLoader::requ +@@ -1180,8 +1180,11 @@ ResourceErrorOr> CachedResourceLoader::requ request.updateReferrerPolicy(document ? document->referrerPolicy() : ReferrerPolicy::Default); @@ -5484,7 +5489,7 @@ index c6a8c86e12ba98ad8b9aab74430523c3dd8cb42e..61c73db33d4c629803d14f243862acea if (RefPtr documentLoader = m_documentLoader.get()) { bool madeHTTPS { request.resourceRequest().wasSchemeOptimisticallyUpgraded() }; -@@ -1810,8 +1813,9 @@ Vector> CachedResourceLoader::allCachedSVGImages() const +@@ -1816,8 +1819,9 @@ Vector> CachedResourceLoader::allCachedSVGImages() const ResourceErrorOr> CachedResourceLoader::preload(CachedResource::Type type, CachedResourceRequest&& request) { @@ -5497,7 +5502,7 @@ index c6a8c86e12ba98ad8b9aab74430523c3dd8cb42e..61c73db33d4c629803d14f243862acea RefPtr document = m_document.get(); ASSERT(document); diff --git a/Source/WebCore/page/ChromeClient.h b/Source/WebCore/page/ChromeClient.h -index 60ba4d0aaa365301aa80d144f2d07e842a71a6b8..a962ce78e3ad912886a9d3328d64cf0faec3177d 100644 +index 7f6d70366b7841b4e5215a3b8709554fe995ba64..342de0c124714355bd5beaea78310500a9c184b7 100644 --- a/Source/WebCore/page/ChromeClient.h +++ b/Source/WebCore/page/ChromeClient.h @@ -379,7 +379,7 @@ public: @@ -5510,10 +5515,10 @@ index 60ba4d0aaa365301aa80d144f2d07e842a71a6b8..a962ce78e3ad912886a9d3328d64cf0f virtual RefPtr createColorChooser(ColorChooserClient&, const Color&) = 0; diff --git a/Source/WebCore/page/EventHandler.cpp b/Source/WebCore/page/EventHandler.cpp -index 04fce4219bdfc1c4a78eb2d849045280416b6f2d..9e44a302b10ba3bc18496f5678262171e8b30b2c 100644 +index d501516df133e721bc2dcf580b4dd1533b3368f3..63fac9ac1a69d522ed733dae33945ca6b685e4e4 100644 --- a/Source/WebCore/page/EventHandler.cpp +++ b/Source/WebCore/page/EventHandler.cpp -@@ -4483,6 +4483,12 @@ bool EventHandler::handleDrag(const MouseEventWithHitTestResults& event, CheckDr +@@ -4515,6 +4515,12 @@ bool EventHandler::handleDrag(const MouseEventWithHitTestResults& event, CheckDr if (!document) return false; @@ -5526,7 +5531,7 @@ index 04fce4219bdfc1c4a78eb2d849045280416b6f2d..9e44a302b10ba3bc18496f5678262171 dragState().dataTransfer = DataTransfer::createForDrag(*document); auto hasNonDefaultPasteboardData = HasNonDefaultPasteboardData::No; -@@ -5077,6 +5083,7 @@ static HitTestResult hitTestResultInFrame(LocalFrame* frame, const LayoutPoint& +@@ -5119,6 +5125,7 @@ static HitTestResult hitTestResultInFrame(LocalFrame* frame, const LayoutPoint& return result; } @@ -5534,7 +5539,7 @@ index 04fce4219bdfc1c4a78eb2d849045280416b6f2d..9e44a302b10ba3bc18496f5678262171 Expected EventHandler::handleTouchEvent(const PlatformTouchEvent& event) { Ref frame = m_frame.get(); -@@ -5150,7 +5157,7 @@ Expected EventHandler::handleTouchEvent(co +@@ -5192,7 +5199,7 @@ Expected EventHandler::handleTouchEvent(co // Increment the platform touch id by 1 to avoid storing a key of 0 in the hashmap. unsigned touchPointTargetKey = point.id() + 1; @@ -5543,7 +5548,7 @@ index 04fce4219bdfc1c4a78eb2d849045280416b6f2d..9e44a302b10ba3bc18496f5678262171 bool pointerCancelled = false; #endif RefPtr touchTarget; -@@ -5197,7 +5204,7 @@ Expected EventHandler::handleTouchEvent(co +@@ -5239,7 +5246,7 @@ Expected EventHandler::handleTouchEvent(co // we also remove it from the map. touchTarget = m_originatingTouchPointTargets.take(touchPointTargetKey); @@ -5552,7 +5557,7 @@ index 04fce4219bdfc1c4a78eb2d849045280416b6f2d..9e44a302b10ba3bc18496f5678262171 HitTestResult result = hitTestResultAtPoint(pagePoint, hitType | HitTestRequest::Type::AllowChildFrameContent); pointerTarget = result.targetElement(); pointerCancelled = (pointerTarget != touchTarget); -@@ -5220,7 +5227,7 @@ Expected EventHandler::handleTouchEvent(co +@@ -5262,7 +5269,7 @@ Expected EventHandler::handleTouchEvent(co if (!targetFrame) continue; @@ -5561,7 +5566,7 @@ index 04fce4219bdfc1c4a78eb2d849045280416b6f2d..9e44a302b10ba3bc18496f5678262171 // FIXME: WPE currently does not send touch stationary events, so create a naive TouchReleased PlatformTouchPoint // on release if the hit test result changed since the previous TouchPressed or TouchMoved if (pointState == PlatformTouchPoint::TouchReleased && pointerCancelled) { -@@ -5310,6 +5317,7 @@ Expected EventHandler::handleTouchEvent(co +@@ -5352,6 +5359,7 @@ Expected EventHandler::handleTouchEvent(co return swallowedEvent; } @@ -5570,10 +5575,10 @@ index 04fce4219bdfc1c4a78eb2d849045280416b6f2d..9e44a302b10ba3bc18496f5678262171 #if ENABLE(TOUCH_EVENTS) diff --git a/Source/WebCore/page/FocusController.cpp b/Source/WebCore/page/FocusController.cpp -index 1b183750e1c51e96fcca727bed3c2eb1d30e77e9..e1d00211b5051387bd4495d88cef637704cf1704 100644 +index 79636a0a8528bfd4e86d8373f9ab7e96e9ac8346..f2a2ebeb6a7e41a3900d35dabde88f9fd7c4ae82 100644 --- a/Source/WebCore/page/FocusController.cpp +++ b/Source/WebCore/page/FocusController.cpp -@@ -580,13 +580,14 @@ bool FocusController::relinquishFocusToChrome(FocusDirection direction) +@@ -595,13 +595,14 @@ bool FocusController::relinquishFocusToChrome(FocusDirection direction) return false; Ref page = m_page.get(); @@ -5625,7 +5630,7 @@ index 3474800864049dcbe6c84746c4216e72a68fa48f..8f5d536921eeb41c77fe689c036dcb77 } diff --git a/Source/WebCore/page/FrameSnapshotting.h b/Source/WebCore/page/FrameSnapshotting.h -index 713cb9c3c59bbad23ef0b821ab27b3d669a7dea8..12077ebf0978b6b52e6af4602767c09d9f3cd9de 100644 +index fd9df9d9564fe29c64342fbf77082ad595612e90..af5687c79e2a5be20cde653107e5827c1d8981e5 100644 --- a/Source/WebCore/page/FrameSnapshotting.h +++ b/Source/WebCore/page/FrameSnapshotting.h @@ -58,6 +58,7 @@ enum class SnapshotFlags : uint16_t { @@ -5637,7 +5642,7 @@ index 713cb9c3c59bbad23ef0b821ab27b3d669a7dea8..12077ebf0978b6b52e6af4602767c09d struct SnapshotOptions { diff --git a/Source/WebCore/page/History.cpp b/Source/WebCore/page/History.cpp -index adc6185687b6fcee3a49819d0149cde24a4485bf..a33e5db46933c514abce86e3f264de9b7e7c20d9 100644 +index f2dfb5966f445ee52b4430e65e0accb7e41bb913..1d25db7e91beaaea3c083f315ec3f21b317f271f 100644 --- a/Source/WebCore/page/History.cpp +++ b/Source/WebCore/page/History.cpp @@ -32,6 +32,7 @@ @@ -5658,7 +5663,7 @@ index adc6185687b6fcee3a49819d0149cde24a4485bf..a33e5db46933c514abce86e3f264de9b } diff --git a/Source/WebCore/page/LocalFrame.cpp b/Source/WebCore/page/LocalFrame.cpp -index f6e26519619d560b5edddb30170f166ac76bd57f..51019ccda3f5953c4a154c271690f3923c1a530a 100644 +index 018a1445506e0b3cae8b7d4056f37bcdd21eb1dd..a1cd251e84d2faa8b58b5ebbf5077bacb1e5bf6e 100644 --- a/Source/WebCore/page/LocalFrame.cpp +++ b/Source/WebCore/page/LocalFrame.cpp @@ -42,6 +42,7 @@ @@ -5690,7 +5695,7 @@ index f6e26519619d560b5edddb30170f166ac76bd57f..51019ccda3f5953c4a154c271690f392 void LocalFrame::init() { + InspectorInstrumentation::frameAttached(this); - protectedLoader()->init(); + loader().init(); } @@ -444,7 +448,7 @@ void LocalFrame::orientationChanged() @@ -6068,7 +6073,7 @@ index f6e26519619d560b5edddb30170f166ac76bd57f..51019ccda3f5953c4a154c271690f392 #undef FRAME_RELEASE_LOG_ERROR diff --git a/Source/WebCore/page/LocalFrame.h b/Source/WebCore/page/LocalFrame.h -index 7e8505e025af3f52a0adbd5e6b98d6a22f449d9c..667a9578f073c216231ca8834354c1d5ce49370d 100644 +index 92a21d8efb29d588a7a530e823079ad299cbd40a..3a925cc017500b37abee9d2d26662f9eccc2eebc 100644 --- a/Source/WebCore/page/LocalFrame.h +++ b/Source/WebCore/page/LocalFrame.h @@ -29,6 +29,7 @@ @@ -6089,7 +6094,7 @@ index 7e8505e025af3f52a0adbd5e6b98d6a22f449d9c..667a9578f073c216231ca8834354c1d5 class LocalFrame final : public Frame { public: -@@ -233,10 +234,6 @@ public: +@@ -229,10 +230,6 @@ public: WEBCORE_EXPORT DataDetectionResultsStorage& dataDetectionResults(); #endif @@ -6100,7 +6105,7 @@ index 7e8505e025af3f52a0adbd5e6b98d6a22f449d9c..667a9578f073c216231ca8834354c1d5 WEBCORE_EXPORT Node* deepestNodeAtLocation(const FloatPoint& viewportLocation); WEBCORE_EXPORT Node* nodeRespondingToClickEvents(const FloatPoint& viewportLocation, FloatPoint& adjustedViewportLocation, SecurityOrigin* = nullptr); WEBCORE_EXPORT Node* nodeRespondingToDoubleClickEvent(const FloatPoint& viewportLocation, FloatPoint& adjustedViewportLocation); -@@ -244,6 +241,10 @@ public: +@@ -240,6 +237,10 @@ public: WEBCORE_EXPORT Node* nodeRespondingToScrollWheelEvents(const FloatPoint& viewportLocation); WEBCORE_EXPORT Node* approximateNodeAtViewportLocationLegacy(const FloatPoint& viewportLocation, FloatPoint& adjustedViewportLocation); @@ -6111,7 +6116,7 @@ index 7e8505e025af3f52a0adbd5e6b98d6a22f449d9c..667a9578f073c216231ca8834354c1d5 WEBCORE_EXPORT NSArray *wordsInCurrentParagraph() const; WEBCORE_EXPORT CGRect renderRectForPoint(CGPoint, bool* isReplaced, float* fontSize) const; -@@ -311,6 +312,7 @@ public: +@@ -307,6 +308,7 @@ public: WEBCORE_EXPORT FloatSize screenSize() const; void setOverrideScreenSize(FloatSize&&); @@ -6119,7 +6124,7 @@ index 7e8505e025af3f52a0adbd5e6b98d6a22f449d9c..667a9578f073c216231ca8834354c1d5 void selfOnlyRef(); void selfOnlyDeref(); -@@ -382,7 +384,6 @@ private: +@@ -378,7 +380,6 @@ private: #if ENABLE(DATA_DETECTION) std::unique_ptr m_dataDetectionResults; #endif @@ -6127,19 +6132,19 @@ index 7e8505e025af3f52a0adbd5e6b98d6a22f449d9c..667a9578f073c216231ca8834354c1d5 void betterApproximateNode(const IntPoint& testPoint, const NodeQualifier&, Node*& best, Node* failedNode, IntPoint& bestPoint, IntRect& bestRect, const IntRect& testRect); bool hitTestResultAtViewportLocation(const FloatPoint& viewportLocation, HitTestResult&, IntPoint& center); -@@ -390,6 +391,7 @@ private: +@@ -386,6 +387,7 @@ private: enum class ShouldFindRootEditableElement : bool { No, Yes }; Node* qualifyingNodeAtViewportLocation(const FloatPoint& viewportLocation, FloatPoint& adjustedViewportLocation, const NodeQualifier&, ShouldApproximate, ShouldFindRootEditableElement = ShouldFindRootEditableElement::Yes); +#if PLATFORM(IOS_FAMILY) void setTimersPausedInternal(bool); - UniqueRef m_viewportArguments; + const UniqueRef m_viewportArguments; diff --git a/Source/WebCore/page/Page.cpp b/Source/WebCore/page/Page.cpp -index 08315ed1fb551d8ad9f9a7dcdc4fea487cd9a8fb..44cf9866c35e309cb90138b73c3d35e5409aa7eb 100644 +index 6b60173931046a6361e7f0d388c6663166236dfe..fe3175f67a82a3328e1a3c1bc74521daa792dedc 100644 --- a/Source/WebCore/page/Page.cpp +++ b/Source/WebCore/page/Page.cpp -@@ -663,6 +663,45 @@ void Page::setOverrideViewportArguments(const std::optional& +@@ -667,6 +667,45 @@ void Page::setOverrideViewportArguments(const std::optional& localTopDocument->updateViewportArguments(); } @@ -6185,7 +6190,7 @@ index 08315ed1fb551d8ad9f9a7dcdc4fea487cd9a8fb..44cf9866c35e309cb90138b73c3d35e5 ScrollingCoordinator* Page::scrollingCoordinator() { if (!m_scrollingCoordinator && m_settings->scrollingCoordinatorEnabled()) { -@@ -4242,6 +4281,26 @@ void Page::setUseDarkAppearanceOverride(std::optional valueOverride) +@@ -4292,6 +4331,26 @@ void Page::setUseDarkAppearanceOverride(std::optional valueOverride) appearanceDidChange(); } @@ -6213,10 +6218,10 @@ index 08315ed1fb551d8ad9f9a7dcdc4fea487cd9a8fb..44cf9866c35e309cb90138b73c3d35e5 { if (insets == m_fullscreenInsets) diff --git a/Source/WebCore/page/Page.h b/Source/WebCore/page/Page.h -index 9f107570fbb2195211ad46358369acb791e0376b..8c753ef756e9fe64ffdca36683ec486edb758f6b 100644 +index cd29ee00f3499c50379f02981fe85e7bdd950d93..10b653145543c308afd3ed1497f622d7adf08e9d 100644 --- a/Source/WebCore/page/Page.h +++ b/Source/WebCore/page/Page.h -@@ -389,6 +389,9 @@ public: +@@ -391,6 +391,9 @@ public: const ViewportArguments* overrideViewportArguments() const { return m_overrideViewportArguments.get(); } WEBCORE_EXPORT void setOverrideViewportArguments(const std::optional&); @@ -6226,7 +6231,7 @@ index 9f107570fbb2195211ad46358369acb791e0376b..8c753ef756e9fe64ffdca36683ec486e static void refreshPlugins(bool reload); WEBCORE_EXPORT PluginData& pluginData(); WEBCORE_EXPORT Ref protectedPluginData(); -@@ -486,6 +489,10 @@ public: +@@ -488,6 +491,10 @@ public: #if ENABLE(DRAG_SUPPORT) DragController& dragController() { return m_dragController.get(); } const DragController& dragController() const { return m_dragController.get(); } @@ -6235,8 +6240,8 @@ index 9f107570fbb2195211ad46358369acb791e0376b..8c753ef756e9fe64ffdca36683ec486e + const String& overrideDragPasteboardName() { return m_overrideDragPasteboardName; } +#endif #endif - FocusController& focusController() const { return *m_focusController; } - WEBCORE_EXPORT CheckedRef checkedFocusController() const; + FocusController& focusController() const { return m_focusController; } + #if ENABLE(CONTEXT_MENUS) @@ -671,6 +678,10 @@ public: WEBCORE_EXPORT void setUseColorAppearance(bool useDarkAppearance, bool useElevatedUserInterfaceLevel); bool defaultUseDarkAppearance() const { return m_useDarkAppearance; } @@ -6248,7 +6253,7 @@ index 9f107570fbb2195211ad46358369acb791e0376b..8c753ef756e9fe64ffdca36683ec486e #if ENABLE(TEXT_AUTOSIZING) float textAutosizingWidth() const { return m_textAutosizingWidth; } -@@ -1143,6 +1154,11 @@ public: +@@ -1148,6 +1159,11 @@ public: WEBCORE_EXPORT void setInteractionRegionsEnabled(bool); #endif @@ -6260,7 +6265,7 @@ index 9f107570fbb2195211ad46358369acb791e0376b..8c753ef756e9fe64ffdca36683ec486e #if ENABLE(DEVICE_ORIENTATION) && PLATFORM(IOS_FAMILY) DeviceOrientationUpdateProvider* deviceOrientationUpdateProvider() const { return m_deviceOrientationUpdateProvider.get(); } #endif -@@ -1424,6 +1440,9 @@ private: +@@ -1437,6 +1453,9 @@ private: #if ENABLE(DRAG_SUPPORT) const UniqueRef m_dragController; @@ -6268,9 +6273,9 @@ index 9f107570fbb2195211ad46358369acb791e0376b..8c753ef756e9fe64ffdca36683ec486e + String m_overrideDragPasteboardName; +#endif #endif - std::unique_ptr m_focusController; + const UniqueRef m_focusController; #if ENABLE(CONTEXT_MENUS) -@@ -1502,6 +1521,8 @@ private: +@@ -1515,6 +1534,8 @@ private: bool m_useElevatedUserInterfaceLevel { false }; bool m_useDarkAppearance { false }; std::optional m_useDarkAppearanceOverride; @@ -6279,7 +6284,7 @@ index 9f107570fbb2195211ad46358369acb791e0376b..8c753ef756e9fe64ffdca36683ec486e #if ENABLE(TEXT_AUTOSIZING) float m_textAutosizingWidth { 0 }; -@@ -1683,6 +1704,11 @@ private: +@@ -1696,6 +1717,11 @@ private: #endif std::unique_ptr m_overrideViewportArguments; @@ -6318,7 +6323,7 @@ index 153fc36199f26adbfb61cbef6744ffe31a68b951..cc667e06700013fd5e994467e19536d2 Ref protectedPage() const; diff --git a/Source/WebCore/page/PointerCaptureController.cpp b/Source/WebCore/page/PointerCaptureController.cpp -index 7f0fe2766310d43552d12b85d1484cc095a752a4..1ab97c02215826ad9210489b64573145755a1b4f 100644 +index f01c803f6afa4e2949378bc9a758af4a55681dc5..9d35c28b4ff837f4333ac50bc917513bbf2b5773 100644 --- a/Source/WebCore/page/PointerCaptureController.cpp +++ b/Source/WebCore/page/PointerCaptureController.cpp @@ -207,7 +207,7 @@ bool PointerCaptureController::preventsCompatibilityMouseEventsForIdentifier(Poi @@ -6340,7 +6345,7 @@ index 7f0fe2766310d43552d12b85d1484cc095a752a4..1ab97c02215826ad9210489b64573145 #endif diff --git a/Source/WebCore/page/PointerCaptureController.h b/Source/WebCore/page/PointerCaptureController.h -index e50f38c2c45f665b4b88fe1aa4f99d1a51328d63..8144be1724ef71a93b8870f64ec5abcb98eaa27f 100644 +index 2c3ae18820bd2be191dbd8d6bd513c0302a1f580..21538527135e659493ee812aa61c943ae37c0b74 100644 --- a/Source/WebCore/page/PointerCaptureController.h +++ b/Source/WebCore/page/PointerCaptureController.h @@ -60,7 +60,7 @@ public: @@ -6368,7 +6373,7 @@ index e50f38c2c45f665b4b88fe1aa4f99d1a51328d63..8144be1724ef71a93b8870f64ec5abcb #endif ; diff --git a/Source/WebCore/page/Screen.cpp b/Source/WebCore/page/Screen.cpp -index 24ed7c019bea4df52f2883db0e40bdbc2dc74ebd..a788f534d9e0e8124153c7f380b4fdb232c51a1a 100644 +index c45965ee6b7fe6c066871b87d7883e639a849adb..16c3a5b027222d1de5bb540c71c0b6aa107c4657 100644 --- a/Source/WebCore/page/Screen.cpp +++ b/Source/WebCore/page/Screen.cpp @@ -124,6 +124,9 @@ int Screen::availLeft() const @@ -6412,7 +6417,7 @@ index 24ed7c019bea4df52f2883db0e40bdbc2dc74ebd..a788f534d9e0e8124153c7f380b4fdb2 } diff --git a/Source/WebCore/page/csp/ContentSecurityPolicy.cpp b/Source/WebCore/page/csp/ContentSecurityPolicy.cpp -index e8cf919fd1d0147fce390aa5750843a6bce40190..fb2d56e3a3b6901f993544e297ad678c469a9cc1 100644 +index 5ba13e25cbe12ea5c7e543a3e9377f662cbfe50e..b5ab59f41db851cb03a56c9dfaf76e47182f8e85 100644 --- a/Source/WebCore/page/csp/ContentSecurityPolicy.cpp +++ b/Source/WebCore/page/csp/ContentSecurityPolicy.cpp @@ -374,6 +374,8 @@ template @@ -6529,7 +6534,7 @@ index 0000000000000000000000000000000000000000..803239911006cfb3b03ea911c003f2d2 + +} diff --git a/Source/WebCore/platform/DragData.h b/Source/WebCore/platform/DragData.h -index 2ebb3221cb3818f20abc7b7c3307ea7644f49d49..e0bffb6f5adee0b0a202340f633c2def1955b384 100644 +index f76eab27bd22db72e23cd53d98fe721f0d7e48b7..9757103efe88c0fed3b68671c3042faa29b31433 100644 --- a/Source/WebCore/platform/DragData.h +++ b/Source/WebCore/platform/DragData.h @@ -47,7 +47,7 @@ typedef void* DragDataRef; @@ -6561,32 +6566,6 @@ index 2ebb3221cb3818f20abc7b7c3307ea7644f49d49..e0bffb6f5adee0b0a202340f633c2def #endif bool m_disallowFileAccess { false }; }; -diff --git a/Source/WebCore/platform/DragImage.cpp b/Source/WebCore/platform/DragImage.cpp -index 41b4b3ca89d0df2ccba562d83c33097539b5ae1c..071bee507b730b89796420222b9787bd411a19ad 100644 ---- a/Source/WebCore/platform/DragImage.cpp -+++ b/Source/WebCore/platform/DragImage.cpp -@@ -280,7 +280,7 @@ DragImage::~DragImage() - deleteDragImage(m_dragImageRef); - } - --#if !PLATFORM(COCOA) && !PLATFORM(GTK) && !PLATFORM(WIN) -+#if !PLATFORM(COCOA) && !PLATFORM(GTK) && !PLATFORM(WIN) && !PLATFORM(WPE) - - IntSize dragImageSize(DragImageRef) - { -diff --git a/Source/WebCore/platform/DragImage.h b/Source/WebCore/platform/DragImage.h -index 77286d8e715825c9c7c0d329480fc0fc497fe561..5ea53eb8b7e53dd5a5950c65a38c1d23bfe46681 100644 ---- a/Source/WebCore/platform/DragImage.h -+++ b/Source/WebCore/platform/DragImage.h -@@ -60,7 +60,7 @@ class Node; - typedef RetainPtr DragImageRef; - #elif PLATFORM(MAC) - typedef RetainPtr DragImageRef; --#elif PLATFORM(WIN) -+#elif PLATFORM(WIN) && USE(CAIRO) - typedef HBITMAP DragImageRef; - #elif USE(CAIRO) - typedef RefPtr DragImageRef; diff --git a/Source/WebCore/platform/Pasteboard.h b/Source/WebCore/platform/Pasteboard.h index f8de8815d483bd3ac684c018159c593798c0495a..e84ce8eec7424fbc3456623289588b1832d9fb7f 100644 --- a/Source/WebCore/platform/Pasteboard.h @@ -6653,7 +6632,7 @@ index f8de8815d483bd3ac684c018159c593798c0495a..e84ce8eec7424fbc3456623289588b18 }; diff --git a/Source/WebCore/platform/PlatformKeyboardEvent.h b/Source/WebCore/platform/PlatformKeyboardEvent.h -index 63ffd6ca32c3baee03db2a9419c4f7e9de45388a..c60c7a8d1f110472117c8c4e969fd05fef71f908 100644 +index 241c4f1cc29a854e8329ba5227b7aaf0cca8138e..0a2a487dd91ddeec37fb996ecc6039fc70c4ba8d 100644 --- a/Source/WebCore/platform/PlatformKeyboardEvent.h +++ b/Source/WebCore/platform/PlatformKeyboardEvent.h @@ -135,6 +135,7 @@ namespace WebCore { @@ -6702,10 +6681,10 @@ index ef0abc9a93e878897ffc9d2497a3da0fca5b37b7..abd96c6d1a6c3ab9e0121c1e78f2f75a +} // namespace WebCore +#endif diff --git a/Source/WebCore/platform/PlatformScreen.h b/Source/WebCore/platform/PlatformScreen.h -index 82a54ac2de2ddf4650e4b48db129fe25f6562264..d144ba15d18d2e892c25988dab27636135353a99 100644 +index 26ca6a098bf39cff7e9a6505e84dbdbbd2aafc24..63d4fa7e39689a47269ae369bcf335642af6e669 100644 --- a/Source/WebCore/platform/PlatformScreen.h +++ b/Source/WebCore/platform/PlatformScreen.h -@@ -160,10 +160,14 @@ WEBCORE_EXPORT float screenScaleFactor(UIScreen * = nullptr); +@@ -165,10 +165,14 @@ WEBCORE_EXPORT float screenScaleFactor(UIScreen * = nullptr); #endif #if ENABLE(TOUCH_EVENTS) @@ -6773,6 +6752,24 @@ index 492c5e76290c2379cda40b9663f5f67ff8f66360..096752985edf39960eb4be6eb733ebe3 static const unsigned scrollbarBorderSize = 1; static const unsigned thumbBorderSize = 1; static const unsigned overlayThumbSize = 3; +diff --git a/Source/WebCore/platform/graphics/ImageAdapter.h b/Source/WebCore/platform/graphics/ImageAdapter.h +index f5d16bcb2d300d6e54b90583a4e9489862a7dfd0..e9c945854a011e9ec1de96cc8dfbc08ef13a91e0 100644 +--- a/Source/WebCore/platform/graphics/ImageAdapter.h ++++ b/Source/WebCore/platform/graphics/ImageAdapter.h +@@ -61,11 +61,12 @@ typedef struct HBITMAP__ *HBITMAP; + #include + #endif + ++#include "NativeImage.h" ++ + namespace WebCore { + + class Image; + class IntSize; +-class NativeImage; + + class ImageAdapter { + WTF_MAKE_TZONE_ALLOCATED(ImageAdapter); diff --git a/Source/WebCore/platform/graphics/cg/ImageBufferUtilitiesCG.h b/Source/WebCore/platform/graphics/cg/ImageBufferUtilitiesCG.h index 5b659c763b9754b025a63f89522954cc39915b9a..448b50a2b131361a75d3f816cdcbb6a102551280 100644 --- a/Source/WebCore/platform/graphics/cg/ImageBufferUtilitiesCG.h @@ -6787,7 +6784,7 @@ index 5b659c763b9754b025a63f89522954cc39915b9a..448b50a2b131361a75d3f816cdcbb6a1 Vector encodeData(std::span, const String& mimeType, std::optional quality); diff --git a/Source/WebCore/platform/graphics/filters/software/FEComponentTransferSoftwareApplier.h b/Source/WebCore/platform/graphics/filters/software/FEComponentTransferSoftwareApplier.h -index c7542b821af8c87660e10b0c07b360cfcc8e28a2..a3cc131ff0410ae31df30c115dd9fd26aec5adf8 100644 +index 515ddea3cd42796efa9f41ad74be07a7447c337e..36db42e2a0822d5609b39046191f05a1f8d2b54b 100644 --- a/Source/WebCore/platform/graphics/filters/software/FEComponentTransferSoftwareApplier.h +++ b/Source/WebCore/platform/graphics/filters/software/FEComponentTransferSoftwareApplier.h @@ -23,6 +23,7 @@ @@ -6799,7 +6796,7 @@ index c7542b821af8c87660e10b0c07b360cfcc8e28a2..a3cc131ff0410ae31df30c115dd9fd26 namespace WebCore { diff --git a/Source/WebCore/platform/graphics/win/ComplexTextControllerUniscribe.cpp b/Source/WebCore/platform/graphics/win/ComplexTextControllerUniscribe.cpp -index 78ea08023ebd5f1b41b06cd843b6dce4ee80dd50..55fed94b774f033b4f75e2dee85dc27cf2e2689e 100644 +index 6239b3ab731ffc0826216ddda46d951d8dace5a0..270423347baa7e6fa47ad1da2154c10423037ae2 100644 --- a/Source/WebCore/platform/graphics/win/ComplexTextControllerUniscribe.cpp +++ b/Source/WebCore/platform/graphics/win/ComplexTextControllerUniscribe.cpp @@ -168,6 +168,33 @@ static Vector stringIndicesFromClusters(const Vector& clusters, @@ -6846,7 +6843,7 @@ index 78ea08023ebd5f1b41b06cd843b6dce4ee80dd50..55fed94b774f033b4f75e2dee85dc27c // Determine the string for this item. const UChar* str = cp.data() + items[i].iCharPos; diff --git a/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp b/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp -index 2b7d3dc70fdfec767d8caa13966c4051ee73aead..7fa8e155bef817c837042c8d6c4dcb84864232bc 100644 +index b3d305bfa5694f074dba36a0ca11bf3f13e80579..05faaf5417e768c958cf6a9807dfcd1f4c455a8d 100644 --- a/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp +++ b/Source/WebCore/platform/gtk/PlatformKeyboardEventGtk.cpp @@ -37,8 +37,10 @@ @@ -7108,7 +7105,7 @@ index 2b7d3dc70fdfec767d8caa13966c4051ee73aead..7fa8e155bef817c837042c8d6c4dcb84 { switch (val) { diff --git a/Source/WebCore/platform/gtk/PlatformScreenGtk.cpp b/Source/WebCore/platform/gtk/PlatformScreenGtk.cpp -index 1339241aae69b657ffa40b603eea992c45db0e65..9194ff5f8308e5cf22fb250723ae4161142da45f 100644 +index 98d93889a1b97450fde21cee53e45111b622dca7..9362518e8351eb8fe5b30a2d7875417bb3da770d 100644 --- a/Source/WebCore/platform/gtk/PlatformScreenGtk.cpp +++ b/Source/WebCore/platform/gtk/PlatformScreenGtk.cpp @@ -121,7 +121,7 @@ bool screenSupportsExtendedColor(Widget*) @@ -7502,6 +7499,18 @@ index 80c20938d5ec8282485fb0152b96304c83124354..22975b7c1b99357a7689747251410f30 #include #include #include +diff --git a/Source/WebCore/platform/mediastream/libwebrtc/gstreamer/RealtimeOutgoingAudioSourceLibWebRTC.h b/Source/WebCore/platform/mediastream/libwebrtc/gstreamer/RealtimeOutgoingAudioSourceLibWebRTC.h +index 95d0e3412831763a80c0c2337e84c29cdc849e71..58d5685a9ae1083b96e687a249d96c5c5be827db 100644 +--- a/Source/WebCore/platform/mediastream/libwebrtc/gstreamer/RealtimeOutgoingAudioSourceLibWebRTC.h ++++ b/Source/WebCore/platform/mediastream/libwebrtc/gstreamer/RealtimeOutgoingAudioSourceLibWebRTC.h +@@ -27,6 +27,7 @@ + #include + #include + #include ++#include + #include + #include + diff --git a/Source/WebCore/platform/network/HTTPHeaderMap.cpp b/Source/WebCore/platform/network/HTTPHeaderMap.cpp index 1178c8fb001994bc9e6166376a367d9bc148913c..fcc6534568cad6b42a819a435f84ba2b9baae6f8 100644 --- a/Source/WebCore/platform/network/HTTPHeaderMap.cpp @@ -7520,7 +7529,7 @@ index 1178c8fb001994bc9e6166376a367d9bc148913c..fcc6534568cad6b42a819a435f84ba2b m_commonHeaders.append(CommonHeader { name, value }); } diff --git a/Source/WebCore/platform/network/NetworkStorageSession.h b/Source/WebCore/platform/network/NetworkStorageSession.h -index c1e50549e674e1620ad0515a061376ba728e75df..7e6e323436d9b1a87dbf4d6fa275629f531e3ce8 100644 +index 2216400912348ab0b6978a40fe02bfc12b249deb..2a0720873e26158c37d7ede83d9a1fd7a02f4409 100644 --- a/Source/WebCore/platform/network/NetworkStorageSession.h +++ b/Source/WebCore/platform/network/NetworkStorageSession.h @@ -198,6 +198,7 @@ public: @@ -7532,7 +7541,7 @@ index c1e50549e674e1620ad0515a061376ba728e75df..7e6e323436d9b1a87dbf4d6fa275629f WEBCORE_EXPORT HTTPCookieAcceptPolicy cookieAcceptPolicy() const; WEBCORE_EXPORT void setCookie(const Cookie&); diff --git a/Source/WebCore/platform/network/ResourceResponseBase.cpp b/Source/WebCore/platform/network/ResourceResponseBase.cpp -index a4ed6a5f1182b43432f6082ffc10b62473c248e5..05113dbf615171a0b18f8998411e603076bdc15b 100644 +index a3eefa06801c54642ce6ec1c3bc7675b491ccf5c..46a8197a447b4eba385eb6b89b4a949147955bbe 100644 --- a/Source/WebCore/platform/network/ResourceResponseBase.cpp +++ b/Source/WebCore/platform/network/ResourceResponseBase.cpp @@ -78,6 +78,7 @@ ResourceResponseBase::ResourceResponseBase(std::optional&& @@ -7572,7 +7581,7 @@ index a4ed6a5f1182b43432f6082ffc10b62473c248e5..05113dbf615171a0b18f8998411e6030 *source, *type, diff --git a/Source/WebCore/platform/network/ResourceResponseBase.h b/Source/WebCore/platform/network/ResourceResponseBase.h -index 9e2bc7f05c5700c5f86bfaf03f13e2308ed097ed..01bae357d25b8dfef4c0d2cdb87cbc7a3895b416 100644 +index 545befb0776b62d546b7b7a28093e6f1c4478d2c..a12b31f99f380ea5fbd9b82916e397cab8b6298c 100644 --- a/Source/WebCore/platform/network/ResourceResponseBase.h +++ b/Source/WebCore/platform/network/ResourceResponseBase.h @@ -256,6 +256,11 @@ protected: @@ -7613,7 +7622,7 @@ index 9e2bc7f05c5700c5f86bfaf03f13e2308ed097ed..01bae357d25b8dfef4c0d2cdb87cbc7a ResourceResponseBase::Source source; ResourceResponseBase::Type type; diff --git a/Source/WebCore/platform/network/cocoa/NetworkStorageSessionCocoa.mm b/Source/WebCore/platform/network/cocoa/NetworkStorageSessionCocoa.mm -index 4a6533db6aff0620e7122e684f1ef6c723bf7942..395f275722b93a7ab08429588fbc1fdc1ebac4f3 100644 +index 632ec5128ee28f9d5ea33bdfaa311bad56ec6396..837d67d61ba14e509cf9f72320cf56457263501b 100644 --- a/Source/WebCore/platform/network/cocoa/NetworkStorageSessionCocoa.mm +++ b/Source/WebCore/platform/network/cocoa/NetworkStorageSessionCocoa.mm @@ -553,6 +553,22 @@ bool NetworkStorageSession::setCookieFromDOM(const URL& firstParty, const SameSi @@ -7653,7 +7662,7 @@ index 37e129136c69b27d509acc01f10be42a8a1fe35a..9df0babc8f82372925fddf2211a7c8c9 bool m_detectedDatabaseCorruption { false }; diff --git a/Source/WebCore/platform/network/curl/NetworkStorageSessionCurl.cpp b/Source/WebCore/platform/network/curl/NetworkStorageSessionCurl.cpp -index 96289d8ae2e4feb60a91fab3f5cf1fc27b9e7c87..8c0b62c44a18571d1f3ea1ed81d59a0aae28d3f1 100644 +index 78584cd0fb75884e96d489d9da6a24f6ddedbeca..ebbb8c3e9a469d11e7433ac6ef85300e5b6d2ad4 100644 --- a/Source/WebCore/platform/network/curl/NetworkStorageSessionCurl.cpp +++ b/Source/WebCore/platform/network/curl/NetworkStorageSessionCurl.cpp @@ -136,6 +136,12 @@ void NetworkStorageSession::setCookieAcceptPolicy(CookieAcceptPolicy policy) con @@ -7670,7 +7679,7 @@ index 96289d8ae2e4feb60a91fab3f5cf1fc27b9e7c87..8c0b62c44a18571d1f3ea1ed81d59a0a { switch (cookieDatabase().acceptPolicy()) { diff --git a/Source/WebCore/platform/network/soup/NetworkStorageSessionSoup.cpp b/Source/WebCore/platform/network/soup/NetworkStorageSessionSoup.cpp -index 274d671614fdca8f425adecb405d5f2402e92f8b..1b19e79e10f0768cfdd66b0fc1b1855388c9f559 100644 +index 5b3deb017487d8362e11479ea814e02e9221fd23..cb829266402ce26b193a62f7fa39d83681302a1e 100644 --- a/Source/WebCore/platform/network/soup/NetworkStorageSessionSoup.cpp +++ b/Source/WebCore/platform/network/soup/NetworkStorageSessionSoup.cpp @@ -551,6 +551,26 @@ void NetworkStorageSession::replaceCookies(const Vector& cookies) @@ -7701,7 +7710,7 @@ index 274d671614fdca8f425adecb405d5f2402e92f8b..1b19e79e10f0768cfdd66b0fc1b18553 { GUniquePtr targetCookie(cookie.toSoupCookie()); diff --git a/Source/WebCore/platform/win/ClipboardUtilitiesWin.cpp b/Source/WebCore/platform/win/ClipboardUtilitiesWin.cpp -index bbcc12d58f7b5df3462c93617f6ef19eef403cf2..b255b05da89248a99ff11965ceae840ef45b9fab 100644 +index 72e1784eff239b69db25a598934a671e7aead036..d647b0d8510edeed5aa27d780a06dd7e5cb413dd 100644 --- a/Source/WebCore/platform/win/ClipboardUtilitiesWin.cpp +++ b/Source/WebCore/platform/win/ClipboardUtilitiesWin.cpp @@ -40,6 +40,7 @@ @@ -7725,7 +7734,7 @@ index bbcc12d58f7b5df3462c93617f6ef19eef403cf2..b255b05da89248a99ff11965ceae840e ReleaseStgMedium(&store); } diff --git a/Source/WebCore/platform/win/ClipboardUtilitiesWin.h b/Source/WebCore/platform/win/ClipboardUtilitiesWin.h -index c3ffc7392c0b7fa099a7dd4e4be977cdee1c803c..9570dbb0f2c42ca38598a8898183c9b310f858ab 100644 +index 61624f0c555886e11a82e933aa89a093b5273693..4471b458941d3394d99f85e983cf0d86e1a562d3 100644 --- a/Source/WebCore/platform/win/ClipboardUtilitiesWin.h +++ b/Source/WebCore/platform/win/ClipboardUtilitiesWin.h @@ -34,6 +34,7 @@ namespace WebCore { @@ -7764,69 +7773,6 @@ index 0379437d84807e4a8d3846afac5ec8a70e743e70..5b0461bf12535d4900ffaddc2a878262 { if (!m_dragDataMap.isEmpty() || !m_platformDragData) return m_dragDataMap; -diff --git a/Source/WebCore/platform/win/DragImageWin.cpp b/Source/WebCore/platform/win/DragImageWin.cpp -index dd24e15115aeff41f0f4452a9cac292d75bc0d5d..8df467c008bdb3de59d301b14c1c20b8bb0b6a41 100644 ---- a/Source/WebCore/platform/win/DragImageWin.cpp -+++ b/Source/WebCore/platform/win/DragImageWin.cpp -@@ -62,16 +62,22 @@ IntSize dragImageSize(DragImageRef image) - { - if (!image) - return IntSize(); -- BITMAP b; -- GetObject(image, sizeof(BITMAP), &b); -- return IntSize(b.bmWidth, b.bmHeight); -+ return { image->width(), image->height() }; - } - -+#if USE(CAIRO) - void deleteDragImage(DragImageRef image) - { - if (image) - ::DeleteObject(image); - } -+#else -+void deleteDragImage(DragImageRef) -+{ -+ // Since this is a RefPtr, there's nothing additional we need to do to -+ // delete it. It will be released when it falls out of scope. -+} -+#endif - - DragImageRef dissolveDragImageToFraction(DragImageRef image, float) - { -@@ -79,8 +85,9 @@ DragImageRef dissolveDragImageToFraction(DragImageRef image, float) - return image; - } - --DragImageRef createDragImageIconForCachedImageFilename(const String& filename) -+DragImageRef createDragImageIconForCachedImageFilename(const String&) - { -+#if USE(CAIRO) - SHFILEINFO shfi { }; - auto fname = filename.wideCharacters(); - if (FAILED(SHGetFileInfo(fname.data(), FILE_ATTRIBUTE_NORMAL, &shfi, sizeof(shfi), SHGFI_ICON | SHGFI_USEFILEATTRIBUTES))) -@@ -96,6 +103,9 @@ DragImageRef createDragImageIconForCachedImageFilename(const String& filename) - DeleteObject(iconInfo.hbmMask); - - return iconInfo.hbmColor; -+#else -+ return nullptr; -+#endif - } - - #if USE(CAIRO) -@@ -221,9 +231,9 @@ DragImageRef createDragImageForColor(const Color&, const FloatRect&, float, Path - } - - #if USE(SKIA) --DragImageRef createDragImageFromImage(Image*, ImageOrientation, GraphicsClient*, float) -+DragImageRef createDragImageFromImage(Image* image, ImageOrientation, GraphicsClient*, float) - { -- return nullptr; -+ return image->currentNativeImage()->platformImage(); - } - - DragImageRef scaleDragImage(DragImageRef, FloatSize) diff --git a/Source/WebCore/platform/win/KeyEventWin.cpp b/Source/WebCore/platform/win/KeyEventWin.cpp index d450bf9d0fd1f0bf8f28db483ac9d3d60fa9d114..72a59403a0b5493aea4a8e28eb15eac24b652b09 100644 --- a/Source/WebCore/platform/win/KeyEventWin.cpp @@ -7852,7 +7798,7 @@ index d450bf9d0fd1f0bf8f28db483ac9d3d60fa9d114..72a59403a0b5493aea4a8e28eb15eac2 OptionSet PlatformKeyboardEvent::currentStateOfModifierKeys() diff --git a/Source/WebCore/platform/win/PasteboardWin.cpp b/Source/WebCore/platform/win/PasteboardWin.cpp -index 7987d1cd71b05ba4cf09eaf0a8b0d55da6bcffb0..5edaa06c1a648402dc0c3224e69c4721a234aa06 100644 +index e862b1201f827c58d0f6971bd1071e0c0d34d44d..3166f7a8dcb1f9c2d2d505eee846c17b370e76a7 100644 --- a/Source/WebCore/platform/win/PasteboardWin.cpp +++ b/Source/WebCore/platform/win/PasteboardWin.cpp @@ -1144,7 +1144,21 @@ void Pasteboard::writeCustomData(const Vector& data) @@ -8001,86 +7947,6 @@ index 0000000000000000000000000000000000000000..fbd32d390129129fd5b213f7f9c3e96b +} + +} -diff --git a/Source/WebCore/platform/wpe/DragImageWPE.cpp b/Source/WebCore/platform/wpe/DragImageWPE.cpp -new file mode 100644 -index 0000000000000000000000000000000000000000..4383ede4974fb2b938aa01f2f19eb5e0c47f208c ---- /dev/null -+++ b/Source/WebCore/platform/wpe/DragImageWPE.cpp -@@ -0,0 +1,74 @@ -+/* -+ * Copyright (C) 2010,2017 Igalia S.L. -+ * -+ * This library is free software; you can redistribute it and/or -+ * modify it under the terms of the GNU Lesser General Public -+ * License as published by the Free Software Foundation; either -+ * version 2 of the License, or (at your option) any later version. -+ * -+ * This library is distributed in the hope that it will be useful, -+ * but WITHOUT ANY WARRANTY; without even the implied warranty of -+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -+ * Lesser General Public License for more details. -+ * -+ * You should have received a copy of the GNU Lesser General Public -+ * License along with this library; if not, write to the Free Software -+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -+ */ -+ -+#include "config.h" -+#include "DragImage.h" -+#include "NativeImage.h" -+#include "NotImplemented.h" -+ -+#include "Image.h" -+ -+namespace WebCore { -+ -+IntSize dragImageSize(DragImageRef) -+{ -+ notImplemented(); -+ return { 0, 0 }; -+} -+ -+void deleteDragImage(DragImageRef) -+{ -+ notImplemented(); -+} -+ -+DragImageRef scaleDragImage(DragImageRef, FloatSize) -+{ -+ notImplemented(); -+ return nullptr; -+} -+ -+DragImageRef dissolveDragImageToFraction(DragImageRef image, float) -+{ -+ notImplemented(); -+ return image; -+} -+ -+DragImageRef createDragImageFromImage(Image* image, ImageOrientation, GraphicsClient*, float) -+{ -+ return image->currentNativeImage()->platformImage(); -+} -+ -+ -+DragImageRef createDragImageIconForCachedImageFilename(const String&) -+{ -+ notImplemented(); -+ return nullptr; -+} -+ -+DragImageRef createDragImageForLink(Element&, URL&, const String&, TextIndicatorData&, float) -+{ -+ notImplemented(); -+ return nullptr; -+} -+ -+DragImageRef createDragImageForColor(const Color&, const FloatRect&, float, Path&) -+{ -+ return nullptr; -+} -+ -+} diff --git a/Source/WebCore/platform/wpe/PasteboardWPE.cpp b/Source/WebCore/platform/wpe/PasteboardWPE.cpp index c0847a84e4aeba3dac78a8ffe9826d906d33a387..c1c60572473dad33e436ab4f52e5cac5bc2d2f76 100644 --- a/Source/WebCore/platform/wpe/PasteboardWPE.cpp @@ -8280,7 +8146,7 @@ index c0847a84e4aeba3dac78a8ffe9826d906d33a387..c1c60572473dad33e436ab4f52e5cac5 #endif // PLATFORM(WPE) diff --git a/Source/WebCore/rendering/RenderTextControl.cpp b/Source/WebCore/rendering/RenderTextControl.cpp -index f8240cb9f855d0c35268d8c0c8fc2a0b985f4be0..3586db4ccf16a4a6bd174c09fc75915730c45ae2 100644 +index 1238b6ac1b2aa43f0b1f7a74e48e9436d67934fa..261ee362bbdc4fb70450a61da3c872ac3d3a8cc7 100644 --- a/Source/WebCore/rendering/RenderTextControl.cpp +++ b/Source/WebCore/rendering/RenderTextControl.cpp @@ -230,13 +230,13 @@ void RenderTextControl::layoutExcludedChildren(RelayoutChildren relayoutChildren @@ -8297,24 +8163,23 @@ index f8240cb9f855d0c35268d8c0c8fc2a0b985f4be0..3586db4ccf16a4a6bd174c09fc759157 +#if PLATFORM(IOS_FAMILY) int RenderTextControl::innerLineHeight() const { - auto innerText = innerTextElement(); + if (auto innerTextElement = this->innerTextElement(); innerTextElement && innerTextElement->renderer()) diff --git a/Source/WebCore/rendering/RenderTextControl.h b/Source/WebCore/rendering/RenderTextControl.h -index faf34133df0bf205072145ffbab8163b93d3c874..fdc4554952e0e33f8827bb3d00c827dec966ad15 100644 +index 82e32c398dfdfa3e1fdb5dda86e98c827a290f84..c9eddb5739b1cd2ab5b744f57ecb8eb01079ad55 100644 --- a/Source/WebCore/rendering/RenderTextControl.h +++ b/Source/WebCore/rendering/RenderTextControl.h -@@ -38,9 +38,9 @@ public: +@@ -38,8 +38,8 @@ public: WEBCORE_EXPORT HTMLTextFormControlElement& textFormControlElement() const; WEBCORE_EXPORT Ref protectedTextFormControlElement() const; -#if PLATFORM(IOS_FAMILY) bool canScroll() const; - +#if PLATFORM(IOS_FAMILY) - // Returns the line height of the inner renderer. - int innerLineHeight() const override; + WEBCORE_EXPORT int innerLineHeight() const; #endif + diff --git a/Source/WebCore/workers/WorkerConsoleClient.cpp b/Source/WebCore/workers/WorkerConsoleClient.cpp -index 7546c0266801803c9f73179e7a370a4f42a4f05e..05bb106972e648f6fe98aaa1f471390e3a98c674 100644 +index e675901b3a15cbdefdb731c949f8a4234af5dede..97d38d8ef0fb379ab689cb2dacba6563bcaf1e85 100644 --- a/Source/WebCore/workers/WorkerConsoleClient.cpp +++ b/Source/WebCore/workers/WorkerConsoleClient.cpp @@ -124,4 +124,6 @@ void WorkerConsoleClient::recordEnd(JSC::JSGlobalObject*, Ref&& @@ -8337,7 +8202,7 @@ index db95c8273bd0deb3f903a45d02fc07bbbd8ab305..bf88228b4c838b90d11d430cc9429d51 WorkerOrWorkletGlobalScope& m_globalScope; }; diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp -index 23a9f5b5afdb7aba0efdc6a1db7898abc641b0fa..220ed1f59582562268f02262ea2f7ed3388aab1f 100644 +index 2d8a4462ea7897aca4455d526fcb5e0fd6b3dcd3..53f0de10cdfb27736855fa837d9e4a650c18c90a 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp @@ -97,6 +97,8 @@ @@ -8349,7 +8214,7 @@ index 23a9f5b5afdb7aba0efdc6a1db7898abc641b0fa..220ed1f59582562268f02262ea2f7ed3 #endif #if ENABLE(APPLE_PAY_REMOTE_UI) -@@ -1237,6 +1239,14 @@ void NetworkConnectionToWebProcess::clearPageSpecificData(PageIdentifier pageID) +@@ -1232,6 +1234,14 @@ void NetworkConnectionToWebProcess::clearPageSpecificData(PageIdentifier pageID) storageSession->clearPageSpecificDataForResourceLoadStatistics(pageID); } @@ -8363,9 +8228,9 @@ index 23a9f5b5afdb7aba0efdc6a1db7898abc641b0fa..220ed1f59582562268f02262ea2f7ed3 + void NetworkConnectionToWebProcess::removeStorageAccessForFrame(FrameIdentifier frameID, PageIdentifier pageID) { - if (auto* storageSession = protectedNetworkProcess()->storageSession(m_sessionID)) + if (CheckedPtr storageSession = m_networkProcess->storageSession(m_sessionID)) diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h -index 3f1539237c6c8d1cd832cd3ece2ba20939e01a41..1259c36e38d46c0ebfdf98be20f217433f831ca5 100644 +index 96e3d7d1370b114591191dea8e0e042c9f06c0c8..a4ddbd6483b8bd1ffb8f360cad5552dbcbcfc018 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h @@ -388,6 +388,8 @@ private: @@ -8378,7 +8243,7 @@ index 3f1539237c6c8d1cd832cd3ece2ba20939e01a41..1259c36e38d46c0ebfdf98be20f21743 void logUserInteraction(RegistrableDomain&&); diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in -index b10706aafd037a2b92a68b0d2c474ec2e42cd2fe..f80792c02d880dbd61849233fdbc348f1eeffb33 100644 +index 26d43eb49dc956a104782a5e10df084dc2b1e7e8..aa92cfd23b61eae7ee90c9fb7846189f9406e379 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in @@ -80,6 +80,8 @@ messages -> NetworkConnectionToWebProcess WantsDispatchMessage { @@ -8391,10 +8256,10 @@ index b10706aafd037a2b92a68b0d2c474ec2e42cd2fe..f80792c02d880dbd61849233fdbc348f LogUserInteraction(WebCore::RegistrableDomain domain) ResourceLoadStatisticsUpdated(Vector statistics) -> () diff --git a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm -index 00a6ce04cae578c8ac9d934e6c4e38041e4a4aee..ab587ef45e27bddf3e31d28882794f7fb11dea0d 100644 +index 23fbdde0494dae83161c9b715f870db48b8caff1..df27897d278c7bb755378ef309376b50d79124cb 100644 --- a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm +++ b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm -@@ -1148,6 +1148,14 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)data +@@ -1136,6 +1136,14 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)data resourceResponse.setDeprecatedNetworkLoadMetrics(WebCore::copyTimingData(taskMetrics.get(), networkDataTask->networkLoadMetrics())); resourceResponse.setProxyName(WTFMove(proxyName)); @@ -8410,7 +8275,7 @@ index 00a6ce04cae578c8ac9d934e6c4e38041e4a4aee..ab587ef45e27bddf3e31d28882794f7f #if !LOG_DISABLED LOG(NetworkSession, "%llu didReceiveResponse completionHandler (%s)", taskIdentifier, toString(policyAction).characters()); diff --git a/Source/WebKit/NetworkProcess/curl/NetworkDataTaskCurl.cpp b/Source/WebKit/NetworkProcess/curl/NetworkDataTaskCurl.cpp -index 975e4097fb5b9cb610ce67c70ea85e6434ec7a9d..6e2676cd751883e2eae26e38ffaefa873857b604 100644 +index 7b1879c44551fae26e338352189adc5b9add0266..dac8034d87d3bcbbb56576c2ef37713ea0ef85ff 100644 --- a/Source/WebKit/NetworkProcess/curl/NetworkDataTaskCurl.cpp +++ b/Source/WebKit/NetworkProcess/curl/NetworkDataTaskCurl.cpp @@ -166,6 +166,7 @@ void NetworkDataTaskCurl::curlDidReceiveResponse(CurlRequest& request, CurlRespo @@ -8441,7 +8306,7 @@ index aebce13abcf8f93c8fa48936120c2065f0a664b1..7b003cf0d65d0179b165fcbce775cfd5 ;; Except deny access to new-style iOS Keychain folders which are UUIDs. (deny file-read* file-write* diff --git a/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.cpp b/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.cpp -index 983947a9ad3abf138c1a6052807c4e86beb5c1d1..c5de2e1f1c74317ee00b9558e0e5c3093496c386 100644 +index 375489f4b3944b7b6f4ac03b1784aef8ca116206..6cce36bd75b872b45b7a6ecc07e2adff00bbc7a4 100644 --- a/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.cpp +++ b/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.cpp @@ -461,6 +461,8 @@ void NetworkDataTaskSoup::didSendRequest(GRefPtr&& inputStream) @@ -8454,7 +8319,7 @@ index 983947a9ad3abf138c1a6052807c4e86beb5c1d1..c5de2e1f1c74317ee00b9558e0e5c309 } diff --git a/Source/WebKit/PlatformGTK.cmake b/Source/WebKit/PlatformGTK.cmake -index 1f0116517d88a3a0fffade7288a4909bd848df88..83b9598353ad6dd9192b0c8827bba25d1cb069ad 100644 +index d39a11b312cc9237973ee095e995afa379da1b0d..e5fc9f02cea359414154756b27958019cbb57374 100644 --- a/Source/WebKit/PlatformGTK.cmake +++ b/Source/WebKit/PlatformGTK.cmake @@ -320,6 +320,9 @@ list(APPEND WebKit_SYSTEM_INCLUDE_DIRECTORIES @@ -8467,7 +8332,7 @@ index 1f0116517d88a3a0fffade7288a4909bd848df88..83b9598353ad6dd9192b0c8827bba25d ) list(APPEND WebKit_INTERFACE_INCLUDE_DIRECTORIES -@@ -359,6 +362,9 @@ if (USE_LIBWEBRTC) +@@ -363,6 +366,9 @@ if (USE_LIBWEBRTC) list(APPEND WebKit_SYSTEM_INCLUDE_DIRECTORIES "${THIRDPARTY_DIR}/libwebrtc/Source/" "${THIRDPARTY_DIR}/libwebrtc/Source/webrtc" @@ -8477,7 +8342,7 @@ index 1f0116517d88a3a0fffade7288a4909bd848df88..83b9598353ad6dd9192b0c8827bba25d ) endif () -@@ -410,6 +416,12 @@ else () +@@ -414,6 +420,12 @@ else () set(WebKitGTK_ENUM_HEADER_TEMPLATE ${WEBKIT_DIR}/UIProcess/API/gtk/WebKitEnumTypesGtk3.h.in) endif () @@ -8491,7 +8356,7 @@ index 1f0116517d88a3a0fffade7288a4909bd848df88..83b9598353ad6dd9192b0c8827bba25d set(WebKitGTK_ENUM_GENERATION_HEADERS ${WebKitGTK_INSTALLED_HEADERS}) list(REMOVE_ITEM WebKitGTK_ENUM_GENERATION_HEADERS ${WebKitGTK_DERIVED_SOURCES_DIR}/webkit/WebKitEnumTypes.h) diff --git a/Source/WebKit/PlatformWPE.cmake b/Source/WebKit/PlatformWPE.cmake -index 56b8ece0181ad1dacd3e33ee1463ac1cc4b2ac20..fb0b4169f860e9064397216b176024760a3503cd 100644 +index 650a918189ad22a6fa76404c80ebca477331c193..74fc793279465c3aa9a586130a98c182e1334568 100644 --- a/Source/WebKit/PlatformWPE.cmake +++ b/Source/WebKit/PlatformWPE.cmake @@ -221,6 +221,7 @@ set(WPE_API_HEADER_TEMPLATES @@ -8600,7 +8465,7 @@ index 35fd0f0397cd92c5bf025d89d7f7c8139c03c69d..b1559f1bb2d2d7a74d0b534f81352e36 } // namespace WebKit diff --git a/Source/WebKit/Shared/NativeWebKeyboardEvent.h b/Source/WebKit/Shared/NativeWebKeyboardEvent.h -index c72c9733800b6f836c4d3ccb0b50d40c3ee83067..e2955ddebe388d886ca43d733dce0eb58256ce8b 100644 +index 60a6308dbbd3f9a09c82214dcd97f86dda9d2078..31036d180a6f319d17c53a363d58d8bad1550a97 100644 --- a/Source/WebKit/Shared/NativeWebKeyboardEvent.h +++ b/Source/WebKit/Shared/NativeWebKeyboardEvent.h @@ -33,6 +33,7 @@ @@ -8682,10 +8547,10 @@ index f8e96218fd2671d1c0aca5e549efe0d8b94ef0f9..6cebd61bceb39c08e916fe991e4c3fc6 NSEvent* nativeEvent() const { return m_nativeEvent.get(); } #elif PLATFORM(GTK) diff --git a/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in b/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in -index e59ca0120c0a684ea1fbdb2262a6a89ccc848828..4fcb529af731becaf446538be42075bfc19bbf90 100644 +index 6bb3cc33a43da97e85efe8afb8e7ea2400dd4787..79e2b2f0196d3edb62b2219d59229bb2822e549f 100644 --- a/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in +++ b/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in -@@ -2827,6 +2827,9 @@ class WebCore::AuthenticationChallenge { +@@ -2841,6 +2841,9 @@ class WebCore::AuthenticationChallenge { class WebCore::DragData { #if PLATFORM(COCOA) String pasteboardName(); @@ -8695,7 +8560,7 @@ index e59ca0120c0a684ea1fbdb2262a6a89ccc848828..4fcb529af731becaf446538be42075bf #endif WebCore::IntPoint clientPosition(); WebCore::IntPoint globalPosition(); -@@ -3625,6 +3628,7 @@ enum class WebCore::WasPrivateRelayed : bool; +@@ -3649,6 +3652,7 @@ enum class WebCore::WasPrivateRelayed : bool; String httpStatusText; String httpVersion; WebCore::HTTPHeaderMap httpHeaderFields; @@ -8821,10 +8686,10 @@ index 20a6e465457151b02daa22e6bc059cf0e117ece5..ef4b1f737aaa683bc13c447aa4ca77e5 void setPosition(const WebCore::IntPoint& position) { m_position = position; } const WebCore::IntPoint& globalPosition() const { return m_globalPosition; } diff --git a/Source/WebKit/Shared/WebPageCreationParameters.h b/Source/WebKit/Shared/WebPageCreationParameters.h -index 36e44162c6f211876bf86b20e186f3da7e895536..68fe668470fef43c7a3af7a5c45ca4bac1fbbb28 100644 +index 02b701cbe003b0f9e16b9712e98c4157ebedbd69..70269920251e4d31f2c73fddc2f8cecbff453498 100644 --- a/Source/WebKit/Shared/WebPageCreationParameters.h +++ b/Source/WebKit/Shared/WebPageCreationParameters.h -@@ -301,6 +301,8 @@ struct WebPageCreationParameters { +@@ -303,6 +303,8 @@ struct WebPageCreationParameters { WebCore::ShouldRelaxThirdPartyCookieBlocking shouldRelaxThirdPartyCookieBlocking { WebCore::ShouldRelaxThirdPartyCookieBlocking::No }; bool httpsUpgradeEnabled { true }; @@ -8834,10 +8699,10 @@ index 36e44162c6f211876bf86b20e186f3da7e895536..68fe668470fef43c7a3af7a5c45ca4ba #if ENABLE(APP_HIGHLIGHTS) WebCore::HighlightVisibility appHighlightsVisible { WebCore::HighlightVisibility::Hidden }; diff --git a/Source/WebKit/Shared/WebPageCreationParameters.serialization.in b/Source/WebKit/Shared/WebPageCreationParameters.serialization.in -index 6df2cea7d9ba7328456822475ed765e33966d4b8..39c08e228a2346b18915bc90e76965e0586d006b 100644 +index 822229ba75d9afe290cc72966cd763bd476f76e1..4487e71729e4c874ffec33374f25aee2ce289c3e 100644 --- a/Source/WebKit/Shared/WebPageCreationParameters.serialization.in +++ b/Source/WebKit/Shared/WebPageCreationParameters.serialization.in -@@ -223,6 +223,8 @@ enum class WebCore::UserInterfaceLayoutDirection : bool; +@@ -225,6 +225,8 @@ enum class WebCore::UserInterfaceLayoutDirection : bool; bool httpsUpgradeEnabled; @@ -8847,7 +8712,7 @@ index 6df2cea7d9ba7328456822475ed765e33966d4b8..39c08e228a2346b18915bc90e76965e0 WebCore::HighlightVisibility appHighlightsVisible; #endif diff --git a/Source/WebKit/Shared/glib/ProcessExecutablePathGLib.cpp b/Source/WebKit/Shared/glib/ProcessExecutablePathGLib.cpp -index 9899d60864664d1abff2b71c1c01e564e5dfb08c..391e0e42ca6a39f82b5a12c6aede069d61095ee2 100644 +index b41816f4ba670a7058014f078ef02441c3ab0abf..983d8679574a2555b348ab0df7699253b7d4a9fe 100644 --- a/Source/WebKit/Shared/glib/ProcessExecutablePathGLib.cpp +++ b/Source/WebKit/Shared/glib/ProcessExecutablePathGLib.cpp @@ -32,7 +32,7 @@ @@ -8869,10 +8734,10 @@ index 9899d60864664d1abff2b71c1c01e564e5dfb08c..391e0e42ca6a39f82b5a12c6aede069d if (execDirectory) { String processPath = FileSystem::pathByAppendingComponent(FileSystem::stringFromFileSystemRepresentation(execDirectory), StringView::fromLatin1(processName)); diff --git a/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp b/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp -index 8d33ceb065fb3e90372b0c696779189d07838da0..6e3194c3e96e46bfa09f8d706324e6515df1e7f4 100644 +index 8aa8b7623aad418b7ddd81f46c803d9c59d6a943..ac3c89fbb0ec41afe841cffebf9250304581d768 100644 --- a/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp +++ b/Source/WebKit/Shared/gtk/NativeWebKeyboardEventGtk.cpp -@@ -51,12 +51,12 @@ NativeWebKeyboardEvent::NativeWebKeyboardEvent(const String& text, std::optional +@@ -51,7 +51,7 @@ NativeWebKeyboardEvent::NativeWebKeyboardEvent(const String& text, std::optional } NativeWebKeyboardEvent::NativeWebKeyboardEvent(WebEventType type, const String& text, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, Vector&& commands, bool isAutoRepeat, bool isKeypad, OptionSet modifiers) @@ -8881,25 +8746,6 @@ index 8d33ceb065fb3e90372b0c696779189d07838da0..6e3194c3e96e46bfa09f8d706324e651 { } - NativeWebKeyboardEvent::NativeWebKeyboardEvent(const NativeWebKeyboardEvent& event) -- : WebKeyboardEvent(WebEvent(event.type(), event.modifiers(), event.timestamp()), event.text(), event.key(), event.code(), event.keyIdentifier(), event.windowsVirtualKeyCode(), event.nativeVirtualKeyCode(), event.handledByInputMethod(), std::optional>(event.preeditUnderlines()), std::optional(event.preeditSelectionRange()), Vector(event.commands()), event.isAutoRepeat(), event.isKeypad()) -+ : WebKeyboardEvent(event) - , m_nativeEvent(event.nativeEvent() ? constructNativeEvent(event.nativeEvent()) : nullptr) - { - } -diff --git a/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp b/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp -index 9a1c3f09c756ea368ac2d68e183a13e2eb47ead7..01c738376230f83376d80d6d225543a3914943dd 100644 ---- a/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp -+++ b/Source/WebKit/Shared/gtk/NativeWebMouseEventGtk.cpp -@@ -61,7 +61,7 @@ NativeWebMouseEvent::NativeWebMouseEvent(WebEventType type, WebMouseEventButton - } - - NativeWebMouseEvent::NativeWebMouseEvent(const NativeWebMouseEvent& event) -- : WebMouseEvent(WebEvent(event.type(), event.modifiers(), event.timestamp()), event.button(), event.buttons(), event.position(), event.globalPosition(), event.deltaX(), event.deltaY(), event.deltaZ(), event.clickCount(), 0, WebMouseEventSyntheticClickType::NoTap, event.isTouchEvent(), event.pointerId(), event.pointerType()) -+ : WebMouseEvent(event) - , m_nativeEvent(event.nativeEvent() ? constructNativeEvent(const_cast(event.nativeEvent())) : nullptr) - { - } diff --git a/Source/WebKit/Shared/unix/AuxiliaryProcessMain.cpp b/Source/WebKit/Shared/unix/AuxiliaryProcessMain.cpp index 7fcd22cd2172cd7fa77aee12ad5cfcf7a435abba..bc822b40eea889fb0499dd4e78f89f04d87c64a1 100644 --- a/Source/WebKit/Shared/unix/AuxiliaryProcessMain.cpp @@ -8947,7 +8793,7 @@ index 053e9336017d8818b3cbea79ce7c145fd5c46274..5632498d6ef875df80fc68ec206a9d08 JSC::Config::configureForTesting(); else if (!strcmp(argv[i], "-disable-jit")) diff --git a/Source/WebKit/Sources.txt b/Source/WebKit/Sources.txt -index bd7ec1f455c83cb4d24185099b6c7f569e612a33..cf4183ca9b2bb85b00bb58a4bc0c4d6b6f44af76 100644 +index a978050575b323345d0de29d1abd1fa0bc993e77..f7ead71d28f77932edb9e7f8958b25e99b50fead 100644 --- a/Source/WebKit/Sources.txt +++ b/Source/WebKit/Sources.txt @@ -392,6 +392,7 @@ UIProcess/AboutSchemeHandler.cpp @@ -8958,7 +8804,7 @@ index bd7ec1f455c83cb4d24185099b6c7f569e612a33..cf4183ca9b2bb85b00bb58a4bc0c4d6b UIProcess/DeviceIdHashSaltStorage.cpp UIProcess/DisplayLink.cpp UIProcess/DisplayLinkProcessProxyClient.cpp -@@ -401,16 +402,20 @@ UIProcess/FrameLoadState.cpp +@@ -401,6 +402,8 @@ UIProcess/FrameLoadState.cpp UIProcess/FrameProcess.cpp UIProcess/GeolocationPermissionRequestManagerProxy.cpp UIProcess/GeolocationPermissionRequestProxy.cpp @@ -8967,8 +8813,9 @@ index bd7ec1f455c83cb4d24185099b6c7f569e612a33..cf4183ca9b2bb85b00bb58a4bc0c4d6b UIProcess/LegacyGlobalSettings.cpp UIProcess/MediaKeySystemPermissionRequestManagerProxy.cpp UIProcess/MediaKeySystemPermissionRequestProxy.cpp - UIProcess/ModelElementController.cpp +@@ -408,10 +411,12 @@ UIProcess/ModelElementController.cpp UIProcess/OverrideLanguages.cpp + UIProcess/PageClient.cpp UIProcess/PageLoadState.cpp +UIProcess/PlaywrightFullScreenManagerProxyClient.cpp UIProcess/ProcessAssertion.cpp @@ -8978,8 +8825,8 @@ index bd7ec1f455c83cb4d24185099b6c7f569e612a33..cf4183ca9b2bb85b00bb58a4bc0c4d6b +UIProcess/RemoteInspectorPipe.cpp UIProcess/RemotePageDrawingAreaProxy.cpp UIProcess/RemotePageFullscreenManagerProxy.cpp - UIProcess/RemotePageProxy.cpp -@@ -453,6 +458,8 @@ UIProcess/WebOpenPanelResultListenerProxy.cpp + UIProcess/RemotePagePlaybackSessionManagerProxy.cpp +@@ -456,6 +461,8 @@ UIProcess/WebOpenPanelResultListenerProxy.cpp UIProcess/WebPageDiagnosticLoggingClient.cpp UIProcess/WebPageGroup.cpp UIProcess/WebPageInjectedBundleClient.cpp @@ -8988,7 +8835,7 @@ index bd7ec1f455c83cb4d24185099b6c7f569e612a33..cf4183ca9b2bb85b00bb58a4bc0c4d6b UIProcess/WebPageProxy.cpp UIProcess/WebPageProxyMessageReceiverRegistration.cpp UIProcess/WebPageProxyTesting.cpp -@@ -604,6 +611,9 @@ UIProcess/Inspector/WebPageDebuggable.cpp +@@ -609,6 +616,9 @@ UIProcess/Inspector/WebPageDebuggable.cpp UIProcess/Inspector/WebPageInspectorController.cpp UIProcess/Inspector/Agents/InspectorBrowserAgent.cpp @@ -8999,10 +8846,10 @@ index bd7ec1f455c83cb4d24185099b6c7f569e612a33..cf4183ca9b2bb85b00bb58a4bc0c4d6b UIProcess/Media/AudioSessionRoutingArbitratorProxy.cpp UIProcess/Media/MediaUsageManager.cpp diff --git a/Source/WebKit/SourcesCocoa.txt b/Source/WebKit/SourcesCocoa.txt -index c146d8baabb0bc80396dc07d7cde88207ef02efa..b13cb0a6e8e10dcf3a068a4bde97536d490f3b52 100644 +index 4e2c7431cfc5c1b952232c30d6aa7f46815813c7..095c1b78812df547ab8cbe62e4ac801b5eb98968 100644 --- a/Source/WebKit/SourcesCocoa.txt +++ b/Source/WebKit/SourcesCocoa.txt -@@ -272,6 +272,7 @@ UIProcess/API/Cocoa/_WKArchiveExclusionRule.mm +@@ -270,6 +270,7 @@ UIProcess/API/Cocoa/_WKArchiveExclusionRule.mm UIProcess/API/Cocoa/_WKAttachment.mm UIProcess/API/Cocoa/_WKAutomationSession.mm UIProcess/API/Cocoa/_WKAutomationSessionConfiguration.mm @@ -9010,7 +8857,7 @@ index c146d8baabb0bc80396dc07d7cde88207ef02efa..b13cb0a6e8e10dcf3a068a4bde97536d UIProcess/API/Cocoa/_WKContentRuleListAction.mm UIProcess/API/Cocoa/_WKContextMenuElementInfo.mm UIProcess/API/Cocoa/_WKCustomHeaderFields.mm @no-unify -@@ -469,6 +470,7 @@ UIProcess/Inspector/ios/WKInspectorHighlightView.mm +@@ -467,6 +468,7 @@ UIProcess/Inspector/ios/WKInspectorHighlightView.mm UIProcess/Inspector/ios/WKInspectorNodeSearchGestureRecognizer.mm UIProcess/Inspector/mac/RemoteWebInspectorUIProxyMac.mm @@ -9019,7 +8866,7 @@ index c146d8baabb0bc80396dc07d7cde88207ef02efa..b13cb0a6e8e10dcf3a068a4bde97536d UIProcess/Inspector/mac/WKInspectorResourceURLSchemeHandler.mm UIProcess/Inspector/mac/WKInspectorViewController.mm diff --git a/Source/WebKit/SourcesGTK.txt b/Source/WebKit/SourcesGTK.txt -index 5bf7f2a0c8d6e6d32a2845885b943064618fe951..9d8bfa3d2cf0920dcfdae5b95563c5cab5e755f4 100644 +index 68733a26a5cb0cd8f944b5ab48eacb256e147976..b8d320e417303329b9a6fa56ce4c3f6fccc286b8 100644 --- a/Source/WebKit/SourcesGTK.txt +++ b/Source/WebKit/SourcesGTK.txt @@ -122,6 +122,7 @@ UIProcess/API/glib/WebKitAutomationSession.cpp @no-unify @@ -9030,15 +8877,15 @@ index 5bf7f2a0c8d6e6d32a2845885b943064618fe951..9d8bfa3d2cf0920dcfdae5b95563c5ca UIProcess/API/glib/WebKitContextMenuClient.cpp @no-unify UIProcess/API/glib/WebKitCookieManager.cpp @no-unify UIProcess/API/glib/WebKitCredential.cpp @no-unify -@@ -253,6 +254,7 @@ UIProcess/glib/DisplayVBlankMonitor.cpp - UIProcess/glib/DisplayVBlankMonitorDRM.cpp +@@ -254,6 +255,7 @@ UIProcess/glib/DisplayVBlankMonitorDRM.cpp + UIProcess/glib/DisplayVBlankMonitorThreaded.cpp UIProcess/glib/DisplayVBlankMonitorTimer.cpp UIProcess/glib/FenceMonitor.cpp +UIProcess/glib/InspectorPlaywrightAgentClientGLib.cpp UIProcess/glib/ScreenManager.cpp UIProcess/glib/SystemSettingsManagerProxy.cpp UIProcess/glib/WebPageProxyGLib.cpp -@@ -271,6 +273,7 @@ UIProcess/gtk/DisplayX11.cpp @no-unify +@@ -272,6 +274,7 @@ UIProcess/gtk/DisplayX11.cpp @no-unify UIProcess/gtk/DisplayWayland.cpp @no-unify UIProcess/gtk/WebDateTimePickerGtk.cpp UIProcess/gtk/HardwareAccelerationManager.cpp @@ -9046,7 +8893,7 @@ index 5bf7f2a0c8d6e6d32a2845885b943064618fe951..9d8bfa3d2cf0920dcfdae5b95563c5ca UIProcess/gtk/KeyBindingTranslator.cpp UIProcess/gtk/PointerLockManager.cpp @no-unify UIProcess/gtk/PointerLockManagerWayland.cpp @no-unify -@@ -284,6 +287,8 @@ UIProcess/gtk/ViewGestureControllerGtk.cpp +@@ -285,6 +288,8 @@ UIProcess/gtk/ViewGestureControllerGtk.cpp UIProcess/gtk/WebColorPickerGtk.cpp UIProcess/gtk/WebContextMenuProxyGtk.cpp UIProcess/gtk/WebDataListSuggestionsDropdownGtk.cpp @@ -9056,7 +8903,7 @@ index 5bf7f2a0c8d6e6d32a2845885b943064618fe951..9d8bfa3d2cf0920dcfdae5b95563c5ca UIProcess/gtk/WebPasteboardProxyGtk.cpp UIProcess/gtk/WebPopupMenuProxyGtk.cpp diff --git a/Source/WebKit/SourcesWPE.txt b/Source/WebKit/SourcesWPE.txt -index 8569ab98ebf37a8e25c9e79f99a26c76b175d2c8..48c634968f8dcf42ca2690c07aa1b2c4c83d5884 100644 +index f3647727106bd3786d17adaf0c5f373184ace9fb..419a5a7002ed6567f0110fa63ff723691f027669 100644 --- a/Source/WebKit/SourcesWPE.txt +++ b/Source/WebKit/SourcesWPE.txt @@ -124,6 +124,7 @@ UIProcess/API/glib/WebKitAuthenticationRequest.cpp @no-unify @@ -9075,21 +8922,22 @@ index 8569ab98ebf37a8e25c9e79f99a26c76b175d2c8..48c634968f8dcf42ca2690c07aa1b2c4 UIProcess/API/glib/WebKitPolicyDecision.cpp @no-unify UIProcess/API/glib/WebKitPrivate.cpp @no-unify UIProcess/API/glib/WebKitProtocolHandler.cpp @no-unify -@@ -225,6 +227,7 @@ UIProcess/glib/DisplayVBlankMonitor.cpp - UIProcess/glib/DisplayVBlankMonitorDRM.cpp +@@ -226,6 +228,7 @@ UIProcess/glib/DisplayVBlankMonitorDRM.cpp + UIProcess/glib/DisplayVBlankMonitorThreaded.cpp UIProcess/glib/DisplayVBlankMonitorTimer.cpp UIProcess/glib/FenceMonitor.cpp +UIProcess/glib/InspectorPlaywrightAgentClientGLib.cpp UIProcess/glib/ScreenManager.cpp UIProcess/glib/SystemSettingsManagerProxy.cpp UIProcess/glib/WebPageProxyGLib.cpp -@@ -256,8 +259,14 @@ UIProcess/linux/MemoryPressureMonitor.cpp - UIProcess/soup/WebProcessPoolSoup.cpp +@@ -258,9 +261,15 @@ UIProcess/soup/WebProcessPoolSoup.cpp UIProcess/wpe/AcceleratedBackingStoreDMABuf.cpp + UIProcess/wpe/DisplayVBlankMonitorWPE.cpp +UIProcess/wpe/InspectorTargetProxyWPE.cpp UIProcess/wpe/ScreenManagerWPE.cpp UIProcess/wpe/SystemSettingsManagerProxyWPE.cpp + UIProcess/wpe/WPEUtilities.cpp +UIProcess/wpe/WebColorPickerWPE.cpp +UIProcess/wpe/WebDataListSuggestionsDropdownWPE.cpp +UIProcess/wpe/WebDateTimePickerWPE.cpp @@ -9098,7 +8946,7 @@ index 8569ab98ebf37a8e25c9e79f99a26c76b175d2c8..48c634968f8dcf42ca2690c07aa1b2c4 UIProcess/wpe/WebPageProxyWPE.cpp UIProcess/wpe/WebPasteboardProxyWPE.cpp UIProcess/wpe/WebPreferencesWPE.cpp -@@ -285,6 +294,8 @@ WebProcess/WebCoreSupport/glib/WebEditorClientGLib.cpp +@@ -292,6 +301,8 @@ WebProcess/WebCoreSupport/glib/WebEditorClientGLib.cpp WebProcess/WebCoreSupport/soup/WebFrameNetworkingContext.cpp @@ -9108,11 +8956,11 @@ index 8569ab98ebf37a8e25c9e79f99a26c76b175d2c8..48c634968f8dcf42ca2690c07aa1b2c4 WebProcess/WebPage/AcceleratedSurface.cpp diff --git a/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp b/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp -index 64952ccd02d9db28c2e1388ce7713703430c8212..c616408e2d09f6d5d98dfd609a51ad080ebb990c 100644 +index 1182f2afe2c857598449d3dee6c1be028746c595..4256f65734adf9c68a30d70cc49c0b3a6fec1b1a 100644 --- a/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp +++ b/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp -@@ -268,6 +268,11 @@ WebPageProxy* PageConfiguration::relatedPage() const - return m_data.relatedPage.get(); +@@ -273,6 +273,11 @@ RefPtr PageConfiguration::protectedRelatedPage() const + return relatedPage(); } +WebKit::WebPageProxy* PageConfiguration::openerPageForInspector() const @@ -9124,12 +8972,12 @@ index 64952ccd02d9db28c2e1388ce7713703430c8212..c616408e2d09f6d5d98dfd609a51ad08 { return m_data.pageToCloneSessionStorageFrom.get(); diff --git a/Source/WebKit/UIProcess/API/APIPageConfiguration.h b/Source/WebKit/UIProcess/API/APIPageConfiguration.h -index ade928c03e6e504dc485e75427c56edfb0b1a4a4..1eef15db8aaecea65be8ec3be5d9be903055e840 100644 +index 43a17d77b13b0e51217506425607b8e0698062e4..96c7747d26cfa1d196796c784c046f6c17666969 100644 --- a/Source/WebKit/UIProcess/API/APIPageConfiguration.h +++ b/Source/WebKit/UIProcess/API/APIPageConfiguration.h -@@ -159,6 +159,10 @@ public: - WebKit::WebPageProxy* relatedPage() const; +@@ -160,6 +160,10 @@ public: void setRelatedPage(WeakPtr&& relatedPage) { m_data.relatedPage = WTFMove(relatedPage); } + RefPtr protectedRelatedPage() const; + // This is similar to relatedPage(), but it is also set for noopener links. + WebKit::WebPageProxy* openerPageForInspector() const; @@ -9138,7 +8986,7 @@ index ade928c03e6e504dc485e75427c56edfb0b1a4a4..1eef15db8aaecea65be8ec3be5d9be90 WebKit::WebPageProxy* pageToCloneSessionStorageFrom() const; void setPageToCloneSessionStorageFrom(WeakPtr&&); -@@ -515,6 +519,7 @@ private: +@@ -516,6 +520,7 @@ private: #endif RefPtr pageGroup; WeakPtr relatedPage; @@ -9162,7 +9010,7 @@ index e256b905bf9727aa7c8a48012237a6a6bc9acdbc..4e855c441af6f235f0fd8dfdd57b9bd6 copy->m_shouldTakeUIBackgroundAssertion = this->m_shouldTakeUIBackgroundAssertion; copy->m_shouldCaptureDisplayInUIProcess = this->m_shouldCaptureDisplayInUIProcess; diff --git a/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.h b/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.h -index af4944e5a5d373bc51995b50d1ea7c70f64ef1b3..403cfdeda26db2b648e32aa0e5f3ef6e076634f6 100644 +index 2fe09a750bf32bd6f124603af08e16adba8a6c7c..5ca91400765fd53559b4f4cefa172b5a1f417196 100644 --- a/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.h +++ b/Source/WebKit/UIProcess/API/APIProcessPoolConfiguration.h @@ -96,6 +96,16 @@ public: @@ -9194,7 +9042,7 @@ index af4944e5a5d373bc51995b50d1ea7c70f64ef1b3..403cfdeda26db2b648e32aa0e5f3ef6e bool m_shouldTakeUIBackgroundAssertion { true }; bool m_shouldCaptureDisplayInUIProcess { DEFAULT_CAPTURE_DISPLAY_IN_UI_PROCESS }; diff --git a/Source/WebKit/UIProcess/API/APIUIClient.h b/Source/WebKit/UIProcess/API/APIUIClient.h -index b0450278b8949985df9f39c7d7a5adbaecb55afe..ac80a31058249d467d21e64f5a053cf0f0012fe1 100644 +index b5c743d02f89afbe0c9a90e5f7e746fbd31fbbb2..29dbedfcf6929788b4810b8e98acaee26d1a686d 100644 --- a/Source/WebKit/UIProcess/API/APIUIClient.h +++ b/Source/WebKit/UIProcess/API/APIUIClient.h @@ -114,6 +114,7 @@ public: @@ -9206,7 +9054,7 @@ index b0450278b8949985df9f39c7d7a5adbaecb55afe..ac80a31058249d467d21e64f5a053cf0 virtual void setStatusText(WebKit::WebPageProxy*, const WTF::String&) { } virtual void mouseDidMoveOverElement(WebKit::WebPageProxy&, const WebKit::WebHitTestResultData&, OptionSet, Object*) { } diff --git a/Source/WebKit/UIProcess/API/C/WKInspector.cpp b/Source/WebKit/UIProcess/API/C/WKInspector.cpp -index bba39f6228ae9cc67c68526c75676648cf3917ef..f1c6f6f14ef0d599bd05410edbe9360e75d6d77a 100644 +index 1aa386d598a35dbda34161ed956d653a2da99bec..9aaeb894594f797931bac01314eb3e2ca8669855 100644 --- a/Source/WebKit/UIProcess/API/C/WKInspector.cpp +++ b/Source/WebKit/UIProcess/API/C/WKInspector.cpp @@ -28,6 +28,11 @@ @@ -9222,7 +9070,7 @@ index bba39f6228ae9cc67c68526c75676648cf3917ef..f1c6f6f14ef0d599bd05410edbe9360e #include "WebFrameProxy.h" #include "WebInspectorUIProxy.h" @@ -131,4 +136,11 @@ void WKInspectorToggleElementSelection(WKInspectorRef inspectorRef) - toImpl(inspectorRef)->toggleElementSelection(); + toProtectedImpl(inspectorRef)->toggleElementSelection(); } +void WKInspectorInitializeRemoteInspectorPipe(ConfigureDataStoreCallback configureDataStore, CreatePageCallback createPage, QuitCallback quit) @@ -9249,10 +9097,10 @@ index 026121d114c5fcad84c1396be8d692625beaa3bd..edd6e5cae033124c589959a42522fde0 } #endif diff --git a/Source/WebKit/UIProcess/API/C/WKPage.cpp b/Source/WebKit/UIProcess/API/C/WKPage.cpp -index 908163888f737cb72569f6ff36dae276a51118eb..636ccf3834aaa47328388a008dbc2bf755ecdf70 100644 +index e15be39b349437382ef59f303986dac5246856dd..08b0290d847a046a5861475b7a5015513b22551f 100644 --- a/Source/WebKit/UIProcess/API/C/WKPage.cpp +++ b/Source/WebKit/UIProcess/API/C/WKPage.cpp -@@ -1935,6 +1935,13 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient +@@ -1943,6 +1943,13 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient m_client.addMessageToConsole(toAPI(&page), toAPI(message.impl()), m_client.base.clientInfo); } @@ -9266,7 +9114,7 @@ index 908163888f737cb72569f6ff36dae276a51118eb..636ccf3834aaa47328388a008dbc2bf7 void setStatusText(WebPageProxy* page, const String& text) final { if (!m_client.setStatusText) -@@ -1964,6 +1971,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient +@@ -1972,6 +1979,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient { if (!m_client.didNotHandleKeyEvent) return; @@ -9335,39 +9183,6 @@ index fc43c44a85a0fc6bf5f8c643bd120a16ce762914..ee86fd213d25682f9b6553ec7da99bc8 // Version 15. WKPageDecidePolicyForSpeechRecognitionPermissionRequestCallback decidePolicyForSpeechRecognitionPermissionRequest; -diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKPreferences.mm b/Source/WebKit/UIProcess/API/Cocoa/WKPreferences.mm -index f2ee51f3f5dc9c55bcfe96d5bd52268957cc5e1a..472eb2530e812d50ecc38d9bfaa5bbe9948bad68 100644 ---- a/Source/WebKit/UIProcess/API/Cocoa/WKPreferences.mm -+++ b/Source/WebKit/UIProcess/API/Cocoa/WKPreferences.mm -@@ -712,6 +712,16 @@ - (void)_setMediaCaptureRequiresSecureConnection:(BOOL)requiresSecureConnection - _preferences->setMediaCaptureRequiresSecureConnection(requiresSecureConnection); - } - -+- (BOOL)_alternateWebMPlayerEnabled -+{ -+ return _preferences->alternateWebMPlayerEnabled(); -+} -+ -+- (void)_setAlternateWebMPlayerEnabled:(BOOL)enabled -+{ -+ _preferences->setAlternateWebMPlayerEnabled(enabled); -+} -+ - - (double)_inactiveMediaCaptureStreamRepromptIntervalInMinutes - { - return _preferences->inactiveMediaCaptureStreamRepromptIntervalInMinutes(); -diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKPreferencesPrivate.h b/Source/WebKit/UIProcess/API/Cocoa/WKPreferencesPrivate.h -index 41525c8980f698aa4326e7d4d232311cee2c43d5..1f1745431c6cc7af66b47dd5d015c52389626e6f 100644 ---- a/Source/WebKit/UIProcess/API/Cocoa/WKPreferencesPrivate.h -+++ b/Source/WebKit/UIProcess/API/Cocoa/WKPreferencesPrivate.h -@@ -119,6 +119,7 @@ typedef NS_ENUM(NSInteger, _WKPitchCorrectionAlgorithm) { - @property (nonatomic, setter=_setMockCaptureDevicesEnabled:) BOOL _mockCaptureDevicesEnabled WK_API_AVAILABLE(macos(10.13), ios(11.0)); - @property (nonatomic, setter=_setMockCaptureDevicesPromptEnabled:) BOOL _mockCaptureDevicesPromptEnabled WK_API_AVAILABLE(macos(10.13.4), ios(11.3)); - @property (nonatomic, setter=_setMediaCaptureRequiresSecureConnection:) BOOL _mediaCaptureRequiresSecureConnection WK_API_AVAILABLE(macos(10.13), ios(11.0)); -+@property (nonatomic, setter=_setAlternateWebMPlayerEnabled:) BOOL _alternateWebMPlayerEnabled WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA)); - @property (nonatomic, setter=_setEnumeratingAllNetworkInterfacesEnabled:) BOOL _enumeratingAllNetworkInterfacesEnabled WK_API_AVAILABLE(macos(10.13), ios(11.0)); - @property (nonatomic, setter=_setICECandidateFilteringEnabled:) BOOL _iceCandidateFilteringEnabled WK_API_AVAILABLE(macos(10.13.4), ios(11.3)); - @property (nonatomic, setter=_setInactiveMediaCaptureStreamRepromptIntervalInMinutes:) double _inactiveMediaCaptureStreamRepromptIntervalInMinutes WK_API_AVAILABLE(macos(10.13.4), ios(11.3)); diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKUIDelegate.h b/Source/WebKit/UIProcess/API/Cocoa/WKUIDelegate.h index 2ce017d213d7875eee965e554af6befb5a3c3908..79b11310d358d3edc49e30420728e1eb905309cb 100644 --- a/Source/WebKit/UIProcess/API/Cocoa/WKUIDelegate.h @@ -9399,7 +9214,7 @@ index 930357ac3469195e9f33d5ffce92777018bb0b13..f62555ec562f8416976d31692e8fb175 NS_ASSUME_NONNULL_END diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKWebsiteDataStore.mm b/Source/WebKit/UIProcess/API/Cocoa/WKWebsiteDataStore.mm -index 7e793fe01f479b9135c54253af2114ce61924fa0..d22dc512f73af0e4152b4503866ba2a4cf5c3e5e 100644 +index e5c1fb49feda418fe4989474a7145016529409c5..ef1ffc4e07e9af85c50bb51d60d0af87c4352eeb 100644 --- a/Source/WebKit/UIProcess/API/Cocoa/WKWebsiteDataStore.mm +++ b/Source/WebKit/UIProcess/API/Cocoa/WKWebsiteDataStore.mm @@ -55,6 +55,7 @@ @@ -9410,7 +9225,7 @@ index 7e793fe01f479b9135c54253af2114ce61924fa0..d22dc512f73af0e4152b4503866ba2a4 #import #import #import -@@ -523,6 +524,11 @@ - (void)removeDataOfTypes:(NSSet *)dataTypes modifiedSince:(NSDate *)date comple +@@ -517,6 +518,11 @@ - (void)removeDataOfTypes:(NSSet *)dataTypes modifiedSince:(NSDate *)date comple }); } @@ -9424,10 +9239,10 @@ index 7e793fe01f479b9135c54253af2114ce61924fa0..d22dc512f73af0e4152b4503866ba2a4 Vector result; diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h new file mode 100644 -index 0000000000000000000000000000000000000000..5fabe06a3289689246c36dfd96eb9900a48b2b0f +index 0000000000000000000000000000000000000000..8938effdcc896cb45f75e3445fbeac6054f69cc6 --- /dev/null +++ b/Source/WebKit/UIProcess/API/Cocoa/_WKBrowserInspector.h -@@ -0,0 +1,55 @@ +@@ -0,0 +1,58 @@ +/* + * Copyright (C) 2019 Microsoft Corporation. + * @@ -9465,7 +9280,10 @@ index 0000000000000000000000000000000000000000..5fabe06a3289689246c36dfd96eb9900 +WK_CLASS_AVAILABLE(macos(10.14.0)) +@interface _WKBrowserContext : NSObject +@property (nonatomic, strong) WKWebsiteDataStore *dataStore; ++#pragma clang diagnostic push ++#pragma clang diagnostic ignored "-Wdeprecated-declarations" +@property (nonatomic, strong) WKProcessPool *processPool; ++#pragma clang diagnostic pop +@end + +@protocol _WKBrowserInspectorDelegate @@ -9550,10 +9368,10 @@ index 0000000000000000000000000000000000000000..69eb9c6aa30beb8ea21a0ef647e46304 +} +@end diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.h b/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.h -index 426c7cbc897e22fd2e962dd3744959975d6ae6a0..f7a52359d7d42f970ef424b6c702b0ec1121a902 100644 +index 7b6c36521ebd5afbd58454192c8dece450b2e90a..a4a0ca2f69b1cc59fccbc261e1dff4e2f15d6e86 100644 --- a/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.h +++ b/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.h -@@ -67,6 +67,7 @@ WK_CLASS_AVAILABLE(macos(10.10), ios(8.0)) +@@ -67,6 +67,7 @@ WK_EXTERN WK_API_DEPRECATED("Creating and using multiple instances of WKProcessP @property (nonatomic) pid_t presentingApplicationPID WK_API_AVAILABLE(macos(10.13), ios(11.0)); @property (nonatomic) audit_token_t presentingApplicationProcessToken WK_API_AVAILABLE(macos(10.13), ios(11.3)); @property (nonatomic) BOOL processSwapsOnNavigation WK_API_AVAILABLE(macos(10.14), ios(12.0)); @@ -9562,10 +9380,10 @@ index 426c7cbc897e22fd2e962dd3744959975d6ae6a0..f7a52359d7d42f970ef424b6c702b0ec @property (nonatomic) BOOL processSwapsOnNavigationWithinSameNonHTTPFamilyProtocol WK_API_AVAILABLE(macos(12.0), ios(15.0)); @property (nonatomic) BOOL prewarmsProcessesAutomatically WK_API_AVAILABLE(macos(10.14.4), ios(12.2)); diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.mm b/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.mm -index 7ccfd9a46cd024c8f6644594b9d0cde8ae7e60db..921ec683dce77583f24d27a0b1b6f478242dfb42 100644 +index 7d6be90da095403d61a44afbe8e08eb9569ecffc..c96e67fdd7887c18d150067cd94c99c62a4abfeb 100644 --- a/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.mm +++ b/Source/WebKit/UIProcess/API/Cocoa/_WKProcessPoolConfiguration.mm -@@ -241,6 +241,16 @@ - (BOOL)processSwapsOnNavigation +@@ -243,6 +243,16 @@ - (BOOL)processSwapsOnNavigation return _processPoolConfiguration->processSwapsOnNavigation(); } @@ -9826,10 +9644,10 @@ index ef24e41f2c62e77e701a6e2b698c60004eaf0789..fd332a5b07f1dea7792eeee3bd9b5efb bool canRunBeforeUnloadConfirmPanel() const final { return true; } diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp b/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp -index 5479cf466e1ba77ede37ff980e78bee85bbb094c..224c04a4bd9de6ad5661b15071c1267a57430547 100644 +index dca689a7ace576571ca8ceb0b51921857b68b64c..e321049561de548aa517df0a8d6f91e86889e8e1 100644 --- a/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp +++ b/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp -@@ -423,10 +423,19 @@ static void webkitWebContextSetProperty(GObject* object, guint propID, const GVa +@@ -430,10 +430,19 @@ static void webkitWebContextSetProperty(GObject* object, guint propID, const GVa } } @@ -9849,7 +9667,7 @@ index 5479cf466e1ba77ede37ff980e78bee85bbb094c..224c04a4bd9de6ad5661b15071c1267a GUniquePtr bundleFilename(g_build_filename(injectedBundleDirectory(), INJECTED_BUNDLE_FILENAME, nullptr)); WebKitWebContext* webContext = WEBKIT_WEB_CONTEXT(object); -@@ -485,6 +494,8 @@ static void webkitWebContextConstructed(GObject* object) +@@ -492,6 +501,8 @@ static void webkitWebContextConstructed(GObject* object) static void webkitWebContextDispose(GObject* object) { @@ -9858,7 +9676,7 @@ index 5479cf466e1ba77ede37ff980e78bee85bbb094c..224c04a4bd9de6ad5661b15071c1267a WebKitWebContextPrivate* priv = WEBKIT_WEB_CONTEXT(object)->priv; if (!priv->clientsDetached) { priv->clientsDetached = true; -@@ -946,6 +957,11 @@ WebKitNetworkSession* webkit_web_context_get_network_session_for_automation(WebK +@@ -953,6 +964,11 @@ WebKitNetworkSession* webkit_web_context_get_network_session_for_automation(WebK return nullptr; #endif } @@ -9895,7 +9713,7 @@ index c1945fbe717a42afc1f51d64a80c7de3fa9009ba..ab63fe19b00ecbd64c9421e6eecad3e2 #endif +int webkitWebContextExistingCount(); diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp b/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp -index df1381b577be94114401e1faaf1979183d82614f..fb8bb4c7676154bb284a06fc3f05d3b45805e938 100644 +index 449a8b1247c7397cd9709e425f8824a90bc77404..b374e3f5104572d44a6fbbb8384d3031815a2202 100644 --- a/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp +++ b/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp @@ -39,6 +39,7 @@ @@ -9906,7 +9724,7 @@ index df1381b577be94114401e1faaf1979183d82614f..fb8bb4c7676154bb284a06fc3f05d3b4 #include "WebKitAuthenticationRequestPrivate.h" #include "WebKitBackForwardListPrivate.h" #include "WebKitContextMenuClient.h" -@@ -152,6 +153,7 @@ enum { +@@ -154,6 +155,7 @@ enum { CLOSE, SCRIPT_DIALOG, @@ -9914,7 +9732,7 @@ index df1381b577be94114401e1faaf1979183d82614f..fb8bb4c7676154bb284a06fc3f05d3b4 DECIDE_POLICY, PERMISSION_REQUEST, -@@ -520,6 +522,13 @@ GRefPtr WebKitWebViewClient::showOptionMenu(WebKitPopupMenu& p +@@ -524,6 +526,13 @@ GRefPtr WebKitWebViewClient::showOptionMenu(WebKitPopupMenu& p void WebKitWebViewClient::frameDisplayed(WKWPE::View&) { @@ -9928,7 +9746,7 @@ index df1381b577be94114401e1faaf1979183d82614f..fb8bb4c7676154bb284a06fc3f05d3b4 { SetForScope inFrameDisplayedGuard(m_webView->priv->inFrameDisplayed, true); for (const auto& callback : m_webView->priv->frameDisplayedCallbacks) { -@@ -536,6 +545,13 @@ void WebKitWebViewClient::frameDisplayed(WKWPE::View&) +@@ -540,6 +549,13 @@ void WebKitWebViewClient::frameDisplayed(WKWPE::View&) } } @@ -9942,7 +9760,7 @@ index df1381b577be94114401e1faaf1979183d82614f..fb8bb4c7676154bb284a06fc3f05d3b4 void WebKitWebViewClient::willStartLoad(WKWPE::View&) { webkitWebViewWillStartLoad(m_webView); -@@ -622,7 +638,7 @@ static gboolean webkitWebViewDecidePolicy(WebKitWebView*, WebKitPolicyDecision* +@@ -631,7 +647,7 @@ static gboolean webkitWebViewDecidePolicy(WebKitWebView*, WebKitPolicyDecision* static gboolean webkitWebViewPermissionRequest(WebKitWebView*, WebKitPermissionRequest* request) { @@ -9951,7 +9769,7 @@ index df1381b577be94114401e1faaf1979183d82614f..fb8bb4c7676154bb284a06fc3f05d3b4 if (WEBKIT_IS_POINTER_LOCK_PERMISSION_REQUEST(request)) { webkit_permission_request_allow(request); return TRUE; -@@ -945,6 +961,10 @@ static void webkitWebViewConstructed(GObject* object) +@@ -954,6 +970,10 @@ static void webkitWebViewConstructed(GObject* object) priv->websitePolicies = adoptGRef(webkit_website_policies_new()); Ref configuration = priv->relatedView && priv->relatedView->priv->configurationForNextRelatedView ? priv->relatedView->priv->configurationForNextRelatedView.releaseNonNull() : webkitWebViewCreatePageConfiguration(webView); @@ -9962,7 +9780,7 @@ index df1381b577be94114401e1faaf1979183d82614f..fb8bb4c7676154bb284a06fc3f05d3b4 webkitWebViewCreatePage(webView, WTFMove(configuration)); webkitWebContextWebViewCreated(priv->context.get(), webView); -@@ -1984,6 +2004,15 @@ static void webkit_web_view_class_init(WebKitWebViewClass* webViewClass) +@@ -2022,6 +2042,15 @@ static void webkit_web_view_class_init(WebKitWebViewClass* webViewClass) G_TYPE_BOOLEAN, 1, WEBKIT_TYPE_SCRIPT_DIALOG); @@ -9978,7 +9796,7 @@ index df1381b577be94114401e1faaf1979183d82614f..fb8bb4c7676154bb284a06fc3f05d3b4 /** * WebKitWebView::decide-policy: * @web_view: the #WebKitWebView on which the signal is emitted -@@ -2769,6 +2798,23 @@ void webkitWebViewRunJavaScriptBeforeUnloadConfirm(WebKitWebView* webView, const +@@ -2812,6 +2841,23 @@ void webkitWebViewRunJavaScriptBeforeUnloadConfirm(WebKitWebView* webView, const webkit_script_dialog_unref(webView->priv->currentScriptDialog); } @@ -10003,7 +9821,7 @@ index df1381b577be94114401e1faaf1979183d82614f..fb8bb4c7676154bb284a06fc3f05d3b4 { if (!webView->priv->currentScriptDialog) diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebViewPrivate.h b/Source/WebKit/UIProcess/API/glib/WebKitWebViewPrivate.h -index bf5b4c2bcca722e4d008f12194344c29c0db8824..ee6ee6b476ac28dee3a5983d03ba89ad0c9eb9ff 100644 +index c80a5073b50ae7273985c9165cffc1d361a2ff03..7b1ce505325c735a0bf1c23f031ad1ee10639b91 100644 --- a/Source/WebKit/UIProcess/API/glib/WebKitWebViewPrivate.h +++ b/Source/WebKit/UIProcess/API/glib/WebKitWebViewPrivate.h @@ -64,6 +64,7 @@ void webkitWebViewRunJavaScriptAlert(WebKitWebView*, const CString& message, Fun @@ -10027,7 +9845,7 @@ index 763cd55f7abca011ac8bc4fef7f233bf52854cda..bd43917b274bf19ff9f3d96b7e80e207 #include <@API_INCLUDE_PREFIX@/WebKitClipboardPermissionRequest.h> #include <@API_INCLUDE_PREFIX@/WebKitColorChooserRequest.h> diff --git a/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp b/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp -index 12efcb801d9d63d159a7f7d2e733cda141dc4e55..54f6703d64d5bcfd1d0f42be444492f9cd923bb0 100644 +index 92218ae7cac5f31db31d9ecde99094331a465ebf..e9d4e786b8640d93fb35187cba96496f6bbb1d32 100644 --- a/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp +++ b/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp @@ -270,6 +270,8 @@ void PageClientImpl::doneWithKeyEvent(const NativeWebKeyboardEvent& event, bool @@ -10052,7 +9870,7 @@ index 12efcb801d9d63d159a7f7d2e733cda141dc4e55..54f6703d64d5bcfd1d0f42be444492f9 void PageClientImpl::didChangeContentSize(const IntSize& size) diff --git a/Source/WebKit/UIProcess/API/gtk/PageClientImpl.h b/Source/WebKit/UIProcess/API/gtk/PageClientImpl.h -index c20bdf245d4e6d2a215615cf573cbbdefdb574c0..8c9f3f8e2e111bd3d19314e33a394c371944d358 100644 +index 3cafc1ed805da8f947dc8c8c28327116f1d3c8a4..a8ed97eb566e4f966f85b03ea55d8dfccc608e58 100644 --- a/Source/WebKit/UIProcess/API/gtk/PageClientImpl.h +++ b/Source/WebKit/UIProcess/API/gtk/PageClientImpl.h @@ -104,7 +104,7 @@ private: @@ -10165,10 +9983,10 @@ index 496079da90993ac37689b060b69ecd4a67c2b6a8..af30181ca922f16c0f6e245c70e5ce7d G_BEGIN_DECLS diff --git a/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBase.cpp b/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBase.cpp -index 3bbacbe1dbe27e9743f608322f06abb1adc75173..be721a26127ffff7904072aa2d9e7d72050ca514 100644 +index b58f8dd27e62f047857ed2f36fcd6dbd80e8038d..897d4f038d6dcbd9a4a37dbdda91fae46f56f521 100644 --- a/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBase.cpp +++ b/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBase.cpp -@@ -2875,6 +2875,11 @@ void webkitWebViewBaseResetClickCounter(WebKitWebViewBase* webkitWebViewBase) +@@ -2884,6 +2884,11 @@ void webkitWebViewBaseResetClickCounter(WebKitWebViewBase* webkitWebViewBase) #endif } @@ -10180,7 +9998,7 @@ index 3bbacbe1dbe27e9743f608322f06abb1adc75173..be721a26127ffff7904072aa2d9e7d72 void webkitWebViewBaseEnterAcceleratedCompositingMode(WebKitWebViewBase* webkitWebViewBase, const LayerTreeContext& layerTreeContext) { ASSERT(webkitWebViewBase->priv->acceleratedBackingStore); -@@ -2931,12 +2936,12 @@ void webkitWebViewBasePageClosed(WebKitWebViewBase* webkitWebViewBase) +@@ -2940,12 +2945,12 @@ void webkitWebViewBasePageClosed(WebKitWebViewBase* webkitWebViewBase) webkitWebViewBase->priv->acceleratedBackingStore->update({ }); } @@ -10196,7 +10014,7 @@ index 3bbacbe1dbe27e9743f608322f06abb1adc75173..be721a26127ffff7904072aa2d9e7d72 #if !USE(GTK4) diff --git a/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBasePrivate.h b/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBasePrivate.h -index 3b8ca9470bab69dc26313111a79f954b10b30bf4..5c056dd6734f42d24bc168b4f5ba436584b3f7a8 100644 +index 8a71b1d7d9b9ef8bfe4a6935e3788e40a5fe6d5d..dad4a47725cc3a5d9d3fd6b1f4ec23c866cfe973 100644 --- a/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBasePrivate.h +++ b/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBasePrivate.h @@ -27,6 +27,7 @@ @@ -10223,7 +10041,7 @@ index 3b8ca9470bab69dc26313111a79f954b10b30bf4..5c056dd6734f42d24bc168b4f5ba4365 + +WebKit::AcceleratedBackingStore* webkitWebViewBaseGetAcceleratedBackingStore(WebKitWebViewBase*); diff --git a/Source/WebKit/UIProcess/API/wpe/APIViewClient.h b/Source/WebKit/UIProcess/API/wpe/APIViewClient.h -index 7636ad733e7be66a74f8fede966b0acb905a5842..777d86d6e160a7cfba6dd50d416ed1881f448de4 100644 +index 9091ae5198e765c2cfe0584d121afe4f88df3c0e..b0efedec419673ef2bfd0fd79406774e24aa98e9 100644 --- a/Source/WebKit/UIProcess/API/wpe/APIViewClient.h +++ b/Source/WebKit/UIProcess/API/wpe/APIViewClient.h @@ -26,6 +26,9 @@ @@ -10249,7 +10067,7 @@ index 7636ad733e7be66a74f8fede966b0acb905a5842..777d86d6e160a7cfba6dd50d416ed188 virtual void didChangePageID(WKWPE::View&) { } virtual void didReceiveUserMessage(WKWPE::View&, WebKit::UserMessage&&, CompletionHandler&& completionHandler) { completionHandler(WebKit::UserMessage()); } diff --git a/Source/WebKit/UIProcess/API/wpe/PageClientImpl.cpp b/Source/WebKit/UIProcess/API/wpe/PageClientImpl.cpp -index bbbdaf4b823048adcb1e77756560b3dfd525297d..d4f873d6778bae8150817968b992d91dd2428518 100644 +index ab67db0aab214edea4f3c7ff80e1fa27c7c0a95b..069d41ca9eb33fdc851fc96a7b260ceca9fdfa9a 100644 --- a/Source/WebKit/UIProcess/API/wpe/PageClientImpl.cpp +++ b/Source/WebKit/UIProcess/API/wpe/PageClientImpl.cpp @@ -35,9 +35,12 @@ @@ -10297,7 +10115,7 @@ index bbbdaf4b823048adcb1e77756560b3dfd525297d..d4f873d6778bae8150817968b992d91d } RefPtr PageClientImpl::createDateTimePicker(WebPageProxy& page) -@@ -546,6 +555,37 @@ void PageClientImpl::selectionDidChange() +@@ -551,6 +560,37 @@ void PageClientImpl::selectionDidChange() m_view.selectionDidChange(); } @@ -10335,7 +10153,7 @@ index bbbdaf4b823048adcb1e77756560b3dfd525297d..d4f873d6778bae8150817968b992d91d WebKitWebResourceLoadManager* PageClientImpl::webResourceLoadManager() { return m_view.webResourceLoadManager(); -@@ -556,4 +596,11 @@ void PageClientImpl::callAfterNextPresentationUpdate(CompletionHandler&& +@@ -561,4 +601,11 @@ void PageClientImpl::callAfterNextPresentationUpdate(CompletionHandler&& m_view.callAfterNextPresentationUpdate(WTFMove(callback)); } @@ -10348,10 +10166,10 @@ index bbbdaf4b823048adcb1e77756560b3dfd525297d..d4f873d6778bae8150817968b992d91d + } // namespace WebKit diff --git a/Source/WebKit/UIProcess/API/wpe/PageClientImpl.h b/Source/WebKit/UIProcess/API/wpe/PageClientImpl.h -index 8809d3fbeeae0387bf683f6154e6db741c3ecc4a..4f1671804b06c82ba221ad74b2b30d0f47ee8718 100644 +index 000b7c9950785f55fdfdcaa186026903a506c79e..2f0f821890ab40bcfa68b105710484674600a2db 100644 --- a/Source/WebKit/UIProcess/API/wpe/PageClientImpl.h +++ b/Source/WebKit/UIProcess/API/wpe/PageClientImpl.h -@@ -183,9 +183,15 @@ private: +@@ -184,9 +184,15 @@ private: void didChangeWebPageID() const override; void selectionDidChange() override; @@ -10527,7 +10345,7 @@ index e4b92ace1531090ae38a7aec3d3d4febf19aee84..b66b573f9148c39c5ce2738add6cd01a + +PlatformImage webkitWebViewBackendTakeScreenshot(WebKitWebViewBackend*); diff --git a/Source/WebKit/UIProcess/API/wpe/WebKitWebViewClient.h b/Source/WebKit/UIProcess/API/wpe/WebKitWebViewClient.h -index 2f1182cb91a00353eace0b71612df096391c2450..f2d09ba39f7d3c1b76ed705a0d945b869823c03e 100644 +index 0fa1f2e970ed0c0232df9b9c7b8b4bcd0ceac655..d68c6747edc699de7d84bae2a63b927dbdd8d0fe 100644 --- a/Source/WebKit/UIProcess/API/wpe/WebKitWebViewClient.h +++ b/Source/WebKit/UIProcess/API/wpe/WebKitWebViewClient.h @@ -51,6 +51,11 @@ private: @@ -10543,7 +10361,7 @@ index 2f1182cb91a00353eace0b71612df096391c2450..f2d09ba39f7d3c1b76ed705a0d945b86 void didChangePageID(WKWPE::View&) override; void didReceiveUserMessage(WKWPE::View&, WebKit::UserMessage&&, CompletionHandler&&) override; diff --git a/Source/WebKit/UIProcess/Automation/WebAutomationSession.h b/Source/WebKit/UIProcess/Automation/WebAutomationSession.h -index b351f11524a994ac6136daf9c277419fce44df0f..31679e63462cfaceeac00ebf31985c118ff1e4e4 100644 +index d0a40e3e9f4d59f894c0574926ee13b2d83767ab..3dfd8e059273d2a14494b76892e5abfe66475c2e 100644 --- a/Source/WebKit/UIProcess/Automation/WebAutomationSession.h +++ b/Source/WebKit/UIProcess/Automation/WebAutomationSession.h @@ -287,6 +287,8 @@ public: @@ -10553,8 +10371,8 @@ index b351f11524a994ac6136daf9c277419fce44df0f..31679e63462cfaceeac00ebf31985c11 + static std::optional platformGetBase64EncodedPNGData(const ViewSnapshot&); + RefPtr webPageProxyForHandle(const String&); + String handleForWebFrameID(std::optional); String handleForWebPageProxy(const WebPageProxy&); - @@ -338,7 +340,6 @@ private: // Get base64-encoded PNG data from a bitmap. @@ -10741,7 +10559,7 @@ index 89d125f7742f81ead8c50f218ecb1771b8000636..baa6cf58ad502c6c033ee6293a6cc8d4 namespace WebKit { diff --git a/Source/WebKit/UIProcess/Cocoa/UIDelegate.h b/Source/WebKit/UIProcess/Cocoa/UIDelegate.h -index 554009ae0e083f7721f97a3952f0f17b7d8d7d9d..fbf643cc61caab6389e4bc727adb897ebbb6a109 100644 +index c633008451c5719866d79300c49d60f979ffb581..9cc370de9b38f70970a6ca11a43651cbb2549529 100644 --- a/Source/WebKit/UIProcess/Cocoa/UIDelegate.h +++ b/Source/WebKit/UIProcess/Cocoa/UIDelegate.h @@ -103,6 +103,7 @@ private: @@ -10752,7 +10570,7 @@ index 554009ae0e083f7721f97a3952f0f17b7d8d7d9d..fbf643cc61caab6389e4bc727adb897e void presentStorageAccessConfirmDialog(const WTF::String& requestingDomain, const WTF::String& currentDomain, CompletionHandler&&); void requestStorageAccessConfirm(WebPageProxy&, WebFrameProxy*, const WebCore::RegistrableDomain& requestingDomain, const WebCore::RegistrableDomain& currentDomain, std::optional&&, CompletionHandler&&) final; void decidePolicyForGeolocationPermissionRequest(WebPageProxy&, WebFrameProxy&, const FrameInfoData&, Function&) final; -@@ -221,6 +222,7 @@ private: +@@ -226,6 +227,7 @@ private: bool webViewRunJavaScriptAlertPanelWithMessageInitiatedByFrameCompletionHandler : 1; bool webViewRunJavaScriptConfirmPanelWithMessageInitiatedByFrameCompletionHandler : 1; bool webViewRunJavaScriptTextInputPanelWithPromptDefaultTextInitiatedByFrameCompletionHandler : 1; @@ -10761,7 +10579,7 @@ index 554009ae0e083f7721f97a3952f0f17b7d8d7d9d..fbf643cc61caab6389e4bc727adb897e bool webViewRequestStorageAccessPanelForDomainUnderCurrentDomainForQuirkDomainsCompletionHandler : 1; bool webViewRunBeforeUnloadConfirmPanelWithMessageInitiatedByFrameCompletionHandler : 1; diff --git a/Source/WebKit/UIProcess/Cocoa/UIDelegate.mm b/Source/WebKit/UIProcess/Cocoa/UIDelegate.mm -index 30de92c390b49a383369fb59397810187f752c8a..0752cc7cefcc4c3579eafca4fa1cc6e4045d5d84 100644 +index 6c934950b89e32e9ff58992497a313062658c235..c3f1b8d1193ae36efe9e5700f85eee4e86e8c548 100644 --- a/Source/WebKit/UIProcess/Cocoa/UIDelegate.mm +++ b/Source/WebKit/UIProcess/Cocoa/UIDelegate.mm @@ -135,6 +135,7 @@ void UIDelegate::setDelegate(id delegate) @@ -10772,7 +10590,7 @@ index 30de92c390b49a383369fb59397810187f752c8a..0752cc7cefcc4c3579eafca4fa1cc6e4 m_delegateMethods.webViewRequestStorageAccessPanelUnderFirstPartyCompletionHandler = [delegate respondsToSelector:@selector(_webView:requestStorageAccessPanelForDomain:underCurrentDomain:completionHandler:)]; m_delegateMethods.webViewRequestStorageAccessPanelForDomainUnderCurrentDomainForQuirkDomainsCompletionHandler = [delegate respondsToSelector:@selector(_webView:requestStorageAccessPanelForDomain:underCurrentDomain:forQuirkDomains:completionHandler:)]; m_delegateMethods.webViewRunBeforeUnloadConfirmPanelWithMessageInitiatedByFrameCompletionHandler = [delegate respondsToSelector:@selector(_webView:runBeforeUnloadConfirmPanelWithMessage:initiatedByFrame:completionHandler:)]; -@@ -495,6 +496,15 @@ void UIDelegate::UIClient::runJavaScriptPrompt(WebPageProxy& page, const WTF::St +@@ -500,6 +501,15 @@ void UIDelegate::UIClient::runJavaScriptPrompt(WebPageProxy& page, const WTF::St }).get()]; } @@ -10789,7 +10607,7 @@ index 30de92c390b49a383369fb59397810187f752c8a..0752cc7cefcc4c3579eafca4fa1cc6e4 { RefPtr uiDelegate = m_uiDelegate.get(); diff --git a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm -index 0eaba44d278da255e3035a4f71ddffb276ad2164..23838dad0c6f4a150ec99ecc1105163db1140a0a 100644 +index 70f71e49666eb0bc9a732c5893d27179b051bd72..a6c273ea6793f213aadc64485fe7c1fb23854b72 100644 --- a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm +++ b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm @@ -43,7 +43,9 @@ @@ -10802,7 +10620,7 @@ index 0eaba44d278da255e3035a4f71ddffb276ad2164..23838dad0c6f4a150ec99ecc1105163d #import "PlatformXRSystem.h" #import "PlaybackSessionManagerProxy.h" #import "RemoteLayerTreeTransaction.h" -@@ -347,11 +349,86 @@ bool WebPageProxy::scrollingUpdatesDisabledForTesting() +@@ -351,11 +353,86 @@ bool WebPageProxy::scrollingUpdatesDisabledForTesting() void WebPageProxy::startDrag(const DragItem& dragItem, ShareableBitmap::Handle&& dragImageHandle, const std::optional& elementID) { @@ -10891,10 +10709,10 @@ index 0eaba44d278da255e3035a4f71ddffb276ad2164..23838dad0c6f4a150ec99ecc1105163d #if ENABLE(ATTACHMENT_ELEMENT) diff --git a/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm -index 7f9f0ecf631ab772460603adb4d0426e1d468085..a98a1388e589d5f079fe29f5b6091aa022b6ecfc 100644 +index 3b6663cb77c2c20e2e6822bfdbf822cea3022fdc..d0e67aa4714f4f8e01c3c2e1ef3fd1adc6274d20 100644 --- a/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm +++ b/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm -@@ -436,7 +436,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END +@@ -442,7 +442,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END auto screenProperties = WebCore::collectScreenProperties(); parameters.screenProperties = WTFMove(screenProperties); #if PLATFORM(MAC) @@ -10903,7 +10721,7 @@ index 7f9f0ecf631ab772460603adb4d0426e1d468085..a98a1388e589d5f079fe29f5b6091aa0 #endif #if PLATFORM(VISION) -@@ -835,8 +835,8 @@ void WebProcessPool::registerNotificationObservers() +@@ -841,8 +841,8 @@ void WebProcessPool::registerNotificationObservers() }]; m_scrollerStyleNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSPreferredScrollerStyleDidChangeNotification object:nil queue:[NSOperationQueue currentQueue] usingBlock:^(NSNotification *notification) { @@ -10915,7 +10733,7 @@ index 7f9f0ecf631ab772460603adb4d0426e1d468085..a98a1388e589d5f079fe29f5b6091aa0 m_activationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationDidBecomeActiveNotification object:NSApp queue:[NSOperationQueue currentQueue] usingBlock:^(NSNotification *notification) { diff --git a/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp b/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp -index 183a2dc44f4d2921e68a6ff5fd2fb0fb815753af..f9e80d965f5b7cd64049d85e57e668d6f2a73d87 100644 +index f356f65a1515e9ed17e51d4bde50a8c0758fd6e1..0caab4689c42a89d1d5060a3bdf9a0502e73b8ab 100644 --- a/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp +++ b/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp @@ -33,6 +33,7 @@ @@ -10954,7 +10772,7 @@ index 183a2dc44f4d2921e68a6ff5fd2fb0fb815753af..f9e80d965f5b7cd64049d85e57e668d6 using namespace WebCore; @@ -182,6 +194,11 @@ void DrawingAreaProxyCoordinatedGraphics::deviceScaleFactorDidChange(CompletionH - sendWithAsyncReply(Messages::DrawingArea::SetDeviceScaleFactor(m_webPageProxy->deviceScaleFactor()), WTFMove(completionHandler)); + sendWithAsyncReply(Messages::DrawingArea::SetDeviceScaleFactor(page()->deviceScaleFactor()), WTFMove(completionHandler)); } +void DrawingAreaProxyCoordinatedGraphics::waitForSizeUpdate(Function&& callback) @@ -10974,7 +10792,7 @@ index 183a2dc44f4d2921e68a6ff5fd2fb0fb815753af..f9e80d965f5b7cd64049d85e57e668d6 +{ + RefPtr surface; + if (isInAcceleratedCompositingMode()) { -+ AcceleratedBackingStore* backingStore = webkitWebViewBaseGetAcceleratedBackingStore(WEBKIT_WEB_VIEW_BASE(protectedWebPageProxy()->viewWidget())); ++ AcceleratedBackingStore* backingStore = webkitWebViewBaseGetAcceleratedBackingStore(WEBKIT_WEB_VIEW_BASE(protectedPage()->viewWidget())); + if (!backingStore) + return; + @@ -11003,7 +10821,7 @@ index 183a2dc44f4d2921e68a6ff5fd2fb0fb815753af..f9e80d965f5b7cd64049d85e57e668d6 + if (!skImage) + return; + -+ protectedWebPageProxy()->inspectorController().didPaint(WTFMove(skImage)); ++ protectedPage()->inspectorController().didPaint(WTFMove(skImage)); +} +#endif // PLATFORM(GTK) + @@ -11018,16 +10836,16 @@ index 183a2dc44f4d2921e68a6ff5fd2fb0fb815753af..f9e80d965f5b7cd64049d85e57e668d6 + auto image = surface->makeImageSnapshot(); + if (!image) + return; -+ protectedWebPageProxy()->inspectorController().didPaint(WTFMove(image)); ++ protectedPage()->inspectorController().didPaint(WTFMove(image)); +} +#endif // PLATFORM(WIN) + bool DrawingAreaProxyCoordinatedGraphics::alwaysUseCompositing() const { - if (!m_webPageProxy) + if (!page()) @@ -310,6 +380,12 @@ void DrawingAreaProxyCoordinatedGraphics::didUpdateGeometry() // we need to resend the new size here. - if (m_lastSentSize != m_size) + if (m_lastSentSize != size()) sendUpdateGeometry(); + else { + Vector> callbacks; @@ -11039,7 +10857,7 @@ index 183a2dc44f4d2921e68a6ff5fd2fb0fb815753af..f9e80d965f5b7cd64049d85e57e668d6 #if !PLATFORM(WPE) diff --git a/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.h b/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.h -index 9c2bde0db0e4032a32e6ae02dc45af335df92f7a..3f3a58ee9b0f5c0ddad84f41cf3acd6199d81c37 100644 +index 61ce018548600fb7e4f3cdb3ee82b444d5b2429f..4e93f979cd98c36b24a08c00193b190a8a76af85 100644 --- a/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.h +++ b/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.h @@ -29,6 +29,7 @@ @@ -11177,7 +10995,7 @@ index 9a92a8cde3b5d1da0fbbf5fe7c549cebb8a7f2f7..9ce201ca2d7aa002c7bd389f1fe03edf } // namespace WebKit diff --git a/Source/WebKit/UIProcess/DrawingAreaProxy.h b/Source/WebKit/UIProcess/DrawingAreaProxy.h -index e1f55b4a7fbc452ca1f2eb025b0c88ec9f4b845f..beb3c7a6619b554bb3186a0de917f497ac0f47f8 100644 +index c30c32f55aff3552bb05112dd53b9b8b62dd4a3f..32417be5f8cf613f58db900ce615518751b1509a 100644 --- a/Source/WebKit/UIProcess/DrawingAreaProxy.h +++ b/Source/WebKit/UIProcess/DrawingAreaProxy.h @@ -94,6 +94,7 @@ public: @@ -11190,7 +11008,7 @@ index e1f55b4a7fbc452ca1f2eb025b0c88ec9f4b845f..beb3c7a6619b554bb3186a0de917f497 virtual void sizeToContentAutoSizeMaximumSizeDidChange() { } diff --git a/Source/WebKit/UIProcess/Inspector/Agents/InspectorScreencastAgent.cpp b/Source/WebKit/UIProcess/Inspector/Agents/InspectorScreencastAgent.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..0615d7de9494901c53131b45018ee48612e7cfca +index 0000000000000000000000000000000000000000..c7a4a2f5fbd8c99d2b53fe0d3b1645c865d46634 --- /dev/null +++ b/Source/WebKit/UIProcess/Inspector/Agents/InspectorScreencastAgent.cpp @@ -0,0 +1,326 @@ @@ -11492,7 +11310,7 @@ index 0000000000000000000000000000000000000000..0615d7de9494901c53131b45018ee486 + + // Do not send the same frame over and over. + auto cryptoDigest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA_1); -+ cryptoDigest->addBytes(std::span(data.data(), data.size())); ++ cryptoDigest->addBytes(std::span(data.mutableSpan().data(), data.size())); + auto digest = cryptoDigest->computeHash(); + if (m_lastFrameDigest != digest) { + String base64Data = base64EncodeToString(data); @@ -11522,10 +11340,10 @@ index 0000000000000000000000000000000000000000..0615d7de9494901c53131b45018ee486 +WTF_ALLOW_UNSAFE_BUFFER_USAGE_END diff --git a/Source/WebKit/UIProcess/Inspector/Agents/InspectorScreencastAgent.h b/Source/WebKit/UIProcess/Inspector/Agents/InspectorScreencastAgent.h new file mode 100644 -index 0000000000000000000000000000000000000000..6e031f61b132176587643bc79b23202009c4aca4 +index 0000000000000000000000000000000000000000..852c199da066320f730226653a47c8e5185dc560 --- /dev/null +++ b/Source/WebKit/UIProcess/Inspector/Agents/InspectorScreencastAgent.h -@@ -0,0 +1,102 @@ +@@ -0,0 +1,106 @@ +/* + * Copyright (C) 2020 Microsoft Corporation. + * @@ -11561,6 +11379,10 @@ index 0000000000000000000000000000000000000000..6e031f61b132176587643bc79b232020 +#include +#include + ++#if USE(SKIA) ++#include ++#endif ++ +namespace Inspector { +class BackendDispatcher; +class FrontendChannel; @@ -12347,7 +12169,7 @@ index edd6e7f1799279ed3d0eb81b6c2eef9f5b375134..d4231f84f3c52641f4d9e88559e8e1a4 String m_identifier; Inspector::InspectorTargetType m_type; diff --git a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp -index 45eb87344ce4249eea90dc0a73a2c717f69f55fa..c070e869819c36e60a5c64bf4c762a50915d8a5e 100644 +index 9e1e41b6a796082231a32b04fe8b13270b02eb3e..58b4d514f7a06b7fb7cdae111f99a9009f018187 100644 --- a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp +++ b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp @@ -26,13 +26,23 @@ @@ -12394,7 +12216,7 @@ index 45eb87344ce4249eea90dc0a73a2c717f69f55fa..c070e869819c36e60a5c64bf4c762a50 , m_backendDispatcher(BackendDispatcher::create(m_frontendRouter.copyRef())) , m_inspectedPage(inspectedPage) { -- auto targetAgent = makeUnique(m_frontendRouter.get(), m_backendDispatcher.get()); +- auto targetAgent = makeUnique(m_frontendRouter, m_backendDispatcher); - m_targetAgent = targetAgent.get(); - m_agents.append(WTFMove(targetAgent)); } @@ -12525,7 +12347,7 @@ index 45eb87344ce4249eea90dc0a73a2c717f69f55fa..c070e869819c36e60a5c64bf4c762a50 + m_pendingNavigations.clear(); + } - auto inspectedPage = protectedInspectedPage(); + Ref inspectedPage = m_inspectedPage.get(); inspectedPage->didChangeInspectorFrontendCount(m_frontendRouter->frontendCount()); @@ -137,6 +241,8 @@ void WebPageInspectorController::disconnectAllFrontends() // Disconnect any remaining remote frontends. @@ -12533,7 +12355,7 @@ index 45eb87344ce4249eea90dc0a73a2c717f69f55fa..c070e869819c36e60a5c64bf4c762a50 + m_pendingNavigations.clear(); + - auto inspectedPage = protectedInspectedPage(); + Ref inspectedPage = m_inspectedPage.get(); inspectedPage->didChangeInspectorFrontendCount(m_frontendRouter->frontendCount()); @@ -165,6 +271,66 @@ void WebPageInspectorController::setIndicating(bool indicating) @@ -12604,7 +12426,7 @@ index 45eb87344ce4249eea90dc0a73a2c717f69f55fa..c070e869819c36e60a5c64bf4c762a50 { addTarget(InspectorTargetProxy::create(protectedInspectedPage(), targetId, type)); @@ -184,6 +350,52 @@ void WebPageInspectorController::sendMessageToInspectorFrontend(const String& ta - m_targetAgent->sendMessageFromTargetToFrontend(targetId, message); + checkedTargetAgent()->sendMessageFromTargetToFrontend(targetId, message); } +void WebPageInspectorController::setPauseOnStart(bool shouldPause) @@ -12665,8 +12487,8 @@ index 45eb87344ce4249eea90dc0a73a2c717f69f55fa..c070e869819c36e60a5c64bf4c762a50 } void WebPageInspectorController::willDestroyProvisionalPage(const ProvisionalPageProxy& provisionalPage) -@@ -287,4 +499,29 @@ void WebPageInspectorController::browserExtensionsDisabled(HashSet&& ext - m_enabledBrowserAgent->extensionsDisabled(WTFMove(extensionIDs)); +@@ -288,4 +500,29 @@ void WebPageInspectorController::browserExtensionsDisabled(HashSet&& ext + enabledBrowserAgent->extensionsDisabled(WTFMove(extensionIDs)); } +void WebPageInspectorController::adjustPageSettings() @@ -12696,7 +12518,7 @@ index 45eb87344ce4249eea90dc0a73a2c717f69f55fa..c070e869819c36e60a5c64bf4c762a50 + } // namespace WebKit diff --git a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h -index c219e0a072057a8d40d8a30a1d404851d6c12d43..aff66705b9acadb9d811f2da439a59dbf5e6ea06 100644 +index 0e6766e155e83f9f5fde89080448b3f08a3a27e5..8ed8b9baf74866fad6498a1a13c6db96f0e2b460 100644 --- a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h +++ b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h @@ -26,19 +26,39 @@ @@ -12813,12 +12635,13 @@ index c219e0a072057a8d40d8a30a1d404851d6c12d43..aff66705b9acadb9d811f2da439a59db bool shouldPauseLoading(const ProvisionalPageProxy&) const; void setContinueLoadingCallback(const ProvisionalPageProxy&, WTF::Function&&); -@@ -86,11 +153,12 @@ public: +@@ -86,12 +153,13 @@ public: void browserExtensionsDisabled(HashSet&&); private: - Ref protectedInspectedPage(); + WeakRef protectedInspectedPage(); + CheckedPtr checkedTargetAgent() { return m_targetAgent; } WebPageAgentContext webPageAgentContext(); void createLazyAgents(); @@ -12827,7 +12650,7 @@ index c219e0a072057a8d40d8a30a1d404851d6c12d43..aff66705b9acadb9d811f2da439a59db const Ref m_frontendRouter; const Ref m_backendDispatcher; -@@ -101,9 +169,16 @@ private: +@@ -102,9 +170,16 @@ private: CheckedPtr m_targetAgent; HashMap> m_targets; @@ -14317,7 +14140,7 @@ index 0000000000000000000000000000000000000000..e7a3dcc533294bb6e12f65d79b5b716b + +#endif // ENABLE(REMOTE_INSPECTOR) diff --git a/Source/WebKit/UIProcess/Launcher/glib/ProcessLauncherGLib.cpp b/Source/WebKit/UIProcess/Launcher/glib/ProcessLauncherGLib.cpp -index 272e0a6edea50acd35032a854d625b5af5a1472e..486c807ba879ecf534db1ffb593709d117e0c35a 100644 +index f5c20099e6f746c3d26035313bf67641927549ec..4d3a40809a303bcb49bcc65cce7b7ce06ac7a96f 100644 --- a/Source/WebKit/UIProcess/Launcher/glib/ProcessLauncherGLib.cpp +++ b/Source/WebKit/UIProcess/Launcher/glib/ProcessLauncherGLib.cpp @@ -168,6 +168,13 @@ void ProcessLauncher::launchProcess() @@ -14346,7 +14169,7 @@ index 272e0a6edea50acd35032a854d625b5af5a1472e..486c807ba879ecf534db1ffb593709d1 WTF_ALLOW_UNSAFE_BUFFER_USAGE_END diff --git a/Source/WebKit/UIProcess/Launcher/win/ProcessLauncherWin.cpp b/Source/WebKit/UIProcess/Launcher/win/ProcessLauncherWin.cpp -index a108acd8a4503a07309fe8c54afc80b0f4175eae..1421d9a761042c31a6ecf3cc78ce3f0e96109abe 100644 +index 71f80f5414e3e0cec1588fb3b4432665aa6facde..23bbdb9f5eac73ebd0cc5aea33bb08bcc67b60dc 100644 --- a/Source/WebKit/UIProcess/Launcher/win/ProcessLauncherWin.cpp +++ b/Source/WebKit/UIProcess/Launcher/win/ProcessLauncherWin.cpp @@ -91,14 +91,21 @@ void ProcessLauncher::launchProcess() @@ -14370,10 +14193,10 @@ index a108acd8a4503a07309fe8c54afc80b0f4175eae..1421d9a761042c31a6ecf3cc78ce3f0e + startupInfo.hStdOutput = ::GetStdHandle(STD_OUTPUT_HANDLE); + startupInfo.hStdError = ::GetStdHandle(STD_ERROR_HANDLE); PROCESS_INFORMATION processInformation { }; - BOOL result = ::CreateProcess(0, commandLine.data(), 0, 0, true, 0, 0, 0, &startupInfo, &processInformation); + BOOL result = ::CreateProcess(0, commandLine.mutableSpan().data(), 0, 0, true, 0, 0, 0, &startupInfo, &processInformation); diff --git a/Source/WebKit/UIProcess/PageClient.h b/Source/WebKit/UIProcess/PageClient.h -index 1e06e94f26fc73c323605da1afb0474559b35c33..1ac2f393d3dc0a7812c1b9a1bdaa6cea4f150bf4 100644 +index 48b9ebe975bff1b8179ac26a31b4724a028ecb94..f61094aa9f8db7832f3adcb3fd77bb19005abc40 100644 --- a/Source/WebKit/UIProcess/PageClient.h +++ b/Source/WebKit/UIProcess/PageClient.h @@ -74,6 +74,11 @@ @@ -14552,7 +14375,7 @@ index 493bde430bef5c064ff6807296ad088d8dee1a72..9b6dbc259e150fba3ba5fb4b488d91e3 #include "ProvisionalFrameCreationParameters.h" diff --git a/Source/WebKit/UIProcess/RemoteInspectorPipe.cpp b/Source/WebKit/UIProcess/RemoteInspectorPipe.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..b1ddac8c1442cb4da17f50d92599ef9aff6d4066 +index 0000000000000000000000000000000000000000..be84b2a5ce00ec2e242785fe8b00a08c44900215 --- /dev/null +++ b/Source/WebKit/UIProcess/RemoteInspectorPipe.cpp @@ -0,0 +1,230 @@ @@ -14766,7 +14589,7 @@ index 0000000000000000000000000000000000000000..b1ddac8c1442cb4da17f50d92599ef9a + break; + + if (end > start) { -+ String message = String::fromUTF8({ line.data() + start, end - start }); ++ String message = String::fromUTF8({ line.mutableSpan().data() + start, end - start }); + RunLoop::main().dispatch([this, message = WTFMove(message)] { + if (!m_terminated) + m_playwrightAgent.dispatchMessageFromFrontend(message); @@ -14776,7 +14599,7 @@ index 0000000000000000000000000000000000000000..b1ddac8c1442cb4da17f50d92599ef9a + start = end; + } + if (start != 0 && start < line.size()) -+ memmove(line.data(), line.data() + start, line.size() - start); ++ memmove(line.mutableSpan().data(), line.mutableSpan().data() + start, line.size() - start); + line.shrink(line.size() - start); + } +} @@ -14857,6 +14680,50 @@ index 0000000000000000000000000000000000000000..6d04f9290135069359ce6bf872654648 +} // namespace WebKit + +#endif // ENABLE(REMOTE_INSPECTOR) +diff --git a/Source/WebKit/UIProcess/RemotePagePlaybackSessionManagerProxy.h b/Source/WebKit/UIProcess/RemotePagePlaybackSessionManagerProxy.h +index 3f6cd845733e2e8f5e25c318e7ab54f77032d4cf..e4399bfd734a28ef5df5ec855b7c78b46634c91d 100644 +--- a/Source/WebKit/UIProcess/RemotePagePlaybackSessionManagerProxy.h ++++ b/Source/WebKit/UIProcess/RemotePagePlaybackSessionManagerProxy.h +@@ -28,11 +28,11 @@ + #if PLATFORM(IOS_FAMILY) || (PLATFORM(MAC) && ENABLE(VIDEO_PRESENTATION_MODE)) + + #include "MessageReceiver.h" ++#include "PlaybackSessionManagerProxy.h" + #include + + namespace WebKit { + +-class PlaybackSessionManagerProxy; + class WebProcessProxy; + + class RemotePagePlaybackSessionManagerProxy : public IPC::MessageReceiver, public RefCounted { +diff --git a/Source/WebKit/UIProcess/RemotePageProxy.cpp b/Source/WebKit/UIProcess/RemotePageProxy.cpp +index 22c8a561850249008f998405712e88f1c2229336..70e7f76c32af780642d254003ee834204784694f 100644 +--- a/Source/WebKit/UIProcess/RemotePageProxy.cpp ++++ b/Source/WebKit/UIProcess/RemotePageProxy.cpp +@@ -37,6 +37,7 @@ + #include "ProvisionalFrameProxy.h" + #include "RemotePageDrawingAreaProxy.h" + #include "RemotePageFullscreenManagerProxy.h" ++#include "RemotePagePlaybackSessionManagerProxy.h" + #include "RemotePageVisitedLinkStoreRegistration.h" + #include "UserMediaProcessManager.h" + #include "WebFrameProxy.h" +@@ -51,12 +52,14 @@ + #include + #include + ++ + #if ENABLE(FULLSCREEN_API) + #include "WebFullScreenManagerProxy.h" + #endif + + #if ENABLE(VIDEO_PRESENTATION_MODE) + #include "RemotePageVideoPresentationManagerProxy.h" ++#include "VideoPresentationManagerProxy.h" + #endif + + namespace WebKit { diff --git a/Source/WebKit/UIProcess/WebContextMenuProxy.h b/Source/WebKit/UIProcess/WebContextMenuProxy.h index 697a350812e1bf73dd44cc3d723a6a291f9d59d1..a8e1edd710d88f48632d51fd05aa964732d727d3 100644 --- a/Source/WebKit/UIProcess/WebContextMenuProxy.h @@ -15610,18 +15477,18 @@ index 0000000000000000000000000000000000000000..26a2a3c0791c334f811ec99a630314f8 + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp -index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744cad3254e10 100644 +index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda102142943 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.cpp +++ b/Source/WebKit/UIProcess/WebPageProxy.cpp -@@ -205,6 +205,7 @@ - #include +@@ -206,6 +206,7 @@ #include #include + #include +#include #include #include #include -@@ -217,6 +218,7 @@ +@@ -218,6 +219,7 @@ #include #include #include @@ -15670,7 +15537,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca DEFINE_DEBUG_ONLY_GLOBAL(WTF::RefCountedLeakCounter, webPageProxyCounter, ("WebPageProxy")); #if PLATFORM(COCOA) -@@ -1007,6 +1018,10 @@ WebPageProxy::~WebPageProxy() +@@ -1006,6 +1017,10 @@ WebPageProxy::~WebPageProxy() #endif internals().updatePlayingMediaDidChangeTimer.stop(); @@ -15681,24 +15548,24 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca } Ref WebPageProxy::Internals::protectedPage() const -@@ -1580,7 +1595,7 @@ void WebPageProxy::didAttachToRunningProcess() +@@ -1590,7 +1605,7 @@ void WebPageProxy::didAttachToRunningProcess() #if ENABLE(FULLSCREEN_API) ASSERT(!m_fullScreenManager); -- m_fullScreenManager = WebFullScreenManagerProxy::create(*this, protectedPageClient()->fullScreenManagerProxyClient()); -+ m_fullScreenManager = WebFullScreenManagerProxy::create(*this, m_fullScreenManagerClientOverride ? *m_fullScreenManagerClientOverride : protectedPageClient()->fullScreenManagerProxyClient()); +- m_fullScreenManager = WebFullScreenManagerProxy::create(*this, protectedPageClient()->checkedFullScreenManagerProxyClient().get()); ++ m_fullScreenManager = WebFullScreenManagerProxy::create(*this, m_fullScreenManagerClientOverride ? *m_fullScreenManagerClientOverride : protectedPageClient()->checkedFullScreenManagerProxyClient().get()); #endif #if ENABLE(VIDEO_PRESENTATION_MODE) ASSERT(!m_playbackSessionManager); -@@ -1739,6 +1754,7 @@ void WebPageProxy::initializeWebPage(const Site& site, WebCore::SandboxFlags eff +@@ -1754,6 +1769,7 @@ void WebPageProxy::initializeWebPage(const Site& site, WebCore::SandboxFlags eff if (preferences->siteIsolationEnabled()) browsingContextGroup->addPage(*this); - process->send(Messages::WebProcess::CreateWebPage(m_webPageID, creationParameters(process, *m_drawingArea, m_mainFrame->frameID(), std::nullopt)), 0); + process->send(Messages::WebProcess::CreateWebPage(m_webPageID, creationParameters(process, *protectedDrawingArea(), m_mainFrame->frameID(), std::nullopt)), 0); + m_inspectorController->didInitializeWebPage(); #if ENABLE(WINDOW_PROXY_PROPERTY_ACCESS_NOTIFICATION) - internals().frameLoadStateObserver = makeUniqueWithoutRefCountedCheck(*this); -@@ -2021,6 +2037,21 @@ Ref WebPageProxy::ensureProtectedRunningProcess() + internals().frameLoadStateObserver = WebPageProxyFrameLoadStateObserver::create(); +@@ -2036,6 +2052,21 @@ Ref WebPageProxy::ensureProtectedRunningProcess() return ensureRunningProcess(); } @@ -15720,7 +15587,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca RefPtr WebPageProxy::loadRequest(WebCore::ResourceRequest&& request, ShouldOpenExternalURLsPolicy shouldOpenExternalURLsPolicy, IsPerformingHTTPFallback isPerformingHTTPFallback, std::unique_ptr&& lastNavigationAction, API::Object* userData) { if (m_isClosed) -@@ -2136,11 +2167,29 @@ void WebPageProxy::loadRequestWithNavigationShared(Ref&& proces +@@ -2152,11 +2183,29 @@ void WebPageProxy::loadRequestWithNavigationShared(Ref&& proces navigation->setIsLoadedWithNavigationShared(true); protectedProcess->markProcessAsRecentlyUsed(); @@ -15754,7 +15621,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca }); } -@@ -2696,6 +2745,63 @@ void WebPageProxy::setControlledByAutomation(bool controlled) +@@ -2712,6 +2761,63 @@ void WebPageProxy::setControlledByAutomation(bool controlled) protectedWebsiteDataStore()->protectedNetworkProcess()->send(Messages::NetworkProcess::SetSessionIsControlledByAutomation(m_websiteDataStore->sessionID(), m_controlledByAutomation), 0); } @@ -15818,7 +15685,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca void WebPageProxy::createInspectorTarget(IPC::Connection& connection, const String& targetId, Inspector::InspectorTargetType type) { MESSAGE_CHECK_BASE(!targetId.isEmpty(), connection); -@@ -2955,6 +3061,24 @@ void WebPageProxy::updateActivityState(OptionSet flagsToUpdate) +@@ -2999,6 +3105,24 @@ void WebPageProxy::updateActivityState(OptionSet flagsToUpdate) bool wasVisible = isViewVisible(); RefPtr pageClient = this->pageClient(); internals().activityState.remove(flagsToUpdate); @@ -15843,7 +15710,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca if (flagsToUpdate & ActivityState::IsFocused && pageClient->isViewFocused()) internals().activityState.add(ActivityState::IsFocused); if (flagsToUpdate & ActivityState::WindowIsActive && pageClient->isViewWindowActive()) -@@ -3718,7 +3842,7 @@ void WebPageProxy::performDragOperation(DragData& dragData, const String& dragSt +@@ -3762,7 +3886,7 @@ void WebPageProxy::performDragOperation(DragData& dragData, const String& dragSt if (!hasRunningProcess()) return; @@ -15852,7 +15719,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca URL url { dragData.asURL() }; if (url.protocolIsFile()) protectedLegacyMainFrameProcess()->assumeReadAccessToBaseURL(*this, url.string(), [] { }); -@@ -3746,6 +3870,8 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag +@@ -3790,6 +3914,8 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag if (!hasRunningProcess()) return; @@ -15861,7 +15728,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca auto completionHandler = [this, protectedThis = Ref { *this }, action, dragData] (std::optional dragOperation, WebCore::DragHandlingMethod dragHandlingMethod, bool mouseIsOverFileInput, unsigned numberOfItemsToBeAccepted, const IntRect& insertionRect, const IntRect& editableElementRect, std::optional remoteUserInputEventData) mutable { if (!m_pageClient) return; -@@ -3757,7 +3883,7 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag +@@ -3801,7 +3927,7 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag dragData.setClientPosition(remoteUserInputEventData->transformedPoint); performDragControllerAction(action, dragData, remoteUserInputEventData->targetFrameID); }; @@ -15870,7 +15737,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca ASSERT(dragData.platformData()); sendWithAsyncReplyToProcessContainingFrame(frameID, Messages::WebPage::PerformDragControllerAction(action, dragData.clientPosition(), dragData.globalPosition(), dragData.draggingSourceOperationMask(), *dragData.platformData(), dragData.flags()), WTFMove(completionHandler)); #else -@@ -3792,14 +3918,35 @@ void WebPageProxy::didPerformDragControllerAction(std::optionalpageClient()) pageClient->didPerformDragControllerAction(); @@ -15910,7 +15777,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca } didStartDrag(); } -@@ -3821,6 +3968,24 @@ void WebPageProxy::dragEnded(const IntPoint& clientPosition, const IntPoint& glo +@@ -3865,6 +4012,24 @@ void WebPageProxy::dragEnded(const IntPoint& clientPosition, const IntPoint& glo setDragCaretRect({ }); } @@ -15935,7 +15802,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca void WebPageProxy::didStartDrag() { if (!hasRunningProcess()) -@@ -3828,6 +3993,26 @@ void WebPageProxy::didStartDrag() +@@ -3872,6 +4037,26 @@ void WebPageProxy::didStartDrag() discardQueuedMouseEvents(); send(Messages::WebPage::DidStartDrag()); @@ -15962,20 +15829,20 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca } void WebPageProxy::dragCancelled() -@@ -3999,26 +4184,47 @@ void WebPageProxy::processNextQueuedMouseEvent() +@@ -4043,26 +4228,47 @@ void WebPageProxy::processNextQueuedMouseEvent() process->startResponsivenessTimer(); } - std::optional> sandboxExtensions; -+ m_lastMousePositionForDrag = event.position(); ++ m_lastMousePositionForDrag = event->position(); + if (!m_dragSelectionData) { + std::optional> sandboxExtensions; #if PLATFORM(MAC) -- bool eventMayStartDrag = !m_currentDragOperation && eventType == WebEventType::MouseMove && event.button() != WebMouseEventButton::None; +- bool eventMayStartDrag = !m_currentDragOperation && eventType == WebEventType::MouseMove && event->button() != WebMouseEventButton::None; - if (eventMayStartDrag) - sandboxExtensions = SandboxExtension::createHandlesForMachLookup({ "com.apple.iconservices"_s, "com.apple.iconservices.store"_s }, process->auditToken(), SandboxExtension::MachBootstrapOptions::EnableMachBootstrap); -+ bool eventMayStartDrag = !m_currentDragOperation && eventType == WebEventType::MouseMove && event.button() != WebMouseEventButton::None; ++ bool eventMayStartDrag = !m_currentDragOperation && eventType == WebEventType::MouseMove && event->button() != WebMouseEventButton::None; + if (eventMayStartDrag) + sandboxExtensions = SandboxExtension::createHandlesForMachLookup({ "com.apple.iconservices"_s, "com.apple.iconservices.store"_s }, process->auditToken(), SandboxExtension::MachBootstrapOptions::EnableMachBootstrap); #endif @@ -15983,13 +15850,13 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca - auto eventWithCoalescedEvents = event; + auto eventWithCoalescedEvents = event; -- if (event.type() == WebEventType::MouseMove) { +- if (event->type() == WebEventType::MouseMove) { - internals().coalescedMouseEvents.append(event); -- eventWithCoalescedEvents.setCoalescedEvents(internals().coalescedMouseEvents); +- eventWithCoalescedEvents->setCoalescedEvents(internals().coalescedMouseEvents); - } -+ if (event.type() == WebEventType::MouseMove) { ++ if (event->type() == WebEventType::MouseMove) { + internals().coalescedMouseEvents.append(event); -+ eventWithCoalescedEvents.setCoalescedEvents(internals().coalescedMouseEvents); ++ eventWithCoalescedEvents->setCoalescedEvents(internals().coalescedMouseEvents); + } - LOG_WITH_STREAM(MouseHandling, stream << "UIProcess: sent mouse event " << eventType << " (queue size " << internals().mouseEventQueue.size() << ", coalesced events size " << internals().coalescedMouseEvents.size() << ")"); @@ -16002,9 +15869,9 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca + internals().coalescedMouseEvents.clear(); + } else { +#if PLATFORM(WIN) || PLATFORM(COCOA) -+ DragData dragData(*m_dragSelectionData, event.position(), event.globalPosition(), m_dragSourceOperationMask); ++ DragData dragData(*m_dragSelectionData, event->position(), event->globalPosition(), m_dragSourceOperationMask); +#else -+ DragData dragData(&*m_dragSelectionData, event.position(), event.globalPosition(), m_dragSourceOperationMask); ++ DragData dragData(&*m_dragSelectionData, event->position(), event->globalPosition(), m_dragSourceOperationMask); +#endif + if (eventType == WebEventType::MouseMove) { + dragUpdated(dragData); @@ -16015,14 +15882,14 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca + performDragOperation(dragData, ""_s, WTFMove(sandboxExtensionHandle), WTFMove(sandboxExtensionsForUpload)); + } + m_dragSelectionData = std::nullopt; -+ dragEnded(event.position(), event.globalPosition(), m_dragSourceOperationMask); ++ dragEnded(event->position(), event->globalPosition(), m_dragSourceOperationMask); + } + didReceiveEventIPC(process->connection(), eventType, true, std::nullopt); + } } void WebPageProxy::doAfterProcessingAllPendingMouseEvents(WTF::Function&& action) -@@ -4215,6 +4421,8 @@ void WebPageProxy::wheelEventHandlingCompleted(bool wasHandled) +@@ -4260,6 +4466,8 @@ void WebPageProxy::wheelEventHandlingCompleted(bool wasHandled) if (RefPtr automationSession = m_configuration->processPool().automationSession()) automationSession->wheelEventsFlushedForPage(*this); @@ -16031,7 +15898,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca } void WebPageProxy::cacheWheelEventScrollingAccelerationCurve(const NativeWebWheelEvent& nativeWheelEvent) -@@ -4351,7 +4559,7 @@ static TrackingType mergeTrackingTypes(TrackingType a, TrackingType b) +@@ -4396,7 +4604,7 @@ static TrackingType mergeTrackingTypes(TrackingType a, TrackingType b) void WebPageProxy::updateTouchEventTracking(const WebTouchEvent& touchStartEvent) { @@ -16040,7 +15907,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca for (auto& touchPoint : touchStartEvent.touchPoints()) { auto location = touchPoint.locationInRootView(); auto update = [this, location](TrackingType& trackingType, EventTrackingRegions::EventType eventType) { -@@ -5012,6 +5220,7 @@ void WebPageProxy::receivedNavigationActionPolicyDecision(WebProcessProxy& proce +@@ -5078,6 +5286,7 @@ Ref WebPageProxy::navigationOriginatingPage(const FrameInfoData& f void WebPageProxy::receivedPolicyDecision(PolicyAction action, API::Navigation* navigation, RefPtr&& websitePolicies, Ref&& navigationAction, WillContinueLoadInNewProcess willContinueLoadInNewProcess, std::optional sandboxExtensionHandle, std::optional&& consoleMessage, CompletionHandler&& completionHandler) { @@ -16048,7 +15915,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca if (!hasRunningProcess()) return completionHandler(PolicyDecision { }); -@@ -6013,6 +6222,7 @@ void WebPageProxy::viewScaleFactorDidChange(IPC::Connection& connection, double +@@ -6080,6 +6289,7 @@ void WebPageProxy::viewScaleFactorDidChange(IPC::Connection& connection, double MESSAGE_CHECK_BASE(scaleFactorIsValid(scaleFactor), connection); if (!legacyMainFrameProcess().hasConnection(connection)) return; @@ -16056,35 +15923,33 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca forEachWebContentProcess([&] (auto& process, auto pageID) { if (&process == &legacyMainFrameProcess()) -@@ -6664,6 +6874,7 @@ void WebPageProxy::didDestroyNavigationShared(Ref&& process, We +@@ -6731,6 +6941,7 @@ void WebPageProxy::didDestroyNavigationShared(Ref&& process, We RefPtr protectedPageClient { pageClient() }; - protectedNavigationState()->didDestroyNavigation(process->coreProcessIdentifier(), navigationID); + m_navigationState->didDestroyNavigation(process->coreProcessIdentifier(), navigationID); + m_inspectorController->didDestroyNavigation(navigationID); } void WebPageProxy::didStartProvisionalLoadForFrame(IPC::Connection& connection, FrameIdentifier frameID, FrameInfoData&& frameInfo, ResourceRequest&& request, std::optional navigationID, URL&& url, URL&& unreachableURL, const UserData& userData, WallTime timestamp) -@@ -7008,6 +7219,8 @@ void WebPageProxy::didFailProvisionalLoadForFrameShared(Ref&& p +@@ -7079,6 +7290,8 @@ void WebPageProxy::didFailProvisionalLoadForFrameShared(Ref&& p m_failingProvisionalLoadURL = { }; + m_inspectorController->didFailProvisionalLoadForFrame(*navigationID, error); + // If the provisional page's load fails then we destroy the provisional page. - if (m_provisionalPage && m_provisionalPage->mainFrame() == &frame && willContinueLoading == WillContinueLoading::No) + if (m_provisionalPage && m_provisionalPage->mainFrame() == &frame && (willContinueLoading == WillContinueLoading::No || protectedPreferences()->siteIsolationEnabled())) m_provisionalPage = nullptr; -@@ -8536,8 +8749,9 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w +@@ -8642,6 +8855,8 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w if (RefPtr page = originatingFrameInfo->page()) openerAppInitiatedState = page->lastNavigationWasAppInitiated(); -- auto navigationDataForNewProcess = navigationActionData.hasOpener ? nullptr : makeUnique(navigationActionData); + m_inspectorController->willCreateNewPage(windowFeatures, request.url()); - -+ auto navigationDataForNewProcess = navigationActionData.hasOpener ? nullptr : makeUnique(navigationActionData); ++ auto completionHandler = [ this, protectedThis = Ref { *this }, -@@ -8617,6 +8831,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w +@@ -8721,6 +8936,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w configuration->setOpenedMainFrameName(openedMainFrameName); if (!protectedPreferences()->siteIsolationEnabled()) configuration->setRelatedPage(*this); @@ -16092,7 +15957,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca if (RefPtr openerFrame = WebFrameProxy::webFrame(originatingFrameInfoData.frameID); navigationActionData.hasOpener && openerFrame) { configuration->setOpenerInfo({ { -@@ -8644,6 +8859,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w +@@ -8748,6 +8964,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w void WebPageProxy::showPage() { m_uiClient->showPage(this); @@ -16100,7 +15965,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca } bool WebPageProxy::hasOpenedPage() const -@@ -8775,6 +8991,10 @@ void WebPageProxy::closePage() +@@ -8898,6 +9115,10 @@ void WebPageProxy::closePage() if (isClosed()) return; @@ -16111,7 +15976,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca WEBPAGEPROXY_RELEASE_LOG(Process, "closePage:"); if (RefPtr pageClient = this->pageClient()) pageClient->clearAllEditCommands(); -@@ -8813,6 +9033,8 @@ void WebPageProxy::runJavaScriptAlert(IPC::Connection& connection, FrameIdentifi +@@ -8936,6 +9157,8 @@ void WebPageProxy::runJavaScriptAlert(IPC::Connection& connection, FrameIdentifi } runModalJavaScriptDialog(WTFMove(frame), WTFMove(frameInfo), WTFMove(message), [reply = WTFMove(reply)](WebPageProxy& page, WebFrameProxy* frame, FrameInfoData&& frameInfo, String&& message, CompletionHandler&& completion) mutable { @@ -16120,7 +15985,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca page.m_uiClient->runJavaScriptAlert(page, WTFMove(message), frame, WTFMove(frameInfo), [reply = WTFMove(reply), completion = WTFMove(completion)]() mutable { reply(); completion(); -@@ -8835,6 +9057,8 @@ void WebPageProxy::runJavaScriptConfirm(IPC::Connection& connection, FrameIdenti +@@ -8958,6 +9181,8 @@ void WebPageProxy::runJavaScriptConfirm(IPC::Connection& connection, FrameIdenti if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this, message, std::nullopt); } @@ -16129,7 +15994,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca runModalJavaScriptDialog(WTFMove(frame), WTFMove(frameInfo), WTFMove(message), [reply = WTFMove(reply)](WebPageProxy& page, WebFrameProxy* frame, FrameInfoData&& frameInfo, String&& message, CompletionHandler&& completion) mutable { page.m_uiClient->runJavaScriptConfirm(page, WTFMove(message), frame, WTFMove(frameInfo), [reply = WTFMove(reply), completion = WTFMove(completion)](bool result) mutable { -@@ -8859,6 +9083,8 @@ void WebPageProxy::runJavaScriptPrompt(IPC::Connection& connection, FrameIdentif +@@ -8982,6 +9207,8 @@ void WebPageProxy::runJavaScriptPrompt(IPC::Connection& connection, FrameIdentif if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this, message, defaultValue); } @@ -16138,7 +16003,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca runModalJavaScriptDialog(WTFMove(frame), WTFMove(frameInfo), WTFMove(message), [reply = WTFMove(reply), defaultValue= WTFMove(defaultValue)](WebPageProxy& page, WebFrameProxy* frame, FrameInfoData&& frameInfo, String&& message, CompletionHandler&& completion) mutable { page.m_uiClient->runJavaScriptPrompt(page, WTFMove(message), WTFMove(defaultValue), frame, WTFMove(frameInfo), [reply = WTFMove(reply), completion = WTFMove(completion)](auto& result) mutable { -@@ -9000,6 +9226,8 @@ void WebPageProxy::runBeforeUnloadConfirmPanel(IPC::Connection& connection, Fram +@@ -9123,6 +9350,8 @@ void WebPageProxy::runBeforeUnloadConfirmPanel(IPC::Connection& connection, Fram return; } } @@ -16147,7 +16012,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca // Since runBeforeUnloadConfirmPanel() can spin a nested run loop we need to turn off the responsiveness timer and the tryClose timer. webProcess->stopResponsivenessTimer(); -@@ -9626,6 +9854,11 @@ void WebPageProxy::resourceLoadDidCompleteWithError(ResourceLoadInfo&& loadInfo, +@@ -9758,6 +9987,11 @@ void WebPageProxy::resourceLoadDidCompleteWithError(ResourceLoadInfo&& loadInfo, } #if ENABLE(FULLSCREEN_API) @@ -16159,7 +16024,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca WebFullScreenManagerProxy* WebPageProxy::fullScreenManager() { return m_fullScreenManager.get(); -@@ -9753,6 +9986,17 @@ void WebPageProxy::requestDOMPasteAccess(IPC::Connection& connection, DOMPasteAc +@@ -9890,6 +10124,17 @@ void WebPageProxy::requestDOMPasteAccess(IPC::Connection& connection, DOMPasteAc } } @@ -16177,7 +16042,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca protectedPageClient()->requestDOMPasteAccess(pasteAccessCategory, requiresInteraction, elementRect, originIdentifier, WTFMove(completionHandler)); } -@@ -10795,6 +11039,8 @@ void WebPageProxy::mouseEventHandlingCompleted(std::optional event +@@ -10900,6 +11145,8 @@ void WebPageProxy::mouseEventHandlingCompleted(std::optional event if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->mouseEventsFlushedForPage(*this); didFinishProcessingAllPendingMouseEvents(); @@ -16186,7 +16051,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca } } -@@ -10830,6 +11076,7 @@ void WebPageProxy::keyEventHandlingCompleted(std::optional eventTy +@@ -10935,6 +11182,7 @@ void WebPageProxy::keyEventHandlingCompleted(std::optional eventTy if (!canProcessMoreKeyEvents) { if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->keyboardEventsFlushedForPage(*this); @@ -16194,7 +16059,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca } } -@@ -11262,7 +11509,10 @@ void WebPageProxy::dispatchProcessDidTerminate(WebProcessProxy& process, Process +@@ -11369,7 +11617,10 @@ void WebPageProxy::dispatchProcessDidTerminate(WebProcessProxy& process, Process if (protectedPreferences()->siteIsolationEnabled()) protectedBrowsingContextGroup()->processDidTerminate(*this, process); @@ -16206,7 +16071,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca if (m_loaderClient) handledByClient = reason != ProcessTerminationReason::RequestedByClient && m_loaderClient->processDidCrash(*this); else -@@ -11913,6 +12163,8 @@ WebPageCreationParameters WebPageProxy::creationParameters(WebProcessProxy& proc +@@ -12022,6 +12273,8 @@ WebPageCreationParameters WebPageProxy::creationParameters(WebProcessProxy& proc parameters.canUseCredentialStorage = m_canUseCredentialStorage; parameters.httpsUpgradeEnabled = preferences->upgradeKnownHostsToHTTPSEnabled() ? m_configuration->httpsUpgradeEnabled() : false; @@ -16215,7 +16080,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca #if ENABLE(APP_HIGHLIGHTS) parameters.appHighlightsVisible = appHighlightsVisibility() ? HighlightVisibility::Visible : HighlightVisibility::Hidden; -@@ -12081,8 +12333,47 @@ void WebPageProxy::allowGamepadAccess() +@@ -12190,8 +12443,47 @@ void WebPageProxy::allowGamepadAccess() #endif // ENABLE(GAMEPAD) @@ -16263,7 +16128,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca if (negotiatedLegacyTLS == NegotiatedLegacyTLS::Yes) { m_navigationClient->shouldAllowLegacyTLS(*this, authenticationChallenge.get(), [this, protectedThis = Ref { *this }, authenticationChallenge] (bool shouldAllowLegacyTLS) { if (shouldAllowLegacyTLS) -@@ -12178,6 +12469,12 @@ void WebPageProxy::requestGeolocationPermissionForFrame(IPC::Connection& connect +@@ -12287,6 +12579,12 @@ void WebPageProxy::requestGeolocationPermissionForFrame(IPC::Connection& connect request->deny(); }; @@ -16276,7 +16141,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca // FIXME: Once iOS migrates to the new WKUIDelegate SPI, clean this up // and make it one UIClient call that calls the completionHandler with false // if there is no delegate instead of returning the completionHandler -@@ -12244,6 +12541,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi +@@ -12353,6 +12651,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi shouldChangeDeniedToPrompt = false; if (sessionID().isEphemeral()) { @@ -16289,7 +16154,7 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca completionHandler(shouldChangeDeniedToPrompt ? PermissionState::Prompt : PermissionState::Denied); return; } -@@ -12258,6 +12561,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi +@@ -12367,6 +12671,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi return; } @@ -16303,19 +16168,19 @@ index 7c380219c6ff83938d0b7425b62932521cdb1edb..7439ed4314cd66866e0eb570e46744ca completionHandler(shouldChangeDeniedToPrompt ? PermissionState::Prompt : PermissionState::Denied); return; diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h -index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6821de584 100644 +index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172ef8a2def 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.h +++ b/Source/WebKit/UIProcess/WebPageProxy.h -@@ -26,6 +26,7 @@ - #pragma once - +@@ -28,6 +28,7 @@ + // Including more headers here slows down build times a lot. + // Use forward declarations and WebPageProxyInternals.h instead. #include "APIObject.h" +#include "APIWebsitePolicies.h" #include "MessageReceiver.h" - #include - #include -@@ -46,6 +47,20 @@ - #include + #include + #include +@@ -38,6 +39,20 @@ + #include #include #include +#include "InspectorDialogAgent.h" @@ -16333,26 +16198,26 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 +#include +#endif - #if USE(DICTATION_ALTERNATIVES) - #include -@@ -130,6 +145,7 @@ class DragData; + namespace API { + class Attachment; +@@ -115,6 +130,7 @@ class DragData; class Exception; class FloatPoint; class FloatQuad; -+typedef UncheckedKeyHashMap> DragDataMap; ++typedef HashMap> DragDataMap; class FloatRect; class FloatSize; class FontAttributeChanges; -@@ -735,6 +751,8 @@ public: +@@ -745,6 +761,8 @@ public: void setControlledByAutomation(bool); - WebPageInspectorController& inspectorController() { return *m_inspectorController; } + WebPageInspectorController& inspectorController() { return m_inspectorController.get(); } + InspectorDialogAgent* inspectorDialogAgent() { return m_inspectorDialogAgent; } + void setInspectorDialogAgent(InspectorDialogAgent * dialogAgent) { m_inspectorDialogAgent = dialogAgent; } #if PLATFORM(IOS_FAMILY) void showInspectorIndication(); -@@ -768,6 +786,7 @@ public: +@@ -778,6 +796,7 @@ public: bool hasSleepDisabler() const; #if ENABLE(FULLSCREEN_API) @@ -16360,7 +16225,7 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 WebFullScreenManagerProxy* fullScreenManager(); RefPtr protectedFullScreenManager(); void setFullScreenClientForTesting(std::unique_ptr&&); -@@ -859,6 +878,12 @@ public: +@@ -870,6 +889,12 @@ public: void setPageLoadStateObserver(RefPtr&&); @@ -16373,7 +16238,7 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 void initializeWebPage(const WebCore::Site&, WebCore::SandboxFlags); void setDrawingArea(RefPtr&&); -@@ -890,6 +915,8 @@ public: +@@ -901,6 +926,8 @@ public: RefPtr loadRequest(WebCore::ResourceRequest&&, WebCore::ShouldOpenExternalURLsPolicy, WebCore::IsPerformingHTTPFallback); RefPtr loadRequest(WebCore::ResourceRequest&&, WebCore::ShouldOpenExternalURLsPolicy, WebCore::IsPerformingHTTPFallback, std::unique_ptr&&, API::Object* userData = nullptr); @@ -16382,7 +16247,7 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 RefPtr loadFile(const String& fileURL, const String& resourceDirectoryURL, bool isAppInitiated = true, API::Object* userData = nullptr); RefPtr loadData(Ref&&, const String& MIMEType, const String& encoding, const String& baseURL, API::Object* userData = nullptr); RefPtr loadData(Ref&&, const String& MIMEType, const String& encoding, const String& baseURL, API::Object* userData, WebCore::ShouldOpenExternalURLsPolicy); -@@ -978,6 +1005,7 @@ public: +@@ -990,6 +1017,7 @@ public: PageClient* pageClient() const; RefPtr protectedPageClient() const; @@ -16390,7 +16255,7 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 void setViewNeedsDisplay(const WebCore::Region&); void requestScroll(const WebCore::FloatPoint& scrollPosition, const WebCore::IntPoint& scrollOrigin, WebCore::ScrollIsAnimated); -@@ -1620,17 +1648,23 @@ public: +@@ -1635,17 +1663,23 @@ public: void didStartDrag(); void dragCancelled(); void setDragCaretRect(const WebCore::IntRect&); @@ -16415,7 +16280,7 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 #endif void processDidBecomeUnresponsive(); -@@ -1883,6 +1917,7 @@ public: +@@ -1897,6 +1931,7 @@ public: void setViewportSizeForCSSViewportUnits(const WebCore::FloatSize&); WebCore::FloatSize viewportSizeForCSSViewportUnits() const; @@ -16423,7 +16288,7 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 void didReceiveAuthenticationChallengeProxy(Ref&&, NegotiatedLegacyTLS); void negotiatedLegacyTLS(); void didNegotiateModernTLS(const URL&); -@@ -1916,6 +1951,8 @@ public: +@@ -1930,6 +1965,8 @@ public: #if PLATFORM(COCOA) || PLATFORM(GTK) RefPtr takeViewSnapshot(std::optional&&); RefPtr takeViewSnapshot(std::optional&&, ForceSoftwareCapturingViewportSnapshot); @@ -16432,7 +16297,7 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 #endif void serializeAndWrapCryptoKey(IPC::Connection&, WebCore::CryptoKeyData&&, CompletionHandler>&&)>&&); -@@ -2931,6 +2968,7 @@ private: +@@ -2956,6 +2993,7 @@ private: RefPtr launchProcessForReload(); void requestNotificationPermission(const String& originString, CompletionHandler&&); @@ -16440,7 +16305,7 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 void didChangeContentSize(const WebCore::IntSize&); void didChangeIntrinsicContentSize(const WebCore::IntSize&); -@@ -3450,8 +3488,10 @@ private: +@@ -3482,8 +3520,10 @@ private: String m_openedMainFrameName; RefPtr m_inspector; @@ -16451,7 +16316,7 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 RefPtr m_fullScreenManager; std::unique_ptr m_fullscreenClient; #endif -@@ -3650,6 +3690,22 @@ private: +@@ -3680,6 +3720,22 @@ private: std::optional m_currentDragOperation; bool m_currentDragIsOverFileInput { false }; unsigned m_currentDragNumberOfFilesToBeAccepted { 0 }; @@ -16474,7 +16339,7 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 #endif bool m_mainFrameHasHorizontalScrollbar { false }; -@@ -3821,6 +3877,10 @@ private: +@@ -3851,6 +3907,10 @@ private: RefPtr messageBody; }; Vector m_pendingInjectedBundleMessages; @@ -16486,7 +16351,7 @@ index c220a17ba9d98b37e16bea6ef2c37ffd4599c906..d5e57548822361620b9a18c283e5e5e6 #if PLATFORM(IOS_FAMILY) && ENABLE(DEVICE_ORIENTATION) RefPtr m_webDeviceOrientationUpdateProviderProxy; diff --git a/Source/WebKit/UIProcess/WebPageProxy.messages.in b/Source/WebKit/UIProcess/WebPageProxy.messages.in -index acdb79859685a55f3cda48014621340177f30e3d..36aca25956f9b073eefa5a68a013cde6017df39a 100644 +index fcb0649584da102c1446029342c8f31c59628301..5559c5f690a568bb1b39fe4bc0ac181a8e1953b3 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.messages.in +++ b/Source/WebKit/UIProcess/WebPageProxy.messages.in @@ -35,6 +35,7 @@ messages -> WebPageProxy { @@ -16507,14 +16372,14 @@ index acdb79859685a55f3cda48014621340177f30e3d..36aca25956f9b073eefa5a68a013cde6 #endif +#if PLATFORM(WIN) && ENABLE(DRAG_SUPPORT) -+ StartDrag(UncheckedKeyHashMap> dragDataMap) ++ StartDrag(HashMap> dragDataMap) +#endif + #if PLATFORM(IOS_FAMILY) && ENABLE(DRAG_SUPPORT) WillReceiveEditDragSnapshot() DidReceiveEditDragSnapshot(struct std::optional textIndicator) diff --git a/Source/WebKit/UIProcess/WebProcessCache.cpp b/Source/WebKit/UIProcess/WebProcessCache.cpp -index dc6f440403cccc5cd93f75806cffbf05cc56041c..e880beba2034cc2b87dcfb3e1e8bacf1bed78cf3 100644 +index 9619dd597aec77547cccdf0d3ce6c711335f6244..db169d23388fd7b1cb7b79eef4160301aee0e0ef 100644 --- a/Source/WebKit/UIProcess/WebProcessCache.cpp +++ b/Source/WebKit/UIProcess/WebProcessCache.cpp @@ -100,6 +100,10 @@ bool WebProcessCache::canCacheProcess(WebProcessProxy& process) const @@ -16529,10 +16394,10 @@ index dc6f440403cccc5cd93f75806cffbf05cc56041c..e880beba2034cc2b87dcfb3e1e8bacf1 } diff --git a/Source/WebKit/UIProcess/WebProcessPool.cpp b/Source/WebKit/UIProcess/WebProcessPool.cpp -index 794f1e40dea081e818326f1d4123ebdf33569f86..35b0c4cdda0257c4844d4390d5901e71809c23f8 100644 +index a406b5a1e1523c07cb6dc2eebc493fa4f0026575..1f4fc38edd3756d5423e89a41c30a8ce7d5bf71f 100644 --- a/Source/WebKit/UIProcess/WebProcessPool.cpp +++ b/Source/WebKit/UIProcess/WebProcessPool.cpp -@@ -447,10 +447,10 @@ void WebProcessPool::setAutomationClient(std::unique_ptr& +@@ -439,10 +439,10 @@ void WebProcessPool::setAutomationClient(std::unique_ptr& void WebProcessPool::setOverrideLanguages(Vector&& languages) { @@ -16545,7 +16410,7 @@ index 794f1e40dea081e818326f1d4123ebdf33569f86..35b0c4cdda0257c4844d4390d5901e71 #if ENABLE(GPU_PROCESS) if (RefPtr gpuProcess = GPUProcessProxy::singletonIfCreated()) -@@ -458,9 +458,10 @@ void WebProcessPool::setOverrideLanguages(Vector&& languages) +@@ -450,9 +450,10 @@ void WebProcessPool::setOverrideLanguages(Vector&& languages) #endif #if USE(SOUP) for (Ref networkProcess : NetworkProcessProxy::allNetworkProcesses()) @@ -16557,7 +16422,7 @@ index 794f1e40dea081e818326f1d4123ebdf33569f86..35b0c4cdda0257c4844d4390d5901e71 void WebProcessPool::fullKeyboardAccessModeChanged(bool fullKeyboardAccessEnabled) { -@@ -945,7 +946,7 @@ void WebProcessPool::initializeNewWebProcess(WebProcessProxy& process, WebsiteDa +@@ -999,7 +1000,7 @@ void WebProcessPool::initializeNewWebProcess(WebProcessProxy& process, WebsiteDa #endif parameters.cacheModel = LegacyGlobalSettings::singleton().cacheModel(); @@ -16565,9 +16430,9 @@ index 794f1e40dea081e818326f1d4123ebdf33569f86..35b0c4cdda0257c4844d4390d5901e71 + parameters.overrideLanguages = configuration().overrideLanguages(); /* playwright revert fb205fb */ LOG_WITH_STREAM(Language, stream << "WebProcessPool is initializing a new web process with overrideLanguages: " << parameters.overrideLanguages); - parameters.urlSchemesRegisteredAsEmptyDocument = copyToVector(m_schemesToRegisterAsEmptyDocument); + parameters.urlSchemesRegisteredAsSecure = copyToVector(LegacyGlobalSettings::singleton().schemesToRegisterAsSecure()); diff --git a/Source/WebKit/UIProcess/WebProcessProxy.cpp b/Source/WebKit/UIProcess/WebProcessProxy.cpp -index f1074d1138c9e6f2a76c1ce76807c775b16c657c..b7386197a24a689a5b0cae6772abefb17819dbe9 100644 +index 51546b2db1e5ca415ceb550e69dc2daa982df450..6d10b802e49a3a0b803eb8a630e7c1c83c53f79c 100644 --- a/Source/WebKit/UIProcess/WebProcessProxy.cpp +++ b/Source/WebKit/UIProcess/WebProcessProxy.cpp @@ -208,6 +208,11 @@ Vector> WebProcessProxy::allProcesses() @@ -16610,7 +16475,7 @@ index f1074d1138c9e6f2a76c1ce76807c775b16c657c..b7386197a24a689a5b0cae6772abefb1 if (isPrewarmed()) diff --git a/Source/WebKit/UIProcess/WebProcessProxy.h b/Source/WebKit/UIProcess/WebProcessProxy.h -index 4471fdf4a46180f78cea1cf035671bfe3c83b8e2..0858fe7f56eb3d8648467f5b82c2d77d6c958b77 100644 +index 4512391119154ce5b0ebb892e853499bda9747d9..33404c266e230e0cf62a0d9a597a7b59a047a1f3 100644 --- a/Source/WebKit/UIProcess/WebProcessProxy.h +++ b/Source/WebKit/UIProcess/WebProcessProxy.h @@ -185,6 +185,7 @@ public: @@ -16622,10 +16487,29 @@ index 4471fdf4a46180f78cea1cf035671bfe3c83b8e2..0858fe7f56eb3d8648467f5b82c2d77d void initializeWebProcess(WebProcessCreationParameters&&); diff --git a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp -index c13d50e1828843714be1f49d2b1d5ea45e9c85aa..d7c76f184e77f1d828cde263ddf01f262c6c8f3a 100644 +index 606401f719eecd52737fc3f6f15a328f74f4f5d9..be13e62a1461f21543f0a51c57003779bb3c1c79 100644 --- a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp +++ b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp -@@ -2061,6 +2061,15 @@ void WebsiteDataStore::setCacheModelSynchronouslyForTesting(CacheModel cacheMode +@@ -321,15 +321,10 @@ SOAuthorizationCoordinator& WebsiteDataStore::soAuthorizationCoordinator(const W + + static Ref networkProcessForSession(PAL::SessionID sessionID) + { +-#if ((PLATFORM(GTK) || PLATFORM(WPE)) && !ENABLE(2022_GLIB_API)) +- if (sessionID.isEphemeral()) { +- // Reuse a previous persistent session network process for ephemeral sessions. +- for (auto& dataStore : allDataStores().values()) { +- if (dataStore->isPersistent()) +- return dataStore->networkProcess(); +- } +- } ++// Playwright begin ++#if PLATFORM(GTK) || PLATFORM(WPE) + return NetworkProcessProxy::create(); ++// Playwright end + #else + UNUSED_PARAM(sessionID); + return NetworkProcessProxy::ensureDefaultNetworkProcess(); +@@ -2061,6 +2056,15 @@ void WebsiteDataStore::setCacheModelSynchronouslyForTesting(CacheModel cacheMode processPool->setCacheModelSynchronouslyForTesting(cacheModel); } @@ -16641,7 +16525,7 @@ index c13d50e1828843714be1f49d2b1d5ea45e9c85aa..d7c76f184e77f1d828cde263ddf01f26 Vector WebsiteDataStore::parametersFromEachWebsiteDataStore() { return WTF::map(allDataStores(), [](auto& entry) { -@@ -2509,6 +2518,12 @@ void WebsiteDataStore::originDirectoryForTesting(WebCore::ClientOrigin&& origin, +@@ -2504,6 +2508,12 @@ void WebsiteDataStore::originDirectoryForTesting(WebCore::ClientOrigin&& origin, protectedNetworkProcess()->websiteDataOriginDirectoryForTesting(m_sessionID, WTFMove(origin), type, WTFMove(completionHandler)); } @@ -16655,7 +16539,7 @@ index c13d50e1828843714be1f49d2b1d5ea45e9c85aa..d7c76f184e77f1d828cde263ddf01f26 void WebsiteDataStore::hasAppBoundSession(CompletionHandler&& completionHandler) const { diff --git a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h -index f1a113cc08f6543ab22be75a03823f49a3f52017..ef6dc1b450e2f4b0bcd374135a4cbc3d626a89d9 100644 +index 64d8fb4e83836c3dde325e77ded6b014e819cb10..c4b923995301697f8a52d72e74c842dcf4ec8161 100644 --- a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h +++ b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h @@ -98,6 +98,7 @@ class DeviceIdHashSaltStorage; @@ -16700,7 +16584,7 @@ index f1a113cc08f6543ab22be75a03823f49a3f52017..ef6dc1b450e2f4b0bcd374135a4cbc3d void setNetworkProxySettings(WebCore::SoupNetworkProxySettings&&); const WebCore::SoupNetworkProxySettings& networkProxySettings() const { return m_networkProxySettings; } void setCookiePersistentStorage(const String&, SoupCookiePersistentStorageType); -@@ -420,6 +432,12 @@ public: +@@ -419,6 +431,12 @@ public: static const String& defaultBaseDataDirectory(); #endif @@ -16713,7 +16597,7 @@ index f1a113cc08f6543ab22be75a03823f49a3f52017..ef6dc1b450e2f4b0bcd374135a4cbc3d void resetQuota(CompletionHandler&&); void resetStoragePersistedState(CompletionHandler&&); #if PLATFORM(IOS_FAMILY) -@@ -618,7 +636,9 @@ private: +@@ -616,7 +634,9 @@ private: #if USE(SOUP) bool m_persistentCredentialStorageEnabled { true }; @@ -16724,7 +16608,7 @@ index f1a113cc08f6543ab22be75a03823f49a3f52017..ef6dc1b450e2f4b0bcd374135a4cbc3d WebCore::SoupNetworkProxySettings m_networkProxySettings; String m_cookiePersistentStoragePath; SoupCookiePersistentStorageType m_cookiePersistentStorageType { SoupCookiePersistentStorageType::SQLite }; -@@ -645,6 +665,10 @@ private: +@@ -643,6 +663,10 @@ private: RefPtr m_cookieStore; RefPtr m_networkProcess; @@ -16779,7 +16663,7 @@ index 96bf77411e2e1f4c835f56b409dc179977d197ee..512af5ffce511711b502248e34e49e45 RunLoop::Timer m_destroyLaterTimer; diff --git a/Source/WebKit/UIProcess/glib/InspectorPlaywrightAgentClientGLib.cpp b/Source/WebKit/UIProcess/glib/InspectorPlaywrightAgentClientGLib.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..5185677fc67a418fac7a09c69246f4406a74c706 +index 0000000000000000000000000000000000000000..dfa06245b5411f2bc6ca7cebdb7c6d5fb46cd800 --- /dev/null +++ b/Source/WebKit/UIProcess/glib/InspectorPlaywrightAgentClientGLib.cpp @@ -0,0 +1,192 @@ @@ -16859,7 +16743,7 @@ index 0000000000000000000000000000000000000000..5185677fc67a418fac7a09c69246f440 + } + } + ignoreHosts.append(nullptr); -+ return parseRawProxySettings(proxyServer, ignoreHosts.data()); ++ return parseRawProxySettings(proxyServer, ignoreHosts.mutableSpan().data()); +} + +InspectorPlaywrightAgentClientGlib::InspectorPlaywrightAgentClientGlib(const WTF::String& proxyURI, const char* const* ignoreHosts) @@ -17043,10 +16927,10 @@ index 0000000000000000000000000000000000000000..441442d899e4088f5c24ae9f70c3e4ff + +#endif // ENABLE(REMOTE_INSPECTOR) diff --git a/Source/WebKit/UIProcess/glib/WebProcessPoolGLib.cpp b/Source/WebKit/UIProcess/glib/WebProcessPoolGLib.cpp -index 51ba8b585ca37a2eed54bce5218e1c92c2844cc6..dc04a2ca0b0ac2333036b897dd18d5303c5237c6 100644 +index 4d782728182319b3d6e8e6d05580d96dd7ea19ac..c28dff07b154dba6ba82e94953b7b24e25a89106 100644 --- a/Source/WebKit/UIProcess/glib/WebProcessPoolGLib.cpp +++ b/Source/WebKit/UIProcess/glib/WebProcessPoolGLib.cpp -@@ -124,6 +124,8 @@ static OptionSet availableInputDevices() +@@ -126,6 +126,8 @@ static OptionSet availableInputDevices() return toAvailableInputDevices(gdk_seat_get_capabilities(seat)); } #endif @@ -17188,7 +17072,7 @@ index 0000000000000000000000000000000000000000..bf78de1915940c2d3292514cf0fe4e68 + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/gtk/SystemSettingsManagerProxyGtk.cpp b/Source/WebKit/UIProcess/gtk/SystemSettingsManagerProxyGtk.cpp -index 9ef483a6b0ab1558c059304d727311ea7984184d..f532d2d2dbe2d7cb66323b6ea5fe2daace15d7b8 100644 +index 349cd3d9efd7a921560fa2f1a9e1a2238e415938..9df937535595281039660b060c7fba221d110691 100644 --- a/Source/WebKit/UIProcess/gtk/SystemSettingsManagerProxyGtk.cpp +++ b/Source/WebKit/UIProcess/gtk/SystemSettingsManagerProxyGtk.cpp @@ -126,6 +126,8 @@ int SystemSettingsManagerProxy::xftDPI() const @@ -17202,10 +17086,10 @@ index 9ef483a6b0ab1558c059304d727311ea7984184d..f532d2d2dbe2d7cb66323b6ea5fe2daa GtkFontRendering fontRendering; diff --git a/Source/WebKit/UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp b/Source/WebKit/UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..7d9672b0f831b5b7f6acf14ede26e1e8e9a65389 +index 0000000000000000000000000000000000000000..6d763018a12b3d7cd33af84b23f4340cea194bce --- /dev/null +++ b/Source/WebKit/UIProcess/gtk/WebPageInspectorEmulationAgentGtk.cpp -@@ -0,0 +1,117 @@ +@@ -0,0 +1,119 @@ +/* + * Copyright (C) 2019 Microsoft Corporation. + * @@ -17235,6 +17119,8 @@ index 0000000000000000000000000000000000000000..7d9672b0f831b5b7f6acf14ede26e1e8 +#include "DrawingAreaProxyCoordinatedGraphics.h" +#include "WebPageInspectorEmulationAgent.h" +#include "WebPageProxy.h" ++#include ++#include +#include +#include + @@ -17406,7 +17292,7 @@ index 0000000000000000000000000000000000000000..36ab6e9aec9f8d79fb13a8a49beadaaf + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/gtk/WebPasteboardProxyGtk.cpp b/Source/WebKit/UIProcess/gtk/WebPasteboardProxyGtk.cpp -index c7db5171b505ea9656f409c95c422dce9f0fa1ae..1992a112468e03840be3696245eecd8452ca51ed 100644 +index e4f31f89b42bc7884da760a18150bd1274d0383d..b2390de63e48a8a48a798199aa99203ef9390451 100644 --- a/Source/WebKit/UIProcess/gtk/WebPasteboardProxyGtk.cpp +++ b/Source/WebKit/UIProcess/gtk/WebPasteboardProxyGtk.cpp @@ -85,8 +85,10 @@ void WebPasteboardProxy::setPrimarySelectionOwner(WebFrameProxy* frame) @@ -17422,6 +17308,23 @@ index c7db5171b505ea9656f409c95c422dce9f0fa1ae..1992a112468e03840be3696245eecd84 m_primarySelectionOwner = frame; } +diff --git a/Source/WebKit/UIProcess/mac/CorrectionPanel.h b/Source/WebKit/UIProcess/mac/CorrectionPanel.h +index 8afb6132fad823816f84328a8b0a1a514f998bf7..54b582e60f4b16b3c7ba038c8c52466cce9875c4 100644 +--- a/Source/WebKit/UIProcess/mac/CorrectionPanel.h ++++ b/Source/WebKit/UIProcess/mac/CorrectionPanel.h +@@ -33,11 +33,10 @@ + #import + #import + #import ++#import "WebViewImpl.h" + + namespace WebKit { + +-class WebViewImpl; +- + class CorrectionPanel { + public: + CorrectionPanel(); diff --git a/Source/WebKit/UIProcess/mac/InspectorPlaywrightAgentClientMac.h b/Source/WebKit/UIProcess/mac/InspectorPlaywrightAgentClientMac.h new file mode 100644 index 0000000000000000000000000000000000000000..2aabc02a4b5432f68a6e85fd9689775608f05a67 @@ -17632,10 +17535,22 @@ index 0000000000000000000000000000000000000000..8adbd51bfecad2a273117588bf50f8f7 + +#endif diff --git a/Source/WebKit/UIProcess/mac/PageClientImplMac.h b/Source/WebKit/UIProcess/mac/PageClientImplMac.h -index 27627ddb817e90c92ff5e533618ded9d017d36d9..9169b380a79d57201a7d3dceab04a1d16d987865 100644 +index c0de66cc6e82f1a2459ba006b5ac0fd63d8da261..a01639bab522df3ecff9df5aa4b6218633b2f049 100644 --- a/Source/WebKit/UIProcess/mac/PageClientImplMac.h +++ b/Source/WebKit/UIProcess/mac/PageClientImplMac.h -@@ -61,6 +61,8 @@ class PageClientImpl final : public PageClientImplCocoa +@@ -31,9 +31,11 @@ + #include "PageClientImplCocoa.h" + #include "WebFullScreenManagerProxy.h" + #include ++#include + #include + #include + #include ++#include + + @class WKEditorUndoTarget; + @class WKView; +@@ -61,6 +63,8 @@ class PageClientImpl final : public PageClientImplCocoa WTF_OVERRIDE_DELETE_FOR_CHECKED_PTR(PageClientImpl); #endif public: @@ -17644,7 +17559,7 @@ index 27627ddb817e90c92ff5e533618ded9d017d36d9..9169b380a79d57201a7d3dceab04a1d1 PageClientImpl(NSView *, WKWebView *); virtual ~PageClientImpl(); -@@ -176,6 +178,9 @@ private: +@@ -175,6 +179,9 @@ private: void updateAcceleratedCompositingMode(const LayerTreeContext&) override; void didFirstLayerFlush(const LayerTreeContext&) override; @@ -17654,7 +17569,7 @@ index 27627ddb817e90c92ff5e533618ded9d017d36d9..9169b380a79d57201a7d3dceab04a1d1 RefPtr takeViewSnapshot(std::optional&&) override; RefPtr takeViewSnapshot(std::optional&&, ForceSoftwareCapturingViewportSnapshot) override; void wheelEventWasNotHandledByWebCore(const NativeWebWheelEvent&) override; -@@ -227,6 +232,10 @@ private: +@@ -226,6 +233,10 @@ private: void beganExitFullScreen(const WebCore::IntRect& initialFrame, const WebCore::IntRect& finalFrame, CompletionHandler&&) override; #endif @@ -17666,10 +17581,10 @@ index 27627ddb817e90c92ff5e533618ded9d017d36d9..9169b380a79d57201a7d3dceab04a1d1 void navigationGestureWillEnd(bool willNavigate, WebBackForwardListItem&) override; void navigationGestureDidEnd(bool willNavigate, WebBackForwardListItem&) override; diff --git a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm -index 22f6f6f08ba1601647898ca0f350672c3cac3816..6fad8bc05d1b458e25f8ddb9515ec8e1dcd896d6 100644 +index 0af0f031313b707c18ad675115385c42d5a1284f..4408666f4d7b4e67670f3edfabe9a1407e22afe2 100644 --- a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm +++ b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm -@@ -108,6 +108,13 @@ namespace WebKit { +@@ -107,6 +107,13 @@ namespace WebKit { using namespace WebCore; @@ -17683,7 +17598,7 @@ index 22f6f6f08ba1601647898ca0f350672c3cac3816..6fad8bc05d1b458e25f8ddb9515ec8e1 PageClientImpl::PageClientImpl(NSView *view, WKWebView *webView) : PageClientImplCocoa(webView) , m_view(view) -@@ -161,6 +168,9 @@ NSWindow *PageClientImpl::activeWindow() const +@@ -162,6 +169,9 @@ NSWindow *PageClientImpl::activeWindow() const bool PageClientImpl::isViewWindowActive() { @@ -17693,7 +17608,7 @@ index 22f6f6f08ba1601647898ca0f350672c3cac3816..6fad8bc05d1b458e25f8ddb9515ec8e1 ASSERT(hasProcessPrivilege(ProcessPrivilege::CanCommunicateWithWindowServer)); RetainPtr activeViewWindow = activeWindow(); return activeViewWindow.get().isKeyWindow || (activeViewWindow && [NSApp keyWindow] == activeViewWindow.get()); -@@ -168,6 +178,9 @@ bool PageClientImpl::isViewWindowActive() +@@ -169,6 +179,9 @@ bool PageClientImpl::isViewWindowActive() bool PageClientImpl::isViewFocused() { @@ -17702,8 +17617,8 @@ index 22f6f6f08ba1601647898ca0f350672c3cac3816..6fad8bc05d1b458e25f8ddb9515ec8e1 + // FIXME: This is called from the WebPageProxy constructor before we have a WebViewImpl. // Once WebViewImpl and PageClient merge, this won't be a problem. - if (!m_impl) -@@ -191,6 +204,9 @@ void PageClientImpl::makeFirstResponder() + if (CheckedPtr impl = m_impl.get()) +@@ -192,6 +205,9 @@ void PageClientImpl::makeFirstResponder() bool PageClientImpl::isViewVisible() { @@ -17713,35 +17628,35 @@ index 22f6f6f08ba1601647898ca0f350672c3cac3816..6fad8bc05d1b458e25f8ddb9515ec8e1 RetainPtr activeView = this->activeView(); RetainPtr activeViewWindow = activeWindow(); -@@ -265,7 +281,8 @@ void PageClientImpl::didRelaunchProcess() +@@ -267,7 +283,8 @@ void PageClientImpl::didRelaunchProcess() void PageClientImpl::preferencesDidChange() { -- m_impl->preferencesDidChange(); -+ if (m_impl) -+ m_impl->preferencesDidChange(); +- checkedImpl()->preferencesDidChange(); ++ if (CheckedPtr impl = m_impl.get()) ++ impl->preferencesDidChange(); } void PageClientImpl::toolTipChanged(const String& oldToolTip, const String& newToolTip) -@@ -474,6 +491,8 @@ IntRect PageClientImpl::rootViewToAccessibilityScreen(const IntRect& rect) +@@ -477,6 +494,8 @@ IntRect PageClientImpl::rootViewToAccessibilityScreen(const IntRect& rect) void PageClientImpl::doneWithKeyEvent(const NativeWebKeyboardEvent& event, bool eventWasHandled) { + if (!event.nativeEvent()) + return; - m_impl->doneWithKeyEvent(event.nativeEvent(), eventWasHandled); + checkedImpl()->doneWithKeyEvent(event.nativeEvent(), eventWasHandled); } -@@ -493,6 +512,8 @@ void PageClientImpl::computeHasVisualSearchResults(const URL& imageURL, Shareabl +@@ -496,6 +515,8 @@ void PageClientImpl::computeHasVisualSearchResults(const URL& imageURL, Shareabl RefPtr PageClientImpl::createPopupMenuProxy(WebPageProxy& page) { + if (_headless) + return nullptr; - return WebPopupMenuProxyMac::create(m_view.get().get(), page.popupMenuClient()); + return WebPopupMenuProxyMac::create(m_view.get().get(), page.checkedPopupMenuClient().get()); } -@@ -633,6 +654,12 @@ CALayer *PageClientImpl::footerBannerLayer() const +@@ -621,6 +642,12 @@ CALayer *PageClientImpl::footerBannerLayer() const return m_impl->footerBannerLayer(); } @@ -17753,8 +17668,8 @@ index 22f6f6f08ba1601647898ca0f350672c3cac3816..6fad8bc05d1b458e25f8ddb9515ec8e1 + RefPtr PageClientImpl::takeViewSnapshot(std::optional&&) { - return m_impl->takeViewSnapshot(); -@@ -844,6 +871,13 @@ void PageClientImpl::beganExitFullScreen(const IntRect& initialFrame, const IntR + return checkedImpl()->takeViewSnapshot(); +@@ -832,6 +859,13 @@ void PageClientImpl::beganExitFullScreen(const IntRect& initialFrame, const IntR #endif // ENABLE(FULLSCREEN_API) @@ -17767,15 +17682,15 @@ index 22f6f6f08ba1601647898ca0f350672c3cac3816..6fad8bc05d1b458e25f8ddb9515ec8e1 + void PageClientImpl::navigationGestureDidBegin() { - m_impl->dismissContentRelativeChildWindowsWithAnimation(true); -@@ -1024,6 +1058,9 @@ void PageClientImpl::requestScrollToRect(const WebCore::FloatRect& targetRect, c + checkedImpl()->dismissContentRelativeChildWindowsWithAnimation(true); +@@ -1012,6 +1046,9 @@ void PageClientImpl::requestScrollToRect(const WebCore::FloatRect& targetRect, c bool PageClientImpl::windowIsFrontWindowUnderMouse(const NativeWebMouseEvent& event) { + // Simulated event. + if (!event.nativeEvent()) + return false; - return m_impl->windowIsFrontWindowUnderMouse(event.nativeEvent()); + return checkedImpl()->windowIsFrontWindowUnderMouse(event.nativeEvent()); } diff --git a/Source/WebKit/UIProcess/mac/SecItemShimProxy.messages.in b/Source/WebKit/UIProcess/mac/SecItemShimProxy.messages.in @@ -17801,6 +17716,18 @@ index f46895285dbc84c624537a194814c18f771a0c08..29ef9e5afa13b8d2b47b7f2dd4ce3784 } +#endif +diff --git a/Source/WebKit/UIProcess/mac/WKFullScreenWindowController.mm b/Source/WebKit/UIProcess/mac/WKFullScreenWindowController.mm +index b9ffbadc03edbe9d260d546b59933c1c50d4148d..d0559dbd0bdfe0aa7c83ad97f5d01d8213c0caf2 100644 +--- a/Source/WebKit/UIProcess/mac/WKFullScreenWindowController.mm ++++ b/Source/WebKit/UIProcess/mac/WKFullScreenWindowController.mm +@@ -29,6 +29,7 @@ + #if ENABLE(FULLSCREEN_API) && !PLATFORM(IOS_FAMILY) + + #import "AppKitSPI.h" ++#import "GPUProcessProxy.h" + #import "LayerTreeContext.h" + #import "NativeWebMouseEvent.h" + #import "VideoPresentationManagerProxy.h" diff --git a/Source/WebKit/UIProcess/mac/WebContextMenuProxyMac.h b/Source/WebKit/UIProcess/mac/WebContextMenuProxyMac.h index a3c53c0bf913385d4d2d92900360d5f7d75927f8..e5570ef599ff1b59224648c353f8ab16f8fe7f88 100644 --- a/Source/WebKit/UIProcess/mac/WebContextMenuProxyMac.h @@ -17814,10 +17741,10 @@ index a3c53c0bf913385d4d2d92900360d5f7d75927f8..e5570ef599ff1b59224648c353f8ab16 bool showAfterPostProcessingContextData(); diff --git a/Source/WebKit/UIProcess/mac/WebContextMenuProxyMac.mm b/Source/WebKit/UIProcess/mac/WebContextMenuProxyMac.mm -index c8d9a4ae3fb5ef85afa837a1743c119ae34e1f7b..486a774cd1c98d9985c9d3c92418f835a9be2de1 100644 +index 273569e22d7c39c451fa39b21d4a0672b04773ad..84632cafb18c3d0f5367ba771daea12e30b2110a 100644 --- a/Source/WebKit/UIProcess/mac/WebContextMenuProxyMac.mm +++ b/Source/WebKit/UIProcess/mac/WebContextMenuProxyMac.mm -@@ -530,6 +530,12 @@ RetainPtr WebContextMenuProxyMac::createShareMenuItem(ShareMenuItemT +@@ -527,6 +527,12 @@ RetainPtr WebContextMenuProxyMac::createShareMenuItem(ShareMenuItemT } #endif @@ -18022,18 +17949,18 @@ index 0000000000000000000000000000000000000000..dd52991f936aa1c046b404801ee97237 + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.h b/Source/WebKit/UIProcess/mac/WebViewImpl.h -index 85f0df23b0b5c1e76f2c9e12ad43b9802c23f579..9202e7923ded8b543dcee636166c100e739a094f 100644 +index ad45b454acf088d5bb375971aae99684ea06d3ab..a9b4c26971969cf352cc045b01505a1751b44e46 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.h +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.h -@@ -35,6 +35,7 @@ +@@ -34,6 +34,7 @@ + #include "PDFPluginIdentifier.h" #include "WKLayoutMode.h" - #include "WKTextAnimationType.h" #include +#include #include #include #include -@@ -570,6 +571,9 @@ public: +@@ -565,6 +566,9 @@ public: void provideDataForPasteboard(NSPasteboard *, NSString *type); NSArray *namesOfPromisedFilesDroppedAtDestination(NSURL *dropDestination); @@ -18044,10 +17971,10 @@ index 85f0df23b0b5c1e76f2c9e12ad43b9802c23f579..9202e7923ded8b543dcee636166c100e RefPtr takeViewSnapshot(ForceSoftwareCapturingViewportSnapshot); void saveBackForwardSnapshotForCurrentItem(); diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.mm b/Source/WebKit/UIProcess/mac/WebViewImpl.mm -index fa8aba243a8808dc781d40f1daf1480e6b655656..85f02dbfa3cfe47ee2adceee2714104445cdb7ba 100644 +index 1e906144a471367de596ccf5dd0ece2706306fe6..51cc9282b0b6d0b66df9f3b4fb69da7261b6e8d3 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.mm +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.mm -@@ -2444,6 +2444,11 @@ WebCore::DestinationColorSpace WebViewImpl::colorSpace() +@@ -2477,6 +2477,11 @@ WebCore::DestinationColorSpace WebViewImpl::colorSpace() if (!m_colorSpace) m_colorSpace = [NSColorSpace sRGBColorSpace]; } @@ -18059,7 +17986,7 @@ index fa8aba243a8808dc781d40f1daf1480e6b655656..85f02dbfa3cfe47ee2adceee27141044 ASSERT(m_colorSpace); return WebCore::DestinationColorSpace { [m_colorSpace CGColorSpace] }; -@@ -4724,6 +4729,17 @@ static RetainPtr takeWindowSnapshot(CGSWindowID windowID, bool captu +@@ -4693,6 +4698,17 @@ static RetainPtr takeWindowSnapshot(CGSWindowID windowID, bool captu return WebCore::cgWindowListCreateImage(CGRectNull, kCGWindowListOptionIncludingWindow, windowID, imageOptions); } @@ -18281,7 +18208,7 @@ index 0000000000000000000000000000000000000000..135a60361fa8fbf907382625e7c8dd4e + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/win/WebContextMenuProxyWin.cpp b/Source/WebKit/UIProcess/win/WebContextMenuProxyWin.cpp -index bae35256ed815f7dac0b11ae439531d4ef3cb108..81e063b50a0132f8b36f3461f95d2ce1968198f5 100644 +index ed40616a49301967dc1c824f68cc57f628357c64..598e812dcfb1d4927841489086386834a2fbe0d5 100644 --- a/Source/WebKit/UIProcess/win/WebContextMenuProxyWin.cpp +++ b/Source/WebKit/UIProcess/win/WebContextMenuProxyWin.cpp @@ -115,5 +115,11 @@ WebContextMenuProxyWin::~WebContextMenuProxyWin() @@ -18434,7 +18361,7 @@ index 0000000000000000000000000000000000000000..8b474c730139b44a13c9d5b2d13ee204 + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/win/WebView.cpp b/Source/WebKit/UIProcess/win/WebView.cpp -index 5180da3ec22d56e8a9520e31cad076d86ae5be9f..f01be74573843e7b374c34035c09f114bdc7349c 100644 +index 2487c6de5b717dfea02a6b5a127c815019582d3c..eae5b89de5f54e94f439a7fbe72547743ad96749 100644 --- a/Source/WebKit/UIProcess/win/WebView.cpp +++ b/Source/WebKit/UIProcess/win/WebView.cpp @@ -576,7 +576,7 @@ LRESULT WebView::onSizeEvent(HWND hwnd, UINT, WPARAM, LPARAM lParam, bool& handl @@ -18987,10 +18914,10 @@ index 9b688ad328317fea4fd96ce66e9714bad8f0f937..402a36a9c565e13ec298aa7f014f0d92 } // namespace WebKit diff --git a/Source/WebKit/WebKit.xcodeproj/project.pbxproj b/Source/WebKit/WebKit.xcodeproj/project.pbxproj -index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d48806565ac2 100644 +index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279ad3acae1 100644 --- a/Source/WebKit/WebKit.xcodeproj/project.pbxproj +++ b/Source/WebKit/WebKit.xcodeproj/project.pbxproj -@@ -1560,6 +1560,7 @@ +@@ -1565,6 +1565,7 @@ 5CABDC8722C40FED001EDE8E /* APIMessageListener.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CABDC8322C40FA7001EDE8E /* APIMessageListener.h */; }; 5CADDE05215046BD0067D309 /* WKWebProcess.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C74300E21500492004BFA17 /* WKWebProcess.h */; settings = {ATTRIBUTES = (Private, ); }; }; 5CAECB6627465AE400AB78D0 /* UnifiedSource115.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5CAECB5E27465AE300AB78D0 /* UnifiedSource115.cpp */; }; @@ -18998,7 +18925,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 5CAF7AA726F93AB00003F19E /* adattributiond.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5CAF7AA526F93A950003F19E /* adattributiond.cpp */; }; 5CAFDE452130846300B1F7E1 /* _WKInspector.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CAFDE422130843500B1F7E1 /* _WKInspector.h */; settings = {ATTRIBUTES = (Private, ); }; }; 5CAFDE472130846A00B1F7E1 /* _WKInspectorInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CAFDE442130843600B1F7E1 /* _WKInspectorInternal.h */; }; -@@ -2346,6 +2347,18 @@ +@@ -2352,6 +2353,18 @@ DF0C5F28252ECB8E00D921DB /* WKDownload.h in Headers */ = {isa = PBXBuildFile; fileRef = DF0C5F24252ECB8D00D921DB /* WKDownload.h */; settings = {ATTRIBUTES = (Public, ); }; }; DF0C5F2A252ECB8E00D921DB /* WKDownloadDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = DF0C5F26252ECB8E00D921DB /* WKDownloadDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; DF0C5F2B252ED44000D921DB /* WKDownloadInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = DF0C5F25252ECB8E00D921DB /* WKDownloadInternal.h */; }; @@ -19017,7 +18944,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 DF462E0F23F22F5500EFF35F /* WKHTTPCookieStorePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DF462E0E23F22F5300EFF35F /* WKHTTPCookieStorePrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; DF462E1223F338BE00EFF35F /* WKContentWorldPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DF462E1123F338AD00EFF35F /* WKContentWorldPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; DF7A231C291B088D00B98DF3 /* WKSnapshotConfigurationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DF7A231B291B088D00B98DF3 /* WKSnapshotConfigurationPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; -@@ -2447,6 +2460,8 @@ +@@ -2452,6 +2465,8 @@ E5BEF6822130C48000F31111 /* WebDataListSuggestionsDropdownIOS.h in Headers */ = {isa = PBXBuildFile; fileRef = E5BEF6802130C47F00F31111 /* WebDataListSuggestionsDropdownIOS.h */; }; E5CB07DC20E1678F0022C183 /* WKFormColorControl.h in Headers */ = {isa = PBXBuildFile; fileRef = E5CB07DA20E1678F0022C183 /* WKFormColorControl.h */; }; E5CBA76427A318E100DF7858 /* UnifiedSource120.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E5CBA75F27A3187800DF7858 /* UnifiedSource120.cpp */; }; @@ -19026,7 +18953,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 E5CBA76527A318E100DF7858 /* UnifiedSource118.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E5CBA76127A3187900DF7858 /* UnifiedSource118.cpp */; }; E5CBA76627A318E100DF7858 /* UnifiedSource116.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E5CBA76327A3187B00DF7858 /* UnifiedSource116.cpp */; }; E5CBA76727A318E100DF7858 /* UnifiedSource119.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E5CBA76027A3187900DF7858 /* UnifiedSource119.cpp */; }; -@@ -2485,6 +2500,9 @@ +@@ -2491,6 +2506,9 @@ F3EEEE592DB318270038CC1D /* BidiBrowserAgent.h in Headers */ = {isa = PBXBuildFile; fileRef = F3EEEE572DB318270038CC1D /* BidiBrowserAgent.h */; }; F3EEEE5A2DB318270038CC1D /* BidiBrowserAgent.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F3EEEE582DB318270038CC1D /* BidiBrowserAgent.cpp */; }; F404455C2D5CFB56000E587E /* AppKitSoftLink.h in Headers */ = {isa = PBXBuildFile; fileRef = F404455A2D5CFB56000E587E /* AppKitSoftLink.h */; }; @@ -19036,7 +18963,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 F409BA181E6E64BC009DA28E /* WKDragDestinationAction.h in Headers */ = {isa = PBXBuildFile; fileRef = F409BA171E6E64B3009DA28E /* WKDragDestinationAction.h */; settings = {ATTRIBUTES = (Private, ); }; }; F40C3B712AB401C5007A3567 /* WKDatePickerPopoverController.h in Headers */ = {isa = PBXBuildFile; fileRef = F40C3B6F2AB40167007A3567 /* WKDatePickerPopoverController.h */; }; F41145682CD939E0004CDBD1 /* _WKTouchEventGenerator.h in Headers */ = {isa = PBXBuildFile; fileRef = F41145652CD939E0004CDBD1 /* _WKTouchEventGenerator.h */; settings = {ATTRIBUTES = (Private, ); }; }; -@@ -6443,6 +6461,7 @@ +@@ -6458,6 +6476,7 @@ 5CABDC8522C40FCC001EDE8E /* WKMessageListener.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WKMessageListener.h; sourceTree = ""; }; 5CABE07A28F60E8A00D83FD9 /* WebPushMessage.serialization.in */ = {isa = PBXFileReference; lastKnownFileType = text; path = WebPushMessage.serialization.in; sourceTree = ""; }; 5CADDE0D2151AA010067D309 /* AuthenticationChallengeDisposition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AuthenticationChallengeDisposition.h; sourceTree = ""; }; @@ -19044,7 +18971,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 5CAECB5E27465AE300AB78D0 /* UnifiedSource115.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = UnifiedSource115.cpp; sourceTree = ""; }; 5CAF7AA426F93A750003F19E /* adattributiond */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = adattributiond; sourceTree = BUILT_PRODUCTS_DIR; }; 5CAF7AA526F93A950003F19E /* adattributiond.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = adattributiond.cpp; sourceTree = ""; }; -@@ -8171,6 +8190,19 @@ +@@ -8191,6 +8210,19 @@ DF0C5F24252ECB8D00D921DB /* WKDownload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WKDownload.h; sourceTree = ""; }; DF0C5F25252ECB8E00D921DB /* WKDownloadInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WKDownloadInternal.h; sourceTree = ""; }; DF0C5F26252ECB8E00D921DB /* WKDownloadDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WKDownloadDelegate.h; sourceTree = ""; }; @@ -19064,16 +18991,16 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 DF462E0E23F22F5300EFF35F /* WKHTTPCookieStorePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WKHTTPCookieStorePrivate.h; sourceTree = ""; }; DF462E1123F338AD00EFF35F /* WKContentWorldPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WKContentWorldPrivate.h; sourceTree = ""; }; DF58C6311371AC5800F9A37C /* NativeWebWheelEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NativeWebWheelEvent.h; sourceTree = ""; }; -@@ -8346,6 +8378,8 @@ +@@ -8365,6 +8397,8 @@ E5CBA76127A3187900DF7858 /* UnifiedSource118.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = UnifiedSource118.cpp; sourceTree = ""; }; E5CBA76227A3187900DF7858 /* UnifiedSource117.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = UnifiedSource117.cpp; sourceTree = ""; }; E5CBA76327A3187B00DF7858 /* UnifiedSource116.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = UnifiedSource116.cpp; sourceTree = ""; }; + E5CBA76F27A3187800DF7858 /* UnifiedSource121.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = UnifiedSource121.cpp; sourceTree = ""; }; + E5CBA77F27A3187800DF7858 /* UnifiedSource122.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = UnifiedSource122.cpp; sourceTree = ""; }; E5DEFA6726F8F42600AB68DB /* PhotosUISPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PhotosUISPI.h; sourceTree = ""; }; + E838FCAF2DE90BF800703353 /* ISO18013MobileDocumentRequest+Extras.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ISO18013MobileDocumentRequest+Extras.swift"; sourceTree = ""; }; E88885662DC914C400C572B8 /* WKISO18013Request.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKISO18013Request.h; sourceTree = ""; }; - E890313A2D96411E00AB0B09 /* DigitalCredentialsCoordinator.messages.in */ = {isa = PBXFileReference; lastKnownFileType = text; path = DigitalCredentialsCoordinator.messages.in; sourceTree = ""; }; -@@ -8398,6 +8432,14 @@ +@@ -8418,6 +8452,14 @@ F404455A2D5CFB56000E587E /* AppKitSoftLink.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppKitSoftLink.h; sourceTree = ""; }; F404455B2D5CFB56000E587E /* AppKitSoftLink.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppKitSoftLink.mm; sourceTree = ""; }; F4063DDE2D71481E00F3FE6E /* LLVMProfiling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LLVMProfiling.h; sourceTree = ""; }; @@ -19088,7 +19015,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 F409BA171E6E64B3009DA28E /* WKDragDestinationAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WKDragDestinationAction.h; sourceTree = ""; }; F40C3B6F2AB40167007A3567 /* WKDatePickerPopoverController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = WKDatePickerPopoverController.h; path = ios/forms/WKDatePickerPopoverController.h; sourceTree = ""; }; F40C3B702AB40167007A3567 /* WKDatePickerPopoverController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = WKDatePickerPopoverController.mm; path = ios/forms/WKDatePickerPopoverController.mm; sourceTree = ""; }; -@@ -8825,6 +8867,7 @@ +@@ -8850,6 +8892,7 @@ 3766F9EE189A1241003CF19B /* JavaScriptCore.framework in Frameworks */, 3766F9F1189A1254003CF19B /* libicucore.dylib in Frameworks */, 7B9FC5BB28A5233B007570E7 /* libWebKitPlatform.a in Frameworks */, @@ -19096,7 +19023,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 3766F9EF189A1244003CF19B /* QuartzCore.framework in Frameworks */, 37694525184FC6B600CDE21F /* Security.framework in Frameworks */, 37BEC4DD1948FC6A008B4286 /* WebCore.framework in Frameworks */, -@@ -12013,6 +12056,7 @@ +@@ -12053,6 +12096,7 @@ 99788ACA1F421DCA00C08000 /* _WKAutomationSessionConfiguration.mm */, 990D28A81C6404B000986977 /* _WKAutomationSessionDelegate.h */, 990D28AF1C65203900986977 /* _WKAutomationSessionInternal.h */, @@ -19104,7 +19031,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 5C4609E222430E4C009943C2 /* _WKContentRuleListAction.h */, 5C4609E322430E4D009943C2 /* _WKContentRuleListAction.mm */, 5C4609E422430E4D009943C2 /* _WKContentRuleListActionInternal.h */, -@@ -13392,6 +13436,7 @@ +@@ -13433,6 +13477,7 @@ E34B110C27C46BC6006D2F2E /* libWebCoreTestShim.dylib */, E34B110F27C46D09006D2F2E /* libWebCoreTestSupport.dylib */, DDE992F4278D06D900F60D26 /* libWebKitAdditions.a */, @@ -19112,7 +19039,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 57A9FF15252C6AEF006A2040 /* libWTF.a */, 5750F32A2032D4E500389347 /* LocalAuthentication.framework */, 570DAAB0230273D200E8FC04 /* NearField.framework */, -@@ -13974,6 +14019,12 @@ +@@ -14015,6 +14060,12 @@ children = ( 9197940423DBC4BB00257892 /* InspectorBrowserAgent.cpp */, 9197940323DBC4BB00257892 /* InspectorBrowserAgent.h */, @@ -19125,7 +19052,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 ); path = Agents; sourceTree = ""; -@@ -13982,6 +14033,7 @@ +@@ -14023,6 +14074,7 @@ isa = PBXGroup; children = ( A5D3504D1D78F0D2005124A9 /* RemoteWebInspectorUIProxyMac.mm */, @@ -19133,7 +19060,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 1CA8B935127C774E00576C2B /* WebInspectorUIProxyMac.mm */, 99A7ACE326012919006D57FD /* WKInspectorResourceURLSchemeHandler.h */, 99A7ACE42601291A006D57FD /* WKInspectorResourceURLSchemeHandler.mm */, -@@ -14736,6 +14788,7 @@ +@@ -14778,6 +14830,7 @@ E1513C65166EABB200149FCB /* AuxiliaryProcessProxy.h */, 46A2B6061E5675A200C3DEDA /* BackgroundProcessResponsivenessTimer.cpp */, 46A2B6071E5675A200C3DEDA /* BackgroundProcessResponsivenessTimer.h */, @@ -19141,7 +19068,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 5C6D69352AC3935D0099BDAF /* BrowsingContextGroup.cpp */, 5C6D69362AC3935D0099BDAF /* BrowsingContextGroup.h */, 5CA98549210BEB5A0057EB6B /* BrowsingWarning.h */, -@@ -14760,6 +14813,8 @@ +@@ -14802,6 +14855,8 @@ BC06F43912DBCCFB002D78DE /* GeolocationPermissionRequestProxy.cpp */, BC06F43812DBCCFB002D78DE /* GeolocationPermissionRequestProxy.h */, 2DD5A72A1EBF09A7009BA597 /* HiddenPageThrottlingAutoIncreasesCounter.h */, @@ -19150,7 +19077,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 5CEABA2B2333251400797797 /* LegacyGlobalSettings.cpp */, 5CEABA2A2333247700797797 /* LegacyGlobalSettings.h */, 31607F3819627002009B87DA /* LegacySessionStateCoding.h */, -@@ -14789,6 +14844,7 @@ +@@ -14832,6 +14887,7 @@ 4683569B21E81CC7006E27A3 /* ProvisionalPageProxy.cpp */, 4683569A21E81CC7006E27A3 /* ProvisionalPageProxy.h */, 411B89CB27B2B89600F9EBD3 /* QueryPermissionResultCallback.h */, @@ -19158,7 +19085,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 5CCB54DC2A4FEA6A0005FAA8 /* RemotePageDrawingAreaProxy.cpp */, 5CCB54DB2A4FEA6A0005FAA8 /* RemotePageDrawingAreaProxy.h */, FABBBC802D35AC6800820017 /* RemotePageFullscreenManagerProxy.cpp */, -@@ -14892,6 +14948,8 @@ +@@ -14939,6 +14995,8 @@ BC7B6204129A0A6700D174A4 /* WebPageGroup.h */, 2D9EA3101A96D9EB002D2807 /* WebPageInjectedBundleClient.cpp */, 2D9EA30E1A96CBFF002D2807 /* WebPageInjectedBundleClient.h */, @@ -19167,7 +19094,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 9B7F8A502C785725000057F3 /* WebPageLoadTiming.h */, BC111B0B112F5E4F00337BAB /* WebPageProxy.cpp */, BC032DCB10F4389F0058C15A /* WebPageProxy.h */, -@@ -15070,6 +15128,7 @@ +@@ -15118,6 +15176,7 @@ BC646C1911DD399F006455B0 /* WKBackForwardListItemRef.h */, BC646C1611DD399F006455B0 /* WKBackForwardListRef.cpp */, BC646C1711DD399F006455B0 /* WKBackForwardListRef.h */, @@ -19175,7 +19102,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 BCB9E24A1120E15C00A137E0 /* WKContext.cpp */, BCB9E2491120E15C00A137E0 /* WKContext.h */, 1AE52F9319201F6B00A1FA37 /* WKContextConfigurationRef.cpp */, -@@ -15646,6 +15705,9 @@ +@@ -15694,6 +15753,9 @@ 07EF07592745A8160066EA04 /* DisplayCaptureSessionManager.h */, 07EF07582745A8160066EA04 /* DisplayCaptureSessionManager.mm */, 7AFA6F682A9F57C50055322A /* DisplayLinkMac.cpp */, @@ -19185,7 +19112,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 1AFDE65B1954E8D500C48FFA /* LegacySessionStateCoding.cpp */, 0FCB4E5818BBE3D9000FCFC9 /* PageClientImplMac.h */, 0FCB4E5918BBE3D9000FCFC9 /* PageClientImplMac.mm */, -@@ -15669,6 +15731,8 @@ +@@ -15717,6 +15779,8 @@ E568B92120A3AC6A00E3C856 /* WebDataListSuggestionsDropdownMac.mm */, E55CD20124D09F1F0042DB9C /* WebDateTimePickerMac.h */, E55CD20224D09F1F0042DB9C /* WebDateTimePickerMac.mm */, @@ -19194,7 +19121,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 BC857E8512B71EBB00EDEB2E /* WebPageProxyMac.mm */, BC5750951268F3C6006F0F12 /* WebPopupMenuProxyMac.h */, BC5750961268F3C6006F0F12 /* WebPopupMenuProxyMac.mm */, -@@ -16775,6 +16839,7 @@ +@@ -16826,6 +16890,7 @@ 99788ACB1F421DDA00C08000 /* _WKAutomationSessionConfiguration.h in Headers */, 990D28AC1C6420CF00986977 /* _WKAutomationSessionDelegate.h in Headers */, 990D28B11C65208D00986977 /* _WKAutomationSessionInternal.h in Headers */, @@ -19202,7 +19129,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 5C4609E7224317B4009943C2 /* _WKContentRuleListAction.h in Headers */, 5C4609E8224317BB009943C2 /* _WKContentRuleListActionInternal.h in Headers */, 9B4CE9512CD99B7C00351173 /* _WKContentWorldConfiguration.h in Headers */, -@@ -17087,6 +17152,7 @@ +@@ -17139,6 +17204,7 @@ E170876C16D6CA6900F99226 /* BlobRegistryProxy.h in Headers */, 4F601432155C5AA2001FBDE0 /* BlockingResponseMap.h in Headers */, 1A5705111BE410E600874AF1 /* BlockSPI.h in Headers */, @@ -19210,7 +19137,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 5CA9854A210BEB640057EB6B /* BrowsingWarning.h in Headers */, A7E69BCC2B2117A100D43D3F /* BufferAndBackendInfo.h in Headers */, BC3065FA1259344E00E71278 /* CacheModel.h in Headers */, -@@ -17270,7 +17336,11 @@ +@@ -17321,7 +17387,11 @@ BC14DF77120B5B7900826C0C /* InjectedBundleScriptWorld.h in Headers */, CE550E152283752200D28791 /* InsertTextOptions.h in Headers */, 9197940523DBC4BB00257892 /* InspectorBrowserAgent.h in Headers */, @@ -19222,7 +19149,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 A5E391FD2183C1F800C8FB31 /* InspectorTargetProxy.h in Headers */, C5BCE5DF1C50766A00CDE3FA /* InteractionInformationAtPosition.h in Headers */, 2D4D2C811DF60BF3002EB10C /* InteractionInformationRequest.h in Headers */, -@@ -17531,6 +17601,7 @@ +@@ -17581,6 +17651,7 @@ 0F6E7C532C4C386800F1DB85 /* RemoteDisplayListRecorderMessages.h in Headers */, F451C0FE2703B263002BA03B /* RemoteDisplayListRecorderProxy.h in Headers */, A78A5FE42B0EB39E005036D3 /* RemoteImageBufferSetIdentifier.h in Headers */, @@ -19230,15 +19157,15 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 2D47B56D1810714E003A3AEE /* RemoteLayerBackingStore.h in Headers */, 2DDF731518E95060004F5A66 /* RemoteLayerBackingStoreCollection.h in Headers */, 1AB16AEA164B3A8800290D62 /* RemoteLayerTreeContext.h in Headers */, -@@ -17588,6 +17659,7 @@ +@@ -17638,6 +17709,7 @@ E1E552C516AE065F004ED653 /* SandboxInitializationParameters.h in Headers */, E36FF00327F36FBD004BE21A /* SandboxStateVariables.h in Headers */, 7BAB111025DD02B3008FC479 /* ScopedActiveMessageReceiveQueue.h in Headers */, + F303B849249A8D640031DE5C /* ScreencastEncoder.h in Headers */, 6D4DF20C2D824242001F964C /* ScreenTimeWebsiteDataSupport.h in Headers */, 463BB93A2B9D08D80098C5C3 /* ScriptMessageHandlerIdentifier.h in Headers */, - F4E28A362C923814008120DD /* ScriptTelemetry.h in Headers */, -@@ -17950,6 +18022,8 @@ + F4E28A362C923814008120DD /* ScriptTrackingPrivacyFilter.h in Headers */, +@@ -18002,6 +18074,8 @@ 939EF87029D112EE00F23AEE /* WebPageInlines.h in Headers */, 9197940823DBC4CB00257892 /* WebPageInspectorAgentBase.h in Headers */, A513F5402154A5D700662841 /* WebPageInspectorController.h in Headers */, @@ -19247,7 +19174,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 A543E30C215C8A8D00279CD9 /* WebPageInspectorTarget.h in Headers */, A543E30D215C8A9000279CD9 /* WebPageInspectorTargetController.h in Headers */, A543E307215AD13700279CD9 /* WebPageInspectorTargetFrontendChannel.h in Headers */, -@@ -20613,7 +20687,43 @@ +@@ -20621,7 +20695,43 @@ 522F792928D50EBB0069B45B /* HidService.mm in Sources */, 2749F6442146561B008380BF /* InjectedBundleNodeHandle.cpp in Sources */, 2749F6452146561E008380BF /* InjectedBundleRangeHandle.cpp in Sources */, @@ -19291,9 +19218,9 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 1C5DC45F2909B05A0061EC62 /* JSWebExtensionWrapperCocoa.mm in Sources */, C14D37FE24ACE086007FF014 /* LaunchServicesDatabaseManager.mm in Sources */, C1710CF724AA643200D7C112 /* LaunchServicesDatabaseObserver.mm in Sources */, -@@ -21013,6 +21123,8 @@ - 074E87E12CF8EA3D0059E469 /* WebPage+NavigationDeciding.swift in Sources */, +@@ -21023,6 +21133,8 @@ 078B04A02CF18EAB00B453A6 /* WebPage+NavigationPreferences.swift in Sources */, + 071467782DFE84E500F77867 /* WebPage+Transferable.swift in Sources */, 07CB79962CE9435700199C49 /* WebPage.swift in Sources */, + D79902B1236E9404005D6F7E /* WebPageInspectorEmulationAgentMac.mm in Sources */, + D79902B3236E9404005D6F7E /* WebPageInspectorInputAgentMac.mm in Sources */, @@ -19301,7 +19228,7 @@ index 7504d2cdd6d31bdfb1892dab8e1716253ce54ccc..c1ecef732f88cbf5c6277a0aa5c0d488 079A4DA12D72CC0D00CA387F /* WebPageWebView.swift in Sources */, CA2506A82DD65327001D1954 /* WebPageWebViewAdditions.swift in Sources */, diff --git a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp -index bd3aaaf9309aec01820d0950d3b6f14a23c6076c..8cdb9a0c339c0499034fa801395367dd62346268 100644 +index 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548ee42dec6c 100644 --- a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp +++ b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp @@ -271,6 +271,11 @@ void WebLoaderStrategy::scheduleLoad(ResourceLoader& resourceLoader, CachedResou @@ -19394,7 +19321,7 @@ index bd3aaaf9309aec01820d0950d3b6f14a23c6076c..8cdb9a0c339c0499034fa801395367dd loadParameters.isMainFrameNavigation = isMainFrameNavigation; if (loadParameters.isMainFrameNavigation && document) -@@ -585,6 +583,25 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -589,6 +587,25 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL if (RefPtr frameLoader = resourceLoader.frameLoader()) loadParameters.requiredCookiesVersion = frameLoader->requiredCookiesVersion(); @@ -19420,7 +19347,7 @@ index bd3aaaf9309aec01820d0950d3b6f14a23c6076c..8cdb9a0c339c0499034fa801395367dd std::optional existingNetworkResourceLoadIdentifierToResume; if (loadParameters.isMainFrameNavigation) existingNetworkResourceLoadIdentifierToResume = std::exchange(m_existingNetworkResourceLoadIdentifierToResume, std::nullopt); -@@ -599,7 +616,7 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -603,7 +620,7 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL } auto loader = WebResourceLoader::create(resourceLoader, trackingParameters); @@ -19429,7 +19356,7 @@ index bd3aaaf9309aec01820d0950d3b6f14a23c6076c..8cdb9a0c339c0499034fa801395367dd } void WebLoaderStrategy::scheduleInternallyFailedLoad(WebCore::ResourceLoader& resourceLoader) -@@ -1017,7 +1034,7 @@ void WebLoaderStrategy::didFinishPreconnection(WebCore::ResourceLoaderIdentifier +@@ -1021,7 +1038,7 @@ void WebLoaderStrategy::didFinishPreconnection(WebCore::ResourceLoaderIdentifier bool WebLoaderStrategy::isOnLine() const { @@ -19438,7 +19365,7 @@ index bd3aaaf9309aec01820d0950d3b6f14a23c6076c..8cdb9a0c339c0499034fa801395367dd } void WebLoaderStrategy::addOnlineStateChangeListener(Function&& listener) -@@ -1044,6 +1061,11 @@ void WebLoaderStrategy::isResourceLoadFinished(CachedResource& resource, Complet +@@ -1047,6 +1064,11 @@ void WebLoaderStrategy::isResourceLoadFinished(CachedResource& resource, Complet void WebLoaderStrategy::setOnLineState(bool isOnLine) { @@ -19450,7 +19377,7 @@ index bd3aaaf9309aec01820d0950d3b6f14a23c6076c..8cdb9a0c339c0499034fa801395367dd if (m_isOnLine == isOnLine) return; -@@ -1052,6 +1074,12 @@ void WebLoaderStrategy::setOnLineState(bool isOnLine) +@@ -1055,6 +1077,12 @@ void WebLoaderStrategy::setOnLineState(bool isOnLine) listener(isOnLine); } @@ -19464,7 +19391,7 @@ index bd3aaaf9309aec01820d0950d3b6f14a23c6076c..8cdb9a0c339c0499034fa801395367dd { WebProcess::singleton().ensureNetworkProcessConnection().connection().send(Messages::NetworkConnectionToWebProcess::SetCaptureExtraNetworkLoadMetricsEnabled(enabled), 0); diff --git a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.h b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.h -index 6492031de6ed6effab3f28e5321419f3390f7651..678139fc26f26e2224a70b0087ddb849280b8ace 100644 +index af1f8eb6b578367e8cb818b1c6020377c5a07a37..41f1a38348b11aa0d99fc9fa429fe6b88b41dbab 100644 --- a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.h +++ b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.h @@ -26,6 +26,7 @@ @@ -19494,7 +19421,7 @@ index 6492031de6ed6effab3f28e5321419f3390f7651..678139fc26f26e2224a70b0087ddb849 } // namespace WebKit diff --git a/Source/WebKit/WebProcess/Network/WebResourceLoader.cpp b/Source/WebKit/WebProcess/Network/WebResourceLoader.cpp -index ce50de02306dfe542299fbb9e534a6db929b8052..5c0a3215a78f3fdcf19fd8bb56c2a9c5596240ed 100644 +index 6d448ac1d302be3e9ad938ade807d26ddd39d103..06d1995872db8de9edc0ad14f0ab556dec75eb7a 100644 --- a/Source/WebKit/WebProcess/Network/WebResourceLoader.cpp +++ b/Source/WebKit/WebProcess/Network/WebResourceLoader.cpp @@ -202,9 +202,6 @@ void WebResourceLoader::didReceiveResponse(ResourceResponse&& response, PrivateR @@ -19517,10 +19444,10 @@ index ce50de02306dfe542299fbb9e534a6db929b8052..5c0a3215a78f3fdcf19fd8bb56c2a9c5 } diff --git a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp -index 78e75b9ebfa13611e7f2f9df4395a586ed9db55e..ad6db691640a56bba7fb153bd4a71eb6b618bb50 100644 +index de8c8f2890b146c6dd4f34c121b92402900e8df5..11792945a16cca303a9b2d3c101edf835f60b398 100644 --- a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp +++ b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp -@@ -532,6 +532,9 @@ void WebChromeClient::addMessageToConsole(MessageSource source, MessageLevel lev +@@ -534,6 +534,9 @@ void WebChromeClient::addMessageToConsole(MessageSource source, MessageLevel lev if (!page) return; @@ -19531,11 +19458,11 @@ index 78e75b9ebfa13611e7f2f9df4395a586ed9db55e..ad6db691640a56bba7fb153bd4a71eb6 page->injectedBundleUIClient().willAddMessageToConsole(page.get(), source, level, message, lineNumber, columnNumber, sourceID); diff --git a/Source/WebKit/WebProcess/WebCoreSupport/WebDragClient.cpp b/Source/WebKit/WebProcess/WebCoreSupport/WebDragClient.cpp -index 2f5b6d189eae2e153847cbf8496805a2660d1105..837f71ee8d9e0b0fb8ae1841f6820431f592dfd8 100644 +index dca2495f0196552e79c73bb0b7466b5c4fa0be46..999a7ab4eef2b8cc1a65b5a26cd69e369dcb7ab7 100644 --- a/Source/WebKit/WebProcess/WebCoreSupport/WebDragClient.cpp +++ b/Source/WebKit/WebProcess/WebCoreSupport/WebDragClient.cpp @@ -53,7 +53,7 @@ OptionSet WebDragClient::dragSourceActionMaskForPoint(const In - return m_page->allowedDragSourceActions(); + return protectedPage()->allowedDragSourceActions(); } -#if !PLATFORM(COCOA) && !PLATFORM(GTK) @@ -19543,8 +19470,20 @@ index 2f5b6d189eae2e153847cbf8496805a2660d1105..837f71ee8d9e0b0fb8ae1841f6820431 void WebDragClient::startDrag(DragItem, DataTransfer&, Frame&, const std::optional&) { } +diff --git a/Source/WebKit/WebProcess/WebCoreSupport/WebUserMediaClient.cpp b/Source/WebKit/WebProcess/WebCoreSupport/WebUserMediaClient.cpp +index bb924c778ae3999d549b14e6d810b32167baece4..341355502afeaf69827308fee41ce964c0c66541 100644 +--- a/Source/WebKit/WebProcess/WebCoreSupport/WebUserMediaClient.cpp ++++ b/Source/WebKit/WebProcess/WebCoreSupport/WebUserMediaClient.cpp +@@ -26,6 +26,7 @@ + #include "UserMediaPermissionRequestManager.h" + #include "WebPage.h" + #include "WebPageProxyMessages.h" ++#include + #include + #include + #include diff --git a/Source/WebKit/WebProcess/WebCoreSupport/mac/WebDragClientMac.mm b/Source/WebKit/WebProcess/WebCoreSupport/mac/WebDragClientMac.mm -index cf8aa32077a57057649722d73358ac91b7130551..b555eb998bab677a452c37e78935ea9bad2196fc 100644 +index 0abeb42354ed65e20d5ad5ab72c31bf00e49d528..259caa5c016b7f5c7eab156bbe4f0ce105339f76 100644 --- a/Source/WebKit/WebProcess/WebCoreSupport/mac/WebDragClientMac.mm +++ b/Source/WebKit/WebProcess/WebCoreSupport/mac/WebDragClientMac.mm @@ -128,7 +128,8 @@ static WebCore::CachedImage* cachedImage(Element& element) @@ -19699,7 +19638,7 @@ index 5fed15fd5dbbfbaff305ce26a8bcf5ba3d0435d9..fec04b9a4d640dcf47e0a2312d8b7123 #include #include diff --git a/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/LayerTreeHost.h b/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/LayerTreeHost.h -index 31af5390779e2ec5f3bc3dc0bbddf56f2617657a..df7af98f4d38f380ee5c0b607570659260a955e0 100644 +index 5c8694cb4229ee87c50f68f8c24b47e5598a5aee..581637fb988e0d9b465a7029fb1bac9884c09b6c 100644 --- a/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/LayerTreeHost.h +++ b/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/LayerTreeHost.h @@ -145,6 +145,7 @@ public: @@ -19711,7 +19650,7 @@ index 31af5390779e2ec5f3bc3dc0bbddf56f2617657a..df7af98f4d38f380ee5c0b6075706592 void updateRootLayer(); WebCore::FloatRect visibleContentsRect() const; diff --git a/Source/WebKit/WebProcess/WebPage/DrawingArea.cpp b/Source/WebKit/WebProcess/WebPage/DrawingArea.cpp -index c07ca9e847d03c27877cf650b67b96b3c3f7bddb..b966edc8778d4a68c6e5bae0c0883ab3bfd0a387 100644 +index e4b6fc981ce302979fd3280e9602bcbea91b9d1f..9ed589d3f29b17c18018798642f34fa375afc1a2 100644 --- a/Source/WebKit/WebProcess/WebPage/DrawingArea.cpp +++ b/Source/WebKit/WebProcess/WebPage/DrawingArea.cpp @@ -27,6 +27,7 @@ @@ -19723,7 +19662,7 @@ index c07ca9e847d03c27877cf650b67b96b3c3f7bddb..b966edc8778d4a68c6e5bae0c0883ab3 #include "WebPage.h" #include "WebPageCreationParameters.h" diff --git a/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp b/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp -index 412dd3c46cb61ec5f3de076de61c34b6a32c6281..77c02eedfb60a7f816788452e20aaf3fbc434164 100644 +index c42601a113c5f4f390630b3f20328de17f4f7d31..1a8f3dcccae5c09220f0f519d275eeaba83b1a5e 100644 --- a/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp +++ b/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp @@ -44,6 +44,7 @@ @@ -19731,7 +19670,7 @@ index 412dd3c46cb61ec5f3de076de61c34b6a32c6281..77c02eedfb60a7f816788452e20aaf3f #include #include +#include - #include + #include #include #include @@ -447,6 +448,12 @@ void WebCookieJar::setOptInCookiePartitioningEnabled(bool enabled) @@ -19761,7 +19700,7 @@ index d5bac4fe6d5103b4e752a7219d7870d4cddcaf27..033effbb445f5da6db5798594e2dbef3 void setOptInCookiePartitioningEnabled(bool); #endif diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp -index eebb80386522babc1ae192dbc470facea0b76e6b..02da88a1fd0d655faf69c7d3bd348009da32851c 100644 +index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98af4924ff6 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp +++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp @@ -244,6 +244,7 @@ @@ -19808,7 +19747,7 @@ index eebb80386522babc1ae192dbc470facea0b76e6b..02da88a1fd0d655faf69c7d3bd348009 void WebPage::loadRequest(LoadParameters&& loadParameters) { WEBPAGE_RELEASE_LOG_FORWARDABLE(Loading, WEBPAGE_LOADREQUEST, loadParameters.navigationID ? loadParameters.navigationID->toUInt64() : 0, static_cast(loadParameters.shouldTreatAsContinuingLoad), loadParameters.request.isAppInitiated(), loadParameters.existingNetworkResourceLoadIdentifierToResume ? loadParameters.existingNetworkResourceLoadIdentifierToResume->toUInt64() : 0); -@@ -2295,7 +2318,9 @@ void WebPage::stopLoading() +@@ -2298,7 +2321,9 @@ void WebPage::stopLoading() void WebPage::stopLoadingDueToProcessSwap() { SetForScope isStoppingLoadingDueToProcessSwap(m_isStoppingLoadingDueToProcessSwap, true); @@ -19818,7 +19757,7 @@ index eebb80386522babc1ae192dbc470facea0b76e6b..02da88a1fd0d655faf69c7d3bd348009 } bool WebPage::defersLoading() const -@@ -2871,7 +2896,7 @@ void WebPage::viewportPropertiesDidChange(const ViewportArguments& viewportArgum +@@ -2874,7 +2899,7 @@ void WebPage::viewportPropertiesDidChange(const ViewportArguments& viewportArgum #if PLATFORM(IOS_FAMILY) if (m_viewportConfiguration.setViewportArguments(viewportArguments)) viewportConfigurationChanged(); @@ -19827,7 +19766,7 @@ index eebb80386522babc1ae192dbc470facea0b76e6b..02da88a1fd0d655faf69c7d3bd348009 // Adjust view dimensions when using fixed layout. RefPtr localMainFrame = this->localMainFrame(); RefPtr view = localMainFrame ? localMainFrame->view() : nullptr; -@@ -3628,6 +3653,13 @@ void WebPage::flushDeferredScrollEvents() +@@ -3631,6 +3656,13 @@ void WebPage::flushDeferredScrollEvents() protectedCorePage()->flushDeferredScrollEvents(); } @@ -19841,7 +19780,7 @@ index eebb80386522babc1ae192dbc470facea0b76e6b..02da88a1fd0d655faf69c7d3bd348009 void WebPage::flushDeferredDidReceiveMouseEvent() { if (auto info = std::exchange(m_deferredDidReceiveMouseEvent, std::nullopt)) -@@ -3902,6 +3934,97 @@ void WebPage::touchEvent(const WebTouchEvent& touchEvent, CompletionHandlersendMessageToTargetBackend(targetId, message); } @@ -19955,8 +19894,8 @@ index eebb80386522babc1ae192dbc470facea0b76e6b..02da88a1fd0d655faf69c7d3bd348009 + void WebPage::insertNewlineInQuotedContent() { - RefPtr frame = protectedCorePage()->checkedFocusController()->focusedOrMainFrame(); -@@ -4233,6 +4366,7 @@ void WebPage::setMainFrameDocumentVisualUpdatesAllowed(bool allowed) + RefPtr frame = corePage()->focusController().focusedOrMainFrame(); +@@ -4236,6 +4369,7 @@ void WebPage::setMainFrameDocumentVisualUpdatesAllowed(bool allowed) void WebPage::show() { send(Messages::WebPageProxy::ShowPage()); @@ -19964,7 +19903,7 @@ index eebb80386522babc1ae192dbc470facea0b76e6b..02da88a1fd0d655faf69c7d3bd348009 } void WebPage::setIsTakingSnapshotsForApplicationSuspension(bool isTakingSnapshotsForApplicationSuspension) -@@ -5448,7 +5582,7 @@ RefPtr WebPage::protectedNotificationPermi +@@ -5456,7 +5590,7 @@ RefPtr WebPage::protectedNotificationPermi #if ENABLE(DRAG_SUPPORT) @@ -19973,7 +19912,7 @@ index eebb80386522babc1ae192dbc470facea0b76e6b..02da88a1fd0d655faf69c7d3bd348009 void WebPage::performDragControllerAction(DragControllerAction action, const IntPoint& clientPosition, const IntPoint& globalPosition, OptionSet draggingSourceOperationMask, SelectionData&& selectionData, OptionSet flags, CompletionHandler, DragHandlingMethod, bool, unsigned, IntRect, IntRect, std::optional)>&& completionHandler) { if (!m_page) -@@ -7961,6 +8095,10 @@ void WebPage::didCommitLoad(WebFrame* frame) +@@ -7952,6 +8086,10 @@ void WebPage::didCommitLoad(WebFrame* frame) m_needsFixedContainerEdgesUpdate = true; flushDeferredDidReceiveMouseEvent(); @@ -19984,7 +19923,7 @@ index eebb80386522babc1ae192dbc470facea0b76e6b..02da88a1fd0d655faf69c7d3bd348009 } void WebPage::didFinishDocumentLoad(WebFrame& frame) -@@ -8270,6 +8408,9 @@ Ref WebPage::createDocumentLoader(LocalFrame& frame, ResourceReq +@@ -8261,6 +8399,9 @@ Ref WebPage::createDocumentLoader(LocalFrame& frame, ResourceReq m_allowsContentJavaScriptFromMostRecentNavigation = m_internals->pendingWebsitePolicies->allowsContentJavaScript; WebsitePoliciesData::applyToDocumentLoader(*std::exchange(m_internals->pendingWebsitePolicies, std::nullopt), documentLoader); } @@ -19995,10 +19934,10 @@ index eebb80386522babc1ae192dbc470facea0b76e6b..02da88a1fd0d655faf69c7d3bd348009 return documentLoader; diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.h b/Source/WebKit/WebProcess/WebPage/WebPage.h -index 360c7068495346fa326a3409afe64c03ff11506e..263acb1412dd593a45a264c7fbd3fc16582db710 100644 +index 47921d36d270fc271bf966dfe183129993ac7336..91da48500f900ddb3b0eb4e99dbd43ef92230dcf 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.h +++ b/Source/WebKit/WebProcess/WebPage/WebPage.h -@@ -47,6 +47,7 @@ +@@ -46,6 +46,7 @@ #include #include #include @@ -20052,7 +19991,7 @@ index 360c7068495346fa326a3409afe64c03ff11506e..263acb1412dd593a45a264c7fbd3fc16 void insertNewlineInQuotedContent(); -@@ -1969,6 +1978,7 @@ public: +@@ -1972,6 +1981,7 @@ public: void showContextMenuFromFrame(const FrameInfoData&, const ContextMenuContextData&, const UserData&); #endif void loadRequest(LoadParameters&&); @@ -20060,7 +19999,7 @@ index 360c7068495346fa326a3409afe64c03ff11506e..263acb1412dd593a45a264c7fbd3fc16 void setObscuredContentInsets(const WebCore::FloatBoxExtent&); -@@ -2171,6 +2181,7 @@ private: +@@ -2176,6 +2186,7 @@ private: void updatePotentialTapSecurityOrigin(const WebTouchEvent&, bool wasHandled); #elif ENABLE(TOUCH_EVENTS) void touchEvent(const WebTouchEvent&, CompletionHandler, bool)>&&); @@ -20068,7 +20007,7 @@ index 360c7068495346fa326a3409afe64c03ff11506e..263acb1412dd593a45a264c7fbd3fc16 #endif void cancelPointer(WebCore::PointerID, const WebCore::IntPoint&); -@@ -2941,6 +2952,7 @@ private: +@@ -2947,6 +2958,7 @@ private: bool m_isAppNapEnabled { true }; Markable m_pendingNavigationID; @@ -20077,10 +20016,10 @@ index 360c7068495346fa326a3409afe64c03ff11506e..263acb1412dd593a45a264c7fbd3fc16 bool m_mainFrameProgressCompleted { false }; bool m_shouldDispatchFakeMouseMoveEvents { true }; diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.messages.in b/Source/WebKit/WebProcess/WebPage/WebPage.messages.in -index c4ae18f9803e00d09a3f479c03ec858dd98c9045..9b1cbcf627f104e97a7ca46b560cdb9d99dc0ffc 100644 +index a2bdf0b62ddcb75a5c834b8aed481e1b14d5737a..e09d2ae7bbe7a1f0b34b84d6a97d2593e89b39f4 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.messages.in +++ b/Source/WebKit/WebProcess/WebPage/WebPage.messages.in -@@ -58,10 +58,13 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -60,10 +60,13 @@ messages -> WebPage WantsAsyncDispatchMessage { MouseEvent(WebCore::FrameIdentifier frameID, WebKit::WebMouseEvent event, std::optional> sandboxExtensions) SetLastKnownMousePosition(WebCore::FrameIdentifier frameID, WebCore::IntPoint eventPoint, WebCore::IntPoint globalPoint); @@ -20095,7 +20034,7 @@ index c4ae18f9803e00d09a3f479c03ec858dd98c9045..9b1cbcf627f104e97a7ca46b560cdb9d SetOverrideViewportArguments(struct std::optional arguments) DynamicViewportSizeUpdate(struct WebKit::DynamicViewportSizeUpdate target) -@@ -153,6 +156,7 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -155,6 +158,7 @@ messages -> WebPage WantsAsyncDispatchMessage { ConnectInspector(String targetId, Inspector::FrontendChannel::ConnectionType connectionType) DisconnectInspector(String targetId) SendMessageToTargetBackend(String targetId, String message) @@ -20103,7 +20042,7 @@ index c4ae18f9803e00d09a3f479c03ec858dd98c9045..9b1cbcf627f104e97a7ca46b560cdb9d #if ENABLE(REMOTE_INSPECTOR) SetIndicating(bool indicating); -@@ -163,6 +167,7 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -165,6 +169,7 @@ messages -> WebPage WantsAsyncDispatchMessage { #endif #if !ENABLE(IOS_TOUCH_EVENTS) && ENABLE(TOUCH_EVENTS) TouchEvent(WebKit::WebTouchEvent event) -> (enum:uint8_t std::optional eventType, bool handled) @@ -20111,7 +20050,7 @@ index c4ae18f9803e00d09a3f479c03ec858dd98c9045..9b1cbcf627f104e97a7ca46b560cdb9d #endif CancelPointer(WebCore::PointerID pointerId, WebCore::IntPoint documentPoint) -@@ -188,6 +193,7 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -190,6 +195,7 @@ messages -> WebPage WantsAsyncDispatchMessage { LoadDataInFrame(std::span data, String MIMEType, String encodingName, URL baseURL, WebCore::FrameIdentifier frameID) LoadRequest(struct WebKit::LoadParameters loadParameters) LoadDidCommitInAnotherProcess(WebCore::FrameIdentifier frameID, std::optional layerHostingContextIdentifier) @@ -20119,7 +20058,7 @@ index c4ae18f9803e00d09a3f479c03ec858dd98c9045..9b1cbcf627f104e97a7ca46b560cdb9d LoadRequestWaitingForProcessLaunch(struct WebKit::LoadParameters loadParameters, URL resourceDirectoryURL, WebKit::WebPageProxyIdentifier pageID, bool checkAssumedReadAccessToResourceURL) LoadData(struct WebKit::LoadParameters loadParameters) LoadSimulatedRequestAndResponse(struct WebKit::LoadParameters loadParameters, WebCore::ResourceResponse simulatedResponse) -@@ -352,10 +358,10 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -354,10 +360,10 @@ messages -> WebPage WantsAsyncDispatchMessage { RemoveLayerForFindOverlay() -> () # Drag and drop. @@ -20132,7 +20071,7 @@ index c4ae18f9803e00d09a3f479c03ec858dd98c9045..9b1cbcf627f104e97a7ca46b560cdb9d PerformDragControllerAction(std::optional frameID, enum:uint8_t WebKit::DragControllerAction action, WebCore::DragData dragData) -> (enum:uint8_t std::optional dragOperation, enum:uint8_t WebCore::DragHandlingMethod dragHandlingMethod, bool mouseIsOverFileInput, unsigned numberOfItemsToBeAccepted, WebCore::IntRect insertionRect, WebCore::IntRect editableElementRect, struct std::optional remoteUserInputEventData) PerformDragOperation(WebCore::DragData dragData, WebKit::SandboxExtensionHandle sandboxExtensionHandle, Vector sandboxExtensionsForUpload) -> (bool handled) #endif -@@ -375,6 +381,10 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -377,6 +383,10 @@ messages -> WebPage WantsAsyncDispatchMessage { ModelDragEnded(WebCore::ElementIdentifier elementID) #endif @@ -20182,10 +20121,10 @@ index 40ec42bb4f998774a2ce4a19e82f68512ad2ebb8..080794e14bfbb3a336d8a89791baee0e const auto& availableInputs = WebProcess::singleton().availableInputDevices(); if (availableInputs.contains(AvailableInputDevices::Mouse)) diff --git a/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm b/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm -index e9dc2250f0f5e68db6ae61f5f734e2e2cf24cf9b..3dc31fc41d8efd990f94a713c0490abf1f1d9f60 100644 +index e36cd746c4a8d35d076b68fd76ccd211a79d63a6..156cacab648a4ad53b854bf880c924bbe13ca4b6 100644 --- a/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm +++ b/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm -@@ -705,21 +705,37 @@ String WebPage::platformUserAgent(const URL&) const +@@ -701,21 +701,37 @@ String WebPage::platformUserAgent(const URL&) const bool WebPage::hoverSupportedByPrimaryPointingDevice() const { @@ -20274,7 +20213,7 @@ index ea3a03b5ee6d4ecadd771314c6059268db917087..91be6f4c687157afcfdaa431d7a1a6ff } diff --git a/Source/WebKit/WebProcess/WebProcess.cpp b/Source/WebKit/WebProcess/WebProcess.cpp -index 230b9bb1f8b1f218f94c10f18f4d672286ce5a9d..c9741c132685eb27737d21c7c840edb9447ec596 100644 +index fe47c90bccb9a567803485e7313e093b48ad4d4c..ec76a5edc15a914d1f00955493625f6c135d75ce 100644 --- a/Source/WebKit/WebProcess/WebProcess.cpp +++ b/Source/WebKit/WebProcess/WebProcess.cpp @@ -93,6 +93,7 @@ @@ -20309,7 +20248,7 @@ index 230b9bb1f8b1f218f94c10f18f4d672286ce5a9d..c9741c132685eb27737d21c7c840edb9 } void WebProcess::initializeConnection(IPC::Connection* connection) -@@ -975,6 +986,8 @@ void WebProcess::createWebPage(PageIdentifier pageID, WebPageCreationParameters& +@@ -979,6 +990,8 @@ void WebProcess::createWebPage(PageIdentifier pageID, WebPageCreationParameters& accessibilityRelayProcessSuspended(false); } ASSERT(result.iterator->value); @@ -20319,10 +20258,10 @@ index 230b9bb1f8b1f218f94c10f18f4d672286ce5a9d..c9741c132685eb27737d21c7c840edb9 void WebProcess::removeWebPage(PageIdentifier pageID) diff --git a/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm b/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm -index 9dd01c8ef00f00d49d54a16996142681d1486852..51a02893b48bc930ebca7dd4b76dc874beecb157 100644 +index 01f0546169ab61e79b730ac1fe4bded1b80ade9d..f274262554df2cc924e85b79467e16110e12af43 100644 --- a/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm +++ b/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm -@@ -4225,7 +4225,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END +@@ -4219,7 +4219,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END _private->handlingMouseDownEvent = NO; } @@ -20332,10 +20271,10 @@ index 9dd01c8ef00f00d49d54a16996142681d1486852..51a02893b48bc930ebca7dd4b76dc874 - (void)touch:(WebEvent *)event { diff --git a/Source/WebKitLegacy/mac/WebView/WebView.mm b/Source/WebKitLegacy/mac/WebView/WebView.mm -index 222de8db3d8475b7a68b017dc88994d4ca7e08fd..ed3460c7b59772410af898d4fb107cc88c3b40a6 100644 +index 30baa8fa7a4f030680c683eebb7e93f9723da373..f236d24511f25dcbcf84af26fe29d8eb3742512d 100644 --- a/Source/WebKitLegacy/mac/WebView/WebView.mm +++ b/Source/WebKitLegacy/mac/WebView/WebView.mm -@@ -4002,7 +4002,7 @@ + (void)_doNotStartObservingNetworkReachability +@@ -3972,7 +3972,7 @@ + (void)_doNotStartObservingNetworkReachability } #endif // PLATFORM(IOS_FAMILY) @@ -20344,7 +20283,7 @@ index 222de8db3d8475b7a68b017dc88994d4ca7e08fd..ed3460c7b59772410af898d4fb107cc8 - (NSArray *)_touchEventRegions { -@@ -4044,7 +4044,7 @@ - (NSArray *)_touchEventRegions +@@ -4014,7 +4014,7 @@ - (NSArray *)_touchEventRegions }).autorelease(); } @@ -20387,7 +20326,7 @@ index 0000000000000000000000000000000000000000..a9db9ec38d05e36517414248237e885b + LIBVPX_LIBRARIES +) diff --git a/Source/cmake/OptionsGTK.cmake b/Source/cmake/OptionsGTK.cmake -index 303550b9618e8c621e246e8c95b9e7542585f1fc..efdb66ef47e46e83287350933047295e6dc0c268 100644 +index 7b9783fe14ac1eca2c6a48e42b74e6f294359b6d..584c9f2ea30a8f670b18ef271ca2f0681c637fdc 100644 --- a/Source/cmake/OptionsGTK.cmake +++ b/Source/cmake/OptionsGTK.cmake @@ -9,6 +9,10 @@ set(USER_AGENT_BRANDING "" CACHE STRING "Branding to add to user agent string") @@ -20423,7 +20362,7 @@ index 303550b9618e8c621e246e8c95b9e7542585f1fc..efdb66ef47e46e83287350933047295e SET_AND_EXPOSE_TO_BUILD(ENABLE_DEVELOPER_MODE ${DEVELOPER_MODE}) if (DEVELOPER_MODE) WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_API_TESTS PRIVATE ON) -@@ -147,6 +159,20 @@ endif () +@@ -148,6 +160,21 @@ endif () WEBKIT_OPTION_DEPEND(ENABLE_GPU_PROCESS USE_GBM) @@ -20432,6 +20371,7 @@ index 303550b9618e8c621e246e8c95b9e7542585f1fc..efdb66ef47e46e83287350933047295e +WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_PDFJS PUBLIC OFF) +WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_MEDIA_RECORDER PRIVATE OFF) +WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_THUNDER PRIVATE OFF) ++WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_WEBXR PRIVATE OFF) + +WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_APPLICATION_MANIFEST PRIVATE ON) +WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_CURSOR_VISIBILITY PRIVATE ON) @@ -20443,9 +20383,9 @@ index 303550b9618e8c621e246e8c95b9e7542585f1fc..efdb66ef47e46e83287350933047295e + include(GStreamerDependencies) - # Finalize the value for all options. Do not attempt to use an option before + WEBKIT_OPTION_DEPEND(ENABLE_WEBXR ENABLE_GAMEPAD) diff --git a/Source/cmake/OptionsWPE.cmake b/Source/cmake/OptionsWPE.cmake -index 45b4da4575d41bdf1b829ddf68947f30bc57734f..378b9114ae03c18f0caead71750c1169b2c45b27 100644 +index d04d2c2504b829f240633a9c7e3de15876190ba0..9f3a7febd7374a60a55c1d6aaee50cc26df59fc6 100644 --- a/Source/cmake/OptionsWPE.cmake +++ b/Source/cmake/OptionsWPE.cmake @@ -23,6 +23,9 @@ find_package(WebP REQUIRED COMPONENTS demux) @@ -20458,7 +20398,7 @@ index 45b4da4575d41bdf1b829ddf68947f30bc57734f..378b9114ae03c18f0caead71750c1169 WEBKIT_OPTION_BEGIN() SET_AND_EXPOSE_TO_BUILD(ENABLE_DEVELOPER_MODE ${DEVELOPER_MODE}) -@@ -81,6 +84,21 @@ else () +@@ -81,6 +84,22 @@ else () WEBKIT_OPTION_DEFAULT_PORT_VALUE(USE_SKIA PRIVATE OFF) endif () @@ -20467,6 +20407,7 @@ index 45b4da4575d41bdf1b829ddf68947f30bc57734f..378b9114ae03c18f0caead71750c1169 +WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_PDFJS PUBLIC OFF) +WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_MEDIA_RECORDER PRIVATE OFF) +WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_THUNDER PRIVATE OFF) ++WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_WEBXR PRIVATE OFF) + +WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_APPLICATION_MANIFEST PRIVATE ON) +WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_CURSOR_VISIBILITY PRIVATE ON) @@ -20480,7 +20421,7 @@ index 45b4da4575d41bdf1b829ddf68947f30bc57734f..378b9114ae03c18f0caead71750c1169 # Public options specific to the WPE port. Do not add any options here unless # there is a strong reason we should support changing the value of the option, # and the option is not relevant to other WebKit ports. -@@ -116,6 +134,11 @@ WEBKIT_OPTION_DEPEND(USE_QT6 ENABLE_WPE_PLATFORM) +@@ -116,6 +135,11 @@ WEBKIT_OPTION_DEPEND(USE_QT6 ENABLE_WPE_PLATFORM) WEBKIT_OPTION_DEPEND(USE_SKIA_OPENTYPE_SVG USE_SKIA) WEBKIT_OPTION_DEPEND(USE_SYSTEM_SYSPROF_CAPTURE USE_SYSPROF_CAPTURE) @@ -20493,7 +20434,7 @@ index 45b4da4575d41bdf1b829ddf68947f30bc57734f..378b9114ae03c18f0caead71750c1169 WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_BUBBLEWRAP_SANDBOX PUBLIC ON) WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_MEMORY_SAMPLER PRIVATE ON) diff --git a/Source/cmake/OptionsWin.cmake b/Source/cmake/OptionsWin.cmake -index 95393f0a6063615b417eb3249d3838ad0e3024d8..beedf862d572718b8f0f5e6daa0f8d029a96ea77 100644 +index 1a8e2da4c2a377e0dbfd7f7b1bc7b5ce597d4a4e..cee24362b16102e6efb9e017c77810ca4bdf32d7 100644 --- a/Source/cmake/OptionsWin.cmake +++ b/Source/cmake/OptionsWin.cmake @@ -55,6 +55,10 @@ find_package(ZLIB 1.2.11 REQUIRED) @@ -20507,7 +20448,7 @@ index 95393f0a6063615b417eb3249d3838ad0e3024d8..beedf862d572718b8f0f5e6daa0f8d02 WEBKIT_OPTION_BEGIN() # FIXME: Most of these options should not be public. -@@ -113,6 +117,14 @@ WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_FTPDIR PRIVATE OFF) +@@ -112,6 +116,14 @@ WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_FTPDIR PRIVATE OFF) SET_AND_EXPOSE_TO_BUILD(ENABLE_WEBDRIVER_KEYBOARD_INTERACTIONS ON) SET_AND_EXPOSE_TO_BUILD(ENABLE_WEBDRIVER_MOUSE_INTERACTIONS ON) @@ -20938,7 +20879,7 @@ index d3fbb968ee463f86c64fecb855b46c8634b4b72d..01dbbfbb93f2cfa6eb6440cce9794ec9 g_clear_object(&interfaceSettings); diff --git a/Tools/MiniBrowser/wpe/main.cpp b/Tools/MiniBrowser/wpe/main.cpp -index 6a898a5bd79a4e6f2f26be7f63347cef7a5c0ab4..d895941dab0f7a0bdfafcf60f203aa82c46874a6 100644 +index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a59760455329d316e33 100644 --- a/Tools/MiniBrowser/wpe/main.cpp +++ b/Tools/MiniBrowser/wpe/main.cpp @@ -52,6 +52,9 @@ static gboolean headlessMode; @@ -21272,19 +21213,6 @@ index 1067b31bc989748dfcc5502209d36d001b9b239e..7629263fb8bc93dca6dfc01c75eed8d2 +if (ENABLE_WEBKIT) + add_subdirectory(Playwright/win) +endif () -diff --git a/Tools/Scripts/build-webkit b/Tools/Scripts/build-webkit -index fcbf4620e66e0a49396fb650f2da1fad43e8a6fc..7c38c04e082313cbd7513ba8b5626f377f6850d4 100755 ---- a/Tools/Scripts/build-webkit -+++ b/Tools/Scripts/build-webkit -@@ -278,7 +278,7 @@ if (isAppleCocoaWebKit()) { - push @projects, ("Source/WebKit"); - - if (!isEmbeddedWebKit()) { -- push @projects, ("Tools/MiniBrowser"); -+ push @projects, ("Tools/Playwright"); - - # WebInspectorUI must come after JavaScriptCore and WebCore but before WebKit and WebKit2 - my $webKitIndex = first { $projects[$_] eq "Source/WebKitLegacy" } 0..$#projects; diff --git a/Tools/WebKitTestRunner/CMakeLists.txt b/Tools/WebKitTestRunner/CMakeLists.txt index 9e53f459e444b9c10fc5248f0e8059df6c1e0041..c17c875a7dd3ca05c4489578ab32378bca45a7c9 100644 --- a/Tools/WebKitTestRunner/CMakeLists.txt @@ -21301,7 +21229,7 @@ index 9e53f459e444b9c10fc5248f0e8059df6c1e0041..c17c875a7dd3ca05c4489578ab32378b "${WebKitTestRunner_DIR}/InjectedBundle/Bindings/AccessibilityController.idl" "${WebKitTestRunner_DIR}/InjectedBundle/Bindings/AccessibilityTextMarker.idl" diff --git a/Tools/WebKitTestRunner/TestController.cpp b/Tools/WebKitTestRunner/TestController.cpp -index ee6c2488fd655c40d250b405a82358e81e2661f6..5b9baf3c2ee0e9bb28fd765c7fdacb3a70f9ef36 100644 +index 567ccd8d7a4bdb3c576c60230123b78465da6505..75bf76270063878f3f1ded7b47eb2f8cc90271d8 100644 --- a/Tools/WebKitTestRunner/TestController.cpp +++ b/Tools/WebKitTestRunner/TestController.cpp @@ -713,6 +713,7 @@ PlatformWebView* TestController::createOtherPlatformWebView(PlatformWebView* par @@ -21484,7 +21412,7 @@ index df22308266c6f69d24a60905f8d05e4e80f21b9b..2d0838070dc10793418cbb648b095a5f static cairo_user_data_key_t bufferKey; cairo_surface_set_user_data(m_snapshot, &bufferKey, buffer, diff --git a/WebKit.xcworkspace/contents.xcworkspacedata b/WebKit.xcworkspace/contents.xcworkspacedata -index 3ad442a8691847c6921c5f66a805b7b0523b1e27..95407368a95d2f7d6fb697110912014cabe29afa 100644 +index 5d6492124eb520ce2b25f5c62775438103050ed3..1c0ea8a3258b5676b81fb00360e2c0589cc93e18 100644 --- a/WebKit.xcworkspace/contents.xcworkspacedata +++ b/WebKit.xcworkspace/contents.xcworkspacedata @@ -4,6 +4,9 @@ @@ -21498,11 +21426,11 @@ index 3ad442a8691847c6921c5f66a805b7b0523b1e27..95407368a95d2f7d6fb697110912014c location = "group:Source/bmalloc/bmalloc.xcodeproj"> diff --git a/WebKit.xcworkspace/xcshareddata/xcschemes/Everything up to WebKit + Tools.xcscheme b/WebKit.xcworkspace/xcshareddata/xcschemes/Everything up to WebKit + Tools.xcscheme -index ded307890926eaf0ca169aaef39ea08bd982a47a..2db0c0abdda702fdff9314ba341b63c5d09289bc 100644 +index 68732987fc02d863415360d066bc2a45d16ec39a..f4f924cb859034306361e9e55cdf31c6a1ca3b32 100644 --- a/WebKit.xcworkspace/xcshareddata/xcschemes/Everything up to WebKit + Tools.xcscheme +++ b/WebKit.xcworkspace/xcshareddata/xcschemes/Everything up to WebKit + Tools.xcscheme -@@ -188,6 +188,20 @@ - ReferencedContainer = "container:Tools/MobileMiniBrowser/MobileMiniBrowser.xcodeproj"> +@@ -202,6 +202,20 @@ + ReferencedContainer = "container:Tools/SwiftBrowser/SwiftBrowser.xcodeproj"> + Date: Tue, 19 Aug 2025 15:58:02 +0200 Subject: [PATCH 004/329] test: move browser._launchServer in child process (#37114) Co-authored-by: Max Schmitt --- tests/config/remote-server-impl.js | 13 +++++++++++-- tests/config/remoteServer.ts | 1 + tests/library/browsertype-connect.spec.ts | 16 +++++----------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/config/remote-server-impl.js b/tests/config/remote-server-impl.js index baf6d946b99a1..e77ed89395214 100644 --- a/tests/config/remote-server-impl.js +++ b/tests/config/remote-server-impl.js @@ -2,7 +2,8 @@ const fs = require('fs'); const cluster = require('cluster'); async function start() { - const { browserTypeName, launchOptions, stallOnClose, disconnectOnSIGHUP, exitOnFile, exitOnWarning, startStopAndRunHttp } = JSON.parse(process.argv[2]); + /** @type {import("./remoteServer").RemoteServerOptions} */ + const { browserTypeName, launchOptions, stallOnClose, disconnectOnSIGHUP, exitOnFile, exitOnWarning, startStopAndRunHttp, existingBrowser } = JSON.parse(process.argv[2]); if (stallOnClose) { launchOptions.__testHookGracefullyClose = () => { console.log(`(stalled=>true)`); @@ -25,7 +26,15 @@ async function start() { return; } - const browserServer = await playwright[browserTypeName].launchServer(launchOptions); + let browserServer; + if (existingBrowser) { + const browser = await playwright[browserTypeName].launch(launchOptions); + const page = await browser.newPage(); + await page.setContent(existingBrowser.content); + browserServer = await browser._launchServer(); + } else { + browserServer = await playwright[browserTypeName].launchServer(launchOptions); + } if (disconnectOnSIGHUP) process.on('SIGHUP', () => browserServer._disconnectForTest()); diff --git a/tests/config/remoteServer.ts b/tests/config/remoteServer.ts index 4c25934581abc..2e9ec35d2a161 100644 --- a/tests/config/remoteServer.ts +++ b/tests/config/remoteServer.ts @@ -69,6 +69,7 @@ export type RemoteServerOptions = { url?: string; startStopAndRunHttp?: boolean; sharedBrowser?: boolean; + existingBrowser?: { content: string }; }; export class RemoteServer implements PlaywrightServer { diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index 826bc07df9d2c..98dcc6e80703f 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -1047,18 +1047,12 @@ test.describe('launchServer only', () => { await expect(browser._parent.launch({ timeout: 0 })).rejects.toThrowError('Launching more browsers is not allowed.'); }); - test('should work with existing browser', async ({ connect, browserType, mode }) => { + test('should work with existing browser', async ({ connect, startRemoteServer, mode }) => { test.skip(mode === 'driver', 'Driver mode does not support browserType.launchServer'); - // can't use browser fixture because it's shared across the worker, launching a server on that would infect other tests - const browser = await browserType.launch(); - const page = await browser.newPage(); - await page.setContent('hello world'); - const server = await (browser as any)._launchServer(); - const secondBrowser = await connect(server.wsEndpoint()); - const secondPage = secondBrowser.contexts()[0].pages()[0]; - expect(await secondPage.content()).toContain('hello world'); - await server.close(); - await browser.close(); + const remoteServer = await startRemoteServer('launchServer', { existingBrowser: { content: 'hello world' } }); + const browser = await connect(remoteServer.wsEndpoint()); + const page = browser.contexts()[0].pages()[0]; + expect(await page.content()).toContain('hello world'); }); }); From 25d83559be97c5abdef6cb908d5ec6a9c0a34f0b Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Tue, 19 Aug 2025 23:35:54 +0200 Subject: [PATCH 005/329] chore(bidi): cancel authentication if it has been attempted for the same request before (#37105) --- .../src/server/bidi/bidiNetworkManager.ts | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts b/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts index f4e4a801d6428..ac418ff615b83 100644 --- a/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts +++ b/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts @@ -34,6 +34,7 @@ export class BidiNetworkManager { private _userRequestInterceptionEnabled: boolean = false; private _protocolRequestInterceptionEnabled: boolean = false; private _credentials: types.Credentials | undefined; + private _attemptedAuthentications = new Set(); private _intercepId: bidi.Network.Intercept | undefined; constructor(bidiSession: BidiSession, page: Page) { @@ -61,7 +62,7 @@ export class BidiNetworkManager { if (!frame) return; if (redirectedFrom) - this._requests.delete(redirectedFrom._id); + this._deleteRequest(redirectedFrom._id); let route; if (param.intercepts) { // We do not support intercepting redirects. @@ -131,7 +132,7 @@ export class BidiNetworkManager { if (isRedirected) { response._requestFinished(responseEndTime); } else { - this._requests.delete(request._id); + this._deleteRequest(request._id); response._requestFinished(responseEndTime); } response._setHttpVersion(params.response.protocol); @@ -143,7 +144,7 @@ export class BidiNetworkManager { const request = this._requests.get(params.request.request); if (!request) return; - this._requests.delete(request._id); + this._deleteRequest(request._id); const response = request.request._existingResponse(); if (response) { response.setTransferSize(null); @@ -159,15 +160,23 @@ export class BidiNetworkManager { const isBasic = params.response.authChallenges?.some(challenge => challenge.scheme.startsWith('Basic')); const credentials = this._page.browserContext._options.httpCredentials; if (isBasic && credentials) { - this._session.sendMayFail('network.continueWithAuth', { - request: params.request.request, - action: 'provideCredentials', - credentials: { - type: 'password', - username: credentials.username, - password: credentials.password, - } - }); + if (this._attemptedAuthentications.has(params.request.request)) { + this._session.sendMayFail('network.continueWithAuth', { + request: params.request.request, + action: 'cancel', + }); + } else { + this._attemptedAuthentications.add(params.request.request); + this._session.sendMayFail('network.continueWithAuth', { + request: params.request.request, + action: 'provideCredentials', + credentials: { + type: 'password', + username: credentials.username, + password: credentials.password, + } + }); + } } else { this._session.sendMayFail('network.continueWithAuth', { request: params.request.request, @@ -176,6 +185,11 @@ export class BidiNetworkManager { } } + _deleteRequest(requestId: string) { + this._requests.delete(requestId); + this._attemptedAuthentications.delete(requestId); + } + async setRequestInterception(value: boolean) { this._userRequestInterceptionEnabled = value; await this._updateProtocolRequestInterception(); From d75df5962ec605361625f8f3d0abe9e3211b8317 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 20 Aug 2025 08:58:26 +0200 Subject: [PATCH 006/329] docs: add release-notes for v1.55 (#37113) Signed-off-by: Max Schmitt Co-authored-by: Simon Knott --- docs/src/release-notes-csharp.md | 27 +++++++++++++++++++++++++++ docs/src/release-notes-java.md | 25 +++++++++++++++++++++++++ docs/src/release-notes-js.md | 29 +++++++++++++++++++++++++++++ docs/src/release-notes-python.md | 25 +++++++++++++++++++++++++ 4 files changed, 106 insertions(+) diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index cb9df98dd64ba..e6bc63a1f6fe5 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -4,6 +4,33 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.55 + +### Codegen + +- Automatic `toBeVisible()` assertions: Codegen can now generate automatic `toBeVisible()` assertions for common UI interactions. This feature can be enabled in the Codegen settings UI. + +### Breaking Changes + +- ⚠️ Dropped support for Chromium extension manifest v2. + +### Miscellaneous + +- Added support for Debian 13 "Trixie". +- Added support for Xunit v3 as part of [`Microsoft.Playwright.Xunit.v3`](https://www.nuget.org/packages/Microsoft.Playwright.Xunit.v3) +- Added support for MSTest v4 as part of [`Microsoft.Playwright.MSTest.v4`](https://www.nuget.org/packages/Microsoft.Playwright.MSTest.v4) + +### Browser Versions + +- Chromium 140.0.7339.16 +- Mozilla Firefox 141.0 +- WebKit 26.0 + +This version was also tested against the following stable channels: + +- Google Chrome 139 +- Microsoft Edge 139 + ## Version 1.54 ### Highlights diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index a190d472a8a4b..ffff3b2420ed2 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -4,6 +4,31 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.55 + +### Codegen + +- Automatic `toBeVisible()` assertions: Codegen can now generate automatic `toBeVisible()` assertions for common UI interactions. This feature can be enabled in the Codegen settings UI. + +### Breaking Changes + +- ⚠️ Dropped support for Chromium extension manifest v2. + +### Miscellaneous + +- Added support for Debian 13 "Trixie". + +### Browser Versions + +- Chromium 140.0.7339.16 +- Mozilla Firefox 141.0 +- WebKit 26.0 + +This version was also tested against the following stable channels: + +- Google Chrome 139 +- Microsoft Edge 139 + ## Version 1.54 ### Highlights diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index d8d88ddf8dfcb..202475742065f 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -6,6 +6,35 @@ toc_max_heading_level: 2 import LiteYouTube from '@site/src/components/LiteYouTube'; +## Version 1.55 + +### New APIs + +- New Property [`property: TestStepInfo.titlePath`] Returns the full title path starting from the test file, including test and step titles. + +### Codegen + +- Automatic `toBeVisible()` assertions: Codegen can now generate automatic `toBeVisible()` assertions for common UI interactions. This feature can be enabled in the Codegen settings UI. + +### Breaking Changes + +- ⚠️ Dropped support for Chromium extension manifest v2. + +### Miscellaneous + +- Added support for Debian 13 "Trixie". + +### Browser Versions + +- Chromium 140.0.7339.16 +- Mozilla Firefox 141.0 +- WebKit 26.0 + +This version was also tested against the following stable channels: + +- Google Chrome 139 +- Microsoft Edge 139 + ## Version 1.54 ### Highlights diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md index 1a7d13fd56559..0cfa77c926d5b 100644 --- a/docs/src/release-notes-python.md +++ b/docs/src/release-notes-python.md @@ -4,6 +4,31 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.55 + +### Codegen + +- Automatic `toBeVisible()` assertions: Codegen can now generate automatic `toBeVisible()` assertions for common UI interactions. This feature can be enabled in the Codegen settings UI. + +### Breaking Changes + +- ⚠️ Dropped support for Chromium extension manifest v2. + +### Miscellaneous + +- Added support for Debian 13 "Trixie". + +### Browser Versions + +- Chromium 140.0.7339.16 +- Mozilla Firefox 141.0 +- WebKit 26.0 + +This version was also tested against the following stable channels: + +- Google Chrome 139 +- Microsoft Edge 139 + ## Version 1.54 ### Highlights From 832e8ac42d1ae2ab7ed942417bafd66c23d119d5 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:05:49 +0200 Subject: [PATCH 007/329] test: roll stable-test-runner to 1.56.0-alpha-2025-08-20 (#37120) --- .../stable-test-runner/package-lock.json | 46 +++++++++---------- .../stable-test-runner/package.json | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index 5783e7dae80ea..89166d8c6bed5 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,16 +5,16 @@ "packages": { "": { "dependencies": { - "@playwright/test": "^1.55.0-alpha-2025-08-18" + "@playwright/test": "^1.56.0-alpha-2025-08-20" } }, "node_modules/@playwright/test": { - "version": "1.55.0-alpha-2025-08-18", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0-alpha-2025-08-18.tgz", - "integrity": "sha512-JRaYyoUUCLEAZTPLKTyHbnTkiQwslxbrQwG99+q5nquET1zIv78NOPeNzDUbRGY9B7eLB9L34KBfM/5gJawqoQ==", + "version": "1.56.0-alpha-2025-08-20", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-08-20.tgz", + "integrity": "sha512-FxvU2d5YuCwaq6ESNaJPAi/koEDai+WyRJx+4UcfeAo4xz+voH3uBEWstk2Lwkao1j9auvaeM+nDlgo0MQubfQ==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.55.0-alpha-2025-08-18" + "playwright": "1.56.0-alpha-2025-08-20" }, "bin": { "playwright": "cli.js" @@ -38,12 +38,12 @@ } }, "node_modules/playwright": { - "version": "1.55.0-alpha-2025-08-18", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0-alpha-2025-08-18.tgz", - "integrity": "sha512-Q4IjkuNEy1eetYFMBGY5QvguL5DUNRsJsha5dIj+VZ7feCtucHup/bfOfs716zTrnZ5WDYmdd3RteTdwx2Vmuw==", + "version": "1.56.0-alpha-2025-08-20", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-08-20.tgz", + "integrity": "sha512-vIck+Po7jKPiVLkGLmhxYPW0Iux1Uhtqpqkzf0Z9jK+D2wYGYDGqlsEOqGbDcJiIxWmK14mByTCtN6LwfgCeMg==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0-alpha-2025-08-18" + "playwright-core": "1.56.0-alpha-2025-08-20" }, "bin": { "playwright": "cli.js" @@ -56,9 +56,9 @@ } }, "node_modules/playwright-core": { - "version": "1.55.0-alpha-2025-08-18", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0-alpha-2025-08-18.tgz", - "integrity": "sha512-UefQibPrUeVhXRbb0V7PniATbscGRqamtjs/TmG4ez4rjWDTPq/wd7qMP25ZeWHUOODYsaQGnn6Nc9JChWNTvQ==", + "version": "1.56.0-alpha-2025-08-20", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-08-20.tgz", + "integrity": "sha512-zYBgXr6+VO3jYc23iNLS7OUS5cNZlAnEdsn/X9lqIO5GTfBINyn1KdyNuqgt00RoxEkgGhtPRFJRBpg6fph5hw==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -70,11 +70,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.55.0-alpha-2025-08-18", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0-alpha-2025-08-18.tgz", - "integrity": "sha512-JRaYyoUUCLEAZTPLKTyHbnTkiQwslxbrQwG99+q5nquET1zIv78NOPeNzDUbRGY9B7eLB9L34KBfM/5gJawqoQ==", + "version": "1.56.0-alpha-2025-08-20", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-08-20.tgz", + "integrity": "sha512-FxvU2d5YuCwaq6ESNaJPAi/koEDai+WyRJx+4UcfeAo4xz+voH3uBEWstk2Lwkao1j9auvaeM+nDlgo0MQubfQ==", "requires": { - "playwright": "1.55.0-alpha-2025-08-18" + "playwright": "1.56.0-alpha-2025-08-20" } }, "fsevents": { @@ -84,18 +84,18 @@ "optional": true }, "playwright": { - "version": "1.55.0-alpha-2025-08-18", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0-alpha-2025-08-18.tgz", - "integrity": "sha512-Q4IjkuNEy1eetYFMBGY5QvguL5DUNRsJsha5dIj+VZ7feCtucHup/bfOfs716zTrnZ5WDYmdd3RteTdwx2Vmuw==", + "version": "1.56.0-alpha-2025-08-20", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-08-20.tgz", + "integrity": "sha512-vIck+Po7jKPiVLkGLmhxYPW0Iux1Uhtqpqkzf0Z9jK+D2wYGYDGqlsEOqGbDcJiIxWmK14mByTCtN6LwfgCeMg==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.55.0-alpha-2025-08-18" + "playwright-core": "1.56.0-alpha-2025-08-20" } }, "playwright-core": { - "version": "1.55.0-alpha-2025-08-18", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0-alpha-2025-08-18.tgz", - "integrity": "sha512-UefQibPrUeVhXRbb0V7PniATbscGRqamtjs/TmG4ez4rjWDTPq/wd7qMP25ZeWHUOODYsaQGnn6Nc9JChWNTvQ==" + "version": "1.56.0-alpha-2025-08-20", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-08-20.tgz", + "integrity": "sha512-zYBgXr6+VO3jYc23iNLS7OUS5cNZlAnEdsn/X9lqIO5GTfBINyn1KdyNuqgt00RoxEkgGhtPRFJRBpg6fph5hw==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index d46a4c4c46dd5..622a9d1e78fa0 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "^1.55.0-alpha-2025-08-18" + "@playwright/test": "^1.56.0-alpha-2025-08-20" } } From 3ce94c43af5eac577c7117b2d6c360c71cad3b18 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Wed, 20 Aug 2025 20:06:01 +0200 Subject: [PATCH 008/329] test(bidi) fix test expectations for BiDi (#37112) --- tests/library/har.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index db5b14a5d135e..e08a089b7ba55 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -56,7 +56,7 @@ it('should have browser', async ({ browserName, browser, contextFactory, server await page.goto(server.EMPTY_PAGE); const log = await getLog(); - expect(log.browser!.name.toLowerCase()).toBe(browserName); + expect(log.browser!.name).toBe(browserName); expect(log.browser!.version).toBe(browser.version()); }); @@ -913,6 +913,6 @@ it('should not hang on slow chunked response', async ({ browserName, browser, co await page.evaluate(() => (window as any).receivedFirstData); const log = await getLog(); - expect(log.browser!.name.toLowerCase()).toBe(browserName); + expect(log.browser!.name).toBe(browserName); expect(log.browser!.version).toBe(browser.version()); }); From dc278780f9c520791676afaf81f168b05014adc6 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 20 Aug 2025 14:34:09 -0700 Subject: [PATCH 009/329] fix(webkit): download navigations do not have frame id (#37129) --- packages/playwright-core/src/server/webkit/wkBrowser.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index 5ee778b8301ba..b7f92e2d45da0 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -121,7 +121,14 @@ export class WKBrowser extends Browser { // TODO: this is racy, because download might be unrelated any navigation, and we will // abort navigation that is still running. We should be able to fix this by // instrumenting policy decision start/proceed/cancel. - page._page.frameManager.frameAbortedNavigation(payload.frameId, 'Download is starting'); + // + // Since https://commits.webkit.org/298732@main, WebKit doesn't provide frame id for + // navigations converted into downloads and the download has a fake frameId. We map it + // to the main frame. + let frameId = payload.frameId; + if (!page._page.frameManager.frame(frameId)) + frameId = page._page.mainFrame()._id; + page._page.frameManager.frameAbortedNavigation(frameId, 'Download is starting'); let originPage = page._page.initializedOrUndefined(); // If it's a new window download, report it on the opener page. if (!originPage) { From e0adf78b3651fcb304ea303659beb2310a220fa2 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 20 Aug 2025 17:00:34 -0700 Subject: [PATCH 010/329] chore: experimental test mcp (#37117) --- package-lock.json | 14 +- packages/playwright-test-mcp/README.md | 1 - packages/playwright-test-mcp/cli.js | 20 -- packages/playwright-test-mcp/package.json | 38 ---- .../bundles/mcp/src/mcpBundleImpl.ts | 1 + packages/playwright/package.json | 2 +- packages/playwright/src/DEPS.list | 3 + packages/playwright/src/index.ts | 52 ++++- packages/playwright/src/mcp/browser/DEPS.list | 2 + .../playwright/src/mcp/browser/backend.ts | 65 ++++++ .../src/mcp/browser/tool.ts} | 20 +- packages/playwright/src/mcp/browser/tools.ts | 137 +++++++++++ packages/playwright/src/mcp/sdk/DEPS.list | 1 + .../playwright/src/mcp/{ => sdk}/bundle.ts | 6 +- packages/playwright/src/mcp/sdk/call.ts | 31 +++ .../playwright/src/mcp/{ => sdk}/exports.ts | 11 +- .../src/mcp/{transport.ts => sdk/http.ts} | 114 ++++------ .../src/mcp/{ => sdk}/inProcessTransport.ts | 0 packages/playwright/src/mcp/sdk/mdb.ts | 215 ++++++++++++++++++ .../src/mcp/{ => sdk}/proxyBackend.ts | 28 +-- .../playwright/src/mcp/{ => sdk}/server.ts | 66 ++++-- packages/playwright/src/mcp/{ => sdk}/tool.ts | 10 +- packages/playwright/src/mcp/test/DEPS.list | 12 + .../src/mcp/test/backend.ts} | 24 +- packages/playwright/src/mcp/test/context.ts | 40 ++++ .../src/mcp/test}/listTests.ts | 8 +- .../src/mcp/test}/program.ts | 22 +- .../src/mcp/test}/runTests.ts | 20 +- .../src/mcp/test}/streams.ts | 0 .../src => playwright/src/mcp/test}/tool.ts | 2 +- packages/playwright/src/runner/testRunner.ts | 2 + utils/workspace.js | 5 - 32 files changed, 738 insertions(+), 234 deletions(-) delete mode 100644 packages/playwright-test-mcp/README.md delete mode 100755 packages/playwright-test-mcp/cli.js delete mode 100644 packages/playwright-test-mcp/package.json create mode 100644 packages/playwright/src/mcp/browser/DEPS.list create mode 100644 packages/playwright/src/mcp/browser/backend.ts rename packages/{playwright-test-mcp/src/context.ts => playwright/src/mcp/browser/tool.ts} (59%) create mode 100644 packages/playwright/src/mcp/browser/tools.ts create mode 100644 packages/playwright/src/mcp/sdk/DEPS.list rename packages/playwright/src/mcp/{ => sdk}/bundle.ts (88%) create mode 100644 packages/playwright/src/mcp/sdk/call.ts rename packages/playwright/src/mcp/{ => sdk}/exports.ts (78%) rename packages/playwright/src/mcp/{transport.ts => sdk/http.ts} (71%) rename packages/playwright/src/mcp/{ => sdk}/inProcessTransport.ts (100%) create mode 100644 packages/playwright/src/mcp/sdk/mdb.ts rename packages/playwright/src/mcp/{ => sdk}/proxyBackend.ts (80%) rename packages/playwright/src/mcp/{ => sdk}/server.ts (61%) rename packages/playwright/src/mcp/{ => sdk}/tool.ts (85%) create mode 100644 packages/playwright/src/mcp/test/DEPS.list rename packages/{playwright-test-mcp/src/testServerBackend.ts => playwright/src/mcp/test/backend.ts} (70%) create mode 100644 packages/playwright/src/mcp/test/context.ts rename packages/{playwright-test-mcp/src/tools => playwright/src/mcp/test}/listTests.ts (92%) rename packages/{playwright-test-mcp/src => playwright/src/mcp/test}/program.ts (65%) rename packages/{playwright-test-mcp/src/tools => playwright/src/mcp/test}/runTests.ts (79%) rename packages/{playwright-test-mcp/src => playwright/src/mcp/test}/streams.ts (100%) rename packages/{playwright-test-mcp/src => playwright/src/mcp/test}/tool.ts (94%) diff --git a/package-lock.json b/package-lock.json index 4d1c0cbc0ca23..39fa270d4331b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1402,10 +1402,6 @@ "resolved": "packages/playwright-test", "link": true }, - "node_modules/@playwright/test-runner-mcp": { - "resolved": "packages/playwright-test-mcp", - "link": true - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.40.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", @@ -3057,15 +3053,6 @@ "node": ">=0.1.90" } }, - "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8892,6 +8879,7 @@ "packages/playwright-test-mcp": { "name": "@playwright/test-runner-mcp", "version": "0.0.1", + "extraneous": true, "license": "Apache-2.0", "dependencies": { "commander": "^13.1.0", diff --git a/packages/playwright-test-mcp/README.md b/packages/playwright-test-mcp/README.md deleted file mode 100644 index a3c65d33b8905..0000000000000 --- a/packages/playwright-test-mcp/README.md +++ /dev/null @@ -1 +0,0 @@ -# 🎭 Experimental diff --git a/packages/playwright-test-mcp/cli.js b/packages/playwright-test-mcp/cli.js deleted file mode 100755 index 8a49330f8f17c..0000000000000 --- a/packages/playwright-test-mcp/cli.js +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env node -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const { program } = require('./lib/program'); -program.parse(process.argv); - diff --git a/packages/playwright-test-mcp/package.json b/packages/playwright-test-mcp/package.json deleted file mode 100644 index b87d2b60c1c0c..0000000000000 --- a/packages/playwright-test-mcp/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@playwright/test-runner-mcp", - "version": "0.0.1", - "private": true, - "description": "Experimental MCP server for Playwright Test", - "repository": { - "type": "git", - "url": "git+https://github.com/microsoft/playwright.git" - }, - "homepage": "https://playwright.dev", - "engines": { - "node": ">=18" - }, - "author": { - "name": "Microsoft Corporation" - }, - "license": "Apache-2.0", - "exports": { - ".": { - "types": "./index.d.ts", - "import": "./index.mjs", - "require": "./index.js", - "default": "./index.js" - }, - "./cli": "./cli.js", - "./package.json": "./package.json", - "./reporter": "./reporter.js" - }, - "bin": { - "mcp-server-playwright-test": "cli.js" - }, - "scripts": {}, - "dependencies": { - "commander": "^13.1.0", - "playwright": "1.56.0-next", - "playwright-core": "1.56.0-next" - } -} diff --git a/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts b/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts index 782b3ac8b8403..1df3787d47804 100644 --- a/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts +++ b/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts @@ -19,6 +19,7 @@ export { Server } from '@modelcontextprotocol/sdk/server/index.js'; export { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; export { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; export { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +export { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; export { CallToolRequestSchema, ListRootsRequestSchema, ListToolsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js'; export { z } from 'zod'; export { zodToJsonSchema } from 'zod-to-json-schema'; diff --git a/packages/playwright/package.json b/packages/playwright/package.json index d4379d66e4cd6..6e2183c72b55f 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -21,7 +21,7 @@ "./package.json": "./package.json", "./lib/common/configLoader": "./lib/common/configLoader.js", "./lib/fsWatcher": "./lib/fsWatcher.js", - "./lib/mcp": "./lib/mcp/exports.js", + "./lib/mcp/sdk/exports": "./lib/mcp/sdk/exports.js", "./lib/program": "./lib/program.js", "./lib/reporters/base": "./lib/reporters/base.js", "./lib/reporters/list": "./lib/reporters/list.js", diff --git a/packages/playwright/src/DEPS.list b/packages/playwright/src/DEPS.list index e9b4b2f49362d..b667396ff338d 100644 --- a/packages/playwright/src/DEPS.list +++ b/packages/playwright/src/DEPS.list @@ -10,6 +10,9 @@ common/ @testIsomorphic/** ./prompt.ts ./worker/testTracing.ts +./mcp/browser/ +./mcp/sdk/ +./transform/babelBundle.ts [internalsForTest.ts] ** diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 9277f03632971..ef94853fc0d03 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -22,8 +22,12 @@ import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringif import { currentTestInfo } from './common/globals'; import { rootTestType } from './common/testType'; +import { BrowserBackend } from './mcp/browser/backend'; +import { runOnPauseBackendLoop } from './mcp/sdk/mdb'; +import { codeFrameColumns } from './transform/babelBundle'; +import { stripAnsiEscapes } from './util'; -import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; +import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode, Location } from '../types/test'; import type { ContextReuseMode } from './common/config'; import type { TestInfoImpl, TestStepInternal } from './worker/testInfo'; import type { ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation'; @@ -236,7 +240,7 @@ const playwrightFixtures: Fixtures = ({ (testInfo as TestInfoImpl)._setDebugMode(); playwright._defaultContextOptions = _combinedContextOptions; - playwright._defaultContextTimeout = actionTimeout || 0; + playwright._defaultContextTimeout = process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP ? 5000 : actionTimeout || 0; playwright._defaultContextNavigationTimeout = navigationTimeout || 0; await use(); playwright._defaultContextOptions = undefined; @@ -287,9 +291,21 @@ const playwrightFixtures: Fixtures = ({ tracingGroupSteps.push(step); }, onApiCallRecovery: (data, error, recoveryHandlers) => { - const step = data.userData as TestStepInternal; - if (step) - recoveryHandlers.push(() => step.recoverFromStepError(error)); + if (!process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP) + return; + + const step = data.userData; + if (!step) + return; + + const code = createErrorCodeframe(error.message, step.location); + + recoveryHandlers.push(async () => { + const [context] = playwright._allContexts(); + const introMessage = `Paused on error:\n\n${stripAnsiEscapes(code)}\n\nTry recovering from the error prior to continuing.`; + await runOnPauseBackendLoop(process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP!, new BrowserBackend(context), introMessage); + return { status: 'recovered' }; + }); }, onApiCallEnd: data => { // "tracing.group" step will end later, when "tracing.groupEnd" finishes. @@ -785,3 +801,29 @@ export const test = _baseTest.extend(playwrightFix export { defineConfig } from './common/configLoader'; export { mergeTests } from './common/testType'; export { mergeExpects } from './matchers/expect'; + + +function createErrorCodeframe(message: string, location: Location) { + let source: string; + try { + source = fs.readFileSync(location.file, 'utf-8') + '\n//'; + } catch (e) { + return ''; + } + + return codeFrameColumns( + source, + { + start: { + line: location.line, + column: location.column, + }, + }, + { + highlightCode: true, + linesAbove: 5, + linesBelow: 5, + message: message.split('\n')[0] || undefined, + } + ); +} diff --git a/packages/playwright/src/mcp/browser/DEPS.list b/packages/playwright/src/mcp/browser/DEPS.list new file mode 100644 index 0000000000000..a9266a844f2e8 --- /dev/null +++ b/packages/playwright/src/mcp/browser/DEPS.list @@ -0,0 +1,2 @@ +[*] +../sdk/ diff --git a/packages/playwright/src/mcp/browser/backend.ts b/packages/playwright/src/mcp/browser/backend.ts new file mode 100644 index 0000000000000..5fe672cec2920 --- /dev/null +++ b/packages/playwright/src/mcp/browser/backend.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as mcp from '../sdk/exports'; +import * as mcpBundle from '../sdk/bundle'; +import { snapshot, pickLocator, evaluate } from './tools'; +import { defineToolSchema } from '../sdk/exports'; + +import type { Tool } from './tool'; +import type * as playwright from '../../../index'; +import type { ServerBackendOnPause } from '../sdk/mdb'; + +export class BrowserBackend implements ServerBackendOnPause { + readonly name = 'Playwright'; + readonly version = '0.0.1'; + private _tools: Tool[] = [snapshot, pickLocator, evaluate]; + private _context: playwright.BrowserContext; + + constructor(context: playwright.BrowserContext) { + this._context = context; + } + + async initialize() { + } + + async listTools(): Promise { + return [...this._tools.map(tool => mcp.toMcpTool(tool.schema)), mcp.toMcpTool(doneToolSchema)]; + } + + async callTool(name: string, args: mcp.CallToolRequest['params']['arguments']): Promise { + if (name === 'done') { + (this as ServerBackendOnPause).requestSelfDestruct?.(); + return { + content: [{ type: 'text', text: 'Done' }], + }; + } + + const tool = this._tools.find(tool => tool.schema.name === name); + if (!tool) + throw new Error(`Tool not found: ${name}. Available tools: ${this._tools.map(tool => tool.schema.name).join(', ')}`); + const parsedArguments = tool.schema.inputSchema.parse(args || {}); + return await tool.handle(this._context, parsedArguments); + } +} + +const doneToolSchema = defineToolSchema({ + name: 'done', + title: 'Done', + description: 'Done', + inputSchema: mcpBundle.z.object({}), + type: 'destructive', +}); diff --git a/packages/playwright-test-mcp/src/context.ts b/packages/playwright/src/mcp/browser/tool.ts similarity index 59% rename from packages/playwright-test-mcp/src/context.ts rename to packages/playwright/src/mcp/browser/tool.ts index aa20aac081cca..1291ddc97c552 100644 --- a/packages/playwright-test-mcp/src/context.ts +++ b/packages/playwright/src/mcp/browser/tool.ts @@ -14,17 +14,15 @@ * limitations under the License. */ -import { TestRunner } from 'playwright/lib/runner/testRunner'; +import type { z } from 'zod'; +import type * as mcp from '../sdk/exports'; +import type * as playwright from '../../../index'; -import type { ConfigLocation } from 'playwright/lib/common/config'; +export type Tool = { + schema: mcp.ToolSchema; + handle: (context: playwright.BrowserContext, params: z.output) => Promise; +}; -export class Context { - readonly testRunner: TestRunner; - - constructor(configLocation: ConfigLocation) { - this.testRunner = new TestRunner(configLocation, {}); - } - - async close() { - } +export function defineTool(tool: Tool): Tool { + return tool; } diff --git a/packages/playwright/src/mcp/browser/tools.ts b/packages/playwright/src/mcp/browser/tools.ts new file mode 100644 index 0000000000000..65a6f377a1a57 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools.ts @@ -0,0 +1,137 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { asLocator } from 'playwright-core/lib/utils'; + +import { defineTool } from './tool.js'; +import * as mcp from '../sdk/bundle'; + +import type * as playwright from '../../../index'; +import type z from 'zod'; + +type PageEx = playwright.Page & { + _snapshotForAI: () => Promise; +}; + +export const snapshot = defineTool({ + schema: { + name: 'browser_snapshot', + title: 'Page snapshot', + description: 'Capture accessibility snapshot of the current page, this is better than screenshot', + inputSchema: mcp.z.object({}), + type: 'readOnly', + }, + + handle: async (context, params) => { + const [page] = context.pages(); + if (!page) + throw new Error('No open pages available'); + + const snapshot = await (page as PageEx)._snapshotForAI(); + return { + content: [ + { + type: 'text', + text: snapshot, + }, + ], + }; + }, +}); + +export const elementSchema = mcp.z.object({ + element: mcp.z.string().describe('Human-readable element description used to obtain permission to interact with the element'), + ref: mcp.z.string().describe('Exact target element reference from the page snapshot'), +}); + +export const pickLocator = defineTool({ + schema: { + name: 'browser_pick_locator', + title: 'Pick locator', + description: 'Pick a locator for the given element', + inputSchema: elementSchema, + type: 'readOnly', + }, + + handle: async (context, params) => { + const [page] = context.pages(); + if (!page) + throw new Error('No open pages available'); + + const locator = await refLocator(page, params); + const locatorString = await generateLocator(locator); + return { + content: [ + { + type: 'text', + text: locatorString, + }, + ], + }; + }, +}); + + +const evaluateSchema = mcp.z.object({ + function: mcp.z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'), + element: mcp.z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'), + ref: mcp.z.string().optional().describe('Exact target element reference from the page snapshot'), +}); + +export const evaluate = defineTool({ + schema: { + name: 'browser_evaluate', + title: 'Evaluate JavaScript', + description: 'Evaluate JavaScript expression on page or element', + inputSchema: evaluateSchema, + type: 'destructive', + }, + + handle: async (context, params) => { + const [page] = context.pages(); + if (!page) + throw new Error('No open pages available'); + + if (params.ref && params.element) { + const locator = await refLocator(page, { ref: params.ref, element: params.element }); + const result = await locator.evaluate(params.function); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) || 'undefined' }], + }; + } + + const result = await page.evaluate(params.function); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) || 'undefined' }], + }; + }, +}); + +async function refLocator(page: playwright.Page, elementRef: z.output): Promise { + const snapshot = await (page as PageEx)._snapshotForAI(); + if (!snapshot.includes(`[ref=${elementRef.ref}]`)) + throw new Error(`Ref ${elementRef.ref} not found in the current page snapshot. Try capturing new snapshot.`); + return page.locator(`aria-ref=${elementRef.ref}`).describe(elementRef.element); +} + +async function generateLocator(locator: playwright.Locator): Promise { + try { + const { resolvedSelector } = await (locator as any)._resolveSelector(); + return asLocator('javascript', resolvedSelector); + } catch (e) { + throw new Error('Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.'); + } +} diff --git a/packages/playwright/src/mcp/sdk/DEPS.list b/packages/playwright/src/mcp/sdk/DEPS.list new file mode 100644 index 0000000000000..e43dcb5d9f30b --- /dev/null +++ b/packages/playwright/src/mcp/sdk/DEPS.list @@ -0,0 +1 @@ +[*] diff --git a/packages/playwright/src/mcp/bundle.ts b/packages/playwright/src/mcp/sdk/bundle.ts similarity index 88% rename from packages/playwright/src/mcp/bundle.ts rename to packages/playwright/src/mcp/sdk/bundle.ts index 3c594f11b81ed..3836bc2cabe45 100644 --- a/packages/playwright/src/mcp/bundle.ts +++ b/packages/playwright/src/mcp/sdk/bundle.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -const bundle = require('./mcpBundleImpl'); -const zodToJsonSchema: typeof import('zod-to-json-schema').zodToJsonSchema = require('./mcpBundleImpl').zodToJsonSchema; +const bundle = require('../../mcpBundleImpl'); +const zodToJsonSchema: typeof import('zod-to-json-schema').zodToJsonSchema = bundle.zodToJsonSchema; const Client: typeof import('@modelcontextprotocol/sdk/client/index.js').Client = bundle.Client; const Server: typeof import('@modelcontextprotocol/sdk/server/index.js').Server = bundle.Server; const SSEServerTransport: typeof import('@modelcontextprotocol/sdk/server/sse.js').SSEServerTransport = bundle.SSEServerTransport; const StdioServerTransport: typeof import('@modelcontextprotocol/sdk/server/stdio.js').StdioServerTransport = bundle.StdioServerTransport; const StreamableHTTPServerTransport: typeof import('@modelcontextprotocol/sdk/server/streamableHttp.js').StreamableHTTPServerTransport = bundle.StreamableHTTPServerTransport; +const StreamableHTTPClientTransport: typeof import('@modelcontextprotocol/sdk/client/streamableHttp.js').StreamableHTTPClientTransport = bundle.StreamableHTTPClientTransport; const CallToolRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').CallToolRequestSchema = bundle.CallToolRequestSchema; const ListRootsRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').ListRootsRequestSchema = bundle.ListRootsRequestSchema; const ListToolsRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').ListToolsRequestSchema = bundle.ListToolsRequestSchema; @@ -35,6 +36,7 @@ export { Server, SSEServerTransport, StdioServerTransport, + StreamableHTTPClientTransport, StreamableHTTPServerTransport, CallToolRequestSchema, ListRootsRequestSchema, diff --git a/packages/playwright/src/mcp/sdk/call.ts b/packages/playwright/src/mcp/sdk/call.ts new file mode 100644 index 0000000000000..2818867f801ea --- /dev/null +++ b/packages/playwright/src/mcp/sdk/call.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as mcpBundle from './bundle.js'; + +import type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/sdk/types'; + +export async function callTool(mcpUrl: string, name: string, params: CallToolRequest['params']['arguments']): Promise { + const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(mcpUrl)); + const client = new mcpBundle.Client({ name: 'Internal', version: '0.0.0' }); + await client.connect(transport); + try { + return await client.callTool({ name, arguments: params }) as CallToolResult; + } finally { + await transport.terminateSession(); + await client.close(); + } +} diff --git a/packages/playwright/src/mcp/exports.ts b/packages/playwright/src/mcp/sdk/exports.ts similarity index 78% rename from packages/playwright/src/mcp/exports.ts rename to packages/playwright/src/mcp/sdk/exports.ts index 9ca581762fb9f..12a1cb87dab4e 100644 --- a/packages/playwright/src/mcp/exports.ts +++ b/packages/playwright/src/mcp/sdk/exports.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -export * from './inProcessTransport.js'; -export * from './proxyBackend.js'; -export * from './server.js'; -export * from './tool.js'; -export * from './transport.js'; +export * from './inProcessTransport'; +export * from './proxyBackend'; +export * from './server'; +export * from './tool'; +export * from './http'; +export * from './call'; diff --git a/packages/playwright/src/mcp/transport.ts b/packages/playwright/src/mcp/sdk/http.ts similarity index 71% rename from packages/playwright/src/mcp/transport.ts rename to packages/playwright/src/mcp/sdk/http.ts index b4161b692c0f4..93b64bdefd961 100644 --- a/packages/playwright/src/mcp/transport.ts +++ b/packages/playwright/src/mcp/sdk/http.ts @@ -15,32 +15,59 @@ */ import assert from 'assert'; -import http from 'http'; import net from 'net'; +import http from 'http'; import crypto from 'crypto'; import { debug } from 'playwright-core/lib/utilsBundle'; -import * as mcpBundle from './bundle'; -import * as mcpServer from './server'; +import * as mcp from './bundle'; +import { connect } from './server'; -import type { ServerBackendFactory } from './server.js'; import type { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { ServerBackendFactory } from './server'; -export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number }) { - if (options.port !== undefined) { - const httpServer = await startHttpServer(options); - startHttpTransport(httpServer, serverBackendFactory); - } else { - await startStdioTransport(serverBackendFactory); - } +const testDebug = debug('pw:mcp:test'); + +export async function startHttpServer(config: { host?: string, port?: number }, abortSignal?: AbortSignal): Promise { + const { host, port } = config; + const httpServer = http.createServer(); + await new Promise((resolve, reject) => { + httpServer.on('error', reject); + abortSignal?.addEventListener('abort', () => { + httpServer.close(); + reject(new Error('Aborted')); + }); + httpServer.listen(port, host, () => { + resolve(); + httpServer.removeListener('error', reject); + }); + }); + return httpServer; } -async function startStdioTransport(serverBackendFactory: ServerBackendFactory) { - await mcpServer.connect(serverBackendFactory, new mcpBundle.StdioServerTransport(), false); +export function httpAddressToString(address: string | net.AddressInfo | null): string { + assert(address, 'Could not bind server socket'); + if (typeof address === 'string') + return address; + const resolvedPort = address.port; + let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`; + if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]') + resolvedHost = 'localhost'; + return `http://${resolvedHost}:${resolvedPort}`; } -const testDebug = debug('pw:mcp:test'); +export async function installHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory) { + const sseSessions = new Map(); + const streamableSessions = new Map(); + httpServer.on('request', async (req, res) => { + const url = new URL(`http://localhost${req.url}`); + if (url.pathname.startsWith('/sse')) + await handleSSE(serverBackendFactory, req, res, url, sseSessions); + else + await handleStreamable(serverBackendFactory, req, res, streamableSessions); + }); +} async function handleSSE(serverBackendFactory: ServerBackendFactory, req: http.IncomingMessage, res: http.ServerResponse, url: URL, sessions: Map) { if (req.method === 'POST') { @@ -58,10 +85,10 @@ async function handleSSE(serverBackendFactory: ServerBackendFactory, req: http.I return await transport.handlePostMessage(req, res); } else if (req.method === 'GET') { - const transport = new mcpBundle.SSEServerTransport('/sse', res); + const transport = new mcp.SSEServerTransport('/sse', res); sessions.set(transport.sessionId, transport); testDebug(`create SSE session: ${transport.sessionId}`); - await mcpServer.connect(serverBackendFactory, transport, false); + await connect(serverBackendFactory, transport, false); res.on('close', () => { testDebug(`delete SSE session: ${transport.sessionId}`); sessions.delete(transport.sessionId); @@ -86,11 +113,11 @@ async function handleStreamable(serverBackendFactory: ServerBackendFactory, req: } if (req.method === 'POST') { - const transport = new mcpBundle.StreamableHTTPServerTransport({ + const transport = new mcp.StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID(), onsessioninitialized: async sessionId => { testDebug(`create http session: ${transport.sessionId}`); - await mcpServer.connect(serverBackendFactory, transport, true); + await connect(serverBackendFactory, transport, true); sessions.set(sessionId, transport); } }); @@ -109,54 +136,3 @@ async function handleStreamable(serverBackendFactory: ServerBackendFactory, req: res.statusCode = 400; res.end('Invalid request'); } - -function startHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory) { - const sseSessions = new Map(); - const streamableSessions = new Map(); - httpServer.on('request', async (req, res) => { - const url = new URL(`http://localhost${req.url}`); - if (url.pathname.startsWith('/sse')) - await handleSSE(serverBackendFactory, req, res, url, sseSessions); - else - await handleStreamable(serverBackendFactory, req, res, streamableSessions); - }); - const url = httpAddressToString(httpServer.address()); - const message = [ - `Listening on ${url}`, - 'Put this in your client config:', - JSON.stringify({ - 'mcpServers': { - 'playwright': { - 'url': `${url}/mcp` - } - } - }, undefined, 2), - 'For legacy SSE transport support, you can use the /sse endpoint instead.', - ].join('\n'); - // eslint-disable-next-line no-console - console.error(message); -} - -async function startHttpServer(config: { host?: string, port?: number }): Promise { - const { host, port } = config; - const httpServer = http.createServer(); - await new Promise((resolve, reject) => { - httpServer.on('error', reject); - httpServer.listen(port, host, () => { - resolve(); - httpServer.removeListener('error', reject); - }); - }); - return httpServer; -} - -function httpAddressToString(address: string | net.AddressInfo | null): string { - assert(address, 'Could not bind server socket'); - if (typeof address === 'string') - return address; - const resolvedPort = address.port; - let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`; - if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]') - resolvedHost = 'localhost'; - return `http://${resolvedHost}:${resolvedPort}`; -} diff --git a/packages/playwright/src/mcp/inProcessTransport.ts b/packages/playwright/src/mcp/sdk/inProcessTransport.ts similarity index 100% rename from packages/playwright/src/mcp/inProcessTransport.ts rename to packages/playwright/src/mcp/sdk/inProcessTransport.ts diff --git a/packages/playwright/src/mcp/sdk/mdb.ts b/packages/playwright/src/mcp/sdk/mdb.ts new file mode 100644 index 0000000000000..3216e4366b1c3 --- /dev/null +++ b/packages/playwright/src/mcp/sdk/mdb.ts @@ -0,0 +1,215 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { debug } from 'playwright-core/lib/utilsBundle'; +import { ManualPromise } from 'playwright-core/lib/utils'; + +import { PingRequestSchema, z } from './bundle.js'; +import { StreamableHTTPClientTransport } from './bundle.js'; +import * as mcpBundle from './bundle.js'; + +import { defineToolSchema } from './tool.js'; +import * as mcpServer from './server.js'; +import * as mcpHttp from './http.js'; +import { callTool } from './call.js'; + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; + +const errorsDebug = debug('pw:mcp:errors'); + +export class MDBBackend implements mcpServer.ServerBackend { + private _stack: { client: Client, toolNames: string[], resultPromise: ManualPromise | undefined }[] = []; + private _interruptPromise: ManualPromise | undefined; + private _server!: mcpServer.Server; + + async initialize(server: mcpServer.Server): Promise { + this._server = server; + } + + async listTools(): Promise { + const response = await this._client().listTools(); + return response.tools; + } + + async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise { + if (name === pushToolsSchema.name) + return await this._pushTools(pushToolsSchema.inputSchema.parse(args || {})); + + this._interruptPromise = new ManualPromise(); + let [entry] = this._stack; + + // Pop the client while the tool is not found. + while (entry && !entry.toolNames.includes(name)) { + this._stack.shift(); + await entry.client.close(); + entry = this._stack[0]; + } + + const resultPromise = new ManualPromise(); + entry.resultPromise = resultPromise; + + this._client().callTool({ + name, + arguments: args, + }).then(result => { + resultPromise.resolve(result as mcpServer.CallToolResult); + }).catch(e => { + if (this._stack.length < 2) + throw e; + this._stack.shift(); + const prevEntry = this._stack[0]; + void prevEntry.resultPromise!.then(result => resultPromise.resolve(result)); + }); + return await Promise.race([this._interruptPromise, resultPromise]); + } + + private _client(): Client { + const [entry] = this._stack; + if (!entry) + throw new Error('No debugging backend available'); + return entry.client; + } + + private async _pushTools(params: { mcpUrl: string, introMessage?: string }): Promise { + const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' }); + client.setRequestHandler(PingRequestSchema, () => ({})); + const transport = new StreamableHTTPClientTransport(new URL(params.mcpUrl)); + await client.connect(transport); + + this._interruptPromise?.resolve({ + content: [{ + type: 'text', + text: params.introMessage || '', + }], + }); + this._interruptPromise = undefined; + + const { tools } = await client.listTools(); + this._stack.unshift({ client, toolNames: tools.map(tool => tool.name), resultPromise: undefined }); + await this._server.notification({ + method: 'notifications/tools/list_changed', + }); + return { content: [{ type: 'text', text: 'Tools pushed' }] }; + } +} + +const pushToolsSchema = defineToolSchema({ + name: 'mdb_push_tools', + title: 'Push MCP tools to the tools stack', + description: 'Push MCP tools to the tools stack', + inputSchema: z.object({ + mcpUrl: z.string(), + introMessage: z.string().optional(), + }), + type: 'readOnly', +}); + +export type ServerBackendOnPause = mcpServer.ServerBackend & { + requestSelfDestruct?: () => void; +}; + +export async function runToolsBackend(backendFactories: mcpServer.ServerBackendFactory[], options: { port: number }): Promise { + const mdbBackend = new MDBBackend(); + const mdbBackendFactory = { + name: 'Playwright MDB', + nameInConfig: 'playwright-mdb', + version: '0.0.0', + create: () => mdbBackend + }; + + const mdbUrl = await startAsHttp(mdbBackendFactory, options); + + for (const backendFactory of backendFactories) { + const backendUrl = await startAsHttp(backendFactory, { port: 0 }); + const result = await callTool(mdbUrl, pushToolsSchema.name, { mcpUrl: backendUrl }); + if (result.isError) + errorsDebug('Failed to push tools', result.content); + } + return mdbUrl; +} + +export async function runOnPauseBackendLoop(mdbUrl: string, backend: ServerBackendOnPause, introMessage: string) { + const wrappedBackend = new OnceTimeServerBackendWrapper(backend); + + const factory = { + name: 'on-pause-backend', + nameInConfig: 'on-pause-backend', + version: '0.0.0', + create: () => wrappedBackend, + }; + + const httpServer = await mcpHttp.startHttpServer({ port: 0 }); + await mcpHttp.installHttpTransport(httpServer, factory); + const url = mcpHttp.httpAddressToString(httpServer.address()); + + const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' }); + client.setRequestHandler(PingRequestSchema, () => ({})); + const transport = new StreamableHTTPClientTransport(new URL(mdbUrl)); + await client.connect(transport); + + const pushToolsResult = await client.callTool({ + name: pushToolsSchema.name, + arguments: { + mcpUrl: url, + introMessage, + }, + }); + if (pushToolsResult.isError) + errorsDebug('Failed to push tools', pushToolsResult.content); + await transport.terminateSession(); + await client.close(); + + await wrappedBackend.waitForClosed(); + httpServer.close(); +} + +async function startAsHttp(backendFactory: mcpServer.ServerBackendFactory, options: { port: number }) { + const httpServer = await mcpHttp.startHttpServer(options); + await mcpHttp.installHttpTransport(httpServer, backendFactory); + return mcpHttp.httpAddressToString(httpServer.address()); +} + + +class OnceTimeServerBackendWrapper implements mcpServer.ServerBackend { + private _backend: ServerBackendOnPause; + private _selfDestructPromise = new ManualPromise(); + + constructor(backend: ServerBackendOnPause) { + this._backend = backend; + this._backend.requestSelfDestruct = () => this._selfDestructPromise.resolve(); + } + + async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise { + await this._backend.initialize?.(server, clientVersion, roots); + } + + async listTools(): Promise { + return this._backend.listTools(); + } + + async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise { + return this._backend.callTool(name, args); + } + + serverClosed() { + this._backend.serverClosed?.(); + this._selfDestructPromise.resolve(); + } + + async waitForClosed() { + await this._selfDestructPromise; + } +} diff --git a/packages/playwright/src/mcp/proxyBackend.ts b/packages/playwright/src/mcp/sdk/proxyBackend.ts similarity index 80% rename from packages/playwright/src/mcp/proxyBackend.ts rename to packages/playwright/src/mcp/sdk/proxyBackend.ts index 529b3bc8e453f..2f7530ebcf4c1 100644 --- a/packages/playwright/src/mcp/proxyBackend.ts +++ b/packages/playwright/src/mcp/sdk/proxyBackend.ts @@ -15,39 +15,33 @@ */ import { debug } from 'playwright-core/lib/utilsBundle'; +import * as mcp from './bundle'; -import * as mcpBundle from './bundle'; - -import type { ServerBackend, ClientVersion, Root } from './server.js'; +import type { ServerBackend, ClientVersion, Root, Server } from './server.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; -const errorsDebug = debug('pw:mcp:errors'); - export type MCPProvider = { name: string; description: string; connect(): Promise; }; -export class ProxyBackend implements ServerBackend { - readonly name: string; - readonly version: string; +const errorsDebug = debug('pw:mcp:errors'); +export class ProxyBackend implements ServerBackend { private _mcpProviders: MCPProvider[]; private _currentClient: Client | undefined; private _contextSwitchTool: Tool; private _roots: Root[] = []; - constructor(name: string, version: string, mcpProviders: MCPProvider[]) { - this.name = name; - this.version = version; + constructor(mcpProviders: MCPProvider[]) { this._mcpProviders = mcpProviders; this._contextSwitchTool = this._defineContextSwitchTool(); } - async initialize(clientVersion: ClientVersion, roots: Root[]): Promise { + async initialize(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise { this._roots = roots; await this._setCurrentClient(this._mcpProviders[0]); } @@ -100,8 +94,8 @@ export class ProxyBackend implements ServerBackend { 'Connect to a browser using one of the available methods:', ...this._mcpProviders.map(factory => `- "${factory.name}": ${factory.description}`), ].join('\n'), - inputSchema: mcpBundle.zodToJsonSchema(mcpBundle.z.object({ - name: mcpBundle.z.enum(this._mcpProviders.map(factory => factory.name) as [string, ...string[]]).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'), + inputSchema: mcp.zodToJsonSchema(mcp.z.object({ + name: mcp.z.enum(this._mcpProviders.map(factory => factory.name) as [string, ...string[]]).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'), }), { strictUnions: true }) as Tool['inputSchema'], annotations: { title: 'Connect to a browser context', @@ -115,14 +109,14 @@ export class ProxyBackend implements ServerBackend { await this._currentClient?.close(); this._currentClient = undefined; - const client = new mcpBundle.Client({ name: this.name, version: this.version }); + const client = new mcp.Client({ name: 'Playwright MCP Proxy', version: '0.0.0' }); client.registerCapabilities({ roots: { listRoots: true, }, }); - client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots })); - client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); + client.setRequestHandler(mcp.ListRootsRequestSchema, () => ({ roots: this._roots })); + client.setRequestHandler(mcp.PingRequestSchema, () => ({})); const transport = await factory.connect(); await client.connect(transport); diff --git a/packages/playwright/src/mcp/server.ts b/packages/playwright/src/mcp/sdk/server.ts similarity index 61% rename from packages/playwright/src/mcp/server.ts rename to packages/playwright/src/mcp/sdk/server.ts index 24a3ce17e236c..b6982a0fc4263 100644 --- a/packages/playwright/src/mcp/server.ts +++ b/packages/playwright/src/mcp/sdk/server.ts @@ -15,7 +15,10 @@ */ import { debug } from 'playwright-core/lib/utilsBundle'; -import * as mcpBundle from './bundle'; + +import * as mcp from './bundle'; +import { InProcessTransport } from './inProcessTransport'; +import { httpAddressToString, installHttpTransport, startHttpServer } from './http'; import type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; @@ -28,32 +31,39 @@ const errorsDebug = debug('pw:mcp:errors'); export type ClientVersion = { name: string, version: string }; export interface ServerBackend { - name: string; - version: string; - initialize?(clientVersion: ClientVersion, roots: Root[]): Promise; + initialize?(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise; listTools(): Promise; callTool(name: string, args: CallToolRequest['params']['arguments']): Promise; serverClosed?(): void; } -export type ServerBackendFactory = () => ServerBackend; +export type ServerBackendFactory = { + name: string; + nameInConfig: string; + version: string; + create: () => ServerBackend; +}; -export async function connect(serverBackendFactory: ServerBackendFactory, transport: Transport, runHeartbeat: boolean) { - const backend = serverBackendFactory(); - const server = createServer(backend, runHeartbeat); +export async function connect(factory: ServerBackendFactory, transport: Transport, runHeartbeat: boolean) { + const server = createServer(factory.name, factory.version, factory.create(), runHeartbeat); await server.connect(transport); } -export function createServer(backend: ServerBackend, runHeartbeat: boolean): Server { - let initializedCallback = () => {}; - const initializedPromise = new Promise(resolve => initializedCallback = resolve); - const server = new mcpBundle.Server({ name: backend.name, version: backend.version }, { +export async function wrapInProcess(backend: ServerBackend): Promise { + const server = createServer('Internal', '0.0.0', backend, false); + return new InProcessTransport(server); +} + +export function createServer(name: string, version: string, backend: ServerBackend, runHeartbeat: boolean): Server { + let initializedPromiseResolve = () => {}; + const initializedPromise = new Promise(resolve => initializedPromiseResolve = resolve); + const server = new mcp.Server({ name, version }, { capabilities: { tools: {}, } }); - server.setRequestHandler(mcpBundle.ListToolsRequestSchema, async () => { + server.setRequestHandler(mcp.ListToolsRequestSchema, async () => { serverDebug('listTools'); await initializedPromise; const tools = await backend.listTools(); @@ -61,7 +71,7 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser }); let heartbeatRunning = false; - server.setRequestHandler(mcpBundle.CallToolRequestSchema, async request => { + server.setRequestHandler(mcp.CallToolRequestSchema, async request => { serverDebug('callTool', request); await initializedPromise; @@ -88,8 +98,8 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser clientRoots = roots; } const clientVersion = server.getClientVersion() ?? { name: 'unknown', version: 'unknown' }; - await backend.initialize?.(clientVersion, clientRoots); - initializedCallback(); + await backend.initialize?.(server, clientVersion, clientRoots); + initializedPromiseResolve(); } catch (e) { errorsDebug(e); } @@ -120,3 +130,27 @@ function addServerListener(server: Server, event: 'close' | 'initialized', liste listener(); }; } + +export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number }) { + if (options.port === undefined) { + await connect(serverBackendFactory, new mcp.StdioServerTransport(), false); + return; + } + + const httpServer = await startHttpServer(options); + await installHttpTransport(httpServer, serverBackendFactory); + const url = httpAddressToString(httpServer.address()); + + const mcpConfig: any = { mcpServers: { } }; + mcpConfig.mcpServers[serverBackendFactory.nameInConfig] = { + url: `${url}/mcp` + }; + const message = [ + `Listening on ${url}`, + 'Put this in your client config:', + JSON.stringify(mcpConfig, undefined, 2), + 'For legacy SSE transport support, you can use the /sse endpoint instead.', + ].join('\n'); + // eslint-disable-next-line no-console + console.error(message); +} diff --git a/packages/playwright/src/mcp/tool.ts b/packages/playwright/src/mcp/sdk/tool.ts similarity index 85% rename from packages/playwright/src/mcp/tool.ts rename to packages/playwright/src/mcp/sdk/tool.ts index 78e39a9a3020b..450170bab3915 100644 --- a/packages/playwright/src/mcp/tool.ts +++ b/packages/playwright/src/mcp/sdk/tool.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import { zodToJsonSchema } from './bundle'; +import { zodToJsonSchema } from './bundle.js'; import type { z } from 'zod'; -import type * as mcpServer from './server'; +import type * as mcpServer from './server.js'; export type ToolSchema = { name: string; title: string; description: string; - type: 'readOnly' | 'destructive'; inputSchema: Input; + type: 'readOnly' | 'destructive'; }; export function toMcpTool(tool: ToolSchema): mcpServer.Tool { @@ -40,3 +40,7 @@ export function toMcpTool(tool: ToolSchema): mcpServer.Tool { }, }; } + +export function defineToolSchema(tool: ToolSchema): ToolSchema { + return tool; +} diff --git a/packages/playwright/src/mcp/test/DEPS.list b/packages/playwright/src/mcp/test/DEPS.list new file mode 100644 index 0000000000000..a24a81a08e8e3 --- /dev/null +++ b/packages/playwright/src/mcp/test/DEPS.list @@ -0,0 +1,12 @@ +[*] +../sdk/ +../../reporters +../../runner +../../transform +../../util +../../common +../../util.ts + + +[backend.ts] +../browser/tools.ts diff --git a/packages/playwright-test-mcp/src/testServerBackend.ts b/packages/playwright/src/mcp/test/backend.ts similarity index 70% rename from packages/playwright-test-mcp/src/testServerBackend.ts rename to packages/playwright/src/mcp/test/backend.ts index c3a8c5fe166ec..3efa3f02740b6 100644 --- a/packages/playwright-test-mcp/src/testServerBackend.ts +++ b/packages/playwright/src/mcp/test/backend.ts @@ -14,14 +14,16 @@ * limitations under the License. */ -import * as mcp from 'playwright/src/mcp/exports'; +import * as mcp from '../sdk/exports.js'; import { Context } from './context'; -import { listTests } from './tools/listTests'; -import { runTests } from './tools/runTests'; +import { listTests } from './listTests'; +import { runTests } from './runTests'; +import { snapshot, pickLocator, evaluate } from '../browser/tools'; -import type { ConfigLocation } from 'playwright/lib/common/config'; +import type { ConfigLocation } from '../../common/config'; import type { Tool } from './tool'; + export class TestServerBackend implements mcp.ServerBackend { readonly name = 'Playwright'; readonly version = '0.0.1'; @@ -32,15 +34,19 @@ export class TestServerBackend implements mcp.ServerBackend { this._context = new Context(resolvedLocation); } - async initialize() { - } - async listTools(): Promise { - return this._tools.map(tool => mcp.toMcpTool(tool.schema)); + return [ + ...this._tools.map(tool => mcp.toMcpTool(tool.schema)), + mcp.toMcpTool(snapshot.schema), + mcp.toMcpTool(pickLocator.schema), + mcp.toMcpTool(evaluate.schema), + ]; } async callTool(name: string, args: mcp.CallToolRequest['params']['arguments']): Promise { - const tool = this._tools.find(tool => tool.schema.name === name)!; + const tool = this._tools.find(tool => tool.schema.name === name); + if (!tool) + throw new Error(`Tool not found: ${name}. Available tools: ${this._tools.map(tool => tool.schema.name).join(', ')}`); const parsedArguments = tool.schema.inputSchema.parse(args || {}); return await tool.handle(this._context!, parsedArguments); } diff --git a/packages/playwright/src/mcp/test/context.ts b/packages/playwright/src/mcp/test/context.ts new file mode 100644 index 0000000000000..fada1fb68dd14 --- /dev/null +++ b/packages/playwright/src/mcp/test/context.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestRunner } from '../../runner/testRunner'; + +import type { ConfigLocation } from '../../common/config'; + +export class Context { + private _testRunner: TestRunner | undefined; + readonly configLocation: ConfigLocation; + + constructor(configLocation: ConfigLocation) { + this.configLocation = configLocation; + } + + async createTestRunner(): Promise { + if (this._testRunner) + await this._testRunner.stopTests(); + const testRunner = new TestRunner(this.configLocation, {}); + await testRunner.initialize({}); + this._testRunner = testRunner; + return testRunner; + } + + async close() { + } +} diff --git a/packages/playwright-test-mcp/src/tools/listTests.ts b/packages/playwright/src/mcp/test/listTests.ts similarity index 92% rename from packages/playwright-test-mcp/src/tools/listTests.ts rename to packages/playwright/src/mcp/test/listTests.ts index 1ebe85de6032f..ab204ea914b8e 100644 --- a/packages/playwright-test-mcp/src/tools/listTests.ts +++ b/packages/playwright/src/mcp/test/listTests.ts @@ -15,8 +15,9 @@ */ import path from 'path'; -import { z } from 'zod'; -import { defineTool } from '../tool.js'; + +import { z } from '../sdk/bundle'; +import { defineTool } from './tool.js'; import type * as reporterTypes from 'playwright/types/testReporter'; @@ -31,7 +32,8 @@ export const listTests = defineTool({ handle: async (context, params) => { const reporter = new ListModeReporter(); - await context.testRunner.listTests(reporter, {}); + const testRunner = await context.createTestRunner(); + await testRunner.listTests(reporter, {}); if (reporter.hasErrors()) throw new Error(reporter.content()); diff --git a/packages/playwright-test-mcp/src/program.ts b/packages/playwright/src/mcp/test/program.ts similarity index 65% rename from packages/playwright-test-mcp/src/program.ts rename to packages/playwright/src/mcp/test/program.ts index 763eb5aa2495d..0d5d0ea48fc2c 100644 --- a/packages/playwright-test-mcp/src/program.ts +++ b/packages/playwright/src/mcp/test/program.ts @@ -15,11 +15,11 @@ */ import path from 'path'; -import { program } from 'commander'; -import { resolveConfigLocation } from 'playwright/lib/common/configLoader'; +import { program } from 'playwright-core/lib/utilsBundle'; -import * as mcp from 'playwright/src/mcp/exports.js'; -import { TestServerBackend } from './testServerBackend.js'; +import { resolveConfigLocation } from '../../common/configLoader'; +import { TestServerBackend } from './backend.js'; +import { runToolsBackend } from '../sdk/mdb'; program .version('Version 0.0.1') @@ -31,8 +31,16 @@ program const resolvedLocation = resolveConfigLocation(options.config); // eslint-disable-next-line no-console console.error('Test config: ', path.relative(process.cwd(), resolvedLocation.resolvedConfigFile ?? resolvedLocation.configDir)); - const serverBackendFactory = () => new TestServerBackend(resolvedLocation); - await mcp.start(serverBackendFactory, options); + const backendFactory = { + name: 'Playwright Test', + nameInConfig: 'playwright-test-mcp', + version: '0.0.0', + create: () => new TestServerBackend(resolvedLocation), + }; + const mdbUrl = await runToolsBackend([backendFactory], { port: 9224 }); + process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP = mdbUrl; + // eslint-disable-next-line no-console + console.error('MCP Listening on: ', mdbUrl); }); -export { program }; +void program.parseAsync(process.argv); diff --git a/packages/playwright-test-mcp/src/tools/runTests.ts b/packages/playwright/src/mcp/test/runTests.ts similarity index 79% rename from packages/playwright-test-mcp/src/tools/runTests.ts rename to packages/playwright/src/mcp/test/runTests.ts index 40fcf860c0fe0..4d75c0bc6db07 100644 --- a/packages/playwright-test-mcp/src/tools/runTests.ts +++ b/packages/playwright/src/mcp/test/runTests.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import { z } from 'zod'; import { noColors } from 'playwright-core/lib/utils'; -import { terminalScreen } from 'playwright/lib/reporters/base'; -import ListReporter from 'playwright/lib/reporters/list'; -import { defineTool } from '../tool'; -import { StringWriteStream } from '../streams'; +import { z } from '../sdk/bundle'; +import { terminalScreen } from '../../reporters/base'; +import ListReporter from '../../reporters/list'; + +import { defineTool } from './tool'; +import { StringWriteStream } from './streams'; export const runTests = defineTool({ schema: { @@ -45,13 +46,16 @@ export const runTests = defineTool({ stdout: stream as unknown as NodeJS.WriteStream, stderr: stream as unknown as NodeJS.WriteStream, }; - const configDir = context.testRunner.configLocation.configDir; + const configDir = context.configLocation.configDir; const reporter = new ListReporter({ configDir, screen }); - const result = await context.testRunner.runTests(reporter, { + const testRunner = await context.createTestRunner(); + const result = await testRunner.runTests(reporter, { testIds: params.tests?.map(test => test.id), + // For automatic recovery + timeout: 0, }); - const text = stream.content(); + const text = stream.content(); return { content: [ { type: 'text', text }, diff --git a/packages/playwright-test-mcp/src/streams.ts b/packages/playwright/src/mcp/test/streams.ts similarity index 100% rename from packages/playwright-test-mcp/src/streams.ts rename to packages/playwright/src/mcp/test/streams.ts diff --git a/packages/playwright-test-mcp/src/tool.ts b/packages/playwright/src/mcp/test/tool.ts similarity index 94% rename from packages/playwright-test-mcp/src/tool.ts rename to packages/playwright/src/mcp/test/tool.ts index 28b05159d4c1b..165c840b02f3a 100644 --- a/packages/playwright-test-mcp/src/tool.ts +++ b/packages/playwright/src/mcp/test/tool.ts @@ -16,7 +16,7 @@ import type { z } from 'zod'; import type { Context } from './context.js'; -import type * as mcp from 'playwright/src/mcp/exports.js'; +import type * as mcp from '../sdk/exports.js'; export type Tool = { schema: mcp.ToolSchema; diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index a2c1774263370..c678043b2b278 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -64,6 +64,7 @@ export type ListTestsParams = { }; export type RunTestsParams = { + timeout?: number; locations?: string[]; grep?: string; grepInvert?: string; @@ -301,6 +302,7 @@ export class TestRunner extends EventEmitter { ...this._configCLIOverrides, repeatEach: 1, retries: 0, + timeout: params.timeout, preserveOutputDir: true, reporter: params.reporters ? params.reporters.map(r => [r]) : undefined, use: { diff --git a/utils/workspace.js b/utils/workspace.js index a03b62f52fa2b..5331dc3335ef7 100755 --- a/utils/workspace.js +++ b/utils/workspace.js @@ -219,11 +219,6 @@ const workspace = new Workspace(ROOT_PATH, [ path: path.join(ROOT_PATH, 'packages', 'playwright-ct-vue'), files: ['LICENSE'], }), - new PWPackage({ - name: '@playwright/test-runner-mcp', - path: path.join(ROOT_PATH, 'packages', 'playwright-test-mcp'), - files: ['LICENSE'], - }), ]); if (require.main === module) { From 82b1cfd3efe9ae0a3744aa52eced7f271e2bfc54 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 20 Aug 2025 20:19:17 -0700 Subject: [PATCH 011/329] chore: remove old recovery, recover from expect errors (#37131) --- .../src/client/channelOwner.ts | 9 ++-- .../src/client/clientInstrumentation.ts | 12 ++--- packages/playwright/src/common/ipc.ts | 10 ----- packages/playwright/src/index.ts | 30 +++++++------ .../src/isomorphic/testServerConnection.ts | 10 ----- .../src/isomorphic/testServerInterface.ts | 10 ----- packages/playwright/src/matchers/DEPS.list | 1 + packages/playwright/src/matchers/expect.ts | 28 +++--------- .../playwright/src/matchers/toBeTruthy.ts | 4 ++ packages/playwright/src/matchers/toEqual.ts | 4 ++ .../playwright/src/matchers/toMatchText.ts | 4 ++ packages/playwright/src/mcp/browser/DEPS.list | 1 + .../playwright/src/mcp/browser/backend.ts | 17 +++++-- packages/playwright/src/mcp/browser/tool.ts | 2 +- packages/playwright/src/mcp/browser/tools.ts | 18 ++------ packages/playwright/src/mcp/sdk/mdb.ts | 12 +++-- packages/playwright/src/mcp/test/program.ts | 2 +- packages/playwright/src/runner/dispatcher.ts | 28 +----------- .../playwright/src/runner/failureTracker.ts | 22 +--------- packages/playwright/src/runner/testRunner.ts | 34 -------------- packages/playwright/src/runner/testServer.ts | 10 +---- packages/playwright/src/runner/watchMode.ts | 44 ------------------- packages/playwright/src/runner/workerHost.ts | 8 +--- packages/playwright/src/worker/testInfo.ts | 35 ++------------- packages/playwright/src/worker/workerMain.ts | 9 +--- 25 files changed, 78 insertions(+), 286 deletions(-) diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index 31adda7d17cf9..26bc4bab7a9a7 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -201,12 +201,9 @@ export abstract class ChannelOwner Promise; +export type RecoverFromApiErrorHandler = () => Promise; export interface ClientInstrumentation { addListener(listener: ClientInstrumentationListener): void; removeListener(listener: ClientInstrumentationListener): void; removeAllListeners(): void; onApiCallBegin(apiCall: ApiCallData, channel: { type: string, method: string, params?: Record }): void; - onApiCallRecovery(apiCall: ApiCallData, error: Error, recoveryHandlers: RecoverFromApiErrorHandler[]): void; + onApiCallRecovery(apiCall: ApiCallData, error: Error, channelOwner: ChannelOwner, recoveryHandlers: RecoverFromApiErrorHandler[]): void; onApiCallEnd(apiCall: ApiCallData): void; onWillPause(options: { keepTestTimeout: boolean }): void; @@ -52,7 +48,7 @@ export interface ClientInstrumentation { export interface ClientInstrumentationListener { onApiCallBegin?(apiCall: ApiCallData, channel: { type: string, method: string, params?: Record }): void; - onApiCallRecovery?(apiCall: ApiCallData, error: Error, recoveryHandlers: RecoverFromApiErrorHandler[]): void; + onApiCallRecovery?(apiCall: ApiCallData, error: Error, channelOwner: ChannelOwner, recoveryHandlers: RecoverFromApiErrorHandler[]): void; onApiCallEnd?(apiCall: ApiCallData): void; onWillPause?(options: { keepTestTimeout: boolean }): void; diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts index e4645d1c69948..806aed053292b 100644 --- a/packages/playwright/src/common/ipc.ts +++ b/packages/playwright/src/common/ipc.ts @@ -21,7 +21,6 @@ import { serializeCompilationCache } from '../transform/compilationCache'; import type { ConfigLocation, FullConfigInternal } from './config'; import type { ReporterDescription, TestInfoError, TestStatus } from '../../types/test'; import type { SerializedCompilationCache } from '../transform/compilationCache'; -import type { RecoverFromStepErrorResult } from '@testIsomorphic/testServerInterface'; export type ConfigCLIOverrides = { debug?: boolean; @@ -67,7 +66,6 @@ export type WorkerInitParams = { projectId: string; config: SerializedConfig; artifactsDir: string; - recoverFromStepErrors: boolean; }; export type TestBeginPayload = { @@ -107,14 +105,6 @@ export type StepBeginPayload = { location?: { file: string, line: number, column: number }; }; -export type StepRecoverFromErrorPayload = { - testId: string; - stepId: string; - error: TestInfoErrorImpl; -}; - -export type ResumeAfterStepErrorPayload = RecoverFromStepErrorResult; - export type StepEndPayload = { testId: string; stepId: string; diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index ef94853fc0d03..d8fed58ca4c2e 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -22,8 +22,7 @@ import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringif import { currentTestInfo } from './common/globals'; import { rootTestType } from './common/testType'; -import { BrowserBackend } from './mcp/browser/backend'; -import { runOnPauseBackendLoop } from './mcp/sdk/mdb'; +import { runBrowserBackendOnError } from './mcp/browser/backend'; import { codeFrameColumns } from './transform/babelBundle'; import { stripAnsiEscapes } from './util'; @@ -37,6 +36,7 @@ import type { BrowserContext as BrowserContextImpl } from '../../playwright-core import type { APIRequestContext as APIRequestContextImpl } from '../../playwright-core/src/client/fetch'; import type { ChannelOwner } from '../../playwright-core/src/client/channelOwner'; import type { Page as PageImpl } from '../../playwright-core/src/client/page'; +import type { Frame as FrameImpl } from '../../playwright-core/src/client/frame'; import type { BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing } from 'playwright-core'; export { expect } from './matchers/expect'; @@ -290,24 +290,21 @@ const playwrightFixtures: Fixtures = ({ if (data.apiName === 'tracing.group') tracingGroupSteps.push(step); }, - onApiCallRecovery: (data, error, recoveryHandlers) => { - if (!process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP) - return; - + onApiCallRecovery: (data, error, channelOwner, recoveryHandlers) => { const step = data.userData; if (!step) return; - - const code = createErrorCodeframe(error.message, step.location); - + const page = channelToPage(channelOwner); + if (!page) + return; recoveryHandlers.push(async () => { - const [context] = playwright._allContexts(); - const introMessage = `Paused on error:\n\n${stripAnsiEscapes(code)}\n\nTry recovering from the error prior to continuing.`; - await runOnPauseBackendLoop(process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP!, new BrowserBackend(context), introMessage); - return { status: 'recovered' }; + await runBrowserBackendOnError(page, () => { + return stripAnsiEscapes(createErrorCodeframe(error.message, step.location)); + }); }); }, onApiCallEnd: data => { + // "tracing.group" step will end later, when "tracing.groupEnd" finishes. if (data.apiName === 'tracing.group') return; @@ -802,6 +799,13 @@ export { defineConfig } from './common/configLoader'; export { mergeTests } from './common/testType'; export { mergeExpects } from './matchers/expect'; +function channelToPage(channelOwner: ChannelOwner): Page | undefined { + if (channelOwner._type === 'Page') + return channelOwner as PageImpl; + if (channelOwner._type === 'Frame') + return (channelOwner as FrameImpl).page(); + return undefined; +} function createErrorCodeframe(message: string, location: Location) { let source: string; diff --git a/packages/playwright/src/isomorphic/testServerConnection.ts b/packages/playwright/src/isomorphic/testServerConnection.ts index 76c383bd36f76..57a416a6042e8 100644 --- a/packages/playwright/src/isomorphic/testServerConnection.ts +++ b/packages/playwright/src/isomorphic/testServerConnection.ts @@ -17,7 +17,6 @@ import * as events from './events'; import type { TestServerInterface, TestServerInterfaceEvents } from '@testIsomorphic/testServerInterface'; -import type * as reporterTypes from '../../types/testReporter'; // -- Reuse boundary -- Everything below this line is reused in the vscode extension. @@ -69,14 +68,12 @@ export class TestServerConnection implements TestServerInterface, TestServerInte readonly onStdio: events.Event<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>; readonly onTestFilesChanged: events.Event<{ testFiles: string[] }>; readonly onLoadTraceRequested: events.Event<{ traceUrl: string }>; - readonly onRecoverFromStepError: events.Event<{ stepId: string, message: string, location: reporterTypes.Location }>; private _onCloseEmitter = new events.EventEmitter(); private _onReportEmitter = new events.EventEmitter(); private _onStdioEmitter = new events.EventEmitter<{ type: 'stderr' | 'stdout'; text?: string | undefined; buffer?: string | undefined; }>(); private _onTestFilesChangedEmitter = new events.EventEmitter<{ testFiles: string[] }>(); private _onLoadTraceRequestedEmitter = new events.EventEmitter<{ traceUrl: string }>(); - private _onRecoverFromStepErrorEmitter = new events.EventEmitter<{ stepId: string, message: string, location: reporterTypes.Location }>(); private _lastId = 0; private _transport: TestServerTransport; @@ -90,7 +87,6 @@ export class TestServerConnection implements TestServerInterface, TestServerInte this.onStdio = this._onStdioEmitter.event; this.onTestFilesChanged = this._onTestFilesChangedEmitter.event; this.onLoadTraceRequested = this._onLoadTraceRequestedEmitter.event; - this.onRecoverFromStepError = this._onRecoverFromStepErrorEmitter.event; this._transport = transport; this._transport.onmessage(data => { @@ -151,8 +147,6 @@ export class TestServerConnection implements TestServerInterface, TestServerInte this._onTestFilesChangedEmitter.fire(params); else if (method === 'loadTraceRequested') this._onLoadTraceRequestedEmitter.fire(params); - else if (method === 'recoverFromStepError') - this._onRecoverFromStepErrorEmitter.fire(params); } async initialize(params: Parameters[0]): ReturnType { @@ -247,10 +241,6 @@ export class TestServerConnection implements TestServerInterface, TestServerInte await this._sendMessage('closeGracefully', params); } - async resumeAfterStepError(params: Parameters[0]): Promise { - await this._sendMessage('resumeAfterStepError', params); - } - close() { try { this._transport.close(); diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 461b9230b5349..66136bab2e386 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -22,12 +22,6 @@ import type * as reporterTypes from '../../types/testReporter'; export type ReportEntry = JsonEvent; -export type RecoverFromStepErrorResult = { - stepId: string; - status: 'recovered' | 'failed'; - value?: string | number | boolean | undefined; -}; - export interface TestServerInterface { initialize(params: { serializer?: string, @@ -35,7 +29,6 @@ export interface TestServerInterface { interceptStdio?: boolean, watchTestDirs?: boolean, populateDependenciesOnList?: boolean, - recoverFromStepErrors?: boolean, }): Promise; ping(params: {}): Promise; @@ -120,8 +113,6 @@ export interface TestServerInterface { stopTests(params: {}): Promise; closeGracefully(params: {}): Promise; - - resumeAfterStepError(params: RecoverFromStepErrorResult): Promise; } export interface TestServerInterfaceEvents { @@ -136,5 +127,4 @@ export interface TestServerInterfaceEventEmitters { dispatchEvent(event: 'stdio', params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }): void; dispatchEvent(event: 'testFilesChanged', params: { testFiles: string[] }): void; dispatchEvent(event: 'loadTraceRequested', params: { traceUrl: string }): void; - dispatchEvent(event: 'recoverFromStepError', params: { stepId: string, message: string, location: reporterTypes.Location }): void; } diff --git a/packages/playwright/src/matchers/DEPS.list b/packages/playwright/src/matchers/DEPS.list index de39c6b5453af..978b4038f5f8b 100644 --- a/packages/playwright/src/matchers/DEPS.list +++ b/packages/playwright/src/matchers/DEPS.list @@ -1,5 +1,6 @@ [*] ../common/ +../mcp/browser/backend.ts ../util.ts ../utilsBundle.ts ../worker/testInfo.ts diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index b6e3c0dc2eb92..97261c40bb9a5 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -365,7 +365,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { const step = testInfo._addStep(stepInfo); - const reportStepError = (isAsync: boolean, e: Error | unknown) => { + const reportStepError = (e: Error | unknown) => { const jestError = isJestError(e) ? e : null; const expectError = jestError ? new ExpectError(jestError, customMessage, stackFrames) : undefined; if (jestError?.matcherResult.suggestedRebaseline) { @@ -378,24 +378,10 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { const error = expectError ?? e; step.complete({ error }); - if (!isAsync || !expectError) { - if (this._info.isSoft) - testInfo._failWithError(error); - else - throw error; - return; - } - - // Recoverable async failure. - return (async () => { - const recoveryResult = await step.recoverFromStepError(expectError); - if (recoveryResult.status === 'recovered') - return recoveryResult.value as any; - if (this._info.isSoft) - testInfo._failWithError(expectError); - else - throw expectError; - })(); + if (this._info.isSoft) + testInfo._failWithError(error); + else + throw error; }; const finalizer = () => { @@ -407,11 +393,11 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { const callback = () => matcher.call(target, ...args); const result = currentZone().with('stepZone', step).run(callback); if (result instanceof Promise) - return result.then(finalizer).catch(reportStepError.bind(null, true)); + return result.then(finalizer).catch(reportStepError); finalizer(); return result; } catch (e) { - void reportStepError(false, e); + void reportStepError(e); } }; } diff --git a/packages/playwright/src/matchers/toBeTruthy.ts b/packages/playwright/src/matchers/toBeTruthy.ts index 0c2364c71fce1..6f627c994c93b 100644 --- a/packages/playwright/src/matchers/toBeTruthy.ts +++ b/packages/playwright/src/matchers/toBeTruthy.ts @@ -16,6 +16,7 @@ import { callLogText, expectTypes } from '../util'; import { kNoElementsFoundError, matcherHint } from './matcherHint'; +import { runBrowserBackendOnError } from '../mcp/browser/backend'; import type { MatcherResult } from './matcherHint'; import type { ExpectMatcherState } from '../../types/test'; @@ -64,6 +65,9 @@ export async function toBeTruthy( const logText = callLogText(log); return `${header}${logText}`; }; + + await runBrowserBackendOnError(receiver.page(), message); + return { message, pass, diff --git a/packages/playwright/src/matchers/toEqual.ts b/packages/playwright/src/matchers/toEqual.ts index bfa1e6e04a62e..c188de257b266 100644 --- a/packages/playwright/src/matchers/toEqual.ts +++ b/packages/playwright/src/matchers/toEqual.ts @@ -18,6 +18,7 @@ import { isRegExp } from 'playwright-core/lib/utils'; import { callLogText, expectTypes } from '../util'; import { matcherHint } from './matcherHint'; +import { runBrowserBackendOnError } from '../mcp/browser/backend'; import type { MatcherResult } from './matcherHint'; import type { ExpectMatcherState } from '../../types/test'; @@ -92,6 +93,9 @@ export async function toEqual( const header = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined, details, messagePreventExtraStatIndent); return `${header}${callLogText(log)}`; }; + + await runBrowserBackendOnError(receiver.page(), message); + // Passing the actual and expected objects so that a custom reporter // could access them, for example in order to display a custom visual diff, // or create a different error message diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index 0320d14f99682..712f8361a8aab 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -24,6 +24,7 @@ import { } from './expect'; import { kNoElementsFoundError, matcherHint } from './matcherHint'; import { EXPECTED_COLOR } from '../common/expectBundle'; +import { runBrowserBackendOnError } from '../mcp/browser/backend'; import type { MatcherResult } from './matcherHint'; import type { ExpectMatcherState } from '../../types/test'; @@ -112,6 +113,9 @@ export async function toMatchText( return hints + callLogText(log); }; + if (receiverType === 'Locator') + await runBrowserBackendOnError((receiver as Locator).page(), message); + return { name: matcherName, expected, diff --git a/packages/playwright/src/mcp/browser/DEPS.list b/packages/playwright/src/mcp/browser/DEPS.list index a9266a844f2e8..9bdbc0d02a5f8 100644 --- a/packages/playwright/src/mcp/browser/DEPS.list +++ b/packages/playwright/src/mcp/browser/DEPS.list @@ -1,2 +1,3 @@ [*] ../sdk/ +../../util.ts diff --git a/packages/playwright/src/mcp/browser/backend.ts b/packages/playwright/src/mcp/browser/backend.ts index 5fe672cec2920..5e2b87c43cec6 100644 --- a/packages/playwright/src/mcp/browser/backend.ts +++ b/packages/playwright/src/mcp/browser/backend.ts @@ -18,6 +18,8 @@ import * as mcp from '../sdk/exports'; import * as mcpBundle from '../sdk/bundle'; import { snapshot, pickLocator, evaluate } from './tools'; import { defineToolSchema } from '../sdk/exports'; +import { runOnPauseBackendLoop } from '../sdk/mdb'; +import { stripAnsiEscapes } from '../../util'; import type { Tool } from './tool'; import type * as playwright from '../../../index'; @@ -27,10 +29,10 @@ export class BrowserBackend implements ServerBackendOnPause { readonly name = 'Playwright'; readonly version = '0.0.1'; private _tools: Tool[] = [snapshot, pickLocator, evaluate]; - private _context: playwright.BrowserContext; + private _page: playwright.Page; - constructor(context: playwright.BrowserContext) { - this._context = context; + constructor(page: playwright.Page) { + this._page = page; } async initialize() { @@ -52,7 +54,7 @@ export class BrowserBackend implements ServerBackendOnPause { if (!tool) throw new Error(`Tool not found: ${name}. Available tools: ${this._tools.map(tool => tool.schema.name).join(', ')}`); const parsedArguments = tool.schema.inputSchema.parse(args || {}); - return await tool.handle(this._context, parsedArguments); + return await tool.handle(this._page, parsedArguments); } } @@ -63,3 +65,10 @@ const doneToolSchema = defineToolSchema({ inputSchema: mcpBundle.z.object({}), type: 'destructive', }); + +export async function runBrowserBackendOnError(page: playwright.Page, message: () => string) { + if (!process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP) + return; + const introMessage = `Paused on error:\n\n${stripAnsiEscapes(message())}\n\nTry recovering from the error prior to continuing.`; + await runOnPauseBackendLoop(process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP!, new BrowserBackend(page), introMessage); +} diff --git a/packages/playwright/src/mcp/browser/tool.ts b/packages/playwright/src/mcp/browser/tool.ts index 1291ddc97c552..5b09b17480c89 100644 --- a/packages/playwright/src/mcp/browser/tool.ts +++ b/packages/playwright/src/mcp/browser/tool.ts @@ -20,7 +20,7 @@ import type * as playwright from '../../../index'; export type Tool = { schema: mcp.ToolSchema; - handle: (context: playwright.BrowserContext, params: z.output) => Promise; + handle: (page: playwright.Page, params: z.output) => Promise; }; export function defineTool(tool: Tool): Tool { diff --git a/packages/playwright/src/mcp/browser/tools.ts b/packages/playwright/src/mcp/browser/tools.ts index 65a6f377a1a57..ee00bd62be385 100644 --- a/packages/playwright/src/mcp/browser/tools.ts +++ b/packages/playwright/src/mcp/browser/tools.ts @@ -35,11 +35,7 @@ export const snapshot = defineTool({ type: 'readOnly', }, - handle: async (context, params) => { - const [page] = context.pages(); - if (!page) - throw new Error('No open pages available'); - + handle: async (page, params) => { const snapshot = await (page as PageEx)._snapshotForAI(); return { content: [ @@ -66,11 +62,7 @@ export const pickLocator = defineTool({ type: 'readOnly', }, - handle: async (context, params) => { - const [page] = context.pages(); - if (!page) - throw new Error('No open pages available'); - + handle: async (page, params) => { const locator = await refLocator(page, params); const locatorString = await generateLocator(locator); return { @@ -100,11 +92,7 @@ export const evaluate = defineTool({ type: 'destructive', }, - handle: async (context, params) => { - const [page] = context.pages(); - if (!page) - throw new Error('No open pages available'); - + handle: async (page, params) => { if (params.ref && params.element) { const locator = await refLocator(page, { ref: params.ref, element: params.element }); const result = await locator.evaluate(params.function); diff --git a/packages/playwright/src/mcp/sdk/mdb.ts b/packages/playwright/src/mcp/sdk/mdb.ts index 3216e4366b1c3..b446587d5e085 100644 --- a/packages/playwright/src/mcp/sdk/mdb.ts +++ b/packages/playwright/src/mcp/sdk/mdb.ts @@ -121,7 +121,7 @@ export type ServerBackendOnPause = mcpServer.ServerBackend & { requestSelfDestruct?: () => void; }; -export async function runToolsBackend(backendFactories: mcpServer.ServerBackendFactory[], options: { port: number }): Promise { +export async function runToolsBackend(backendFactory: mcpServer.ServerBackendFactory, options: { port: number }): Promise { const mdbBackend = new MDBBackend(); const mdbBackendFactory = { name: 'Playwright MDB', @@ -132,12 +132,10 @@ export async function runToolsBackend(backendFactories: mcpServer.ServerBackendF const mdbUrl = await startAsHttp(mdbBackendFactory, options); - for (const backendFactory of backendFactories) { - const backendUrl = await startAsHttp(backendFactory, { port: 0 }); - const result = await callTool(mdbUrl, pushToolsSchema.name, { mcpUrl: backendUrl }); - if (result.isError) - errorsDebug('Failed to push tools', result.content); - } + const backendUrl = await startAsHttp(backendFactory, { port: 0 }); + const result = await callTool(mdbUrl, pushToolsSchema.name, { mcpUrl: backendUrl }); + if (result.isError) + errorsDebug('Failed to push tools', result.content); return mdbUrl; } diff --git a/packages/playwright/src/mcp/test/program.ts b/packages/playwright/src/mcp/test/program.ts index 0d5d0ea48fc2c..4eecec8b906db 100644 --- a/packages/playwright/src/mcp/test/program.ts +++ b/packages/playwright/src/mcp/test/program.ts @@ -37,7 +37,7 @@ program version: '0.0.0', create: () => new TestServerBackend(resolvedLocation), }; - const mdbUrl = await runToolsBackend([backendFactory], { port: 9224 }); + const mdbUrl = await runToolsBackend(backendFactory, { port: 9224 }); process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP = mdbUrl; // eslint-disable-next-line no-console console.error('MCP Listening on: ', mdbUrl); diff --git a/packages/playwright/src/runner/dispatcher.ts b/packages/playwright/src/runner/dispatcher.ts index 5e0525e5410f0..22293b2a9d994 100644 --- a/packages/playwright/src/runner/dispatcher.ts +++ b/packages/playwright/src/runner/dispatcher.ts @@ -26,12 +26,11 @@ import type { ProcessExitData } from './processHost'; import type { TestGroup } from './testGroups'; import type { TestError, TestResult, TestStep } from '../../types/testReporter'; import type { FullConfigInternal } from '../common/config'; -import type { AttachmentPayload, DonePayload, RunPayload, SerializedConfig, StepBeginPayload, StepEndPayload, StepRecoverFromErrorPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestOutputPayload } from '../common/ipc'; +import type { AttachmentPayload, DonePayload, RunPayload, SerializedConfig, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestOutputPayload } from '../common/ipc'; import type { Suite } from '../common/test'; import type { TestCase } from '../common/test'; import type { ReporterV2 } from '../reporters/reporterV2'; import type { RegisteredListener } from 'playwright-core/lib/utils'; -import type { RecoverFromStepErrorResult } from '@testIsomorphic/testServerInterface'; export type EnvByProjectId = Map>; @@ -219,8 +218,7 @@ export class Dispatcher { _createWorker(testGroup: TestGroup, parallelIndex: number, loaderData: SerializedConfig) { const projectConfig = this._config.projects.find(p => p.id === testGroup.projectId)!; const outputDir = projectConfig.project.outputDir; - const recoverFromStepErrors = this._failureTracker.canRecoverFromStepError(); - const worker = new WorkerHost(testGroup, parallelIndex, loaderData, recoverFromStepErrors, this._extraEnvByProjectId.get(testGroup.projectId) || {}, outputDir); + const worker = new WorkerHost(testGroup, parallelIndex, loaderData, this._extraEnvByProjectId.get(testGroup.projectId) || {}, outputDir); const handleOutput = (params: TestOutputPayload) => { const chunk = chunkFromParams(params); if (worker.didFail()) { @@ -398,26 +396,6 @@ class JobDispatcher { this._reporter.onStepEnd?.(test, result, step); } - private _onStepRecoverFromError(resumeAfterStepError: (result: RecoverFromStepErrorResult) => void, params: StepRecoverFromErrorPayload) { - const data = this._dataByTestId.get(params.testId); - if (!data) { - resumeAfterStepError({ stepId: params.stepId, status: 'failed' }); - return; - } - const { steps } = data; - const step = steps.get(params.stepId); - if (!step) { - resumeAfterStepError({ stepId: params.stepId, status: 'failed' }); - return; - } - - const testError: TestError = { - ...params.error, - location: step.location, - }; - this._failureTracker.recoverFromStepError(params.stepId, testError, resumeAfterStepError); - } - private _onAttach(params: AttachmentPayload) { const data = this._dataByTestId.get(params.testId)!; if (!data) { @@ -582,14 +560,12 @@ class JobDispatcher { }), }; worker.runTestGroup(runPayload); - const resumeAfterStepError = worker.resumeAfterStepError.bind(worker); this._listeners = [ eventsHelper.addEventListener(worker, 'testBegin', this._onTestBegin.bind(this)), eventsHelper.addEventListener(worker, 'testEnd', this._onTestEnd.bind(this)), eventsHelper.addEventListener(worker, 'stepBegin', this._onStepBegin.bind(this)), eventsHelper.addEventListener(worker, 'stepEnd', this._onStepEnd.bind(this)), - eventsHelper.addEventListener(worker, 'stepRecoverFromError', this._onStepRecoverFromError.bind(this, resumeAfterStepError)), eventsHelper.addEventListener(worker, 'attach', this._onAttach.bind(this)), eventsHelper.addEventListener(worker, 'done', this._onDone.bind(this)), eventsHelper.addEventListener(worker, 'exit', this.onExit.bind(this)), diff --git a/packages/playwright/src/runner/failureTracker.ts b/packages/playwright/src/runner/failureTracker.ts index 42481e06ca2ff..dd8ce505876dc 100644 --- a/packages/playwright/src/runner/failureTracker.ts +++ b/packages/playwright/src/runner/failureTracker.ts @@ -14,30 +14,18 @@ * limitations under the License. */ -import type { RecoverFromStepErrorResult } from '@testIsomorphic/testServerInterface'; -import type { TestResult, TestError } from '../../types/testReporter'; +import type { TestResult } from '../../types/testReporter'; import type { FullConfigInternal } from '../common/config'; import type { Suite, TestCase } from '../common/test'; -export type RecoverFromStepErrorHandler = (stepId: string, error: TestError) => Promise; - export class FailureTracker { private _failureCount = 0; private _hasWorkerErrors = false; private _rootSuite: Suite | undefined; - private _recoverFromStepErrorHandler: RecoverFromStepErrorHandler | undefined; constructor(private _config: FullConfigInternal) { } - canRecoverFromStepError(): boolean { - return !!this._recoverFromStepErrorHandler; - } - - setRecoverFromStepErrorHandler(recoverFromStepErrorHandler: RecoverFromStepErrorHandler) { - this._recoverFromStepErrorHandler = recoverFromStepErrorHandler; - } - onRootSuite(rootSuite: Suite) { this._rootSuite = rootSuite; } @@ -48,14 +36,6 @@ export class FailureTracker { ++this._failureCount; } - recoverFromStepError(stepId: string, error: TestError, resumeAfterStepError: (result: RecoverFromStepErrorResult) => void) { - if (!this._recoverFromStepErrorHandler) { - resumeAfterStepError({ stepId, status: 'failed' }); - return; - } - void this._recoverFromStepErrorHandler(stepId, error).then(resumeAfterStepError).catch(() => {}); - } - onWorkerError() { this._hasWorkerErrors = true; } diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index c678043b2b278..b973a8852be1c 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -40,20 +40,12 @@ import type { ConfigCLIOverrides } from '../common/ipc'; import type { TestRunnerPluginRegistration } from '../plugins'; import type { AnyReporter } from '../reporters/reporterV2'; -export type RecoverFromStepErrorResult = { - stepId: string; - status: 'recovered' | 'failed'; - value?: string | number | boolean | undefined; -}; - export const TestRunnerEvent = { TestFilesChanged: 'testFilesChanged', - RecoverFromStepError: 'recoverFromStepError', } as const; export type TestRunnerEventMap = { [TestRunnerEvent.TestFilesChanged]: [testFiles: string[]]; - [TestRunnerEvent.RecoverFromStepError]: [stepId: string, message: string, location: reporterTypes.Location]; }; export type ListTestsParams = { @@ -99,8 +91,6 @@ export class TestRunner extends EventEmitter { private _plugins: TestRunnerPluginRegistration[] | undefined; private _watchTestDirs = false; private _populateDependenciesOnList = false; - private _recoverFromStepErrors = false; - private _resumeAfterStepErrors: Map> = new Map(); constructor(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides) { super(); @@ -116,11 +106,9 @@ export class TestRunner extends EventEmitter { async initialize(params: { watchTestDirs?: boolean; populateDependenciesOnList?: boolean; - recoverFromStepErrors?: boolean; }) { this._watchTestDirs = !!params.watchTestDirs; this._populateDependenciesOnList = !!params.populateDependenciesOnList; - this._recoverFromStepErrors = !!params.recoverFromStepErrors; } resizeTerminal(params: { cols: number, rows: number }) { @@ -348,7 +336,6 @@ export class TestRunner extends EventEmitter { ...createRunTestsTasks(config), ]; const testRun = new TestRun(config, reporter); - testRun.failureTracker.setRecoverFromStepErrorHandler(this._recoverFromStepError.bind(this)); const run = runTasks(testRun, tasks, 0, stop).then(async status => { this._testRun = undefined; return status; @@ -357,26 +344,6 @@ export class TestRunner extends EventEmitter { return { status: await run }; } - private async _recoverFromStepError(stepId: string, error: reporterTypes.TestError): Promise { - if (!this._recoverFromStepErrors) - return { stepId, status: 'failed' }; - const recoveryPromise = new ManualPromise(); - this._resumeAfterStepErrors.set(stepId, recoveryPromise); - if (!error?.message || !error?.location) - return { stepId, status: 'failed' }; - this.emit(TestRunnerEvent.RecoverFromStepError, stepId, error.message, error.location); - const recoveredResult = await recoveryPromise; - if (recoveredResult.stepId !== stepId) - return { stepId, status: 'failed' }; - return recoveredResult; - } - - async resumeAfterStepError(params: RecoverFromStepErrorResult): Promise { - const recoveryPromise = this._resumeAfterStepErrors.get(params.stepId); - if (recoveryPromise) - recoveryPromise.resolve(params); - } - async watch(fileNames: string[]) { this._watchedTestDependencies = new Set(); for (const fileName of fileNames) { @@ -404,7 +371,6 @@ export class TestRunner extends EventEmitter { async stopTests() { this._testRun?.stop?.resolve(); await this._testRun?.run; - this._resumeAfterStepErrors.clear(); } async closeGracefully() { diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 9461e20e05f25..4f8758623aabe 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -31,7 +31,7 @@ import type { HttpServer, Transport } from 'playwright-core/lib/utils'; import type * as reporterTypes from '../../types/testReporter'; import type { ConfigLocation } from '../common/config'; import type { ConfigCLIOverrides } from '../common/ipc'; -import type { RecoverFromStepErrorResult, ReportEntry, TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface'; +import type { ReportEntry, TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface'; import type { ReporterV2 } from '../reporters/reporterV2'; const originalDebugLog = debug.log; @@ -63,12 +63,10 @@ class TestServer { export const TestRunnerEvent = { TestFilesChanged: 'testFilesChanged', - RecoverFromStepError: 'recoverFromStepError', } as const; export type TestRunnerEventMap = { [TestRunnerEvent.TestFilesChanged]: [testFiles: string[]]; - [TestRunnerEvent.RecoverFromStepError]: [stepId: string, message: string, location: reporterTypes.Location]; }; export type ListTestsParams = { @@ -117,7 +115,6 @@ export class TestServerDispatcher implements TestServerInterface { this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params); this._testRunner.on(TestRunnerEvent.TestFilesChanged, testFiles => this._dispatchEvent('testFilesChanged', { testFiles })); - this._testRunner.on(TestRunnerEvent.RecoverFromStepError, (stepId, message, location) => this._dispatchEvent('recoverFromStepError', { stepId, message, location })); } private async _wireReporter(messageSink: (message: any) => void) { @@ -140,7 +137,6 @@ export class TestServerDispatcher implements TestServerInterface { await this._testRunner.initialize({ watchTestDirs: !!params.watchTestDirs, populateDependenciesOnList: !!params.populateDependenciesOnList, - recoverFromStepErrors: !!params.recoverFromStepErrors, }); } @@ -218,10 +214,6 @@ export class TestServerDispatcher implements TestServerInterface { return { status }; } - async resumeAfterStepError(params: RecoverFromStepErrorResult): Promise { - await this._testRunner.resumeAfterStepError(params); - } - async watch(params: { fileNames: string[]; }) { await this._testRunner.watch(params.fileNames); } diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts index 6b3c7b405db34..7915405bd964e 100644 --- a/packages/playwright/src/runner/watchMode.ts +++ b/packages/playwright/src/runner/watchMode.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import fs from 'fs'; import path from 'path'; import readline from 'readline'; import { EventEmitter } from 'stream'; @@ -28,8 +27,6 @@ import { enquirer } from '../utilsBundle'; import { TestServerDispatcher } from './testServer'; import { TeleSuiteUpdater } from '../isomorphic/teleSuiteUpdater'; import { TestServerConnection } from '../isomorphic/testServerConnection'; -import { stripAnsiEscapes } from '../util'; -import { codeFrameColumns } from '../transform/babelBundle'; import type * as reporterTypes from '../../types/testReporter'; import type { ConfigLocation } from '../common/config'; @@ -131,27 +128,11 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp }); }); testServerConnection.onReport(report => teleSuiteUpdater.processTestReportEvent(report)); - testServerConnection.onRecoverFromStepError(({ stepId, message, location }) => { - process.stdout.write(`\nTest error occurred.\n`); - process.stdout.write('\n' + createErrorCodeframe(message, location) + '\n'); - process.stdout.write(`\n${colors.dim('Try recovering from the error. Press')} ${colors.bold('c')} ${colors.dim('to continue or')} ${colors.bold('t')} ${colors.dim('to throw the error')}\n`); - readKeyPress(text => { - if (text === 'c') { - process.stdout.write(`\n${colors.dim('Continuing after recovery...')}\n`); - testServerConnection.resumeAfterStepError({ stepId, status: 'recovered', value: undefined }).catch(() => {}); - } else if (text === 't') { - process.stdout.write(`\n${colors.dim('Throwing error...')}\n`); - testServerConnection.resumeAfterStepError({ stepId, status: 'failed' }).catch(() => {}); - } - return text; - }); - }); await testServerConnection.initialize({ interceptStdio: false, watchTestDirs: true, populateDependenciesOnList: true, - recoverFromStepErrors: !process.env.PWTEST_RECOVERY_DISABLED, }); await testServerConnection.runGlobalSetup({}); @@ -450,28 +431,3 @@ async function toggleShowBrowser() { } type Command = 'run' | 'failed' | 'repeat' | 'changed' | 'project' | 'file' | 'grep' | 'exit' | 'interrupted' | 'toggle-show-browser' | 'toggle-buffer-mode'; - -function createErrorCodeframe(message: string, location: reporterTypes.Location) { - let source: string; - try { - source = fs.readFileSync(location.file, 'utf-8') + '\n//'; - } catch (e) { - return; - } - - return codeFrameColumns( - source, - { - start: { - line: location.line, - column: location.column, - }, - }, - { - highlightCode: true, - linesAbove: 5, - linesBelow: 5, - message: stripAnsiEscapes(message).split('\n')[0] || undefined, - } - ); -} diff --git a/packages/playwright/src/runner/workerHost.ts b/packages/playwright/src/runner/workerHost.ts index 3879971ef0d1f..9d695063300e3 100644 --- a/packages/playwright/src/runner/workerHost.ts +++ b/packages/playwright/src/runner/workerHost.ts @@ -25,7 +25,6 @@ import { artifactsFolderName } from '../isomorphic/folders'; import type { TestGroup } from './testGroups'; import type { RunPayload, SerializedConfig, WorkerInitParams } from '../common/ipc'; -import type { RecoverFromStepErrorResult } from '@testIsomorphic/testServerInterface'; let lastWorkerIndex = 0; @@ -37,7 +36,7 @@ export class WorkerHost extends ProcessHost { private _params: WorkerInitParams; private _didFail = false; - constructor(testGroup: TestGroup, parallelIndex: number, config: SerializedConfig, recoverFromStepErrors: boolean, extraEnv: Record, outputDir: string) { + constructor(testGroup: TestGroup, parallelIndex: number, config: SerializedConfig, extraEnv: Record, outputDir: string) { const workerIndex = lastWorkerIndex++; super(require.resolve('../worker/workerMain.js'), `worker-${workerIndex}`, { ...extraEnv, @@ -55,7 +54,6 @@ export class WorkerHost extends ProcessHost { projectId: testGroup.projectId, config, artifactsDir: path.join(outputDir, artifactsFolderName(workerIndex)), - recoverFromStepErrors, }; } @@ -81,10 +79,6 @@ export class WorkerHost extends ProcessHost { this.sendMessageNoReply({ method: 'runTestGroup', params: runPayload }); } - resumeAfterStepError(result: RecoverFromStepErrorResult) { - this.sendMessageNoReply({ method: 'resumeAfterStepError', params: result }); - } - hash() { return this._hash; } diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 4e7a0d4e2b6e7..fa71ac1b15692 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -17,10 +17,10 @@ import fs from 'fs'; import path from 'path'; -import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone, createGuid, escapeWithQuotes, ManualPromise } from 'playwright-core/lib/utils'; +import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone, createGuid, escapeWithQuotes } from 'playwright-core/lib/utils'; import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager'; -import { addSuffixToFilePath, filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, serializeError, trimLongString, windowsFilesystemFriendlyLength } from '../util'; +import { addSuffixToFilePath, filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, trimLongString, windowsFilesystemFriendlyLength } from '../util'; import { TestTracing } from './testTracing'; import { testInfoError } from './util'; import { wrapFunctionWithLocation } from '../transform/transform'; @@ -29,10 +29,9 @@ import type { RunnableDescription } from './timeoutManager'; import type { FullProject, TestInfo, TestStatus, TestStepInfo, TestAnnotation } from '../../types/test'; import type { FullConfig, Location } from '../../types/testReporter'; import type { FullConfigInternal, FullProjectInternal } from '../common/config'; -import type { AttachmentPayload, ResumeAfterStepErrorPayload, StepBeginPayload, StepEndPayload, StepRecoverFromErrorPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc'; +import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc'; import type { TestCase } from '../common/test'; import type { StackFrame } from '@protocol/channels'; -import type { RecoverFromStepErrorResult } from '@testIsomorphic/testServerInterface'; export type TestStepCategory = 'expect' | 'fixture' | 'hook' | 'pw:api' | 'test.step' | 'test.attach'; @@ -50,7 +49,6 @@ interface TestStepData { } export interface TestStepInternal extends TestStepData { - recoverFromStepError(error: Error): Promise; complete(result: { error?: Error | unknown, suggestedRebaseline?: string }): void; info: TestStepInfoImpl; attachmentIndices: number[]; @@ -68,7 +66,6 @@ type SnapshotNames = { export class TestInfoImpl implements TestInfo { private _onStepBegin: (payload: StepBeginPayload) => void; - private _onStepRecoverFromError: (payload: StepRecoverFromErrorPayload) => void; private _onStepEnd: (payload: StepEndPayload) => void; private _onAttach: (payload: AttachmentPayload) => void; private _snapshotNames: SnapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} }; @@ -122,7 +119,6 @@ export class TestInfoImpl implements TestInfo { readonly snapshotDir: string; errors: TestInfoErrorImpl[] = []; readonly _attachmentsPush: (...items: TestInfo['attachments']) => number; - private _recoverFromStepErrorResults: Map> | undefined; get error(): TestInfoErrorImpl | undefined { return this.errors[0]; @@ -162,13 +158,11 @@ export class TestInfoImpl implements TestInfo { test: TestCase | undefined, retry: number, onStepBegin: (payload: StepBeginPayload) => void, - onStepRecoverFromError: (payload: StepRecoverFromErrorPayload) => void, onStepEnd: (payload: StepEndPayload) => void, onAttach: (payload: AttachmentPayload) => void, ) { this.testId = test?.id ?? ''; this._onStepBegin = onStepBegin; - this._onStepRecoverFromError = onStepRecoverFromError; this._onStepEnd = onStepEnd; this._onAttach = onAttach; this._startTime = monotonicTime(); @@ -192,7 +186,6 @@ export class TestInfoImpl implements TestInfo { this.tags = test?.tags ?? []; this.fn = test?.fn ?? (() => {}); this.expectedStatus = test?.expectedStatus ?? 'skipped'; - this._recoverFromStepErrorResults = workerParams.recoverFromStepErrors ? new Map() : undefined; this._timeoutManager = new TimeoutManager(this.project.timeout); if (configInternal.configCLIOverrides.debug) @@ -304,22 +297,6 @@ export class TestInfoImpl implements TestInfo { steps: [], attachmentIndices: [], info: new TestStepInfoImpl(this, stepId, data.title, parentStep?.info), - recoverFromStepError: async (error: Error) => { - if (!this._recoverFromStepErrorResults) - return { stepId, status: 'failed' }; - const payload: StepRecoverFromErrorPayload = { - testId: this.testId, - stepId, - error: serializeError(error), - }; - this._onStepRecoverFromError(payload); - const recoveryPromise = new ManualPromise(); - this._recoverFromStepErrorResults.set(stepId, recoveryPromise); - const recoveryResult = await recoveryPromise; - if (recoveryResult.stepId !== stepId) - return { stepId, status: 'failed' }; - return recoveryResult; - }, complete: result => { if (step.endWallTime) return; @@ -396,12 +373,6 @@ export class TestInfoImpl implements TestInfo { return step; } - resumeAfterStepError(result: ResumeAfterStepErrorPayload) { - const recoveryPromise = this._recoverFromStepErrorResults?.get(result.stepId); - if (recoveryPromise) - recoveryPromise.resolve(result); - } - _interrupt() { // Mark as interrupted so we can ignore TimeoutError thrown by interrupt() call. this._wasInterrupted = true; diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index 0cfe0f65db068..e4eb1b82ade6d 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -33,7 +33,7 @@ import { loadTestFile } from '../common/testLoader'; import type { TimeSlot } from './timeoutManager'; import type { Location } from '../../types/testReporter'; import type { FullConfigInternal, FullProjectInternal } from '../common/config'; -import type { DonePayload, ResumeAfterStepErrorPayload, RunPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc'; +import type { DonePayload, RunPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc'; import type { Suite, TestCase } from '../common/test'; import type { TestAnnotation } from '../../types/test'; @@ -115,7 +115,7 @@ export class WorkerMain extends ProcessRunner { return; } // Ignore top-level errors, they are already inside TestInfo.errors. - const fakeTestInfo = new TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {}, () => {}); + const fakeTestInfo = new TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {}); const runnable = { type: 'teardown' } as const; // We have to load the project to get the right deadline below. await fakeTestInfo._runWithTimeout(runnable, () => this._loadIfNeeded()).catch(() => {}); @@ -261,14 +261,9 @@ export class WorkerMain extends ProcessRunner { } } - resumeAfterStepError(params: ResumeAfterStepErrorPayload): void { - this._currentTest?.resumeAfterStepError(params); - } - private async _runTest(test: TestCase, retry: number, nextTest: TestCase | undefined) { const testInfo = new TestInfoImpl(this._config, this._project, this._params, test, retry, stepBeginPayload => this.dispatchEvent('stepBegin', stepBeginPayload), - stepRecoverFromErrorPayload => this.dispatchEvent('stepRecoverFromError', stepRecoverFromErrorPayload), stepEndPayload => this.dispatchEvent('stepEnd', stepEndPayload), attachment => this.dispatchEvent('attach', attachment)); From 9173dc9a46273d0f75393bb15ffdc8857556b79d Mon Sep 17 00:00:00 2001 From: Marcin Szafranek Date: Thu, 21 Aug 2025 11:05:12 +0200 Subject: [PATCH 012/329] fix: update Galaxy S24 device dimensions and scale factor (#37125) --- .../src/server/deviceDescriptorsSource.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index a40feff1ba715..dc77eead35eae 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -178,10 +178,10 @@ "Galaxy S24": { "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", "viewport": { - "width": 480, - "height": 1040 + "width": 360, + "height": 780 }, - "deviceScaleFactor": 2.25, + "deviceScaleFactor": 3, "isMobile": true, "hasTouch": true, "defaultBrowserType": "chromium" @@ -189,10 +189,10 @@ "Galaxy S24 landscape": { "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", "viewport": { - "width": 1040, - "height": 480 + "width": 780, + "height": 360 }, - "deviceScaleFactor": 2.25, + "deviceScaleFactor": 3, "isMobile": true, "hasTouch": true, "defaultBrowserType": "chromium" From 05245120799617b9f299ee74f62831c70747ef87 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 21 Aug 2025 05:33:46 -0700 Subject: [PATCH 013/329] feat(ui-mode): reenables the update-snapshots option and uses a dropdown (#37098) --- .../src/ui/defaultSettingsView.tsx | 11 +++- packages/trace-viewer/src/ui/settingsView.css | 5 ++ packages/trace-viewer/src/ui/settingsView.tsx | 63 ++++++++++++++----- packages/trace-viewer/src/ui/uiModeView.tsx | 46 +++++++------- 4 files changed, 84 insertions(+), 41 deletions(-) diff --git a/packages/trace-viewer/src/ui/defaultSettingsView.tsx b/packages/trace-viewer/src/ui/defaultSettingsView.tsx index 03e1a018f0a6c..5c12f823451c4 100644 --- a/packages/trace-viewer/src/ui/defaultSettingsView.tsx +++ b/packages/trace-viewer/src/ui/defaultSettingsView.tsx @@ -34,24 +34,33 @@ export const DefaultSettingsView: React.FC<{}> = () => { return ( setActionsFilter(value ? [...actionsFilter, 'getter'] : actionsFilter.filter(a => a !== 'getter')), name: 'Show getter actions', }, { + type: 'check', value: actionsFilter.includes('route'), set: value => setActionsFilter(value ? [...actionsFilter, 'route'] : actionsFilter.filter(a => a !== 'route')), name: 'Show route actions', }, { + type: 'check', value: actionsFilter.includes('configuration'), set: value => setActionsFilter(value ? [...actionsFilter, 'configuration'] : actionsFilter.filter(a => a !== 'configuration')), name: 'Show configuration actions', diff --git a/packages/trace-viewer/src/ui/settingsView.css b/packages/trace-viewer/src/ui/settingsView.css index 4142b6897e694..4e590552272dd 100644 --- a/packages/trace-viewer/src/ui/settingsView.css +++ b/packages/trace-viewer/src/ui/settingsView.css @@ -39,3 +39,8 @@ margin-right: 5px; flex-shrink: 0; } + +.settings-view .setting select { + margin-left: 8px; + flex-shrink: 0; +} diff --git a/packages/trace-viewer/src/ui/settingsView.tsx b/packages/trace-viewer/src/ui/settingsView.tsx index 69439e773c996..824dc22e3833b 100644 --- a/packages/trace-viewer/src/ui/settingsView.tsx +++ b/packages/trace-viewer/src/ui/settingsView.tsx @@ -17,33 +17,66 @@ import * as React from 'react'; import './settingsView.css'; -export type Setting = { - value: T; - set: (value: T) => void; +export type Setting = { name: string; title?: string; -}; +} & ({ + type: 'check', + value: boolean; + set: (value: boolean) => void; +} | { + type: 'select', + options: Array<{ label: string, value: string }>; + value: string; + set: (value: string) => void; +}); export const SettingsView: React.FunctionComponent<{ - settings: Setting[]; + settings: Setting[]; }> = ({ settings }) => { return (
- {settings.map(({ value, set, name, title }) => { - const labelId = `setting-${name}`; + {settings.map(setting => { + const labelId = `setting-${setting.name}`; return ( -
- set(!value)} - /> - +
+ {renderSetting(setting, labelId)}
); })}
); }; + +const renderSetting = (setting: Setting, labelId: string) => { + switch (setting.type) { + case 'check': + return ( + <> + setting.set(!setting.value)} + /> + + + ); + case 'select': + return ( + <> + + + + ); + default: + return null; + } +}; diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 4375018765aea..1c3b5ad400c6b 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -57,11 +57,11 @@ const queryParams = { projects: searchParams.getAll('project'), workers: searchParams.get('workers') || undefined, headed: searchParams.has('headed'), - updateSnapshots: (searchParams.get('updateSnapshots') as 'all' | 'none' | 'missing' | undefined) || undefined, + updateSnapshots: (searchParams.get('updateSnapshots') as reporterTypes.FullConfig['updateSnapshots'] | undefined) || undefined, reporters: searchParams.has('reporter') ? searchParams.getAll('reporter') : undefined, pathSeparator: searchParams.get('pathSeparator') || '/', }; -if (queryParams.updateSnapshots && !['all', 'none', 'missing'].includes(queryParams.updateSnapshots)) +if (queryParams.updateSnapshots && !['all', 'changed', 'none', 'missing'].includes(queryParams.updateSnapshots)) queryParams.updateSnapshots = undefined; const isMac = navigator.platform === 'MacIntel'; @@ -100,10 +100,7 @@ export const UIModeView: React.FC<{}> = ({ const [revealSource, setRevealSource] = React.useState(false); const onRevealSource = React.useCallback(() => setRevealSource(true), [setRevealSource]); - const showTestingOptions = false; - const [singleWorker, setSingleWorker] = React.useState(false); - const [showBrowser, setShowBrowser] = React.useState(false); - const [updateSnapshots, setUpdateSnapshots] = React.useState(false); + const [updateSnapshots, setUpdateSnapshots] = useSetting('updateSnapshots', 'missing'); const inputRef = React.useRef(null); @@ -288,9 +285,7 @@ export const UIModeView: React.FC<{}> = ({ grepInvert: queryParams.grepInvert, testIds: [...testIds], projects: [...projectFilters].filter(([_, v]) => v).map(([p]) => p), - ...(singleWorker ? { workers: '1' } : {}), - ...(showBrowser ? { headed: true } : {}), - ...(updateSnapshots ? { updateSnapshots: 'all' } : {}), + updateSnapshots, reporters: queryParams.reporters, trace: 'on', }); @@ -302,7 +297,7 @@ export const UIModeView: React.FC<{}> = ({ setTestModel({ ...testModel }); setRunningState(oldState => oldState ? ({ ...oldState, completed: true }) : undefined); }); - }, [projectFilters, isRunningTest, testModel, testServerConnection, singleWorker, showBrowser, updateSnapshots]); + }, [projectFilters, isRunningTest, testModel, testServerConnection, updateSnapshots]); React.useEffect(() => { if (!testServerConnection || !teleSuiteUpdater) @@ -497,21 +492,22 @@ export const UIModeView: React.FC<{}> = ({ setFilterText={setFilterText} onRevealSource={onRevealSource} /> - {showTestingOptions && <> - setTestingOptionsVisible(!testingOptionsVisible)}> - -
Testing Options
-
- {testingOptionsVisible && } - } + setTestingOptionsVisible(!testingOptionsVisible)}> + +
Testing Options
+
+ {testingOptionsVisible && void, name: 'Update snapshots' }, + ]} />} setSettingsVisible(!settingsVisible)}> Date: Thu, 21 Aug 2025 09:33:04 -0700 Subject: [PATCH 014/329] Revert "fix(a11y): track inert elements as hidden (#36947)" (#37137) --- packages/injected/src/roleUtils.ts | 10 +++++----- tests/library/role-utils.spec.ts | 26 -------------------------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/packages/injected/src/roleUtils.ts b/packages/injected/src/roleUtils.ts index 3f0b5c14d601c..e652ee8c1581a 100644 --- a/packages/injected/src/roleUtils.ts +++ b/packages/injected/src/roleUtils.ts @@ -284,10 +284,10 @@ export function isElementHiddenForAria(element: Element): boolean { const isOptionInsideSelect = element.nodeName === 'OPTION' && !!element.closest('select'); if (!isOptionInsideSelect && !isSlot && !isElementStyleVisibilityVisible(element, style)) return true; - return belongsToDisplayNoneOrAriaHiddenOrNonSlottedOrInert(element); + return belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element); } -function belongsToDisplayNoneOrAriaHiddenOrNonSlottedOrInert(element: Element): boolean { +function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element: Element): boolean { let hidden = cacheIsHidden?.get(element); if (hidden === undefined) { hidden = false; @@ -298,17 +298,17 @@ function belongsToDisplayNoneOrAriaHiddenOrNonSlottedOrInert(element: Element): if (element.parentElement && element.parentElement.shadowRoot && !element.assignedSlot) hidden = true; - // display:none and aria-hidden=true and inert are considered hidden for aria. + // display:none and aria-hidden=true are considered hidden for aria. if (!hidden) { const style = getElementComputedStyle(element); - hidden = !style || style.display === 'none' || getAriaBoolean(element.getAttribute('aria-hidden')) === true || element.getAttribute('inert') !== null; + hidden = !style || style.display === 'none' || getAriaBoolean(element.getAttribute('aria-hidden')) === true; } // Check recursively. if (!hidden) { const parent = parentElementOrShadowHost(element); if (parent) - hidden = belongsToDisplayNoneOrAriaHiddenOrNonSlottedOrInert(parent); + hidden = belongsToDisplayNoneOrAriaHiddenOrNonSlotted(parent); } cacheIsHidden?.set(element, hidden); } diff --git a/tests/library/role-utils.spec.ts b/tests/library/role-utils.spec.ts index 796bad3f3bcbd..d894edbb29f31 100644 --- a/tests/library/role-utils.spec.ts +++ b/tests/library/role-utils.spec.ts @@ -564,32 +564,6 @@ test('should support search element', async ({ page }) => { await expect.soft(page.getByRole('search', { name: 'example' })).toBeVisible(); }); -test('should consider inert elements to be hidden', async ({ page }) => { - await page.setContent(` - -
- -
- - `); - - await expect(page.getByRole('button', { name: 'First' })).toHaveCount(0); - await expect(page.getByRole('button', { name: 'Second' })).toHaveCount(0); - await expect(page.getByRole('button', { name: 'Third' })).toHaveCount(0); - - await expect( - page.getByRole('button', { name: 'First', includeHidden: true }) - ).toHaveCount(1); - await expect( - page.getByRole('button', { name: 'Second', includeHidden: true }) - ).toHaveCount(1); - await expect( - page.getByRole('button', { name: 'Third', includeHidden: true }) - ).toHaveCount(1); -}); - function toArray(x: any): any[] { return Array.isArray(x) ? x : [x]; } From 3eeccaa499e12b13759807e9f5742bdfeef1a79b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 21 Aug 2025 11:40:52 -0700 Subject: [PATCH 015/329] fix(route): preserve fulfilled body (#37141) --- .../playwright-core/src/server/network.ts | 7 +++++ tests/page/page-request-fulfill.spec.ts | 28 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index a34050b0222c0..21253fdac9012 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -115,6 +115,7 @@ export class Request extends SdkObject { _responseEndTiming = -1; private _overrides: NormalizedContinueOverrides | undefined; private _bodySize: number | undefined; + _responseBodyOverride: { body: string; isBase64: boolean; } | undefined; constructor(context: contexts.BrowserContext, frame: frames.Frame | null, serviceWorker: pages.Worker | null, redirectedFrom: Request | null, documentId: string | undefined, url: string, resourceType: string, method: string, postData: Buffer | null, headers: HeadersArray) { @@ -311,6 +312,8 @@ export class Route extends SdkObject { body = ''; isBase64 = false; } + } else if (!overrides.status || overrides.status < 200 || overrides.status >= 400) { + this._request._responseBodyOverride = { body, isBase64 }; } const headers = [...(overrides.headers || [])]; this._maybeAddCorsHeaders(headers); @@ -535,6 +538,10 @@ export class Response extends SdkObject { this._contentPromise = this._finishedPromise.then(async () => { if (this._status >= 300 && this._status <= 399) throw new Error('Response body is unavailable for redirect responses'); + if (this._request._responseBodyOverride) { + const { body, isBase64 } = this._request._responseBodyOverride; + return Buffer.from(body, isBase64 ? 'base64' : 'utf-8'); + } return this._getResponseBodyCallback(); }); } diff --git a/tests/page/page-request-fulfill.spec.ts b/tests/page/page-request-fulfill.spec.ts index 0d939a0a5cbc2..6eef1eb9bd61e 100644 --- a/tests/page/page-request-fulfill.spec.ts +++ b/tests/page/page-request-fulfill.spec.ts @@ -462,7 +462,7 @@ it('should fulfill with gzip and readback', { it('should not go to the network for fulfilled requests body', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30760' }, -}, async ({ page, server, browserName }) => { +}, async ({ page, server }) => { await page.route('**/one-style.css', async route => { return route.fulfill({ status: 404, @@ -482,6 +482,30 @@ it('should not go to the network for fulfilled requests body', { await page.goto(server.PREFIX + '/one-style.html'); const response = await responsePromise; const body = await response.body(); - expect(body).toBeTruthy(); + expect(body.toString()).toBe('Not Found! (mocked)'); expect(serverHit).toBe(false); }); + +it('should return body for fulfilled responses', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-java/issues/1792' }, +}, async ({ page, server }) => { + for (const status of [100, 200, 404, 500]) { + await it.step(`status ${status}`, async () => { + const bodyOverride = `Custom body ${status}`; + await page.route('**/one-style.css', async route => { + return route.fulfill({ + status, + contentType: 'text/plain', + body: bodyOverride, + }); + }); + + const responsePromise = page.waitForResponse('**/one-style.css'); + await page.goto(server.PREFIX + '/one-style.html'); + const response = await responsePromise; + const body = await response.body(); + expect(body.toString()).toBe(bodyOverride); + }); + await page.unrouteAll(); + } +}); From e32f3ea1432a865cf43aef966a6475b4919b3880 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 21 Aug 2025 18:28:41 -0700 Subject: [PATCH 016/329] chore: allow mdb running over stdio (#37143) --- packages/playwright/src/index.ts | 2 +- .../playwright/src/mcp/browser/backend.ts | 22 +++++- packages/playwright/src/mcp/browser/tools.ts | 43 ++++------ packages/playwright/src/mcp/sdk/mdb.ts | 79 ++++++++++++------- packages/playwright/src/mcp/sdk/server.ts | 4 +- packages/playwright/src/mcp/test/backend.ts | 10 ++- packages/playwright/src/mcp/test/context.ts | 31 +++++++- packages/playwright/src/mcp/test/program.ts | 46 ----------- packages/playwright/src/mcp/test/runTests.ts | 1 + packages/playwright/src/program.ts | 24 ++++++ packages/playwright/src/runner/testRunner.ts | 57 +++++++++++++ packages/playwright/src/runner/testServer.ts | 60 +++----------- 12 files changed, 214 insertions(+), 165 deletions(-) delete mode 100644 packages/playwright/src/mcp/test/program.ts diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index d8fed58ca4c2e..bdaeda9832339 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -240,7 +240,7 @@ const playwrightFixtures: Fixtures = ({ (testInfo as TestInfoImpl)._setDebugMode(); playwright._defaultContextOptions = _combinedContextOptions; - playwright._defaultContextTimeout = process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP ? 5000 : actionTimeout || 0; + playwright._defaultContextTimeout = process.env.PLAYWRIGHT_DEBUGGER_MCP ? 5000 : actionTimeout || 0; playwright._defaultContextNavigationTimeout = navigationTimeout || 0; await use(); playwright._defaultContextOptions = undefined; diff --git a/packages/playwright/src/mcp/browser/backend.ts b/packages/playwright/src/mcp/browser/backend.ts index 5e2b87c43cec6..cd815dc393bff 100644 --- a/packages/playwright/src/mcp/browser/backend.ts +++ b/packages/playwright/src/mcp/browser/backend.ts @@ -25,10 +25,16 @@ import type { Tool } from './tool'; import type * as playwright from '../../../index'; import type { ServerBackendOnPause } from '../sdk/mdb'; +type PageEx = playwright.Page & { + _snapshotForAI: () => Promise; +}; + +const tools = [snapshot, pickLocator, evaluate]; + export class BrowserBackend implements ServerBackendOnPause { readonly name = 'Playwright'; readonly version = '0.0.1'; - private _tools: Tool[] = [snapshot, pickLocator, evaluate]; + private _tools: Tool[] = tools; private _page: playwright.Page; constructor(page: playwright.Page) { @@ -67,8 +73,16 @@ const doneToolSchema = defineToolSchema({ }); export async function runBrowserBackendOnError(page: playwright.Page, message: () => string) { - if (!process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP) + if (!process.env.PLAYWRIGHT_DEBUGGER_MCP) return; - const introMessage = `Paused on error:\n\n${stripAnsiEscapes(message())}\n\nTry recovering from the error prior to continuing.`; - await runOnPauseBackendLoop(process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP!, new BrowserBackend(page), introMessage); + const snapshot = await (page as PageEx)._snapshotForAI(); + const introMessage = `### Paused on error: +${stripAnsiEscapes(message())} + +### Current page snapshot: +${snapshot} + +### Task +Try recovering from the error prior to continuing, use following tools to recover: ${tools.map(tool => tool.schema.name).join(', ')}`; + await runOnPauseBackendLoop(process.env.PLAYWRIGHT_DEBUGGER_MCP!, new BrowserBackend(page), introMessage); } diff --git a/packages/playwright/src/mcp/browser/tools.ts b/packages/playwright/src/mcp/browser/tools.ts index ee00bd62be385..c9ddcccec9c06 100644 --- a/packages/playwright/src/mcp/browser/tools.ts +++ b/packages/playwright/src/mcp/browser/tools.ts @@ -28,9 +28,9 @@ type PageEx = playwright.Page & { export const snapshot = defineTool({ schema: { - name: 'browser_snapshot', - title: 'Page snapshot', - description: 'Capture accessibility snapshot of the current page, this is better than screenshot', + name: 'playwright_test_browser_snapshot', + title: 'Capture page snapshot', + description: 'Capture page snapshot for debugging', inputSchema: mcp.z.object({}), type: 'readOnly', }, @@ -55,28 +55,26 @@ export const elementSchema = mcp.z.object({ export const pickLocator = defineTool({ schema: { - name: 'browser_pick_locator', - title: 'Pick locator', - description: 'Pick a locator for the given element', + name: 'playwright_test_generate_locator', + title: 'Create locator for element', + description: 'Generate locator for the given element to use in tests', inputSchema: elementSchema, type: 'readOnly', }, handle: async (page, params) => { const locator = await refLocator(page, params); - const locatorString = await generateLocator(locator); - return { - content: [ - { - type: 'text', - text: locatorString, - }, - ], - }; + + try { + const { resolvedSelector } = await (locator as any)._resolveSelector(); + const locatorString = asLocator('javascript', resolvedSelector); + return { content: [{ type: 'text', text: locatorString }] }; + } catch (e) { + throw new Error(`Ref not found, likely because element was removed. Use ${snapshot.schema.name} to see what elements are currently on the page.`); + } }, }); - const evaluateSchema = mcp.z.object({ function: mcp.z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'), element: mcp.z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'), @@ -85,8 +83,8 @@ const evaluateSchema = mcp.z.object({ export const evaluate = defineTool({ schema: { - name: 'browser_evaluate', - title: 'Evaluate JavaScript', + name: 'playwright_test_evaluate_on_pause', + title: 'Evaluate in page', description: 'Evaluate JavaScript expression on page or element', inputSchema: evaluateSchema, type: 'destructive', @@ -114,12 +112,3 @@ async function refLocator(page: playwright.Page, elementRef: z.output { - try { - const { resolvedSelector } = await (locator as any)._resolveSelector(); - return asLocator('javascript', resolvedSelector); - } catch (e) { - throw new Error('Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.'); - } -} diff --git a/packages/playwright/src/mcp/sdk/mdb.ts b/packages/playwright/src/mcp/sdk/mdb.ts index b446587d5e085..4c58863bde74a 100644 --- a/packages/playwright/src/mcp/sdk/mdb.ts +++ b/packages/playwright/src/mcp/sdk/mdb.ts @@ -24,19 +24,30 @@ import * as mcpBundle from './bundle.js'; import { defineToolSchema } from './tool.js'; import * as mcpServer from './server.js'; import * as mcpHttp from './http.js'; -import { callTool } from './call.js'; +import { wrapInProcess } from './server.js'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +const mdbDebug = debug('pw:mcp:mdb'); const errorsDebug = debug('pw:mcp:errors'); export class MDBBackend implements mcpServer.ServerBackend { private _stack: { client: Client, toolNames: string[], resultPromise: ManualPromise | undefined }[] = []; private _interruptPromise: ManualPromise | undefined; - private _server!: mcpServer.Server; + private _topLevelBackend: mcpServer.ServerBackend; + private _initialized = false; + + constructor(topLevelBackend: mcpServer.ServerBackend) { + this._topLevelBackend = topLevelBackend; + } async initialize(server: mcpServer.Server): Promise { - this._server = server; + if (this._initialized) + return; + this._initialized = true; + const transport = await wrapInProcess(this._topLevelBackend); + await this._pushClient(transport); } async listTools(): Promise { @@ -48,11 +59,13 @@ export class MDBBackend implements mcpServer.ServerBackend { if (name === pushToolsSchema.name) return await this._pushTools(pushToolsSchema.inputSchema.parse(args || {})); - this._interruptPromise = new ManualPromise(); + const interruptPromise = new ManualPromise(); + this._interruptPromise = interruptPromise; let [entry] = this._stack; // Pop the client while the tool is not found. while (entry && !entry.toolNames.includes(name)) { + mdbDebug('popping client from stack for ', name); this._stack.shift(); await entry.client.close(); entry = this._stack[0]; @@ -67,13 +80,19 @@ export class MDBBackend implements mcpServer.ServerBackend { }).then(result => { resultPromise.resolve(result as mcpServer.CallToolResult); }).catch(e => { + mdbDebug('error in client call', e); if (this._stack.length < 2) throw e; this._stack.shift(); const prevEntry = this._stack[0]; void prevEntry.resultPromise!.then(result => resultPromise.resolve(result)); }); - return await Promise.race([this._interruptPromise, resultPromise]); + const result = await Promise.race([interruptPromise, resultPromise]); + if (interruptPromise.isDone()) + mdbDebug('client call intercepted', result); + else + mdbDebug('client call result', result); + return result; } private _client(): Client { @@ -84,24 +103,29 @@ export class MDBBackend implements mcpServer.ServerBackend { } private async _pushTools(params: { mcpUrl: string, introMessage?: string }): Promise { + mdbDebug('pushing tools to the stack', params.mcpUrl); + const transport = new StreamableHTTPClientTransport(new URL(params.mcpUrl)); + await this._pushClient(transport, params.introMessage); + return { content: [{ type: 'text', text: 'Tools pushed' }] }; + } + + private async _pushClient(transport: Transport, introMessage?: string): Promise { + mdbDebug('pushing client to the stack'); const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' }); client.setRequestHandler(PingRequestSchema, () => ({})); - const transport = new StreamableHTTPClientTransport(new URL(params.mcpUrl)); await client.connect(transport); - + mdbDebug('connected to the new client'); + const { tools } = await client.listTools(); + this._stack.unshift({ client, toolNames: tools.map(tool => tool.name), resultPromise: undefined }); + mdbDebug('new tools added to the stack:', tools.map(tool => tool.name)); + mdbDebug('interrupting current call:', !!this._interruptPromise); this._interruptPromise?.resolve({ content: [{ type: 'text', - text: params.introMessage || '', + text: introMessage || '', }], }); this._interruptPromise = undefined; - - const { tools } = await client.listTools(); - this._stack.unshift({ client, toolNames: tools.map(tool => tool.name), resultPromise: undefined }); - await this._server.notification({ - method: 'notifications/tools/list_changed', - }); return { content: [{ type: 'text', text: 'Tools pushed' }] }; } } @@ -121,22 +145,21 @@ export type ServerBackendOnPause = mcpServer.ServerBackend & { requestSelfDestruct?: () => void; }; -export async function runToolsBackend(backendFactory: mcpServer.ServerBackendFactory, options: { port: number }): Promise { - const mdbBackend = new MDBBackend(); - const mdbBackendFactory = { - name: 'Playwright MDB', - nameInConfig: 'playwright-mdb', - version: '0.0.0', +export async function runMainBackend(backendFactory: mcpServer.ServerBackendFactory, options?: { port?: number }): Promise { + const mdbBackend = new MDBBackend(backendFactory.create()); + // Start HTTP unconditionally. + const factory: mcpServer.ServerBackendFactory = { + ...backendFactory, create: () => mdbBackend }; + const url = await startAsHttp(factory, { port: options?.port || 0 }); + process.env.PLAYWRIGHT_DEBUGGER_MCP = url; - const mdbUrl = await startAsHttp(mdbBackendFactory, options); + if (options?.port !== undefined) + return url; - const backendUrl = await startAsHttp(backendFactory, { port: 0 }); - const result = await callTool(mdbUrl, pushToolsSchema.name, { mcpUrl: backendUrl }); - if (result.isError) - errorsDebug('Failed to push tools', result.content); - return mdbUrl; + // Start stdio conditionally. + await mcpServer.connect(factory, new mcpBundle.StdioServerTransport(), false); } export async function runOnPauseBackendLoop(mdbUrl: string, backend: ServerBackendOnPause, introMessage: string) { @@ -202,8 +225,8 @@ class OnceTimeServerBackendWrapper implements mcpServer.ServerBackend { return this._backend.callTool(name, args); } - serverClosed() { - this._backend.serverClosed?.(); + serverClosed(server: mcpServer.Server) { + this._backend.serverClosed?.(server); this._selfDestructPromise.resolve(); } diff --git a/packages/playwright/src/mcp/sdk/server.ts b/packages/playwright/src/mcp/sdk/server.ts index b6982a0fc4263..74543a9fca137 100644 --- a/packages/playwright/src/mcp/sdk/server.ts +++ b/packages/playwright/src/mcp/sdk/server.ts @@ -34,7 +34,7 @@ export interface ServerBackend { initialize?(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise; listTools(): Promise; callTool(name: string, args: CallToolRequest['params']['arguments']): Promise; - serverClosed?(): void; + serverClosed?(server: Server): void; } export type ServerBackendFactory = { @@ -104,7 +104,7 @@ export function createServer(name: string, version: string, backend: ServerBacke errorsDebug(e); } }); - addServerListener(server, 'close', () => backend.serverClosed?.()); + addServerListener(server, 'close', () => backend.serverClosed?.(server)); return server; } diff --git a/packages/playwright/src/mcp/test/backend.ts b/packages/playwright/src/mcp/test/backend.ts index 3efa3f02740b6..30f9f7b8eb8c3 100644 --- a/packages/playwright/src/mcp/test/backend.ts +++ b/packages/playwright/src/mcp/test/backend.ts @@ -30,8 +30,8 @@ export class TestServerBackend implements mcp.ServerBackend { private _tools: Tool[] = [listTests, runTests]; private _context: Context; - constructor(resolvedLocation: ConfigLocation) { - this._context = new Context(resolvedLocation); + constructor(resolvedLocation: ConfigLocation, options?: { muteConsole?: boolean }) { + this._context = new Context(resolvedLocation, options); } async listTools(): Promise { @@ -48,7 +48,11 @@ export class TestServerBackend implements mcp.ServerBackend { if (!tool) throw new Error(`Tool not found: ${name}. Available tools: ${this._tools.map(tool => tool.schema.name).join(', ')}`); const parsedArguments = tool.schema.inputSchema.parse(args || {}); - return await tool.handle(this._context!, parsedArguments); + const result = await tool.handle(this._context!, parsedArguments); + const stdio = this._context.takeStdio(); + if (stdio.trim()) + result.content.push({ type: 'text', text: stdio }); + return result; } serverClosed() { diff --git a/packages/playwright/src/mcp/test/context.ts b/packages/playwright/src/mcp/test/context.ts index fada1fb68dd14..05dcd8aa7b601 100644 --- a/packages/playwright/src/mcp/test/context.ts +++ b/packages/playwright/src/mcp/test/context.ts @@ -14,27 +14,52 @@ * limitations under the License. */ -import { TestRunner } from '../../runner/testRunner'; +import { TestRunner, TestRunnerEvent } from '../../runner/testRunner'; import type { ConfigLocation } from '../../common/config'; export class Context { private _testRunner: TestRunner | undefined; readonly configLocation: ConfigLocation; + readonly options?: { muteConsole?: boolean }; + private _stdio: { chunk: string | Buffer, stdio: 'stdout' | 'stderr' }[] = []; - constructor(configLocation: ConfigLocation) { + constructor(configLocation: ConfigLocation, options?: { muteConsole?: boolean }) { this.configLocation = configLocation; + this.options = options; } async createTestRunner(): Promise { if (this._testRunner) await this._testRunner.stopTests(); const testRunner = new TestRunner(this.configLocation, {}); - await testRunner.initialize({}); + await testRunner.initialize({ + sendStdioEvents: true, + muteConsole: this.options?.muteConsole, + }); + testRunner.on(TestRunnerEvent.StdioChunk, (chunk, stdio) => { + this._stdio.push({ chunk, stdio }); + }); + this._testRunner = testRunner; + testRunner.on(TestRunnerEvent.TestFilesChanged, testFiles => { + this._testRunner?.emit(TestRunnerEvent.TestFilesChanged, testFiles); + }); this._testRunner = testRunner; return testRunner; } + takeStdio(): string { + const text = this._stdio.map(entry => chunkToPayload(entry.stdio, entry.chunk)).join('\n'); + this._stdio = []; + return text; + } + async close() { } } + +function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string): string { + if (chunk instanceof Uint8Array) + return ''; + return `[${type}] ${chunk}`; +} diff --git a/packages/playwright/src/mcp/test/program.ts b/packages/playwright/src/mcp/test/program.ts deleted file mode 100644 index 4eecec8b906db..0000000000000 --- a/packages/playwright/src/mcp/test/program.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import path from 'path'; -import { program } from 'playwright-core/lib/utilsBundle'; - -import { resolveConfigLocation } from '../../common/configLoader'; -import { TestServerBackend } from './backend.js'; -import { runToolsBackend } from '../sdk/mdb'; - -program - .version('Version 0.0.1') - .name('playwright-test-mcp') - .option('--config ', 'Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"') - .option('--host ', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.') - .option('--port ', 'port to listen on for SSE transport.') - .action(async options => { - const resolvedLocation = resolveConfigLocation(options.config); - // eslint-disable-next-line no-console - console.error('Test config: ', path.relative(process.cwd(), resolvedLocation.resolvedConfigFile ?? resolvedLocation.configDir)); - const backendFactory = { - name: 'Playwright Test', - nameInConfig: 'playwright-test-mcp', - version: '0.0.0', - create: () => new TestServerBackend(resolvedLocation), - }; - const mdbUrl = await runToolsBackend(backendFactory, { port: 9224 }); - process.env.PLAYWRIGHT_TEST_DEBUGGER_MCP = mdbUrl; - // eslint-disable-next-line no-console - console.error('MCP Listening on: ', mdbUrl); - }); - -void program.parseAsync(process.argv); diff --git a/packages/playwright/src/mcp/test/runTests.ts b/packages/playwright/src/mcp/test/runTests.ts index 4d75c0bc6db07..3e9d6dc605c52 100644 --- a/packages/playwright/src/mcp/test/runTests.ts +++ b/packages/playwright/src/mcp/test/runTests.ts @@ -53,6 +53,7 @@ export const runTests = defineTool({ testIds: params.tests?.map(test => test.id), // For automatic recovery timeout: 0, + workers: 1, }); const text = stream.content(); diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 5e955dbce70e9..5bef1dd87b0cd 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -33,6 +33,9 @@ import * as testServer from './runner/testServer'; import { runWatchModeLoop } from './runner/watchMode'; import { runAllTestsWithConfig, TestRunner } from './runner/testRunner'; import { createErrorCollectingReporter } from './runner/reporters'; +import { ServerBackendFactory } from './mcp/sdk/server'; +import { TestServerBackend } from './mcp/test/backend'; +import { runMainBackend } from './mcp/sdk/mdb'; import type { ConfigCLIOverrides } from './common/ipc'; import type { TraceMode } from '../types/test'; @@ -140,6 +143,26 @@ Examples: $ npx playwright merge-reports playwright-report`); } +function addMCPServerCommand(program: Command) { + const command = program.command('run-mcp-server', { hidden: true }); + command.description('Interact with the test runner over MCP'); + command.option('-c, --config ', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`); + command.option('--host ', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.'); + command.option('--port ', 'port to listen on for SSE transport.'); + command.action(async options => { + const resolvedLocation = resolveConfigLocation(options.config); + const backendFactory: ServerBackendFactory = { + name: 'Playwright Test Runner', + nameInConfig: 'playwright-test-runner', + version: '0.0.0', + create: () => new TestServerBackend(resolvedLocation, { muteConsole: options.port === undefined }), + }; + const mdbUrl = await runMainBackend(backendFactory, { port: options.port === undefined ? undefined : +options.port }); + if (mdbUrl) + console.error('MCP Listening on: ', mdbUrl); + }); +} + async function runTests(args: string[], opts: { [key: string]: any }) { await startProfiling(); const cliOverrides = overridesFromOptions(opts); @@ -366,5 +389,6 @@ addTestCommand(program); addShowReportCommand(program); addMergeReportsCommand(program); addClearCacheCommand(program); +addMCPServerCommand(program); addDevServerCommand(program); addTestServerCommand(program); diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index b973a8852be1c..faf2e50d7f6df 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -17,7 +17,9 @@ import EventEmitter from 'events'; import fs from 'fs'; import path from 'path'; +import util from 'util'; +import { debug } from 'playwright-core/lib/utilsBundle'; import { registry } from 'playwright-core/lib/server'; import { ManualPromise, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils'; @@ -42,10 +44,12 @@ import type { AnyReporter } from '../reporters/reporterV2'; export const TestRunnerEvent = { TestFilesChanged: 'testFilesChanged', + StdioChunk: 'stdioChunk', } as const; export type TestRunnerEventMap = { [TestRunnerEvent.TestFilesChanged]: [testFiles: string[]]; + [TestRunnerEvent.StdioChunk]: [chunk: string | Buffer, stdio: 'stdout' | 'stderr']; }; export type ListTestsParams = { @@ -75,6 +79,12 @@ export type RunTestsParams = { type FullResultStatus = reporterTypes.FullResult['status']; +const originalDebugLog = debug.log; +// eslint-disable-next-line no-restricted-properties +const originalStdoutWrite = process.stdout.write; +// eslint-disable-next-line no-restricted-properties +const originalStderrWrite = process.stderr.write; + export class TestRunner extends EventEmitter { readonly configLocation: ConfigLocation; private _configCLIOverrides: ConfigCLIOverrides; @@ -106,9 +116,15 @@ export class TestRunner extends EventEmitter { async initialize(params: { watchTestDirs?: boolean; populateDependenciesOnList?: boolean; + sendStdioEvents?: boolean; + muteConsole?: boolean; }) { this._watchTestDirs = !!params.watchTestDirs; this._populateDependenciesOnList = !!params.populateDependenciesOnList; + this._setInterceptStdio({ + sendStdioEvents: !!params.sendStdioEvents, + muteConsole: !!params.muteConsole, + }); } resizeTerminal(params: { cols: number, rows: number }) { @@ -377,6 +393,11 @@ export class TestRunner extends EventEmitter { gracefullyProcessExitDoNotHang(0); } + async stop() { + this._setInterceptStdio({ sendStdioEvents: false, muteConsole: false }); + await this.runGlobalTeardown(); + } + private async _loadConfig(overrides?: ConfigCLIOverrides): Promise<{ config: FullConfigInternal | null, error?: reporterTypes.TestError }> { try { const config = await loadConfig(this.configLocation, overrides); @@ -405,6 +426,42 @@ export class TestRunner extends EventEmitter { await reporter.onExit(); return null; } + + _setInterceptStdio(options: { sendStdioEvents: boolean, muteConsole: boolean }) { + /* eslint-disable no-restricted-properties */ + if (process.env.PWTEST_DEBUG) + return; + if (options.sendStdioEvents || options.muteConsole) { + if (debug.log === originalDebugLog) { + // Only if debug.log hasn't already been tampered with, don't intercept any DEBUG=* logging + debug.log = (...args) => { + const string = util.format(...args) + '\n'; + return (originalStderrWrite as any).apply(process.stderr, [string]); + }; + } + const stdoutWrite = (chunk: string | Buffer) => { + if (options.sendStdioEvents) + this.emit(TestRunnerEvent.StdioChunk, chunk, 'stdout'); + if (!options.muteConsole) + originalStdoutWrite.apply(process.stdout, [chunk]); + return true; + }; + const stderrWrite = (chunk: string | Buffer) => { + if (options.sendStdioEvents) + this.emit(TestRunnerEvent.StdioChunk, chunk, 'stderr'); + if (!options.muteConsole) + originalStderrWrite.apply(process.stderr, [chunk]); + return true; + }; + process.stdout.write = stdoutWrite; + process.stderr.write = stderrWrite; + } else { + debug.log = originalDebugLog; + process.stdout.write = originalStdoutWrite; + process.stderr.write = originalStderrWrite; + } + /* eslint-enable no-restricted-properties */ + } } function printInternalError(e: Error) { diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 4f8758623aabe..616fd6a53b6f8 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -14,17 +14,15 @@ * limitations under the License. */ -import util from 'util'; - import { installRootRedirect, openTraceInBrowser, openTraceViewerApp, startTraceViewerServer } from 'playwright-core/lib/server'; import { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'playwright-core/lib/utils'; -import { debug, open } from 'playwright-core/lib/utilsBundle'; +import { open } from 'playwright-core/lib/utilsBundle'; import { loadConfig, resolveConfigLocation } from '../common/configLoader'; import ListReporter from '../reporters/list'; import { createReporterForTestServer } from './reporters'; import { SigIntWatcher } from './sigIntWatcher'; -import { TestRunner } from './testRunner'; +import { TestRunner, TestRunnerEvent } from './testRunner'; import type { TraceViewerRedirectOptions, TraceViewerServerOptions } from 'playwright-core/lib/server/trace/viewer/traceViewer'; import type { HttpServer, Transport } from 'playwright-core/lib/utils'; @@ -34,12 +32,6 @@ import type { ConfigCLIOverrides } from '../common/ipc'; import type { ReportEntry, TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface'; import type { ReporterV2 } from '../reporters/reporterV2'; -const originalDebugLog = debug.log; -// eslint-disable-next-line no-restricted-properties -const originalStdoutWrite = process.stdout.write; -// eslint-disable-next-line no-restricted-properties -const originalStderrWrite = process.stderr.write; - class TestServer { private _configLocation: ConfigLocation; private _configCLIOverrides: ConfigCLIOverrides; @@ -56,19 +48,10 @@ class TestServer { } async stop() { - await this._dispatcher?._setInterceptStdio(false); - await this._dispatcher?.runGlobalTeardown(); + await this._dispatcher?.stop(); } } -export const TestRunnerEvent = { - TestFilesChanged: 'testFilesChanged', -} as const; - -export type TestRunnerEventMap = { - [TestRunnerEvent.TestFilesChanged]: [testFiles: string[]]; -}; - export type ListTestsParams = { projects?: string[]; locations?: string[]; @@ -115,6 +98,7 @@ export class TestServerDispatcher implements TestServerInterface { this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params); this._testRunner.on(TestRunnerEvent.TestFilesChanged, testFiles => this._dispatchEvent('testFilesChanged', { testFiles })); + this._testRunner.on(TestRunnerEvent.StdioChunk, (chunk, stdio) => this._dispatchEvent('stdio', chunkToPayload(stdio, chunk))); } private async _wireReporter(messageSink: (message: any) => void) { @@ -133,10 +117,10 @@ export class TestServerDispatcher implements TestServerInterface { // Note: this method can be called multiple times, for example from a new connection after UI mode reload. this._serializer = params.serializer || require.resolve('./uiModeReporter'); this._closeOnDisconnect = !!params.closeOnDisconnect; - await this._setInterceptStdio(!!params.interceptStdio); await this._testRunner.initialize({ - watchTestDirs: !!params.watchTestDirs, - populateDependenciesOnList: !!params.populateDependenciesOnList, + ...params, + sendStdioEvents: !!params.interceptStdio, + muteConsole: !!params.interceptStdio, }); } @@ -226,34 +210,8 @@ export class TestServerDispatcher implements TestServerInterface { await this._testRunner.stopTests(); } - async _setInterceptStdio(intercept: boolean) { - /* eslint-disable no-restricted-properties */ - if (process.env.PWTEST_DEBUG) - return; - if (intercept) { - if (debug.log === originalDebugLog) { - // Only if debug.log hasn't already been tampered with, don't intercept any DEBUG=* logging - debug.log = (...args) => { - const string = util.format(...args) + '\n'; - return (originalStderrWrite as any).apply(process.stderr, [string]); - }; - } - const stdoutWrite = (chunk: string | Buffer) => { - this._dispatchEvent('stdio', chunkToPayload('stdout', chunk)); - return true; - }; - const stderrWrite = (chunk: string | Buffer) => { - this._dispatchEvent('stdio', chunkToPayload('stderr', chunk)); - return true; - }; - process.stdout.write = stdoutWrite; - process.stderr.write = stderrWrite; - } else { - debug.log = originalDebugLog; - process.stdout.write = originalStdoutWrite; - process.stderr.write = originalStderrWrite; - } - /* eslint-enable no-restricted-properties */ + async stop() { + await this._testRunner.stop(); } async closeGracefully() { From ca3cf0f22fda5926e38965fc30521a4ddbfebfc0 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:56:22 +0200 Subject: [PATCH 017/329] feat(webkit): roll to r2204 (#37138) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 169663e8ee3ba..650a1f87cc196 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -39,7 +39,7 @@ }, { "name": "webkit", - "revision": "2203", + "revision": "2204", "installByDefault": true, "revisionOverrides": { "debian11-x64": "2105", From 4bc9fb38db4964f0a682f2e6c2f46d78b44a6d89 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 22 Aug 2025 05:37:29 -0700 Subject: [PATCH 018/329] chore(ui-mode): improve toolbar alignment and remove some odd sizings (#37135) --- packages/trace-viewer/src/ui/timeline.css | 7 ++++++- packages/trace-viewer/src/ui/timeline.tsx | 2 +- packages/trace-viewer/src/ui/uiModeView.css | 4 ++++ packages/trace-viewer/src/ui/uiModeView.tsx | 2 +- packages/web/src/components/toolbar.css | 2 +- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/trace-viewer/src/ui/timeline.css b/packages/trace-viewer/src/ui/timeline.css index b22b48a4c5d27..fd8dc01ae91d7 100644 --- a/packages/trace-viewer/src/ui/timeline.css +++ b/packages/trace-viewer/src/ui/timeline.css @@ -14,12 +14,17 @@ limitations under the License. */ +.timeline-view-container { + flex: none; + border-bottom: 1px solid var(--vscode-panel-border); +} + .timeline-view { flex: none; position: relative; display: flex; flex-direction: column; - padding: 20px 0 5px; + padding: 20px 0 4px; cursor: text; user-select: none; margin-left: 10px; diff --git a/packages/trace-viewer/src/ui/timeline.tsx b/packages/trace-viewer/src/ui/timeline.tsx index e4010f972c287..4e8b194b1aec3 100644 --- a/packages/trace-viewer/src/ui/timeline.tsx +++ b/packages/trace-viewer/src/ui/timeline.tsx @@ -233,7 +233,7 @@ export const Timeline: React.FunctionComponent<{ setSelectedTime(undefined); }, [setSelectedTime]); - return
+ return
{!!dragWindow && = ({ setProjectFilters={setProjectFilters} testModel={testModel} runTests={() => runTests('bounce-if-busy', visibleTestIds)} /> - + {!isRunningTest && !progress &&
Tests
} {!isRunningTest && progress &&
{progress.passed}/{progress.total} passed ({(progress.passed / progress.total) * 100 | 0}%)
diff --git a/packages/web/src/components/toolbar.css b/packages/web/src/components/toolbar.css index 08e3614e34710..5a532a77d5323 100644 --- a/packages/web/src/components/toolbar.css +++ b/packages/web/src/components/toolbar.css @@ -18,7 +18,7 @@ position: relative; display: flex; color: var(--vscode-sideBarTitle-foreground); - min-height: 35px; + min-height: 30px; align-items: center; flex: none; padding-right: 4px; From 9f39b39cbe1feca0f2ffd19d5e1cb8481c040d7b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 22 Aug 2025 15:48:09 -0700 Subject: [PATCH 019/329] fix: align glob ** pattern matching with docs (#37156) --- .../src/utils/isomorphic/urlMatch.ts | 13 +++---------- tests/page/interception.spec.ts | 4 ++++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts index 84fcf8434a596..b526ca732e7b0 100644 --- a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts +++ b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts @@ -30,22 +30,15 @@ export function globToRegexPattern(glob: string): string { continue; } if (c === '*') { - const beforeDeep = glob[i - 1]; let starCount = 1; while (glob[i + 1] === '*') { starCount++; i++; } - const afterDeep = glob[i + 1]; - const isDeep = starCount > 1 && - (beforeDeep === '/' || beforeDeep === undefined) && - (afterDeep === '/' || afterDeep === undefined); - if (isDeep) { - tokens.push('((?:[^/]*(?:\/|$))*)'); - i++; - } else { + if (starCount > 1) + tokens.push('(.*)'); + else tokens.push('([^/]*)'); - } continue; } diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts index 08c502e9e1a85..b8653f9ef3838 100644 --- a/tests/page/interception.spec.ts +++ b/tests/page/interception.spec.ts @@ -123,6 +123,10 @@ it('should work with glob', async () => { expect(urlMatches(undefined, 'https://playwright.dev/foobar', 'https://playwright.dev/fooBAR')).toBeFalsy(); expect(urlMatches(undefined, 'https://playwright.dev/foobar?a=b', 'https://playwright.dev/foobar?A=B')).toBeFalsy(); + expect(urlMatches(undefined, 'https://localhost:3000/?a=b', '**/?a=b')).toBeTruthy(); + expect(urlMatches(undefined, 'https://localhost:3000/?a=b', '**?a=b')).toBeTruthy(); + expect(urlMatches(undefined, 'https://localhost:3000/?a=b', '**=b')).toBeTruthy(); + // This is not supported, we treat ? as a query separator. expect(globToRegex('http://localhost:8080/?imple/path.js').test('http://localhost:8080/Simple/path.js')).toBeFalsy(); expect(urlMatches(undefined, 'http://playwright.dev/', 'http://playwright.?ev')).toBeFalsy(); From 8b282902ed242480e8af38b85d626038ff862737 Mon Sep 17 00:00:00 2001 From: Amit Biswal <108086198+amitbiswal007@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:40:49 +0530 Subject: [PATCH 020/329] docs: improvement of the ci intro page for java (#37159) --- docs/src/ci-intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/ci-intro.md b/docs/src/ci-intro.md index 0ca8073923b00..3933e06034169 100644 --- a/docs/src/ci-intro.md +++ b/docs/src/ci-intro.md @@ -21,7 +21,7 @@ Playwright tests can be run on any CI provider. This guide covers one way of run ## Introduction * langs: python, java, csharp -Playwright tests can be run on any CI provider. In this section we cover running tests on GitHub using GitHub Actions. If you would like to see how to configure other CI providers, check out our detailed doc on Continuous Integration. +Playwright tests can be run on any CI provider. In this section we 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). #### You will learn * langs: python, java, csharp From 67b1accbc9b7d4de7e2e11be2e6d80ed7db73066 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 11:25:54 +0200 Subject: [PATCH 021/329] feat(webkit): roll to r2205 (#37157) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 650a1f87cc196..42f521221ed35 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -39,7 +39,7 @@ }, { "name": "webkit", - "revision": "2204", + "revision": "2205", "installByDefault": true, "revisionOverrides": { "debian11-x64": "2105", From 43878f8dc6d2a59e62883b1af879c9fa55999394 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:19:27 +0200 Subject: [PATCH 022/329] test: roll stable-test-runner to 1.56.0-alpha-2025-08-25 (#37165) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- .../stable-test-runner/package-lock.json | 46 +++++++++---------- .../stable-test-runner/package.json | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index 89166d8c6bed5..e737d97b72054 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,16 +5,16 @@ "packages": { "": { "dependencies": { - "@playwright/test": "^1.56.0-alpha-2025-08-20" + "@playwright/test": "^1.56.0-alpha-2025-08-25" } }, "node_modules/@playwright/test": { - "version": "1.56.0-alpha-2025-08-20", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-08-20.tgz", - "integrity": "sha512-FxvU2d5YuCwaq6ESNaJPAi/koEDai+WyRJx+4UcfeAo4xz+voH3uBEWstk2Lwkao1j9auvaeM+nDlgo0MQubfQ==", + "version": "1.56.0-alpha-2025-08-25", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-08-25.tgz", + "integrity": "sha512-uJ9s4hgFoJ8qRI3nLKwyE6Ms9o04EZkMfEszm1yC5WAZfAGn0XU2X05lXzXLMo65j4PdCYDQWH0buMm2Ibi2Lw==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.0-alpha-2025-08-20" + "playwright": "1.56.0-alpha-2025-08-25" }, "bin": { "playwright": "cli.js" @@ -38,12 +38,12 @@ } }, "node_modules/playwright": { - "version": "1.56.0-alpha-2025-08-20", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-08-20.tgz", - "integrity": "sha512-vIck+Po7jKPiVLkGLmhxYPW0Iux1Uhtqpqkzf0Z9jK+D2wYGYDGqlsEOqGbDcJiIxWmK14mByTCtN6LwfgCeMg==", + "version": "1.56.0-alpha-2025-08-25", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-08-25.tgz", + "integrity": "sha512-Zurkh6SJxizvoRS61yFcTXHMQeXDiSRUvjW0gAUQxwWZ9rAjeV59+CzSrA5QvSB60b+Pr8w8uPfgF0gByWfcFQ==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-alpha-2025-08-20" + "playwright-core": "1.56.0-alpha-2025-08-25" }, "bin": { "playwright": "cli.js" @@ -56,9 +56,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.0-alpha-2025-08-20", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-08-20.tgz", - "integrity": "sha512-zYBgXr6+VO3jYc23iNLS7OUS5cNZlAnEdsn/X9lqIO5GTfBINyn1KdyNuqgt00RoxEkgGhtPRFJRBpg6fph5hw==", + "version": "1.56.0-alpha-2025-08-25", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-08-25.tgz", + "integrity": "sha512-hbMI9NttWQgshEaCtyEkTN1NxKR1bpOCJH3MjXTpNlwXTA8SK1RCYaqSgAwAH5yxJJdsi3+nz4AdF0xToaeCTA==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -70,11 +70,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.56.0-alpha-2025-08-20", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-08-20.tgz", - "integrity": "sha512-FxvU2d5YuCwaq6ESNaJPAi/koEDai+WyRJx+4UcfeAo4xz+voH3uBEWstk2Lwkao1j9auvaeM+nDlgo0MQubfQ==", + "version": "1.56.0-alpha-2025-08-25", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-08-25.tgz", + "integrity": "sha512-uJ9s4hgFoJ8qRI3nLKwyE6Ms9o04EZkMfEszm1yC5WAZfAGn0XU2X05lXzXLMo65j4PdCYDQWH0buMm2Ibi2Lw==", "requires": { - "playwright": "1.56.0-alpha-2025-08-20" + "playwright": "1.56.0-alpha-2025-08-25" } }, "fsevents": { @@ -84,18 +84,18 @@ "optional": true }, "playwright": { - "version": "1.56.0-alpha-2025-08-20", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-08-20.tgz", - "integrity": "sha512-vIck+Po7jKPiVLkGLmhxYPW0Iux1Uhtqpqkzf0Z9jK+D2wYGYDGqlsEOqGbDcJiIxWmK14mByTCtN6LwfgCeMg==", + "version": "1.56.0-alpha-2025-08-25", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-08-25.tgz", + "integrity": "sha512-Zurkh6SJxizvoRS61yFcTXHMQeXDiSRUvjW0gAUQxwWZ9rAjeV59+CzSrA5QvSB60b+Pr8w8uPfgF0gByWfcFQ==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.56.0-alpha-2025-08-20" + "playwright-core": "1.56.0-alpha-2025-08-25" } }, "playwright-core": { - "version": "1.56.0-alpha-2025-08-20", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-08-20.tgz", - "integrity": "sha512-zYBgXr6+VO3jYc23iNLS7OUS5cNZlAnEdsn/X9lqIO5GTfBINyn1KdyNuqgt00RoxEkgGhtPRFJRBpg6fph5hw==" + "version": "1.56.0-alpha-2025-08-25", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-08-25.tgz", + "integrity": "sha512-hbMI9NttWQgshEaCtyEkTN1NxKR1bpOCJH3MjXTpNlwXTA8SK1RCYaqSgAwAH5yxJJdsi3+nz4AdF0xToaeCTA==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index 622a9d1e78fa0..944dcc77bc082 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "^1.56.0-alpha-2025-08-20" + "@playwright/test": "^1.56.0-alpha-2025-08-25" } } From 8289f9f2d7128e3d437a46c47136143b6ab337f6 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Mon, 25 Aug 2025 18:55:07 +0200 Subject: [PATCH 023/329] chore(bidi): map BiDi SameSite value 'default' to 'Lax' (#37173) --- .../playwright-core/src/server/bidi/bidiBrowser.ts | 1 + tests/config/browserTest.ts | 4 ++-- .../browsercontext-cookies-third-party.spec.ts | 12 ++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/playwright-core/src/server/bidi/bidiBrowser.ts b/packages/playwright-core/src/server/bidi/bidiBrowser.ts index ce55d0fb1ec9f..cbf7154534208 100644 --- a/packages/playwright-core/src/server/bidi/bidiBrowser.ts +++ b/packages/playwright-core/src/server/bidi/bidiBrowser.ts @@ -420,6 +420,7 @@ function fromBidiSameSite(sameSite: bidi.Network.SameSite): channels.NetworkCook case 'strict': return 'Strict'; case 'lax': return 'Lax'; case 'none': return 'None'; + case 'default': return 'Lax'; } return 'None'; } diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index 45cf6fba67b85..04ad410030dea 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -74,13 +74,13 @@ const test = baseTest.extend }, { scope: 'worker' }], defaultSameSiteCookieValue: [async ({ browserName, platform, macVersion }, run) => { - if (browserName === 'chromium' || browserName as any === '_bidiChromium') + if (browserName === 'chromium' || browserName as any === '_bidiChromium' || browserName as any === '_bidiFirefox') await run('Lax'); else if (browserName === 'webkit' && platform === 'linux') await run('Lax'); else if (browserName === 'webkit') await run('None'); // Windows + older macOS - else if (browserName === 'firefox' || browserName as any === '_bidiFirefox') + else if (browserName === 'firefox') await run('None'); else throw new Error('unknown browser - ' + browserName); diff --git a/tests/library/browsercontext-cookies-third-party.spec.ts b/tests/library/browsercontext-cookies-third-party.spec.ts index 41d8b74f6911e..0e78cbd7bcb26 100644 --- a/tests/library/browsercontext-cookies-third-party.spec.ts +++ b/tests/library/browsercontext-cookies-third-party.spec.ts @@ -523,16 +523,16 @@ test('should be able to send third party cookies via an iframe', async ({ browse } }); -test('should(not) block third party cookies - persistent context', async ({ httpsServer, launchPersistent, allowsThirdParty }) => { +test('should(not) block third party cookies - persistent context', async ({ httpsServer, launchPersistent, allowsThirdParty, defaultSameSiteCookieValue }) => { const { page, context } = await launchPersistent(); - await testThirdPartyCookiesAreBlocked(page, context, httpsServer, allowsThirdParty); + await testThirdPartyCookiesAreBlocked(page, context, httpsServer, allowsThirdParty, defaultSameSiteCookieValue); }); -test('should(not) block third party cookies - ephemeral context', async ({ page, context, httpsServer, allowsThirdParty }) => { - await testThirdPartyCookiesAreBlocked(page, context, httpsServer, allowsThirdParty); +test('should(not) block third party cookies - ephemeral context', async ({ page, context, httpsServer, allowsThirdParty, defaultSameSiteCookieValue }) => { + await testThirdPartyCookiesAreBlocked(page, context, httpsServer, allowsThirdParty, defaultSameSiteCookieValue); }); -async function testThirdPartyCookiesAreBlocked(page: Page, context: BrowserContext, server: TestServer, allowsThirdParty: boolean) { +async function testThirdPartyCookiesAreBlocked(page: Page, context: BrowserContext, server: TestServer, allowsThirdParty: boolean, defaultSameSiteCookieValue: string) { await page.goto(server.EMPTY_PAGE); await page.evaluate(src => { let fulfill; @@ -558,7 +558,7 @@ async function testThirdPartyCookiesAreBlocked(page: Page, context: BrowserConte 'httpOnly': false, 'name': 'username', 'path': '/', - 'sameSite': 'None', + 'sameSite': defaultSameSiteCookieValue, 'secure': false, 'value': 'John Doe' } From 35cd6fb279ccf4c91eae4b8094697eceb61426cc Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:54:38 -0700 Subject: [PATCH 024/329] feat(webkit): roll to r2206 (#37175) --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 42f521221ed35..f7f054393eab2 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -39,7 +39,7 @@ }, { "name": "webkit", - "revision": "2205", + "revision": "2206", "installByDefault": true, "revisionOverrides": { "debian11-x64": "2105", From 409d13f730daf21049466cd7bda7e759b70c39c6 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 26 Aug 2025 10:26:40 +0200 Subject: [PATCH 025/329] fix(test): attaching in boxed fixture (#37149) --- packages/playwright/src/worker/testInfo.ts | 5 +++- .../reporter-attachment.spec.ts | 27 +++++++++++++++++ .../ui-mode-test-attachments.spec.ts | 29 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index fa71ac1b15692..8d93429136fac 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -455,7 +455,10 @@ export class TestInfoImpl implements TestInfo { title: `Attach ${escapeWithQuotes(name, '"')}`, category: 'test.attach', }); - this._attach(await normalizeAndSaveAttachment(this.outputPath(), name, options), step.stepId); + this._attach( + await normalizeAndSaveAttachment(this.outputPath(), name, options), + step.group ? undefined : step.stepId + ); step.complete({}); } diff --git a/tests/playwright-test/reporter-attachment.spec.ts b/tests/playwright-test/reporter-attachment.spec.ts index 50d6d1ee0c8c4..65a536e77fadf 100644 --- a/tests/playwright-test/reporter-attachment.spec.ts +++ b/tests/playwright-test/reporter-attachment.spec.ts @@ -322,3 +322,30 @@ test('render text attachment with multiple lines', async ({ runInlineTest }) => expect(text).toContain(' ────────────────────────────────────────────────────────────────────────────────────────────────'); expect(result.exitCode).toBe(1); }); + +test('attaching inside boxed fixture should not log error', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37147' } }, async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test as base } from '@playwright/test'; + + const test = base.extend<{ myFixture: void }>({ + myFixture: [async ({}, use, testInfo) => { + await testInfo.attach('my attachment', { + body: 'foo', + contentType: 'text/plain', + }); + await use(); + }, { box: true }], + }); + + test('my test', ({ myFixture }) => { + expect(1).toBe(0); + }); + `, + }, { reporter: 'line' }, {}); + const text = result.output; + expect(text).toContain(' attachment #1: my attachment (text/plain) ──────────────────────────────────────────────────────'); + expect(text).toContain(' foo'); + expect(text).toContain(' ────────────────────────────────────────────────────────────────────────────────────────────────'); + expect(result.exitCode).toBe(1); +}); diff --git a/tests/playwright-test/ui-mode-test-attachments.spec.ts b/tests/playwright-test/ui-mode-test-attachments.spec.ts index 6d466acec547b..4984afd9fc07e 100644 --- a/tests/playwright-test/ui-mode-test-attachments.spec.ts +++ b/tests/playwright-test/ui-mode-test-attachments.spec.ts @@ -173,6 +173,35 @@ test('should link from attachment step to attachments view', async ({ runUITest await expect(attachment).toBeInViewport(); }); +test('attachments from inside boxed fixture should be visible', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37147' } }, async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.test.ts': ` + import { test as base } from '@playwright/test'; + + const test = base.extend<{ myFixture: void }>({ + myFixture: [async ({}, use, testInfo) => { + await testInfo.attach('my attachment', { + body: 'foo', + contentType: 'text/plain', + }); + await use(); + }, { box: true }], + }); + + test('my test', ({ myFixture }) => {}); + `, + }, { reporter: 'line' }, {}); + await page.getByText('my test').click(); + await page.getByTitle('Run all').click(); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); + + await page.getByRole('treeitem', { name: 'attach "my attachment"' }).getByLabel('Open Attachment').click(); + await expect(page.getByRole('tabpanel', { name: 'Attachments' })).toMatchAriaSnapshot(` + - tabpanel: + - button /my attachment/ + `); +}); + function readAllFromStream(stream: NodeJS.ReadableStream): Promise { return new Promise(resolve => { const chunks: Buffer[] = []; From cb6be94000a3cf6bf8461196125dfa6a715cd4cd Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 26 Aug 2025 11:33:19 +0200 Subject: [PATCH 026/329] chore: config.webServer.command cannot be empty (#37136) --- .../playwright/src/plugins/webServerPlugin.ts | 3 +++ tests/playwright-test/web-server.spec.ts | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/playwright/src/plugins/webServerPlugin.ts b/packages/playwright/src/plugins/webServerPlugin.ts index afd067c855054..bdbcfe3d0ce9d 100644 --- a/packages/playwright/src/plugins/webServerPlugin.ts +++ b/packages/playwright/src/plugins/webServerPlugin.ts @@ -94,6 +94,9 @@ export class WebServerPlugin implements TestRunnerPlugin { throw new Error(`${this._options.url ?? `http://localhost${port ? ':' + port : ''}`} is already used, make sure that nothing is running on the port/url or set reuseExistingServer:true in config.webServer.`); } + if (!this._options.command) + throw new Error('config.webServer.command cannot be empty'); + debugWebServer(`Starting WebServer process ${this._options.command}...`); const { launchedProcess, gracefullyClose } = await launchProcess({ command: this._options.command, diff --git a/tests/playwright-test/web-server.spec.ts b/tests/playwright-test/web-server.spec.ts index 92600121052a2..52c541de45460 100644 --- a/tests/playwright-test/web-server.spec.ts +++ b/tests/playwright-test/web-server.spec.ts @@ -864,3 +864,24 @@ test.describe('name option', () => { expect(result.output).toContain(`[${defaultPrefix}]`); }); }); + +test('should throw helpful error when command is empty', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'test.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({}) => {}); + `, + 'playwright.config.ts': ` + module.exports = { + webServer: [ + { + command: '', + url: 'http://localhost:3000', + } + ], + }; + `, + }, undefined); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('config.webServer.command cannot be empty'); +}); From fd0954374079ff67038d2473c8c70dfb3c96f6cc Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 26 Aug 2025 13:30:02 +0200 Subject: [PATCH 027/329] chore: update ESLint linters (#37181) --- eslint.config.mjs | 2 +- package-lock.json | 519 ++++++++++-------- package.json | 22 +- packages/injected/src/bindingsController.ts | 2 - packages/injected/src/clock.ts | 2 - packages/injected/src/injectedScript.ts | 2 - packages/injected/src/utilityScript.ts | 4 - packages/injected/src/webSocketMock.ts | 1 - .../isomorphic/utilityScriptSerializers.ts | 1 - 9 files changed, 316 insertions(+), 239 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index f11395ad00b33..d4c01d5d45699 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -176,7 +176,7 @@ export const baseRules = { before: true, }, ], - "@stylistic/func-call-spacing": 2, + "@stylistic/function-call-spacing": 2, "@stylistic/type-annotation-spacing": 2, // file whitespace diff --git a/package-lock.json b/package-lock.json index 39fa270d4331b..438ceb7c153b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,12 +15,12 @@ "@actions/core": "^1.10.0", "@actions/github": "^6.0.0", "@babel/code-frame": "^7.26.2", - "@eslint/compat": "^1.2.6", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.19.0", + "@eslint/compat": "^1.3.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.34.0", "@modelcontextprotocol/sdk": "^1.17.3", "@octokit/graphql-schema": "^15.26.0", - "@stylistic/eslint-plugin": "^3.0.1", + "@stylistic/eslint-plugin": "^5.2.3", "@types/codemirror": "^5.60.7", "@types/formidable": "^2.0.4", "@types/immutable": "^3.8.7", @@ -29,9 +29,9 @@ "@types/react-dom": "^18.0.5", "@types/ws": "^8.5.3", "@types/xml2js": "^0.4.9", - "@typescript-eslint/eslint-plugin": "^8.26.1", - "@typescript-eslint/parser": "^8.26.1", - "@typescript-eslint/utils": "^8.26.1", + "@typescript-eslint/eslint-plugin": "^8.41.0", + "@typescript-eslint/parser": "^8.41.0", + "@typescript-eslint/utils": "^8.41.0", "@vitejs/plugin-basic-ssl": "^1.1.0", "@vitejs/plugin-react": "^4.2.1", "@zip.js/zip.js": "^2.7.29", @@ -44,11 +44,11 @@ "dotenv": "^16.4.5", "electron": "^30.1.2", "esbuild": "^0.25.0", - "eslint": "^9.19.0", - "eslint-plugin-import": "^2.31.0", + "eslint": "^9.34.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-notice": "^1.0.0", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", "formidable": "^2.1.1", "immutable": "^4.3.7", "license-checker": "^25.0.1", @@ -822,9 +822,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -864,16 +864,16 @@ } }, "node_modules/@eslint/compat": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.6.tgz", - "integrity": "sha512-k7HNCqApoDHM6XzT30zGoETj+D+uUcZUb+IVAJmar3u6bvHf7hhHJcWx09QHj4/a2qrKZMWU0E16tvkiAdv06Q==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.2.tgz", + "integrity": "sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==", "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": "^9.10.0" + "eslint": "^8.40 || 9" }, "peerDependenciesMeta": { "eslint": { @@ -882,9 +882,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -896,10 +896,20 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -910,9 +920,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -947,13 +957,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -967,32 +980,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -1056,9 +1056,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1683,23 +1683,24 @@ } }, "node_modules/@stylistic/eslint-plugin": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-3.1.0.tgz", - "integrity": "sha512-pA6VOrOqk0+S8toJYhQGv2MWpQQR0QpeUo9AhNkC49Y26nxBQ/nH1rta9bUU1rPw2fJ1zZEMV5oCX5AazT7J2g==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.2.3.tgz", + "integrity": "sha512-oY7GVkJGVMI5benlBDCaRrSC1qPasafyv5dOBLLv5MTilMGnErKhO6ziEfodDDIZbo5QxPUNW360VudJOFODMw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.13.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/types": "^8.38.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "estraverse": "^5.3.0", - "picomatch": "^4.0.2" + "picomatch": "^4.0.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": ">=8.40.0" + "eslint": ">=9.0.0" } }, "node_modules/@sveltejs/acorn-typescript": { @@ -1936,21 +1937,21 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", - "integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", + "integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.26.1", - "@typescript-eslint/type-utils": "8.26.1", - "@typescript-eslint/utils": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/type-utils": "8.41.0", + "@typescript-eslint/utils": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1960,22 +1961,32 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", - "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.41.0.tgz", + "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.26.1", - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/typescript-estree": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4" }, "engines": { @@ -1987,18 +1998,40 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", + "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.41.0", + "@typescript-eslint/types": "^8.41.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", - "integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", + "integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1" + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2008,17 +2041,35 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", + "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz", - "integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", + "integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.26.1", - "@typescript-eslint/utils": "8.26.1", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/utils": "8.41.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2029,13 +2080,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", - "integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", + "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", "dev": true, "license": "MIT", "engines": { @@ -2047,20 +2098,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz", - "integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", + "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/visitor-keys": "8.26.1", + "@typescript-eslint/project-service": "8.41.0", + "@typescript-eslint/tsconfig-utils": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2070,13 +2123,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2100,9 +2153,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -2113,16 +2166,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.1.tgz", - "integrity": "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.41.0.tgz", + "integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.26.1", - "@typescript-eslint/types": "8.26.1", - "@typescript-eslint/typescript-estree": "8.26.1" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2133,18 +2186,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.26.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz", - "integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", + "integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.26.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.41.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2356,9 +2409,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2491,18 +2544,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "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" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2533,18 +2588,19 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2868,14 +2924,14 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -3561,9 +3617,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -3571,18 +3627,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -3594,21 +3650,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -3617,7 +3676,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -3812,22 +3871,23 @@ } }, "node_modules/eslint": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", - "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.34.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -3835,9 +3895,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3894,9 +3954,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -3922,30 +3982,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -3981,9 +4041,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", - "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { @@ -3997,7 +4057,7 @@ "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.8", + "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", @@ -4014,9 +4074,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", - "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { @@ -4045,9 +4105,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4062,9 +4122,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4094,15 +4154,15 @@ "license": "MIT" }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4589,18 +4649,18 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -5334,6 +5394,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -6252,15 +6325,16 @@ } }, "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -7568,6 +7642,20 @@ "node": ">= 0.8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7848,9 +7936,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -8374,16 +8462,17 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", - "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "for-each": "^0.3.3", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, diff --git a/package.json b/package.json index 969d856fbe815..c802aaccd4d3e 100644 --- a/package.json +++ b/package.json @@ -53,12 +53,12 @@ "@actions/core": "^1.10.0", "@actions/github": "^6.0.0", "@babel/code-frame": "^7.26.2", - "@eslint/compat": "^1.2.6", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.19.0", + "@eslint/compat": "^1.3.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.34.0", "@modelcontextprotocol/sdk": "^1.17.3", "@octokit/graphql-schema": "^15.26.0", - "@stylistic/eslint-plugin": "^3.0.1", + "@stylistic/eslint-plugin": "^5.2.3", "@types/codemirror": "^5.60.7", "@types/formidable": "^2.0.4", "@types/immutable": "^3.8.7", @@ -67,9 +67,9 @@ "@types/react-dom": "^18.0.5", "@types/ws": "^8.5.3", "@types/xml2js": "^0.4.9", - "@typescript-eslint/eslint-plugin": "^8.26.1", - "@typescript-eslint/parser": "^8.26.1", - "@typescript-eslint/utils": "^8.26.1", + "@typescript-eslint/eslint-plugin": "^8.41.0", + "@typescript-eslint/parser": "^8.41.0", + "@typescript-eslint/utils": "^8.41.0", "@vitejs/plugin-basic-ssl": "^1.1.0", "@vitejs/plugin-react": "^4.2.1", "@zip.js/zip.js": "^2.7.29", @@ -82,11 +82,11 @@ "dotenv": "^16.4.5", "electron": "^30.1.2", "esbuild": "^0.25.0", - "eslint": "^9.19.0", - "eslint-plugin-import": "^2.31.0", + "eslint": "^9.34.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-notice": "^1.0.0", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", "formidable": "^2.1.1", "immutable": "^4.3.7", "license-checker": "^25.0.1", diff --git a/packages/injected/src/bindingsController.ts b/packages/injected/src/bindingsController.ts index ec0ff89eee7cd..a9a8d42c7bf09 100644 --- a/packages/injected/src/bindingsController.ts +++ b/packages/injected/src/bindingsController.ts @@ -32,12 +32,10 @@ type BindingData = { }; export class BindingsController { - // eslint-disable-next-line no-restricted-globals private _global: typeof globalThis; private _globalBindingName: string; private _bindings = new Map(); - // eslint-disable-next-line no-restricted-globals constructor(global: typeof globalThis, globalBindingName: string) { this._global = global; this._globalBindingName = globalBindingName; diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index 673a2da6facf1..753996a36e9ff 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -430,7 +430,6 @@ function mirrorDateProperties(target: any, source: Builtins['Date']): Builtins[' } function createDate(clock: ClockController, NativeDate: Builtins['Date']): Builtins['Date'] { - // eslint-disable-next-line no-restricted-globals function ClockDate(this: typeof ClockDate, year: number, month: number, date: number, hour: number, minute: number, second: number, ms: number): Date | string { // the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2. // This remains so in the 10th edition of 2019 as well. @@ -497,7 +496,6 @@ function createIntl(clock: ClockController, NativeIntl: Builtins['Intl']): Built ClockIntl.DateTimeFormat = function(...args: any[]) { const realFormatter = new NativeIntl.DateTimeFormat(...args); - // eslint-disable-next-line no-restricted-globals const formatter: Intl.DateTimeFormat = { formatRange: realFormatter.formatRange.bind(realFormatter), formatRangeToParts: realFormatter.formatRangeToParts.bind(realFormatter), diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index 29446799cb80c..a18c6ce2b5ab3 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -87,7 +87,6 @@ export class InjectedScript { private _sdkLanguage: Language; private _testIdAttributeNameForStrictErrorAndConsoleCodegen: string = 'data-testid'; private _markedElements?: { callId: string, elements: Set }; - // eslint-disable-next-line no-restricted-globals readonly window: Window & typeof globalThis; readonly document: Document; readonly consoleApi: ConsoleAPI; @@ -120,7 +119,6 @@ export class InjectedScript { private _mouseHitTargetInterceptorEvents: Set; private _allHitTargetInterceptorEvents: Set; - // eslint-disable-next-line no-restricted-globals constructor(window: Window & typeof globalThis, options: InjectedScriptOptions) { this.window = window; this.document = window.document; diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index 4bacbcdff8df6..e61ad96d45b5f 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -27,20 +27,16 @@ export type Builtins = { requestIdleCallback: Window['requestIdleCallback'], cancelIdleCallback: Window['cancelIdleCallback'], performance: Window['performance'], - // eslint-disable-next-line no-restricted-globals Intl: typeof window['Intl'], - // eslint-disable-next-line no-restricted-globals Date: typeof window['Date'], }; export class UtilityScript { - // eslint-disable-next-line no-restricted-globals readonly global: typeof globalThis; // Builtins protect injected code from clock emulation. readonly builtins: Builtins; readonly isUnderTest: boolean; - // eslint-disable-next-line no-restricted-globals constructor(global: typeof globalThis, isUnderTest: boolean) { this.global = global; this.isUnderTest = isUnderTest; diff --git a/packages/injected/src/webSocketMock.ts b/packages/injected/src/webSocketMock.ts index f45885a4e8abc..2ae2188e85607 100644 --- a/packages/injected/src/webSocketMock.ts +++ b/packages/injected/src/webSocketMock.ts @@ -33,7 +33,6 @@ export type ClosePageRequest = { type: 'closePage', id: string, code: number | u export type CloseServerRequest = { type: 'closeServer', id: string, code: number | undefined, reason: string | undefined, wasClean: boolean }; export type APIRequest = ConnectRequest | PassthroughRequest | EnsureOpenedRequest | SendToPageRequest | SendToServerRequest | ClosePageRequest | CloseServerRequest; -// eslint-disable-next-line no-restricted-globals type GlobalThis = typeof globalThis; export function inject(globalThis: GlobalThis) { diff --git a/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts index 36e24104898cd..a523528f27975 100644 --- a/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts @@ -45,7 +45,6 @@ function isRegExp(obj: any): obj is RegExp { } } -// eslint-disable-next-line no-restricted-globals function isDate(obj: any): obj is Date { try { // eslint-disable-next-line no-restricted-globals From dfccf2950b75e04a33b2b54917d95f07411d3c46 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 26 Aug 2025 16:32:03 +0200 Subject: [PATCH 028/329] chore: replace ESLint indent rule with @stylistic/indent (#37190) --- eslint.config.mjs | 2 +- .../src/server/accessibility.ts | 16 +++++----- .../bidi/third_party/bidiProtocolCore.ts | 12 +++---- .../server/bidi/third_party/bidiSerializer.ts | 4 +-- .../server/bidi/third_party/firefoxPrefs.ts | 18 +++++------ .../src/server/chromium/crCoverage.ts | 6 ++-- packages/playwright-core/src/server/frames.ts | 8 ++--- .../playwright-core/src/server/harBackend.ts | 13 ++++---- .../playwright-core/src/server/javascript.ts | 16 +++++----- .../src/server/utils/eventsHelper.ts | 8 ++--- packages/playwright/src/reporters/base.ts | 18 +++++------ packages/playwright/src/runner/watchMode.ts | 6 ++-- ...rowsercontext-fetch-happy-eyeballs.spec.ts | 6 ++-- tests/library/chromium/extensions.spec.ts | 32 +++++++++---------- tests/page/page-request-fulfill.spec.ts | 18 +++++------ tests/playwright-test/reporter-blob.spec.ts | 24 +++++++------- 16 files changed, 105 insertions(+), 102 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index d4c01d5d45699..141bcdccf1ebd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -184,7 +184,7 @@ export const baseRules = { "no-mixed-spaces-and-tabs": 2, "no-trailing-spaces": 2, "linebreak-style": [process.platform === "win32" ? 0 : 2, "unix"], - indent: [ + "@stylistic/indent": [ 2, 2, { SwitchCase: 1, CallExpression: { arguments: 2 }, MemberExpression: 2 }, diff --git a/packages/playwright-core/src/server/accessibility.ts b/packages/playwright-core/src/server/accessibility.ts index b92c5411f5e35..fbf8012c9583b 100644 --- a/packages/playwright-core/src/server/accessibility.ts +++ b/packages/playwright-core/src/server/accessibility.ts @@ -19,11 +19,11 @@ import type * as dom from './dom'; import type * as channels from '@protocol/channels'; export interface AXNode { - isInteresting(insideControl: boolean): boolean; - isLeafNode(): boolean; - isControl(): boolean; - serialize(): channels.AXNode; - children(): Iterable; + isInteresting(insideControl: boolean): boolean; + isLeafNode(): boolean; + isControl(): boolean; + serialize(): channels.AXNode; + children(): Iterable; } export class Accessibility { @@ -33,9 +33,9 @@ export class Accessibility { } async snapshot(options: { - interestingOnly?: boolean; - root?: dom.ElementHandle; - } = {}): Promise { + interestingOnly?: boolean; + root?: dom.ElementHandle; + } = {}): Promise { const { interestingOnly = true, root = null, diff --git a/packages/playwright-core/src/server/bidi/third_party/bidiProtocolCore.ts b/packages/playwright-core/src/server/bidi/third_party/bidiProtocolCore.ts index 5d9bedd66261e..cdb7919260228 100644 --- a/packages/playwright-core/src/server/bidi/third_party/bidiProtocolCore.ts +++ b/packages/playwright-core/src/server/bidi/third_party/bidiProtocolCore.ts @@ -164,8 +164,8 @@ export namespace Session { httpProxy?: string; sslProxy?: string; } & ({} | Session.SocksProxyConfiguration) & { - noProxy?: [...string[]]; - } & Extensible; + noProxy?: [...string[]]; + } & Extensible; } export namespace Session { export type SocksProxyConfiguration = { @@ -1013,11 +1013,11 @@ export namespace Emulation { export namespace Emulation { export type SetGeolocationOverrideParameters = ( | { - coordinates: Emulation.GeolocationCoordinates | null; - } + coordinates: Emulation.GeolocationCoordinates | null; + } | { - error: Emulation.GeolocationPositionError; - } + error: Emulation.GeolocationPositionError; + } ) & { contexts?: [ BrowsingContext.BrowsingContext, diff --git a/packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts b/packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts index c325e2d99882a..6f4df4dd12165 100644 --- a/packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts +++ b/packages/playwright-core/src/server/bidi/third_party/bidiSerializer.ts @@ -9,7 +9,7 @@ import type * as Bidi from './bidiProtocol'; -/* eslint-disable curly, indent */ +/* eslint-disable curly */ /** * @internal @@ -123,7 +123,7 @@ export class BidiSerializer { } throw new UnserializableError( - 'Custom object serialization not possible. Use plain objects instead.' + 'Custom object serialization not possible. Use plain objects instead.' ); } } diff --git a/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts b/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts index af6617098b0ba..5139f0266c897 100644 --- a/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts +++ b/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts @@ -9,7 +9,7 @@ import fs from 'fs'; import path from 'path'; -/* eslint-disable curly, indent */ +/* eslint-disable curly */ interface ProfileOptions { preferences: Record; @@ -291,14 +291,14 @@ async function writePreferences(options: ProfileOptions): Promise { fs.promises.writeFile(path.join(options.path, 'user.js'), lines.join('\n')), // Create a backup of the preferences file if it already exitsts. fs.promises.access(prefsPath, fs.constants.F_OK).then( - async () => { - await fs.promises.copyFile( - prefsPath, - path.join(options.path, 'prefs.js.playwright') - ); - }, - // Swallow only if file does not exist - () => {} + async () => { + await fs.promises.copyFile( + prefsPath, + path.join(options.path, 'prefs.js.playwright') + ); + }, + // Swallow only if file does not exist + () => {} ), ]); for (const command of result) { diff --git a/packages/playwright-core/src/server/chromium/crCoverage.ts b/packages/playwright-core/src/server/chromium/crCoverage.ts index cf68dd59f45ef..8946754c9b4ed 100644 --- a/packages/playwright-core/src/server/chromium/crCoverage.ts +++ b/packages/playwright-core/src/server/chromium/crCoverage.ts @@ -238,9 +238,9 @@ class CSSCoverage { } function convertToDisjointRanges(nestedRanges: { - startOffset: number; - endOffset: number; - count: number; }[]): { start: number; end: number; }[] { + startOffset: number; + endOffset: number; + count: number; }[]): { start: number; end: number; }[] { const points = []; for (const range of nestedRanges) { points.push({ offset: range.startOffset, type: 0, range }); diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index b695b35781f55..891de22817dfa 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -918,10 +918,10 @@ export class Frame extends SdkObject { } async addScriptTag(params: { - url?: string, - content?: string, - type?: string, - }): Promise { + url?: string, + content?: string, + type?: string, + }): Promise { const { url = null, content = null, diff --git a/packages/playwright-core/src/server/harBackend.ts b/packages/playwright-core/src/server/harBackend.ts index a8ebb2e54d7f2..8e990e7f4a912 100644 --- a/packages/playwright-core/src/server/harBackend.ts +++ b/packages/playwright-core/src/server/harBackend.ts @@ -39,12 +39,13 @@ export class HarBackend { } async lookup(url: string, method: string, headers: HeadersArray, postData: Buffer | undefined, isNavigationRequest: boolean): Promise<{ - action: 'error' | 'redirect' | 'fulfill' | 'noentry', - message?: string, - redirectURL?: string, - status?: number, - headers?: HeadersArray, - body?: Buffer }> { + action: 'error' | 'redirect' | 'fulfill' | 'noentry', + message?: string, + redirectURL?: string, + status?: number, + headers?: HeadersArray, + body?: Buffer + }> { let entry; try { entry = await this._harFindResponse(url, method, headers, postData); diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index f754fb775c8f5..bc9261b8d5862 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -32,14 +32,14 @@ interface TaggedAsElementHandle { type NoHandles = Arg extends TaggedAsJSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles } : Arg); type Unboxed = Arg extends TaggedAsElementHandle ? T : - Arg extends TaggedAsJSHandle ? T : - Arg extends NoHandles ? Arg : - Arg extends [infer A0] ? [Unboxed] : - Arg extends [infer A0, infer A1] ? [Unboxed, Unboxed] : - Arg extends [infer A0, infer A1, infer A2] ? [Unboxed, Unboxed, Unboxed] : - Arg extends Array ? Array> : - Arg extends object ? { [Key in keyof Arg]: Unboxed } : - Arg; + Arg extends TaggedAsJSHandle ? T : + Arg extends NoHandles ? Arg : + Arg extends [infer A0] ? [Unboxed] : + Arg extends [infer A0, infer A1] ? [Unboxed, Unboxed] : + Arg extends [infer A0, infer A1, infer A2] ? [Unboxed, Unboxed, Unboxed] : + Arg extends Array ? Array> : + Arg extends object ? { [Key in keyof Arg]: Unboxed } : + Arg; export type Func0 = string | (() => R | Promise); export type Func1 = string | ((arg: Unboxed) => R | Promise); export type FuncOn = string | ((on: On, arg2: Unboxed) => R | Promise); diff --git a/packages/playwright-core/src/server/utils/eventsHelper.ts b/packages/playwright-core/src/server/utils/eventsHelper.ts index bdf9a1502f4f9..a46a6d7d477a5 100644 --- a/packages/playwright-core/src/server/utils/eventsHelper.ts +++ b/packages/playwright-core/src/server/utils/eventsHelper.ts @@ -33,10 +33,10 @@ class EventsHelper { } static removeEventListeners(listeners: Array<{ - emitter: EventEmitter; - eventName: (string | symbol); - handler: (...args: any[]) => void; - }>) { + emitter: EventEmitter; + eventName: (string | symbol); + handler: (...args: any[]) => void; + }>) { for (const listener of listeners) listener.emitter.removeListener(listener.eventName, listener.handler); listeners.splice(0, listeners.length); diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index 0277a6dc3fb4a..7211692770046 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -653,15 +653,15 @@ function resolveFromEnv(name: string): string | undefined { // In addition to `outputFile` the function returns `outputDir` which should // be cleaned up if present by some reporters contract. export function resolveOutputFile(reporterName: string, options: { - configDir: string, - outputDir?: string, - fileName?: string, - outputFile?: string, - default?: { - fileName: string, - outputDir: string, - } - }): { outputFile: string, outputDir?: string } | undefined { + configDir: string, + outputDir?: string, + fileName?: string, + outputFile?: string, + default?: { + fileName: string, + outputDir: string, + } +}): { outputFile: string, outputDir?: string } | undefined { const name = reporterName.toUpperCase(); let outputFile = resolveFromEnv(`PLAYWRIGHT_${name}_OUTPUT_FILE`); if (!outputFile && options.outputFile) diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts index 7915405bd964e..ce147dc3384c9 100644 --- a/packages/playwright/src/runner/watchMode.ts +++ b/packages/playwright/src/runner/watchMode.ts @@ -301,9 +301,9 @@ function readKeyPress(handler: (text: string, key: any) => T | const isInterrupt = (text: string, key: any) => text === '\x03' || text === '\x1B' || (key && key.name === 'escape') || (key && key.ctrl && key.name === 'c'); async function runTests(watchOptions: WatchModeOptions, testServerConnection: TestServerConnection, options?: { - title?: string, - testIds?: string[], - }) { + title?: string, + testIds?: string[], +}) { printConfiguration(watchOptions, options?.title); const waitForDone = readKeyPress((text: string, key: any) => { diff --git a/tests/library/browsercontext-fetch-happy-eyeballs.spec.ts b/tests/library/browsercontext-fetch-happy-eyeballs.spec.ts index e9de33bcfb5f8..974d096e797b5 100644 --- a/tests/library/browsercontext-fetch-happy-eyeballs.spec.ts +++ b/tests/library/browsercontext-fetch-happy-eyeballs.spec.ts @@ -58,8 +58,10 @@ it('get should work on request fixture', async ({ request, server }) => { }); it('https post should work with ignoreHTTPSErrors option', async ({ context, httpsServer }) => { - const response = await context.request.post(httpsServer.EMPTY_PAGE, - { ignoreHTTPSErrors: true, __testHookLookup } as any); + const response = await context.request.post(httpsServer.EMPTY_PAGE, { + ignoreHTTPSErrors: true, + __testHookLookup + } as any); expect(response.status()).toBe(200); expect(interceptedHostnameLookup).toBe('localhost'); }); diff --git a/tests/library/chromium/extensions.spec.ts b/tests/library/chromium/extensions.spec.ts index ef6782da43b68..feb88267f4f49 100644 --- a/tests/library/chromium/extensions.spec.ts +++ b/tests/library/chromium/extensions.spec.ts @@ -20,22 +20,22 @@ import { playwrightTest as base, expect } from '../../config/browserTest'; const it = base.extend<{ launchPersistentContext: (extensionPath: string, options?: Parameters[1]) => Promise; - }>({ - launchPersistentContext: async ({ browserType }, use) => { - const browsers: BrowserContext[] = []; - await use(async (extensionPath, options = {}) => { - const extensionOptions = { - ...options, - args: [ - `--disable-extensions-except=${extensionPath}`, - `--load-extension=${extensionPath}`, - ], - }; - return await browserType.launchPersistentContext('', extensionOptions); - }); - await Promise.all(browsers.map(browser => browser.close())); - } - }); +}>({ + launchPersistentContext: async ({ browserType }, use) => { + const browsers: BrowserContext[] = []; + await use(async (extensionPath, options = {}) => { + const extensionOptions = { + ...options, + args: [ + `--disable-extensions-except=${extensionPath}`, + `--load-extension=${extensionPath}`, + ], + }; + return await browserType.launchPersistentContext('', extensionOptions); + }); + await Promise.all(browsers.map(browser => browser.close())); + } +}); it.skip(({ isHeadlessShell }) => isHeadlessShell, 'Headless Shell has no support for extensions'); diff --git a/tests/page/page-request-fulfill.spec.ts b/tests/page/page-request-fulfill.spec.ts index 6eef1eb9bd61e..7e5f0f9caf8c4 100644 --- a/tests/page/page-request-fulfill.spec.ts +++ b/tests/page/page-request-fulfill.spec.ts @@ -25,15 +25,15 @@ const it = base.extend<{ // which is actually forwarded to the desktop localhost. // To use request such an url with apiRequestContext on the desktop, we need to change it back to localhost. rewriteAndroidLoopbackURL(url: string): string - }>({ - rewriteAndroidLoopbackURL: ({ isAndroid }, use) => use(givenURL => { - if (!isAndroid) - return givenURL; - const requestURL = new URL(givenURL); - requestURL.hostname = 'localhost'; - return requestURL.toString(); - }) - }); +}>({ + rewriteAndroidLoopbackURL: ({ isAndroid }, use) => use(givenURL => { + if (!isAndroid) + return givenURL; + const requestURL = new URL(givenURL); + requestURL.hostname = 'localhost'; + return requestURL.toString(); + }) +}); it('should work', async ({ page, server }) => { await page.route('**/*', route => { diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 3b17948081000..e16e6d753ea7d 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -33,18 +33,18 @@ const NEGATIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? 'x ' : '✘ '; const test = baseTest.extend<{ showReport: (reportFolder?: string) => Promise - }>({ - showReport: async ({ page }, use) => { - let server: HttpServer | undefined; - await use(async (reportFolder?: string) => { - reportFolder ??= test.info().outputPath('playwright-report'); - server = startHtmlReportServer(reportFolder) as HttpServer; - await server.start(); - await page.goto(server.urlPrefix('precise')); - }); - await server?.stop(); - } - }); +}>({ + showReport: async ({ page }, use) => { + let server: HttpServer | undefined; + await use(async (reportFolder?: string) => { + reportFolder ??= test.info().outputPath('playwright-report'); + server = startHtmlReportServer(reportFolder) as HttpServer; + await server.start(); + await page.goto(server.urlPrefix('precise')); + }); + await server?.stop(); + } +}); test.use({ channel: 'chrome' }); test.slow(!!process.env.CI); From fa01f2b27c1e102acfe29bb98a7d1aecbfbb56b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:46:27 +0200 Subject: [PATCH 029/329] chore(deps): bump actions/setup-java from 4 to 5 in the actions group (#37191) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/infra.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/infra.yml b/.github/workflows/infra.yml index af92b588694a8..5eca82d8d5fc1 100644 --- a/.github/workflows/infra.yml +++ b/.github/workflows/infra.yml @@ -50,7 +50,7 @@ jobs: - uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: '21' From 69dd0bfd6e342403079c70a1a5f74266f057beed Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 26 Aug 2025 17:40:11 +0200 Subject: [PATCH 030/329] docs: update Windows requirements (#37166) --- docs/src/intro-csharp.md | 2 +- docs/src/intro-java.md | 2 +- docs/src/intro-js.md | 2 +- docs/src/intro-python.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/intro-csharp.md b/docs/src/intro-csharp.md index f48722c0706c9..15d507e54dace 100644 --- a/docs/src/intro-csharp.md +++ b/docs/src/intro-csharp.md @@ -284,7 +284,7 @@ See our doc on [Running and Debugging Tests](./running-tests.md) to learn more a ## System requirements - Playwright is distributed as a .NET Standard 2.0 library. We recommend .NET 8. -- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). +- Windows 11+, Windows Server 2019+ or Windows Subsystem for Linux (WSL). - macOS 14 Ventura, or later. - Debian 12, Debian 13, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. diff --git a/docs/src/intro-java.md b/docs/src/intro-java.md index 35530dd3eb3f2..12ce2702e185a 100644 --- a/docs/src/intro-java.md +++ b/docs/src/intro-java.md @@ -129,7 +129,7 @@ By default browsers launched with Playwright run headless, meaning no browser UI ## System requirements - Java 8 or higher. -- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). +- Windows 11+, Windows Server 2019+ or Windows Subsystem for Linux (WSL). - macOS 14 Ventura, or later. - Debian 12, Debian 13, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. diff --git a/docs/src/intro-js.md b/docs/src/intro-js.md index a9bce9752a29a..33827ae864e1b 100644 --- a/docs/src/intro-js.md +++ b/docs/src/intro-js.md @@ -305,7 +305,7 @@ pnpm exec playwright --version ## System requirements - Node.js: latest 20.x, 22.x or 24.x. -- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). +- Windows 11+, Windows Server 2019+ or Windows Subsystem for Linux (WSL). - macOS 14 (Ventura) or later. - Debian 12 / 13, Ubuntu 22.04 / 24.04 (x86-64 or arm64). diff --git a/docs/src/intro-python.md b/docs/src/intro-python.md index b85fad8361c28..4045b6f4207de 100644 --- a/docs/src/intro-python.md +++ b/docs/src/intro-python.md @@ -100,7 +100,7 @@ pip install pytest-playwright playwright -U ## System requirements - Python 3.8 or higher. -- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). +- Windows 11+, Windows Server 2019+ or Windows Subsystem for Linux (WSL). - macOS 14 Ventura, or later. - Debian 12, Debian 13, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. From 64983cf0ecdc22c0c19b72d6acf1237b3e06d697 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 26 Aug 2025 17:56:22 +0200 Subject: [PATCH 031/329] chore: update TypeScript to 5.9 (#37182) --- package-lock.json | 8 ++++---- package.json | 2 +- packages/playwright-ct-core/src/router.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 438ceb7c153b2..c021c6c6039a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,7 +57,7 @@ "react": "^18.1.0", "react-dom": "^18.1.0", "ssim.js": "^3.5.0", - "typescript": "^5.8.2", + "typescript": "^5.9.2", "vite": "^6.3.4", "ws": "^8.17.1", "xml2js": "^0.5.0", @@ -8112,9 +8112,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index c802aaccd4d3e..6948430c30baa 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "react": "^18.1.0", "react-dom": "^18.1.0", "ssim.js": "^3.5.0", - "typescript": "^5.8.2", + "typescript": "^5.9.2", "vite": "^6.3.4", "ws": "^8.17.1", "xml2js": "^0.5.0", diff --git a/packages/playwright-ct-core/src/router.ts b/packages/playwright-ct-core/src/router.ts index 5ff746ddf29dc..b27d5242b283e 100644 --- a/packages/playwright-ct-core/src/router.ts +++ b/packages/playwright-ct-core/src/router.ts @@ -105,7 +105,7 @@ export class Router { headers.append(name, value); const buffer = request.postDataBuffer(); - const body = buffer?.byteLength ? new Int8Array(buffer.buffer, buffer.byteOffset, buffer.length) : undefined; + const body = buffer?.byteLength ? new Int8Array(buffer.buffer as ArrayBuffer, buffer.byteOffset, buffer.length) : undefined; const newRequest = new Request(request.url(), { body: body, From 9dd68d6a1e83cd2a98188cbfd85a0b475fef1834 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 26 Aug 2025 09:00:04 -0700 Subject: [PATCH 032/329] chore: fix evaluate on pause (#37177) --- packages/injected/src/consoleApi.ts | 5 +++-- packages/playwright/src/mcp/browser/tools.ts | 13 +++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/injected/src/consoleApi.ts b/packages/injected/src/consoleApi.ts index 2f2da420b280f..2406c2e894061 100644 --- a/packages/injected/src/consoleApi.ts +++ b/packages/injected/src/consoleApi.ts @@ -21,6 +21,7 @@ import { escapeForTextSelector } from '@isomorphic/stringUtils'; import type { InjectedScript } from './injectedScript'; import type { Language } from '@isomorphic/locatorGenerators'; import type { ByRoleOptions } from '@isomorphic/locatorUtils'; +import type { AriaTreeOptions } from './ariaSnapshot'; const selectorSymbol = Symbol('selector'); @@ -91,8 +92,8 @@ export class ConsoleAPI { inspect: (selector: string) => this._inspect(selector), selector: (element: Element) => this._selector(element), generateLocator: (element: Element, language?: Language) => this._generateLocator(element, language), - ariaSnapshot: (element?: Element) => { - return this._injectedScript.ariaSnapshot(element || this._injectedScript.document.body, { mode: 'expect' }); + ariaSnapshot: (element?: Element, options?: AriaTreeOptions) => { + return this._injectedScript.ariaSnapshot(element || this._injectedScript.document.body, options || { mode: 'expect' }); }, resume: () => this._resume(), ...new Locator(this._injectedScript, ''), diff --git a/packages/playwright/src/mcp/browser/tools.ts b/packages/playwright/src/mcp/browser/tools.ts index c9ddcccec9c06..69e20772a4068 100644 --- a/packages/playwright/src/mcp/browser/tools.ts +++ b/packages/playwright/src/mcp/browser/tools.ts @@ -91,15 +91,12 @@ export const evaluate = defineTool({ }, handle: async (page, params) => { - if (params.ref && params.element) { - const locator = await refLocator(page, { ref: params.ref, element: params.element }); - const result = await locator.evaluate(params.function); - return { - content: [{ type: 'text', text: JSON.stringify(result, null, 2) || 'undefined' }], - }; - } + let locator: playwright.Locator | undefined; + if (params.ref && params.element) + locator = await refLocator(page, { ref: params.ref, element: params.element }); - const result = await page.evaluate(params.function); + const receiver = locator ?? page as any; + const result = await receiver._evaluateFunction(params.function); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) || 'undefined' }], }; From 2275968a942cae93d3f1eaf127fb4fde711474cb Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 26 Aug 2025 11:56:22 -0700 Subject: [PATCH 033/329] chore: add placeholder for inputs in the snapshots (#37193) --- packages/injected/src/ariaSnapshot.ts | 5 +++++ tests/page/page-aria-snapshot.spec.ts | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 09c3bef843a7d..8c06d9e2e315d 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -188,6 +188,11 @@ export function generateAriaTree(rootElement: Element, publicOptions: AriaTreeOp const href = element.getAttribute('href')!; ariaNode.props['url'] = href; } + + if (ariaNode.role === 'textbox' && element.hasAttribute('placeholder') && element.getAttribute('placeholder') !== ariaNode.name) { + const placeholder = element.getAttribute('placeholder')!; + ariaNode.props['placeholder'] = placeholder; + } } roleUtils.beginAriaCaches(); diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts index e08f4830da0f6..5bb5cebc338e1 100644 --- a/tests/page/page-aria-snapshot.spec.ts +++ b/tests/page/page-aria-snapshot.spec.ts @@ -669,3 +669,20 @@ it('should not show unhidden children of aria-hidden elements', { annotation: { expect(await page.locator('body').ariaSnapshot()).toBe(''); }); + +it('should snapshot placeholder when different from the name', async ({ page }) => { + await page.setContent(` + + `); + expect(await page.locator('body').ariaSnapshot()).toContainYaml(` + - textbox "Placeholder" + `); + + await page.setContent(` + + `); + expect(await page.locator('body').ariaSnapshot()).toContainYaml(` + - textbox "Label": + - /placeholder: Placeholder + `); +}); From 0c92f5e5f77be445657eefe45d0e74abd40e8734 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 26 Aug 2025 13:16:17 -0700 Subject: [PATCH 034/329] chore: use non-aria visibility for ai snapshots (#37194) --- packages/injected/src/ariaSnapshot.ts | 8 ++++---- tests/page/page-aria-snapshot-ai.spec.ts | 21 ++++++--------------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 8c06d9e2e315d..1fcde612246da 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -54,7 +54,7 @@ export type AriaTreeOptions = { }; type InternalOptions = { - visibility: 'aria' | 'ariaOrVisible' | 'ariaAndVisible', + visibility: 'aria' | 'visible' | 'ariaAndVisible', refs: 'all' | 'interactable' | 'none', refPrefix?: string, includeGenericRole?: boolean, @@ -67,7 +67,7 @@ function toInternalOptions(options: AriaTreeOptions): InternalOptions { if (options.mode === 'ai') { // For AI consumption. return { - visibility: 'ariaOrVisible', + visibility: 'visible', refs: 'interactable', refPrefix: options.refPrefix, includeGenericRole: true, @@ -119,8 +119,8 @@ export function generateAriaTree(rootElement: Element, publicOptions: AriaTreeOp const element = node as Element; const isElementVisibleForAria = !roleUtils.isElementHiddenForAria(element); let visible = isElementVisibleForAria; - if (options.visibility === 'ariaOrVisible') - visible = isElementVisibleForAria || isElementVisible(element); + if (options.visibility === 'visible') + visible = isElementVisible(element); if (options.visibility === 'ariaAndVisible') visible = isElementVisibleForAria && isElementVisible(element); diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index c7afd03cf34da..86b6f344ad491 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -17,7 +17,7 @@ // @ts-ignore import { asLocator } from 'playwright-core/lib/utils'; -import { test as it, expect, unshift } from './pageTest'; +import { test as it, expect } from './pageTest'; function snapshotForAI(page: any, options?: { timeout?: number }): Promise { return page._snapshotForAI(options); @@ -184,18 +184,9 @@ it('emit generic roles for nodes w/o roles', async ({ page }) => { expect(snapshot).toContainYaml(` - generic [ref=e2]: - - generic [ref=e3]: - - generic [ref=e4]: - - radio "Apple" [checked] - - generic [ref=e5]: Apple - - generic [ref=e6]: - - generic [ref=e7]: - - radio "Pear" - - generic [ref=e8]: Pear - - generic [ref=e9]: - - generic [ref=e10]: - - radio "Orange" - - generic [ref=e11]: Orange + - generic [ref=e5]: Apple + - generic [ref=e8]: Pear + - generic [ref=e11]: Orange `); }); @@ -281,11 +272,11 @@ it('should show visible children of hidden elements', { annotation: { type: 'iss
`); - expect(await snapshotForAI(page)).toEqual(unshift(` + expect(await snapshotForAI(page)).toContainYaml(` - generic [active] [ref=e1]: - button "Visible" [ref=e3] - button "Visible" [ref=e4] - `)); + `); }); it('should include active element information', async ({ page }) => { From f52c5057381e0e7acaa23f4fbc11db09bb0fbc15 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 26 Aug 2025 21:34:19 +0100 Subject: [PATCH 035/329] chore: move actions filter from settings to actions toolbar (#37186) --- .../src/ui/defaultSettingsView.tsx | 20 ------- .../src/ui/settingsToolbarButton.tsx | 52 ------------------- packages/trace-viewer/src/ui/workbench.tsx | 31 +++++++++++ .../trace-viewer/src/ui/workbenchLoader.tsx | 8 +-- packages/web/src/components/dialog.tsx | 39 +++++++++++++- tests/config/traceViewerFixtures.ts | 10 ++-- tests/playwright-test/ui-mode-trace.spec.ts | 9 ++-- 7 files changed, 84 insertions(+), 85 deletions(-) delete mode 100644 packages/trace-viewer/src/ui/settingsToolbarButton.tsx diff --git a/packages/trace-viewer/src/ui/defaultSettingsView.tsx b/packages/trace-viewer/src/ui/defaultSettingsView.tsx index 5c12f823451c4..9d555708789cf 100644 --- a/packages/trace-viewer/src/ui/defaultSettingsView.tsx +++ b/packages/trace-viewer/src/ui/defaultSettingsView.tsx @@ -18,7 +18,6 @@ import * as React from 'react'; import { SettingsView } from './settingsView'; import { useDarkModeSetting } from '@web/theme'; import { useSetting } from '@web/uiUtils'; -import type { ActionGroup } from '@isomorphic/protocolFormatter'; /** * A view of the collection of standard settings used between various applications @@ -28,7 +27,6 @@ export const DefaultSettingsView: React.FC<{}> = () => { shouldPopulateCanvasFromScreenshot, setShouldPopulateCanvasFromScreenshot, ] = useSetting('shouldPopulateCanvasFromScreenshot', false); - const [actionsFilter, setActionsFilter] = useSetting('actionsFilter', []); const [darkMode, setDarkMode] = useDarkModeSetting(); return ( @@ -47,24 +45,6 @@ export const DefaultSettingsView: React.FC<{}> = () => { name: 'Display canvas content', title: 'Attempt to display the captured canvas appearance in the snapshot preview. May not be accurate.', }, - { - type: 'check', - value: actionsFilter.includes('getter'), - set: value => setActionsFilter(value ? [...actionsFilter, 'getter'] : actionsFilter.filter(a => a !== 'getter')), - name: 'Show getter actions', - }, - { - type: 'check', - value: actionsFilter.includes('route'), - set: value => setActionsFilter(value ? [...actionsFilter, 'route'] : actionsFilter.filter(a => a !== 'route')), - name: 'Show route actions', - }, - { - type: 'check', - value: actionsFilter.includes('configuration'), - set: value => setActionsFilter(value ? [...actionsFilter, 'configuration'] : actionsFilter.filter(a => a !== 'configuration')), - name: 'Show configuration actions', - }, ]} /> ); diff --git a/packages/trace-viewer/src/ui/settingsToolbarButton.tsx b/packages/trace-viewer/src/ui/settingsToolbarButton.tsx deleted file mode 100644 index d4474007767d9..0000000000000 --- a/packages/trace-viewer/src/ui/settingsToolbarButton.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - Copyright (c) Microsoft Corporation. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -import * as React from 'react'; -import { Dialog } from '@web/components/dialog'; -import { ToolbarButton } from '@web/components/toolbarButton'; -import { DefaultSettingsView } from './defaultSettingsView'; - -export const SettingsToolbarButton: React.FC<{}> = () => { - const hostingRef = React.useRef(null); - - const [open, setOpen] = React.useState(false); - - return ( - <> - setOpen(current => !current)} - /> - setOpen(false)} - anchor={hostingRef} - dataTestId='settings-toolbar-dialog' - > - - - - ); -}; diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 9c0a3fccc34d0..2581b672cb0d5 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -45,6 +45,8 @@ import type { HighlightedElement } from './snapshotTab'; import type { TestAnnotation } from '@playwright/test'; import { MetadataWithCommitInfo } from '@testIsomorphic/types'; import type { ActionGroup } from '@isomorphic/protocolFormatter'; +import { DialogToolbarButton } from '@web/components/dialog'; +import { SettingsView } from './settingsView'; export const Workbench: React.FunctionComponent<{ model?: modelUtil.MultiTraceModel, @@ -358,6 +360,7 @@ export const Workbench: React.FunctionComponent<{ tabs={[actionsTab, metadataTab]} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab} + rightToolbar={[]} /> } />} @@ -379,3 +382,31 @@ export const Workbench: React.FunctionComponent<{ />
; }; + +const ActionsFilterButton: React.FC<{}> = () => { + const [actionsFilter, setActionsFilter] = useSetting('actionsFilter', []); + return + setActionsFilter(value ? [...actionsFilter, 'getter'] : actionsFilter.filter(a => a !== 'getter')), + name: 'Getters', + }, + { + type: 'check', + value: actionsFilter.includes('route'), + set: value => setActionsFilter(value ? [...actionsFilter, 'route'] : actionsFilter.filter(a => a !== 'route')), + name: 'Network routes', + }, + { + type: 'check', + value: actionsFilter.includes('configuration'), + set: value => setActionsFilter(value ? [...actionsFilter, 'configuration'] : actionsFilter.filter(a => a !== 'configuration')), + name: 'Configuration', + }, + ]} + /> + ; +}; diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx index 747ffa4e3f9b8..06ae35afa5718 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -20,8 +20,8 @@ import { MultiTraceModel } from './modelUtil'; import './workbenchLoader.css'; import { Workbench } from './workbench'; import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection'; -import { SettingsToolbarButton } from './settingsToolbarButton'; -import { Dialog } from '@web/components/dialog'; +import { Dialog, DialogToolbarButton } from '@web/components/dialog'; +import { DefaultSettingsView } from './defaultSettingsView'; export const WorkbenchLoader: React.FunctionComponent<{ }> = () => { @@ -193,7 +193,9 @@ export const WorkbenchLoader: React.FunctionComponent<{
Playwright
{model.title &&
{model.title}
}
- + + +
{fileForLocalModeError &&
diff --git a/packages/web/src/components/dialog.tsx b/packages/web/src/components/dialog.tsx index 9c73b509eae60..4e2972e4f6a57 100644 --- a/packages/web/src/components/dialog.tsx +++ b/packages/web/src/components/dialog.tsx @@ -15,6 +15,7 @@ */ import * as React from 'react'; +import { ToolbarButton } from './toolbarButton'; export interface DialogProps { className?: string; @@ -56,7 +57,7 @@ export const Dialog: React.FC> = ({ top: bounds.bottom + (verticalOffset ?? 0), left: buildTopLeftCoord(bounds, width ?? 0), width, - zIndex: 1, + zIndex: 100, ...externalStyle }; } @@ -163,3 +164,39 @@ const buildTopLeftCoordWithAlignment = ( }; } }; + +export interface DialogToolbarButtonProps { + title?: string; + icon?: string; + dialogDataTestId?: string; +} + +export const DialogToolbarButton: React.FC> = ({ title, icon, dialogDataTestId, children }) => { + const hostingRef = React.useRef(null); + const [open, setOpen] = React.useState(false); + return ( + <> + setOpen(current => !current)} + /> + setOpen(false)} + anchor={hostingRef} + dataTestId={dialogDataTestId} + > + {children} + + + ); +}; diff --git a/tests/config/traceViewerFixtures.ts b/tests/config/traceViewerFixtures.ts index 6d839675bae83..9dedc6c579ca7 100644 --- a/tests/config/traceViewerFixtures.ts +++ b/tests/config/traceViewerFixtures.ts @@ -73,11 +73,11 @@ class TraceViewerPage { @step async showAllActions() { - await this.page.getByRole('button', { name: 'Settings' }).click(); - await this.page.locator('.setting').getByText('Show route actions').click(); - await this.page.locator('.setting').getByText('Show getter actions').click(); - await this.page.locator('.setting').getByText('Show configuration actions').click(); - await this.page.getByRole('button', { name: 'Settings' }).click(); + await this.page.getByRole('button', { name: 'Filter actions' }).click(); + await this.page.locator('.setting').getByText('Network routes').click(); + await this.page.locator('.setting').getByText('Getters').click(); + await this.page.locator('.setting').getByText('Configuration').click(); + await this.page.getByRole('button', { name: 'Filter actions' }).click(); } stackFrames(options: { selected?: boolean } = {}) { diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index 56c4607bef23b..48fd3873e5b4b 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -506,10 +506,11 @@ test('should hide boxed fixtures and contents, reveal upon show all actions sett - treeitem /After Hooks/ `); - await page.getByText('Settings').click(); - await page.getByText('Show route actions').click(); - await page.getByText('Show configuration actions').click(); - await page.getByText('Show getter actions').click(); + await page.getByRole('button', { name: 'Filter actions' }).click(); + await page.locator('.setting').getByText('Network routes').click(); + await page.locator('.setting').getByText('Getters').click(); + await page.locator('.setting').getByText('Configuration').click(); + await page.getByRole('button', { name: 'Filter actions' }).click(); await page.getByTestId('actions-tree').getByRole('treeitem', { name: 'Before Hooks' }).locator('.codicon-chevron-right').click(); await page.getByTestId('actions-tree').getByRole('treeitem', { name: 'Fixture "fixture"' }).locator('.codicon-chevron-right').click(); From a114f92a3c185b57c9dd15c210c8fa2275bc83b5 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 26 Aug 2025 14:03:01 -0700 Subject: [PATCH 036/329] chore: only render top-level cursor=pointer (#37196) --- packages/injected/src/ariaSnapshot.ts | 13 ++++++++----- tests/page/page-aria-snapshot-ai.spec.ts | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 1fcde612246da..ad4ccbd23351b 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -469,7 +469,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr const lines: string[] = []; const includeText = options.renderStringsAsRegex ? textContributesInfo : () => true; const renderString = options.renderStringsAsRegex ? convertToBestGuessRegex : (str: string) => str; - const visit = (ariaNode: AriaNode | string, parentAriaNode: AriaNode | null, indent: string) => { + const visit = (ariaNode: AriaNode | string, parentAriaNode: AriaNode | null, indent: string, renderCursorPointer: boolean) => { if (typeof ariaNode === 'string') { if (parentAriaNode && !includeText(parentAriaNode, ariaNode)) return; @@ -507,10 +507,13 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr if (ariaNode.selected === true) key += ` [selected]`; + let inCursorPointer = false; if (ariaNode.ref) { key += ` [ref=${ariaNode.ref}]`; - if (options.renderCursorPointer && hasPointerCursor(ariaNode)) + if (renderCursorPointer && hasPointerCursor(ariaNode)) { + inCursorPointer = true; key += ' [cursor=pointer]'; + } } const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded(key); @@ -528,7 +531,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr for (const [name, value] of Object.entries(ariaNode.props)) lines.push(indent + ' - /' + name + ': ' + yamlEscapeValueIfNeeded(value)); for (const child of ariaNode.children || []) - visit(child, ariaNode, indent + ' '); + visit(child, ariaNode, indent + ' ', renderCursorPointer && !inCursorPointer); } }; @@ -536,9 +539,9 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr if (ariaNode.role === 'fragment') { // Render fragment. for (const child of ariaNode.children || []) - visit(child, ariaNode, ''); + visit(child, ariaNode, '', !!options.renderCursorPointer); } else { - visit(ariaNode, null, ''); + visit(ariaNode, null, '', !!options.renderCursorPointer); } return lines.join('\n'); } diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 86b6f344ad491..1493f65291bc4 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -218,6 +218,23 @@ it('should include cursor pointer hint', async ({ page }) => { `); }); +it('should not nest cursor pointer hints', async ({ page }) => { + await page.setContent(` + + Link with a button + + + `); + + const snapshot = await snapshotForAI(page); + expect(snapshot).toContainYaml(` + - link \"Link with a button Button\" [ref=e2] [cursor=pointer]: + - /url: about:blank + - text: Link with a button + - button "Button" [ref=e3] + `); +}); + it('should gracefully fallback when child frame cant be captured', async ({ page, server }) => { await page.setContent(`

Test

From ab4a113b5c2b5a744b14bbdb0c769adae815cde3 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 27 Aug 2025 11:19:03 +0100 Subject: [PATCH 037/329] test: unflake a few tests (#37200) --- tests/library/debug-controller.spec.ts | 10 +++++++--- tests/library/inspector/inspectorTest.ts | 2 +- tests/library/permissions.spec.ts | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/library/debug-controller.spec.ts b/tests/library/debug-controller.spec.ts index 115ace0cb4020..1e72ee9b5fc27 100644 --- a/tests/library/debug-controller.spec.ts +++ b/tests/library/debug-controller.spec.ts @@ -76,15 +76,19 @@ const test = baseTest.extend({ test.slow(true, 'All controller tests are slow'); test.skip(({ mode }) => mode.startsWith('service') || mode === 'driver'); +// Force a separate worker to avoid registered selector engines from other tests. +// See https://github.com/microsoft/playwright/pull/37103. +test.use({ launchOptions: {} }); + test('should pick element', async ({ backend, connectedBrowser }) => { const events = []; backend.on('inspectRequested', event => events.push(event)); - const context = await connectedBrowser.newContextForReuse(); - const page = await context.newPage(); - await backend.setRecorderMode({ mode: 'inspecting' }); + const context = await connectedBrowser.newContextForReuse(); + const [page] = context.pages(); + await page.setContent(''); await page.getByRole('button').click(); await page.getByRole('button').click(); diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index f85a5607073a9..0f0ec91910995 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -273,7 +273,7 @@ class CLIMock { @step async waitFor(text: string): Promise { - await expect.poll(() => this.text()).toContain(text); + await expect.poll(() => this.text(), { timeout: 30000 }).toContain(text); } @step diff --git a/tests/library/permissions.spec.ts b/tests/library/permissions.spec.ts index cd2ab184b19ce..b723aee813971 100644 --- a/tests/library/permissions.spec.ts +++ b/tests/library/permissions.spec.ts @@ -241,8 +241,9 @@ it.describe(() => { // Secure context it.use({ ignoreHTTPSErrors: true, }); - it('should be able to use the local-fonts API', async ({ page, context, httpsServer, browserName }) => { + it('should be able to use the local-fonts API', async ({ page, context, httpsServer, browserName, channel }) => { it.skip(browserName !== 'chromium', 'chromium-only api'); + it.fixme(!!channel && channel.startsWith('msedge'), 'always times out in edge'); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/36113' }); await page.goto(httpsServer.EMPTY_PAGE); From b89bb31bd6785f1dc5ffb6629bf0476d18c332d8 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 27 Aug 2025 12:57:08 +0100 Subject: [PATCH 038/329] chore: enable new screenshots implementation in Chromium (#37201) --- .../playwright-core/src/server/chromium/chromiumSwitches.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts index cedf70082c0f8..3a4550a2713a9 100644 --- a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts +++ b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts @@ -58,7 +58,7 @@ export const chromiumSwitches = (assistantMode?: boolean, channel?: string) => [ '--disable-dev-shm-usage', '--disable-extensions', '--disable-features=' + disabledFeatures(assistantMode).join(','), - channel === 'chromium-tip-of-tree' ? '--enable-features=CDPScreenshotNewSurface' : '', + '--enable-features=CDPScreenshotNewSurface', '--allow-pre-commit-input', '--disable-hang-monitor', '--disable-ipc-flooding-protection', From f9cffd6cb1f1db9a11d98f9f94b66e6101a52698 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 27 Aug 2025 05:31:34 -0700 Subject: [PATCH 039/329] fix(html): don't display a chip with empty content with no projects (#37153) --- packages/html-reporter/src/labels.tsx | 5 ++++- tests/playwright-test/reporter-html.spec.ts | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/html-reporter/src/labels.tsx b/packages/html-reporter/src/labels.tsx index 8e1e6b5b6600d..253518eaa958d 100644 --- a/packages/html-reporter/src/labels.tsx +++ b/packages/html-reporter/src/labels.tsx @@ -44,7 +44,10 @@ export const ProjectAndTagLabelsView: React.FC<{ useLinks?: boolean, style?: React.CSSProperties, }> = ({ projectNames, activeProjectName, otherLabels, useLinks, style }) => { - return (projectNames.length > 0 || otherLabels.length > 0) && + // We can have an empty project name if we have no projects specified in the config + const hasProjectNames = projectNames.length > 0 && !!activeProjectName; + + return (hasProjectNames || otherLabels.length > 0) && {!!useLinks ? : } ; diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 24c6bbea1179c..6ee8959a00bf9 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -1717,6 +1717,25 @@ for (const useIntermediateMergeReport of [true, false] as const) { await expect(page.locator('.label')).toHaveText('webkit'); }); + test('project label should not show if there are no explicit projects', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('pass', async ({}) => { + expect(1).toBe(1); + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + + await showReport(); + + await expect(page.locator('.test-file-test .label')).toHaveCount(0); + await expect(page.locator('.label-row')).not.toBeVisible(); + }); + test('testCaseView - after click test label and go back, testCaseView should be visible', async ({ runInlineTest, showReport, page }) => { const result = await runInlineTest({ 'playwright.config.js': ` From 192aa8eb9405825003504a84570cdecac8b505d1 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 27 Aug 2025 18:36:51 +0200 Subject: [PATCH 040/329] feat(chromium-tip-of-tree): roll to r1362 (#37210) --- packages/playwright-core/browsers.json | 8 ++++---- tests/config/proxy.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index f7f054393eab2..c3ba948eeea16 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,15 +15,15 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1357", + "revision": "1362", "installByDefault": false, - "browserVersion": "141.0.7342.0" + "browserVersion": "141.0.7376.0" }, { "name": "chromium-tip-of-tree-headless-shell", - "revision": "1357", + "revision": "1362", "installByDefault": false, - "browserVersion": "141.0.7342.0" + "browserVersion": "141.0.7376.0" }, { "name": "firefox", diff --git a/tests/config/proxy.ts b/tests/config/proxy.ts index 9fdf336bc4b32..e1ac6ab5a3cbb 100644 --- a/tests/config/proxy.ts +++ b/tests/config/proxy.ts @@ -24,6 +24,7 @@ import { SocksProxy } from '../../packages/playwright-core/lib/server/utils/sock // Certain browsers perform telemetry requests which we want to ignore. const kConnectHostsToIgnore = new Set([ 'www.bing.com:443', + 'www.google.com:443', ]); export class TestProxy { From 80693a697dd377ffa6b9b1df593cef9b8e1d11ed Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:37:07 +0200 Subject: [PATCH 041/329] feat(chromium): roll to r1188 (#37212) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- README.md | 4 +- packages/playwright-core/browsers.json | 8 +- .../src/server/deviceDescriptorsSource.json | 108 +++++++++--------- 3 files changed, 60 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index b3441ab8c93a8..074a166b68402 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-140.0.7339.16-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-141.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-140.0.7339.24-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-141.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 140.0.7339.16 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 140.0.7339.24 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 26.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 141.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index c3ba948eeea16..421f3c1ecaf9b 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,15 +3,15 @@ "browsers": [ { "name": "chromium", - "revision": "1187", + "revision": "1188", "installByDefault": true, - "browserVersion": "140.0.7339.16" + "browserVersion": "140.0.7339.24" }, { "name": "chromium-headless-shell", - "revision": "1187", + "revision": "1188", "installByDefault": true, - "browserVersion": "140.0.7339.16" + "browserVersion": "140.0.7339.24" }, { "name": "chromium-tip-of-tree", diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index dc77eead35eae..df5d7497108a0 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -110,7 +110,7 @@ "defaultBrowserType": "webkit" }, "Galaxy S5": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -121,7 +121,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -132,7 +132,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 360, "height": 740 @@ -143,7 +143,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 740, "height": 360 @@ -154,7 +154,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 320, "height": 658 @@ -165,7 +165,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+ landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 658, "height": 320 @@ -176,7 +176,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S24": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 360, "height": 780 @@ -187,7 +187,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S24 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 780, "height": 360 @@ -198,7 +198,7 @@ "defaultBrowserType": "chromium" }, "Galaxy A55": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 480, "height": 1040 @@ -209,7 +209,7 @@ "defaultBrowserType": "chromium" }, "Galaxy A55 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 1040, "height": 480 @@ -220,7 +220,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", "viewport": { "width": 712, "height": 1138 @@ -231,7 +231,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", "viewport": { "width": 1138, "height": 712 @@ -242,7 +242,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S9": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", "viewport": { "width": 640, "height": 1024 @@ -253,7 +253,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S9 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", "viewport": { "width": 1024, "height": 640 @@ -1208,7 +1208,7 @@ "defaultBrowserType": "webkit" }, "LG Optimus L70": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1219,7 +1219,7 @@ "defaultBrowserType": "chromium" }, "LG Optimus L70 landscape": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1230,7 +1230,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1241,7 +1241,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1252,7 +1252,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1263,7 +1263,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1274,7 +1274,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", "viewport": { "width": 800, "height": 1280 @@ -1285,7 +1285,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", "viewport": { "width": 1280, "height": 800 @@ -1296,7 +1296,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1307,7 +1307,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1318,7 +1318,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1329,7 +1329,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1340,7 +1340,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1351,7 +1351,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1362,7 +1362,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1373,7 +1373,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1384,7 +1384,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1395,7 +1395,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1406,7 +1406,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", "viewport": { "width": 600, "height": 960 @@ -1417,7 +1417,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", "viewport": { "width": 960, "height": 600 @@ -1472,7 +1472,7 @@ "defaultBrowserType": "webkit" }, "Pixel 2": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 411, "height": 731 @@ -1483,7 +1483,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 731, "height": 411 @@ -1494,7 +1494,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 411, "height": 823 @@ -1505,7 +1505,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 823, "height": 411 @@ -1516,7 +1516,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 393, "height": 786 @@ -1527,7 +1527,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 786, "height": 393 @@ -1538,7 +1538,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 353, "height": 745 @@ -1549,7 +1549,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 745, "height": 353 @@ -1560,7 +1560,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G)": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "screen": { "width": 412, "height": 892 @@ -1575,7 +1575,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G) landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "screen": { "height": 892, "width": 412 @@ -1590,7 +1590,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "screen": { "width": 393, "height": 851 @@ -1605,7 +1605,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "screen": { "width": 851, "height": 393 @@ -1620,7 +1620,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "screen": { "width": 412, "height": 915 @@ -1635,7 +1635,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "screen": { "width": 915, "height": 412 @@ -1650,7 +1650,7 @@ "defaultBrowserType": "chromium" }, "Moto G4": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1661,7 +1661,7 @@ "defaultBrowserType": "chromium" }, "Moto G4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1672,7 +1672,7 @@ "defaultBrowserType": "chromium" }, "Desktop Chrome HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", "screen": { "width": 1792, "height": 1120 @@ -1687,7 +1687,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36 Edg/140.0.7339.16", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36 Edg/140.0.7339.24", "screen": { "width": 1792, "height": 1120 @@ -1732,7 +1732,7 @@ "defaultBrowserType": "webkit" }, "Desktop Chrome": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", "screen": { "width": 1920, "height": 1080 @@ -1747,7 +1747,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36 Edg/140.0.7339.16", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36 Edg/140.0.7339.24", "screen": { "width": 1920, "height": 1080 From 8bb781300cd5bbd85893135d4bb75fdb56564025 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 23:29:40 +0200 Subject: [PATCH 042/329] feat(chromium-tip-of-tree): roll to r1363 (#37213) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 421f3c1ecaf9b..c2b11841af441 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,15 +15,15 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1362", + "revision": "1363", "installByDefault": false, - "browserVersion": "141.0.7376.0" + "browserVersion": "141.0.7378.0" }, { "name": "chromium-tip-of-tree-headless-shell", - "revision": "1362", + "revision": "1363", "installByDefault": false, - "browserVersion": "141.0.7376.0" + "browserVersion": "141.0.7378.0" }, { "name": "firefox", From 36978d25e9fc4b9892780c694e21be425cfa8b41 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 27 Aug 2025 15:26:45 -0700 Subject: [PATCH 043/329] docs: fix method names in release notes (#37214) --- docs/src/release-notes-csharp.md | 2 +- docs/src/release-notes-java.md | 2 +- docs/src/release-notes-python.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index e6bc63a1f6fe5..f954e68cffca6 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -8,7 +8,7 @@ toc_max_heading_level: 2 ### Codegen -- Automatic `toBeVisible()` assertions: Codegen can now generate automatic `toBeVisible()` assertions for common UI interactions. This feature can be enabled in the Codegen settings UI. +- Automatic `ToBeVisibleAsync()` assertions: Codegen can now generate automatic `ToBeVisibleAsync()` assertions for common UI interactions. This feature can be enabled in the Codegen settings UI. ### Breaking Changes diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index ffff3b2420ed2..ed8d74746afb2 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -8,7 +8,7 @@ toc_max_heading_level: 2 ### Codegen -- Automatic `toBeVisible()` assertions: Codegen can now generate automatic `toBeVisible()` assertions for common UI interactions. This feature can be enabled in the Codegen settings UI. +- Automatic `isVisible()` assertions: Codegen can now generate automatic `isVisible()` assertions for common UI interactions. This feature can be enabled in the Codegen settings UI. ### Breaking Changes diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md index 0cfa77c926d5b..0ecd8eb89eed5 100644 --- a/docs/src/release-notes-python.md +++ b/docs/src/release-notes-python.md @@ -8,7 +8,7 @@ toc_max_heading_level: 2 ### Codegen -- Automatic `toBeVisible()` assertions: Codegen can now generate automatic `toBeVisible()` assertions for common UI interactions. This feature can be enabled in the Codegen settings UI. +- Automatic `to_be_visible()` assertions: Codegen can now generate automatic `to_be_visible()` assertions for common UI interactions. This feature can be enabled in the Codegen settings UI. ### Breaking Changes From 341611871ac1d86a3359a450c135114a00134565 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 27 Aug 2025 16:27:09 -0700 Subject: [PATCH 044/329] chore: recover from exceptions in matcher queries (#37215) --- packages/playwright/src/matchers/toBeTruthy.ts | 8 +++++++- packages/playwright/src/matchers/toEqual.ts | 7 ++++++- packages/playwright/src/matchers/toMatchText.ts | 8 +++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/playwright/src/matchers/toBeTruthy.ts b/packages/playwright/src/matchers/toBeTruthy.ts index 6f627c994c93b..7e3f1dbcaf2cb 100644 --- a/packages/playwright/src/matchers/toBeTruthy.ts +++ b/packages/playwright/src/matchers/toBeTruthy.ts @@ -40,7 +40,13 @@ export async function toBeTruthy( }; const timeout = options.timeout ?? this.timeout; - const { matches: pass, log, timedOut, received } = await query(!!this.isNot, timeout); + + const { matches: pass, log, timedOut, received } = await query(!!this.isNot, timeout).catch(async error => { + // FIXME: query should not throw, but it does for strict mode violations for example. + await runBrowserBackendOnError(receiver.page(), () => error.message); + throw error; + }); + if (pass === !this.isNot) { return { name: matcherName, diff --git a/packages/playwright/src/matchers/toEqual.ts b/packages/playwright/src/matchers/toEqual.ts index c188de257b266..410049b23abaa 100644 --- a/packages/playwright/src/matchers/toEqual.ts +++ b/packages/playwright/src/matchers/toEqual.ts @@ -48,7 +48,12 @@ export async function toEqual( const timeout = options.timeout ?? this.timeout; - const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout); + const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout).catch(async error => { + // FIXME: query should not throw, but it does for strict mode violations for example. + await runBrowserBackendOnError(receiver.page(), () => error.message); + throw error; + }); + if (pass === !this.isNot) { return { name: matcherName, diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index 712f8361a8aab..65630e2ffc700 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -60,7 +60,13 @@ export async function toMatchText( const timeout = options.timeout ?? this.timeout; - const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout); + const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout).catch(async error => { + // FIXME: query should not throw, but it does for strict mode violations for example. + if (receiverType === 'Locator') + await runBrowserBackendOnError((receiver as Locator).page(), () => error.message); + throw error; + }); + if (pass === !this.isNot) { return { name: matcherName, From 78b498cee9bf55945bceef6a103a95d5a3a65fbb Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 28 Aug 2025 14:24:22 +0200 Subject: [PATCH 045/329] chore(debug controller): add per-browser highlight + close (#37061) --- .../playwright-core/src/protocol/validator.ts | 6 ++++ .../src/server/debugController.ts | 25 +++++++++++---- .../dispatchers/debugControllerDispatcher.ts | 4 +++ .../src/utils/isomorphic/protocolMetainfo.ts | 1 + packages/protocol/src/channels.d.ts | 11 +++++++ packages/protocol/src/protocol.yml | 7 +++++ tests/library/debug-controller.spec.ts | 31 ++++++++++++++++++- 7 files changed, 78 insertions(+), 7 deletions(-) diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 7cc9066da70c0..f290f49e2dd6e 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -475,6 +475,7 @@ scheme.DebugControllerSetReportStateChangedParams = tObject({ }); scheme.DebugControllerSetReportStateChangedResult = tOptional(tObject({})); scheme.DebugControllerSetRecorderModeParams = tObject({ + browserId: tOptional(tString), mode: tEnum(['inspecting', 'recording', 'none']), testIdAttributeName: tOptional(tString), generateAutoExpect: tOptional(tBoolean), @@ -489,6 +490,11 @@ scheme.DebugControllerHideHighlightParams = tOptional(tObject({})); scheme.DebugControllerHideHighlightResult = tOptional(tObject({})); scheme.DebugControllerResumeParams = tOptional(tObject({})); scheme.DebugControllerResumeResult = tOptional(tObject({})); +scheme.DebugControllerCloseBrowserParams = tObject({ + id: tString, + reason: tOptional(tString), +}); +scheme.DebugControllerCloseBrowserResult = tOptional(tObject({})); scheme.DebugControllerKillParams = tOptional(tObject({})); scheme.DebugControllerKillResult = tOptional(tObject({})); scheme.SocksSupportInitializer = tOptional(tObject({})); diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index 6871d37775d7a..4efe0c8f21864 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -89,12 +89,12 @@ export class DebugController extends SdkObject { } } - async setRecorderMode(progress: Progress, params: { mode: Mode, testIdAttributeName?: string, generateAutoExpect?: boolean }) { + async setRecorderMode(progress: Progress, params: { mode: Mode, testIdAttributeName?: string, generateAutoExpect?: boolean, browserId?: string }) { await progress.race(this._closeBrowsersWithoutPages()); this._generateAutoExpect = !!params.generateAutoExpect; if (params.mode === 'none') { - for (const recorder of await progress.race(this._allRecorders())) { + for (const recorder of await progress.race(this._allRecorders(params.browserId))) { recorder.hideHighlightedSelector(); recorder.setMode('none'); } @@ -112,11 +112,14 @@ export class DebugController extends SdkObject { } // Update test id attribute. if (params.testIdAttributeName) { - for (const page of this._playwright.allPages()) + for (const page of this._playwright.allPages()) { + if (params.browserId && page.browserContext._browser.guid !== params.browserId) + continue; page.browserContext.selectors().setTestIdAttributeName(params.testIdAttributeName); + } } // Toggle the mode. - for (const recorder of await progress.race(this._allRecorders())) { + for (const recorder of await progress.race(this._allRecorders(params.browserId))) { recorder.hideHighlightedSelector(); recorder.setMode(params.mode); } @@ -148,6 +151,13 @@ export class DebugController extends SdkObject { recorder.resume(); } + async closeBrowser(progress: Progress, id: string, reason?: string) { + const browser = this._playwright.allBrowsers().find(b => b.guid === id); + if (!browser) + return; + await progress.race(browser.close({ reason })); + } + kill() { gracefullyProcessExitDoNotHang(0); } @@ -171,10 +181,13 @@ export class DebugController extends SdkObject { }); } - private async _allRecorders(): Promise { + private async _allRecorders(browserId?: string): Promise { const contexts = new Set(); - for (const page of this._playwright.allPages()) + for (const page of this._playwright.allPages()) { + if (browserId && page.browserContext._browser.guid !== browserId) + continue; contexts.add(page.browserContext); + } const recorders = await Promise.all([...contexts].map(c => Recorder.forContext(c, { omitCallTracking: true }))); const nonNullRecorders = recorders.filter(Boolean) as Recorder[]; for (const recorder of recorders) diff --git a/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts index 595535925741f..28e4e93a2f509 100644 --- a/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts @@ -78,6 +78,10 @@ export class DebugControllerDispatcher extends Dispatcher; hideHighlight(params?: DebugControllerHideHighlightParams, progress?: Progress): Promise; resume(params?: DebugControllerResumeParams, progress?: Progress): Promise; + closeBrowser(params: DebugControllerCloseBrowserParams, progress?: Progress): Promise; kill(params?: DebugControllerKillParams, progress?: Progress): Promise; } export type DebugControllerInspectRequestedEvent = { @@ -791,11 +792,13 @@ export type DebugControllerSetReportStateChangedOptions = { }; export type DebugControllerSetReportStateChangedResult = void; export type DebugControllerSetRecorderModeParams = { + browserId?: string, mode: 'inspecting' | 'recording' | 'none', testIdAttributeName?: string, generateAutoExpect?: boolean, }; export type DebugControllerSetRecorderModeOptions = { + browserId?: string, testIdAttributeName?: string, generateAutoExpect?: boolean, }; @@ -815,6 +818,14 @@ export type DebugControllerHideHighlightResult = void; export type DebugControllerResumeParams = {}; export type DebugControllerResumeOptions = {}; export type DebugControllerResumeResult = void; +export type DebugControllerCloseBrowserParams = { + id: string, + reason?: string, +}; +export type DebugControllerCloseBrowserOptions = { + reason?: string, +}; +export type DebugControllerCloseBrowserResult = void; export type DebugControllerKillParams = {}; export type DebugControllerKillOptions = {}; export type DebugControllerKillResult = void; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 080251712ae74..337488643baed 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -896,6 +896,7 @@ DebugController: setRecorderMode: internal: true parameters: + browserId: string? mode: type: enum literals: @@ -917,6 +918,12 @@ DebugController: resume: internal: true + closeBrowser: + internal: true + parameters: + id: string + reason: string? + kill: internal: true diff --git a/tests/library/debug-controller.spec.ts b/tests/library/debug-controller.spec.ts index 1e72ee9b5fc27..fb560db964f88 100644 --- a/tests/library/debug-controller.spec.ts +++ b/tests/library/debug-controller.spec.ts @@ -34,7 +34,7 @@ const test = baseTest.extend({ wsEndpoint: async ({ headless }, use) => { if (headless) process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1'; - const server = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false }); + const server = new PlaywrightServer({ mode: 'default', path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false, debugController: true }); const wsEndpoint = await server.listen(); await use(wsEndpoint); await server.close(); @@ -111,6 +111,30 @@ test('should pick element', async ({ backend, connectedBrowser }) => { expect(events).toHaveLength(2); }); +test('should allow setting recorder mode only for specific browser', async ({ backend, connectedBrowserFactory }) => { + const events = []; + backend.on('inspectRequested', event => events.push(event)); + + const browser1 = await connectedBrowserFactory(); + const browser2 = await connectedBrowserFactory(); + expect((browser1 as any)._guid).not.toBe((browser2 as any)._guid); + const page1 = await browser1.newPage(); + await page1.setContent(''); + const page2 = await browser2.newPage(); + await page2.setContent(''); + + await backend.setRecorderMode({ mode: 'inspecting', browserId: (browser1 as any)._guid }); + await page1.getByRole('button').click(); + expect(events).toHaveLength(1); + + await page2.getByRole('button').click(); + expect(events).toHaveLength(1); + + await backend.setRecorderMode({ mode: 'inspecting', browserId: (browser2 as any)._guid }); + await page2.getByRole('button').click(); + expect(events).toHaveLength(2); +}); + test('should report pages', async ({ backend, connectedBrowser, browserName, channel }) => { const events = []; backend.on('stateChanged', event => events.push(event)); @@ -402,3 +426,8 @@ test('should not work with browser._launchServer(_debugController: false)', asyn await server.close(); await browser.close(); }); + +test('should support closing browsers', async ({ backend, connectedBrowser }) => { + await backend.closeBrowser({ id: (connectedBrowser as any)._guid, reason: 'some reason' }); + await expect.poll(() => connectedBrowser.isConnected()).toBe(false); +}); From 2aa5569cae369dccf1dc8a2b04e97ffbffa8aeb0 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 28 Aug 2025 15:42:05 +0200 Subject: [PATCH 046/329] chore(tele): emit globalSetup and teardown (#37221) --- packages/playwright/src/isomorphic/teleReceiver.ts | 2 +- packages/playwright/src/reporters/merge.ts | 2 ++ packages/playwright/src/reporters/teleEmitter.ts | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/isomorphic/teleReceiver.ts b/packages/playwright/src/isomorphic/teleReceiver.ts index df3e1c84a4ded..f9ed643a171fd 100644 --- a/packages/playwright/src/isomorphic/teleReceiver.ts +++ b/packages/playwright/src/isomorphic/teleReceiver.ts @@ -25,7 +25,7 @@ export type JsonStackFrame = { file: string, line: number, column: number }; export type JsonStdIOType = 'stdout' | 'stderr'; -export type JsonConfig = Pick; +export type JsonConfig = Pick; export type JsonPattern = { s?: string; diff --git a/packages/playwright/src/reporters/merge.ts b/packages/playwright/src/reporters/merge.ts index b3f6b457ae3a3..aa415ceab3d76 100644 --- a/packages/playwright/src/reporters/merge.ts +++ b/packages/playwright/src/reporters/merge.ts @@ -263,6 +263,8 @@ function mergeConfigureEvents(configureEvents: JsonOnConfigureEvent[], rootDirOv rootDir: '', version: '', workers: 0, + globalSetup: null, + globalTeardown: null, }; for (const event of configureEvents) config = mergeConfigs(config, event.params.config); diff --git a/packages/playwright/src/reporters/teleEmitter.ts b/packages/playwright/src/reporters/teleEmitter.ts index 44fbcec7230d7..f7cfeb57e9bb4 100644 --- a/packages/playwright/src/reporters/teleEmitter.ts +++ b/packages/playwright/src/reporters/teleEmitter.ts @@ -170,6 +170,8 @@ export class TeleReporterEmitter implements ReporterV2 { rootDir: config.rootDir, version: config.version, workers: config.workers, + globalSetup: config.globalSetup, + globalTeardown: config.globalTeardown, }; } From 15a7e7e12e6b55bc477a0efe06a1f68ce9fe05ec Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 28 Aug 2025 16:02:24 +0100 Subject: [PATCH 047/329] test: fix launchOptions being reset in some test files (#37220) --- tests/android/android.spec.ts | 2 +- tests/android/launch-server.spec.ts | 2 +- tests/library/debug-controller.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/android/android.spec.ts b/tests/android/android.spec.ts index 1142551d3c309..4accaa3cc6b4a 100644 --- a/tests/android/android.spec.ts +++ b/tests/android/android.spec.ts @@ -18,7 +18,7 @@ import net from 'net'; import { androidTest as test, expect } from './androidTest'; // Force a separate worker to avoid messing up with `androidDevice` fixture. -test.use({ launchOptions: {} }); +test.use({ launchOptions: [async ({ launchOptions }, use) => use(launchOptions), { scope: 'worker' }] }); test('androidDevice.close', async function({ playwright }) { const devices = await playwright._android.devices(); diff --git a/tests/android/launch-server.spec.ts b/tests/android/launch-server.spec.ts index 21c01b8e4d159..bcb51b9ac74e2 100644 --- a/tests/android/launch-server.spec.ts +++ b/tests/android/launch-server.spec.ts @@ -19,7 +19,7 @@ import { androidTest as test, expect } from './androidTest'; import { kTargetClosedErrorMessage } from '../config/errors'; // Force a separate worker to avoid messing up with `androidDevice` fixture. -test.use({ launchOptions: {} }); +test.use({ launchOptions: [async ({ launchOptions }, use) => use(launchOptions), { scope: 'worker' }] }); test('android.launchServer should connect to a device', async ({ playwright }) => { const browserServer = await playwright._android.launchServer(); diff --git a/tests/library/debug-controller.spec.ts b/tests/library/debug-controller.spec.ts index fb560db964f88..b34f0582e7bfd 100644 --- a/tests/library/debug-controller.spec.ts +++ b/tests/library/debug-controller.spec.ts @@ -78,7 +78,7 @@ test.skip(({ mode }) => mode.startsWith('service') || mode === 'driver'); // Force a separate worker to avoid registered selector engines from other tests. // See https://github.com/microsoft/playwright/pull/37103. -test.use({ launchOptions: {} }); +test.use({ launchOptions: [async ({ launchOptions }, use) => use(launchOptions), { scope: 'worker' }] }); test('should pick element', async ({ backend, connectedBrowser }) => { const events = []; From fe5803e5543b6e3f60529bca2355b56573bd24f2 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 28 Aug 2025 10:18:17 -0700 Subject: [PATCH 048/329] chore: inline the dom-inline generic nodes (#37197) --- packages/injected/src/ariaSnapshot.ts | 12 +++++--- packages/injected/src/domUtils.ts | 17 +++++------ tests/page/page-aria-snapshot-ai.spec.ts | 36 ++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index ad4ccbd23351b..89d447299a62d 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -16,7 +16,7 @@ import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace } from '@isomorphic/stringUtils'; -import { box, getElementComputedStyle, isElementVisible } from './domUtils'; +import { computeBox, getElementComputedStyle, isElementVisible } from './domUtils'; import * as roleUtils from './roleUtils'; import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from './yaml'; @@ -92,7 +92,7 @@ export function generateAriaTree(rootElement: Element, publicOptions: AriaTreeOp const visited = new Set(); const snapshot: AriaSnapshot = { - root: { role: 'fragment', name: '', children: [], element: rootElement, props: {}, box: box(rootElement), receivesPointerEvents: true }, + root: { role: 'fragment', name: '', children: [], element: rootElement, props: {}, box: computeBox(rootElement), receivesPointerEvents: true }, elements: new Map(), refs: new Map(), }; @@ -231,7 +231,7 @@ function toAriaNode(element: Element, options: InternalOptions): AriaNode | null children: [], props: {}, element, - box: box(element), + box: computeBox(element), receivesPointerEvents: true, active }; @@ -247,13 +247,17 @@ function toAriaNode(element: Element, options: InternalOptions): AriaNode | null const name = normalizeWhiteSpace(roleUtils.getElementAccessibleName(element, false) || ''); const receivesPointerEvents = roleUtils.receivesPointerEvents(element); + const box = computeBox(element); + if (role === 'generic' && box.inline && element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE) + return null; + const result: AriaNode = { role, name, children: [], props: {}, element, - box: box(element), + box, receivesPointerEvents, active }; diff --git a/packages/injected/src/domUtils.ts b/packages/injected/src/domUtils.ts index e11d6476949cb..c40c6918f0822 100644 --- a/packages/injected/src/domUtils.ts +++ b/packages/injected/src/domUtils.ts @@ -105,33 +105,34 @@ export function isElementStyleVisibilityVisible(element: Element, style?: CSSSty export type Box = { visible: boolean; + inline: boolean; rect?: DOMRect; style?: CSSStyleDeclaration; }; -export function box(element: Element): Box { +export function computeBox(element: Element): Box { // Note: this logic should be similar to waitForDisplayedAtStablePosition() to avoid surprises. const style = getElementComputedStyle(element); if (!style) - return { visible: true }; + return { visible: true, inline: false }; if (style.display === 'contents') { // display:contents is not rendered itself, but its child nodes are. for (let child = element.firstChild; child; child = child.nextSibling) { if (child.nodeType === 1 /* Node.ELEMENT_NODE */ && isElementVisible(child as Element)) - return { visible: true, style }; + return { visible: true, inline: false, style }; if (child.nodeType === 3 /* Node.TEXT_NODE */ && isVisibleTextNode(child as Text)) - return { visible: true, style }; + return { visible: true, inline: true, style }; } - return { visible: false, style }; + return { visible: false, inline: false, style }; } if (!isElementStyleVisibilityVisible(element, style)) - return { style, visible: false }; + return { style, visible: false, inline: false }; const rect = element.getBoundingClientRect(); - return { rect, style, visible: rect.width > 0 && rect.height > 0 }; + return { rect, style, visible: rect.width > 0 && rect.height > 0, inline: style.display === 'inline' }; } export function isElementVisible(element: Element): boolean { - return box(element).visible; + return computeBox(element).visible; } export function isVisibleTextNode(node: Text) { diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 1493f65291bc4..39a716294defd 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -184,9 +184,9 @@ it('emit generic roles for nodes w/o roles', async ({ page }) => { expect(snapshot).toContainYaml(` - generic [ref=e2]: - - generic [ref=e5]: Apple - - generic [ref=e8]: Pear - - generic [ref=e11]: Orange + - generic [ref=e3]: Apple + - generic [ref=e5]: Pear + - generic [ref=e7]: Orange `); }); @@ -400,3 +400,33 @@ it('should support many properties on iframes', async ({ page }) => { - textbox "Input in iframe" [active] [ref=f1e2] `); }); + +it('should collapse inline generic nodes', async ({ page }) => { + await page.setContent(` +
    +
  • 3 bds
  • +
  • 2 ba
  • +
  • 1,200 sqft
  • +
+
    +
  • 3
  • +
  • 2
  • +
  • 1,200
  • +
`); + + const snapshot1 = await snapshotForAI(page); + expect(snapshot1).toContainYaml(` + - generic [active] [ref=e1]: + - list [ref=e2]: + - listitem [ref=e3]: 3 bds + - listitem [ref=e4]: 2 ba + - listitem [ref=e5]: 1,200 sqft + - list [ref=e6]: + - listitem [ref=e7]: + - generic [ref=e8]: "3" + - listitem [ref=e9]: + - generic [ref=e10]: "2" + - listitem [ref=e11]: + - generic [ref=e12]: 1,200 + `); +}); From bd4363e4af7761554bd7e02be94ea126dcab6b5b Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 29 Aug 2025 09:18:27 +0200 Subject: [PATCH 049/329] chore(mcp): drop iconv-lite from bundle (#37203) --- packages/playwright/bundles/mcp/raw-body.ts | 43 +++++++++++++++++++++ utils/build/build.js | 5 +++ 2 files changed, 48 insertions(+) create mode 100644 packages/playwright/bundles/mcp/raw-body.ts diff --git a/packages/playwright/bundles/mcp/raw-body.ts b/packages/playwright/bundles/mcp/raw-body.ts new file mode 100644 index 0000000000000..1e70fa555e70f --- /dev/null +++ b/packages/playwright/bundles/mcp/raw-body.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @ts-expect-error untyped module +import bytes from 'bytes'; +import type { IncomingMessage } from 'node:http'; + +export default function getRawBody(req: IncomingMessage, { limit, encoding }: { limit: string, encoding: BufferEncoding }) { + const limitNumber = bytes.parse(limit); + return new Promise((resolve, reject) => { + let received = 0; + + const chunks: Buffer[] = []; + req.on('data', (chunk: Buffer) => { + received += chunk.length; + if (received > limitNumber) + return reject(new Error(`Message size exceeds limit of ${limit} bytes`)); + chunks.push(chunk); + }); + req.on('end', () => { + try { + resolve(Buffer.concat(chunks).toString(encoding)); + } catch (error) { + reject(error); + } + }); + req.on('error', error => { + reject(error); + }); + }); +} diff --git a/utils/build/build.js b/utils/build/build.js index 37ea64d3920e2..7d16fa15fab11 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -236,6 +236,7 @@ function copyFile(file, from, to) { * outdir?: string, * outfile?: string, * minify?: boolean, + * alias?: Record, * }} BundleOptions */ @@ -260,6 +261,9 @@ bundles.push({ outdir: 'packages/playwright/lib', entryPoints: ['src/mcpBundleImpl.ts'], external: ['express'], + alias: { + 'raw-body': 'raw-body.ts', + }, }); bundles.push({ @@ -508,6 +512,7 @@ for (const bundle of bundles) { ...(bundle.outfile ? { outfile: filePath(bundle.outfile) } : {}), ...(bundle.external ? { external: bundle.external } : {}), ...(bundle.minify !== undefined ? { minify: bundle.minify } : {}), + alias: bundle.alias ? Object.fromEntries(Object.entries(bundle.alias).map(([k, v]) => [k, path.join(filePath(bundle.modulePath), v)])) : undefined, metafile: true, plugins: [pkgSizePlugin], }; From 4fdeec7b7c4611ac01a24457cf2ae7ad79e3b453 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 29 Aug 2025 10:55:01 +0100 Subject: [PATCH 050/329] feat: --last-run-file (#37209) --- docs/src/test-cli-js.md | 1 + packages/playwright/src/common/config.ts | 1 + packages/playwright/src/program.ts | 2 + packages/playwright/src/runner/lastRun.ts | 28 +++++++---- packages/playwright/src/runner/testRunner.ts | 3 +- tests/playwright-test/runner.spec.ts | 51 ++++++++++++++++++-- 6 files changed, 72 insertions(+), 14 deletions(-) diff --git a/docs/src/test-cli-js.md b/docs/src/test-cli-js.md index 0688e328d767a..a47f1a60da1e4 100644 --- a/docs/src/test-cli-js.md +++ b/docs/src/test-cli-js.md @@ -91,6 +91,7 @@ npx playwright test --ui | `--ignore-snapshots` | Ignore screenshot and snapshot expectations. | | `-j ` or `--workers ` | Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%). | | `--last-failed` | Only re-run the failures. | +| `--last-run-file` | Path to the last-run file (default: "test-results/.last-run.json"). This json file is used for the `--last-failed` option. You can also apply a custom filter by providing an array of [`property: TestCase.id`] in the `filterTests` property. | | `--list` | Collect all the tests and report them, but do not run. | | `--max-failures ` or `-x` | Stop after the first `N` failures. Passing `-x` stops after the first failure. | | `--no-deps` | Do not run project dependencies. | diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 660aa77add36f..98d079ababe40 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -57,6 +57,7 @@ export class FullConfigInternal { cliListOnly = false; cliPassWithNoTests?: boolean; cliLastFailed?: boolean; + cliLastRunFile?: string; preOnlyTestFilters: TestCaseFilter[] = []; postShardTestFilters: TestCaseFilter[] = []; defineConfigWasUsed = false; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 5bef1dd87b0cd..b23c389ca2ce3 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -176,6 +176,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { config.cliProjectFilter = opts.project || undefined; config.cliPassWithNoTests = !!opts.passWithNoTests; config.cliLastFailed = !!opts.lastFailed; + config.cliLastRunFile = opts.lastRunFile ? path.resolve(process.cwd(), opts.lastRunFile) : undefined; // Evaluate project filters against config before starting execution. This enables a consistent error message across run modes filterProjects(config.projects, config.cliProjectFilter); @@ -361,6 +362,7 @@ const testOptions: [string, { description: string, choices?: string[], preset?: ['--headed', { description: `Run tests in headed browsers (default: headless)` }], ['--ignore-snapshots', { description: `Ignore screenshot and snapshot expectations` }], ['--last-failed', { description: `Only re-run the failures` }], + ['--last-run-file ', { description: `Path to the last-run file (default: "test-results/.last-run.json")` }], ['--list', { description: `Collect all the tests and report them, but do not run` }], ['--max-failures ', { description: `Stop after the first N failures` }], ['--no-deps', { description: `Do not run project dependencies` }], diff --git a/packages/playwright/src/runner/lastRun.ts b/packages/playwright/src/runner/lastRun.ts index 93ffe9205f925..d6721fd54e7d9 100644 --- a/packages/playwright/src/runner/lastRun.ts +++ b/packages/playwright/src/runner/lastRun.ts @@ -24,8 +24,9 @@ import type { FullConfigInternal } from '../common/config'; import type { ReporterV2 } from '../reporters/reporterV2'; type LastRunInfo = { - status: FullResult['status']; - failedTests: string[]; + status?: FullResult['status']; + failedTests?: string[]; + filterTests?: string[]; }; export class LastRunReporter implements ReporterV2 { @@ -35,19 +36,28 @@ export class LastRunReporter implements ReporterV2 { constructor(config: FullConfigInternal) { this._config = config; - const [project] = filterProjects(config.projects, config.cliProjectFilter); - if (project) - this._lastRunFile = path.join(project.project.outputDir, '.last-run.json'); + if (config.cliLastRunFile) { + this._lastRunFile = config.cliLastRunFile; + } else { + const [project] = filterProjects(config.projects, config.cliProjectFilter); + if (project) + this._lastRunFile = path.join(project.project.outputDir, '.last-run.json'); + } } - async filterLastFailed() { + async applyFilter() { if (!this._lastRunFile) return; try { const lastRunInfo = JSON.parse(await fs.promises.readFile(this._lastRunFile, 'utf8')) as LastRunInfo; - const failedTestIds = new Set(lastRunInfo.failedTests); - // Explicitly apply --last-failed filter after sharding. - this._config.postShardTestFilters.push(test => failedTestIds.has(test.id)); + if (lastRunInfo.filterTests) { + const filterTestIds = new Set(lastRunInfo.filterTests); + this._config.preOnlyTestFilters.push(test => filterTestIds.has(test.id)); + } + if (this._config.cliLastFailed) { + const failedTestIds = new Set(lastRunInfo.failedTests ?? []); + this._config.postShardTestFilters.push(test => failedTestIds.has(test.id)); + } } catch { } } diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index faf2e50d7f6df..b2612e7aa0b6f 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -493,8 +493,7 @@ export async function runAllTestsWithConfig(config: FullConfigInternal): Promise const reporters = await createReporters(config, listOnly ? 'list' : 'test', false); const lastRun = new LastRunReporter(config); - if (config.cliLastFailed) - await lastRun.filterLastFailed(); + await lastRun.applyFilter(); const reporter = new InternalReporter([...reporters, lastRun]); const tasks = listOnly ? [ diff --git a/tests/playwright-test/runner.spec.ts b/tests/playwright-test/runner.spec.ts index dece006a06ceb..fb57fe16c458a 100644 --- a/tests/playwright-test/runner.spec.ts +++ b/tests/playwright-test/runner.spec.ts @@ -842,7 +842,7 @@ test('should run last failed tests', async ({ runInlineTest }) => { expect(result2.failed).toBe(1); }); -test('should run last failed tests in a shard', async ({ runInlineTest }) => { +test('should run last failed tests in a shard with a custom path', async ({ runInlineTest }) => { const workspace = { 'a.spec.js': ` import { test, expect } from '@playwright/test'; @@ -859,17 +859,62 @@ test('should run last failed tests in a shard', async ({ runInlineTest }) => { }); `, }; - const result1 = await runInlineTest(workspace, { shard: '2/2' }); + const result1 = await runInlineTest(workspace, { 'shard': '2/2', 'last-run-file': 'custom.json' }); expect(result1.exitCode).toBe(1); expect(result1.passed).toBe(1); expect(result1.failed).toBe(1); expect(result1.output).toContain('b.spec.js:3:11 › pass-b'); expect(result1.output).toContain('b.spec.js:4:11 › fail-b'); + expect(fs.existsSync(test.info().outputPath('custom.json'))).toBe(true); - const result2 = await runInlineTest(workspace, { shard: '2/2' }, {}, { additionalArgs: ['--last-failed'] }); + const result2 = await runInlineTest(workspace, { 'shard': '2/2', 'last-failed': true, 'last-run-file': 'custom.json' }); expect(result2.exitCode).toBe(1); expect(result2.passed).toBe(0); expect(result2.failed).toBe(1); expect(result2.output).not.toContain('b.spec.js:3:11 › pass-b'); expect(result2.output).toContain('b.spec.js:4:11 › fail-b'); }); + +test('should apply filterTests from last-run.json', async ({ runInlineTest }) => { + const workspace = { + 'reporter.ts': ` + import fs from 'fs'; + import path from 'path'; + export default class MyReporter { + onBegin(config, suite) { + const filterTests = suite.allTests().filter(t => t.title !== 'test2').map(t => t.id); + const json = JSON.stringify({ filterTests }); + fs.writeFileSync(path.join(__dirname, 'filter.json'), json); + } + } + `, + 'a.spec.js': ` + import { test, expect } from '@playwright/test'; + test('test1', async () => { + console.log('\\n%%test1'); + }); + test('test2', async () => { + console.log('\\n%%test2'); + }); + test('test3', async () => { + console.log('\\n%%test3'); + expect(1).toBe(2); + }); + ` + }; + const result1 = await runInlineTest(workspace, { reporter: './reporter.ts', list: true }); + expect(result1.exitCode).toBe(0); + expect(fs.existsSync(test.info().outputPath('filter.json'))).toBe(true); + + const result2 = await runInlineTest(workspace, { 'last-run-file': 'filter.json' }); + expect(result2.exitCode).toBe(1); + expect(result2.passed).toBe(1); + expect(result2.failed).toBe(1); + expect(result2.outputLines).toEqual(['test1', 'test3']); + + const result3 = await runInlineTest(workspace, { 'last-run-file': 'filter.json', 'last-failed': true }); + expect(result3.exitCode).toBe(1); + expect(result3.passed).toBe(0); + expect(result3.failed).toBe(1); + expect(result3.outputLines).toEqual(['test3']); +}); From 25c105ab7e9b4137d4ae48c9e95eeeb50b26c198 Mon Sep 17 00:00:00 2001 From: Shahed <125728402+dev-shahed@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:58:53 +0600 Subject: [PATCH 051/329] fix(locator): improve error message when unchecking radio buttons (#37184) --- packages/injected/src/injectedScript.ts | 4 +++- packages/playwright-core/src/server/dom.ts | 10 +++++++--- tests/page/page-check.spec.ts | 6 ++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index a18c6ce2b5ab3..dde268ee61f63 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -51,7 +51,7 @@ export type FrameExpectParams = Omit; -export type ElementStateQueryResult = { matches: boolean, received?: string | 'error:notconnected' }; +export type ElementStateQueryResult = { matches: boolean, received?: string | 'error:notconnected', isRadio?: boolean }; export type HitTargetInterceptionResult = { stop: () => 'done' | { hitTargetDescription: string }; @@ -735,9 +735,11 @@ export class InjectedScript { const checked = getCheckedWithoutMixed(element); if (checked === 'error') throw this.createStacklessError('Not a checkbox or radio button'); + const isRadio = element.nodeName === 'INPUT' && (element as HTMLInputElement).type === 'radio'; return { matches: need === checked, received: checked ? 'checked' : 'unchecked', + isRadio, }; } diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index a393afc170b16..8aa3a6e864430 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -730,17 +730,21 @@ export class ElementHandle extends js.JSHandle { const result = await progress.race(this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {})); if (result === 'error:notconnected' || result.received === 'error:notconnected') throwElementIsNotAttached(); - return result.matches; + return { matches: result.matches, isRadio: result.isRadio }; }; await this._markAsTargetElement(progress); - if (await isChecked() === state) + const checkedState = await isChecked(); + if (checkedState.matches === state) return 'done'; + if (!state && checkedState.isRadio) + throw new NonRecoverableDOMError('Cannot uncheck radio button. Radio buttons can only be unchecked by selecting another radio button in the same group.'); const result = await this._click(progress, { ...options, waitAfter: 'disabled' }); if (result !== 'done') return result; if (options.trial) return 'done'; - if (await isChecked() !== state) + const finalState = await isChecked(); + if (finalState.matches !== state) throw new NonRecoverableDOMError('Clicking the checkbox did not change its state'); return 'done'; } diff --git a/tests/page/page-check.spec.ts b/tests/page/page-check.spec.ts index 01b00ddc55c90..73fe05acb40a5 100644 --- a/tests/page/page-check.spec.ts +++ b/tests/page/page-check.spec.ts @@ -145,3 +145,9 @@ it('should check the box using setChecked', async ({ page }) => { await page.setChecked('input', false); expect(await page.evaluate(() => window['checkbox'].checked)).toBe(false); }); + +it('should throw when trying to uncheck radio button', async ({ page }) => { + await page.setContent(``); + const error = await page.uncheck('#radio').catch(e => e); + expect(error.message).toContain('Cannot uncheck radio button'); +}); From 93df4d6761b7865d66ed0021f63a551071f49828 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 29 Aug 2025 15:14:56 +0200 Subject: [PATCH 052/329] test(debug-controller): ensure ID aligns with browser ID, not dispatcher ID (#37238) --- tests/library/debug-controller.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/library/debug-controller.spec.ts b/tests/library/debug-controller.spec.ts index b34f0582e7bfd..8d8ec44ba9735 100644 --- a/tests/library/debug-controller.spec.ts +++ b/tests/library/debug-controller.spec.ts @@ -155,7 +155,7 @@ test('should report pages', async ({ backend, connectedBrowser, browserName, cha { pageCount: 1, browsers: [{ - id: expect.any(String), + id: (connectedBrowser as any)._guid, name: browserName, channel, contexts: [{ @@ -167,7 +167,7 @@ test('should report pages', async ({ backend, connectedBrowser, browserName, cha }, { pageCount: 2, browsers: [{ - id: expect.any(String), + id: (connectedBrowser as any)._guid, name: browserName, channel, contexts: [{ @@ -180,7 +180,7 @@ test('should report pages', async ({ backend, connectedBrowser, browserName, cha }, { pageCount: 1, browsers: [{ - id: expect.any(String), + id: (connectedBrowser as any)._guid, name: browserName, channel, contexts: [{ @@ -192,7 +192,7 @@ test('should report pages', async ({ backend, connectedBrowser, browserName, cha }, { pageCount: 1, browsers: [{ - id: expect.any(String), + id: (connectedBrowser as any)._guid, name: browserName, channel, contexts: [{ @@ -204,7 +204,7 @@ test('should report pages', async ({ backend, connectedBrowser, browserName, cha }, { pageCount: 0, browsers: [{ - id: expect.any(String), + id: (connectedBrowser as any)._guid, name: browserName, channel, contexts: [{ From c923ff527398a23a4dbba924aa89800c2578d8b0 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 29 Aug 2025 15:47:58 +0200 Subject: [PATCH 053/329] chore: emit snapshot when browser closes (#37241) --- packages/playwright-core/src/server/debugController.ts | 3 +++ tests/library/debug-controller.spec.ts | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index 4efe0c8f21864..850ddaeed2b4d 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -78,6 +78,9 @@ export class DebugController extends SdkObject { page.on(Page.Events.Close, () => this._disposeListeners.delete(dispose)); }, onPageClose: () => this._emitSnapshot(false), + onBrowserClose: () => { + this._emitSnapshot(false); + }, }; this._playwright.instrumentation.addListener(listener, null); this._disposeListeners.add(() => this._playwright.instrumentation.removeListener(listener)); diff --git a/tests/library/debug-controller.spec.ts b/tests/library/debug-controller.spec.ts index 8d8ec44ba9735..8109e4171a8db 100644 --- a/tests/library/debug-controller.spec.ts +++ b/tests/library/debug-controller.spec.ts @@ -428,6 +428,13 @@ test('should not work with browser._launchServer(_debugController: false)', asyn }); test('should support closing browsers', async ({ backend, connectedBrowser }) => { + const events: channels.DebugControllerStateChangedEvent[] = []; + backend.on('stateChanged', event => events.push(event)); + await backend.setReportStateChanged({ enabled: true }); + await connectedBrowser.newPage(); + await backend.closeBrowser({ id: (connectedBrowser as any)._guid, reason: 'some reason' }); await expect.poll(() => connectedBrowser.isConnected()).toBe(false); + + await expect.poll(() => events[events.length - 1]?.browsers).toEqual([]); }); From b4c79d8221d7c012fa14807348425b6eb566b88a Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 29 Aug 2025 16:30:39 +0100 Subject: [PATCH 054/329] fix(test runner): do not use config.workers as project.workers (#37239) --- packages/playwright/src/common/config.ts | 3 ++- tests/playwright-test/worker-index.spec.ts | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 98d079ababe40..e76c49a6a9da2 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -131,7 +131,8 @@ export class FullConfigInternal { this.webServers = []; } - const projectConfigs = configCLIOverrides.projects || userConfig.projects || [userConfig]; + // When no projects are defined, do not use config.workers as a hard limit for project.workers. + const projectConfigs = configCLIOverrides.projects || userConfig.projects || [{ ...userConfig, workers: undefined }]; this.projects = projectConfigs.map(p => new FullProjectInternal(configDir, userConfig, this, p, this.configCLIOverrides, packageJsonDir)); resolveProjectDependencies(this.projects); this._assignUniqueProjectIds(this.projects); diff --git a/tests/playwright-test/worker-index.spec.ts b/tests/playwright-test/worker-index.spec.ts index 7ab48caefa311..e84a205d565c0 100644 --- a/tests/playwright-test/worker-index.spec.ts +++ b/tests/playwright-test/worker-index.spec.ts @@ -324,3 +324,24 @@ test('should respect project.workers>1', async ({ runInlineTest }) => { 'test1-end', ]); }); + +test('should not inherit config.workers into project.workers', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + export default { + workers: 1, + }; + `, + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test.describe.configure({ mode: 'parallel' }); + test('test1', async ({}, testInfo) => { + }); + test('test2', async ({}, testInfo) => { + }); + `, + }, { workers: 2 }); + expect(result.passed).toBe(2); + expect(result.exitCode).toBe(0); + expect(result.output).toContain('Running 2 tests using 2 workers'); +}); From 02fdfa39e64446c34b9e122e8019e41a85c5f6db Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Fri, 29 Aug 2025 18:34:39 +0200 Subject: [PATCH 055/329] test(bidi): add browser-specific expectations for _bidiFirefox (#37187) --- tests/library/headful.spec.ts | 4 ++-- tests/library/page-close.spec.ts | 2 +- tests/page/page-basic.spec.ts | 2 +- tests/page/page-goto.spec.ts | 2 ++ tests/page/page-route.spec.ts | 2 ++ 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/library/headful.spec.ts b/tests/library/headful.spec.ts index 715d0bf86d2cc..c09a379b743be 100644 --- a/tests/library/headful.spec.ts +++ b/tests/library/headful.spec.ts @@ -119,7 +119,7 @@ it('should close browser after context menu was triggered', async ({ browserType await browser.close(); }); -it('should(not) block third party cookies', async ({ page, server, allowsThirdParty }) => { +it('should(not) block third party cookies', async ({ page, server, allowsThirdParty, defaultSameSiteCookieValue }) => { await page.goto(server.EMPTY_PAGE); await page.evaluate(src => { let fulfill; @@ -145,7 +145,7 @@ it('should(not) block third party cookies', async ({ page, server, allowsThirdPa 'httpOnly': false, 'name': 'username', 'path': '/', - 'sameSite': 'None', + 'sameSite': defaultSameSiteCookieValue, 'secure': false, 'value': 'John Doe' } diff --git a/tests/library/page-close.spec.ts b/tests/library/page-close.spec.ts index f8b712fc433fc..e06dfabe98aad 100644 --- a/tests/library/page-close.spec.ts +++ b/tests/library/page-close.spec.ts @@ -153,7 +153,7 @@ test('interrupt request.response() and request.allHeaders() on page.close', asyn await page.close(); expect((await respPromise).message).toContain(kTargetClosedErrorMessage); // All headers are the same as "provisional" headers in Firefox. - if (browserName === 'firefox') + if (browserName === 'firefox' || browserName as any === '_bidiFirefox') expect((await headersPromise)['user-agent']).toBeTruthy(); else expect((await headersPromise).message).toContain(kTargetClosedErrorMessage); diff --git a/tests/page/page-basic.spec.ts b/tests/page/page-basic.spec.ts index d8e8d40dec7ce..715c03b54ca32 100644 --- a/tests/page/page-basic.spec.ts +++ b/tests/page/page-basic.spec.ts @@ -125,7 +125,7 @@ it('should have sane user agent', async ({ page, browserName, isElectron, isAndr // Second part in parenthesis is platform - ignore it. // Third part for Firefox is the last one and encodes engine and browser versions. - if (browserName === 'firefox') { + if (browserName === 'firefox' || browserName as any === '_bidiFirefox') { const [engine, browser] = part3.split(' '); expect(engine.startsWith('Gecko')).toBe(true); expect(browser.startsWith('Firefox')).toBe(true); diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index e3ffb1f93cf54..df0aa64894607 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -295,6 +295,8 @@ it('should fail when navigating to bad url', async ({ mode, page, browserName }) await page.goto('asdfasdf').catch(e => error = e); if (browserName === 'chromium' || browserName === 'webkit') expect(error.message).toContain('Cannot navigate to invalid URL'); + else if (browserName === '_bidiFirefox') + expect(error.message).toContain('NS_ERROR_MALFORMED_URI'); else expect(error.message).toContain('Invalid url'); }); diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts index 1d38c0c2e806d..32ae41cbe0c08 100644 --- a/tests/page/page-route.spec.ts +++ b/tests/page/page-route.spec.ts @@ -353,6 +353,8 @@ it('should fail navigation when aborting main resource', async ({ page, server, expect(error.message).toContain(isMac && macVersion < 11 ? 'Request intercepted' : 'Blocked by Web Inspector'); else if (browserName === 'firefox') expect(error.message).toContain('NS_ERROR_FAILURE'); + else if (browserName === '_bidiFirefox') + expect(error.message).toContain('NS_ERROR_ABORT'); else expect(error.message).toContain('net::ERR_FAILED'); }); From 7d43a4574441df562789f6a673d3ad232a4b5885 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 29 Aug 2025 13:02:18 -0700 Subject: [PATCH 056/329] chore: do not mute console for mcp runner (#37233) --- packages/playwright/src/mcp/test/backend.ts | 6 +-- packages/playwright/src/mcp/test/context.ts | 21 +------- packages/playwright/src/runner/testRunner.ts | 53 -------------------- packages/playwright/src/runner/testServer.ts | 45 +++++++++++++++-- 4 files changed, 43 insertions(+), 82 deletions(-) diff --git a/packages/playwright/src/mcp/test/backend.ts b/packages/playwright/src/mcp/test/backend.ts index 30f9f7b8eb8c3..cb0a1b0733b4e 100644 --- a/packages/playwright/src/mcp/test/backend.ts +++ b/packages/playwright/src/mcp/test/backend.ts @@ -48,11 +48,7 @@ export class TestServerBackend implements mcp.ServerBackend { if (!tool) throw new Error(`Tool not found: ${name}. Available tools: ${this._tools.map(tool => tool.schema.name).join(', ')}`); const parsedArguments = tool.schema.inputSchema.parse(args || {}); - const result = await tool.handle(this._context!, parsedArguments); - const stdio = this._context.takeStdio(); - if (stdio.trim()) - result.content.push({ type: 'text', text: stdio }); - return result; + return await tool.handle(this._context!, parsedArguments); } serverClosed() { diff --git a/packages/playwright/src/mcp/test/context.ts b/packages/playwright/src/mcp/test/context.ts index 05dcd8aa7b601..602abc97e1300 100644 --- a/packages/playwright/src/mcp/test/context.ts +++ b/packages/playwright/src/mcp/test/context.ts @@ -22,7 +22,6 @@ export class Context { private _testRunner: TestRunner | undefined; readonly configLocation: ConfigLocation; readonly options?: { muteConsole?: boolean }; - private _stdio: { chunk: string | Buffer, stdio: 'stdout' | 'stderr' }[] = []; constructor(configLocation: ConfigLocation, options?: { muteConsole?: boolean }) { this.configLocation = configLocation; @@ -33,13 +32,7 @@ export class Context { if (this._testRunner) await this._testRunner.stopTests(); const testRunner = new TestRunner(this.configLocation, {}); - await testRunner.initialize({ - sendStdioEvents: true, - muteConsole: this.options?.muteConsole, - }); - testRunner.on(TestRunnerEvent.StdioChunk, (chunk, stdio) => { - this._stdio.push({ chunk, stdio }); - }); + await testRunner.initialize({}); this._testRunner = testRunner; testRunner.on(TestRunnerEvent.TestFilesChanged, testFiles => { this._testRunner?.emit(TestRunnerEvent.TestFilesChanged, testFiles); @@ -48,18 +41,6 @@ export class Context { return testRunner; } - takeStdio(): string { - const text = this._stdio.map(entry => chunkToPayload(entry.stdio, entry.chunk)).join('\n'); - this._stdio = []; - return text; - } - async close() { } } - -function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string): string { - if (chunk instanceof Uint8Array) - return ''; - return `[${type}] ${chunk}`; -} diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index b2612e7aa0b6f..8255f67ae3acb 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -17,9 +17,7 @@ import EventEmitter from 'events'; import fs from 'fs'; import path from 'path'; -import util from 'util'; -import { debug } from 'playwright-core/lib/utilsBundle'; import { registry } from 'playwright-core/lib/server'; import { ManualPromise, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils'; @@ -44,12 +42,10 @@ import type { AnyReporter } from '../reporters/reporterV2'; export const TestRunnerEvent = { TestFilesChanged: 'testFilesChanged', - StdioChunk: 'stdioChunk', } as const; export type TestRunnerEventMap = { [TestRunnerEvent.TestFilesChanged]: [testFiles: string[]]; - [TestRunnerEvent.StdioChunk]: [chunk: string | Buffer, stdio: 'stdout' | 'stderr']; }; export type ListTestsParams = { @@ -79,12 +75,6 @@ export type RunTestsParams = { type FullResultStatus = reporterTypes.FullResult['status']; -const originalDebugLog = debug.log; -// eslint-disable-next-line no-restricted-properties -const originalStdoutWrite = process.stdout.write; -// eslint-disable-next-line no-restricted-properties -const originalStderrWrite = process.stderr.write; - export class TestRunner extends EventEmitter { readonly configLocation: ConfigLocation; private _configCLIOverrides: ConfigCLIOverrides; @@ -116,15 +106,9 @@ export class TestRunner extends EventEmitter { async initialize(params: { watchTestDirs?: boolean; populateDependenciesOnList?: boolean; - sendStdioEvents?: boolean; - muteConsole?: boolean; }) { this._watchTestDirs = !!params.watchTestDirs; this._populateDependenciesOnList = !!params.populateDependenciesOnList; - this._setInterceptStdio({ - sendStdioEvents: !!params.sendStdioEvents, - muteConsole: !!params.muteConsole, - }); } resizeTerminal(params: { cols: number, rows: number }) { @@ -394,7 +378,6 @@ export class TestRunner extends EventEmitter { } async stop() { - this._setInterceptStdio({ sendStdioEvents: false, muteConsole: false }); await this.runGlobalTeardown(); } @@ -426,42 +409,6 @@ export class TestRunner extends EventEmitter { await reporter.onExit(); return null; } - - _setInterceptStdio(options: { sendStdioEvents: boolean, muteConsole: boolean }) { - /* eslint-disable no-restricted-properties */ - if (process.env.PWTEST_DEBUG) - return; - if (options.sendStdioEvents || options.muteConsole) { - if (debug.log === originalDebugLog) { - // Only if debug.log hasn't already been tampered with, don't intercept any DEBUG=* logging - debug.log = (...args) => { - const string = util.format(...args) + '\n'; - return (originalStderrWrite as any).apply(process.stderr, [string]); - }; - } - const stdoutWrite = (chunk: string | Buffer) => { - if (options.sendStdioEvents) - this.emit(TestRunnerEvent.StdioChunk, chunk, 'stdout'); - if (!options.muteConsole) - originalStdoutWrite.apply(process.stdout, [chunk]); - return true; - }; - const stderrWrite = (chunk: string | Buffer) => { - if (options.sendStdioEvents) - this.emit(TestRunnerEvent.StdioChunk, chunk, 'stderr'); - if (!options.muteConsole) - originalStderrWrite.apply(process.stderr, [chunk]); - return true; - }; - process.stdout.write = stdoutWrite; - process.stderr.write = stderrWrite; - } else { - debug.log = originalDebugLog; - process.stdout.write = originalStdoutWrite; - process.stderr.write = originalStderrWrite; - } - /* eslint-enable no-restricted-properties */ - } } function printInternalError(e: Error) { diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 616fd6a53b6f8..26ac3a0e5245d 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -14,9 +14,11 @@ * limitations under the License. */ +import util from 'util'; + import { installRootRedirect, openTraceInBrowser, openTraceViewerApp, startTraceViewerServer } from 'playwright-core/lib/server'; import { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'playwright-core/lib/utils'; -import { open } from 'playwright-core/lib/utilsBundle'; +import { debug, open } from 'playwright-core/lib/utilsBundle'; import { loadConfig, resolveConfigLocation } from '../common/configLoader'; import ListReporter from '../reporters/list'; @@ -32,6 +34,12 @@ import type { ConfigCLIOverrides } from '../common/ipc'; import type { ReportEntry, TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface'; import type { ReporterV2 } from '../reporters/reporterV2'; +const originalDebugLog = debug.log; +// eslint-disable-next-line no-restricted-properties +const originalStdoutWrite = process.stdout.write; +// eslint-disable-next-line no-restricted-properties +const originalStderrWrite = process.stderr.write; + class TestServer { private _configLocation: ConfigLocation; private _configCLIOverrides: ConfigCLIOverrides; @@ -98,7 +106,6 @@ export class TestServerDispatcher implements TestServerInterface { this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params); this._testRunner.on(TestRunnerEvent.TestFilesChanged, testFiles => this._dispatchEvent('testFilesChanged', { testFiles })); - this._testRunner.on(TestRunnerEvent.StdioChunk, (chunk, stdio) => this._dispatchEvent('stdio', chunkToPayload(stdio, chunk))); } private async _wireReporter(messageSink: (message: any) => void) { @@ -119,9 +126,8 @@ export class TestServerDispatcher implements TestServerInterface { this._closeOnDisconnect = !!params.closeOnDisconnect; await this._testRunner.initialize({ ...params, - sendStdioEvents: !!params.interceptStdio, - muteConsole: !!params.interceptStdio, }); + this._setInterceptStdio(!!params.interceptStdio); } async ping() {} @@ -211,12 +217,43 @@ export class TestServerDispatcher implements TestServerInterface { } async stop() { + this._setInterceptStdio(false); await this._testRunner.stop(); } async closeGracefully() { await this._testRunner.closeGracefully(); } + + private _setInterceptStdio(interceptStdio: boolean) { + /* eslint-disable no-restricted-properties */ + if (process.env.PWTEST_DEBUG) + return; + if (interceptStdio) { + if (debug.log === originalDebugLog) { + // Only if debug.log hasn't already been tampered with, don't intercept any DEBUG=* logging + debug.log = (...args) => { + const string = util.format(...args) + '\n'; + return (originalStderrWrite as any).apply(process.stderr, [string]); + }; + } + const stdoutWrite = (chunk: string | Buffer) => { + this._dispatchEvent('stdio', chunkToPayload('stdout', chunk)); + return true; + }; + const stderrWrite = (chunk: string | Buffer) => { + this._dispatchEvent('stdio', chunkToPayload('stderr', chunk)); + return true; + }; + process.stdout.write = stdoutWrite; + process.stderr.write = stderrWrite; + } else { + debug.log = originalDebugLog; + process.stdout.write = originalStdoutWrite; + process.stderr.write = originalStderrWrite; + } + /* eslint-enable no-restricted-properties */ + } } export async function runUIMode(configFile: string | undefined, configCLIOverrides: ConfigCLIOverrides, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise { From cba961dbacd5fbbec1f6703c0e566c8d621083b5 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 29 Aug 2025 15:06:46 -0700 Subject: [PATCH 057/329] chore: prepare mcp bundle to be reused in the dep (#37247) --- packages/playwright/bundles/mcp/src/mcpBundleImpl.ts | 1 + packages/playwright/package.json | 1 + packages/playwright/src/mcp/sdk/bundle.ts | 6 ++---- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts b/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts index 1df3787d47804..a613f59ebb6fb 100644 --- a/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts +++ b/packages/playwright/bundles/mcp/src/mcpBundleImpl.ts @@ -17,6 +17,7 @@ export { Client } from '@modelcontextprotocol/sdk/client/index.js'; export { Server } from '@modelcontextprotocol/sdk/server/index.js'; export { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +export { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; export { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; export { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; export { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; diff --git a/packages/playwright/package.json b/packages/playwright/package.json index 6e2183c72b55f..f592f9ebf2e5d 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -21,6 +21,7 @@ "./package.json": "./package.json", "./lib/common/configLoader": "./lib/common/configLoader.js", "./lib/fsWatcher": "./lib/fsWatcher.js", + "./lib/mcpBundleImpl": "./lib/mcpBundleImpl.js", "./lib/mcp/sdk/exports": "./lib/mcp/sdk/exports.js", "./lib/program": "./lib/program.js", "./lib/reporters/base": "./lib/reporters/base.js", diff --git a/packages/playwright/src/mcp/sdk/bundle.ts b/packages/playwright/src/mcp/sdk/bundle.ts index 3836bc2cabe45..a03bb6712bac7 100644 --- a/packages/playwright/src/mcp/sdk/bundle.ts +++ b/packages/playwright/src/mcp/sdk/bundle.ts @@ -19,6 +19,7 @@ const zodToJsonSchema: typeof import('zod-to-json-schema').zodToJsonSchema = bun const Client: typeof import('@modelcontextprotocol/sdk/client/index.js').Client = bundle.Client; const Server: typeof import('@modelcontextprotocol/sdk/server/index.js').Server = bundle.Server; const SSEServerTransport: typeof import('@modelcontextprotocol/sdk/server/sse.js').SSEServerTransport = bundle.SSEServerTransport; +const StdioClientTransport: typeof import('@modelcontextprotocol/sdk/client/stdio.js').StdioClientTransport = bundle.StdioClientTransport; const StdioServerTransport: typeof import('@modelcontextprotocol/sdk/server/stdio.js').StdioServerTransport = bundle.StdioServerTransport; const StreamableHTTPServerTransport: typeof import('@modelcontextprotocol/sdk/server/streamableHttp.js').StreamableHTTPServerTransport = bundle.StreamableHTTPServerTransport; const StreamableHTTPClientTransport: typeof import('@modelcontextprotocol/sdk/client/streamableHttp.js').StreamableHTTPClientTransport = bundle.StreamableHTTPClientTransport; @@ -28,13 +29,12 @@ const ListToolsRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js' const PingRequestSchema: typeof import('@modelcontextprotocol/sdk/types.js').PingRequestSchema = bundle.PingRequestSchema; const z: typeof import('zod') = bundle.z; -type ToolSchema = import('./tool').ToolSchema; - export { zodToJsonSchema, Client, Server, SSEServerTransport, + StdioClientTransport, StdioServerTransport, StreamableHTTPClientTransport, StreamableHTTPServerTransport, @@ -44,5 +44,3 @@ export { PingRequestSchema, z, }; - -export type { ToolSchema }; From 9181309fc213aac07a32d96fa94a509f5ff8e244 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 29 Aug 2025 15:11:58 -0700 Subject: [PATCH 058/329] chore: revert #37194 since select options got hidden (#37248) --- packages/injected/src/ariaSnapshot.ts | 8 ++++---- tests/page/page-aria-snapshot-ai.spec.ts | 21 +++++++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 89d447299a62d..06f4f75807a06 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -54,7 +54,7 @@ export type AriaTreeOptions = { }; type InternalOptions = { - visibility: 'aria' | 'visible' | 'ariaAndVisible', + visibility: 'aria' | 'ariaOrVisible' | 'ariaAndVisible', refs: 'all' | 'interactable' | 'none', refPrefix?: string, includeGenericRole?: boolean, @@ -67,7 +67,7 @@ function toInternalOptions(options: AriaTreeOptions): InternalOptions { if (options.mode === 'ai') { // For AI consumption. return { - visibility: 'visible', + visibility: 'ariaOrVisible', refs: 'interactable', refPrefix: options.refPrefix, includeGenericRole: true, @@ -119,8 +119,8 @@ export function generateAriaTree(rootElement: Element, publicOptions: AriaTreeOp const element = node as Element; const isElementVisibleForAria = !roleUtils.isElementHiddenForAria(element); let visible = isElementVisibleForAria; - if (options.visibility === 'visible') - visible = isElementVisible(element); + if (options.visibility === 'ariaOrVisible') + visible = isElementVisibleForAria || isElementVisible(element); if (options.visibility === 'ariaAndVisible') visible = isElementVisibleForAria && isElementVisible(element); diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 39a716294defd..816ac4a688456 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -17,7 +17,7 @@ // @ts-ignore import { asLocator } from 'playwright-core/lib/utils'; -import { test as it, expect } from './pageTest'; +import { test as it, expect, unshift } from './pageTest'; function snapshotForAI(page: any, options?: { timeout?: number }): Promise { return page._snapshotForAI(options); @@ -184,9 +184,18 @@ it('emit generic roles for nodes w/o roles', async ({ page }) => { expect(snapshot).toContainYaml(` - generic [ref=e2]: - - generic [ref=e3]: Apple - - generic [ref=e5]: Pear - - generic [ref=e7]: Orange + - generic [ref=e3]: + - generic [ref=e4]: + - radio "Apple" [checked] + - text: Apple + - generic [ref=e5]: + - generic [ref=e6]: + - radio "Pear" + - text: Pear + - generic [ref=e7]: + - generic [ref=e8]: + - radio "Orange" + - text: Orange `); }); @@ -289,11 +298,11 @@ it('should show visible children of hidden elements', { annotation: { type: 'iss
`); - expect(await snapshotForAI(page)).toContainYaml(` + expect(await snapshotForAI(page)).toEqual(unshift(` - generic [active] [ref=e1]: - button "Visible" [ref=e3] - button "Visible" [ref=e4] - `); + `)); }); it('should include active element information', async ({ page }) => { From 981bcd517bbcaaf5526638901cb4a70fa2b428cb Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 29 Aug 2025 16:36:48 -0700 Subject: [PATCH 059/329] chore: split run tests into run and debug (#37246) --- packages/playwright/src/index.ts | 2 +- .../playwright/src/mcp/browser/backend.ts | 4 +- packages/playwright/src/mcp/sdk/mdb.ts | 2 +- packages/playwright/src/mcp/test/backend.ts | 5 +- packages/playwright/src/mcp/test/listTests.ts | 78 ---------- packages/playwright/src/mcp/test/runTests.ts | 67 --------- packages/playwright/src/mcp/test/tools.ts | 137 ++++++++++++++++++ packages/playwright/src/reporters/base.ts | 37 +++-- .../src/reporters/listModeReporter.ts | 10 +- 9 files changed, 168 insertions(+), 174 deletions(-) delete mode 100644 packages/playwright/src/mcp/test/listTests.ts delete mode 100644 packages/playwright/src/mcp/test/runTests.ts create mode 100644 packages/playwright/src/mcp/test/tools.ts diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index bdaeda9832339..7a786304755c7 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -240,7 +240,7 @@ const playwrightFixtures: Fixtures = ({ (testInfo as TestInfoImpl)._setDebugMode(); playwright._defaultContextOptions = _combinedContextOptions; - playwright._defaultContextTimeout = process.env.PLAYWRIGHT_DEBUGGER_MCP ? 5000 : actionTimeout || 0; + playwright._defaultContextTimeout = process.env.PLAYWRIGHT_DEBUGGER_ENABLED ? 5000 : actionTimeout || 0; playwright._defaultContextNavigationTimeout = navigationTimeout || 0; await use(); playwright._defaultContextOptions = undefined; diff --git a/packages/playwright/src/mcp/browser/backend.ts b/packages/playwright/src/mcp/browser/backend.ts index cd815dc393bff..aadf7a48ef525 100644 --- a/packages/playwright/src/mcp/browser/backend.ts +++ b/packages/playwright/src/mcp/browser/backend.ts @@ -73,7 +73,7 @@ const doneToolSchema = defineToolSchema({ }); export async function runBrowserBackendOnError(page: playwright.Page, message: () => string) { - if (!process.env.PLAYWRIGHT_DEBUGGER_MCP) + if (!process.env.PLAYWRIGHT_DEBUGGER_ENABLED) return; const snapshot = await (page as PageEx)._snapshotForAI(); const introMessage = `### Paused on error: @@ -84,5 +84,5 @@ ${snapshot} ### Task Try recovering from the error prior to continuing, use following tools to recover: ${tools.map(tool => tool.schema.name).join(', ')}`; - await runOnPauseBackendLoop(process.env.PLAYWRIGHT_DEBUGGER_MCP!, new BrowserBackend(page), introMessage); + await runOnPauseBackendLoop(process.env.PLAYWRIGHT_MDB_URL!, new BrowserBackend(page), introMessage); } diff --git a/packages/playwright/src/mcp/sdk/mdb.ts b/packages/playwright/src/mcp/sdk/mdb.ts index 4c58863bde74a..1e488654546bb 100644 --- a/packages/playwright/src/mcp/sdk/mdb.ts +++ b/packages/playwright/src/mcp/sdk/mdb.ts @@ -153,7 +153,7 @@ export async function runMainBackend(backendFactory: mcpServer.ServerBackendFact create: () => mdbBackend }; const url = await startAsHttp(factory, { port: options?.port || 0 }); - process.env.PLAYWRIGHT_DEBUGGER_MCP = url; + process.env.PLAYWRIGHT_MDB_URL = url; if (options?.port !== undefined) return url; diff --git a/packages/playwright/src/mcp/test/backend.ts b/packages/playwright/src/mcp/test/backend.ts index cb0a1b0733b4e..ca37bbcd18a99 100644 --- a/packages/playwright/src/mcp/test/backend.ts +++ b/packages/playwright/src/mcp/test/backend.ts @@ -16,8 +16,7 @@ import * as mcp from '../sdk/exports.js'; import { Context } from './context'; -import { listTests } from './listTests'; -import { runTests } from './runTests'; +import { listTests, runTests, debugTest } from './tools.js'; import { snapshot, pickLocator, evaluate } from '../browser/tools'; import type { ConfigLocation } from '../../common/config'; @@ -27,7 +26,7 @@ import type { Tool } from './tool'; export class TestServerBackend implements mcp.ServerBackend { readonly name = 'Playwright'; readonly version = '0.0.1'; - private _tools: Tool[] = [listTests, runTests]; + private _tools: Tool[] = [listTests, runTests, debugTest]; private _context: Context; constructor(resolvedLocation: ConfigLocation, options?: { muteConsole?: boolean }) { diff --git a/packages/playwright/src/mcp/test/listTests.ts b/packages/playwright/src/mcp/test/listTests.ts deleted file mode 100644 index ab204ea914b8e..0000000000000 --- a/packages/playwright/src/mcp/test/listTests.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import path from 'path'; - -import { z } from '../sdk/bundle'; -import { defineTool } from './tool.js'; - -import type * as reporterTypes from 'playwright/types/testReporter'; - -export const listTests = defineTool({ - schema: { - name: 'playwright_test_list_tests', - title: 'List tests', - description: 'List tests', - inputSchema: z.object({}), - type: 'readOnly', - }, - - handle: async (context, params) => { - const reporter = new ListModeReporter(); - const testRunner = await context.createTestRunner(); - await testRunner.listTests(reporter, {}); - - if (reporter.hasErrors()) - throw new Error(reporter.content()); - - return { - content: [{ type: 'text', text: reporter.content() }], - }; - }, -}); - -class ListModeReporter implements reporterTypes.Reporter { - private _lines: string[] = []; - private _hasErrors = false; - - onBegin(config: reporterTypes.FullConfig, suite: reporterTypes.Suite): void { - this._lines.push(`Listing tests:`); - const tests = suite.allTests(); - const files = new Set(); - for (const test of tests) { - // root, project, file, ...describes, test - const [, projectName, , ...titles] = test.titlePath(); - const location = `${path.relative(config.rootDir, test.location.file)}:${test.location.line}:${test.location.column}`; - const projectTitle = projectName ? `[${projectName}] › ` : ''; - this._lines.push(` [id=${test.id}] ${projectTitle}${location} › ${titles.join(' › ')}`); - files.add(test.location.file); - } - this._lines.push(`Total: ${tests.length} ${tests.length === 1 ? 'test' : 'tests'} in ${files.size} ${files.size === 1 ? 'file' : 'files'}`); - } - - onError(error: reporterTypes.TestError) { - this._hasErrors = true; - this._lines.push(error.stack || error.message || error.value || ''); - } - - hasErrors(): boolean { - return this._hasErrors; - } - - content(): string { - return this._lines.join('\n'); - } -} diff --git a/packages/playwright/src/mcp/test/runTests.ts b/packages/playwright/src/mcp/test/runTests.ts deleted file mode 100644 index 3e9d6dc605c52..0000000000000 --- a/packages/playwright/src/mcp/test/runTests.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { noColors } from 'playwright-core/lib/utils'; - -import { z } from '../sdk/bundle'; -import { terminalScreen } from '../../reporters/base'; -import ListReporter from '../../reporters/list'; - -import { defineTool } from './tool'; -import { StringWriteStream } from './streams'; - -export const runTests = defineTool({ - schema: { - name: 'playwright_test_run_tests', - title: 'Run tests', - description: 'Run tests', - inputSchema: z.object({ - tests: z.array(z.object({ - id: z.string().describe('Test ID to run.'), - title: z.string().describe('Human readable test title for granting permission to run the test.'), - })).optional().describe('Tests to run. All tests are run if not provided.'), - }), - type: 'readOnly', - }, - - handle: async (context, params) => { - const stream = new StringWriteStream(); - const screen = { - ...terminalScreen, - isTTY: false, - colors: noColors, - stdout: stream as unknown as NodeJS.WriteStream, - stderr: stream as unknown as NodeJS.WriteStream, - }; - const configDir = context.configLocation.configDir; - const reporter = new ListReporter({ configDir, screen }); - const testRunner = await context.createTestRunner(); - const result = await testRunner.runTests(reporter, { - testIds: params.tests?.map(test => test.id), - // For automatic recovery - timeout: 0, - workers: 1, - }); - - const text = stream.content(); - return { - content: [ - { type: 'text', text }, - ], - isError: result.status !== 'passed', - }; - }, -}); diff --git a/packages/playwright/src/mcp/test/tools.ts b/packages/playwright/src/mcp/test/tools.ts new file mode 100644 index 0000000000000..bae91402fd344 --- /dev/null +++ b/packages/playwright/src/mcp/test/tools.ts @@ -0,0 +1,137 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { noColors } from 'playwright-core/lib/utils'; + +import { z } from '../sdk/bundle'; +import { terminalScreen } from '../../reporters/base'; +import ListReporter from '../../reporters/list'; +import ListModeReporter from '../../reporters/listModeReporter'; + +import { defineTool } from './tool'; +import { StringWriteStream } from './streams'; + +export const listTests = defineTool({ + schema: { + name: 'playwright_test_list_tests', + title: 'List tests', + description: 'List tests', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async context => { + const { screen, stream } = createScreen(); + const reporter = new ListModeReporter({ screen, includeTestId: true }); + const testRunner = await context.createTestRunner(); + await testRunner.listTests(reporter, {}); + + return { + content: [{ type: 'text', text: stream.content() }], + }; + }, +}); + +export const runTests = defineTool({ + schema: { + name: 'playwright_test_run_tests', + title: 'Run tests', + description: 'Run tests', + inputSchema: z.object({ + locations: z.array(z.string()).describe('Folder, file or location to run: "test/e2e" or "test/e2e/file.spec.ts" or "test/e2e/file.spec.ts:20"'), + projects: z.array(z.string()).optional().describe('Projects to run, projects from playwright.config.ts, by default runs all projects. Running with "chromium" is a good start'), + }), + type: 'readOnly', + }, + + handle: async (context, params) => { + const { screen, stream } = createScreen(); + const configDir = context.configLocation.configDir; + const reporter = new ListReporter({ configDir, screen, includeTestId: true }); + const testRunner = await context.createTestRunner(); + const result = await testRunner.runTests(reporter, { + locations: params.locations, + projects: params.projects, + }); + + const text = stream.content(); + return { + content: [ + { type: 'text', text }, + ], + isError: result.status !== 'passed', + }; + }, +}); + +export const debugTest = defineTool({ + schema: { + name: 'playwright_test_debug_test', + title: 'Debug single test', + description: 'Debug single test', + inputSchema: z.object({ + test: z.object({ + id: z.string().describe('Test ID to debug.'), + title: z.string().describe('Human readable test title for granting permission to debug the test.'), + }), + }), + type: 'readOnly', + }, + + handle: async (context, params) => { + const stream = new StringWriteStream(); + const screen = { + ...terminalScreen, + isTTY: false, + colors: noColors, + stdout: stream as unknown as NodeJS.WriteStream, + stderr: stream as unknown as NodeJS.WriteStream, + }; + const configDir = context.configLocation.configDir; + const reporter = new ListReporter({ configDir, screen }); + const testRunner = await context.createTestRunner(); + process.env.PLAYWRIGHT_DEBUGGER_ENABLED = '1'; + const result = await testRunner.runTests(reporter, { + headed: true, + testIds: [params.test.id], + // For automatic recovery + timeout: 0, + workers: 1, + }).finally(() => { + process.env.PLAYWRIGHT_DEBUGGER_ENABLED = undefined; + }); + + const text = stream.content(); + return { + content: [ + { type: 'text', text }, + ], + isError: result.status !== 'passed', + }; + }, +}); + +function createScreen() { + const stream = new StringWriteStream(); + const screen = { + ...terminalScreen, + isTTY: false, + colors: noColors, + stdout: stream as unknown as NodeJS.WriteStream, + stderr: stream as unknown as NodeJS.WriteStream, + }; + return { screen, stream }; +} diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index 7211692770046..c11657d69da73 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -145,6 +145,7 @@ export const internalScreen: Screen = { export type TerminalReporterOptions = { screen?: TerminalScreen; omitFailures?: boolean; + includeTestId?: boolean; }; export class TerminalReporter implements ReporterV2 { @@ -154,13 +155,13 @@ export class TerminalReporter implements ReporterV2 { totalTestCount = 0; result!: FullResult; private fileDurations = new Map }>(); - private _omitFailures: boolean; + private _options: TerminalReporterOptions; private _fatalErrors: TestError[] = []; private _failureCount: number = 0; constructor(options: TerminalReporterOptions = {}) { this.screen = options.screen ?? terminalScreen; - this._omitFailures = options.omitFailures || false; + this._options = options; } version(): 'v2' { @@ -312,7 +313,7 @@ export class TerminalReporter implements ReporterV2 { epilogue(full: boolean) { const summary = this.generateSummary(); const summaryMessage = this.generateSummaryMessage(summary); - if (full && summary.failuresToPrint.length && !this._omitFailures) + if (full && summary.failuresToPrint.length && !this._options.omitFailures) this._printFailures(summary.failuresToPrint); this._printSlowTests(); this._printSummary(summaryMessage); @@ -343,16 +344,16 @@ export class TerminalReporter implements ReporterV2 { return test.outcome() === 'unexpected' && test.results.length <= test.retries; } - formatTestTitle(test: TestCase, step?: TestStep, omitLocation: boolean = false): string { - return formatTestTitle(this.screen, this.config, test, step, omitLocation); + formatTestTitle(test: TestCase, step?: TestStep): string { + return formatTestTitle(this.screen, this.config, test, step, this._options); } formatTestHeader(test: TestCase, options: { indent?: string, index?: number, mode?: 'default' | 'error' } = {}): string { - return formatTestHeader(this.screen, this.config, test, options); + return formatTestHeader(this.screen, this.config, test, { ...options, includeTestId: this._options.includeTestId }); } formatFailure(test: TestCase, index?: number): string { - return formatFailure(this.screen, this.config, test, index); + return formatFailure(this.screen, this.config, test, index, this._options); } formatError(error: TestError): ErrorDetails { @@ -364,9 +365,9 @@ export class TerminalReporter implements ReporterV2 { } } -export function formatFailure(screen: Screen, config: FullConfig, test: TestCase, index?: number): string { +export function formatFailure(screen: Screen, config: FullConfig, test: TestCase, index?: number, options?: TerminalReporterOptions): string { const lines: string[] = []; - const header = formatTestHeader(screen, config, test, { indent: ' ', index, mode: 'error' }); + const header = formatTestHeader(screen, config, test, { indent: ' ', index, mode: 'error', includeTestId: options?.includeTestId }); lines.push(screen.colors.red(header)); for (const result of test.results) { const resultLines: string[] = []; @@ -494,22 +495,20 @@ export function stepSuffix(step: TestStep | undefined) { return stepTitles.map(t => t.split('\n')[0]).map(t => ' › ' + t).join(''); } -function formatTestTitle(screen: Screen, config: FullConfig, test: TestCase, step?: TestStep, omitLocation: boolean = false): string { +function formatTestTitle(screen: Screen, config: FullConfig, test: TestCase, step?: TestStep, options: { includeTestId?: boolean } = {}): string { // root, project, file, ...describes, test const [, projectName, , ...titles] = test.titlePath(); - let location; - if (omitLocation) - location = `${relativeTestPath(screen, config, test)}`; - else - location = `${relativeTestPath(screen, config, test)}:${test.location.line}:${test.location.column}`; - const projectTitle = projectName ? `[${projectName}] › ` : ''; - const testTitle = `${projectTitle}${location} › ${titles.join(' › ')}`; + const location = `${relativeTestPath(screen, config, test)}:${test.location.line}:${test.location.column}`; + const testId = options.includeTestId ? `[id=${test.id}] ` : ''; + const projectLabel = options.includeTestId ? `project=` : ''; + const projectTitle = projectName ? `[${projectLabel}${projectName}] › ` : ''; + const testTitle = `${testId}${projectTitle}${location} › ${titles.join(' › ')}`; const extraTags = test.tags.filter(t => !testTitle.includes(t)); return `${testTitle}${stepSuffix(step)}${extraTags.length ? ' ' + extraTags.join(' ') : ''}`; } -function formatTestHeader(screen: Screen, config: FullConfig, test: TestCase, options: { indent?: string, index?: number, mode?: 'default' | 'error' } = {}): string { - const title = formatTestTitle(screen, config, test); +function formatTestHeader(screen: Screen, config: FullConfig, test: TestCase, options: { indent?: string, index?: number, mode?: 'default' | 'error', includeTestId?: boolean } = {}): string { + const title = formatTestTitle(screen, config, test, undefined, options); const header = `${options.indent || ''}${options.index ? options.index + ') ' : ''}${title}`; let fullHeader = header; diff --git a/packages/playwright/src/reporters/listModeReporter.ts b/packages/playwright/src/reporters/listModeReporter.ts index 2337067ed039f..1732cd8db6fcb 100644 --- a/packages/playwright/src/reporters/listModeReporter.ts +++ b/packages/playwright/src/reporters/listModeReporter.ts @@ -26,8 +26,10 @@ import type { ReporterV2 } from './reporterV2'; class ListModeReporter implements ReporterV2 { private config!: FullConfig; private screen: TerminalScreen; + private _options: { screen?: TerminalScreen, includeTestId?: boolean }; - constructor(options?: { screen?: TerminalScreen }) { + constructor(options: { screen?: TerminalScreen, includeTestId?: boolean } = {}) { + this._options = options; this.screen = options?.screen ?? terminalScreen; } @@ -47,8 +49,10 @@ class ListModeReporter implements ReporterV2 { // root, project, file, ...describes, test const [, projectName, , ...titles] = test.titlePath(); const location = `${path.relative(this.config.rootDir, test.location.file)}:${test.location.line}:${test.location.column}`; - const projectTitle = projectName ? `[${projectName}] › ` : ''; - this._writeLine(` ${projectTitle}${location} › ${titles.join(' › ')}`); + const testId = this._options.includeTestId ? `[id=${test.id}] ` : ''; + const projectLabel = this._options.includeTestId ? `project=` : ''; + const projectTitle = projectName ? `[${projectLabel}${projectName}] › ` : ''; + this._writeLine(` ${testId}${projectTitle}${location} › ${titles.join(' › ')}`); files.add(test.location.file); } this._writeLine(`Total: ${tests.length} ${tests.length === 1 ? 'test' : 'tests'} in ${files.size} ${files.size === 1 ? 'file' : 'files'}`); From 27194da5662f633741d1bfa1ca7fa41b9f2a09f8 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Sun, 31 Aug 2025 07:10:01 +0100 Subject: [PATCH 060/329] fix(toHaveScreenshot): nice error when rebasing with bad expectation (#37243) --- .../src/server/utils/comparators.ts | 4 ++-- .../src/matchers/toMatchSnapshot.ts | 17 +++++++++----- .../to-have-screenshot.spec.ts | 22 +++++++++++++++++-- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/playwright-core/src/server/utils/comparators.ts b/packages/playwright-core/src/server/utils/comparators.ts index 59871ac5ef815..747abb2ecf609 100644 --- a/packages/playwright-core/src/server/utils/comparators.ts +++ b/packages/playwright-core/src/server/utils/comparators.ts @@ -98,11 +98,11 @@ function validateBuffer(buffer: Buffer, mimeType: string): void { if (mimeType === 'image/png') { const pngMagicNumber = [137, 80, 78, 71, 13, 10, 26, 10]; if (buffer.length < pngMagicNumber.length || !pngMagicNumber.every((byte, index) => buffer[index] === byte)) - throw new Error('could not decode image as PNG.'); + throw new Error('Could not decode expected image as PNG.'); } else if (mimeType === 'image/jpeg') { const jpegMagicNumber = [255, 216]; if (buffer.length < jpegMagicNumber.length || !jpegMagicNumber.every((byte, index) => buffer[index] === byte)) - throw new Error('could not decode image as JPEG.'); + throw new Error('Could not decode expected image as JPEG.'); } } diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts index d6223063eeadb..5136fa68847cc 100644 --- a/packages/playwright/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchSnapshot.ts @@ -398,9 +398,9 @@ export async function toHaveScreenshot( expectScreenshotOptions.expected = helper.updateSnapshots === 'all' ? undefined : expected; const { actual, previous, diff, errorMessage, log, timedOut } = await page._expectScreenshot(expectScreenshotOptions); - const writeFiles = () => { - writeFileSync(helper.expectedPath, actual!); - writeFileSync(helper.actualPath, actual!); + const writeFiles = (actualBuffer: Buffer) => { + writeFileSync(helper.expectedPath, actualBuffer); + writeFileSync(helper.actualPath, actualBuffer); /* eslint-disable no-console */ console.log(helper.expectedPath + ' is re-generated, writing actual.'); return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true); @@ -410,13 +410,18 @@ export async function toHaveScreenshot( // Screenshot is matching, but is not necessarily the same as the expected. if (helper.updateSnapshots === 'all' && actual && compareBuffersOrStrings(actual, expected)) { console.log(helper.expectedPath + ' is re-generated, writing actual.'); - return writeFiles(); + return writeFiles(actual); } return helper.handleMatching(); } - if (helper.updateSnapshots === 'changed' || helper.updateSnapshots === 'all') - return writeFiles(); + if (helper.updateSnapshots === 'changed' || helper.updateSnapshots === 'all') { + if (actual) + return writeFiles(actual); + let header = matcherHint(this, undefined, 'toHaveScreenshot', receiver, undefined, undefined, timedOut ? timeout : undefined); + header += ' Failed to re-generate expected.\n'; + return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log, this._stepInfo); + } const header = matcherHint(this, undefined, 'toHaveScreenshot', receiver, undefined, undefined, timedOut ? timeout : undefined); return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log, this._stepInfo); diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts index 0fea415bf1058..6ec17e52f79dd 100644 --- a/tests/playwright-test/to-have-screenshot.spec.ts +++ b/tests/playwright-test/to-have-screenshot.spec.ts @@ -1350,8 +1350,26 @@ test('should throw pretty error if expected PNG file is not a PNG', async ({ run `, }); expect(result.exitCode).toBe(1); - expect(result.output).toContain('could not decode image as PNG.'); - expect(result.output).toContain('could not decode image as JPEG.'); + expect(result.output).toContain('Could not decode expected image as PNG.'); + expect(result.output).toContain('Could not decode expected image as JPEG.'); +}); + +test('should throw pretty error if expected PNG file is not a PNG while rebasing', async ({ runInlineTest }) => { + const result = await runInlineTest({ + ...playwrightConfig({ + snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}', + }), + '__screenshots__/a.spec.js/snapshot.png': 'not a png', + 'a.spec.js': ` + const { test, expect } = require('@playwright/test'); + test('png', async ({ page }) => { + await expect(page).toHaveScreenshot('snapshot.png'); + }); + `, + }, { 'update-snapshots': true }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('Failed to re-generate expected.'); + expect(result.output).toContain('Could not decode expected image as PNG.'); }); test('should support maskColor option', async ({ runInlineTest }) => { From 0fb8027124d15ef70356b6c5a90734be633c8934 Mon Sep 17 00:00:00 2001 From: papple23g Date: Mon, 1 Sep 2025 14:51:04 +0800 Subject: [PATCH 061/329] docs: fix Python method name in Locator.and example (#37251) Co-authored-by: papple23g --- docs/src/api/class-locator.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 0859289a7fe66..a5a92939bc2f6 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -133,11 +133,11 @@ Locator button = page.getByRole(AriaRole.BUTTON).and(page.getByTitle("Subscribe" ``` ```python async -button = page.get_by_role("button").and_(page.getByTitle("Subscribe")) +button = page.get_by_role("button").and_(page.get_by_title("Subscribe")) ``` ```python sync -button = page.get_by_role("button").and_(page.getByTitle("Subscribe")) +button = page.get_by_role("button").and_(page.get_by_title("Subscribe")) ``` ```csharp From 2b5e9ba1fb902aa2088e3b5918a83adaf31cc154 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:48:43 +0200 Subject: [PATCH 062/329] test: roll stable-test-runner to 1.56.0-alpha-2025-09-01 (#37254) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- .../stable-test-runner/package-lock.json | 46 +++++++++---------- .../stable-test-runner/package.json | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index e737d97b72054..ad4cb4165b9c4 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,16 +5,16 @@ "packages": { "": { "dependencies": { - "@playwright/test": "^1.56.0-alpha-2025-08-25" + "@playwright/test": "^1.56.0-alpha-2025-09-01" } }, "node_modules/@playwright/test": { - "version": "1.56.0-alpha-2025-08-25", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-08-25.tgz", - "integrity": "sha512-uJ9s4hgFoJ8qRI3nLKwyE6Ms9o04EZkMfEszm1yC5WAZfAGn0XU2X05lXzXLMo65j4PdCYDQWH0buMm2Ibi2Lw==", + "version": "1.56.0-alpha-2025-09-01", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-09-01.tgz", + "integrity": "sha512-076KFGO1l6dX+k33C6mCpCvTJ0fWjhg0HHow/dr5+Tunnflf8lkSsBK+thk+gTlj3zTMfizTvcYF1H8UQe4anA==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.0-alpha-2025-08-25" + "playwright": "1.56.0-alpha-2025-09-01" }, "bin": { "playwright": "cli.js" @@ -38,12 +38,12 @@ } }, "node_modules/playwright": { - "version": "1.56.0-alpha-2025-08-25", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-08-25.tgz", - "integrity": "sha512-Zurkh6SJxizvoRS61yFcTXHMQeXDiSRUvjW0gAUQxwWZ9rAjeV59+CzSrA5QvSB60b+Pr8w8uPfgF0gByWfcFQ==", + "version": "1.56.0-alpha-2025-09-01", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-09-01.tgz", + "integrity": "sha512-WKc7c0KeJjgWLQ/KCTtGALGqz8sP/PF27pt6sweo2d11ZDRNh5Fz095zZceT8ABxzB1pjpftvYt2dyG5emi1IA==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-alpha-2025-08-25" + "playwright-core": "1.56.0-alpha-2025-09-01" }, "bin": { "playwright": "cli.js" @@ -56,9 +56,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.0-alpha-2025-08-25", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-08-25.tgz", - "integrity": "sha512-hbMI9NttWQgshEaCtyEkTN1NxKR1bpOCJH3MjXTpNlwXTA8SK1RCYaqSgAwAH5yxJJdsi3+nz4AdF0xToaeCTA==", + "version": "1.56.0-alpha-2025-09-01", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-09-01.tgz", + "integrity": "sha512-tER9C21Q0xSkZxrpoPa0LkPOgygqGCwEQPPVJgi2AGOVrOwG42q07P5k0U5Oh0MUv+QvmF4cIb8BdG67dOjiUg==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -70,11 +70,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.56.0-alpha-2025-08-25", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-08-25.tgz", - "integrity": "sha512-uJ9s4hgFoJ8qRI3nLKwyE6Ms9o04EZkMfEszm1yC5WAZfAGn0XU2X05lXzXLMo65j4PdCYDQWH0buMm2Ibi2Lw==", + "version": "1.56.0-alpha-2025-09-01", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-09-01.tgz", + "integrity": "sha512-076KFGO1l6dX+k33C6mCpCvTJ0fWjhg0HHow/dr5+Tunnflf8lkSsBK+thk+gTlj3zTMfizTvcYF1H8UQe4anA==", "requires": { - "playwright": "1.56.0-alpha-2025-08-25" + "playwright": "1.56.0-alpha-2025-09-01" } }, "fsevents": { @@ -84,18 +84,18 @@ "optional": true }, "playwright": { - "version": "1.56.0-alpha-2025-08-25", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-08-25.tgz", - "integrity": "sha512-Zurkh6SJxizvoRS61yFcTXHMQeXDiSRUvjW0gAUQxwWZ9rAjeV59+CzSrA5QvSB60b+Pr8w8uPfgF0gByWfcFQ==", + "version": "1.56.0-alpha-2025-09-01", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-09-01.tgz", + "integrity": "sha512-WKc7c0KeJjgWLQ/KCTtGALGqz8sP/PF27pt6sweo2d11ZDRNh5Fz095zZceT8ABxzB1pjpftvYt2dyG5emi1IA==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.56.0-alpha-2025-08-25" + "playwright-core": "1.56.0-alpha-2025-09-01" } }, "playwright-core": { - "version": "1.56.0-alpha-2025-08-25", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-08-25.tgz", - "integrity": "sha512-hbMI9NttWQgshEaCtyEkTN1NxKR1bpOCJH3MjXTpNlwXTA8SK1RCYaqSgAwAH5yxJJdsi3+nz4AdF0xToaeCTA==" + "version": "1.56.0-alpha-2025-09-01", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-09-01.tgz", + "integrity": "sha512-tER9C21Q0xSkZxrpoPa0LkPOgygqGCwEQPPVJgi2AGOVrOwG42q07P5k0U5Oh0MUv+QvmF4cIb8BdG67dOjiUg==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index 944dcc77bc082..1327275f71046 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "^1.56.0-alpha-2025-08-25" + "@playwright/test": "^1.56.0-alpha-2025-09-01" } } From 0cdfc8dd20c11b9ac7c50ac0c96277ebc9318334 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 1 Sep 2025 17:18:43 +0200 Subject: [PATCH 063/329] test: extension tests need firefox (#37257) --- .github/workflows/tests_primary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests_primary.yml b/.github/workflows/tests_primary.yml index 5176a7335bdb9..0a6f2e185749f 100644 --- a/.github/workflows/tests_primary.yml +++ b/.github/workflows/tests_primary.yml @@ -185,7 +185,7 @@ jobs: env: DEBUG: pw:install - run: npm run build - - run: npx playwright install chromium + - run: npx playwright install chromium firefox - name: Checkout extension run: git clone https://github.com/microsoft/playwright-vscode.git - name: Print extension revision From 0d17c706984aa77d445ee902633898ec122aa7ea Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:40:22 +0200 Subject: [PATCH 064/329] chore(driver): roll driver to recent Node.js LTS version (#37264) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- utils/build/build-playwright-driver.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/build/build-playwright-driver.sh b/utils/build/build-playwright-driver.sh index 35cff644fd4b6..6a09a57e57cff 100755 --- a/utils/build/build-playwright-driver.sh +++ b/utils/build/build-playwright-driver.sh @@ -4,7 +4,7 @@ set -x trap "cd $(pwd -P)" EXIT SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" -NODE_VERSION="22.18.0" # autogenerated via ./update-playwright-driver-version.mjs +NODE_VERSION="22.19.0" # autogenerated via ./update-playwright-driver-version.mjs cd "$(dirname "$0")" PACKAGE_VERSION=$(node -p "require('../../package.json').version") From 8f12ad0f8be71b9c9bcba08feaa746356a56b03b Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Tue, 2 Sep 2025 11:09:56 -0700 Subject: [PATCH 065/329] fix(html): properly handle retry button highlight, fixing contrast issue (#36861) --- packages/html-reporter/src/colors.css | 2 ++ packages/html-reporter/src/tabbedPane.css | 12 ++++++++---- packages/html-reporter/src/tabbedPane.tsx | 1 + packages/html-reporter/src/testCaseView.css | 4 ++++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/html-reporter/src/colors.css b/packages/html-reporter/src/colors.css index c11249829415c..42b254ffa3805 100644 --- a/packages/html-reporter/src/colors.css +++ b/packages/html-reporter/src/colors.css @@ -215,6 +215,7 @@ SOFTWARE. */ --color-header-search-border: #57606a; --color-sidenav-selected-bg: #ffffff; --color-menu-bg-active: rgba(0,0,0,0); + --color-control-transparent-bg-hover: #818b981a; --color-input-disabled-bg: rgba(175,184,193,0.2); --color-timeline-badge-bg: #eaeef2; --color-ansi-black: #24292f; @@ -649,6 +650,7 @@ SOFTWARE. */ --color-header-search-border: #30363d; --color-sidenav-selected-bg: #21262d; --color-menu-bg-active: #161b22; + --color-control-transparent-bg-hover: #656c7633; --color-input-disabled-bg: rgba(110,118,129,0); --color-timeline-badge-bg: #21262d; --color-ansi-black: #484f58; diff --git a/packages/html-reporter/src/tabbedPane.css b/packages/html-reporter/src/tabbedPane.css index 7a128da217f7e..042ab9f7608d5 100644 --- a/packages/html-reporter/src/tabbedPane.css +++ b/packages/html-reporter/src/tabbedPane.css @@ -59,12 +59,16 @@ overflow: hidden; text-overflow: ellipsis; display: inline-block; + height: 30px; + padding: 0 8px; + border-radius: 6px; } -.tabbed-pane-tab-element.selected { - border-bottom-color: #666; +.tabbed-pane-tab-label:hover { + background-color: var(--color-control-transparent-bg-hover); } -.tabbed-pane-tab-element:hover { - color: #333; +.tabbed-pane-tab-element.selected { + border-bottom-color: #666; + -webkit-text-stroke: 0.5px currentColor; } diff --git a/packages/html-reporter/src/tabbedPane.tsx b/packages/html-reporter/src/tabbedPane.tsx index af15b6b4fa381..25d1f72169db8 100644 --- a/packages/html-reporter/src/tabbedPane.tsx +++ b/packages/html-reporter/src/tabbedPane.tsx @@ -15,6 +15,7 @@ */ import { clsx } from '@web/uiUtils'; +import './colors.css'; import './tabbedPane.css'; import * as React from 'react'; diff --git a/packages/html-reporter/src/testCaseView.css b/packages/html-reporter/src/testCaseView.css index 9c8dd8f22eda1..496f1bdb2f409 100644 --- a/packages/html-reporter/src/testCaseView.css +++ b/packages/html-reporter/src/testCaseView.css @@ -41,6 +41,10 @@ padding: 0 8px 8px; } +.selected .test-case-run-duration { + -webkit-text-stroke: 0; +} + .test-case-run-duration { color: var(--color-fg-muted); padding-left: 8px; From f9e53c33263b3d13bda15d70e70a13faf2689043 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 2 Sep 2025 20:55:42 +0100 Subject: [PATCH 066/329] fix(snapshotter): mismatching snapshot index (#37258) --- .../src/server/trace/recorder/snapshotter.ts | 21 ++++++++++-- .../trace/recorder/snapshotterInjected.ts | 4 ++- tests/library/trace-viewer.spec.ts | 34 +++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts index 5d0139df0718a..9b55439052dc5 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts @@ -100,13 +100,26 @@ export class Snapshotter { eventsHelper.removeEventListeners(this._eventListeners); } - async captureSnapshot(page: Page, callId: string, snapshotName: string): Promise { + private async _captureFrameSnapshot(frame: Frame): Promise { // Prepare expression synchronously. - const expression = `window["${this._snapshotStreamer}"].captureSnapshot(${JSON.stringify(snapshotName)})`; + const needsReset = !!(frame as any)[kNeedsResetSymbol]; + (frame as any)[kNeedsResetSymbol] = false; + const expression = `window["${this._snapshotStreamer}"].captureSnapshot(${needsReset ? 'true' : 'false'})`; + try { + return await frame.nonStallingRawEvaluateInExistingMainContext(expression); + } catch (e) { + // If we fail to capture snapshot in this frame, we cannot rely on the snapshot index + // being the same here and in snapshotter injected script. + // Therefore, next time force a reset to avoid using node references. + (frame as any)[kNeedsResetSymbol] = true; + debugLogger.log('error', e); + } + } + async captureSnapshot(page: Page, callId: string, snapshotName: string): Promise { // In each frame, in a non-stalling manner, capture the snapshots. const snapshots = page.frames().map(async frame => { - const data = await frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => debugLogger.log('error', e)) as SnapshotData; + const data = await this._captureFrameSnapshot(frame); // Something went wrong -> bail out, our snapshots are best-efforty. if (!data || !this._started) return; @@ -163,3 +176,5 @@ export class Snapshotter { } } } + +const kNeedsResetSymbol = Symbol('kNeedsReset'); diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts index 953868585ba70..86fa015eb3d9e 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts @@ -318,9 +318,11 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript: } } - captureSnapshot(): SnapshotData | undefined { + captureSnapshot(needsReset: boolean): SnapshotData | undefined { const timestamp = performance.now(); const snapshotNumber = ++this._lastSnapshotNumber; + if (needsReset) + this.reset(); let nodeCounter = 0; let shadowDomNesting = 0; let headNesting = 0; diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 5352621bafac6..47fc325b28764 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -1979,3 +1979,37 @@ test('should show all actions', async ({ runAndTrace, page }) => { /Expect "toBeChecked"/, ]); }); + +test('should handle failed snapshots due to dialog', async ({ page, server, runAndTrace }) => { + const traceViewer = await runAndTrace(async () => { + await page.setContent(` + + + + + + + + `); + let dialogMessage = ''; + page.on('dialog', async dialog => { + dialogMessage = dialog.message(); + await dialog.accept(); + }); + await page.goBack(); + await expect.poll(() => dialogMessage).toBe('ready?'); + await expect(page.getByRole('button')).toHaveText('Clicked'); + }); + + const frame = await traceViewer.snapshotFrame('Expect'); + await expect(frame.getByRole('button')).toHaveCSS('color', 'rgb(255, 0, 0)'); +}); From 22540e9839901289605efc1667d435693ca0d3ff Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:05:34 +0200 Subject: [PATCH 067/329] feat(chromium-tip-of-tree): roll to r1365 (#37269) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index c2b11841af441..1a87f1a0f7089 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,15 +15,15 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1363", + "revision": "1365", "installByDefault": false, - "browserVersion": "141.0.7378.0" + "browserVersion": "141.0.7390.0" }, { "name": "chromium-tip-of-tree-headless-shell", - "revision": "1363", + "revision": "1365", "installByDefault": false, - "browserVersion": "141.0.7378.0" + "browserVersion": "141.0.7390.0" }, { "name": "firefox", From 3966e8ccb29ee5c64a1e6358a1c4d12c7e0ed9ab Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:56:24 +0200 Subject: [PATCH 068/329] feat(webkit): roll to r2207 (#37272) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 1a87f1a0f7089..c598c923a028f 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -39,7 +39,7 @@ }, { "name": "webkit", - "revision": "2206", + "revision": "2207", "installByDefault": true, "revisionOverrides": { "debian11-x64": "2105", From 7cab4ec393be25449d47f2a471b1439d3c5df5fd Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:08:02 +0200 Subject: [PATCH 069/329] feat(chromium): roll to r1189 (#37231) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- README.md | 4 +- packages/playwright-core/browsers.json | 8 +- .../src/server/deviceDescriptorsSource.json | 108 +++++++++--------- 3 files changed, 60 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 074a166b68402..4be427c5ae676 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-140.0.7339.24-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-141.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-140.0.7339.41-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-141.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 140.0.7339.24 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 140.0.7339.41 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 26.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 141.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index c598c923a028f..39ed0bf87398d 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,15 +3,15 @@ "browsers": [ { "name": "chromium", - "revision": "1188", + "revision": "1189", "installByDefault": true, - "browserVersion": "140.0.7339.24" + "browserVersion": "140.0.7339.41" }, { "name": "chromium-headless-shell", - "revision": "1188", + "revision": "1189", "installByDefault": true, - "browserVersion": "140.0.7339.24" + "browserVersion": "140.0.7339.41" }, { "name": "chromium-tip-of-tree", diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index df5d7497108a0..4adee517afb90 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -110,7 +110,7 @@ "defaultBrowserType": "webkit" }, "Galaxy S5": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -121,7 +121,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -132,7 +132,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 360, "height": 740 @@ -143,7 +143,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 740, "height": 360 @@ -154,7 +154,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 320, "height": 658 @@ -165,7 +165,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+ landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 658, "height": 320 @@ -176,7 +176,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S24": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 360, "height": 780 @@ -187,7 +187,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S24 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-S921U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 780, "height": 360 @@ -198,7 +198,7 @@ "defaultBrowserType": "chromium" }, "Galaxy A55": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 480, "height": 1040 @@ -209,7 +209,7 @@ "defaultBrowserType": "chromium" }, "Galaxy A55 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-A556B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 1040, "height": 480 @@ -220,7 +220,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36", "viewport": { "width": 712, "height": 1138 @@ -231,7 +231,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36", "viewport": { "width": 1138, "height": 712 @@ -242,7 +242,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S9": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36", "viewport": { "width": 640, "height": 1024 @@ -253,7 +253,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S9 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; SM-X710) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36", "viewport": { "width": 1024, "height": 640 @@ -1208,7 +1208,7 @@ "defaultBrowserType": "webkit" }, "LG Optimus L70": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1219,7 +1219,7 @@ "defaultBrowserType": "chromium" }, "LG Optimus L70 landscape": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1230,7 +1230,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1241,7 +1241,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1252,7 +1252,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1263,7 +1263,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1274,7 +1274,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36", "viewport": { "width": 800, "height": 1280 @@ -1285,7 +1285,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36", "viewport": { "width": 1280, "height": 800 @@ -1296,7 +1296,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1307,7 +1307,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1318,7 +1318,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1329,7 +1329,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1340,7 +1340,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1351,7 +1351,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1362,7 +1362,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1373,7 +1373,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1384,7 +1384,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1395,7 +1395,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1406,7 +1406,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36", "viewport": { "width": 600, "height": 960 @@ -1417,7 +1417,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36", "viewport": { "width": 960, "height": 600 @@ -1472,7 +1472,7 @@ "defaultBrowserType": "webkit" }, "Pixel 2": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 411, "height": 731 @@ -1483,7 +1483,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 731, "height": 411 @@ -1494,7 +1494,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 411, "height": 823 @@ -1505,7 +1505,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 823, "height": 411 @@ -1516,7 +1516,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 393, "height": 786 @@ -1527,7 +1527,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 786, "height": 393 @@ -1538,7 +1538,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 353, "height": 745 @@ -1549,7 +1549,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 745, "height": 353 @@ -1560,7 +1560,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G)": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "screen": { "width": 412, "height": 892 @@ -1575,7 +1575,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G) landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "screen": { "height": 892, "width": 412 @@ -1590,7 +1590,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "screen": { "width": 393, "height": 851 @@ -1605,7 +1605,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "screen": { "width": 851, "height": 393 @@ -1620,7 +1620,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "screen": { "width": 412, "height": 915 @@ -1635,7 +1635,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "screen": { "width": 915, "height": 412 @@ -1650,7 +1650,7 @@ "defaultBrowserType": "chromium" }, "Moto G4": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1661,7 +1661,7 @@ "defaultBrowserType": "chromium" }, "Moto G4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1672,7 +1672,7 @@ "defaultBrowserType": "chromium" }, "Desktop Chrome HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36", "screen": { "width": 1792, "height": 1120 @@ -1687,7 +1687,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36 Edg/140.0.7339.24", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36 Edg/140.0.7339.41", "screen": { "width": 1792, "height": 1120 @@ -1732,7 +1732,7 @@ "defaultBrowserType": "webkit" }, "Desktop Chrome": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36", "screen": { "width": 1920, "height": 1080 @@ -1747,7 +1747,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.24 Safari/537.36 Edg/140.0.7339.24", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36 Edg/140.0.7339.41", "screen": { "width": 1920, "height": 1080 From 8036df3735fe20df82cbcc5866e68efef52586c8 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 3 Sep 2025 17:00:43 +0200 Subject: [PATCH 070/329] test: unflake 'should handle failed snapshots due to dialog' test (#37282) --- tests/library/trace-viewer.spec.ts | 67 +++++++++++++++++------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 47fc325b28764..2b8a3376e6658 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -1980,36 +1980,43 @@ test('should show all actions', async ({ runAndTrace, page }) => { ]); }); -test('should handle failed snapshots due to dialog', async ({ page, server, runAndTrace }) => { - const traceViewer = await runAndTrace(async () => { - await page.setContent(` - - - - - - - - `); - let dialogMessage = ''; - page.on('dialog', async dialog => { - dialogMessage = dialog.message(); - await dialog.accept(); +test.describe(() => { + // NOTE: In Firefox/WebKit, history.pushState() requires a SecureContext. + // On http/about:blank it throws "The operation is insecure". + test.use({ ignoreHTTPSErrors: true }); + + test('should handle failed snapshots due to dialog', async ({ page, httpsServer, runAndTrace }) => { + const traceViewer = await runAndTrace(async () => { + await page.goto(httpsServer.EMPTY_PAGE); + await page.setContent(` + + + + + + + + `); + let dialogMessage = ''; + page.on('dialog', async dialog => { + dialogMessage = dialog.message(); + await dialog.accept(); + }); + await page.goBack(); + await expect.poll(() => dialogMessage).toBe('ready?'); + await expect(page.getByRole('button')).toHaveText('Clicked'); }); - await page.goBack(); - await expect.poll(() => dialogMessage).toBe('ready?'); - await expect(page.getByRole('button')).toHaveText('Clicked'); - }); - const frame = await traceViewer.snapshotFrame('Expect'); - await expect(frame.getByRole('button')).toHaveCSS('color', 'rgb(255, 0, 0)'); + const frame = await traceViewer.snapshotFrame('Expect'); + await expect(frame.getByRole('button')).toHaveCSS('color', 'rgb(255, 0, 0)'); + }); }); From 90e302ba39cf04812054fcd80f633a913dceb46e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 3 Sep 2025 17:29:46 -0700 Subject: [PATCH 071/329] chore: move playwright-mcp into the monorepo (#37286) --- .github/workflows/test_mcp.yml | 85 + .gitignore | 2 + eslint.config.mjs | 1 + package-lock.json | 2174 ++--------------- package.json | 9 +- packages/mcp-extension/README.md | 48 + packages/mcp-extension/icons/icon-128.png | Bin 0 -> 6352 bytes packages/mcp-extension/icons/icon-16.png | Bin 0 -> 571 bytes packages/mcp-extension/icons/icon-32.png | Bin 0 -> 1258 bytes packages/mcp-extension/icons/icon-48.png | Bin 0 -> 2043 bytes packages/mcp-extension/manifest.json | 35 + packages/mcp-extension/package.json | 35 + packages/mcp-extension/src/background.ts | 222 ++ packages/mcp-extension/src/relayConnection.ts | 178 ++ packages/mcp-extension/src/ui/connect.css | 206 ++ packages/mcp-extension/src/ui/connect.html | 29 + packages/mcp-extension/src/ui/connect.tsx | 233 ++ packages/mcp-extension/src/ui/status.html | 13 + packages/mcp-extension/src/ui/status.tsx | 110 + packages/mcp-extension/src/ui/tabItem.tsx | 67 + packages/mcp-extension/src/ui/tsconfig.json | 4 + packages/mcp-extension/tsconfig.json | 22 + packages/mcp-extension/tsconfig.ui.json | 19 + packages/mcp-extension/vite.config.mts | 54 + packages/mcp-extension/vite.sw.config.mts | 31 + .../bundles/utils/src/utilsBundleImpl.ts | 1 + packages/playwright-core/src/utilsBundle.ts | 1 + packages/playwright/ThirdPartyNotices.txt | 6 +- .../playwright/bundles/mcp/package-lock.json | 8 +- packages/playwright/bundles/mcp/package.json | 2 +- packages/playwright/package.json | 5 +- packages/playwright/src/DEPS.list | 1 + packages/playwright/src/index.ts | 2 +- packages/playwright/src/matchers/DEPS.list | 2 +- .../playwright/src/matchers/toBeTruthy.ts | 2 +- packages/playwright/src/matchers/toEqual.ts | 2 +- .../playwright/src/matchers/toMatchText.ts | 2 +- packages/playwright/src/mcp/DEPS.list | 8 + packages/playwright/src/mcp/browser/DEPS.list | 4 +- .../playwright/src/mcp/browser/actions.d.ts | 172 ++ .../src/mcp/browser/browserContextFactory.ts | 251 ++ .../src/mcp/browser/browserServerBackend.ts | 88 + .../playwright/src/mcp/browser/codegen.ts | 53 + packages/playwright/src/mcp/browser/config.ts | 327 +++ .../playwright/src/mcp/browser/context.ts | 276 +++ .../playwright/src/mcp/browser/response.ts | 201 ++ .../playwright/src/mcp/browser/sessionLog.ts | 176 ++ packages/playwright/src/mcp/browser/tab.ts | 314 +++ packages/playwright/src/mcp/browser/tools.ts | 137 +- .../src/mcp/browser/tools/DEPS.list | 3 + .../src/mcp/browser/tools/common.ts | 63 + .../src/mcp/browser/tools/console.ts | 36 + .../src/mcp/browser/tools/dialogs.ts | 55 + .../src/mcp/browser/tools/evaluate.ts | 61 + .../playwright/src/mcp/browser/tools/files.ts | 52 + .../playwright/src/mcp/browser/tools/form.ts | 60 + .../src/mcp/browser/tools/install.ts | 56 + .../src/mcp/browser/tools/keyboard.ts | 88 + .../playwright/src/mcp/browser/tools/mouse.ts | 113 + .../src/mcp/browser/tools/navigate.ts | 62 + .../src/mcp/browser/tools/network.ts | 49 + .../playwright/src/mcp/browser/tools/pdf.ts | 46 + .../src/mcp/browser/tools/screenshot.ts | 91 + .../src/mcp/browser/tools/snapshot.ts | 165 ++ .../playwright/src/mcp/browser/tools/tabs.ts | 64 + .../playwright/src/mcp/browser/tools/tool.ts | 70 + .../playwright/src/mcp/browser/tools/utils.ts | 84 + .../src/mcp/browser/tools/verify.ts | 148 ++ .../playwright/src/mcp/browser/tools/wait.ts | 65 + packages/playwright/src/mcp/config.d.ts | 120 + .../playwright/src/mcp/extension/DEPS.list | 4 + .../playwright/src/mcp/extension/cdpRelay.ts | 421 ++++ .../mcp/extension/extensionContextFactory.ts | 66 + .../playwright/src/mcp/extension/protocol.ts | 42 + packages/playwright/src/mcp/index.ts | 52 + .../src/mcp/{test/tool.ts => log.ts} | 15 +- packages/playwright/src/mcp/program.ts | 135 + packages/playwright/src/mcp/sdk/DEPS.list | 1 + packages/playwright/src/mcp/sdk/README.md | 1 + packages/playwright/src/mcp/sdk/bundle.ts | 4 +- packages/playwright/src/mcp/sdk/call.ts | 31 - packages/playwright/src/mcp/sdk/exports.ts | 2 +- packages/playwright/src/mcp/sdk/http.ts | 32 +- packages/playwright/src/mcp/sdk/mdb.ts | 28 +- .../playwright/src/mcp/sdk/proxyBackend.ts | 16 +- packages/playwright/src/mcp/sdk/server.ts | 15 +- packages/playwright/src/mcp/sdk/tool.ts | 4 +- .../backend.ts => test/browserBackend.ts} | 13 +- .../{browser/tool.ts => test/browserTool.ts} | 4 +- .../playwright/src/mcp/test/browserTools.ts | 113 + .../mcp/test/{backend.ts => testBackend.ts} | 16 +- .../mcp/test/{context.ts => testContext.ts} | 2 +- packages/playwright/src/mcp/test/testTool.ts | 29 + .../src/mcp/test/{tools.ts => testTools.ts} | 8 +- packages/playwright/src/program.ts | 5 +- tests/config/testserver/index.ts | 9 + tests/mcp/capabilities.spec.ts | 106 + tests/mcp/cdp.spec.ts | 92 + tests/mcp/click.spec.ts | 99 + tests/mcp/config.spec.ts | 80 + tests/mcp/console.spec.ts | 100 + tests/mcp/core.spec.ts | 190 ++ tests/mcp/device.spec.ts | 45 + tests/mcp/dialogs.spec.ts | 255 ++ tests/mcp/evaluate.spec.ts | 99 + tests/mcp/extension.spec.ts | 306 +++ tests/mcp/files.spec.ts | 151 ++ tests/mcp/fixtures.ts | 280 +++ tests/mcp/form.spec.ts | 123 + tests/mcp/headed.spec.ts | 49 + tests/mcp/http.spec.ts | 254 ++ tests/mcp/iframes.spec.ts | 46 + tests/mcp/install.spec.ts | 26 + tests/mcp/launch.spec.ts | 169 ++ tests/mcp/library.spec.ts | 28 + tests/mcp/mdb.spec.ts | 208 ++ tests/mcp/network.spec.ts | 47 + tests/mcp/pdf.spec.ts | 88 + tests/mcp/playwright.config.extension.ts | 32 + tests/mcp/playwright.config.ts | 43 + tests/mcp/request-blocking.spec.ts | 82 + tests/mcp/roots.spec.ts | 83 + tests/mcp/screenshot.spec.ts | 327 +++ tests/mcp/session-log.spec.ts | 275 +++ tests/mcp/sse.spec.ts | 231 ++ tests/mcp/tabs.spec.ts | 155 ++ tests/mcp/trace.spec.ts | 37 + tests/mcp/type.spec.ts | 138 ++ tests/mcp/verify.spec.ts | 522 ++++ tests/mcp/wait.spec.ts | 107 + tests/mcp/webdriver.spec.ts | 40 + tsconfig.json | 1 + 132 files changed, 11025 insertions(+), 2126 deletions(-) create mode 100644 .github/workflows/test_mcp.yml create mode 100644 packages/mcp-extension/README.md create mode 100644 packages/mcp-extension/icons/icon-128.png create mode 100644 packages/mcp-extension/icons/icon-16.png create mode 100644 packages/mcp-extension/icons/icon-32.png create mode 100644 packages/mcp-extension/icons/icon-48.png create mode 100644 packages/mcp-extension/manifest.json create mode 100644 packages/mcp-extension/package.json create mode 100644 packages/mcp-extension/src/background.ts create mode 100644 packages/mcp-extension/src/relayConnection.ts create mode 100644 packages/mcp-extension/src/ui/connect.css create mode 100644 packages/mcp-extension/src/ui/connect.html create mode 100644 packages/mcp-extension/src/ui/connect.tsx create mode 100644 packages/mcp-extension/src/ui/status.html create mode 100644 packages/mcp-extension/src/ui/status.tsx create mode 100644 packages/mcp-extension/src/ui/tabItem.tsx create mode 100644 packages/mcp-extension/src/ui/tsconfig.json create mode 100644 packages/mcp-extension/tsconfig.json create mode 100644 packages/mcp-extension/tsconfig.ui.json create mode 100644 packages/mcp-extension/vite.config.mts create mode 100644 packages/mcp-extension/vite.sw.config.mts create mode 100644 packages/playwright/src/mcp/DEPS.list create mode 100644 packages/playwright/src/mcp/browser/actions.d.ts create mode 100644 packages/playwright/src/mcp/browser/browserContextFactory.ts create mode 100644 packages/playwright/src/mcp/browser/browserServerBackend.ts create mode 100644 packages/playwright/src/mcp/browser/codegen.ts create mode 100644 packages/playwright/src/mcp/browser/config.ts create mode 100644 packages/playwright/src/mcp/browser/context.ts create mode 100644 packages/playwright/src/mcp/browser/response.ts create mode 100644 packages/playwright/src/mcp/browser/sessionLog.ts create mode 100644 packages/playwright/src/mcp/browser/tab.ts create mode 100644 packages/playwright/src/mcp/browser/tools/DEPS.list create mode 100644 packages/playwright/src/mcp/browser/tools/common.ts create mode 100644 packages/playwright/src/mcp/browser/tools/console.ts create mode 100644 packages/playwright/src/mcp/browser/tools/dialogs.ts create mode 100644 packages/playwright/src/mcp/browser/tools/evaluate.ts create mode 100644 packages/playwright/src/mcp/browser/tools/files.ts create mode 100644 packages/playwright/src/mcp/browser/tools/form.ts create mode 100644 packages/playwright/src/mcp/browser/tools/install.ts create mode 100644 packages/playwright/src/mcp/browser/tools/keyboard.ts create mode 100644 packages/playwright/src/mcp/browser/tools/mouse.ts create mode 100644 packages/playwright/src/mcp/browser/tools/navigate.ts create mode 100644 packages/playwright/src/mcp/browser/tools/network.ts create mode 100644 packages/playwright/src/mcp/browser/tools/pdf.ts create mode 100644 packages/playwright/src/mcp/browser/tools/screenshot.ts create mode 100644 packages/playwright/src/mcp/browser/tools/snapshot.ts create mode 100644 packages/playwright/src/mcp/browser/tools/tabs.ts create mode 100644 packages/playwright/src/mcp/browser/tools/tool.ts create mode 100644 packages/playwright/src/mcp/browser/tools/utils.ts create mode 100644 packages/playwright/src/mcp/browser/tools/verify.ts create mode 100644 packages/playwright/src/mcp/browser/tools/wait.ts create mode 100644 packages/playwright/src/mcp/config.d.ts create mode 100644 packages/playwright/src/mcp/extension/DEPS.list create mode 100644 packages/playwright/src/mcp/extension/cdpRelay.ts create mode 100644 packages/playwright/src/mcp/extension/extensionContextFactory.ts create mode 100644 packages/playwright/src/mcp/extension/protocol.ts create mode 100644 packages/playwright/src/mcp/index.ts rename packages/playwright/src/mcp/{test/tool.ts => log.ts} (60%) create mode 100644 packages/playwright/src/mcp/program.ts create mode 100644 packages/playwright/src/mcp/sdk/README.md delete mode 100644 packages/playwright/src/mcp/sdk/call.ts rename packages/playwright/src/mcp/{browser/backend.ts => test/browserBackend.ts} (87%) rename packages/playwright/src/mcp/{browser/tool.ts => test/browserTool.ts} (83%) create mode 100644 packages/playwright/src/mcp/test/browserTools.ts rename packages/playwright/src/mcp/test/{backend.ts => testBackend.ts} (79%) rename packages/playwright/src/mcp/test/{context.ts => testContext.ts} (98%) create mode 100644 packages/playwright/src/mcp/test/testTool.ts rename packages/playwright/src/mcp/test/{tools.ts => testTools.ts} (96%) create mode 100644 tests/mcp/capabilities.spec.ts create mode 100644 tests/mcp/cdp.spec.ts create mode 100644 tests/mcp/click.spec.ts create mode 100644 tests/mcp/config.spec.ts create mode 100644 tests/mcp/console.spec.ts create mode 100644 tests/mcp/core.spec.ts create mode 100644 tests/mcp/device.spec.ts create mode 100644 tests/mcp/dialogs.spec.ts create mode 100644 tests/mcp/evaluate.spec.ts create mode 100644 tests/mcp/extension.spec.ts create mode 100644 tests/mcp/files.spec.ts create mode 100644 tests/mcp/fixtures.ts create mode 100644 tests/mcp/form.spec.ts create mode 100644 tests/mcp/headed.spec.ts create mode 100644 tests/mcp/http.spec.ts create mode 100644 tests/mcp/iframes.spec.ts create mode 100644 tests/mcp/install.spec.ts create mode 100644 tests/mcp/launch.spec.ts create mode 100644 tests/mcp/library.spec.ts create mode 100644 tests/mcp/mdb.spec.ts create mode 100644 tests/mcp/network.spec.ts create mode 100644 tests/mcp/pdf.spec.ts create mode 100644 tests/mcp/playwright.config.extension.ts create mode 100644 tests/mcp/playwright.config.ts create mode 100644 tests/mcp/request-blocking.spec.ts create mode 100644 tests/mcp/roots.spec.ts create mode 100644 tests/mcp/screenshot.spec.ts create mode 100644 tests/mcp/session-log.spec.ts create mode 100644 tests/mcp/sse.spec.ts create mode 100644 tests/mcp/tabs.spec.ts create mode 100644 tests/mcp/trace.spec.ts create mode 100644 tests/mcp/type.spec.ts create mode 100644 tests/mcp/verify.spec.ts create mode 100644 tests/mcp/wait.spec.ts create mode 100644 tests/mcp/webdriver.spec.ts diff --git a/.github/workflows/test_mcp.yml b/.github/workflows/test_mcp.yml new file mode 100644 index 0000000000000..91b569f8da757 --- /dev/null +++ b/.github/workflows/test_mcp.yml @@ -0,0 +1,85 @@ +name: MCP + +on: + push: + branches: + - main + - release-* + pull_request: + paths-ignore: + - 'browser_patches/**' + - 'docs/**' + branches: + - main + - release-* + +concurrency: + # For pull requests, cancel all currently-running jobs for this workflow + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + # Force terminal colors. @see https://www.npmjs.com/package/colors + FORCE_COLOR: 1 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + DEBUG_GIT_COMMIT_INFO: 1 + +jobs: + test_mcp: + name: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-15, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + - run: npm ci + - run: npm run build + - run: npx playwright install --with-deps + - run: npm run mcp-test + + test_mcp_extension: + name: Extension + strategy: + fail-fast: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: "Extension: Install dependencies" + run: npm ci + working-directory: ./packages/mcp-extension + - name: "Extension: Build" + run: npm run build + working-directory: ./packages/mcp-extension + - name: "Extension: Upload artifact" + uses: actions/upload-artifact@v4 + with: + name: extension + path: ./packages/mcp-extension/dist + retention-days: 7 + + - run: npm ci + - run: npm run build + - run: npx playwright install chromium + + - name: Run tests + run: | + if [[ "$(uname)" == "Linux" ]]; then + xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run mcp-etest + else + npm run mcp-etest + fi + shell: bash diff --git a/.gitignore b/.gitignore index 8e41e318108ae..07cfae4d196a6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ yarn.lock /packages/playwright-core/src/generated /packages/playwright-ct-core/src/generated packages/*/lib/ +packages/mcp-extension/dist/ +packages/mcp-extension/lib/ drivers/ .android-sdk/ .gradle/ diff --git a/eslint.config.mjs b/eslint.config.mjs index 141bcdccf1ebd..02dbc2f1a453c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -54,6 +54,7 @@ const ignores = [ "packages/html-reporter/playwright.config.ts", "packages/html-reporter/playwright/*", "packages/html-reporter/vite.config.ts", + "packages/mcp-extension/", "test-results/", "tests/assets/", "tests/components/", diff --git a/package-lock.json b/package-lock.json index c021c6c6039a5..55ebf2271fb2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.34.0", - "@modelcontextprotocol/sdk": "^1.17.3", + "@modelcontextprotocol/sdk": "^1.17.5", "@octokit/graphql-schema": "^15.26.0", "@stylistic/eslint-plugin": "^5.2.3", "@types/codemirror": "^5.60.7", @@ -61,7 +61,9 @@ "vite": "^6.3.4", "ws": "^8.17.1", "xml2js": "^0.5.0", - "yaml": "2.6.0" + "yaml": "2.6.0", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.24.6" }, "engines": { "node": ">=18" @@ -69,8 +71,6 @@ }, "node_modules/@actions/core": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", - "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "dev": true, "license": "MIT", "dependencies": { @@ -80,8 +80,6 @@ }, "node_modules/@actions/exec": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", - "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "dev": true, "license": "MIT", "dependencies": { @@ -90,8 +88,6 @@ }, "node_modules/@actions/github": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", - "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", "dev": true, "license": "MIT", "dependencies": { @@ -103,8 +99,6 @@ }, "node_modules/@actions/http-client": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", - "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", "dev": true, "license": "MIT", "dependencies": { @@ -114,15 +108,11 @@ }, "node_modules/@actions/io": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", - "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", "dev": true, "license": "MIT" }, "node_modules/@ampproject/remapping": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -134,8 +124,6 @@ }, "node_modules/@babel/code-frame": { "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -148,8 +136,6 @@ }, "node_modules/@babel/compat-data": { "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -157,8 +143,6 @@ }, "node_modules/@babel/core": { "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", - "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -187,8 +171,6 @@ }, "node_modules/@babel/generator": { "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", - "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", "license": "MIT", "dependencies": { "@babel/parser": "^7.26.9", @@ -203,8 +185,6 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.26.5", @@ -219,8 +199,6 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -232,8 +210,6 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -249,8 +225,6 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -258,8 +232,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -267,8 +239,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -276,8 +246,6 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -285,8 +253,6 @@ }, "node_modules/@babel/helpers": { "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "license": "MIT", "dependencies": { "@babel/template": "^7.26.9", @@ -298,8 +264,6 @@ }, "node_modules/@babel/parser": { "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", "license": "MIT", "dependencies": { "@babel/types": "^7.26.9" @@ -313,8 +277,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", - "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -328,8 +290,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", - "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -343,8 +303,6 @@ }, "node_modules/@babel/runtime": { "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", - "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dev": true, "license": "MIT", "dependencies": { @@ -356,8 +314,6 @@ }, "node_modules/@babel/template": { "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -370,8 +326,6 @@ }, "node_modules/@babel/traverse": { "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", - "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -388,8 +342,6 @@ }, "node_modules/@babel/types": { "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -401,8 +353,6 @@ }, "node_modules/@electron/get": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", "dev": true, "license": "MIT", "dependencies": { @@ -421,266 +371,8 @@ "global-agent": "^3.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", "cpu": [ "x64" ], @@ -693,138 +385,8 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -842,8 +404,6 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -855,8 +415,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", "engines": { @@ -865,8 +423,6 @@ }, "node_modules/@eslint/compat": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.2.tgz", - "integrity": "sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -883,8 +439,6 @@ }, "node_modules/@eslint/config-array": { "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -898,8 +452,6 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -908,8 +460,6 @@ }, "node_modules/@eslint/core": { "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -921,8 +471,6 @@ }, "node_modules/@eslint/eslintrc": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -945,8 +493,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -958,8 +504,6 @@ }, "node_modules/@eslint/js": { "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { @@ -971,8 +515,6 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -981,8 +523,6 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -995,8 +535,6 @@ }, "node_modules/@fastify/busboy": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "dev": true, "license": "MIT", "engines": { @@ -1005,8 +543,6 @@ }, "node_modules/@humanfs/core": { "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1015,8 +551,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1029,8 +563,6 @@ }, "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1043,8 +575,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1057,8 +587,6 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1071,8 +599,6 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -1085,8 +611,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1094,8 +618,6 @@ }, "node_modules/@jridgewell/set-array": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1103,14 +625,10 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1118,9 +636,7 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz", - "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==", + "version": "1.17.5", "dev": true, "license": "MIT", "dependencies": { @@ -1143,8 +659,6 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -1157,8 +671,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -1167,8 +679,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1181,8 +691,6 @@ }, "node_modules/@octokit/auth-token": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", - "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", "dev": true, "license": "MIT", "engines": { @@ -1191,8 +699,6 @@ }, "node_modules/@octokit/core": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", - "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1210,8 +716,6 @@ }, "node_modules/@octokit/endpoint": { "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", - "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", "dev": true, "license": "MIT", "dependencies": { @@ -1224,8 +728,6 @@ }, "node_modules/@octokit/graphql": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", - "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", "dev": true, "license": "MIT", "dependencies": { @@ -1239,8 +741,6 @@ }, "node_modules/@octokit/graphql-schema": { "version": "15.26.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql-schema/-/graphql-schema-15.26.0.tgz", - "integrity": "sha512-SoVbh+sXe9nsoweFbLT3tAk3XWYbYLs5ku05wij1zhyQ2U3lewdrhjo5Tb7lfaOGWNHSkPZT4uuPZp8neF7P7A==", "dev": true, "license": "MIT", "dependencies": { @@ -1250,15 +750,11 @@ }, "node_modules/@octokit/openapi-types": { "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", "dev": true, "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", - "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1273,15 +769,11 @@ }, "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", - "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", "dev": true, "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "dev": true, "license": "MIT", "dependencies": { @@ -1290,8 +782,6 @@ }, "node_modules/@octokit/plugin-rest-endpoint-methods": { "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", - "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", "dev": true, "license": "MIT", "dependencies": { @@ -1306,15 +796,11 @@ }, "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", - "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", "dev": true, "license": "MIT" }, "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "dev": true, "license": "MIT", "dependencies": { @@ -1323,8 +809,6 @@ }, "node_modules/@octokit/request": { "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", - "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", "dev": true, "license": "MIT", "dependencies": { @@ -1339,8 +823,6 @@ }, "node_modules/@octokit/request-error": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", - "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", "dev": true, "license": "MIT", "dependencies": { @@ -1354,8 +836,6 @@ }, "node_modules/@octokit/types": { "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", "dev": true, "license": "MIT", "dependencies": { @@ -1398,222 +878,16 @@ "resolved": "packages/playwright-ct-vue", "link": true }, - "node_modules/@playwright/test": { - "resolved": "packages/playwright-test", + "node_modules/@playwright/mcp-extension": { + "resolved": "packages/mcp-extension", "link": true }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@playwright/test": { + "resolved": "packages/playwright-test", + "link": true }, - "node_modules/@rollup/rollup-linux-x64-musl": { + "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", "cpu": [ "x64" ], @@ -1623,56 +897,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { + "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ] }, "node_modules/@rtsao/scc": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true, "license": "MIT" }, "node_modules/@sindresorhus/is": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, "license": "MIT", "engines": { @@ -1684,8 +926,6 @@ }, "node_modules/@stylistic/eslint-plugin": { "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.2.3.tgz", - "integrity": "sha512-oY7GVkJGVMI5benlBDCaRrSC1qPasafyv5dOBLLv5MTilMGnErKhO6ziEfodDDIZbo5QxPUNW360VudJOFODMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1705,8 +945,6 @@ }, "node_modules/@sveltejs/acorn-typescript": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", - "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -1714,8 +952,6 @@ }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "dev": true, "license": "MIT", "dependencies": { @@ -1727,8 +963,6 @@ }, "node_modules/@types/babel__core": { "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -1740,8 +974,6 @@ }, "node_modules/@types/babel__generator": { "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -1749,8 +981,6 @@ }, "node_modules/@types/babel__template": { "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -1759,8 +989,6 @@ }, "node_modules/@types/babel__traverse": { "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" @@ -1768,8 +996,6 @@ }, "node_modules/@types/cacheable-request": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dev": true, "license": "MIT", "dependencies": { @@ -1779,10 +1005,17 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/chrome": { + "version": "0.0.315", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, "node_modules/@types/codemirror": { "version": "5.60.15", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", - "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==", "dev": true, "license": "MIT", "dependencies": { @@ -1791,32 +1024,41 @@ }, "node_modules/@types/estree": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "dev": true, "license": "MIT" }, "node_modules/@types/formidable": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.6.tgz", - "integrity": "sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "dev": true, "license": "MIT" }, "node_modules/@types/immutable": { "version": "3.8.7", - "resolved": "https://registry.npmjs.org/@types/immutable/-/immutable-3.8.7.tgz", - "integrity": "sha512-nsHFDX48Tl3RaP4BF47HHe5njx40Pcp+0a8CIqzJata80Fp7JzkcuGB7UhZBGjH9aA1fMEahIqvPQQNmro5YLg==", - "deprecated": "This is a stub types definition for Facebook's Immutable (https://github.com/facebook/immutable-js). Facebook's Immutable provides its own type definitions, so you don't need @types/immutable installed!", "dev": true, "license": "MIT", "dependencies": { @@ -1825,22 +1067,16 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true, "license": "MIT" }, "node_modules/@types/keyv": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dev": true, "license": "MIT", "dependencies": { @@ -1849,8 +1085,6 @@ }, "node_modules/@types/node": { "version": "18.19.76", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz", - "integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1859,15 +1093,11 @@ }, "node_modules/@types/prop-types": { "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", - "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1877,8 +1107,6 @@ }, "node_modules/@types/react-dom": { "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", - "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1887,8 +1115,6 @@ }, "node_modules/@types/responselike": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dev": true, "license": "MIT", "dependencies": { @@ -1897,8 +1123,6 @@ }, "node_modules/@types/tern": { "version": "0.23.9", - "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", - "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", "dev": true, "license": "MIT", "dependencies": { @@ -1907,8 +1131,6 @@ }, "node_modules/@types/ws": { "version": "8.5.14", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", - "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", "dev": true, "license": "MIT", "dependencies": { @@ -1917,8 +1139,6 @@ }, "node_modules/@types/xml2js": { "version": "0.4.14", - "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", - "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1927,8 +1147,6 @@ }, "node_modules/@types/yauzl": { "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, "license": "MIT", "optional": true, @@ -1938,8 +1156,6 @@ }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", - "integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1968,8 +1184,6 @@ }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -1978,8 +1192,6 @@ }, "node_modules/@typescript-eslint/parser": { "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.41.0.tgz", - "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", "dependencies": { @@ -2003,8 +1215,6 @@ }, "node_modules/@typescript-eslint/project-service": { "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", - "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2025,8 +1235,6 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", - "integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2043,8 +1251,6 @@ }, "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", - "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", "dev": true, "license": "MIT", "engines": { @@ -2060,8 +1266,6 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", - "integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2085,8 +1289,6 @@ }, "node_modules/@typescript-eslint/types": { "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", - "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", "dev": true, "license": "MIT", "engines": { @@ -2099,8 +1301,6 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", - "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2128,8 +1328,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2138,8 +1336,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -2154,8 +1350,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -2167,8 +1361,6 @@ }, "node_modules/@typescript-eslint/utils": { "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.41.0.tgz", - "integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", "dev": true, "license": "MIT", "dependencies": { @@ -2191,8 +1383,6 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", - "integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", "dev": true, "license": "MIT", "dependencies": { @@ -2209,8 +1399,6 @@ }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", - "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", "dev": true, "license": "MIT", "engines": { @@ -2222,8 +1410,6 @@ }, "node_modules/@vitejs/plugin-react": { "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", - "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", "license": "MIT", "dependencies": { "@babel/core": "^7.26.0", @@ -2241,8 +1427,6 @@ }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", - "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", "license": "MIT", "engines": { "node": "^18.0.0 || >=20.0.0" @@ -2254,8 +1438,6 @@ }, "node_modules/@vue/compiler-core": { "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", - "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", "license": "MIT", "peer": true, "dependencies": { @@ -2268,15 +1450,11 @@ }, "node_modules/@vue/compiler-core/node_modules/estree-walker": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT", "peer": true }, "node_modules/@vue/compiler-dom": { "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", - "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", "license": "MIT", "peer": true, "dependencies": { @@ -2286,8 +1464,6 @@ }, "node_modules/@vue/compiler-sfc": { "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", - "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", "license": "MIT", "peer": true, "dependencies": { @@ -2304,15 +1480,11 @@ }, "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT", "peer": true }, "node_modules/@vue/compiler-ssr": { "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", - "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", "license": "MIT", "peer": true, "dependencies": { @@ -2322,8 +1494,6 @@ }, "node_modules/@vue/reactivity": { "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", - "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", "license": "MIT", "peer": true, "dependencies": { @@ -2332,8 +1502,6 @@ }, "node_modules/@vue/runtime-core": { "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", - "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", "license": "MIT", "peer": true, "dependencies": { @@ -2343,8 +1511,6 @@ }, "node_modules/@vue/runtime-dom": { "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", - "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", "license": "MIT", "peer": true, "dependencies": { @@ -2356,8 +1522,6 @@ }, "node_modules/@vue/server-renderer": { "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", - "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", "license": "MIT", "peer": true, "dependencies": { @@ -2370,15 +1534,11 @@ }, "node_modules/@vue/shared": { "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", - "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "license": "MIT", "peer": true }, "node_modules/@zip.js/zip.js": { "version": "2.7.57", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.57.tgz", - "integrity": "sha512-BtonQ1/jDnGiMed6OkV6rZYW78gLmLswkHOzyMrMb+CAR7CZO8phOHO6c2qw6qb1g1betN7kwEHhhZk30dv+NA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2389,15 +1549,11 @@ }, "node_modules/abbrev": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "license": "ISC" }, "node_modules/accepts": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, "license": "MIT", "dependencies": { @@ -2410,8 +1566,6 @@ }, "node_modules/acorn": { "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2422,8 +1576,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2432,8 +1584,6 @@ }, "node_modules/ajv": { "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -2449,8 +1599,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -2459,8 +1607,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -2475,8 +1621,6 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "license": "ISC", "dependencies": { @@ -2489,8 +1633,6 @@ }, "node_modules/anymatch/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { @@ -2502,15 +1644,11 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -2518,8 +1656,6 @@ }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { @@ -2535,8 +1671,6 @@ }, "node_modules/array-find-index": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", "dev": true, "license": "MIT", "engines": { @@ -2545,8 +1679,6 @@ }, "node_modules/array-includes": { "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2568,8 +1700,6 @@ }, "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, "license": "MIT", "dependencies": { @@ -2589,8 +1719,6 @@ }, "node_modules/array.prototype.findlastindex": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2611,8 +1739,6 @@ }, "node_modules/array.prototype.flat": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", "dependencies": { @@ -2630,8 +1756,6 @@ }, "node_modules/array.prototype.flatmap": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -2649,8 +1773,6 @@ }, "node_modules/array.prototype.tosorted": { "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, "license": "MIT", "dependencies": { @@ -2666,8 +1788,6 @@ }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2688,15 +1808,11 @@ }, "node_modules/asap": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true, "license": "MIT" }, "node_modules/async-function": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", "engines": { @@ -2705,8 +1821,6 @@ }, "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, "license": "MIT", "dependencies": { @@ -2721,8 +1835,6 @@ }, "node_modules/axobject-query": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -2730,22 +1842,16 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/before-after-hook": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true, "license": "Apache-2.0" }, "node_modules/binary-extensions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "engines": { @@ -2757,8 +1863,6 @@ }, "node_modules/body-parser": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "dev": true, "license": "MIT", "dependencies": { @@ -2778,17 +1882,12 @@ }, "node_modules/boolean": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, "license": "MIT", "optional": true }, "node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -2798,8 +1897,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { @@ -2811,8 +1908,6 @@ }, "node_modules/browserslist": { "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "funding": [ { "type": "opencollective", @@ -2843,8 +1938,6 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "license": "MIT", "engines": { @@ -2853,8 +1946,6 @@ }, "node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", "engines": { @@ -2863,8 +1954,6 @@ }, "node_modules/cacheable-lookup": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "dev": true, "license": "MIT", "engines": { @@ -2873,8 +1962,6 @@ }, "node_modules/cacheable-request": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dev": true, "license": "MIT", "dependencies": { @@ -2892,8 +1979,6 @@ }, "node_modules/call-bind": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { @@ -2911,8 +1996,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2925,8 +2008,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { @@ -2942,8 +2023,6 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -2952,8 +2031,6 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001700", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", - "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", "funding": [ { "type": "opencollective", @@ -2972,8 +2049,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -2989,8 +2064,6 @@ }, "node_modules/chalk/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -3002,8 +2075,6 @@ }, "node_modules/chokidar": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -3027,8 +2098,6 @@ }, "node_modules/chromium-bidi": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.2.0.tgz", - "integrity": "sha512-gREyhyBstermK+0RbcJLbFhcQctg92AGgDe/h/taMJEOLRdtSswBAO9KmvltFSQWgM2LrwWu5SIuEUbdm3JsyQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3041,8 +2110,6 @@ }, "node_modules/cliui": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3053,8 +2120,6 @@ }, "node_modules/clone-response": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dev": true, "license": "MIT", "dependencies": { @@ -3066,8 +2131,6 @@ }, "node_modules/clsx": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -3075,14 +2138,10 @@ }, "node_modules/codemirror": { "version": "5.65.18", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz", - "integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==", "license": "MIT" }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3094,15 +2153,11 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/colors": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true, "license": "MIT", "engines": { @@ -3111,15 +2166,11 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/concurrently": { "version": "6.5.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz", - "integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==", "dev": true, "license": "MIT", "dependencies": { @@ -3141,8 +2192,6 @@ }, "node_modules/content-disposition": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "dev": true, "license": "MIT", "dependencies": { @@ -3154,8 +2203,6 @@ }, "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", "engines": { @@ -3164,14 +2211,10 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, "node_modules/cookie": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -3180,8 +2223,6 @@ }, "node_modules/cookie-signature": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, "license": "MIT", "engines": { @@ -3190,8 +2231,6 @@ }, "node_modules/cors": { "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, "license": "MIT", "dependencies": { @@ -3204,8 +2243,6 @@ }, "node_modules/cross-env": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, "license": "MIT", "dependencies": { @@ -3223,8 +2260,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -3238,14 +2273,10 @@ }, "node_modules/csstype": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, "node_modules/data-view-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3262,8 +2293,6 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3280,8 +2309,6 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3298,8 +2325,6 @@ }, "node_modules/date-fns": { "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "dev": true, "license": "MIT", "dependencies": { @@ -3315,8 +2340,6 @@ }, "node_modules/debug": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3332,9 +2355,6 @@ }, "node_modules/debuglog": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, "license": "MIT", "engines": { @@ -3343,8 +2363,6 @@ }, "node_modules/decompress-response": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3359,8 +2377,6 @@ }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", "engines": { @@ -3372,15 +2388,11 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3388,8 +2400,6 @@ }, "node_modules/defer-to-connect": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true, "license": "MIT", "engines": { @@ -3398,8 +2408,6 @@ }, "node_modules/define-data-property": { "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, "license": "MIT", "dependencies": { @@ -3416,8 +2424,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -3434,8 +2440,6 @@ }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", "engines": { @@ -3444,31 +2448,23 @@ }, "node_modules/deprecation": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "dev": true, "license": "ISC" }, "node_modules/detect-node": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true, "license": "MIT", "optional": true }, "node_modules/devtools-protocol": { "version": "0.0.1421213", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1421213.tgz", - "integrity": "sha512-E0JrRYeXiTZXJwFlJ0j8wz8TmM+VwCv128JaSyCEgTmyE54QE/GDhT4w6kS1m+nFKmulHWWrbrO2AHexZgcxFQ==", "dev": true, "license": "BSD-3-Clause", "peer": true }, "node_modules/dezalgo": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "dev": true, "license": "ISC", "dependencies": { @@ -3478,8 +2474,6 @@ }, "node_modules/doctrine": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3491,8 +2485,6 @@ }, "node_modules/dotenv": { "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -3504,8 +2496,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { @@ -3519,15 +2509,11 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, "license": "MIT" }, "node_modules/electron": { "version": "30.5.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-30.5.1.tgz", - "integrity": "sha512-AhL7+mZ8Lg14iaNfoYTkXQ2qee8mmsQyllKdqxlpv/zrKgfxz6jNVtcRRbQtLxtF8yzcImWdfTQROpYiPumdbw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3545,14 +2531,10 @@ }, "node_modules/electron-to-chromium": { "version": "1.5.102", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz", - "integrity": "sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==", "license": "ISC" }, "node_modules/electron/node_modules/@types/node": { "version": "20.17.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz", - "integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==", "dev": true, "license": "MIT", "dependencies": { @@ -3561,22 +2543,16 @@ }, "node_modules/electron/node_modules/undici-types": { "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT" }, "node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { @@ -3585,8 +2561,6 @@ }, "node_modules/end-of-stream": { "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3595,8 +2569,6 @@ }, "node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "peer": true, "engines": { @@ -3608,8 +2580,6 @@ }, "node_modules/env-paths": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, "license": "MIT", "engines": { @@ -3618,8 +2588,6 @@ }, "node_modules/es-abstract": { "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -3687,8 +2655,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { @@ -3697,8 +2663,6 @@ }, "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, "license": "MIT", "engines": { @@ -3707,8 +2671,6 @@ }, "node_modules/es-iterator-helpers": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, "license": "MIT", "dependencies": { @@ -3735,8 +2697,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { @@ -3748,8 +2708,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { @@ -3764,8 +2722,6 @@ }, "node_modules/es-shim-unscopables": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, "license": "MIT", "dependencies": { @@ -3777,8 +2733,6 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { @@ -3795,16 +2749,12 @@ }, "node_modules/es6-error": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true, "license": "MIT", "optional": true }, "node_modules/esbuild": { "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3843,8 +2793,6 @@ }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -3852,15 +2800,11 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -3872,8 +2816,6 @@ }, "node_modules/eslint": { "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { @@ -3933,8 +2875,6 @@ }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "license": "MIT", "dependencies": { @@ -3945,8 +2885,6 @@ }, "node_modules/eslint-import-resolver-node/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3955,8 +2893,6 @@ }, "node_modules/eslint-module-utils": { "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -3973,8 +2909,6 @@ }, "node_modules/eslint-module-utils/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3983,8 +2917,6 @@ }, "node_modules/eslint-plugin-import": { "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { @@ -4017,8 +2949,6 @@ }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4027,8 +2957,6 @@ }, "node_modules/eslint-plugin-notice": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-notice/-/eslint-plugin-notice-1.0.0.tgz", - "integrity": "sha512-M3VLQMZzZpvfTZ/vy9FmClIKq5rLBbQpM0KgfLZPJPrVXpmJYeobmmb+lfJzHWdNm8PWwvw8KlafQWo2N9xx1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4042,8 +2970,6 @@ }, "node_modules/eslint-plugin-react": { "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { @@ -4075,8 +3001,6 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { @@ -4088,8 +3012,6 @@ }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "license": "MIT", "dependencies": { @@ -4106,8 +3028,6 @@ }, "node_modules/eslint-scope": { "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4123,8 +3043,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4136,8 +3054,6 @@ }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -4149,14 +3065,10 @@ }, "node_modules/esm-env": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "license": "MIT" }, "node_modules/espree": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4173,8 +3085,6 @@ }, "node_modules/esquery": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4186,8 +3096,6 @@ }, "node_modules/esrap": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", - "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -4195,8 +3103,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4208,8 +3114,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4218,8 +3122,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4228,8 +3130,6 @@ }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { @@ -4238,8 +3138,6 @@ }, "node_modules/eventsource": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "dev": true, "license": "MIT", "dependencies": { @@ -4251,8 +3149,6 @@ }, "node_modules/eventsource-parser": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", - "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", "dev": true, "license": "MIT", "engines": { @@ -4261,8 +3157,6 @@ }, "node_modules/express": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, "license": "MIT", "dependencies": { @@ -4304,8 +3198,6 @@ }, "node_modules/express-rate-limit": { "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", "dev": true, "license": "MIT", "engines": { @@ -4320,8 +3212,6 @@ }, "node_modules/extract-zip": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4341,15 +3231,11 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -4365,22 +3251,16 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fastq": { "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4389,8 +3269,6 @@ }, "node_modules/fd-slicer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "license": "MIT", "dependencies": { @@ -4399,8 +3277,6 @@ }, "node_modules/fdir": { "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -4413,8 +3289,6 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4426,8 +3300,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -4439,8 +3311,6 @@ }, "node_modules/finalhandler": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4457,15 +3327,11 @@ }, "node_modules/find-root": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", "dev": true, "license": "MIT" }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -4481,8 +3347,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -4495,15 +3359,11 @@ }, "node_modules/flatted": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, "node_modules/for-each": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { @@ -4518,8 +3378,6 @@ }, "node_modules/formidable": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", - "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", "dev": true, "license": "MIT", "dependencies": { @@ -4534,8 +3392,6 @@ }, "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, "license": "MIT", "engines": { @@ -4544,8 +3400,6 @@ }, "node_modules/fresh": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", "engines": { @@ -4554,8 +3408,6 @@ }, "node_modules/fs-extra": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", "dependencies": { @@ -4569,15 +3421,13 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4590,8 +3440,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { @@ -4600,8 +3448,6 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4621,8 +3467,6 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, "license": "MIT", "funding": { @@ -4631,8 +3475,6 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -4640,8 +3482,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", "engines": { @@ -4650,8 +3490,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4675,8 +3513,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { @@ -4689,8 +3525,6 @@ }, "node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "license": "MIT", "dependencies": { @@ -4705,8 +3539,6 @@ }, "node_modules/get-symbol-description": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { @@ -4723,9 +3555,6 @@ }, "node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -4745,8 +3574,6 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -4758,8 +3585,6 @@ }, "node_modules/global-agent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -4777,8 +3602,6 @@ }, "node_modules/global-agent/node_modules/semver": { "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "optional": true, @@ -4791,8 +3614,6 @@ }, "node_modules/globals": { "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "license": "MIT", "engines": { "node": ">=4" @@ -4800,8 +3621,6 @@ }, "node_modules/globalthis": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4817,8 +3636,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { @@ -4830,8 +3647,6 @@ }, "node_modules/got": { "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "dev": true, "license": "MIT", "dependencies": { @@ -4856,22 +3671,16 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, "node_modules/graphql": { "version": "16.10.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz", - "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==", "dev": true, "license": "MIT", "engines": { @@ -4880,8 +3689,6 @@ }, "node_modules/graphql-tag": { "version": "2.12.6", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", - "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", "dev": true, "license": "MIT", "dependencies": { @@ -4896,15 +3703,11 @@ }, "node_modules/graphql-tag/node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD" }, "node_modules/has-bigints": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", "engines": { @@ -4916,8 +3719,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -4926,8 +3727,6 @@ }, "node_modules/has-property-descriptors": { "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, "license": "MIT", "dependencies": { @@ -4939,8 +3738,6 @@ }, "node_modules/has-proto": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4955,8 +3752,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -4968,8 +3763,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { @@ -4984,8 +3777,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4997,8 +3788,6 @@ }, "node_modules/hexoid": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", "dev": true, "license": "MIT", "engines": { @@ -5007,8 +3796,6 @@ }, "node_modules/hosted-git-info": { "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true, "license": "ISC" }, @@ -5018,15 +3805,11 @@ }, "node_modules/http-cache-semantics": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/http-errors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5042,8 +3825,6 @@ }, "node_modules/http-errors/node_modules/statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, "license": "MIT", "engines": { @@ -5052,8 +3833,6 @@ }, "node_modules/http2-wrapper": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "dev": true, "license": "MIT", "dependencies": { @@ -5066,8 +3845,6 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -5079,8 +3856,6 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -5089,15 +3864,11 @@ }, "node_modules/immutable": { "version": "4.3.7", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", - "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", "dev": true, "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5113,8 +3884,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -5123,9 +3892,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", "dependencies": { @@ -5135,15 +3901,11 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "license": "ISC" }, "node_modules/internal-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { @@ -5157,8 +3919,6 @@ }, "node_modules/ipaddr.js": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { @@ -5167,8 +3927,6 @@ }, "node_modules/is-array-buffer": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { @@ -5185,8 +3943,6 @@ }, "node_modules/is-async-function": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5205,8 +3961,6 @@ }, "node_modules/is-bigint": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5221,8 +3975,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", "dependencies": { @@ -5234,8 +3986,6 @@ }, "node_modules/is-boolean-object": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { @@ -5251,8 +4001,6 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", "engines": { @@ -5264,8 +4012,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -5280,8 +4026,6 @@ }, "node_modules/is-data-view": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", "dependencies": { @@ -5298,8 +4042,6 @@ }, "node_modules/is-date-object": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { @@ -5315,8 +4057,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -5325,8 +4065,6 @@ }, "node_modules/is-finalizationregistry": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", "dependencies": { @@ -5341,8 +4079,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -5351,8 +4087,6 @@ }, "node_modules/is-generator-function": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5370,8 +4104,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -5383,8 +4115,6 @@ }, "node_modules/is-map": { "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, "license": "MIT", "engines": { @@ -5396,8 +4126,6 @@ }, "node_modules/is-negative-zero": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", "engines": { @@ -5409,8 +4137,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -5419,8 +4145,6 @@ }, "node_modules/is-number-object": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { @@ -5436,15 +4160,11 @@ }, "node_modules/is-promise": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true, "license": "MIT" }, "node_modules/is-reference": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -5452,8 +4172,6 @@ }, "node_modules/is-regex": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { @@ -5471,8 +4189,6 @@ }, "node_modules/is-set": { "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, "license": "MIT", "engines": { @@ -5484,8 +4200,6 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { @@ -5500,8 +4214,6 @@ }, "node_modules/is-string": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { @@ -5517,8 +4229,6 @@ }, "node_modules/is-symbol": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5535,8 +4245,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5551,8 +4259,6 @@ }, "node_modules/is-weakmap": { "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, "license": "MIT", "engines": { @@ -5564,8 +4270,6 @@ }, "node_modules/is-weakref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { @@ -5580,8 +4284,6 @@ }, "node_modules/is-weakset": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5597,22 +4299,16 @@ }, "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, "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", "dependencies": { @@ -5629,14 +4325,10 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { @@ -5648,8 +4340,6 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -5660,44 +4350,32 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true, "license": "ISC", "optional": true }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -5708,8 +4386,6 @@ }, "node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", "optionalDependencies": { @@ -5718,8 +4394,6 @@ }, "node_modules/jsx-ast-utils": { "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5734,8 +4408,6 @@ }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -5744,8 +4416,6 @@ }, "node_modules/kleur": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "license": "MIT", "engines": { "node": ">=6" @@ -5753,8 +4423,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5767,8 +4435,6 @@ }, "node_modules/license-checker": { "version": "25.0.1", - "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", - "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5789,8 +4455,6 @@ }, "node_modules/license-checker/node_modules/ansi-styles": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", "dependencies": { @@ -5802,8 +4466,6 @@ }, "node_modules/license-checker/node_modules/chalk": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5817,8 +4479,6 @@ }, "node_modules/license-checker/node_modules/color-convert": { "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", "dependencies": { @@ -5827,15 +4487,11 @@ }, "node_modules/license-checker/node_modules/color-name": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, "license": "MIT" }, "node_modules/license-checker/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5844,8 +4500,6 @@ }, "node_modules/license-checker/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { @@ -5854,8 +4508,6 @@ }, "node_modules/license-checker/node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", "engines": { @@ -5864,8 +4516,6 @@ }, "node_modules/license-checker/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -5874,8 +4524,6 @@ }, "node_modules/license-checker/node_modules/supports-color": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", "dependencies": { @@ -5887,14 +4535,10 @@ }, "node_modules/locate-character": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -5909,22 +4553,16 @@ }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5936,8 +4574,6 @@ }, "node_modules/lowercase-keys": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "dev": true, "license": "MIT", "engines": { @@ -5946,8 +4582,6 @@ }, "node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -5955,8 +4589,6 @@ }, "node_modules/magic-string": { "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" @@ -5964,8 +4596,6 @@ }, "node_modules/matcher": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "dev": true, "license": "MIT", "optional": true, @@ -5978,8 +4608,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { @@ -5988,8 +4616,6 @@ }, "node_modules/media-typer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "dev": true, "license": "MIT", "engines": { @@ -5998,8 +4624,6 @@ }, "node_modules/merge-descriptors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, "license": "MIT", "engines": { @@ -6011,8 +4635,6 @@ }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -6021,15 +4643,11 @@ }, "node_modules/metric-lcs": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/metric-lcs/-/metric-lcs-0.1.2.tgz", - "integrity": "sha512-+TZ5dUDPKPJaU/rscTzxyN8ZkX7eAVLAiQU/e+YINleXPv03SCmJShaMT1If1liTH8OcmWXZs0CmzCBRBLcMpA==", "dev": true, "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -6042,8 +4660,6 @@ }, "node_modules/micromatch/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { @@ -6055,8 +4671,6 @@ }, "node_modules/mime": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true, "license": "MIT", "bin": { @@ -6068,8 +4682,6 @@ }, "node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { @@ -6078,8 +4690,6 @@ }, "node_modules/mime-types": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dev": true, "license": "MIT", "dependencies": { @@ -6091,8 +4701,6 @@ }, "node_modules/mimic-response": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, "license": "MIT", "engines": { @@ -6101,8 +4709,6 @@ }, "node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -6114,8 +4720,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", "funding": { @@ -6124,15 +4728,11 @@ }, "node_modules/mitt": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "dev": true, "license": "MIT" }, "node_modules/mkdirp": { "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", "dependencies": { @@ -6144,14 +4744,10 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -6168,15 +4764,11 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", "engines": { @@ -6185,14 +4777,10 @@ }, "node_modules/node-releases": { "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "license": "MIT" }, "node_modules/node-stream-zip": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", - "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", "dev": true, "license": "MIT", "engines": { @@ -6205,8 +4793,6 @@ }, "node_modules/nopt": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", "dev": true, "license": "ISC", "dependencies": { @@ -6219,8 +4805,6 @@ }, "node_modules/normalize-package-data": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6232,8 +4816,6 @@ }, "node_modules/normalize-package-data/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -6242,8 +4824,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", "engines": { @@ -6252,8 +4832,6 @@ }, "node_modules/normalize-url": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true, "license": "MIT", "engines": { @@ -6265,15 +4843,11 @@ }, "node_modules/npm-normalize-package-bin": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", "dev": true, "license": "ISC" }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "license": "MIT", "engines": { @@ -6282,8 +4856,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", "engines": { @@ -6295,8 +4867,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", "engines": { @@ -6305,8 +4875,6 @@ }, "node_modules/object.assign": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { @@ -6326,8 +4894,6 @@ }, "node_modules/object.entries": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, "license": "MIT", "dependencies": { @@ -6342,8 +4908,6 @@ }, "node_modules/object.fromentries": { "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6361,8 +4925,6 @@ }, "node_modules/object.groupby": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6376,8 +4938,6 @@ }, "node_modules/object.values": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -6395,8 +4955,6 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", "dependencies": { @@ -6408,8 +4966,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { @@ -6418,8 +4974,6 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -6436,8 +4990,6 @@ }, "node_modules/os-homedir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", "dev": true, "license": "MIT", "engines": { @@ -6446,8 +4998,6 @@ }, "node_modules/os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "dev": true, "license": "MIT", "engines": { @@ -6456,9 +5006,6 @@ }, "node_modules/osenv": { "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "deprecated": "This package is no longer supported.", "dev": true, "license": "ISC", "dependencies": { @@ -6468,8 +5015,6 @@ }, "node_modules/own-keys": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, "license": "MIT", "dependencies": { @@ -6486,8 +5031,6 @@ }, "node_modules/p-cancelable": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true, "license": "MIT", "engines": { @@ -6496,8 +5039,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6512,8 +5053,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -6526,10 +5065,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -6541,8 +5089,6 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", "engines": { @@ -6551,8 +5097,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -6561,8 +5105,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", "engines": { @@ -6571,8 +5113,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -6581,15 +5121,11 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, "node_modules/path-to-regexp": { "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "dev": true, "license": "MIT", "engines": { @@ -6598,21 +5134,15 @@ }, "node_modules/pend": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -6623,8 +5153,6 @@ }, "node_modules/pkce-challenge": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", "dev": true, "license": "MIT", "engines": { @@ -6653,8 +5181,6 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { @@ -6663,8 +5189,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -6691,8 +5215,6 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -6701,8 +5223,6 @@ }, "node_modules/progress": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, "license": "MIT", "engines": { @@ -6711,8 +5231,6 @@ }, "node_modules/prop-types": { "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, "license": "MIT", "dependencies": { @@ -6723,8 +5241,6 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "license": "MIT", "dependencies": { @@ -6737,8 +5253,6 @@ }, "node_modules/pump": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, "license": "MIT", "dependencies": { @@ -6748,8 +5262,6 @@ }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -6758,8 +5270,6 @@ }, "node_modules/qs": { "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6774,8 +5284,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -6795,8 +5303,6 @@ }, "node_modules/quick-lru": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, "license": "MIT", "engines": { @@ -6808,8 +5314,6 @@ }, "node_modules/range-parser": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, "license": "MIT", "engines": { @@ -6818,8 +5322,6 @@ }, "node_modules/raw-body": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "dev": true, "license": "MIT", "dependencies": { @@ -6834,8 +5336,6 @@ }, "node_modules/react": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6847,8 +5347,6 @@ }, "node_modules/react-dom": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "license": "MIT", "dependencies": { @@ -6861,15 +5359,11 @@ }, "node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true, "license": "MIT" }, "node_modules/react-refresh": { "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6877,9 +5371,6 @@ }, "node_modules/read-installed": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", - "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", - "deprecated": "This package is no longer supported.", "dev": true, "license": "ISC", "dependencies": { @@ -6896,8 +5387,6 @@ }, "node_modules/read-installed/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -6906,9 +5395,6 @@ }, "node_modules/read-package-json": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", - "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", - "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", "dev": true, "license": "ISC", "dependencies": { @@ -6920,9 +5406,6 @@ }, "node_modules/readdir-scoped-modules": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", - "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", - "deprecated": "This functionality has been moved to @npmcli/fs", "dev": true, "license": "ISC", "dependencies": { @@ -6934,8 +5417,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", "dependencies": { @@ -6947,8 +5428,6 @@ }, "node_modules/readdirp/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { @@ -6964,8 +5443,6 @@ }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { @@ -6987,15 +5464,11 @@ }, "node_modules/regenerator-runtime": { "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true, "license": "MIT" }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { @@ -7015,8 +5488,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { @@ -7025,8 +5496,6 @@ }, "node_modules/resolve": { "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "license": "MIT", "dependencies": { @@ -7046,15 +5515,11 @@ }, "node_modules/resolve-alpn": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true, "license": "MIT" }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -7063,8 +5528,6 @@ }, "node_modules/responselike": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "dev": true, "license": "MIT", "dependencies": { @@ -7076,8 +5539,6 @@ }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -7087,8 +5548,6 @@ }, "node_modules/roarr": { "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -7106,8 +5565,6 @@ }, "node_modules/rollup": { "version": "4.40.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", "license": "MIT", "dependencies": { "@types/estree": "1.0.7" @@ -7145,8 +5602,6 @@ }, "node_modules/router": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7162,8 +5617,6 @@ }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -7186,8 +5639,6 @@ }, "node_modules/rxjs": { "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7199,8 +5650,6 @@ }, "node_modules/safe-array-concat": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7219,8 +5668,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { @@ -7240,8 +5687,6 @@ }, "node_modules/safe-push-apply": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, "license": "MIT", "dependencies": { @@ -7257,8 +5702,6 @@ }, "node_modules/safe-regex-test": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "license": "MIT", "dependencies": { @@ -7275,22 +5718,16 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "license": "MIT" }, "node_modules/sax": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true, "license": "ISC" }, "node_modules/scheduler": { "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7299,8 +5736,6 @@ }, "node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7308,16 +5743,12 @@ }, "node_modules/semver-compare": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true, "license": "MIT", "optional": true }, "node_modules/send": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "dev": true, "license": "MIT", "dependencies": { @@ -7339,8 +5770,6 @@ }, "node_modules/serialize-error": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "dev": true, "license": "MIT", "optional": true, @@ -7356,8 +5785,6 @@ }, "node_modules/serve-static": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7372,8 +5799,6 @@ }, "node_modules/set-function-length": { "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, "license": "MIT", "dependencies": { @@ -7390,8 +5815,6 @@ }, "node_modules/set-function-name": { "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, "license": "MIT", "dependencies": { @@ -7406,8 +5829,6 @@ }, "node_modules/set-proto": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, "license": "MIT", "dependencies": { @@ -7421,15 +5842,11 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true, "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -7441,8 +5858,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -7451,8 +5866,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { @@ -7471,8 +5884,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, "license": "MIT", "dependencies": { @@ -7488,8 +5899,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { @@ -7507,8 +5916,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { @@ -7527,8 +5934,6 @@ }, "node_modules/slide": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", "dev": true, "license": "ISC", "engines": { @@ -7537,8 +5942,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7546,14 +5949,10 @@ }, "node_modules/spawn-command": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", - "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, "node_modules/spdx-compare": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", - "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", "dev": true, "license": "MIT", "dependencies": { @@ -7564,8 +5963,6 @@ }, "node_modules/spdx-correct": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7575,15 +5972,11 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7593,22 +5986,16 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", "dev": true, "license": "CC0-1.0" }, "node_modules/spdx-ranges": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", - "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", "dev": true, "license": "(MIT AND CC-BY-3.0)" }, "node_modules/spdx-satisfies": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", - "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", "dev": true, "license": "MIT", "dependencies": { @@ -7619,23 +6006,17 @@ }, "node_modules/sprintf-js": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true, "license": "BSD-3-Clause", "optional": true }, "node_modules/ssim.js": { "version": "3.5.0", - "resolved": "https://registry.npmjs.org/ssim.js/-/ssim.js-3.5.0.tgz", - "integrity": "sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==", "dev": true, "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -7644,8 +6025,6 @@ }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7658,8 +6037,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -7673,8 +6050,6 @@ }, "node_modules/string.prototype.matchall": { "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "license": "MIT", "dependencies": { @@ -7701,8 +6076,6 @@ }, "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, "license": "MIT", "dependencies": { @@ -7712,8 +6085,6 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", "dependencies": { @@ -7734,8 +6105,6 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7753,8 +6122,6 @@ }, "node_modules/string.prototype.trimstart": { "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, "license": "MIT", "dependencies": { @@ -7771,8 +6138,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -7784,8 +6149,6 @@ }, "node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { @@ -7794,8 +6157,6 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -7807,8 +6168,6 @@ }, "node_modules/sumchecker": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7820,8 +6179,6 @@ }, "node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7836,8 +6193,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", "engines": { @@ -7849,8 +6204,6 @@ }, "node_modules/svelte": { "version": "5.37.3", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.37.3.tgz", - "integrity": "sha512-7t/ejshehHd+95z3Z7ebS7wsqHDQxi/8nBTuTRwpMgNegfRBfuitCSKTUDKIBOExqfT2+DhQ2VLG8Xn+cBXoaQ==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -7874,8 +6227,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "license": "MIT", "dependencies": { "fdir": "^6.4.4", @@ -7890,8 +6241,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7903,8 +6252,6 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, "license": "MIT", "engines": { @@ -7917,8 +6264,6 @@ }, "node_modules/tree-kill": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", "bin": { @@ -7927,8 +6272,6 @@ }, "node_modules/treeify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", - "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", "dev": true, "license": "MIT", "engines": { @@ -7937,8 +6280,6 @@ }, "node_modules/ts-api-utils": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -7950,8 +6291,6 @@ }, "node_modules/tsconfig-paths": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "license": "MIT", "dependencies": { @@ -7963,8 +6302,6 @@ }, "node_modules/tsconfig-paths/node_modules/json5": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "license": "MIT", "dependencies": { @@ -7976,15 +6313,11 @@ }, "node_modules/tslib": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, "license": "0BSD" }, "node_modules/tunnel": { "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true, "license": "MIT", "engines": { @@ -7993,8 +6326,6 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -8006,8 +6337,6 @@ }, "node_modules/type-fest": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true, "license": "(MIT OR CC0-1.0)", "optional": true, @@ -8020,8 +6349,6 @@ }, "node_modules/type-is": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dev": true, "license": "MIT", "dependencies": { @@ -8035,8 +6362,6 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { @@ -8050,8 +6375,6 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", "dependencies": { @@ -8070,8 +6393,6 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8092,8 +6413,6 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "license": "MIT", "dependencies": { @@ -8113,8 +6432,6 @@ }, "node_modules/typescript": { "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -8127,8 +6444,6 @@ }, "node_modules/unbox-primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { @@ -8146,8 +6461,6 @@ }, "node_modules/undici": { "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", "dev": true, "license": "MIT", "dependencies": { @@ -8159,22 +6472,16 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "devOptional": true, "license": "MIT" }, "node_modules/universal-user-agent": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", "dev": true, "license": "ISC" }, "node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", "engines": { @@ -8183,8 +6490,6 @@ }, "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, "license": "MIT", "engines": { @@ -8193,8 +6498,6 @@ }, "node_modules/update-browserslist-db": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "funding": [ { "type": "opencollective", @@ -8223,8 +6526,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8233,15 +6534,11 @@ }, "node_modules/util-extend": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", - "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", "dev": true, "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -8251,8 +6548,6 @@ }, "node_modules/vary": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "license": "MIT", "engines": { @@ -8261,8 +6556,6 @@ }, "node_modules/vite": { "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -8333,10 +6626,72 @@ } } }, + "node_modules/vite-plugin-static-copy": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "fs-extra": "^11.3.0", + "p-map": "^7.0.3", + "picocolors": "^1.1.1", + "tinyglobby": "^0.2.14" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/fs-extra": { + "version": "11.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vitefu": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", - "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "license": "MIT", "workspaces": [ "tests/deps/*", @@ -8354,8 +6709,6 @@ }, "node_modules/vue": { "version": "3.5.13", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", - "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", "license": "MIT", "peer": true, "dependencies": { @@ -8380,8 +6733,6 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -8396,8 +6747,6 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", "dependencies": { @@ -8416,8 +6765,6 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8444,8 +6791,6 @@ }, "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "license": "MIT", "dependencies": { @@ -8463,8 +6808,6 @@ }, "node_modules/which-typed-array": { "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "license": "MIT", "dependencies": { @@ -8485,8 +6828,6 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -8495,8 +6836,6 @@ }, "node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8513,15 +6852,11 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/ws": { "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "dev": true, "license": "MIT", "engines": { @@ -8542,8 +6877,6 @@ }, "node_modules/xml2js": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "license": "MIT", "dependencies": { @@ -8556,8 +6889,6 @@ }, "node_modules/xmlbuilder": { "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true, "license": "MIT", "engines": { @@ -8566,16 +6897,10 @@ }, "node_modules/xterm": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", - "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", - "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", "license": "MIT" }, "node_modules/xterm-addon-fit": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz", - "integrity": "sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==", - "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", "license": "MIT", "peerDependencies": { "xterm": "^5.0.0" @@ -8583,8 +6908,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { @@ -8593,14 +6916,10 @@ }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, "node_modules/yaml": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", - "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -8611,8 +6930,6 @@ }, "node_modules/yargs": { "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "license": "MIT", "dependencies": { @@ -8630,8 +6947,6 @@ }, "node_modules/yargs-parser": { "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "license": "ISC", "engines": { @@ -8640,8 +6955,6 @@ }, "node_modules/yauzl": { "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "license": "MIT", "dependencies": { @@ -8651,8 +6964,6 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -8664,14 +6975,10 @@ }, "node_modules/zimmerframe": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", "license": "MIT" }, "node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "version": "3.25.76", "dev": true, "license": "MIT", "funding": { @@ -8680,8 +6987,6 @@ }, "node_modules/zod-to-json-schema": { "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", "dev": true, "license": "ISC", "peerDependencies": { @@ -8691,6 +6996,150 @@ "packages/html-reporter": { "version": "0.0.0" }, + "packages/mcp-extension": { + "name": "@playwright/mcp-extension", + "version": "0.0.36", + "license": "Apache-2.0", + "devDependencies": { + "@types/chrome": "^0.0.315", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.8.2", + "vite": "^5.0.0", + "vite-plugin-static-copy": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "packages/mcp-extension/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/mcp-extension/node_modules/esbuild": { + "version": "0.21.5", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "packages/mcp-extension/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "packages/mcp-extension/node_modules/vite": { + "version": "5.4.19", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "packages/playwright": { "version": "1.56.0-next", "license": "Apache-2.0", @@ -8841,8 +7290,6 @@ }, "packages/playwright-ct-svelte/node_modules/@sveltejs/vite-plugin-svelte": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", - "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "license": "MIT", "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", @@ -8862,8 +7309,6 @@ }, "packages/playwright-ct-svelte/node_modules/@sveltejs/vite-plugin-svelte-inspector": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", "license": "MIT", "dependencies": { "debug": "^4.3.7" @@ -8906,51 +7351,6 @@ "node": ">=18" } }, - "packages/playwright-mcp": { - "name": "@playwright/mcp", - "version": "1.52.0-next", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.6.1", - "commander": "^13.1.0", - "playwright": "1.52.0-next", - "zod-to-json-schema": "^3.24.4" - }, - "bin": { - "mcp": "cli.js" - }, - "devDependencies": { - "@types/node": "^22.13.10" - }, - "engines": { - "node": ">=18" - } - }, - "packages/playwright-mdd": { - "name": "@playwright/mdd", - "version": "0.0.1", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "commander": "^13.1.0", - "debug": "^4.4.1", - "dotenv": "^16.5.0", - "mime": "^4.0.7", - "openai": "^5.7.0", - "playwright-core": "1.55.0-next", - "zod-to-json-schema": "^3.24.4" - }, - "bin": { - "playwright-mdd": "cli.js" - }, - "devDependencies": { - "@types/debug": "^4.1.7" - }, - "engines": { - "node": ">=18" - } - }, "packages/playwright-test": { "name": "@playwright/test", "version": "1.56.0-next", @@ -8965,40 +7365,6 @@ "node": ">=18" } }, - "packages/playwright-test-mcp": { - "name": "@playwright/test-runner-mcp", - "version": "0.0.1", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "commander": "^13.1.0", - "playwright": "1.56.0-next", - "playwright-core": "1.56.0-next" - }, - "bin": { - "mcp-server-playwright-test": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "packages/playwright-tools": { - "name": "@playwright/experimental-tools", - "version": "0.0.0", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.52.0-next" - }, - "devDependencies": { - "@anthropic-ai/sdk": "^0.33.1", - "@modelcontextprotocol/sdk": "^1.6.1", - "openai": "^4.79.1" - }, - "engines": { - "node": ">=18" - } - }, "packages/playwright-webkit": { "version": "1.56.0-next", "hasInstallScript": true, @@ -9013,20 +7379,6 @@ "node": ">=18" } }, - "packages/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "packages/recorder": { "version": "0.0.0", "dependencies": { diff --git a/package.json b/package.json index 6948430c30baa..80dd33e8119ac 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,9 @@ "ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright.config.ts", "ct": "playwright test tests/components/test-all.spec.js --reporter=list", "test": "playwright test --config=tests/library/playwright.config.ts", + "mcp-test": "playwright test --config=tests/mcp/playwright.config.ts", + "mcp-ctest": "playwright test --config=tests/mcp/playwright.config.ts --project=chrome", + "mcp-etest": "playwright test --config=tests/mcp/playwright.config.extension.ts", "eslint": "eslint --cache", "tsc": "tsc -p . && tsc -p packages/html-reporter/", "doc": "node utils/doclint/cli.js", @@ -56,7 +59,7 @@ "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.34.0", - "@modelcontextprotocol/sdk": "^1.17.3", + "@modelcontextprotocol/sdk": "^1.17.5", "@octokit/graphql-schema": "^15.26.0", "@stylistic/eslint-plugin": "^5.2.3", "@types/codemirror": "^5.60.7", @@ -99,6 +102,8 @@ "vite": "^6.3.4", "ws": "^8.17.1", "xml2js": "^0.5.0", - "yaml": "2.6.0" + "yaml": "2.6.0", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.24.6" } } diff --git a/packages/mcp-extension/README.md b/packages/mcp-extension/README.md new file mode 100644 index 0000000000000..6421798e03a68 --- /dev/null +++ b/packages/mcp-extension/README.md @@ -0,0 +1,48 @@ +# Playwright MCP Chrome Extension + +## Introduction + +The Playwright MCP Chrome Extension allows you to connect to pages in your existing browser and leverage the state of your default user profile. This means the AI assistant can interact with websites where you're already logged in, using your existing cookies, sessions, and browser state, providing a seamless experience without requiring separate authentication or setup. + +## Prerequisites + +- Chrome/Edge/Chromium browser + +## Installation Steps + +### Download the Extension + +Download the latest Chrome extension from GitHub: +- **Download link**: https://github.com/microsoft/playwright-mcp/releases + +### Load Chrome Extension + +1. Open Chrome and navigate to `chrome://extensions/` +2. Enable "Developer mode" (toggle in the top right corner) +3. Click "Load unpacked" and select the extension directory + +### Configure Playwright MCP server + +Configure Playwright MCP server to connect to the browser using the extension by passing the `--extension` option when running the MCP server: + +```json +{ + "mcpServers": { + "playwright-extension": { + "command": "npx", + "args": [ + "@playwright/mcp@latest", + "--extension" + ] + } + } +} +``` + +## Usage + +### Browser Tab Selection + +When the LLM interacts with the browser for the first time, it will load a page where you can select which browser tab the LLM will connect to. This allows you to control which specific page the AI assistant will interact with during the session. + + diff --git a/packages/mcp-extension/icons/icon-128.png b/packages/mcp-extension/icons/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..c4bc8b02508b40cec8a0d73465324a9612744172 GIT binary patch literal 6352 zcmb7JWmnXV)Bf$U^a7HCASFn5m#lQBba#i+y|jdYAX2h~l%#Y>xrB5}r*tFT`}h6^ z&ogso=FPmAGv~})6EPZU@_0DUZ~y?nQ&f=A`e)()2^Pjb-Sx_1@Snl3RF;VMe&-7<*EP~0QQ-e zyPEv64$;N8t5@Htw|oavKfM1-X3m~l`|-KlSi0S_&s^fcK)*57v>O(C>Pr#51Gt?1w1oSgyZ zedR~@)(h_;^n;ic3U<4En>p%V?e!pfb?7B!W!m_c2ES%)$S$j#Pis^j{P`2Tp_~;* zJ9e4N%F*$2Bcv`M{4dqIXz8}>yTYjn(FDN1yxDxgJ3u_h7@Q%Qo^qaqx`dNq53CY4js>CT{=bOAlkx~C2O^GnfL8-s^NA-@d2 z12s8#X<0Fe$-|GX9H(I01GP_nmHN1}UYpcFW&S_{f+y&@Gf5UJYZCc5Hd)(Yw3Txg zjxL#l>bhz8gZD2!Cy;JM%hhlB^VQgyr=IJ2DWle=0DTGPWA58bO#V+f2@_JmB8$^7MeThKdvSR7&RO$-;f)BFiBerbg$JfxA)!{6<- z|4wHBDEGT)u**VQnK z=e+{3&E7%)cmZZsvMwnqU^W_l@7CnkY?4tc2Qrjww-wezTV;tjta{IL`(x!G0tq{z9$P5qgosqxaD6STSl7GuM3(OSF4wsxobSb#Rx??j z@4I$ZRMM})hnVoEDh1Ikl4$*5JC;$5Sau*7Z#3ki5$|bCQWrZDPYMCVEnPL#;whse z)GdtXj}wjT$(I{RZaT}WXM9prXE~bW-ybY{KegnyT>iA{>Rkz$7C0TYh1HYaz=trc z@Bw{I9*VfHqDO0}?)P)tXvq{|a!2dzwh@I8;D80En@D2a-ZiLIR+s?yCU2>R=L2)- z@p00IstLoLJ`V{416RCrX5de#f)$NG96KDSpXH*A)|F;aQ2o-)Z583wH9fZA<_I?% zM(~H^VPSCuP!U-S5ICoO@JfOwyzW$$;q|IMptE9z?R-I-T^mIFE5uT!faPGo?y_=W z!KTq&r9WdFA54w~?o%}2r{pFlBOhS2GQoJ^S&5Q7;v;fRBe6_1&NfsZp%~9rrZ3o; zcv?MVDXC&+9_BzES$&_PZ#{V8h9#P=uq#|TC=Zf)-+zo@Cj$aHlcBQ4<-kYg?Nx=~ zS#Kj5NLog~)Wu$TKZ$NEh=hQK7?|41djl?`U8HQ^_2s5VV~iOnYp&@BpDfEL{W@wQ z#{{j`6~xOK%blXppGt9gPC0Qw%1e}l-9{G$uLeD~HS}Y4hggFu>5r9d7)6Si8k1-j z8esM~5t$MXlEh4(2FSF%3QL1&3nKlc*D^SbJa8Q>3T_@0rm%8{Nmnp61YA0QxCEyo z=heOkW}qjq5WSAG&!=U#UB&1Ayq?@t+Te34Zl*V)7_vF;z%FuRov}6Y@{HiSOc4F% zy6I~EUM6v*l$tNE(5LaWTzSo0nI1IJFggf3t`<7=qx}H|EH74J#0d9MJ$Q^J<>$etp7f`<$7MI_~2UO=rJHdt1+{eH$ZV7MWTYQ>vpZ&YCo zo$l=@8#?D92K~Dow+^vKxC|DKoGvxXIJJEiVVUtHTNVL-d+W`*ObMF1rcm)wmbkk1 z5|17pA@ksE$bss{8k#o=44UzYTgEL}swd*TH6(u@%Epu*(zt<+o@Aabe!dzY@t3qS zUjJq-^yCGa3EBs~4sB84?HF{o)K@x>kp`${q=eQ@luH%@ZOsbc#PRd@86l~5E*L{s z=r8>gD@0WBL=O~Eumre|X~_u`#=xMT8AhnAE>3);D~b0<{L7)f}( z{Da@1cD86ZODn&jX-F9V`mH%;rMB~hTPt9W9|Y;-7pdbLS9)8u8qQ&KA6M#hDQHj^ zA0>71c3V7#dFt2HMMGW$0@sl-I5!La9B@gSm>sVR8oX)u+Kv5D+5Gw`=g-rcq*^~U zP&xCku$2|W{OMqwICsS+@YyH!q|r8{qg=wCdVbwMpx0r1of88nyvgius`rS9Q(=B| zdk33~BUpNvstV(<&+khTXZaI3S`H-PWBv$6wRF!2&bNT+{j)RD&}m!5lTzflFxN*h z#2?qm6v1t1C*BPkY^q`iNnXR}OPl^DafqM0FJ#x)KLg#ek8m@}uO)TXywY25h_|Lr zYQi<%FqRuus#R3b%Eb2BD4R~1lyZ{t50u-GfJBpP<5pN(@IlqkCTu zu)>g6p>gugJs6N+i46Y+5|~uGTCU`R1M_3*{0>eh^f0S=tyUhX)MLWc`oyQ*yC;?u zJ#icVk%ebFVS<{0ue^jd$*f;R9LdX(7TI}zG6irWS=KL6gyY?mMI_Z`Z5*~D()~Y2 zkcpOVrUX?^RZNNX{1ko$rw!J7b2t{w##{J6XijAPE{TU64KO%o!+F;A^{S%zg>H3t zeHmL$Yp+KiUP5!xie-BV?unCCD^~Z*>Zp)F-Iabxgsxn0eQHJr_G+2TIXV0oG#kl` z=8nka1xa!mgmKPf#S9?hgX3Oc&!7=0;F`VBsilYr2>DENppzbVFr^83PMmATJ@mXN zHK30p>`Zm60vd?4ba1lyAO*HVemrXiLV`WS=H(PM`blKACpzR?0zfuTgAHqzYNluo zLTtKX2$gc*sSfiy{5gZ%6T;&va?HbD`ITb3W_0??EYE8E*lve#t|501$QWLaqQO#F zS`MeH8Af>v?me_)U2;3CpPZpG*-`UHU6?>Wk?s^Ov81rrWeK&{xt@F<$0;sNuwC81 zo{wiFl;pC6vgAMCnk4P0s?6r8I~F16kslEv$v$^u)7VYel(I`uo7 zm|ffpj)d2{&mWiWy(}^hQz_Hh^_q8Es)DT7FN9_uYFxWNPKG|uO2C|2Rf0E|dYZ#B zkQK{5F0sU9)-Njn>^AjO9Raa)_l1y6^nf=f@2%FWbH%7ca+pt3jpoPm65XCA2?=nG zLsvoErdEHRV$Cb#)&Jl$lov)F{Rr~c2%_vno=I-?ar*CAgW$r;FWnqUmORwykuoV7 z4B^-)yw!$7!g`-nS8ElJ@Bnrzzb3ZhcmIZu0MM-fp&JMLdE42% zi_tx9*>R8tY|3hul^`}oyyT#W%E>^*j~1bPq!guKPcK%25rR*9l9D7C&Fk>tYYULW z)9!KXfQ2H0?*os@wykpu`^UzuZ+E$Rw$<75z*b$dj7DvL?|}{nbfIE;6{*t2fNOVO zEr**xjL(ZD{nO$#LeCUZ;eJ1{%X6Md-3cdv7yT-C1mL@!Xy$aGN3SD0!YcPAWwoV0 zaLWqam&R>j6m^%}IcqPqfB5!Ae2tBeNS%8v1;xA5vK7$Y*(_^lg`2$(!Gge}km2jy z>BS+qh^XW-epYOdbaVPwB!l*H1;ZNU+{r+Z4{)$&y>XW}YYI=oVIXm(C1`iv_{zdm zjgT)v))Cg!FOZRNd>!l%|N7iq%EM}SxlvoyHsU8du(woI*yd3UP07Q ztA9T!DcWl*m{kXU*VsLxpakR&b0?xK0p16*JR%N9^=OdANz3t7oZ)A0d6`(=-=oOg zB`@6uEANcAC)E!`yh`WT5k{uC=G1_D``g}=n?tc2C{fCg?xywQjYS#xRO{Z~FsX+z znZ$jC>cXEv`Xqg((*;?DR9+u*8vAgG)@foHPJAe-eNly;b*Xi#9v3RUf?87#Gs8u^ zKb{W6(ChP@9UsubM{=TC8}9@6d&!)G9+9B@hBf!xiM0z$N6W(~oIBe+#~5sFWA8Qw zMI9rY!Ku$uO(6Hz8!8*~mDIH*9i5*((tlV6mJ}&KTM$5BSWN6s#BT8M?9OM8MkO#7 zTNI?tqjV)t*Yg!I#5O^odft+#5@p+*$abS!mB|J!)w`gL4ET(ne*Npt@#>Mm?>ae{ z<;pm~0%!|_wo7;Jh-?uRP;sPh`r8~)Jv7%41=~jTSE8iOzc{3LK~@GBmr9Q3$8FmQPBNnF)=1brfo!)xBJ zY-WGd(-2hOe2w3v`Ok|fW>tqQJra)ktbg!Az!L6DAiANx0`PZ6cx+e}wz(8D_}4rl z`ckaPx8ARz^ia)Z{U3MsqLiv};4ce}K9cZns3b~<46dOjHpi%O+F(()b!7rP)bRdH zS@biX5Gkx1MPame47<*_x3wr12REvd~$A(rq|e zmRp`E0hjH{Nlx|JH=P!r@Jssm245>U_jl-07>EJ9GH62cTYQj81bpKLw zDwKggRT`|HrXz$v>_$ZfE@pq_inz@l1)0M8}h=8Ts%JH zETDpMhBY=zGZ~YsW68^aMF=e1=yGEIl58VOsN2b0hWvvpao`MN%ho8T1A$ZOiYcGA zjs}4_k~MAamO{^3x5XC9b&3Yu54w3-g=7lGF@SWJZUZJ%Lp%?z@LN+H18^m4OBIa8 zuGP~iS`wLe4_K68i`T)2M@!!Z_?}r2&6Kx(y~MCx#;4sTrvAF=I{f{Rq!;OyzI}xa zfltT0eDu~d63u)?K$zQ9hp*KmcgccE-|<0bLzoGg=ub-nLC-oDKUsr9tjrD(RYenW z_a5?~|Fte~-)~^^NE84%lK=&9;k!q=-5-ZqurSgU*bx&AIzm)P4nq zi1&JWf89+T&VuQqk7Z;>@jeWOBWjbG3xm!i272g23QXifMFc17)*ElrXa>8xFnN;< ze>b^IQfkE-k=u(nXpW=VNkj&4REQiA)oQwzvE~y8>Vrc9J9Yc$EP5G15r4~~G9^|< zrgnLGYk&WqUleFQzqJFb!0A_n<&C+KXhrbb10xoe2L`wdOk@2c7h*uNlp41X2mT!0OeSS8zC-%fYrFVRM zrM=r?n$qU_$9T3*p_G0k5d$kCN#m{PA0B+ttHQ&7S1t@awbr)5Nu{UkrORiXNAVj5 z_eL(kk*@NdGcw+O=^bb#M608lDq~@Wts#rSuxKP-Se#bZ#@eXd! z^Djm%G^L{c3D_z1nM;Z;CrPa9Ije$0w-=V480 z(ERSuk?fo(ze;+2tTXeWh=M1Dy5l3g2Dij+;zuSVEpDlk@!j(Dwd}8`15G%?w-VDk z4)DXq*3#I_H2L@HH_M@Jxuu1;hFSuiEhSJ%N)+Ve>2?XSxCPcrZ#muoq{*o8vEQTU* zIK}cZ<$O;S%!>On($OTCRLy|e9foC2@!{ciS4MUkUsV0}Bw`|!RWBP1{ST<@UY{S6 zZ)yt}Q|(}Jh>#{Q=k1PX!N^obFszGLSa$FmfRDoSlq;y%Z==+sfci0 z=k@u{9hY0(19s!z)lyx!1({w&n=F11xuwk_vIUyY;2W=ul(5v2K|0;$*5F49fHTGaT6w)FOK_rd&O{f<{wIdjUXE6nKRwtbq=a*V$ceu4^P)5vgd!sD#D!20RJ7_s!G%Fhg;rAR15BY}BoejF>z;e>85fmMp>Zn?%x31} z$H&YE{P#tj7Rl+cCoIAN$C8=iRI%DgCw3ZMA3vEE+Ryqeb+A_V%E_Cf_tUr2gF8CN zWHQFBJ_o|mGAadCh=nnOfOz_2TK6p1kr-q>?xSq{lH8ih)!d$F;0ZL`BV8O07w9+ zloo5Lk#uWVa+bh}!xdxi!^BVc((6YL&pY2@*RW={u+KX{1Ya8ZO!;1Uvkd@%XCIf} z#D+#oNvTs2E%duS6;uu75HM^F(OK&Eju62F=-m9o`$GGTodPnJo0wF`OP3J$;!ez# zP;v!zg?>s-}L{cegPiHz@Uarj;R0u002ov JPDHLkV1gAA4qyNP literal 0 HcmV?d00001 diff --git a/packages/mcp-extension/icons/icon-32.png b/packages/mcp-extension/icons/icon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..1f9a8ccb89bdbccd6bdc1c7f945f72eed9b94017 GIT binary patch literal 1258 zcmVvS_*I>m_KT&x^fBtGyo{vygB`pAML~lkiZCl zFf-D?$W*Ys3K9gyUC;dcrg?P$@B=7epk(@p9G}{ohz9_lo3_~Duup;mPzcN{s+i$0 zqdPq|IjJTrO_?Dj?R3X>5;^)fz?|lSZ%zYPWQA^zz&8{G2a_f+!%_+z8RG!Dq22SJ zDgKZgu>{}`qMN!0#$jWj0)0f4%?I;Y2#2`WLP9MJtimggcQyB%i4 zT!lV!9Kg1m?2*cruSa&Y?ul->e!~1UFDNu}r7j?}iM?hHlS1Fj?gVb=-qM|rGI0xz zNA!#F;|ha*))l>wrSrJ}RK8+{%Zy>2CslD>!x^=n9#fHn!{x1+tXOEC3HgWg)9Ra* z$JJLWhxy0sb}&NmJcg*73X}l2q2#d`i2(rcv8rI9I+de7Q_KPw-vb4eR8wXKE0FG) zkUwbq&kduHn!MFqR8oXDy{ z&C&tD^;-e0EBAfR)!e_#>)DI-2Wes9YS-X*24HXyz|8yDXPDUA|82iL4R9vW+6}Kh ze>5079)MZ_02-);iC0G)*X;DiaPXNsiCE3h!osI-yRonJ!kY5!1ws>*%&26E60!AO z6LQDaPd1`=ukEa_nWfMy%>X=Gv2%Y-VDyj&oE!HkW(?RC_Lts9RH$hcG>>*GeQEl~ zil~=ZJqATPA^GLWApWJP`ARts&E8@;(*Xc|H`*I6EnCx9tYdYS;3WVA5y4dqQ)sF& zI64iSq{uV_#w!Hg4Z?N^6is#ZDArLw* zE2FMO23bPs)EpYbtiU#)fkwy0I&2yVgZN-jKFdudbhrD*teZ!yQjj}`*z%h)<+kD5{ zfGbNrZLY1oIbW@Z0sw&O>T22XK&@w}3PGYkfpr4fq4usnWC$RR~H#Xhj^UO2PJoEe?L#Y&7d1z%`*vs9* zl-F{EXH&!X8vLo$PlQO7&|{l+WuxEy7iB4r#Ki|su+n#NePvg?>GGwEp_8Xz2%9gR9SlS?843zufP{cCp&U>Om+}L1Uc8V3!kF_V7;L_F zVJJo?@fer%VcQWUztm#A4)%4_|?47h(`$hI0ej4T_;MGJHuHW z(eq8`+;@P9&L%YgfIkRR(pEyCz$nYkI@?hX0E&UIW{kRGb`(Jxd(K;RY4GN^2G04a z+cuBw>)cCBe{%Ktk zX>K^Q;x#@{c6dGFTYNyNP~)@)G~?qz7SAstRSW7#c^pv{ z!oWw-{Xas?J~mf8Qul7q^hHgA1-)z34zy|Dvc+5(eZJBRo2ECA2-7A1{euv|*M`IV z!mW$uGd(XmZs)BA@F%{S`2@UXOuZB6Xd69Qb z8McrZKPA?+;RWzoFV(*#zkll-GVRO!S$1aBw6N*mzs_s`fM)$#gC%=svls#90SDr5 z!_&0lWZj1dnE+L5=-<=6-bZPNHzP{#NJCYFH}M*#aP{QTmf!juT||+JP0T37CqMLM z9(v0WZ#Hc_uRXo@p&L$0yCqE-iXNMF{}(*@GQ_N3OL|(OgMP-(Pg_P;vPlZ!C+m;A z|Fn;AT8^Z-rj7@*?i88V@^8A2uPn$oR_4t5Y}K&Kzo6YT9A3jI@6OJ_B#lkg7e7fh zC=(qV?rQ0~S-7@hk4rsF;FtwU5~tT3dH=fr@Z-{w9Xmt(jdO)}DI9e%vu3H+-s$yp z=aebiI7>kzX!K;Qa9qz{ipCrhea;qP@-=wWI3zE?H18$E{QThC{T)filMR$_s>)m9 zmY@F06DrZnlL1UAf31~%iV0=5)-G=-jKih(@b`uBiFLTmGaW;1n{f~}0F-U5E+$|* z3v7qxEg)pTsYhtlcq^V5Sp`CF^n51NHtlJc_5~>64gf%Z_jSK(S!p|n*OyA28!@Yy zH!!e-0fUGTQX07-11A=b#%U;97v`kr9VnaZV^W>|x zEBFyD3<*?>EGLtvegLopQ_bMpeFJgAQOx-0R<>>Tb2Se2a*Z=w!U@hFw`>Ho!iyXG zF=QrxFOa4TXFQzsCb7Q4fFDFlhKV%|M?v z)~z>H8i1HKA_c*3E362*aH?P|sgB{4(&=%m184_ndyVZQI_&j3OZfL8B9zyI9gmogLKU8{4w99d9D@p-f2UjQQ%+ z0^6?cZb;g79@s!K9@(~g3o{+pIiXSC#&)-|Y|ms6A&^~|HN8NX>Q#UGv&}Eo-k3|= zlztm6+giO9#D`fTEA(V{cgS7k58m3Wsn{TB1xz&AAJ6ykMj`{-fKQ$1c zbbC#S(0U)h+rcOUA@XqMW8fkI$1Kfnw0?N>_J3*(DW#;cGP8VZ%?ua9JoC&m&piKS Z{soKhKGhw*_GbV9002ovPDHLkV1gda&R_rl literal 0 HcmV?d00001 diff --git a/packages/mcp-extension/manifest.json b/packages/mcp-extension/manifest.json new file mode 100644 index 0000000000000..ffc9e9d3fdbed --- /dev/null +++ b/packages/mcp-extension/manifest.json @@ -0,0 +1,35 @@ +{ + "manifest_version": 3, + "name": "Playwright MCP Bridge", + "version": "0.0.36", + "description": "Share browser tabs with Playwright MCP server", + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nMS2b0WCohjVHPGb8D9qAdkbIngDqoAjTeSccHJijgcONejge+OJxOQOMLu7b0ovt1c9BiEJa5JcpM+EHFVGL1vluBxK71zmBy1m2f9vZF3HG0LSCp7YRkum9rAIEthDwbkxx6XTvpmAY5rjFa/NON6b9Hlbo+8peUSkoOK7HTwYnnI36asZ9eUTiveIf+DMPLojW2UX33vDWG2UKvMVDewzclb4+uLxAYshY7Mx8we/b44xu+Anb/EBLKjOPk9Yh541xJ5Ozc8EiP/5yxOp9c/lRiYUHaRW+4r0HKZyFt0eZ52ti2iM4Nfk7jRXR7an3JPsUIf5deC/1cVM/+1ZQIDAQAB", + "permissions": [ + "debugger", + "activeTab", + "tabs", + "storage" + ], + "host_permissions": [ + "" + ], + "background": { + "service_worker": "lib/background.mjs", + "type": "module" + }, + "action": { + "default_title": "Playwright MCP Bridge", + "default_icon": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } + }, + "icons": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } +} diff --git a/packages/mcp-extension/package.json b/packages/mcp-extension/package.json new file mode 100644 index 0000000000000..89b75ebf3d7ea --- /dev/null +++ b/packages/mcp-extension/package.json @@ -0,0 +1,35 @@ +{ + "name": "@playwright/mcp-extension", + "version": "0.0.36", + "description": "Playwright MCP Browser Extension", + "private": true, + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/playwright-mcp.git" + }, + "homepage": "https://playwright.dev", + "engines": { + "node": ">=18" + }, + "author": { + "name": "Microsoft Corporation" + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc --project . && tsc --project tsconfig.ui.json && vite build && vite build --config vite.sw.config.mts", + "watch": "tsc --watch --project . & tsc --watch --project tsconfig.ui.json & vite build --watch & vite build --watch --config vite.sw.config.mts", + "test": "playwright test", + "clean": "rm -rf dist" + }, + "devDependencies": { + "@types/chrome": "^0.0.315", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.8.2", + "vite": "^5.0.0", + "vite-plugin-static-copy": "^3.1.1" + } +} diff --git a/packages/mcp-extension/src/background.ts b/packages/mcp-extension/src/background.ts new file mode 100644 index 0000000000000..e74ebb80cdcff --- /dev/null +++ b/packages/mcp-extension/src/background.ts @@ -0,0 +1,222 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RelayConnection, debugLog } from './relayConnection'; + +type PageMessage = { + type: 'connectToMCPRelay'; + mcpRelayUrl: string; +} | { + type: 'getTabs'; +} | { + type: 'connectToTab'; + tabId?: number; + windowId?: number; + mcpRelayUrl: string; +} | { + type: 'getConnectionStatus'; +} | { + type: 'disconnect'; +}; + +class TabShareExtension { + private _activeConnection: RelayConnection | undefined; + private _connectedTabId: number | null = null; + private _pendingTabSelection = new Map(); + + constructor() { + chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this)); + chrome.tabs.onUpdated.addListener(this._onTabUpdated.bind(this)); + chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this)); + chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); + chrome.action.onClicked.addListener(this._onActionClicked.bind(this)); + } + + // Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031 + private _onMessage(message: PageMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) { + switch (message.type) { + case 'connectToMCPRelay': + this._connectToRelay(sender.tab!.id!, message.mcpRelayUrl).then( + () => sendResponse({ success: true }), + (error: any) => sendResponse({ success: false, error: error.message })); + return true; + case 'getTabs': + this._getTabs().then( + tabs => sendResponse({ success: true, tabs, currentTabId: sender.tab?.id }), + (error: any) => sendResponse({ success: false, error: error.message })); + return true; + case 'connectToTab': + const tabId = message.tabId || sender.tab?.id!; + const windowId = message.windowId || sender.tab?.windowId!; + this._connectTab(sender.tab!.id!, tabId, windowId, message.mcpRelayUrl!).then( + () => sendResponse({ success: true }), + (error: any) => sendResponse({ success: false, error: error.message })); + return true; // Return true to indicate that the response will be sent asynchronously + case 'getConnectionStatus': + sendResponse({ + connectedTabId: this._connectedTabId + }); + return false; + case 'disconnect': + this._disconnect().then( + () => sendResponse({ success: true }), + (error: any) => sendResponse({ success: false, error: error.message })); + return true; + } + return false; + } + + private async _connectToRelay(selectorTabId: number, mcpRelayUrl: string): Promise { + try { + debugLog(`Connecting to relay at ${mcpRelayUrl}`); + const socket = new WebSocket(mcpRelayUrl); + await new Promise((resolve, reject) => { + socket.onopen = () => resolve(); + socket.onerror = () => reject(new Error('WebSocket error')); + setTimeout(() => reject(new Error('Connection timeout')), 5000); + }); + + const connection = new RelayConnection(socket); + connection.onclose = () => { + debugLog('Connection closed'); + this._pendingTabSelection.delete(selectorTabId); + // TODO: show error in the selector tab? + }; + this._pendingTabSelection.set(selectorTabId, { connection }); + debugLog(`Connected to MCP relay`); + } catch (error: any) { + const message = `Failed to connect to MCP relay: ${error.message}`; + debugLog(message); + throw new Error(message); + } + } + + private async _connectTab(selectorTabId: number, tabId: number, windowId: number, mcpRelayUrl: string): Promise { + try { + debugLog(`Connecting tab ${tabId} to relay at ${mcpRelayUrl}`); + try { + this._activeConnection?.close('Another connection is requested'); + } catch (error: any) { + debugLog(`Error closing active connection:`, error); + } + await this._setConnectedTabId(null); + + this._activeConnection = this._pendingTabSelection.get(selectorTabId)?.connection; + if (!this._activeConnection) + throw new Error('No active MCP relay connection'); + this._pendingTabSelection.delete(selectorTabId); + + this._activeConnection.setTabId(tabId); + this._activeConnection.onclose = () => { + debugLog('MCP connection closed'); + this._activeConnection = undefined; + void this._setConnectedTabId(null); + }; + + await Promise.all([ + this._setConnectedTabId(tabId), + chrome.tabs.update(tabId, { active: true }), + chrome.windows.update(windowId, { focused: true }), + ]); + debugLog(`Connected to MCP bridge`); + } catch (error: any) { + await this._setConnectedTabId(null); + debugLog(`Failed to connect tab ${tabId}:`, error.message); + throw error; + } + } + + private async _setConnectedTabId(tabId: number | null): Promise { + const oldTabId = this._connectedTabId; + this._connectedTabId = tabId; + if (oldTabId && oldTabId !== tabId) + await this._updateBadge(oldTabId, { text: '' }); + if (tabId) + await this._updateBadge(tabId, { text: '✓', color: '#4CAF50', title: 'Connected to MCP client' }); + } + + private async _updateBadge(tabId: number, { text, color, title }: { text: string; color?: string, title?: string }): Promise { + try { + await chrome.action.setBadgeText({ tabId, text }); + await chrome.action.setTitle({ tabId, title: title || '' }); + if (color) + await chrome.action.setBadgeBackgroundColor({ tabId, color }); + } catch (error: any) { + // Ignore errors as the tab may be closed already. + } + } + + private async _onTabRemoved(tabId: number): Promise { + const pendingConnection = this._pendingTabSelection.get(tabId)?.connection; + if (pendingConnection) { + this._pendingTabSelection.delete(tabId); + pendingConnection.close('Browser tab closed'); + return; + } + if (this._connectedTabId !== tabId) + return; + this._activeConnection?.close('Browser tab closed'); + this._activeConnection = undefined; + this._connectedTabId = null; + } + + private _onTabActivated(activeInfo: chrome.tabs.TabActiveInfo) { + for (const [tabId, pending] of this._pendingTabSelection) { + if (tabId === activeInfo.tabId) { + if (pending.timerId) { + clearTimeout(pending.timerId); + pending.timerId = undefined; + } + continue; + } + if (!pending.timerId) { + pending.timerId = setTimeout(() => { + const existed = this._pendingTabSelection.delete(tabId); + if (existed) { + pending.connection.close('Tab has been inactive for 5 seconds'); + chrome.tabs.sendMessage(tabId, { type: 'connectionTimeout' }); + } + }, 5000); + return; + } + } + } + + private _onTabUpdated(tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) { + if (this._connectedTabId === tabId) + void this._setConnectedTabId(tabId); + } + + private async _getTabs(): Promise { + const tabs = await chrome.tabs.query({}); + return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme))); + } + + private async _onActionClicked(): Promise { + await chrome.tabs.create({ + url: chrome.runtime.getURL('status.html'), + active: true + }); + } + + private async _disconnect(): Promise { + this._activeConnection?.close('User disconnected'); + this._activeConnection = undefined; + await this._setConnectedTabId(null); + } +} + +new TabShareExtension(); diff --git a/packages/mcp-extension/src/relayConnection.ts b/packages/mcp-extension/src/relayConnection.ts new file mode 100644 index 0000000000000..b203af4222df5 --- /dev/null +++ b/packages/mcp-extension/src/relayConnection.ts @@ -0,0 +1,178 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function debugLog(...args: unknown[]): void { + const enabled = true; + if (enabled) { + // eslint-disable-next-line no-console + console.log('[Extension]', ...args); + } +} + +type ProtocolCommand = { + id: number; + method: string; + params?: any; +}; + +type ProtocolResponse = { + id?: number; + method?: string; + params?: any; + result?: any; + error?: string; +}; + +export class RelayConnection { + private _debuggee: chrome.debugger.Debuggee; + private _ws: WebSocket; + private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void; + private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void; + private _tabPromise: Promise; + private _tabPromiseResolve!: () => void; + private _closed = false; + + onclose?: () => void; + + constructor(ws: WebSocket) { + this._debuggee = { }; + this._tabPromise = new Promise(resolve => this._tabPromiseResolve = resolve); + this._ws = ws; + this._ws.onmessage = this._onMessage.bind(this); + this._ws.onclose = () => this._onClose(); + // Store listeners for cleanup + this._eventListener = this._onDebuggerEvent.bind(this); + this._detachListener = this._onDebuggerDetach.bind(this); + chrome.debugger.onEvent.addListener(this._eventListener); + chrome.debugger.onDetach.addListener(this._detachListener); + } + + // Either setTabId or close is called after creating the connection. + setTabId(tabId: number): void { + this._debuggee = { tabId }; + this._tabPromiseResolve(); + } + + close(message: string): void { + this._ws.close(1000, message); + // ws.onclose is called asynchronously, so we call it here to avoid forwarding + // CDP events to the closed connection. + this._onClose(); + } + + private _onClose() { + if (this._closed) + return; + this._closed = true; + chrome.debugger.onEvent.removeListener(this._eventListener); + chrome.debugger.onDetach.removeListener(this._detachListener); + chrome.debugger.detach(this._debuggee).catch(() => {}); + this.onclose?.(); + } + + private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void { + if (source.tabId !== this._debuggee.tabId) + return; + debugLog('Forwarding CDP event:', method, params); + const sessionId = source.sessionId; + this._sendMessage({ + method: 'forwardCDPEvent', + params: { + sessionId, + method, + params, + }, + }); + } + + private _onDebuggerDetach(source: chrome.debugger.Debuggee, reason: string): void { + if (source.tabId !== this._debuggee.tabId) + return; + this.close(`Debugger detached: ${reason}`); + this._debuggee = { }; + } + + private _onMessage(event: MessageEvent): void { + this._onMessageAsync(event).catch(e => debugLog('Error handling message:', e)); + } + + private async _onMessageAsync(event: MessageEvent): Promise { + let message: ProtocolCommand; + try { + message = JSON.parse(event.data); + } catch (error: any) { + debugLog('Error parsing message:', error); + this._sendError(-32700, `Error parsing message: ${error.message}`); + return; + } + + debugLog('Received message:', message); + + const response: ProtocolResponse = { + id: message.id, + }; + try { + response.result = await this._handleCommand(message); + } catch (error: any) { + debugLog('Error handling command:', error); + response.error = error.message; + } + debugLog('Sending response:', response); + this._sendMessage(response); + } + + private async _handleCommand(message: ProtocolCommand): Promise { + if (message.method === 'attachToTab') { + await this._tabPromise; + debugLog('Attaching debugger to tab:', this._debuggee); + await chrome.debugger.attach(this._debuggee, '1.3'); + const result: any = await chrome.debugger.sendCommand(this._debuggee, 'Target.getTargetInfo'); + return { + targetInfo: result?.targetInfo, + }; + } + if (!this._debuggee.tabId) + throw new Error('No tab is connected. Please go to the Playwright MCP extension and select the tab you want to connect to.'); + if (message.method === 'forwardCDPCommand') { + const { sessionId, method, params } = message.params; + debugLog('CDP command:', method, params); + const debuggerSession: chrome.debugger.DebuggerSession = { + ...this._debuggee, + sessionId, + }; + // Forward CDP command to chrome.debugger + return await chrome.debugger.sendCommand( + debuggerSession, + method, + params + ); + } + } + + private _sendError(code: number, message: string): void { + this._sendMessage({ + error: { + code, + message, + }, + }); + } + + private _sendMessage(message: any): void { + if (this._ws.readyState === WebSocket.OPEN) + this._ws.send(JSON.stringify(message)); + } +} diff --git a/packages/mcp-extension/src/ui/connect.css b/packages/mcp-extension/src/ui/connect.css new file mode 100644 index 0000000000000..60945d4d4bd09 --- /dev/null +++ b/packages/mcp-extension/src/ui/connect.css @@ -0,0 +1,206 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +body { + margin: 0; + padding: 0; +} + +/* Base styles */ +.app-container { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; + background-color: #ffffff; + color: #1f2328; + margin: 0; + padding: 16px; + min-height: 100vh; + font-size: 14px; +} + +.content-wrapper { + max-width: 600px; + margin: 0 auto; +} + +/* Status Banner */ +.status-container { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + padding-right: 12px; +} + +.status-banner { + padding: 12px; + font-size: 14px; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; + flex: 1; +} + +.status-banner.connected { + color: #1f2328; +} + +.status-banner.connected::before { + content: "\2705"; + margin-right: 8px; +} + +.status-banner.error { + color: #1f2328; +} + +.status-banner.error::before { + content: "\274C"; + margin-right: 8px; +} + +/* Buttons */ +.button-container { + margin-bottom: 16px; + display: flex; + justify-content: flex-end; + padding-right: 12px; +} + +.button { + padding: 8px 16px; + border-radius: 6px; + border: none; + font-size: 14px; + font-weight: 500; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + margin-right: 8px; + min-width: 90px; +} + +.button.primary { + background-color: #f8f9fa; + color: #3c4043; + border: 1px solid #dadce0; +} + +.button.primary:hover { + background-color: #f1f3f4; + border-color: #dadce0; + box-shadow: 0 1px 2px 0 rgba(60,64,67,.1); +} + +.button.default { + background-color: #f6f8fa; + color: #24292f; +} + +.button.default:hover { + background-color: #f3f4f6; +} + +.button.reject { + background-color: #da3633; + color: #ffffff; + border: 1px solid #da3633; +} + +.button.reject:hover { + background-color: #c73836; + border-color: #c73836; +} + +/* Tab selection */ +.tab-section-title { + padding-left: 12px; + font-size: 12px; + font-weight: 400; + margin-bottom: 12px; + color: #656d76; +} + +.tab-item { + display: flex; + align-items: center; + padding: 12px; + margin-bottom: 8px; + background-color: #ffffff; + cursor: pointer; + border-radius: 6px; + transition: background-color 0.2s ease; +} + +.tab-item:hover { + background-color: #f8f9fa; +} + +.tab-item.selected { + background-color: #f6f8fa; +} + +.tab-item.disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.tab-radio { + margin-right: 12px; + flex-shrink: 0; +} + +.tab-favicon { + width: 16px; + height: 16px; + margin-right: 8px; + flex-shrink: 0; +} + +.tab-content { + flex: 1; + min-width: 0; +} + +.tab-title { + font-weight: 500; + color: #1f2328; + margin-bottom: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.tab-url { + font-size: 12px; + color: #656d76; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Link-style button */ +.link-button { + background: none; + border: none; + color: #0066cc; + text-decoration: underline; + cursor: pointer; + padding: 0; + font: inherit; +} \ No newline at end of file diff --git a/packages/mcp-extension/src/ui/connect.html b/packages/mcp-extension/src/ui/connect.html new file mode 100644 index 0000000000000..3f20e4b36010e --- /dev/null +++ b/packages/mcp-extension/src/ui/connect.html @@ -0,0 +1,29 @@ + + + + + Playwright MCP extension + + + + + + +
+ + + \ No newline at end of file diff --git a/packages/mcp-extension/src/ui/connect.tsx b/packages/mcp-extension/src/ui/connect.tsx new file mode 100644 index 0000000000000..153017b5bbf95 --- /dev/null +++ b/packages/mcp-extension/src/ui/connect.tsx @@ -0,0 +1,233 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { createRoot } from 'react-dom/client'; +import { Button, TabItem } from './tabItem'; +import type { TabInfo } from './tabItem'; + +type Status = + | { type: 'connecting'; message: string } + | { type: 'connected'; message: string } + | { type: 'error'; message: string } + | { type: 'error'; versionMismatch: { extensionVersion: string; } }; + +const SUPPORTED_PROTOCOL_VERSION = 1; + +const ConnectApp: React.FC = () => { + const [tabs, setTabs] = useState([]); + const [status, setStatus] = useState(null); + const [showButtons, setShowButtons] = useState(true); + const [showTabList, setShowTabList] = useState(true); + const [clientInfo, setClientInfo] = useState('unknown'); + const [mcpRelayUrl, setMcpRelayUrl] = useState(''); + const [newTab, setNewTab] = useState(false); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const relayUrl = params.get('mcpRelayUrl'); + + if (!relayUrl) { + setShowButtons(false); + setStatus({ type: 'error', message: 'Missing mcpRelayUrl parameter in URL.' }); + return; + } + + setMcpRelayUrl(relayUrl); + + try { + const client = JSON.parse(params.get('client') || '{}'); + const info = `${client.name}/${client.version}`; + setClientInfo(info); + setStatus({ + type: 'connecting', + message: `🎭 Playwright MCP started from "${info}" is trying to connect. Do you want to continue?` + }); + } catch (e) { + setStatus({ type: 'error', message: 'Failed to parse client version.' }); + return; + } + + const parsedVersion = parseInt(params.get('protocolVersion') ?? '', 10); + const requiredVersion = isNaN(parsedVersion) ? 1 : parsedVersion; + if (requiredVersion > SUPPORTED_PROTOCOL_VERSION) { + const extensionVersion = chrome.runtime.getManifest().version; + setShowButtons(false); + setShowTabList(false); + setStatus({ + type: 'error', + versionMismatch: { + extensionVersion, + } + }); + return; + } + + void connectToMCPRelay(relayUrl); + + // If this is a browser_navigate command, hide the tab list and show simple allow/reject + if (params.get('newTab') === 'true') { + setNewTab(true); + setShowTabList(false); + } else { + void loadTabs(); + } + }, []); + + const handleReject = useCallback((message: string) => { + setShowButtons(false); + setShowTabList(false); + setStatus({ type: 'error', message }); + }, []); + + const connectToMCPRelay = useCallback(async (mcpRelayUrl: string) => { + + const response = await chrome.runtime.sendMessage({ type: 'connectToMCPRelay', mcpRelayUrl }); + if (!response.success) + handleReject(response.error); + }, [handleReject]); + + const loadTabs = useCallback(async () => { + const response = await chrome.runtime.sendMessage({ type: 'getTabs' }); + if (response.success) + setTabs(response.tabs); + else + setStatus({ type: 'error', message: 'Failed to load tabs: ' + response.error }); + }, []); + + const handleConnectToTab = useCallback(async (tab?: TabInfo) => { + setShowButtons(false); + setShowTabList(false); + + try { + const response = await chrome.runtime.sendMessage({ + type: 'connectToTab', + mcpRelayUrl, + tabId: tab?.id, + windowId: tab?.windowId, + }); + + if (response?.success) { + setStatus({ type: 'connected', message: `MCP client "${clientInfo}" connected.` }); + } else { + setStatus({ + type: 'error', + message: response?.error || `MCP client "${clientInfo}" failed to connect.` + }); + } + } catch (e) { + setStatus({ + type: 'error', + message: `MCP client "${clientInfo}" failed to connect: ${e}` + }); + } + }, [clientInfo, mcpRelayUrl]); + + useEffect(() => { + const listener = (message: any) => { + if (message.type === 'connectionTimeout') + handleReject('Connection timed out.'); + }; + chrome.runtime.onMessage.addListener(listener); + return () => { + chrome.runtime.onMessage.removeListener(listener); + }; + }, [handleReject]); + + return ( +
+
+ {status && ( +
+ + {showButtons && ( +
+ {newTab ? ( + <> + + + + ) : ( + + )} +
+ )} +
+ )} + + {showTabList && ( +
+
+ Select page to expose to MCP server: +
+
+ {tabs.map(tab => ( + handleConnectToTab(tab)}> + Connect + + } + /> + ))} +
+
+ )} +
+
+ ); +}; + +const VersionMismatchError: React.FC<{ extensionVersion: string }> = ({ extensionVersion }) => { + const readmeUrl = 'https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md'; + const latestReleaseUrl = 'https://github.com/microsoft/playwright-mcp/releases/latest'; + return ( +
+ Playwright MCP version trying to connect requires newer extension version (current version: {extensionVersion}).{' '} + Click here to download latest version of the extension, then drag and drop it into the Chrome Extensions page.{' '} + See installation instructions for more details. +
+ ); +}; + +const StatusBanner: React.FC<{ status: Status }> = ({ status }) => { + return ( +
+ {'versionMismatch' in status ? ( + + ) : ( + status.message + )} +
+ ); +}; + +// Initialize the React app +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render(); +} diff --git a/packages/mcp-extension/src/ui/status.html b/packages/mcp-extension/src/ui/status.html new file mode 100644 index 0000000000000..ccc1f04aa91b9 --- /dev/null +++ b/packages/mcp-extension/src/ui/status.html @@ -0,0 +1,13 @@ + + + + + + Playwright MCP Bridge Status + + + +
+ + + \ No newline at end of file diff --git a/packages/mcp-extension/src/ui/status.tsx b/packages/mcp-extension/src/ui/status.tsx new file mode 100644 index 0000000000000..f8de78880707d --- /dev/null +++ b/packages/mcp-extension/src/ui/status.tsx @@ -0,0 +1,110 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { createRoot } from 'react-dom/client'; +import { Button, TabItem } from './tabItem'; + +import type { TabInfo } from './tabItem'; + +interface ConnectionStatus { + isConnected: boolean; + connectedTabId: number | null; + connectedTab?: TabInfo; +} + +const StatusApp: React.FC = () => { + const [status, setStatus] = useState({ + isConnected: false, + connectedTabId: null + }); + + useEffect(() => { + void loadStatus(); + }, []); + + const loadStatus = async () => { + // Get current connection status from background script + const { connectedTabId } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' }); + if (connectedTabId) { + const tab = await chrome.tabs.get(connectedTabId); + setStatus({ + isConnected: true, + connectedTabId, + connectedTab: { + id: tab.id!, + windowId: tab.windowId!, + title: tab.title!, + url: tab.url!, + favIconUrl: tab.favIconUrl + } + }); + } else { + setStatus({ + isConnected: false, + connectedTabId: null + }); + } + }; + + const openConnectedTab = async () => { + if (!status.connectedTabId) + return; + await chrome.tabs.update(status.connectedTabId, { active: true }); + window.close(); + }; + + const disconnect = async () => { + await chrome.runtime.sendMessage({ type: 'disconnect' }); + window.close(); + }; + + return ( +
+
+ {status.isConnected && status.connectedTab ? ( +
+
+ Page with connected MCP client: +
+
+ + Disconnect + + } + onClick={openConnectedTab} + /> +
+
+ ) : ( +
+ No MCP clients are currently connected. +
+ )} +
+
+ ); +}; + +// Initialize the React app +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render(); +} diff --git a/packages/mcp-extension/src/ui/tabItem.tsx b/packages/mcp-extension/src/ui/tabItem.tsx new file mode 100644 index 0000000000000..148374292f5b9 --- /dev/null +++ b/packages/mcp-extension/src/ui/tabItem.tsx @@ -0,0 +1,67 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +export interface TabInfo { + id: number; + windowId: number; + title: string; + url: string; + favIconUrl?: string; +} + +export const Button: React.FC<{ variant: 'primary' | 'default' | 'reject'; onClick: () => void; children: React.ReactNode }> = ({ + variant, + onClick, + children +}) => { + return ( + + ); +}; + + +export interface TabItemProps { + tab: TabInfo; + onClick?: () => void; + button?: React.ReactNode; +} + +export const TabItem: React.FC = ({ + tab, + onClick, + button +}) => { + return ( +
+ '} + alt='' + className='tab-favicon' + /> +
+
+ {tab.title || 'Untitled'} +
+
{tab.url}
+
+ {button} +
+ ); +}; diff --git a/packages/mcp-extension/src/ui/tsconfig.json b/packages/mcp-extension/src/ui/tsconfig.json new file mode 100644 index 0000000000000..77839b62d222b --- /dev/null +++ b/packages/mcp-extension/src/ui/tsconfig.json @@ -0,0 +1,4 @@ +// Help VSCode to find right tsconfig file. +{ + "extends": "../../tsconfig.ui.json" +} diff --git a/packages/mcp-extension/tsconfig.json b/packages/mcp-extension/tsconfig.json new file mode 100644 index 0000000000000..9c22b0bca0018 --- /dev/null +++ b/packages/mcp-extension/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "esModuleInterop": true, + "moduleResolution": "node", + "strict": true, + "module": "ESNext", + "rootDir": "src", + "outDir": "./dist/lib", + "resolveJsonModule": true, + "types": ["chrome"], + "jsx": "react-jsx", + "jsxImportSource": "react", + "noEmit": true + }, + "include": [ + "src", + ], + "exclude": [ + "src/ui", + ] +} diff --git a/packages/mcp-extension/tsconfig.ui.json b/packages/mcp-extension/tsconfig.ui.json new file mode 100644 index 0000000000000..f62dd8a3f74c3 --- /dev/null +++ b/packages/mcp-extension/tsconfig.ui.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "esModuleInterop": true, + "moduleResolution": "node", + "strict": true, + "module": "ESNext", + "rootDir": "src", + "outDir": "./lib", + "resolveJsonModule": true, + "types": ["chrome"], + "jsx": "react-jsx", + "jsxImportSource": "react", + "noEmit": true, + }, + "include": [ + "src/ui", + ], +} diff --git a/packages/mcp-extension/vite.config.mts b/packages/mcp-extension/vite.config.mts new file mode 100644 index 0000000000000..89ec56c6898a2 --- /dev/null +++ b/packages/mcp-extension/vite.config.mts @@ -0,0 +1,54 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolve } from 'path'; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + viteStaticCopy({ + targets: [ + { + src: '../../icons/*', + dest: 'icons' + }, + { + src: '../../manifest.json', + dest: '.' + } + ] + }) + ], + root: resolve(__dirname, 'src/ui'), + build: { + outDir: resolve(__dirname, 'dist/'), + emptyOutDir: false, + minify: false, + rollupOptions: { + input: ['src/ui/connect.html', 'src/ui/status.html'], + output: { + manualChunks: undefined, + entryFileNames: 'lib/ui/[name].js', + chunkFileNames: 'lib/ui/[name].js', + assetFileNames: 'lib/ui/[name].[ext]' + } + } + } +}); diff --git a/packages/mcp-extension/vite.sw.config.mts b/packages/mcp-extension/vite.sw.config.mts new file mode 100644 index 0000000000000..a383e4b4a50e9 --- /dev/null +++ b/packages/mcp-extension/vite.sw.config.mts @@ -0,0 +1,31 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolve } from 'path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, 'src/background.ts'), + fileName: 'lib/background', + formats: ['es'] + }, + outDir: 'dist', + emptyOutDir: false, + minify: false + } +}); diff --git a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts index 409372e89448e..83a1074f17639 100644 --- a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts +++ b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts @@ -50,6 +50,7 @@ export const open = openLibrary; export { PNG } from 'pngjs'; export { program } from 'commander'; +export { Option as ProgramOption } from 'commander'; import progressLibrary from 'progress'; export const progress = progressLibrary; diff --git a/packages/playwright-core/src/utilsBundle.ts b/packages/playwright-core/src/utilsBundle.ts index 41cccd1f57263..a0a9e695b7c56 100644 --- a/packages/playwright-core/src/utilsBundle.ts +++ b/packages/playwright-core/src/utilsBundle.ts @@ -27,6 +27,7 @@ export const minimatch: typeof import('../bundles/utils/node_modules/@types/mini export const open: typeof import('../bundles/utils/node_modules/open') = require('./utilsBundleImpl').open; export const PNG: typeof import('../bundles/utils/node_modules/@types/pngjs').PNG = require('./utilsBundleImpl').PNG; export const program: typeof import('../bundles/utils/node_modules/commander').program = require('./utilsBundleImpl').program; +export const ProgramOption: typeof import('../bundles/utils/node_modules/commander').Option = require('./utilsBundleImpl').ProgramOption; export const progress: typeof import('../bundles/utils/node_modules/@types/progress') = require('./utilsBundleImpl').progress; export const SocksProxyAgent: typeof import('../bundles/utils/node_modules/socks-proxy-agent').SocksProxyAgent = require('./utilsBundleImpl').SocksProxyAgent; export const yaml: typeof import('../bundles/utils/node_modules/yaml') = require('./utilsBundleImpl').yaml; diff --git a/packages/playwright/ThirdPartyNotices.txt b/packages/playwright/ThirdPartyNotices.txt index cdfac862b8b2f..5754ac2a36fb5 100644 --- a/packages/playwright/ThirdPartyNotices.txt +++ b/packages/playwright/ThirdPartyNotices.txt @@ -62,7 +62,7 @@ This project incorporates components from the projects listed below. The origina - @jridgewell/resolve-uri@3.1.1 (https://github.com/jridgewell/resolve-uri) - @jridgewell/sourcemap-codec@1.5.4 (https://github.com/jridgewell/sourcemaps) - @jridgewell/trace-mapping@0.3.29 (https://github.com/jridgewell/sourcemaps) -- @modelcontextprotocol/sdk@1.17.3 (https://github.com/modelcontextprotocol/typescript-sdk) +- @modelcontextprotocol/sdk@1.17.5 (https://github.com/modelcontextprotocol/typescript-sdk) - @sinclair/typebox@0.27.8 (https://github.com/sinclairzx81/typebox) - @types/istanbul-lib-coverage@2.0.6 (https://github.com/DefinitelyTyped/DefinitelyTyped) - @types/istanbul-lib-report@3.0.3 (https://github.com/DefinitelyTyped/DefinitelyTyped) @@ -1955,7 +1955,7 @@ SOFTWARE. ========================================= END OF @jridgewell/trace-mapping@0.3.29 AND INFORMATION -%% @modelcontextprotocol/sdk@1.17.3 NOTICES AND INFORMATION BEGIN HERE +%% @modelcontextprotocol/sdk@1.17.5 NOTICES AND INFORMATION BEGIN HERE ========================================= MIT License @@ -1979,7 +1979,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF @modelcontextprotocol/sdk@1.17.3 AND INFORMATION +END OF @modelcontextprotocol/sdk@1.17.5 AND INFORMATION %% @sinclair/typebox@0.27.8 NOTICES AND INFORMATION BEGIN HERE ========================================= diff --git a/packages/playwright/bundles/mcp/package-lock.json b/packages/playwright/bundles/mcp/package-lock.json index 9c8d24a2ed358..fa26c67851736 100644 --- a/packages/playwright/bundles/mcp/package-lock.json +++ b/packages/playwright/bundles/mcp/package-lock.json @@ -8,15 +8,15 @@ "name": "mcp-bundle", "version": "0.0.1", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.3", + "@modelcontextprotocol/sdk": "^1.17.5", "zod": "^3.25.76", "zod-to-json-schema": "^3.24.6" } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz", - "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==", + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", + "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", diff --git a/packages/playwright/bundles/mcp/package.json b/packages/playwright/bundles/mcp/package.json index be2ee06a43b55..9c39c6db792a7 100644 --- a/packages/playwright/bundles/mcp/package.json +++ b/packages/playwright/bundles/mcp/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "private": true, "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.3", + "@modelcontextprotocol/sdk": "^1.17.5", "zod": "^3.25.76", "zod-to-json-schema": "^3.24.6" } diff --git a/packages/playwright/package.json b/packages/playwright/package.json index f592f9ebf2e5d..0e47ef02e9ca5 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -21,7 +21,10 @@ "./package.json": "./package.json", "./lib/common/configLoader": "./lib/common/configLoader.js", "./lib/fsWatcher": "./lib/fsWatcher.js", - "./lib/mcpBundleImpl": "./lib/mcpBundleImpl.js", + "./lib/mcp/index": "./lib/mcp/index.js", + "./lib/mcp/browser/tools": "./lib/mcp/browser/tools.js", + "./lib/mcp/program": "./lib/mcp/program.js", + "./lib/mcp/sdk/bundle": "./lib/mcp/sdk/bundle.js", "./lib/mcp/sdk/exports": "./lib/mcp/sdk/exports.js", "./lib/program": "./lib/program.js", "./lib/reporters/base": "./lib/reporters/base.js", diff --git a/packages/playwright/src/DEPS.list b/packages/playwright/src/DEPS.list index b667396ff338d..d0c1ac71bc33a 100644 --- a/packages/playwright/src/DEPS.list +++ b/packages/playwright/src/DEPS.list @@ -12,6 +12,7 @@ common/ ./worker/testTracing.ts ./mcp/browser/ ./mcp/sdk/ +./mcp/test/ ./transform/babelBundle.ts [internalsForTest.ts] diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 7a786304755c7..b9b681838eca3 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -22,7 +22,7 @@ import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringif import { currentTestInfo } from './common/globals'; import { rootTestType } from './common/testType'; -import { runBrowserBackendOnError } from './mcp/browser/backend'; +import { runBrowserBackendOnError } from './mcp/test/browserBackend'; import { codeFrameColumns } from './transform/babelBundle'; import { stripAnsiEscapes } from './util'; diff --git a/packages/playwright/src/matchers/DEPS.list b/packages/playwright/src/matchers/DEPS.list index 978b4038f5f8b..b205c62130af7 100644 --- a/packages/playwright/src/matchers/DEPS.list +++ b/packages/playwright/src/matchers/DEPS.list @@ -1,6 +1,6 @@ [*] ../common/ -../mcp/browser/backend.ts +../mcp/test/browserBackend.ts ../util.ts ../utilsBundle.ts ../worker/testInfo.ts diff --git a/packages/playwright/src/matchers/toBeTruthy.ts b/packages/playwright/src/matchers/toBeTruthy.ts index 7e3f1dbcaf2cb..3f6144eaf8819 100644 --- a/packages/playwright/src/matchers/toBeTruthy.ts +++ b/packages/playwright/src/matchers/toBeTruthy.ts @@ -16,7 +16,7 @@ import { callLogText, expectTypes } from '../util'; import { kNoElementsFoundError, matcherHint } from './matcherHint'; -import { runBrowserBackendOnError } from '../mcp/browser/backend'; +import { runBrowserBackendOnError } from '../mcp/test/browserBackend'; import type { MatcherResult } from './matcherHint'; import type { ExpectMatcherState } from '../../types/test'; diff --git a/packages/playwright/src/matchers/toEqual.ts b/packages/playwright/src/matchers/toEqual.ts index 410049b23abaa..1c4a0bf6c94da 100644 --- a/packages/playwright/src/matchers/toEqual.ts +++ b/packages/playwright/src/matchers/toEqual.ts @@ -18,7 +18,7 @@ import { isRegExp } from 'playwright-core/lib/utils'; import { callLogText, expectTypes } from '../util'; import { matcherHint } from './matcherHint'; -import { runBrowserBackendOnError } from '../mcp/browser/backend'; +import { runBrowserBackendOnError } from '../mcp/test/browserBackend'; import type { MatcherResult } from './matcherHint'; import type { ExpectMatcherState } from '../../types/test'; diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index 65630e2ffc700..04b4c84a74577 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -24,7 +24,7 @@ import { } from './expect'; import { kNoElementsFoundError, matcherHint } from './matcherHint'; import { EXPECTED_COLOR } from '../common/expectBundle'; -import { runBrowserBackendOnError } from '../mcp/browser/backend'; +import { runBrowserBackendOnError } from '../mcp/test/browserBackend'; import type { MatcherResult } from './matcherHint'; import type { ExpectMatcherState } from '../../types/test'; diff --git a/packages/playwright/src/mcp/DEPS.list b/packages/playwright/src/mcp/DEPS.list new file mode 100644 index 0000000000000..37011f717216c --- /dev/null +++ b/packages/playwright/src/mcp/DEPS.list @@ -0,0 +1,8 @@ +[program.ts] +./sdk/ +./browser/ +./extension/ + +[index.ts] +./sdk/ +./browser/ diff --git a/packages/playwright/src/mcp/browser/DEPS.list b/packages/playwright/src/mcp/browser/DEPS.list index 9bdbc0d02a5f8..936611f47be24 100644 --- a/packages/playwright/src/mcp/browser/DEPS.list +++ b/packages/playwright/src/mcp/browser/DEPS.list @@ -1,3 +1,5 @@ [*] +./tools/ ../sdk/ -../../util.ts +../log.ts +../package.ts diff --git a/packages/playwright/src/mcp/browser/actions.d.ts b/packages/playwright/src/mcp/browser/actions.d.ts new file mode 100644 index 0000000000000..e52b420b9041f --- /dev/null +++ b/packages/playwright/src/mcp/browser/actions.d.ts @@ -0,0 +1,172 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type Point = { x: number, y: number }; + +export type ActionName = + 'check' | + 'click' | + 'closePage' | + 'fill' | + 'navigate' | + 'openPage' | + 'press' | + 'select' | + 'uncheck' | + 'setInputFiles' | + 'assertText' | + 'assertValue' | + 'assertChecked' | + 'assertVisible' | + 'assertSnapshot'; + +export type ActionBase = { + name: ActionName, + signals: Signal[], + ariaSnapshot?: string, +}; + +export type ActionWithSelector = ActionBase & { + selector: string, + ref?: string, +}; + +export type ClickAction = ActionWithSelector & { + name: 'click', + button: 'left' | 'middle' | 'right', + modifiers: number, + clickCount: number, + position?: Point, +}; + +export type CheckAction = ActionWithSelector & { + name: 'check', +}; + +export type UncheckAction = ActionWithSelector & { + name: 'uncheck', +}; + +export type FillAction = ActionWithSelector & { + name: 'fill', + text: string, +}; + +export type NavigateAction = ActionBase & { + name: 'navigate', + url: string, +}; + +export type OpenPageAction = ActionBase & { + name: 'openPage', + url: string, +}; + +export type ClosesPageAction = ActionBase & { + name: 'closePage', +}; + +export type PressAction = ActionWithSelector & { + name: 'press', + key: string, + modifiers: number, +}; + +export type SelectAction = ActionWithSelector & { + name: 'select', + options: string[], +}; + +export type SetInputFilesAction = ActionWithSelector & { + name: 'setInputFiles', + files: string[], +}; + +export type AssertTextAction = ActionWithSelector & { + name: 'assertText', + text: string, + substring: boolean, +}; + +export type AssertValueAction = ActionWithSelector & { + name: 'assertValue', + value: string, +}; + +export type AssertCheckedAction = ActionWithSelector & { + name: 'assertChecked', + checked: boolean, +}; + +export type AssertVisibleAction = ActionWithSelector & { + name: 'assertVisible', +}; + +export type AssertSnapshotAction = ActionWithSelector & { + name: 'assertSnapshot', + ariaSnapshot: string, +}; + +export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertSnapshotAction; +export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction | AssertSnapshotAction; +export type PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction; + +// Signals. + +export type BaseSignal = { +}; + +export type NavigationSignal = BaseSignal & { + name: 'navigation', + url: string, +}; + +export type PopupSignal = BaseSignal & { + name: 'popup', + popupAlias: string, +}; + +export type DownloadSignal = BaseSignal & { + name: 'download', + downloadAlias: string, +}; + +export type DialogSignal = BaseSignal & { + name: 'dialog', + dialogAlias: string, +}; + +export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal; + +export type FrameDescription = { + pageGuid: string; + pageAlias: string; + framePath: string[]; +}; + +export type ActionInContext = { + frame: FrameDescription; + description?: string; + action: Action; + startTime: number; + endTime?: number; +}; + +export type SignalInContext = { + frame: FrameDescription; + signal: Signal; + timestamp: number; +}; diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts new file mode 100644 index 0000000000000..6021896cbbe2f --- /dev/null +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -0,0 +1,251 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import crypto from 'crypto'; +import fs from 'fs'; +import net from 'net'; +import path from 'path'; + +import * as playwright from 'playwright-core'; +import { registryDirectory } from 'playwright-core/lib/server/registry/index'; +import { startTraceViewerServer } from 'playwright-core/lib/server'; +import { logUnhandledError, testDebug } from '../log'; +import { outputFile } from './config'; + +import type { FullConfig } from './config'; + +export function contextFactory(config: FullConfig): BrowserContextFactory { + if (config.browser.remoteEndpoint) + return new RemoteContextFactory(config); + if (config.browser.cdpEndpoint) + return new CdpContextFactory(config); + if (config.browser.isolated) + return new IsolatedContextFactory(config); + return new PersistentContextFactory(config); +} + +export type ClientInfo = { name?: string, version?: string, rootPath?: string }; + +export interface BrowserContextFactory { + createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }>; +} + +class BaseContextFactory implements BrowserContextFactory { + readonly config: FullConfig; + private _logName: string; + protected _browserPromise: Promise | undefined; + + constructor(name: string, config: FullConfig) { + this._logName = name; + this.config = config; + } + + protected async _obtainBrowser(clientInfo: ClientInfo): Promise { + if (this._browserPromise) + return this._browserPromise; + testDebug(`obtain browser (${this._logName})`); + this._browserPromise = this._doObtainBrowser(clientInfo); + void this._browserPromise.then(browser => { + browser.on('disconnected', () => { + this._browserPromise = undefined; + }); + }).catch(() => { + this._browserPromise = undefined; + }); + return this._browserPromise; + } + + protected async _doObtainBrowser(clientInfo: ClientInfo): Promise { + throw new Error('Not implemented'); + } + + async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { + testDebug(`create browser context (${this._logName})`); + const browser = await this._obtainBrowser(clientInfo); + const browserContext = await this._doCreateContext(browser); + return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) }; + } + + protected async _doCreateContext(browser: playwright.Browser): Promise { + throw new Error('Not implemented'); + } + + private async _closeBrowserContext(browserContext: playwright.BrowserContext, browser: playwright.Browser) { + testDebug(`close browser context (${this._logName})`); + if (browser.contexts().length === 1) + this._browserPromise = undefined; + await browserContext.close().catch(logUnhandledError); + if (browser.contexts().length === 0) { + testDebug(`close browser (${this._logName})`); + await browser.close().catch(logUnhandledError); + } + } +} + +class IsolatedContextFactory extends BaseContextFactory { + constructor(config: FullConfig) { + super('isolated', config); + } + + protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise { + await injectCdpPort(this.config.browser); + const browserType = playwright[this.config.browser.browserName]; + return browserType.launch({ + tracesDir: await startTraceServer(this.config, clientInfo.rootPath), + ...this.config.browser.launchOptions, + handleSIGINT: false, + handleSIGTERM: false, + }).catch(error => { + if (error.message.includes('Executable doesn\'t exist')) + throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); + throw error; + }); + } + + protected override async _doCreateContext(browser: playwright.Browser): Promise { + return browser.newContext(this.config.browser.contextOptions); + } +} + +class CdpContextFactory extends BaseContextFactory { + constructor(config: FullConfig) { + super('cdp', config); + } + + protected override async _doObtainBrowser(): Promise { + return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint!); + } + + protected override async _doCreateContext(browser: playwright.Browser): Promise { + return this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0]; + } +} + +class RemoteContextFactory extends BaseContextFactory { + constructor(config: FullConfig) { + super('remote', config); + } + + protected override async _doObtainBrowser(): Promise { + const url = new URL(this.config.browser.remoteEndpoint!); + url.searchParams.set('browser', this.config.browser.browserName); + if (this.config.browser.launchOptions) + url.searchParams.set('launch-options', JSON.stringify(this.config.browser.launchOptions)); + return playwright[this.config.browser.browserName].connect(String(url)); + } + + protected override async _doCreateContext(browser: playwright.Browser): Promise { + return browser.newContext(); + } +} + +class PersistentContextFactory implements BrowserContextFactory { + readonly config: FullConfig; + readonly name = 'persistent'; + readonly description = 'Create a new persistent browser context'; + + private _userDataDirs = new Set(); + + constructor(config: FullConfig) { + this.config = config; + } + + async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { + await injectCdpPort(this.config.browser); + testDebug('create browser context (persistent)'); + const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath); + const tracesDir = await startTraceServer(this.config, clientInfo.rootPath); + + this._userDataDirs.add(userDataDir); + testDebug('lock user data dir', userDataDir); + + const browserType = playwright[this.config.browser.browserName]; + for (let i = 0; i < 5; i++) { + try { + const browserContext = await browserType.launchPersistentContext(userDataDir, { + tracesDir, + ...this.config.browser.launchOptions, + ...this.config.browser.contextOptions, + handleSIGINT: false, + handleSIGTERM: false, + }); + const close = () => this._closeBrowserContext(browserContext, userDataDir); + return { browserContext, close }; + } catch (error: any) { + if (error.message.includes('Executable doesn\'t exist')) + throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); + if (error.message.includes('ProcessSingleton') || error.message.includes('Invalid URL')) { + // User data directory is already in use, try again. + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + throw error; + } + } + throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); + } + + private async _closeBrowserContext(browserContext: playwright.BrowserContext, userDataDir: string) { + testDebug('close browser context (persistent)'); + testDebug('release user data dir', userDataDir); + await browserContext.close().catch(() => {}); + this._userDataDirs.delete(userDataDir); + testDebug('close browser context complete (persistent)'); + } + + private async _createUserDataDir(rootPath: string | undefined) { + const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? registryDirectory; + const browserToken = this.config.browser.launchOptions?.channel ?? this.config.browser?.browserName; + // Hesitant putting hundreds of files into the user's workspace, so using it for hashing instead. + const rootPathToken = rootPath ? `-${createHash(rootPath)}` : ''; + const result = path.join(dir, `mcp-${browserToken}${rootPathToken}`); + await fs.promises.mkdir(result, { recursive: true }); + return result; + } +} + +async function injectCdpPort(browserConfig: FullConfig['browser']) { + if (browserConfig.browserName === 'chromium') + (browserConfig.launchOptions as any).cdpPort = await findFreePort(); +} + +async function findFreePort(): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + server.close(() => resolve(port)); + }); + server.on('error', reject); + }); +} + +async function startTraceServer(config: FullConfig, rootPath: string | undefined): Promise { + if (!config.saveTrace) + return undefined; + + const tracesDir = await outputFile(config, rootPath, `traces-${Date.now()}`); + const server = await startTraceViewerServer(); + const urlPrefix = server.urlPrefix('human-readable'); + const url = urlPrefix + '/trace/index.html?trace=' + tracesDir + '/trace.json'; + // eslint-disable-next-line no-console + console.error('\nTrace viewer listening on ' + url); + return tracesDir; +} + +function createHash(data: string): string { + return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); +} diff --git a/packages/playwright/src/mcp/browser/browserServerBackend.ts b/packages/playwright/src/mcp/browser/browserServerBackend.ts new file mode 100644 index 0000000000000..c773786afaeb2 --- /dev/null +++ b/packages/playwright/src/mcp/browser/browserServerBackend.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fileURLToPath } from 'url'; +import { FullConfig } from './config'; +import { Context } from './context'; +import { logUnhandledError } from '../log'; +import { Response } from './response'; +import { SessionLog } from './sessionLog'; +import { filteredTools } from './tools'; +import { toMcpTool } from '../sdk/tool'; + +import type { Tool } from './tools/tool'; +import type { BrowserContextFactory } from './browserContextFactory'; +import type * as mcpServer from '../sdk/server'; +import type { ServerBackend } from '../sdk/server'; + +export class BrowserServerBackend implements ServerBackend { + private _tools: Tool[]; + private _context: Context | undefined; + private _sessionLog: SessionLog | undefined; + private _config: FullConfig; + private _browserContextFactory: BrowserContextFactory; + + constructor(config: FullConfig, factory: BrowserContextFactory) { + this._config = config; + this._browserContextFactory = factory; + this._tools = filteredTools(config); + } + + async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise { + let rootPath: string | undefined; + if (roots.length > 0) { + const firstRootUri = roots[0]?.uri; + const url = firstRootUri ? new URL(firstRootUri) : undefined; + rootPath = url ? fileURLToPath(url) : undefined; + } + this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, rootPath) : undefined; + this._context = new Context({ + tools: this._tools, + config: this._config, + browserContextFactory: this._browserContextFactory, + sessionLog: this._sessionLog, + clientInfo: { ...clientVersion, rootPath }, + }); + } + + async listTools(): Promise { + return this._tools.map(tool => toMcpTool(tool.schema)); + } + + async callTool(name: string, rawArguments: mcpServer.CallToolRequest['params']['arguments']) { + const tool = this._tools.find(tool => tool.schema.name === name)!; + if (!tool) + throw new Error(`Tool "${name}" not found`); + const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {}); + const context = this._context!; + const response = new Response(context, name, parsedArguments); + context.setRunningTool(name); + try { + await tool.handle(context, parsedArguments, response); + await response.finish(); + this._sessionLog?.logResponse(response); + } catch (error: any) { + response.addError(String(error)); + } finally { + context.setRunningTool(undefined); + } + return response.serialize(); + } + + serverClosed() { + void this._context?.dispose().catch(logUnhandledError); + } +} diff --git a/packages/playwright/src/mcp/browser/codegen.ts b/packages/playwright/src/mcp/browser/codegen.ts new file mode 100644 index 0000000000000..a3bc8d0e02b27 --- /dev/null +++ b/packages/playwright/src/mcp/browser/codegen.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// adapted from: +// - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/utils/isomorphic/stringUtils.ts +// - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/server/codegen/javascript.ts + +// NOTE: this function should not be used to escape any selectors. +export function escapeWithQuotes(text: string, char: string = '\'') { + const stringified = JSON.stringify(text); + const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"'); + if (char === '\'') + return char + escapedText.replace(/[']/g, '\\\'') + char; + if (char === '"') + return char + escapedText.replace(/["]/g, '\\"') + char; + if (char === '`') + return char + escapedText.replace(/[`]/g, '\\`') + char; + throw new Error('Invalid escape char'); +} + +export function quote(text: string) { + return escapeWithQuotes(text, '\''); +} + +export function formatObject(value: any, indent = ' '): string { + if (typeof value === 'string') + return quote(value); + if (Array.isArray(value)) + return `[${value.map(o => formatObject(o)).join(', ')}]`; + if (typeof value === 'object') { + const keys = Object.keys(value).filter(key => value[key] !== undefined).sort(); + if (!keys.length) + return '{}'; + const tokens: string[] = []; + for (const key of keys) + tokens.push(`${key}: ${formatObject(value[key])}`); + return `{\n${indent}${tokens.join(`,\n${indent}`)}\n}`; + } + return String(value); +} diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts new file mode 100644 index 0000000000000..1d26b13bb4f52 --- /dev/null +++ b/packages/playwright/src/mcp/browser/config.ts @@ -0,0 +1,327 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { devices } from 'playwright-core'; + +import type * as playwright from '../../../types/test'; +import type { Config, ToolCapability } from '../config'; + +export type CLIOptions = { + allowedOrigins?: string[]; + blockedOrigins?: string[]; + blockServiceWorkers?: boolean; + browser?: string; + caps?: string[]; + cdpEndpoint?: string; + config?: string; + device?: string; + executablePath?: string; + headless?: boolean; + host?: string; + ignoreHttpsErrors?: boolean; + isolated?: boolean; + imageResponses?: 'allow' | 'omit'; + sandbox?: boolean; + outputDir?: string; + port?: number; + proxyBypass?: string; + proxyServer?: string; + saveSession?: boolean; + saveTrace?: boolean; + storageState?: string; + userAgent?: string; + userDataDir?: string; + viewportSize?: string; +}; + +const defaultConfig: FullConfig = { + browser: { + browserName: 'chromium', + launchOptions: { + channel: 'chrome', + headless: os.platform() === 'linux' && !process.env.DISPLAY, + chromiumSandbox: true, + }, + contextOptions: { + viewport: null, + }, + }, + network: { + allowedOrigins: undefined, + blockedOrigins: undefined, + }, + server: {}, + saveTrace: false, +}; + +type BrowserUserConfig = NonNullable; + +export type FullConfig = Config & { + browser: Omit & { + browserName: 'chromium' | 'firefox' | 'webkit'; + launchOptions: NonNullable; + contextOptions: NonNullable; + }, + network: NonNullable, + saveTrace: boolean; + server: NonNullable, +}; + +export async function resolveConfig(config: Config): Promise { + return mergeConfig(defaultConfig, config); +} + +export async function resolveCLIConfig(cliOptions: CLIOptions): Promise { + const configInFile = await loadConfig(cliOptions.config); + const envOverrides = configFromEnv(); + const cliOverrides = configFromCLIOptions(cliOptions); + let result = defaultConfig; + result = mergeConfig(result, configInFile); + result = mergeConfig(result, envOverrides); + result = mergeConfig(result, cliOverrides); + return result; +} + +export function configFromCLIOptions(cliOptions: CLIOptions): Config { + let browserName: 'chromium' | 'firefox' | 'webkit' | undefined; + let channel: string | undefined; + switch (cliOptions.browser) { + case 'chrome': + case 'chrome-beta': + case 'chrome-canary': + case 'chrome-dev': + case 'chromium': + case 'msedge': + case 'msedge-beta': + case 'msedge-canary': + case 'msedge-dev': + browserName = 'chromium'; + channel = cliOptions.browser; + break; + case 'firefox': + browserName = 'firefox'; + break; + case 'webkit': + browserName = 'webkit'; + break; + } + + // Launch options + const launchOptions: playwright.LaunchOptions = { + channel, + executablePath: cliOptions.executablePath, + headless: cliOptions.headless, + }; + + // --no-sandbox was passed, disable the sandbox + if (cliOptions.sandbox === false) + launchOptions.chromiumSandbox = false; + + if (cliOptions.proxyServer) { + launchOptions.proxy = { + server: cliOptions.proxyServer + }; + if (cliOptions.proxyBypass) + launchOptions.proxy.bypass = cliOptions.proxyBypass; + } + + if (cliOptions.device && cliOptions.cdpEndpoint) + throw new Error('Device emulation is not supported with cdpEndpoint.'); + + // Context options + const contextOptions: playwright.BrowserContextOptions = cliOptions.device ? devices[cliOptions.device] : {}; + if (cliOptions.storageState) + contextOptions.storageState = cliOptions.storageState; + + if (cliOptions.userAgent) + contextOptions.userAgent = cliOptions.userAgent; + + if (cliOptions.viewportSize) { + try { + const [width, height] = cliOptions.viewportSize.split(',').map(n => +n); + if (isNaN(width) || isNaN(height)) + throw new Error('bad values'); + contextOptions.viewport = { width, height }; + } catch (e) { + throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"'); + } + } + + if (cliOptions.ignoreHttpsErrors) + contextOptions.ignoreHTTPSErrors = true; + + if (cliOptions.blockServiceWorkers) + contextOptions.serviceWorkers = 'block'; + + const result: Config = { + browser: { + browserName, + isolated: cliOptions.isolated, + userDataDir: cliOptions.userDataDir, + launchOptions, + contextOptions, + cdpEndpoint: cliOptions.cdpEndpoint, + }, + server: { + port: cliOptions.port, + host: cliOptions.host, + }, + capabilities: cliOptions.caps as ToolCapability[], + network: { + allowedOrigins: cliOptions.allowedOrigins, + blockedOrigins: cliOptions.blockedOrigins, + }, + saveSession: cliOptions.saveSession, + saveTrace: cliOptions.saveTrace, + outputDir: cliOptions.outputDir, + imageResponses: cliOptions.imageResponses, + }; + + return result; +} + +function configFromEnv(): Config { + const options: CLIOptions = {}; + options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS); + options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS); + options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS); + options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER); + options.caps = commaSeparatedList(process.env.PLAYWRIGHT_MCP_CAPS); + options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT); + options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG); + options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE); + options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH); + options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS); + options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST); + options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS); + options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED); + if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === 'omit') + options.imageResponses = 'omit'; + options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX); + options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR); + options.port = envToNumber(process.env.PLAYWRIGHT_MCP_PORT); + options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS); + options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER); + options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE); + options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE); + options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT); + options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR); + options.viewportSize = envToString(process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE); + return configFromCLIOptions(options); +} + +async function loadConfig(configFile: string | undefined): Promise { + if (!configFile) + return {}; + + try { + return JSON.parse(await fs.promises.readFile(configFile, 'utf8')); + } catch (error) { + throw new Error(`Failed to load config file: ${configFile}, ${error}`); + } +} + +export async function outputFile(config: FullConfig, rootPath: string | undefined, name: string): Promise { + const outputDir = config.outputDir + ?? (rootPath ? path.join(rootPath, '.playwright-mcp') : undefined) + ?? path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString())); + + await fs.promises.mkdir(outputDir, { recursive: true }); + const fileName = sanitizeForFilePath(name); + return path.join(outputDir, fileName); +} + +function pickDefined(obj: T | undefined): Partial { + return Object.fromEntries( + Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined) + ) as Partial; +} + +function mergeConfig(base: FullConfig, overrides: Config): FullConfig { + const browser: FullConfig['browser'] = { + ...pickDefined(base.browser), + ...pickDefined(overrides.browser), + browserName: overrides.browser?.browserName ?? base.browser?.browserName ?? 'chromium', + isolated: overrides.browser?.isolated ?? base.browser?.isolated ?? false, + launchOptions: { + ...pickDefined(base.browser?.launchOptions), + ...pickDefined(overrides.browser?.launchOptions), + ...{ assistantMode: true }, + }, + contextOptions: { + ...pickDefined(base.browser?.contextOptions), + ...pickDefined(overrides.browser?.contextOptions), + }, + }; + + if (browser.browserName !== 'chromium' && browser.launchOptions) + delete browser.launchOptions.channel; + + return { + ...pickDefined(base), + ...pickDefined(overrides), + browser, + network: { + ...pickDefined(base.network), + ...pickDefined(overrides.network), + }, + server: { + ...pickDefined(base.server), + ...pickDefined(overrides.server), + }, + } as FullConfig; +} + +export function semicolonSeparatedList(value: string | undefined): string[] | undefined { + if (!value) + return undefined; + return value.split(';').map(v => v.trim()); +} + +export function commaSeparatedList(value: string | undefined): string[] | undefined { + if (!value) + return undefined; + return value.split(',').map(v => v.trim()); +} + +function envToNumber(value: string | undefined): number | undefined { + if (!value) + return undefined; + return +value; +} + +function envToBoolean(value: string | undefined): boolean | undefined { + if (value === 'true' || value === '1') + return true; + if (value === 'false' || value === '0') + return false; + return undefined; +} + +function envToString(value: string | undefined): string | undefined { + return value ? value.trim() : undefined; +} + +function sanitizeForFilePath(s: string) { + const sanitize = (s: string) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-'); + const separator = s.lastIndexOf('.'); + if (separator === -1) + return sanitize(s); + return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1)); +} diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts new file mode 100644 index 0000000000000..c886c88a80bcc --- /dev/null +++ b/packages/playwright/src/mcp/browser/context.ts @@ -0,0 +1,276 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { debug } from 'playwright-core/lib/utilsBundle'; + +import { logUnhandledError } from '../log'; +import { Tab } from './tab'; +import { outputFile } from './config'; + +import type * as playwright from '../../../types/test'; +import type { FullConfig } from './config'; +import type { Tool } from './tools/tool'; +import type { BrowserContextFactory, ClientInfo } from './browserContextFactory'; +import type * as actions from './actions'; +import type { SessionLog } from './sessionLog'; + +const testDebug = debug('pw:mcp:test'); + +type ContextOptions = { + tools: Tool[]; + config: FullConfig; + browserContextFactory: BrowserContextFactory; + sessionLog: SessionLog | undefined; + clientInfo: ClientInfo; +}; + +export class Context { + readonly tools: Tool[]; + readonly config: FullConfig; + readonly sessionLog: SessionLog | undefined; + readonly options: ContextOptions; + private _browserContextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> | undefined; + private _browserContextFactory: BrowserContextFactory; + private _tabs: Tab[] = []; + private _currentTab: Tab | undefined; + private _clientInfo: ClientInfo; + + private static _allContexts: Set = new Set(); + private _closeBrowserContextPromise: Promise | undefined; + private _runningToolName: string | undefined; + private _abortController = new AbortController(); + + constructor(options: ContextOptions) { + this.tools = options.tools; + this.config = options.config; + this.sessionLog = options.sessionLog; + this.options = options; + this._browserContextFactory = options.browserContextFactory; + this._clientInfo = options.clientInfo; + testDebug('create context'); + Context._allContexts.add(this); + } + + static async disposeAll() { + await Promise.all([...Context._allContexts].map(context => context.dispose())); + } + + tabs(): Tab[] { + return this._tabs; + } + + currentTab(): Tab | undefined { + return this._currentTab; + } + + currentTabOrDie(): Tab { + if (!this._currentTab) + throw new Error('No open pages available. Use the "browser_navigate" tool to navigate to a page first.'); + return this._currentTab; + } + + async newTab(): Promise { + const { browserContext } = await this._ensureBrowserContext(); + const page = await browserContext.newPage(); + this._currentTab = this._tabs.find(t => t.page === page)!; + return this._currentTab; + } + + async selectTab(index: number) { + const tab = this._tabs[index]; + if (!tab) + throw new Error(`Tab ${index} not found`); + await tab.page.bringToFront(); + this._currentTab = tab; + return tab; + } + + async ensureTab(): Promise { + const { browserContext } = await this._ensureBrowserContext(); + if (!this._currentTab) + await browserContext.newPage(); + return this._currentTab!; + } + + async closeTab(index: number | undefined): Promise { + const tab = index === undefined ? this._currentTab : this._tabs[index]; + if (!tab) + throw new Error(`Tab ${index} not found`); + const url = tab.page.url(); + await tab.page.close(); + return url; + } + + async outputFile(name: string): Promise { + return outputFile(this.config, this._clientInfo.rootPath, name); + } + + private _onPageCreated(page: playwright.Page) { + const tab = new Tab(this, page, tab => this._onPageClosed(tab)); + this._tabs.push(tab); + if (!this._currentTab) + this._currentTab = tab; + } + + private _onPageClosed(tab: Tab) { + const index = this._tabs.indexOf(tab); + if (index === -1) + return; + this._tabs.splice(index, 1); + + if (this._currentTab === tab) + this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)]; + if (!this._tabs.length) + void this.closeBrowserContext(); + } + + async closeBrowserContext() { + if (!this._closeBrowserContextPromise) + this._closeBrowserContextPromise = this._closeBrowserContextImpl().catch(logUnhandledError); + await this._closeBrowserContextPromise; + this._closeBrowserContextPromise = undefined; + } + + isRunningTool() { + return this._runningToolName !== undefined; + } + + setRunningTool(name: string | undefined) { + this._runningToolName = name; + } + + private async _closeBrowserContextImpl() { + if (!this._browserContextPromise) + return; + + testDebug('close context'); + + const promise = this._browserContextPromise; + this._browserContextPromise = undefined; + + await promise.then(async ({ browserContext, close }) => { + if (this.config.saveTrace) + await browserContext.tracing.stop(); + await close(); + }); + } + + async dispose() { + this._abortController.abort('MCP context disposed'); + await this.closeBrowserContext(); + Context._allContexts.delete(this); + } + + private async _setupRequestInterception(context: playwright.BrowserContext) { + if (this.config.network?.allowedOrigins?.length) { + await context.route('**', route => route.abort('blockedbyclient')); + + for (const origin of this.config.network.allowedOrigins) + await context.route(`*://${origin}/**`, route => route.continue()); + } + + if (this.config.network?.blockedOrigins?.length) { + for (const origin of this.config.network.blockedOrigins) + await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient')); + } + } + + private _ensureBrowserContext() { + if (!this._browserContextPromise) { + this._browserContextPromise = this._setupBrowserContext(); + this._browserContextPromise.catch(() => { + this._browserContextPromise = undefined; + }); + } + return this._browserContextPromise; + } + + private async _setupBrowserContext(): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { + if (this._closeBrowserContextPromise) + throw new Error('Another browser context is being closed.'); + // TODO: move to the browser context factory to make it based on isolation mode. + const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName); + const { browserContext } = result; + await this._setupRequestInterception(browserContext); + if (this.sessionLog) + await InputRecorder.create(this, browserContext); + for (const page of browserContext.pages()) + this._onPageCreated(page); + browserContext.on('page', page => this._onPageCreated(page)); + if (this.config.saveTrace) { + await browserContext.tracing.start({ + name: 'trace', + screenshots: false, + snapshots: true, + sources: false, + }); + } + return result; + } +} + +export class InputRecorder { + private _context: Context; + private _browserContext: playwright.BrowserContext; + + private constructor(context: Context, browserContext: playwright.BrowserContext) { + this._context = context; + this._browserContext = browserContext; + } + + static async create(context: Context, browserContext: playwright.BrowserContext) { + const recorder = new InputRecorder(context, browserContext); + await recorder._initialize(); + return recorder; + } + + private async _initialize() { + const sessionLog = this._context.sessionLog!; + await (this._browserContext as any)._enableRecorder({ + mode: 'recording', + recorderMode: 'api', + }, { + actionAdded: (page: playwright.Page, data: actions.ActionInContext, code: string) => { + if (this._context.isRunningTool()) + return; + const tab = Tab.forPage(page); + if (tab) + sessionLog.logUserAction(data.action, tab, code, false); + }, + actionUpdated: (page: playwright.Page, data: actions.ActionInContext, code: string) => { + if (this._context.isRunningTool()) + return; + const tab = Tab.forPage(page); + if (tab) + sessionLog.logUserAction(data.action, tab, code, true); + }, + signalAdded: (page: playwright.Page, data: actions.SignalInContext) => { + if (this._context.isRunningTool()) + return; + if (data.signal.name !== 'navigation') + return; + const tab = Tab.forPage(page); + const navigateAction: actions.Action = { + name: 'navigate', + url: data.signal.url, + signals: [], + }; + if (tab) + sessionLog.logUserAction(navigateAction, tab, `await page.goto('${data.signal.url}');`, false); + }, + }); + } +} diff --git a/packages/playwright/src/mcp/browser/response.ts b/packages/playwright/src/mcp/browser/response.ts new file mode 100644 index 0000000000000..3dc95ab826284 --- /dev/null +++ b/packages/playwright/src/mcp/browser/response.ts @@ -0,0 +1,201 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderModalStates } from './tab'; + +import type { Tab, TabSnapshot } from './tab'; +import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; +import type { Context } from './context'; + +export class Response { + private _result: string[] = []; + private _code: string[] = []; + private _images: { contentType: string, data: Buffer }[] = []; + private _context: Context; + private _includeSnapshot = false; + private _includeTabs = false; + private _tabSnapshot: TabSnapshot | undefined; + + readonly toolName: string; + readonly toolArgs: Record; + private _isError: boolean | undefined; + + constructor(context: Context, toolName: string, toolArgs: Record) { + this._context = context; + this.toolName = toolName; + this.toolArgs = toolArgs; + } + + addResult(result: string) { + this._result.push(result); + } + + addError(error: string) { + this._result.push(error); + this._isError = true; + } + + isError() { + return this._isError; + } + + result() { + return this._result.join('\n'); + } + + addCode(code: string) { + this._code.push(code); + } + + code() { + return this._code.join('\n'); + } + + addImage(image: { contentType: string, data: Buffer }) { + this._images.push(image); + } + + images() { + return this._images; + } + + setIncludeSnapshot() { + this._includeSnapshot = true; + } + + setIncludeTabs() { + this._includeTabs = true; + } + + async finish() { + // All the async snapshotting post-action is happening here. + // Everything below should race against modal states. + if (this._includeSnapshot && this._context.currentTab()) + this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot(); + for (const tab of this._context.tabs()) + await tab.updateTitle(); + } + + tabSnapshot(): TabSnapshot | undefined { + return this._tabSnapshot; + } + + serialize(): { content: (TextContent | ImageContent)[], isError?: boolean } { + const response: string[] = []; + + // Start with command result. + if (this._result.length) { + response.push('### Result'); + response.push(this._result.join('\n')); + response.push(''); + } + + // Add code if it exists. + if (this._code.length) { + response.push(`### Ran Playwright code +\`\`\`js +${this._code.join('\n')} +\`\`\``); + response.push(''); + } + + // List browser tabs. + if (this._includeSnapshot || this._includeTabs) + response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs)); + + // Add snapshot if provided. + if (this._tabSnapshot?.modalStates.length) { + response.push(...renderModalStates(this._context, this._tabSnapshot.modalStates)); + response.push(''); + } else if (this._tabSnapshot) { + response.push(renderTabSnapshot(this._tabSnapshot)); + response.push(''); + } + + // Main response part + const content: (TextContent | ImageContent)[] = [ + { type: 'text', text: response.join('\n') }, + ]; + + // Image attachments. + if (this._context.config.imageResponses !== 'omit') { + for (const image of this._images) + content.push({ type: 'image', data: image.data.toString('base64'), mimeType: image.contentType }); + } + + return { content, isError: this._isError }; + } +} + +function renderTabSnapshot(tabSnapshot: TabSnapshot): string { + const lines: string[] = []; + + if (tabSnapshot.consoleMessages.length) { + lines.push(`### New console messages`); + for (const message of tabSnapshot.consoleMessages) + lines.push(`- ${trim(message.toString(), 100)}`); + lines.push(''); + } + + if (tabSnapshot.downloads.length) { + lines.push(`### Downloads`); + for (const entry of tabSnapshot.downloads) { + if (entry.finished) + lines.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`); + else + lines.push(`- Downloading file ${entry.download.suggestedFilename()} ...`); + } + lines.push(''); + } + + lines.push(`### Page state`); + lines.push(`- Page URL: ${tabSnapshot.url}`); + lines.push(`- Page Title: ${tabSnapshot.title}`); + lines.push(`- Page Snapshot:`); + lines.push('```yaml'); + lines.push(tabSnapshot.ariaSnapshot); + lines.push('```'); + + return lines.join('\n'); +} + +function renderTabsMarkdown(tabs: Tab[], force: boolean = false): string[] { + if (tabs.length === 1 && !force) + return []; + + if (!tabs.length) { + return [ + '### Open tabs', + 'No open tabs. Use the "browser_navigate" tool to navigate to a page first.', + '', + ]; + } + + const lines: string[] = ['### Open tabs']; + for (let i = 0; i < tabs.length; i++) { + const tab = tabs[i]; + const current = tab.isCurrentTab() ? ' (current)' : ''; + lines.push(`- ${i}:${current} [${tab.lastTitle()}] (${tab.page.url()})`); + } + lines.push(''); + return lines; +} + +function trim(text: string, maxLength: number) { + if (text.length <= maxLength) + return text; + return text.slice(0, maxLength) + '...'; +} diff --git a/packages/playwright/src/mcp/browser/sessionLog.ts b/packages/playwright/src/mcp/browser/sessionLog.ts new file mode 100644 index 0000000000000..7e80087454eb5 --- /dev/null +++ b/packages/playwright/src/mcp/browser/sessionLog.ts @@ -0,0 +1,176 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; + +import { Response } from './response'; +import { logUnhandledError } from '../log'; +import { outputFile } from './config'; + +import type { FullConfig } from './config'; +import type * as actions from './actions'; +import type { Tab, TabSnapshot } from './tab'; + +type LogEntry = { + timestamp: number; + toolCall?: { + toolName: string; + toolArgs: Record; + result: string; + isError?: boolean; + }; + userAction?: actions.Action; + code: string; + tabSnapshot?: TabSnapshot; +}; + +export class SessionLog { + private _folder: string; + private _file: string; + private _ordinal = 0; + private _pendingEntries: LogEntry[] = []; + private _sessionFileQueue = Promise.resolve(); + private _flushEntriesTimeout: NodeJS.Timeout | undefined; + + constructor(sessionFolder: string) { + this._folder = sessionFolder; + this._file = path.join(this._folder, 'session.md'); + } + + static async create(config: FullConfig, rootPath: string | undefined): Promise { + const sessionFolder = await outputFile(config, rootPath, `session-${Date.now()}`); + await fs.promises.mkdir(sessionFolder, { recursive: true }); + // eslint-disable-next-line no-console + console.error(`Session: ${sessionFolder}`); + return new SessionLog(sessionFolder); + } + + logResponse(response: Response) { + const entry: LogEntry = { + timestamp: performance.now(), + toolCall: { + toolName: response.toolName, + toolArgs: response.toolArgs, + result: response.result(), + isError: response.isError(), + }, + code: response.code(), + tabSnapshot: response.tabSnapshot(), + }; + this._appendEntry(entry); + } + + logUserAction(action: actions.Action, tab: Tab, code: string, isUpdate: boolean) { + code = code.trim(); + if (isUpdate) { + const lastEntry = this._pendingEntries[this._pendingEntries.length - 1]; + if (lastEntry.userAction?.name === action.name) { + lastEntry.userAction = action; + lastEntry.code = code; + return; + } + } + if (action.name === 'navigate') { + // Already logged at this location. + const lastEntry = this._pendingEntries[this._pendingEntries.length - 1]; + if (lastEntry?.tabSnapshot?.url === action.url) + return; + } + const entry: LogEntry = { + timestamp: performance.now(), + userAction: action, + code, + tabSnapshot: { + url: tab.page.url(), + title: '', + ariaSnapshot: action.ariaSnapshot || '', + modalStates: [], + consoleMessages: [], + downloads: [], + }, + }; + this._appendEntry(entry); + } + + private _appendEntry(entry: LogEntry) { + this._pendingEntries.push(entry); + if (this._flushEntriesTimeout) + clearTimeout(this._flushEntriesTimeout); + this._flushEntriesTimeout = setTimeout(() => this._flushEntries(), 1000); + } + + private async _flushEntries() { + clearTimeout(this._flushEntriesTimeout); + const entries = this._pendingEntries; + this._pendingEntries = []; + const lines: string[] = ['']; + + for (const entry of entries) { + const ordinal = (++this._ordinal).toString().padStart(3, '0'); + if (entry.toolCall) { + lines.push( + `### Tool call: ${entry.toolCall.toolName}`, + `- Args`, + '```json', + JSON.stringify(entry.toolCall.toolArgs, null, 2), + '```', + ); + if (entry.toolCall.result) { + lines.push( + entry.toolCall.isError ? `- Error` : `- Result`, + '```', + entry.toolCall.result, + '```', + ); + } + } + + if (entry.userAction) { + const actionData = { ...entry.userAction } as any; + delete actionData.ariaSnapshot; + delete actionData.selector; + delete actionData.signals; + + lines.push( + `### User action: ${entry.userAction.name}`, + `- Args`, + '```json', + JSON.stringify(actionData, null, 2), + '```', + ); + } + + if (entry.code) { + lines.push( + `- Code`, + '```js', + entry.code, + '```'); + } + + if (entry.tabSnapshot) { + const fileName = `${ordinal}.snapshot.yml`; + fs.promises.writeFile(path.join(this._folder, fileName), entry.tabSnapshot.ariaSnapshot).catch(logUnhandledError); + lines.push(`- Snapshot: ${fileName}`); + } + + lines.push('', ''); + } + + this._sessionFileQueue = this._sessionFileQueue.then(() => fs.promises.appendFile(this._file, lines.join('\n'))); + } +} diff --git a/packages/playwright/src/mcp/browser/tab.ts b/packages/playwright/src/mcp/browser/tab.ts new file mode 100644 index 0000000000000..8904604e8f207 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tab.ts @@ -0,0 +1,314 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventEmitter } from 'events'; +import * as playwright from 'playwright-core'; +import { ManualPromise } from 'playwright-core/lib/utils'; + +import { callOnPageNoTrace, waitForCompletion } from './tools/utils'; +import { logUnhandledError } from '../log'; +import { ModalState } from './tools/tool'; + +import type { Context } from './context'; + +type PageEx = playwright.Page & { + _snapshotForAI: () => Promise; +}; + +export const TabEvents = { + modalState: 'modalState' +}; + +export type TabEventsInterface = { + [TabEvents.modalState]: [modalState: ModalState]; +}; + +export type TabSnapshot = { + url: string; + title: string; + ariaSnapshot: string; + modalStates: ModalState[]; + consoleMessages: ConsoleMessage[]; + downloads: { download: playwright.Download, finished: boolean, outputFile: string }[]; +}; + +export class Tab extends EventEmitter { + readonly context: Context; + readonly page: playwright.Page; + private _lastTitle = 'about:blank'; + private _consoleMessages: ConsoleMessage[] = []; + private _recentConsoleMessages: ConsoleMessage[] = []; + private _requests: Map = new Map(); + private _onPageClose: (tab: Tab) => void; + private _modalStates: ModalState[] = []; + private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = []; + + constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) { + super(); + this.context = context; + this.page = page; + this._onPageClose = onPageClose; + page.on('console', event => this._handleConsoleMessage(messageToConsoleMessage(event))); + page.on('pageerror', error => this._handleConsoleMessage(pageErrorToConsoleMessage(error))); + page.on('request', request => this._requests.set(request, null)); + page.on('response', response => this._requests.set(response.request(), response)); + page.on('close', () => this._onClose()); + page.on('filechooser', chooser => { + this.setModalState({ + type: 'fileChooser', + description: 'File chooser', + fileChooser: chooser, + }); + }); + page.on('dialog', dialog => this._dialogShown(dialog)); + page.on('download', download => { + void this._downloadStarted(download); + }); + page.setDefaultNavigationTimeout(60000); + page.setDefaultTimeout(5000); + (page as any)[tabSymbol] = this; + } + + static forPage(page: playwright.Page): Tab | undefined { + return (page as any)[tabSymbol]; + } + + modalStates(): ModalState[] { + return this._modalStates; + } + + setModalState(modalState: ModalState) { + this._modalStates.push(modalState); + this.emit(TabEvents.modalState, modalState); + } + + clearModalState(modalState: ModalState) { + this._modalStates = this._modalStates.filter(state => state !== modalState); + } + + modalStatesMarkdown(): string[] { + return renderModalStates(this.context, this.modalStates()); + } + + private _dialogShown(dialog: playwright.Dialog) { + this.setModalState({ + type: 'dialog', + description: `"${dialog.type()}" dialog with message "${dialog.message()}"`, + dialog, + }); + } + + private async _downloadStarted(download: playwright.Download) { + const entry = { + download, + finished: false, + outputFile: await this.context.outputFile(download.suggestedFilename()) + }; + this._downloads.push(entry); + await download.saveAs(entry.outputFile); + entry.finished = true; + } + + private _clearCollectedArtifacts() { + this._consoleMessages.length = 0; + this._recentConsoleMessages.length = 0; + this._requests.clear(); + } + + private _handleConsoleMessage(message: ConsoleMessage) { + this._consoleMessages.push(message); + this._recentConsoleMessages.push(message); + } + + private _onClose() { + this._clearCollectedArtifacts(); + this._onPageClose(this); + } + + async updateTitle() { + await this._raceAgainstModalStates(async () => { + this._lastTitle = await callOnPageNoTrace(this.page, page => page.title()); + }); + } + + lastTitle(): string { + return this._lastTitle; + } + + isCurrentTab(): boolean { + return this === this.context.currentTab(); + } + + async waitForLoadState(state: 'load', options?: { timeout?: number }): Promise { + await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(logUnhandledError)); + } + + async navigate(url: string) { + this._clearCollectedArtifacts(); + + const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(logUnhandledError)); + try { + await this.page.goto(url, { waitUntil: 'domcontentloaded' }); + } catch (_e: unknown) { + const e = _e as Error; + const mightBeDownload = + e.message.includes('net::ERR_ABORTED') // chromium + || e.message.includes('Download is starting'); // firefox + webkit + if (!mightBeDownload) + throw e; + // on chromium, the download event is fired *after* page.goto rejects, so we wait a lil bit + const download = await Promise.race([ + downloadEvent, + new Promise(resolve => setTimeout(resolve, 3000)), + ]); + if (!download) + throw e; + // Make sure other "download" listeners are notified first. + await new Promise(resolve => setTimeout(resolve, 500)); + return; + } + + // Cap load event to 5 seconds, the page is operational at this point. + await this.waitForLoadState('load', { timeout: 5000 }); + } + + consoleMessages(): ConsoleMessage[] { + return this._consoleMessages; + } + + requests(): Map { + return this._requests; + } + + async captureSnapshot(): Promise { + let tabSnapshot: TabSnapshot | undefined; + const modalStates = await this._raceAgainstModalStates(async () => { + const snapshot = await (this.page as PageEx)._snapshotForAI(); + tabSnapshot = { + url: this.page.url(), + title: await this.page.title(), + ariaSnapshot: snapshot, + modalStates: [], + consoleMessages: [], + downloads: this._downloads, + }; + }); + if (tabSnapshot) { + // Assign console message late so that we did not lose any to modal state. + tabSnapshot.consoleMessages = this._recentConsoleMessages; + this._recentConsoleMessages = []; + } + return tabSnapshot ?? { + url: this.page.url(), + title: '', + ariaSnapshot: '', + modalStates, + consoleMessages: [], + downloads: [], + }; + } + + private _javaScriptBlocked(): boolean { + return this._modalStates.some(state => state.type === 'dialog'); + } + + private async _raceAgainstModalStates(action: () => Promise): Promise { + if (this.modalStates().length) + return this.modalStates(); + + const promise = new ManualPromise(); + const listener = (modalState: ModalState) => promise.resolve([modalState]); + this.once(TabEvents.modalState, listener); + + return await Promise.race([ + action().then(() => { + this.off(TabEvents.modalState, listener); + return []; + }), + promise, + ]); + } + + async waitForCompletion(callback: () => Promise) { + await this._raceAgainstModalStates(() => waitForCompletion(this, callback)); + } + + async refLocator(params: { element: string, ref: string }): Promise { + return (await this.refLocators([params]))[0]; + } + + async refLocators(params: { element: string, ref: string }[]): Promise { + const snapshot = await (this.page as PageEx)._snapshotForAI(); + return params.map(param => { + if (!snapshot.includes(`[ref=${param.ref}]`)) + throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`); + return this.page.locator(`aria-ref=${param.ref}`).describe(param.element); + }); + } + + async waitForTimeout(time: number) { + if (this._javaScriptBlocked()) { + await new Promise(f => setTimeout(f, time)); + return; + } + + await callOnPageNoTrace(this.page, page => { + return page.evaluate(() => new Promise(f => setTimeout(f, 1000))); + }); + } +} + +export type ConsoleMessage = { + type: ReturnType | undefined; + text: string; + toString(): string; +}; + +function messageToConsoleMessage(message: playwright.ConsoleMessage): ConsoleMessage { + return { + type: message.type(), + text: message.text(), + toString: () => `[${message.type().toUpperCase()}] ${message.text()} @ ${message.location().url}:${message.location().lineNumber}`, + }; +} + +function pageErrorToConsoleMessage(errorOrValue: Error | any): ConsoleMessage { + if (errorOrValue instanceof Error) { + return { + type: undefined, + text: errorOrValue.message, + toString: () => errorOrValue.stack || errorOrValue.message, + }; + } + return { + type: undefined, + text: String(errorOrValue), + toString: () => String(errorOrValue), + }; +} + +export function renderModalStates(context: Context, modalStates: ModalState[]): string[] { + const result: string[] = ['### Modal state']; + if (modalStates.length === 0) + result.push('- There is no modal state present'); + for (const state of modalStates) { + const tool = context.tools.filter(tool => 'clearsModalState' in tool).find(tool => tool.clearsModalState === state.type); + result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`); + } + return result; +} + +const tabSymbol = Symbol('tabSymbol'); diff --git a/packages/playwright/src/mcp/browser/tools.ts b/packages/playwright/src/mcp/browser/tools.ts index 69e20772a4068..b1e8059f914cb 100644 --- a/packages/playwright/src/mcp/browser/tools.ts +++ b/packages/playwright/src/mcp/browser/tools.ts @@ -14,98 +14,47 @@ * limitations under the License. */ -import { asLocator } from 'playwright-core/lib/utils'; - -import { defineTool } from './tool.js'; -import * as mcp from '../sdk/bundle'; - -import type * as playwright from '../../../index'; -import type z from 'zod'; - -type PageEx = playwright.Page & { - _snapshotForAI: () => Promise; -}; - -export const snapshot = defineTool({ - schema: { - name: 'playwright_test_browser_snapshot', - title: 'Capture page snapshot', - description: 'Capture page snapshot for debugging', - inputSchema: mcp.z.object({}), - type: 'readOnly', - }, - - handle: async (page, params) => { - const snapshot = await (page as PageEx)._snapshotForAI(); - return { - content: [ - { - type: 'text', - text: snapshot, - }, - ], - }; - }, -}); - -export const elementSchema = mcp.z.object({ - element: mcp.z.string().describe('Human-readable element description used to obtain permission to interact with the element'), - ref: mcp.z.string().describe('Exact target element reference from the page snapshot'), -}); - -export const pickLocator = defineTool({ - schema: { - name: 'playwright_test_generate_locator', - title: 'Create locator for element', - description: 'Generate locator for the given element to use in tests', - inputSchema: elementSchema, - type: 'readOnly', - }, - - handle: async (page, params) => { - const locator = await refLocator(page, params); - - try { - const { resolvedSelector } = await (locator as any)._resolveSelector(); - const locatorString = asLocator('javascript', resolvedSelector); - return { content: [{ type: 'text', text: locatorString }] }; - } catch (e) { - throw new Error(`Ref not found, likely because element was removed. Use ${snapshot.schema.name} to see what elements are currently on the page.`); - } - }, -}); - -const evaluateSchema = mcp.z.object({ - function: mcp.z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'), - element: mcp.z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'), - ref: mcp.z.string().optional().describe('Exact target element reference from the page snapshot'), -}); - -export const evaluate = defineTool({ - schema: { - name: 'playwright_test_evaluate_on_pause', - title: 'Evaluate in page', - description: 'Evaluate JavaScript expression on page or element', - inputSchema: evaluateSchema, - type: 'destructive', - }, - - handle: async (page, params) => { - let locator: playwright.Locator | undefined; - if (params.ref && params.element) - locator = await refLocator(page, { ref: params.ref, element: params.element }); - - const receiver = locator ?? page as any; - const result = await receiver._evaluateFunction(params.function); - return { - content: [{ type: 'text', text: JSON.stringify(result, null, 2) || 'undefined' }], - }; - }, -}); - -async function refLocator(page: playwright.Page, elementRef: z.output): Promise { - const snapshot = await (page as PageEx)._snapshotForAI(); - if (!snapshot.includes(`[ref=${elementRef.ref}]`)) - throw new Error(`Ref ${elementRef.ref} not found in the current page snapshot. Try capturing new snapshot.`); - return page.locator(`aria-ref=${elementRef.ref}`).describe(elementRef.element); +import common from './tools/common'; +import console from './tools/console'; +import dialogs from './tools/dialogs'; +import evaluate from './tools/evaluate'; +import files from './tools/files'; +import form from './tools/form'; +import install from './tools/install'; +import keyboard from './tools/keyboard'; +import mouse from './tools/mouse'; +import navigate from './tools/navigate'; +import network from './tools/network'; +import pdf from './tools/pdf'; +import snapshot from './tools/snapshot'; +import tabs from './tools/tabs'; +import screenshot from './tools/screenshot'; +import wait from './tools/wait'; +import verify from './tools/verify'; + +import type { Tool } from './tools/tool'; +import type { FullConfig } from './config'; + +export const allTools: Tool[] = [ + ...common, + ...console, + ...dialogs, + ...evaluate, + ...files, + ...form, + ...install, + ...keyboard, + ...navigate, + ...network, + ...mouse, + ...pdf, + ...screenshot, + ...snapshot, + ...tabs, + ...wait, + ...verify, +]; + +export function filteredTools(config: FullConfig) { + return allTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability)); } diff --git a/packages/playwright/src/mcp/browser/tools/DEPS.list b/packages/playwright/src/mcp/browser/tools/DEPS.list new file mode 100644 index 0000000000000..11683dbcd8d5a --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/DEPS.list @@ -0,0 +1,3 @@ +[*] +../ +../../sdk/ \ No newline at end of file diff --git a/packages/playwright/src/mcp/browser/tools/common.ts b/packages/playwright/src/mcp/browser/tools/common.ts new file mode 100644 index 0000000000000..62f175ecd7f3c --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/common.ts @@ -0,0 +1,63 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool, defineTool } from './tool'; + +const close = defineTool({ + capability: 'core', + + schema: { + name: 'browser_close', + title: 'Close browser', + description: 'Close the page', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async (context, params, response) => { + await context.closeBrowserContext(); + response.setIncludeTabs(); + response.addCode(`await page.close()`); + }, +}); + +const resize = defineTabTool({ + capability: 'core', + schema: { + name: 'browser_resize', + title: 'Resize browser window', + description: 'Resize the browser window', + inputSchema: z.object({ + width: z.number().describe('Width of the browser window'), + height: z.number().describe('Height of the browser window'), + }), + type: 'readOnly', + }, + + handle: async (tab, params, response) => { + response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`); + + await tab.waitForCompletion(async () => { + await tab.page.setViewportSize({ width: params.width, height: params.height }); + }); + }, +}); + +export default [ + close, + resize +]; diff --git a/packages/playwright/src/mcp/browser/tools/console.ts b/packages/playwright/src/mcp/browser/tools/console.ts new file mode 100644 index 0000000000000..3ad0ba4db4409 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/console.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool } from './tool'; + +const console = defineTabTool({ + capability: 'core', + schema: { + name: 'browser_console_messages', + title: 'Get console messages', + description: 'Returns all console messages', + inputSchema: z.object({}), + type: 'readOnly', + }, + handle: async (tab, params, response) => { + tab.consoleMessages().map(message => response.addResult(message.toString())); + }, +}); + +export default [ + console, +]; diff --git a/packages/playwright/src/mcp/browser/tools/dialogs.ts b/packages/playwright/src/mcp/browser/tools/dialogs.ts new file mode 100644 index 0000000000000..f6954b7225629 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/dialogs.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool } from './tool'; + +const handleDialog = defineTabTool({ + capability: 'core', + + schema: { + name: 'browser_handle_dialog', + title: 'Handle a dialog', + description: 'Handle a dialog', + inputSchema: z.object({ + accept: z.boolean().describe('Whether to accept the dialog.'), + promptText: z.string().optional().describe('The text of the prompt in case of a prompt dialog.'), + }), + type: 'destructive', + }, + + handle: async (tab, params, response) => { + response.setIncludeSnapshot(); + + const dialogState = tab.modalStates().find(state => state.type === 'dialog'); + if (!dialogState) + throw new Error('No dialog visible'); + + tab.clearModalState(dialogState); + await tab.waitForCompletion(async () => { + if (params.accept) + await dialogState.dialog.accept(params.promptText); + else + await dialogState.dialog.dismiss(); + }); + }, + + clearsModalState: 'dialog', +}); + +export default [ + handleDialog, +]; diff --git a/packages/playwright/src/mcp/browser/tools/evaluate.ts b/packages/playwright/src/mcp/browser/tools/evaluate.ts new file mode 100644 index 0000000000000..b7c7645c776de --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/evaluate.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool } from './tool'; +import * as javascript from '../codegen'; +import { generateLocator } from './utils'; + +import type * as playwright from 'playwright-core'; + +const evaluateSchema = z.object({ + function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'), + element: z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'), + ref: z.string().optional().describe('Exact target element reference from the page snapshot'), +}); + +const evaluate = defineTabTool({ + capability: 'core', + schema: { + name: 'browser_evaluate', + title: 'Evaluate JavaScript', + description: 'Evaluate JavaScript expression on page or element', + inputSchema: evaluateSchema, + type: 'destructive', + }, + + handle: async (tab, params, response) => { + response.setIncludeSnapshot(); + + let locator: playwright.Locator | undefined; + if (params.ref && params.element) { + locator = await tab.refLocator({ ref: params.ref, element: params.element }); + response.addCode(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`); + } else { + response.addCode(`await page.evaluate(${javascript.quote(params.function)});`); + } + + await tab.waitForCompletion(async () => { + const receiver = locator ?? tab.page as any; + const result = await receiver._evaluateFunction(params.function); + response.addResult(JSON.stringify(result, null, 2) || 'undefined'); + }); + }, +}); + +export default [ + evaluate, +]; diff --git a/packages/playwright/src/mcp/browser/tools/files.ts b/packages/playwright/src/mcp/browser/tools/files.ts new file mode 100644 index 0000000000000..c1cdf6660b7db --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/files.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool } from './tool'; + +const uploadFile = defineTabTool({ + capability: 'core', + + schema: { + name: 'browser_file_upload', + title: 'Upload files', + description: 'Upload one or multiple files', + inputSchema: z.object({ + paths: z.array(z.string()).describe('The absolute paths to the files to upload. Can be a single file or multiple files.'), + }), + type: 'destructive', + }, + + handle: async (tab, params, response) => { + response.setIncludeSnapshot(); + + const modalState = tab.modalStates().find(state => state.type === 'fileChooser'); + if (!modalState) + throw new Error('No file chooser visible'); + + response.addCode(`await fileChooser.setFiles(${JSON.stringify(params.paths)})`); + + tab.clearModalState(modalState); + await tab.waitForCompletion(async () => { + await modalState.fileChooser.setFiles(params.paths); + }); + }, + clearsModalState: 'fileChooser', +}); + +export default [ + uploadFile, +]; diff --git a/packages/playwright/src/mcp/browser/tools/form.ts b/packages/playwright/src/mcp/browser/tools/form.ts new file mode 100644 index 0000000000000..aad12ad919c69 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/form.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool } from './tool'; +import { generateLocator } from './utils'; +import * as javascript from '../codegen'; + +const fillForm = defineTabTool({ + capability: 'core', + + schema: { + name: 'browser_fill_form', + title: 'Fill form', + description: 'Fill multiple form fields', + inputSchema: z.object({ + fields: z.array(z.object({ + name: z.string().describe('Human-readable field name'), + type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the field'), + ref: z.string().describe('Exact target field reference from the page snapshot'), + value: z.string().describe('Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.'), + })).describe('Fields to fill in'), + }), + type: 'destructive', + }, + + handle: async (tab, params, response) => { + for (const field of params.fields) { + const locator = await tab.refLocator({ element: field.name, ref: field.ref }); + const locatorSource = `await page.${await generateLocator(locator)}`; + if (field.type === 'textbox' || field.type === 'slider') { + await locator.fill(field.value); + response.addCode(`${locatorSource}.fill(${javascript.quote(field.value)});`); + } else if (field.type === 'checkbox' || field.type === 'radio') { + await locator.setChecked(field.value === 'true'); + response.addCode(`${locatorSource}.setChecked(${javascript.quote(field.value)});`); + } else if (field.type === 'combobox') { + await locator.selectOption({ label: field.value }); + response.addCode(`${locatorSource}.selectOption(${javascript.quote(field.value)});`); + } + } + }, +}); + +export default [ + fillForm, +]; diff --git a/packages/playwright/src/mcp/browser/tools/install.ts b/packages/playwright/src/mcp/browser/tools/install.ts new file mode 100644 index 0000000000000..54d67a7d06908 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/install.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fork } from 'child_process'; +import path from 'path'; + +import { z } from '../../sdk/bundle'; +import { defineTool } from './tool'; + +const install = defineTool({ + capability: 'core-install', + schema: { + name: 'browser_install', + title: 'Install the browser specified in the config', + description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.', + inputSchema: z.object({}), + type: 'destructive', + }, + + handle: async (context, params, response) => { + const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome'; + const cliPath = path.join(require.resolve('playwright/package.json'), '../cli.js'); + const child = fork(cliPath, ['install', channel], { + stdio: 'pipe', + }); + const output: string[] = []; + child.stdout?.on('data', data => output.push(data.toString())); + child.stderr?.on('data', data => output.push(data.toString())); + await new Promise((resolve, reject) => { + child.on('close', code => { + if (code === 0) + resolve(); + else + reject(new Error(`Failed to install browser: ${output.join('')}`)); + }); + }); + response.setIncludeTabs(); + }, +}); + +export default [ + install, +]; diff --git a/packages/playwright/src/mcp/browser/tools/keyboard.ts b/packages/playwright/src/mcp/browser/tools/keyboard.ts new file mode 100644 index 0000000000000..e567bcb65bb03 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/keyboard.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool } from './tool'; +import { elementSchema } from './snapshot'; +import { generateLocator } from './utils'; +import * as javascript from '../codegen'; + +const pressKey = defineTabTool({ + capability: 'core', + + schema: { + name: 'browser_press_key', + title: 'Press a key', + description: 'Press a key on the keyboard', + inputSchema: z.object({ + key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'), + }), + type: 'destructive', + }, + + handle: async (tab, params, response) => { + response.setIncludeSnapshot(); + response.addCode(`// Press ${params.key}`); + response.addCode(`await page.keyboard.press('${params.key}');`); + + await tab.waitForCompletion(async () => { + await tab.page.keyboard.press(params.key); + }); + }, +}); + +const typeSchema = elementSchema.extend({ + text: z.string().describe('Text to type into the element'), + submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'), + slowly: z.boolean().optional().describe('Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.'), +}); + +const type = defineTabTool({ + capability: 'core', + schema: { + name: 'browser_type', + title: 'Type text', + description: 'Type text into editable element', + inputSchema: typeSchema, + type: 'destructive', + }, + + handle: async (tab, params, response) => { + const locator = await tab.refLocator(params); + + await tab.waitForCompletion(async () => { + if (params.slowly) { + response.setIncludeSnapshot(); + response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`); + await locator.pressSequentially(params.text); + } else { + response.addCode(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`); + await locator.fill(params.text); + } + + if (params.submit) { + response.setIncludeSnapshot(); + response.addCode(`await page.${await generateLocator(locator)}.press('Enter');`); + await locator.press('Enter'); + } + }); + }, +}); + +export default [ + pressKey, + type, +]; diff --git a/packages/playwright/src/mcp/browser/tools/mouse.ts b/packages/playwright/src/mcp/browser/tools/mouse.ts new file mode 100644 index 0000000000000..0e0fc88e1a3bb --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/mouse.ts @@ -0,0 +1,113 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool } from './tool'; + +const elementSchema = z.object({ + element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'), +}); + +const mouseMove = defineTabTool({ + capability: 'vision', + schema: { + name: 'browser_mouse_move_xy', + title: 'Move mouse', + description: 'Move mouse to a given position', + inputSchema: elementSchema.extend({ + x: z.number().describe('X coordinate'), + y: z.number().describe('Y coordinate'), + }), + type: 'readOnly', + }, + + handle: async (tab, params, response) => { + response.addCode(`// Move mouse to (${params.x}, ${params.y})`); + response.addCode(`await page.mouse.move(${params.x}, ${params.y});`); + + await tab.waitForCompletion(async () => { + await tab.page.mouse.move(params.x, params.y); + }); + }, +}); + +const mouseClick = defineTabTool({ + capability: 'vision', + schema: { + name: 'browser_mouse_click_xy', + title: 'Click', + description: 'Click left mouse button at a given position', + inputSchema: elementSchema.extend({ + x: z.number().describe('X coordinate'), + y: z.number().describe('Y coordinate'), + }), + type: 'destructive', + }, + + handle: async (tab, params, response) => { + response.setIncludeSnapshot(); + + response.addCode(`// Click mouse at coordinates (${params.x}, ${params.y})`); + response.addCode(`await page.mouse.move(${params.x}, ${params.y});`); + response.addCode(`await page.mouse.down();`); + response.addCode(`await page.mouse.up();`); + + await tab.waitForCompletion(async () => { + await tab.page.mouse.move(params.x, params.y); + await tab.page.mouse.down(); + await tab.page.mouse.up(); + }); + }, +}); + +const mouseDrag = defineTabTool({ + capability: 'vision', + schema: { + name: 'browser_mouse_drag_xy', + title: 'Drag mouse', + description: 'Drag left mouse button to a given position', + inputSchema: elementSchema.extend({ + startX: z.number().describe('Start X coordinate'), + startY: z.number().describe('Start Y coordinate'), + endX: z.number().describe('End X coordinate'), + endY: z.number().describe('End Y coordinate'), + }), + type: 'destructive', + }, + + handle: async (tab, params, response) => { + response.setIncludeSnapshot(); + + response.addCode(`// Drag mouse from (${params.startX}, ${params.startY}) to (${params.endX}, ${params.endY})`); + response.addCode(`await page.mouse.move(${params.startX}, ${params.startY});`); + response.addCode(`await page.mouse.down();`); + response.addCode(`await page.mouse.move(${params.endX}, ${params.endY});`); + response.addCode(`await page.mouse.up();`); + + await tab.waitForCompletion(async () => { + await tab.page.mouse.move(params.startX, params.startY); + await tab.page.mouse.down(); + await tab.page.mouse.move(params.endX, params.endY); + await tab.page.mouse.up(); + }); + }, +}); + +export default [ + mouseMove, + mouseClick, + mouseDrag, +]; diff --git a/packages/playwright/src/mcp/browser/tools/navigate.ts b/packages/playwright/src/mcp/browser/tools/navigate.ts new file mode 100644 index 0000000000000..9998756e577e4 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/navigate.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTool, defineTabTool } from './tool'; + +const navigate = defineTool({ + capability: 'core', + + schema: { + name: 'browser_navigate', + title: 'Navigate to a URL', + description: 'Navigate to a URL', + inputSchema: z.object({ + url: z.string().describe('The URL to navigate to'), + }), + type: 'destructive', + }, + + handle: async (context, params, response) => { + const tab = await context.ensureTab(); + await tab.navigate(params.url); + + response.setIncludeSnapshot(); + response.addCode(`await page.goto('${params.url}');`); + }, +}); + +const goBack = defineTabTool({ + capability: 'core', + schema: { + name: 'browser_navigate_back', + title: 'Go back', + description: 'Go back to the previous page', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async (tab, params, response) => { + await tab.page.goBack(); + response.setIncludeSnapshot(); + response.addCode(`await page.goBack();`); + }, +}); + +export default [ + navigate, + goBack, +]; diff --git a/packages/playwright/src/mcp/browser/tools/network.ts b/packages/playwright/src/mcp/browser/tools/network.ts new file mode 100644 index 0000000000000..2ea081671c5d0 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/network.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool } from './tool'; + +import type * as playwright from 'playwright-core'; + +const requests = defineTabTool({ + capability: 'core', + + schema: { + name: 'browser_network_requests', + title: 'List network requests', + description: 'Returns all network requests since loading the page', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async (tab, params, response) => { + const requests = tab.requests(); + [...requests.entries()].forEach(([req, res]) => response.addResult(renderRequest(req, res))); + }, +}); + +function renderRequest(request: playwright.Request, response: playwright.Response | null) { + const result: string[] = []; + result.push(`[${request.method().toUpperCase()}] ${request.url()}`); + if (response) + result.push(`=> [${response.status()}] ${response.statusText()}`); + return result.join(' '); +} + +export default [ + requests, +]; diff --git a/packages/playwright/src/mcp/browser/tools/pdf.ts b/packages/playwright/src/mcp/browser/tools/pdf.ts new file mode 100644 index 0000000000000..b4c9b14ef46a8 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/pdf.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool } from './tool'; +import * as javascript from '../codegen'; + +const pdfSchema = z.object({ + filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'), +}); + +const pdf = defineTabTool({ + capability: 'pdf', + + schema: { + name: 'browser_pdf_save', + title: 'Save as PDF', + description: 'Save page as PDF', + inputSchema: pdfSchema, + type: 'readOnly', + }, + + handle: async (tab, params, response) => { + const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.pdf`); + response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`); + response.addResult(`Saved page as ${fileName}`); + await tab.page.pdf({ path: fileName }); + }, +}); + +export default [ + pdf, +]; diff --git a/packages/playwright/src/mcp/browser/tools/screenshot.ts b/packages/playwright/src/mcp/browser/tools/screenshot.ts new file mode 100644 index 0000000000000..4ee5cf81d91af --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/screenshot.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool } from './tool'; +import * as javascript from '../codegen'; +import { generateLocator } from './utils'; + +import type * as playwright from 'playwright-core'; + +const screenshotSchema = z.object({ + type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'), + filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'), + element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'), + ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'), + fullPage: z.boolean().optional().describe('When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.'), +}).refine(data => { + return !!data.element === !!data.ref; +}, { + message: 'Both element and ref must be provided or neither.', + path: ['ref', 'element'] +}).refine(data => { + return !(data.fullPage && (data.element || data.ref)); +}, { + message: 'fullPage cannot be used with element screenshots.', + path: ['fullPage'] +}); + +const screenshot = defineTabTool({ + capability: 'core', + schema: { + name: 'browser_take_screenshot', + title: 'Take a screenshot', + description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`, + inputSchema: screenshotSchema, + type: 'readOnly', + }, + + handle: async (tab, params, response) => { + const fileType = params.type || 'png'; + const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.${fileType}`); + const options: playwright.PageScreenshotOptions = { + type: fileType, + quality: fileType === 'png' ? undefined : 90, + scale: 'css', + path: fileName, + ...(params.fullPage !== undefined && { fullPage: params.fullPage }) + }; + const isElementScreenshot = params.element && params.ref; + + const screenshotTarget = isElementScreenshot ? params.element : (params.fullPage ? 'full page' : 'viewport'); + response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`); + + // Only get snapshot when element screenshot is needed + const locator = params.ref ? await tab.refLocator({ element: params.element || '', ref: params.ref }) : null; + + if (locator) + response.addCode(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`); + else + response.addCode(`await page.screenshot(${javascript.formatObject(options)});`); + + const buffer = locator ? await locator.screenshot(options) : await tab.page.screenshot(options); + response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`); + + // https://github.com/microsoft/playwright-mcp/issues/817 + // Never return large images to LLM, saving them to the file system is enough. + if (!params.fullPage) { + response.addImage({ + contentType: fileType === 'png' ? 'image/png' : 'image/jpeg', + data: buffer + }); + } + } +}); + +export default [ + screenshot, +]; diff --git a/packages/playwright/src/mcp/browser/tools/snapshot.ts b/packages/playwright/src/mcp/browser/tools/snapshot.ts new file mode 100644 index 0000000000000..03c6852e14813 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/snapshot.ts @@ -0,0 +1,165 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool, defineTool } from './tool'; +import * as javascript from '../codegen'; +import { generateLocator } from './utils'; + +const snapshot = defineTool({ + capability: 'core', + schema: { + name: 'browser_snapshot', + title: 'Page snapshot', + description: 'Capture accessibility snapshot of the current page, this is better than screenshot', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async (context, params, response) => { + await context.ensureTab(); + response.setIncludeSnapshot(); + }, +}); + +export const elementSchema = z.object({ + element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'), + ref: z.string().describe('Exact target element reference from the page snapshot'), +}); + +const clickSchema = elementSchema.extend({ + doubleClick: z.boolean().optional().describe('Whether to perform a double click instead of a single click'), + button: z.enum(['left', 'right', 'middle']).optional().describe('Button to click, defaults to left'), +}); + +const click = defineTabTool({ + capability: 'core', + schema: { + name: 'browser_click', + title: 'Click', + description: 'Perform click on a web page', + inputSchema: clickSchema, + type: 'destructive', + }, + + handle: async (tab, params, response) => { + response.setIncludeSnapshot(); + + const locator = await tab.refLocator(params); + const button = params.button; + const buttonAttr = button ? `{ button: '${button}' }` : ''; + + if (params.doubleClick) + response.addCode(`await page.${await generateLocator(locator)}.dblclick(${buttonAttr});`); + else + response.addCode(`await page.${await generateLocator(locator)}.click(${buttonAttr});`); + + + await tab.waitForCompletion(async () => { + if (params.doubleClick) + await locator.dblclick({ button }); + else + await locator.click({ button }); + }); + }, +}); + +const drag = defineTabTool({ + capability: 'core', + schema: { + name: 'browser_drag', + title: 'Drag mouse', + description: 'Perform drag and drop between two elements', + inputSchema: z.object({ + startElement: z.string().describe('Human-readable source element description used to obtain the permission to interact with the element'), + startRef: z.string().describe('Exact source element reference from the page snapshot'), + endElement: z.string().describe('Human-readable target element description used to obtain the permission to interact with the element'), + endRef: z.string().describe('Exact target element reference from the page snapshot'), + }), + type: 'destructive', + }, + + handle: async (tab, params, response) => { + response.setIncludeSnapshot(); + + const [startLocator, endLocator] = await tab.refLocators([ + { ref: params.startRef, element: params.startElement }, + { ref: params.endRef, element: params.endElement }, + ]); + + await tab.waitForCompletion(async () => { + await startLocator.dragTo(endLocator); + }); + + response.addCode(`await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});`); + }, +}); + +const hover = defineTabTool({ + capability: 'core', + schema: { + name: 'browser_hover', + title: 'Hover mouse', + description: 'Hover over element on page', + inputSchema: elementSchema, + type: 'readOnly', + }, + + handle: async (tab, params, response) => { + response.setIncludeSnapshot(); + + const locator = await tab.refLocator(params); + response.addCode(`await page.${await generateLocator(locator)}.hover();`); + + await tab.waitForCompletion(async () => { + await locator.hover(); + }); + }, +}); + +const selectOptionSchema = elementSchema.extend({ + values: z.array(z.string()).describe('Array of values to select in the dropdown. This can be a single value or multiple values.'), +}); + +const selectOption = defineTabTool({ + capability: 'core', + schema: { + name: 'browser_select_option', + title: 'Select option', + description: 'Select an option in a dropdown', + inputSchema: selectOptionSchema, + type: 'destructive', + }, + + handle: async (tab, params, response) => { + response.setIncludeSnapshot(); + + const locator = await tab.refLocator(params); + response.addCode(`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`); + + await tab.waitForCompletion(async () => { + await locator.selectOption(params.values); + }); + }, +}); + +export default [ + snapshot, + click, + drag, + hover, + selectOption, +]; diff --git a/packages/playwright/src/mcp/browser/tools/tabs.ts b/packages/playwright/src/mcp/browser/tools/tabs.ts new file mode 100644 index 0000000000000..77ad91ff90430 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/tabs.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTool } from './tool'; + +const browserTabs = defineTool({ + capability: 'core-tabs', + + schema: { + name: 'browser_tabs', + title: 'Manage tabs', + description: 'List, create, close, or select a browser tab.', + inputSchema: z.object({ + action: z.enum(['list', 'new', 'close', 'select']).describe('Operation to perform'), + index: z.number().optional().describe('Tab index, used for close/select. If omitted for close, current tab is closed.'), + }), + type: 'destructive', + }, + + handle: async (context, params, response) => { + switch (params.action) { + case 'list': { + await context.ensureTab(); + response.setIncludeTabs(); + return; + } + case 'new': { + await context.newTab(); + response.setIncludeTabs(); + return; + } + case 'close': { + await context.closeTab(params.index); + response.setIncludeSnapshot(); + return; + } + case 'select': { + if (params.index === undefined) + throw new Error('Tab index is required'); + await context.selectTab(params.index); + response.setIncludeSnapshot(); + return; + } + } + }, +}); + +export default [ + browserTabs, +]; diff --git a/packages/playwright/src/mcp/browser/tools/tool.ts b/packages/playwright/src/mcp/browser/tools/tool.ts new file mode 100644 index 0000000000000..a4f8a11a6323e --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/tool.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { z } from 'zod'; +import type { Context } from '../context'; +import type * as playwright from 'playwright-core'; +import type { ToolCapability } from '../../config'; +import type { Tab } from '../tab'; +import type { Response } from '../response'; +import type { ToolSchema } from '../../sdk/tool'; + +export type FileUploadModalState = { + type: 'fileChooser'; + description: string; + fileChooser: playwright.FileChooser; +}; + +export type DialogModalState = { + type: 'dialog'; + description: string; + dialog: playwright.Dialog; +}; + +export type ModalState = FileUploadModalState | DialogModalState; + +export type Tool = { + capability: ToolCapability; + schema: ToolSchema; + handle: (context: Context, params: z.output, response: Response) => Promise; +}; + +export function defineTool(tool: Tool): Tool { + return tool; +} + +export type TabTool = { + capability: ToolCapability; + schema: ToolSchema; + clearsModalState?: ModalState['type']; + handle: (tab: Tab, params: z.output, response: Response) => Promise; +}; + +export function defineTabTool(tool: TabTool): Tool { + return { + ...tool, + handle: async (context, params, response) => { + const tab = context.currentTabOrDie(); + const modalStates = tab.modalStates().map(state => state.type); + if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState)) + response.addError(`Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n')); + else if (!tool.clearsModalState && modalStates.length) + response.addError(`Error: Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n')); + else + return tool.handle(tab, params, response); + }, + }; +} diff --git a/packages/playwright/src/mcp/browser/tools/utils.ts b/packages/playwright/src/mcp/browser/tools/utils.ts new file mode 100644 index 0000000000000..2aedef1e507fd --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/utils.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { asLocator } from 'playwright-core/lib/utils'; + +import type * as playwright from 'playwright-core'; +import type { Tab } from '../tab'; + +export async function waitForCompletion(tab: Tab, callback: () => Promise): Promise { + const requests = new Set(); + let frameNavigated = false; + let waitCallback: () => void = () => {}; + const waitBarrier = new Promise(f => { waitCallback = f; }); + + const requestListener = (request: playwright.Request) => requests.add(request); + const requestFinishedListener = (request: playwright.Request) => { + requests.delete(request); + if (!requests.size) + waitCallback(); + }; + + const frameNavigateListener = (frame: playwright.Frame) => { + if (frame.parentFrame()) + return; + frameNavigated = true; + dispose(); + clearTimeout(timeout); + void tab.waitForLoadState('load').then(waitCallback); + }; + + const onTimeout = () => { + dispose(); + waitCallback(); + }; + + tab.page.on('request', requestListener); + tab.page.on('requestfinished', requestFinishedListener); + tab.page.on('framenavigated', frameNavigateListener); + const timeout = setTimeout(onTimeout, 10000); + + const dispose = () => { + tab.page.off('request', requestListener); + tab.page.off('requestfinished', requestFinishedListener); + tab.page.off('framenavigated', frameNavigateListener); + clearTimeout(timeout); + }; + + try { + const result = await callback(); + if (!requests.size && !frameNavigated) + waitCallback(); + await waitBarrier; + await tab.waitForTimeout(1000); + return result; + } finally { + dispose(); + } +} + +export async function generateLocator(locator: playwright.Locator): Promise { + try { + const { resolvedSelector } = await (locator as any)._resolveSelector(); + return asLocator('javascript', resolvedSelector); + } catch (e) { + throw new Error('Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.'); + } +} + +export async function callOnPageNoTrace(page: playwright.Page, callback: (page: playwright.Page) => Promise): Promise { + return await (page as any)._wrapApiCall(() => callback(page), { internal: true }); +} diff --git a/packages/playwright/src/mcp/browser/tools/verify.ts b/packages/playwright/src/mcp/browser/tools/verify.ts new file mode 100644 index 0000000000000..809ed2ab02275 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/verify.ts @@ -0,0 +1,148 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTabTool } from './tool'; +import * as javascript from '../codegen'; +import { generateLocator } from './utils'; + +const verifyElement = defineTabTool({ + capability: 'verify', + schema: { + name: 'browser_verify_element_visible', + title: 'Verify element visible', + description: 'Verify element is visible on the page', + inputSchema: z.object({ + role: z.string().describe('ROLE of the element. Can be found in the snapshot like this: \`- {ROLE} "Accessible Name":\`'), + accessibleName: z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: \`- role "{ACCESSIBLE_NAME}"\`'), + }), + type: 'readOnly', + }, + + handle: async (tab, params, response) => { + const locator = tab.page.getByRole(params.role as any, { name: params.accessibleName }); + if (await locator.count() === 0) { + response.addError(`Element with role "${params.role}" and accessible name "${params.accessibleName}" not found`); + return; + } + + response.addCode(`await expect(page.getByRole(${javascript.escapeWithQuotes(params.role)}, { name: ${javascript.escapeWithQuotes(params.accessibleName)} })).toBeVisible();`); + response.addResult('Done'); + }, +}); + +const verifyText = defineTabTool({ + capability: 'verify', + schema: { + name: 'browser_verify_text_visible', + title: 'Verify text visible', + description: `Verify text is visible on the page. Prefer ${verifyElement.schema.name} if possible.`, + inputSchema: z.object({ + text: z.string().describe('TEXT to verify. Can be found in the snapshot like this: \`- role "Accessible Name": {TEXT}\` or like this: \`- text: {TEXT}\`'), + }), + type: 'readOnly', + }, + + handle: async (tab, params, response) => { + const locator = tab.page.getByText(params.text).filter({ visible: true }); + if (await locator.count() === 0) { + response.addError('Text not found'); + return; + } + + response.addCode(`await expect(page.getByText(${javascript.escapeWithQuotes(params.text)})).toBeVisible();`); + response.addResult('Done'); + }, +}); + +const verifyList = defineTabTool({ + capability: 'verify', + schema: { + name: 'browser_verify_list_visible', + title: 'Verify list visible', + description: 'Verify list is visible on the page', + inputSchema: z.object({ + element: z.string().describe('Human-readable list description'), + ref: z.string().describe('Exact target element reference that points to the list'), + items: z.array(z.string()).describe('Items to verify'), + }), + type: 'readOnly', + }, + + handle: async (tab, params, response) => { + const locator = await tab.refLocator({ ref: params.ref, element: params.element }); + const itemTexts: string[] = []; + for (const item of params.items) { + const itemLocator = locator.getByText(item); + if (await itemLocator.count() === 0) { + response.addError(`Item "${item}" not found`); + return; + } + itemTexts.push((await itemLocator.textContent())!); + } + const ariaSnapshot = `\` +- list: +${itemTexts.map(t => ` - listitem: ${javascript.escapeWithQuotes(t, '"')}`).join('\n')} +\``; + response.addCode(`await expect(page.locator('body')).toMatchAriaSnapshot(${ariaSnapshot});`); + response.addResult('Done'); + }, +}); + +const verifyValue = defineTabTool({ + capability: 'verify', + schema: { + name: 'browser_verify_value', + title: 'Verify value', + description: 'Verify element value', + inputSchema: z.object({ + type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the element'), + element: z.string().describe('Human-readable element description'), + ref: z.string().describe('Exact target element reference that points to the element'), + value: z.string().describe('Value to verify. For checkbox, use "true" or "false".'), + }), + type: 'readOnly', + }, + + handle: async (tab, params, response) => { + const locator = await tab.refLocator({ ref: params.ref, element: params.element }); + const locatorSource = `page.${await generateLocator(locator)}`; + if (params.type === 'textbox' || params.type === 'slider' || params.type === 'combobox') { + const value = await locator.inputValue(); + if (value !== params.value) { + response.addError(`Expected value "${params.value}", but got "${value}"`); + return; + } + response.addCode(`await expect(${locatorSource}).toHaveValue(${javascript.quote(params.value)});`); + } else if (params.type === 'checkbox' || params.type === 'radio') { + const value = await locator.isChecked(); + if (value !== (params.value === 'true')) { + response.addError(`Expected value "${params.value}", but got "${value}"`); + return; + } + const matcher = value ? 'toBeChecked' : 'not.toBeChecked'; + response.addCode(`await expect(${locatorSource}).${matcher}();`); + } + response.addResult('Done'); + }, +}); + +export default [ + verifyElement, + verifyText, + verifyList, + verifyValue, +]; diff --git a/packages/playwright/src/mcp/browser/tools/wait.ts b/packages/playwright/src/mcp/browser/tools/wait.ts new file mode 100644 index 0000000000000..a0f5382b6afe7 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/wait.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTool } from './tool'; + +const wait = defineTool({ + capability: 'core', + + schema: { + name: 'browser_wait_for', + title: 'Wait for', + description: 'Wait for text to appear or disappear or a specified time to pass', + inputSchema: z.object({ + time: z.number().optional().describe('The time to wait in seconds'), + text: z.string().optional().describe('The text to wait for'), + textGone: z.string().optional().describe('The text to wait for to disappear'), + }), + type: 'readOnly', + }, + + handle: async (context, params, response) => { + if (!params.text && !params.textGone && !params.time) + throw new Error('Either time, text or textGone must be provided'); + + if (params.time) { + response.addCode(`await new Promise(f => setTimeout(f, ${params.time!} * 1000));`); + await new Promise(f => setTimeout(f, Math.min(30000, params.time! * 1000))); + } + + const tab = context.currentTabOrDie(); + const locator = params.text ? tab.page.getByText(params.text).first() : undefined; + const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined; + + if (goneLocator) { + response.addCode(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`); + await goneLocator.waitFor({ state: 'hidden' }); + } + + if (locator) { + response.addCode(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`); + await locator.waitFor({ state: 'visible' }); + } + + response.addResult(`Waited for ${params.text || params.textGone || params.time}`); + response.setIncludeSnapshot(); + }, +}); + +export default [ + wait, +]; diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts new file mode 100644 index 0000000000000..ffeb8120b1485 --- /dev/null +++ b/packages/playwright/src/mcp/config.d.ts @@ -0,0 +1,120 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type * as playwright from 'playwright-core'; + +export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'verify'; + +export type Config = { + /** + * The browser to use. + */ + browser?: { + /** + * The type of browser to use. + */ + browserName?: 'chromium' | 'firefox' | 'webkit'; + + /** + * Keep the browser profile in memory, do not save it to disk. + */ + isolated?: boolean; + + /** + * Path to a user data directory for browser profile persistence. + * Temporary directory is created by default. + */ + userDataDir?: string; + + /** + * Launch options passed to + * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context + * + * This is useful for settings options like `channel`, `headless`, `executablePath`, etc. + */ + launchOptions?: playwright.LaunchOptions; + + /** + * Context options for the browser context. + * + * This is useful for settings options like `viewport`. + */ + contextOptions?: playwright.BrowserContextOptions; + + /** + * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers. + */ + cdpEndpoint?: string; + + /** + * Remote endpoint to connect to an existing Playwright server. + */ + remoteEndpoint?: string; + }, + + server?: { + /** + * The port to listen on for SSE or MCP transport. + */ + port?: number; + + /** + * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces. + */ + host?: string; + }, + + /** + * List of enabled tool capabilities. Possible values: + * - 'core': Core browser automation features. + * - 'pdf': PDF generation and manipulation. + * - 'vision': Coordinate-based interactions. + */ + capabilities?: ToolCapability[]; + + /** + * Whether to save the Playwright session into the output directory. + */ + saveSession?: boolean; + + /** + * Whether to save the Playwright trace of the session into the output directory. + */ + saveTrace?: boolean; + + /** + * The directory to save output files. + */ + outputDir?: string; + + network?: { + /** + * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + */ + allowedOrigins?: string[]; + + /** + * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + */ + blockedOrigins?: string[]; + }; + + /** + * Whether to send image responses to the client. Can be "allow", "omit", or "auto". Defaults to "auto", which sends images if the client can display them. + */ + imageResponses?: 'allow' | 'omit'; +}; + diff --git a/packages/playwright/src/mcp/extension/DEPS.list b/packages/playwright/src/mcp/extension/DEPS.list new file mode 100644 index 0000000000000..65f47af0538b4 --- /dev/null +++ b/packages/playwright/src/mcp/extension/DEPS.list @@ -0,0 +1,4 @@ +[*] +../sdk/ +../browser/ +../log.ts diff --git a/packages/playwright/src/mcp/extension/cdpRelay.ts b/packages/playwright/src/mcp/extension/cdpRelay.ts new file mode 100644 index 0000000000000..87660277b105d --- /dev/null +++ b/packages/playwright/src/mcp/extension/cdpRelay.ts @@ -0,0 +1,421 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * WebSocket server that bridges Playwright MCP and Chrome Extension + * + * Endpoints: + * - /cdp/guid - Full CDP interface for Playwright MCP + * - /extension/guid - Extension connection for chrome.debugger forwarding + */ + +import { spawn } from 'child_process'; +import http from 'http'; + +import { debug, ws, wsServer } from 'playwright-core/lib/utilsBundle'; +import { registry } from 'playwright-core/lib/server/registry/index'; +import { ManualPromise } from 'playwright-core/lib/utils'; + +import { httpAddressToString } from '../sdk/http'; +import { logUnhandledError } from '../log'; +import * as protocol from './protocol'; + +import type websocket from 'ws'; +import type { ClientInfo } from '../browser/browserContextFactory'; +import type { ExtensionCommand, ExtensionEvents } from './protocol'; +import type { WebSocket, WebSocketServer } from 'playwright-core/lib/utilsBundle'; + + +const debugLogger = debug('pw:mcp:relay'); + +type CDPCommand = { + id: number; + sessionId?: string; + method: string; + params?: any; +}; + +type CDPResponse = { + id?: number; + sessionId?: string; + method?: string; + params?: any; + result?: any; + error?: { code?: number; message: string }; +}; + +export class CDPRelayServer { + private _wsHost: string; + private _browserChannel: string; + private _userDataDir?: string; + private _executablePath?: string; + private _cdpPath: string; + private _extensionPath: string; + private _wss: WebSocketServer; + private _playwrightConnection: WebSocket | null = null; + private _extensionConnection: ExtensionConnection | null = null; + private _connectedTabInfo: { + targetInfo: any; + // Page sessionId that should be used by this connection. + sessionId: string; + } | undefined; + private _nextSessionId: number = 1; + private _extensionConnectionPromise!: ManualPromise; + + constructor(server: http.Server, browserChannel: string, userDataDir?: string, executablePath?: string) { + this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws'); + this._browserChannel = browserChannel; + this._userDataDir = userDataDir; + this._executablePath = executablePath; + + const uuid = crypto.randomUUID(); + this._cdpPath = `/cdp/${uuid}`; + this._extensionPath = `/extension/${uuid}`; + + this._resetExtensionConnection(); + this._wss = new wsServer({ server }); + this._wss.on('connection', this._onConnection.bind(this)); + } + + cdpEndpoint() { + return `${this._wsHost}${this._cdpPath}`; + } + + extensionEndpoint() { + return `${this._wsHost}${this._extensionPath}`; + } + + async ensureExtensionConnectionForMCPContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined) { + debugLogger('Ensuring extension connection for MCP context'); + if (this._extensionConnection) + return; + this._connectBrowser(clientInfo, toolName); + debugLogger('Waiting for incoming extension connection'); + await Promise.race([ + this._extensionConnectionPromise, + new Promise((_, reject) => setTimeout(() => { + reject(new Error(`Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed. See https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md for installation instructions.`)); + }, process.env.PWMCP_TEST_CONNECTION_TIMEOUT ? parseInt(process.env.PWMCP_TEST_CONNECTION_TIMEOUT, 10) : 5_000)), + new Promise((_, reject) => abortSignal.addEventListener('abort', reject)) + ]); + debugLogger('Extension connection established'); + } + + private _connectBrowser(clientInfo: ClientInfo, toolName: string | undefined) { + const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`; + // Need to specify "key" in the manifest.json to make the id stable when loading from file. + const url = new URL('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); + url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint); + const client = { + name: clientInfo.name, + version: clientInfo.version, + }; + url.searchParams.set('client', JSON.stringify(client)); + url.searchParams.set('protocolVersion', process.env.PWMCP_TEST_PROTOCOL_VERSION ?? protocol.VERSION.toString()); + if (toolName) + url.searchParams.set('newTab', String(toolName === 'browser_navigate')); + const href = url.toString(); + + let executablePath = this._executablePath; + if (!executablePath) { + const executableInfo = registry.findExecutable(this._browserChannel); + if (!executableInfo) + throw new Error(`Unsupported channel: "${this._browserChannel}"`); + executablePath = executableInfo.executablePath('javascript'); + if (!executablePath) + throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`); + } + + const args: string[] = []; + if (this._userDataDir) + args.push(`--user-data-dir=${this._userDataDir}`); + args.push(href); + + spawn(executablePath, args, { + windowsHide: true, + detached: true, + shell: false, + stdio: 'ignore', + }); + } + + stop(): void { + this.closeConnections('Server stopped'); + this._wss.close(); + } + + closeConnections(reason: string) { + this._closePlaywrightConnection(reason); + this._closeExtensionConnection(reason); + } + + private _onConnection(ws: WebSocket, request: http.IncomingMessage): void { + const url = new URL(`http://localhost${request.url}`); + debugLogger(`New connection to ${url.pathname}`); + if (url.pathname === this._cdpPath) { + this._handlePlaywrightConnection(ws); + } else if (url.pathname === this._extensionPath) { + this._handleExtensionConnection(ws); + } else { + debugLogger(`Invalid path: ${url.pathname}`); + ws.close(4004, 'Invalid path'); + } + } + + private _handlePlaywrightConnection(ws: WebSocket): void { + if (this._playwrightConnection) { + debugLogger('Rejecting second Playwright connection'); + ws.close(1000, 'Another CDP client already connected'); + return; + } + this._playwrightConnection = ws; + ws.on('message', async data => { + try { + const message = JSON.parse(data.toString()); + await this._handlePlaywrightMessage(message); + } catch (error: any) { + debugLogger(`Error while handling Playwright message\n${data.toString()}\n`, error); + } + }); + ws.on('close', () => { + if (this._playwrightConnection !== ws) + return; + this._playwrightConnection = null; + this._closeExtensionConnection('Playwright client disconnected'); + debugLogger('Playwright WebSocket closed'); + }); + ws.on('error', error => { + debugLogger('Playwright WebSocket error:', error); + }); + debugLogger('Playwright MCP connected'); + } + + private _closeExtensionConnection(reason: string) { + this._extensionConnection?.close(reason); + this._extensionConnectionPromise.reject(new Error(reason)); + this._resetExtensionConnection(); + } + + private _resetExtensionConnection() { + this._connectedTabInfo = undefined; + this._extensionConnection = null; + this._extensionConnectionPromise = new ManualPromise(); + void this._extensionConnectionPromise.catch(logUnhandledError); + } + + private _closePlaywrightConnection(reason: string) { + if (this._playwrightConnection?.readyState === ws.OPEN) + this._playwrightConnection.close(1000, reason); + this._playwrightConnection = null; + } + + private _handleExtensionConnection(ws: WebSocket): void { + if (this._extensionConnection) { + ws.close(1000, 'Another extension connection already established'); + return; + } + this._extensionConnection = new ExtensionConnection(ws); + this._extensionConnection.onclose = (c, reason) => { + debugLogger('Extension WebSocket closed:', reason, c === this._extensionConnection); + if (this._extensionConnection !== c) + return; + this._resetExtensionConnection(); + this._closePlaywrightConnection(`Extension disconnected: ${reason}`); + }; + this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this); + this._extensionConnectionPromise.resolve(); + } + + private _handleExtensionMessage(method: M, params: ExtensionEvents[M]['params']) { + switch (method) { + case 'forwardCDPEvent': + const sessionId = params.sessionId || this._connectedTabInfo?.sessionId; + this._sendToPlaywright({ + sessionId, + method: params.method, + params: params.params + }); + break; + } + } + + private async _handlePlaywrightMessage(message: CDPCommand): Promise { + debugLogger('← Playwright:', `${message.method} (id=${message.id})`); + const { id, sessionId, method, params } = message; + try { + const result = await this._handleCDPCommand(method, params, sessionId); + this._sendToPlaywright({ id, sessionId, result }); + } catch (e) { + debugLogger('Error in the extension:', e); + this._sendToPlaywright({ + id, + sessionId, + error: { message: (e as Error).message } + }); + } + } + + private async _handleCDPCommand(method: string, params: any, sessionId: string | undefined): Promise { + switch (method) { + case 'Browser.getVersion': { + return { + protocolVersion: '1.3', + product: 'Chrome/Extension-Bridge', + userAgent: 'CDP-Bridge-Server/1.0.0', + }; + } + case 'Browser.setDownloadBehavior': { + return { }; + } + case 'Target.setAutoAttach': { + // Forward child session handling. + if (sessionId) + break; + // Simulate auto-attach behavior with real target info + const { targetInfo } = await this._extensionConnection!.send('attachToTab', { }); + this._connectedTabInfo = { + targetInfo, + sessionId: `pw-tab-${this._nextSessionId++}`, + }; + debugLogger('Simulating auto-attach'); + this._sendToPlaywright({ + method: 'Target.attachedToTarget', + params: { + sessionId: this._connectedTabInfo.sessionId, + targetInfo: { + ...this._connectedTabInfo.targetInfo, + attached: true, + }, + waitingForDebugger: false + } + }); + return { }; + } + case 'Target.getTargetInfo': { + return this._connectedTabInfo?.targetInfo; + } + } + return await this._forwardToExtension(method, params, sessionId); + } + + private async _forwardToExtension(method: string, params: any, sessionId: string | undefined): Promise { + if (!this._extensionConnection) + throw new Error('Extension not connected'); + // Top level sessionId is only passed between the relay and the client. + if (this._connectedTabInfo?.sessionId === sessionId) + sessionId = undefined; + return await this._extensionConnection.send('forwardCDPCommand', { sessionId, method, params }); + } + + private _sendToPlaywright(message: CDPResponse): void { + debugLogger('→ Playwright:', `${message.method ?? `response(id=${message.id})`}`); + this._playwrightConnection?.send(JSON.stringify(message)); + } +} + +type ExtensionResponse = { + id?: number; + method?: string; + params?: any; + result?: any; + error?: string; +}; + +class ExtensionConnection { + private readonly _ws: WebSocket; + private readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error }>(); + private _lastId = 0; + + onmessage?: (method: M, params: ExtensionEvents[M]['params']) => void; + onclose?: (self: ExtensionConnection, reason: string) => void; + + constructor(ws: WebSocket) { + this._ws = ws; + this._ws.on('message', this._onMessage.bind(this)); + this._ws.on('close', this._onClose.bind(this)); + this._ws.on('error', this._onError.bind(this)); + } + + async send(method: M, params: ExtensionCommand[M]['params']): Promise { + if (this._ws.readyState !== ws.OPEN) + throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`); + const id = ++this._lastId; + this._ws.send(JSON.stringify({ id, method, params })); + const error = new Error(`Protocol error: ${method}`); + return new Promise((resolve, reject) => { + this._callbacks.set(id, { resolve, reject, error }); + }); + } + + close(message: string) { + debugLogger('closing extension connection:', message); + if (this._ws.readyState === ws.OPEN) + this._ws.close(1000, message); + } + + private _onMessage(event: websocket.RawData) { + const eventData = event.toString(); + let parsedJson; + try { + parsedJson = JSON.parse(eventData); + } catch (e: any) { + debugLogger(` Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`); + this._ws.close(); + return; + } + try { + this._handleParsedMessage(parsedJson); + } catch (e: any) { + debugLogger(` Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`); + this._ws.close(); + } + } + + private _handleParsedMessage(object: ExtensionResponse) { + if (object.id && this._callbacks.has(object.id)) { + const callback = this._callbacks.get(object.id)!; + this._callbacks.delete(object.id); + if (object.error) { + const error = callback.error; + error.message = object.error; + callback.reject(error); + } else { + callback.resolve(object.result); + } + } else if (object.id) { + debugLogger('← Extension: unexpected response', object); + } else { + this.onmessage?.(object.method! as keyof ExtensionEvents, object.params); + } + } + + private _onClose(event: websocket.CloseEvent) { + debugLogger(` code=${event.code} reason=${event.reason}`); + this._dispose(); + this.onclose?.(this, event.reason); + } + + private _onError(event: websocket.ErrorEvent) { + debugLogger(` message=${event.message} type=${event.type} target=${event.target}`); + this._dispose(); + } + + private _dispose() { + for (const callback of this._callbacks.values()) + callback.reject(new Error('WebSocket closed')); + this._callbacks.clear(); + } +} diff --git a/packages/playwright/src/mcp/extension/extensionContextFactory.ts b/packages/playwright/src/mcp/extension/extensionContextFactory.ts new file mode 100644 index 0000000000000..ed7a21c9b16c8 --- /dev/null +++ b/packages/playwright/src/mcp/extension/extensionContextFactory.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as playwright from 'playwright-core'; +import { debug } from 'playwright-core/lib/utilsBundle'; + +import { startHttpServer } from '../sdk/http'; +import { CDPRelayServer } from './cdpRelay'; + +import type { BrowserContextFactory, ClientInfo } from '../browser/browserContextFactory'; + +const debugLogger = debug('pw:mcp:relay'); + +export class ExtensionContextFactory implements BrowserContextFactory { + private _browserChannel: string; + private _userDataDir?: string; + private _executablePath?: string; + + constructor(browserChannel: string, userDataDir: string | undefined, executablePath: string | undefined) { + this._browserChannel = browserChannel; + this._userDataDir = userDataDir; + this._executablePath = executablePath; + } + + async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { + const browser = await this._obtainBrowser(clientInfo, abortSignal, toolName); + return { + browserContext: browser.contexts()[0], + close: async () => { + debugLogger('close() called for browser context'); + await browser.close(); + } + }; + } + + private async _obtainBrowser(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise { + const relay = await this._startRelay(abortSignal); + await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName); + return await playwright.chromium.connectOverCDP(relay.cdpEndpoint()); + } + + private async _startRelay(abortSignal: AbortSignal) { + const httpServer = await startHttpServer({}); + if (abortSignal.aborted) { + httpServer.close(); + throw new Error(abortSignal.reason); + } + const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir, this._executablePath); + abortSignal.addEventListener('abort', () => cdpRelayServer.stop()); + debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`); + return cdpRelayServer; + } +} diff --git a/packages/playwright/src/mcp/extension/protocol.ts b/packages/playwright/src/mcp/extension/protocol.ts new file mode 100644 index 0000000000000..7b0cb261669ad --- /dev/null +++ b/packages/playwright/src/mcp/extension/protocol.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Whenever the commands/events change, the version must be updated. The latest +// extension version should be compatible with the old MCP clients. +export const VERSION = 1; + +export type ExtensionCommand = { + 'attachToTab': { + params: {}; + }; + 'forwardCDPCommand': { + params: { + method: string, + sessionId?: string + params?: any, + }; + }; +}; + +export type ExtensionEvents = { + 'forwardCDPEvent': { + params: { + method: string, + sessionId?: string + params?: any, + }; + }; +}; diff --git a/packages/playwright/src/mcp/index.ts b/packages/playwright/src/mcp/index.ts new file mode 100644 index 0000000000000..a6a8d4725ec86 --- /dev/null +++ b/packages/playwright/src/mcp/index.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BrowserServerBackend } from './browser/browserServerBackend'; +import { resolveConfig } from './browser/config'; +import { contextFactory } from './browser/browserContextFactory'; +import * as mcpServer from './sdk/server'; + +import type { Config } from './config'; +import type { BrowserContext } from 'playwright'; +import type { BrowserContextFactory } from './browser/browserContextFactory'; +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; + +const packageJSON = require('../../package.json'); + +export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise): Promise { + const config = await resolveConfig(userConfig); + const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config); + return mcpServer.createServer('Playwright', packageJSON.version, new BrowserServerBackend(config, factory), false); +} + +class SimpleBrowserContextFactory implements BrowserContextFactory { + name = 'custom'; + description = 'Connect to a browser using a custom context getter'; + + private readonly _contextGetter: () => Promise; + + constructor(contextGetter: () => Promise) { + this._contextGetter = contextGetter; + } + + async createContext(): Promise<{ browserContext: BrowserContext, close: () => Promise }> { + const browserContext = await this._contextGetter(); + return { + browserContext, + close: () => browserContext.close() + }; + } +} diff --git a/packages/playwright/src/mcp/test/tool.ts b/packages/playwright/src/mcp/log.ts similarity index 60% rename from packages/playwright/src/mcp/test/tool.ts rename to packages/playwright/src/mcp/log.ts index 165c840b02f3a..072ec9ad47447 100644 --- a/packages/playwright/src/mcp/test/tool.ts +++ b/packages/playwright/src/mcp/log.ts @@ -14,15 +14,12 @@ * limitations under the License. */ -import type { z } from 'zod'; -import type { Context } from './context.js'; -import type * as mcp from '../sdk/exports.js'; +import { debug } from 'playwright-core/lib/utilsBundle'; -export type Tool = { - schema: mcp.ToolSchema; - handle: (context: Context, params: z.output) => Promise; -}; +const errorsDebug = debug('pw:mcp:errors'); -export function defineTool(tool: Tool): Tool { - return tool; +export function logUnhandledError(error: unknown) { + errorsDebug(error); } + +export const testDebug = debug('pw:mcp:test'); diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts new file mode 100644 index 0000000000000..561472e85aa43 --- /dev/null +++ b/packages/playwright/src/mcp/program.ts @@ -0,0 +1,135 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { program, ProgramOption } from 'playwright-core/lib/utilsBundle'; +import * as mcpServer from './sdk/server'; +import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; +import { Context } from './browser/context'; +import { contextFactory } from './browser/browserContextFactory'; +import { ProxyBackend } from './sdk/proxyBackend'; +import { BrowserServerBackend } from './browser/browserServerBackend'; +import { ExtensionContextFactory } from './extension/extensionContextFactory'; + +import type { MCPProvider } from './sdk/proxyBackend'; + +const packageJSON = require('../../package.json'); + +program + .version('Version ' + packageJSON.version) + .name('Playwright MCP') + .option('--allowed-origins ', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList) + .option('--blocked-origins ', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList) + .option('--block-service-workers', 'block service workers') + .option('--browser ', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.') + .option('--caps ', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList) + .option('--cdp-endpoint ', 'CDP endpoint to connect to.') + .option('--config ', 'path to the configuration file.') + .option('--device ', 'device to emulate, for example: "iPhone 15"') + .option('--executable-path ', 'path to the browser executable.') + .option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.') + .option('--headless', 'run browser in headless mode, headed by default') + .option('--host ', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.') + .option('--ignore-https-errors', 'ignore https errors') + .option('--isolated', 'keep the browser profile in memory, do not save it to disk.') + .option('--image-responses ', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".') + .option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.') + .option('--output-dir ', 'path to the directory for output files.') + .option('--port ', 'port to listen on for SSE transport.') + .option('--proxy-bypass ', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"') + .option('--proxy-server ', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"') + .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.') + .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.') + .option('--storage-state ', 'path to the storage state file for isolated sessions.') + .option('--user-agent ', 'specify user agent string') + .option('--user-data-dir ', 'path to the user data directory. If not specified, a temporary directory will be created.') + .option('--viewport-size ', 'specify browser viewport size in pixels, for example "1280, 720"') + .addOption(new ProgramOption('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp()) + .addOption(new ProgramOption('--vision', 'Legacy option, use --caps=vision instead').hideHelp()) + .action(async options => { + setupExitWatchdog(); + + if (options.vision) { + // eslint-disable-next-line no-console + console.error('The --vision option is deprecated, use --caps=vision instead'); + options.caps = 'vision'; + } + + const config = await resolveCLIConfig(options); + const browserContextFactory = contextFactory(config); + const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir, config.browser.launchOptions.executablePath); + + if (options.extension) { + const serverBackendFactory: mcpServer.ServerBackendFactory = { + name: 'Playwright w/ extension', + nameInConfig: 'playwright-extension', + version: packageJSON.version, + create: () => new BrowserServerBackend(config, extensionContextFactory) + }; + await mcpServer.start(serverBackendFactory, config.server); + return; + } + + if (options.connectTool) { + const providers: MCPProvider[] = [ + { + name: 'default', + description: 'Starts standalone browser', + connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, browserContextFactory)), + }, + { + name: 'extension', + description: 'Connect to a browser using the Playwright MCP extension', + connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, extensionContextFactory)), + }, + ]; + const factory: mcpServer.ServerBackendFactory = { + name: 'Playwright w/ switch', + nameInConfig: 'playwright-switch', + version: packageJSON.version, + create: () => new ProxyBackend(providers), + }; + await mcpServer.start(factory, config.server); + return; + } + + const factory: mcpServer.ServerBackendFactory = { + name: 'Playwright', + nameInConfig: 'playwright', + version: packageJSON.version, + create: () => new BrowserServerBackend(config, browserContextFactory) + }; + await mcpServer.start(factory, config.server); + }); + +function setupExitWatchdog() { + let isExiting = false; + const handleExit = async () => { + if (isExiting) + return; + isExiting = true; + // eslint-disable-next-line no-restricted-properties + setTimeout(() => process.exit(0), 15000); + await Context.disposeAll(); + // eslint-disable-next-line no-restricted-properties + process.exit(0); + }; + + process.stdin.on('close', handleExit); + process.on('SIGINT', handleExit); + process.on('SIGTERM', handleExit); +} + +void program.parseAsync(process.argv); diff --git a/packages/playwright/src/mcp/sdk/DEPS.list b/packages/playwright/src/mcp/sdk/DEPS.list index e43dcb5d9f30b..184eda1c16070 100644 --- a/packages/playwright/src/mcp/sdk/DEPS.list +++ b/packages/playwright/src/mcp/sdk/DEPS.list @@ -1 +1,2 @@ [*] +../../mcpBundleImpl diff --git a/packages/playwright/src/mcp/sdk/README.md b/packages/playwright/src/mcp/sdk/README.md new file mode 100644 index 0000000000000..b8b280e5a0d05 --- /dev/null +++ b/packages/playwright/src/mcp/sdk/README.md @@ -0,0 +1 @@ +- Generic MCP utils, no dependencies on anything. diff --git a/packages/playwright/src/mcp/sdk/bundle.ts b/packages/playwright/src/mcp/sdk/bundle.ts index a03bb6712bac7..ce64a07f2adc2 100644 --- a/packages/playwright/src/mcp/sdk/bundle.ts +++ b/packages/playwright/src/mcp/sdk/bundle.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -const bundle = require('../../mcpBundleImpl'); +// @ts-ignore +import * as bundle from '../../mcpBundleImpl'; + const zodToJsonSchema: typeof import('zod-to-json-schema').zodToJsonSchema = bundle.zodToJsonSchema; const Client: typeof import('@modelcontextprotocol/sdk/client/index.js').Client = bundle.Client; const Server: typeof import('@modelcontextprotocol/sdk/server/index.js').Server = bundle.Server; diff --git a/packages/playwright/src/mcp/sdk/call.ts b/packages/playwright/src/mcp/sdk/call.ts deleted file mode 100644 index 2818867f801ea..0000000000000 --- a/packages/playwright/src/mcp/sdk/call.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as mcpBundle from './bundle.js'; - -import type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/sdk/types'; - -export async function callTool(mcpUrl: string, name: string, params: CallToolRequest['params']['arguments']): Promise { - const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(mcpUrl)); - const client = new mcpBundle.Client({ name: 'Internal', version: '0.0.0' }); - await client.connect(transport); - try { - return await client.callTool({ name, arguments: params }) as CallToolResult; - } finally { - await transport.terminateSession(); - await client.close(); - } -} diff --git a/packages/playwright/src/mcp/sdk/exports.ts b/packages/playwright/src/mcp/sdk/exports.ts index 12a1cb87dab4e..92c0c176ba020 100644 --- a/packages/playwright/src/mcp/sdk/exports.ts +++ b/packages/playwright/src/mcp/sdk/exports.ts @@ -19,4 +19,4 @@ export * from './proxyBackend'; export * from './server'; export * from './tool'; export * from './http'; -export * from './call'; +export * from './mdb'; diff --git a/packages/playwright/src/mcp/sdk/http.ts b/packages/playwright/src/mcp/sdk/http.ts index 93b64bdefd961..d9359880a3686 100644 --- a/packages/playwright/src/mcp/sdk/http.ts +++ b/packages/playwright/src/mcp/sdk/http.ts @@ -18,20 +18,22 @@ import assert from 'assert'; import net from 'net'; import http from 'http'; import crypto from 'crypto'; + import { debug } from 'playwright-core/lib/utilsBundle'; -import * as mcp from './bundle'; -import { connect } from './server'; +import * as mcpBundle from './bundle'; +import * as mcpServer from './server'; +import type { ServerBackendFactory } from './server'; import type { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import type { ServerBackendFactory } from './server'; const testDebug = debug('pw:mcp:test'); export async function startHttpServer(config: { host?: string, port?: number }, abortSignal?: AbortSignal): Promise { const { host, port } = config; const httpServer = http.createServer(); + decorateServer(httpServer); await new Promise((resolve, reject) => { httpServer.on('error', reject); abortSignal?.addEventListener('abort', () => { @@ -85,10 +87,10 @@ async function handleSSE(serverBackendFactory: ServerBackendFactory, req: http.I return await transport.handlePostMessage(req, res); } else if (req.method === 'GET') { - const transport = new mcp.SSEServerTransport('/sse', res); + const transport = new mcpBundle.SSEServerTransport('/sse', res); sessions.set(transport.sessionId, transport); testDebug(`create SSE session: ${transport.sessionId}`); - await connect(serverBackendFactory, transport, false); + await mcpServer.connect(serverBackendFactory, transport, false); res.on('close', () => { testDebug(`delete SSE session: ${transport.sessionId}`); sessions.delete(transport.sessionId); @@ -113,11 +115,11 @@ async function handleStreamable(serverBackendFactory: ServerBackendFactory, req: } if (req.method === 'POST') { - const transport = new mcp.StreamableHTTPServerTransport({ + const transport = new mcpBundle.StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID(), onsessioninitialized: async sessionId => { testDebug(`create http session: ${transport.sessionId}`); - await connect(serverBackendFactory, transport, true); + await mcpServer.connect(serverBackendFactory, transport, true); sessions.set(sessionId, transport); } }); @@ -136,3 +138,19 @@ async function handleStreamable(serverBackendFactory: ServerBackendFactory, req: res.statusCode = 400; res.end('Invalid request'); } + +function decorateServer(server: net.Server) { + const sockets = new Set(); + server.on('connection', socket => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + }); + + const close = server.close; + server.close = (callback?: (err?: Error) => void) => { + for (const socket of sockets) + socket.destroy(); + sockets.clear(); + return close.call(server, callback); + }; +} diff --git a/packages/playwright/src/mcp/sdk/mdb.ts b/packages/playwright/src/mcp/sdk/mdb.ts index 1e488654546bb..7fbcc3d188312 100644 --- a/packages/playwright/src/mcp/sdk/mdb.ts +++ b/packages/playwright/src/mcp/sdk/mdb.ts @@ -17,20 +17,18 @@ import { debug } from 'playwright-core/lib/utilsBundle'; import { ManualPromise } from 'playwright-core/lib/utils'; -import { PingRequestSchema, z } from './bundle.js'; -import { StreamableHTTPClientTransport } from './bundle.js'; -import * as mcpBundle from './bundle.js'; +import { defineToolSchema } from './tool'; +import * as mcpBundle from './bundle'; +import * as mcpServer from './server'; +import * as mcpHttp from './http'; +import { wrapInProcess } from './server'; -import { defineToolSchema } from './tool.js'; -import * as mcpServer from './server.js'; -import * as mcpHttp from './http.js'; -import { wrapInProcess } from './server.js'; - -import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; const mdbDebug = debug('pw:mcp:mdb'); const errorsDebug = debug('pw:mcp:errors'); +const z = mcpBundle.z; export class MDBBackend implements mcpServer.ServerBackend { private _stack: { client: Client, toolNames: string[], resultPromise: ManualPromise | undefined }[] = []; @@ -70,6 +68,8 @@ export class MDBBackend implements mcpServer.ServerBackend { await entry.client.close(); entry = this._stack[0]; } + if (!entry) + throw new Error(`Tool ${name} not found in the tool stack`); const resultPromise = new ManualPromise(); entry.resultPromise = resultPromise; @@ -104,7 +104,7 @@ export class MDBBackend implements mcpServer.ServerBackend { private async _pushTools(params: { mcpUrl: string, introMessage?: string }): Promise { mdbDebug('pushing tools to the stack', params.mcpUrl); - const transport = new StreamableHTTPClientTransport(new URL(params.mcpUrl)); + const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(params.mcpUrl)); await this._pushClient(transport, params.introMessage); return { content: [{ type: 'text', text: 'Tools pushed' }] }; } @@ -112,7 +112,7 @@ export class MDBBackend implements mcpServer.ServerBackend { private async _pushClient(transport: Transport, introMessage?: string): Promise { mdbDebug('pushing client to the stack'); const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' }); - client.setRequestHandler(PingRequestSchema, () => ({})); + client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); await client.connect(transport); mdbDebug('connected to the new client'); const { tools } = await client.listTools(); @@ -153,7 +153,7 @@ export async function runMainBackend(backendFactory: mcpServer.ServerBackendFact create: () => mdbBackend }; const url = await startAsHttp(factory, { port: options?.port || 0 }); - process.env.PLAYWRIGHT_MDB_URL = url; + process.env.PLAYWRIGHT_DEBUGGER_MCP = url; if (options?.port !== undefined) return url; @@ -177,8 +177,8 @@ export async function runOnPauseBackendLoop(mdbUrl: string, backend: ServerBacke const url = mcpHttp.httpAddressToString(httpServer.address()); const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' }); - client.setRequestHandler(PingRequestSchema, () => ({})); - const transport = new StreamableHTTPClientTransport(new URL(mdbUrl)); + client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); + const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(mdbUrl)); await client.connect(transport); const pushToolsResult = await client.callTool({ diff --git a/packages/playwright/src/mcp/sdk/proxyBackend.ts b/packages/playwright/src/mcp/sdk/proxyBackend.ts index 2f7530ebcf4c1..de55736947a39 100644 --- a/packages/playwright/src/mcp/sdk/proxyBackend.ts +++ b/packages/playwright/src/mcp/sdk/proxyBackend.ts @@ -15,9 +15,10 @@ */ import { debug } from 'playwright-core/lib/utilsBundle'; -import * as mcp from './bundle'; -import type { ServerBackend, ClientVersion, Root, Server } from './server.js'; +import * as mcpBundle from './bundle'; + +import type { ServerBackend, ClientVersion, Root, Server } from './server'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; @@ -29,6 +30,7 @@ export type MCPProvider = { }; const errorsDebug = debug('pw:mcp:errors'); +const { z, zodToJsonSchema } = mcpBundle; export class ProxyBackend implements ServerBackend { private _mcpProviders: MCPProvider[]; @@ -94,8 +96,8 @@ export class ProxyBackend implements ServerBackend { 'Connect to a browser using one of the available methods:', ...this._mcpProviders.map(factory => `- "${factory.name}": ${factory.description}`), ].join('\n'), - inputSchema: mcp.zodToJsonSchema(mcp.z.object({ - name: mcp.z.enum(this._mcpProviders.map(factory => factory.name) as [string, ...string[]]).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'), + inputSchema: zodToJsonSchema(z.object({ + name: z.enum(this._mcpProviders.map(factory => factory.name) as [string, ...string[]]).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'), }), { strictUnions: true }) as Tool['inputSchema'], annotations: { title: 'Connect to a browser context', @@ -109,14 +111,14 @@ export class ProxyBackend implements ServerBackend { await this._currentClient?.close(); this._currentClient = undefined; - const client = new mcp.Client({ name: 'Playwright MCP Proxy', version: '0.0.0' }); + const client = new mcpBundle.Client({ name: 'Playwright MCP Proxy', version: '0.0.0' }); client.registerCapabilities({ roots: { listRoots: true, }, }); - client.setRequestHandler(mcp.ListRootsRequestSchema, () => ({ roots: this._roots })); - client.setRequestHandler(mcp.PingRequestSchema, () => ({})); + client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots })); + client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); const transport = await factory.connect(); await client.connect(transport); diff --git a/packages/playwright/src/mcp/sdk/server.ts b/packages/playwright/src/mcp/sdk/server.ts index 74543a9fca137..bf80d80494050 100644 --- a/packages/playwright/src/mcp/sdk/server.ts +++ b/packages/playwright/src/mcp/sdk/server.ts @@ -16,20 +16,21 @@ import { debug } from 'playwright-core/lib/utilsBundle'; -import * as mcp from './bundle'; -import { InProcessTransport } from './inProcessTransport'; +import * as mcpBundle from './bundle'; import { httpAddressToString, installHttpTransport, startHttpServer } from './http'; +import { InProcessTransport } from './inProcessTransport'; import type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; -import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; export type { Server } from '@modelcontextprotocol/sdk/server/index.js'; export type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js'; +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; const serverDebug = debug('pw:mcp:server'); const errorsDebug = debug('pw:mcp:errors'); export type ClientVersion = { name: string, version: string }; + export interface ServerBackend { initialize?(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise; listTools(): Promise; @@ -57,13 +58,13 @@ export async function wrapInProcess(backend: ServerBackend): Promise export function createServer(name: string, version: string, backend: ServerBackend, runHeartbeat: boolean): Server { let initializedPromiseResolve = () => {}; const initializedPromise = new Promise(resolve => initializedPromiseResolve = resolve); - const server = new mcp.Server({ name, version }, { + const server = new mcpBundle.Server({ name, version }, { capabilities: { tools: {}, } }); - server.setRequestHandler(mcp.ListToolsRequestSchema, async () => { + server.setRequestHandler(mcpBundle.ListToolsRequestSchema, async () => { serverDebug('listTools'); await initializedPromise; const tools = await backend.listTools(); @@ -71,7 +72,7 @@ export function createServer(name: string, version: string, backend: ServerBacke }); let heartbeatRunning = false; - server.setRequestHandler(mcp.CallToolRequestSchema, async request => { + server.setRequestHandler(mcpBundle.CallToolRequestSchema, async request => { serverDebug('callTool', request); await initializedPromise; @@ -133,7 +134,7 @@ function addServerListener(server: Server, event: 'close' | 'initialized', liste export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number }) { if (options.port === undefined) { - await connect(serverBackendFactory, new mcp.StdioServerTransport(), false); + await connect(serverBackendFactory, new mcpBundle.StdioServerTransport(), false); return; } diff --git a/packages/playwright/src/mcp/sdk/tool.ts b/packages/playwright/src/mcp/sdk/tool.ts index 450170bab3915..bd94547a30e4d 100644 --- a/packages/playwright/src/mcp/sdk/tool.ts +++ b/packages/playwright/src/mcp/sdk/tool.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { zodToJsonSchema } from './bundle.js'; +import { zodToJsonSchema } from '../sdk/bundle'; import type { z } from 'zod'; -import type * as mcpServer from './server.js'; +import type * as mcpServer from './server'; export type ToolSchema = { name: string; diff --git a/packages/playwright/src/mcp/browser/backend.ts b/packages/playwright/src/mcp/test/browserBackend.ts similarity index 87% rename from packages/playwright/src/mcp/browser/backend.ts rename to packages/playwright/src/mcp/test/browserBackend.ts index aadf7a48ef525..42ded6d8762a7 100644 --- a/packages/playwright/src/mcp/browser/backend.ts +++ b/packages/playwright/src/mcp/test/browserBackend.ts @@ -16,12 +16,11 @@ import * as mcp from '../sdk/exports'; import * as mcpBundle from '../sdk/bundle'; -import { snapshot, pickLocator, evaluate } from './tools'; -import { defineToolSchema } from '../sdk/exports'; -import { runOnPauseBackendLoop } from '../sdk/mdb'; + +import { snapshot, pickLocator, evaluate } from './browserTools'; import { stripAnsiEscapes } from '../../util'; -import type { Tool } from './tool'; +import type { BrowserTool } from './browserTool'; import type * as playwright from '../../../index'; import type { ServerBackendOnPause } from '../sdk/mdb'; @@ -34,7 +33,7 @@ const tools = [snapshot, pickLocator, evaluate]; export class BrowserBackend implements ServerBackendOnPause { readonly name = 'Playwright'; readonly version = '0.0.1'; - private _tools: Tool[] = tools; + private _tools: BrowserTool[] = tools; private _page: playwright.Page; constructor(page: playwright.Page) { @@ -64,7 +63,7 @@ export class BrowserBackend implements ServerBackendOnPause { } } -const doneToolSchema = defineToolSchema({ +const doneToolSchema = mcp.defineToolSchema({ name: 'done', title: 'Done', description: 'Done', @@ -84,5 +83,5 @@ ${snapshot} ### Task Try recovering from the error prior to continuing, use following tools to recover: ${tools.map(tool => tool.schema.name).join(', ')}`; - await runOnPauseBackendLoop(process.env.PLAYWRIGHT_MDB_URL!, new BrowserBackend(page), introMessage); + await mcp.runOnPauseBackendLoop(process.env.PLAYWRIGHT_MDB_URL!, new BrowserBackend(page), introMessage); } diff --git a/packages/playwright/src/mcp/browser/tool.ts b/packages/playwright/src/mcp/test/browserTool.ts similarity index 83% rename from packages/playwright/src/mcp/browser/tool.ts rename to packages/playwright/src/mcp/test/browserTool.ts index 5b09b17480c89..f1b167475d1f2 100644 --- a/packages/playwright/src/mcp/browser/tool.ts +++ b/packages/playwright/src/mcp/test/browserTool.ts @@ -18,11 +18,11 @@ import type { z } from 'zod'; import type * as mcp from '../sdk/exports'; import type * as playwright from '../../../index'; -export type Tool = { +export type BrowserTool = { schema: mcp.ToolSchema; handle: (page: playwright.Page, params: z.output) => Promise; }; -export function defineTool(tool: Tool): Tool { +export function defineBrowserTool(tool: BrowserTool): BrowserTool { return tool; } diff --git a/packages/playwright/src/mcp/test/browserTools.ts b/packages/playwright/src/mcp/test/browserTools.ts new file mode 100644 index 0000000000000..eb55d7f464c71 --- /dev/null +++ b/packages/playwright/src/mcp/test/browserTools.ts @@ -0,0 +1,113 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { asLocator } from 'playwright-core/lib/utils'; + +import { defineBrowserTool } from './browserTool.js'; +import * as mcpBundle from '../sdk/bundle'; + +import type * as playwright from '../../../index'; +import type zod from 'zod'; + +const { z } = mcpBundle; + +type PageEx = playwright.Page & { + _snapshotForAI: () => Promise; +}; + +export const snapshot = defineBrowserTool({ + schema: { + name: 'playwright_test_browser_snapshot', + title: 'Capture page snapshot', + description: 'Capture page snapshot for debugging', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async (page, params) => { + const snapshot = await (page as PageEx)._snapshotForAI(); + return { + content: [ + { + type: 'text', + text: snapshot, + }, + ], + }; + }, +}); + +export const elementSchema = z.object({ + element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'), + ref: z.string().describe('Exact target element reference from the page snapshot'), +}); + +export const pickLocator = defineBrowserTool({ + schema: { + name: 'playwright_test_generate_locator', + title: 'Create locator for element', + description: 'Generate locator for the given element to use in tests', + inputSchema: elementSchema, + type: 'readOnly', + }, + + handle: async (page, params) => { + const locator = await refLocator(page, params); + + try { + const { resolvedSelector } = await (locator as any)._resolveSelector(); + const locatorString = asLocator('javascript', resolvedSelector); + return { content: [{ type: 'text', text: locatorString }] }; + } catch (e) { + throw new Error(`Ref not found, likely because element was removed. Use ${snapshot.schema.name} to see what elements are currently on the page.`); + } + }, +}); + +const evaluateSchema = z.object({ + function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'), + element: z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'), + ref: z.string().optional().describe('Exact target element reference from the page snapshot'), +}); + +export const evaluate = defineBrowserTool({ + schema: { + name: 'playwright_test_evaluate_on_pause', + title: 'Evaluate in page', + description: 'Evaluate JavaScript expression on page or element', + inputSchema: evaluateSchema, + type: 'destructive', + }, + + handle: async (page, params) => { + let locator: playwright.Locator | undefined; + if (params.ref && params.element) + locator = await refLocator(page, { ref: params.ref, element: params.element }); + + const receiver = locator ?? page as any; + const result = await receiver._evaluateFunction(params.function); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) || 'undefined' }], + }; + }, +}); + +async function refLocator(page: playwright.Page, elementRef: zod.output): Promise { + const snapshot = await (page as PageEx)._snapshotForAI(); + if (!snapshot.includes(`[ref=${elementRef.ref}]`)) + throw new Error(`Ref ${elementRef.ref} not found in the current page snapshot. Try capturing new snapshot.`); + return page.locator(`aria-ref=${elementRef.ref}`).describe(elementRef.element); +} diff --git a/packages/playwright/src/mcp/test/backend.ts b/packages/playwright/src/mcp/test/testBackend.ts similarity index 79% rename from packages/playwright/src/mcp/test/backend.ts rename to packages/playwright/src/mcp/test/testBackend.ts index ca37bbcd18a99..0ef240a2ad544 100644 --- a/packages/playwright/src/mcp/test/backend.ts +++ b/packages/playwright/src/mcp/test/testBackend.ts @@ -14,23 +14,23 @@ * limitations under the License. */ -import * as mcp from '../sdk/exports.js'; -import { Context } from './context'; -import { listTests, runTests, debugTest } from './tools.js'; -import { snapshot, pickLocator, evaluate } from '../browser/tools'; +import * as mcp from '../sdk/exports'; +import { TestContext } from './testContext'; +import { listTests, runTests, debugTest } from './testTools.js'; +import { snapshot, pickLocator, evaluate } from './browserTools'; import type { ConfigLocation } from '../../common/config'; -import type { Tool } from './tool'; +import type { TestTool } from './testTool'; export class TestServerBackend implements mcp.ServerBackend { readonly name = 'Playwright'; readonly version = '0.0.1'; - private _tools: Tool[] = [listTests, runTests, debugTest]; - private _context: Context; + private _tools: TestTool[] = [listTests, runTests, debugTest]; + private _context: TestContext; constructor(resolvedLocation: ConfigLocation, options?: { muteConsole?: boolean }) { - this._context = new Context(resolvedLocation, options); + this._context = new TestContext(resolvedLocation, options); } async listTools(): Promise { diff --git a/packages/playwright/src/mcp/test/context.ts b/packages/playwright/src/mcp/test/testContext.ts similarity index 98% rename from packages/playwright/src/mcp/test/context.ts rename to packages/playwright/src/mcp/test/testContext.ts index 602abc97e1300..11b5ab5d013c3 100644 --- a/packages/playwright/src/mcp/test/context.ts +++ b/packages/playwright/src/mcp/test/testContext.ts @@ -18,7 +18,7 @@ import { TestRunner, TestRunnerEvent } from '../../runner/testRunner'; import type { ConfigLocation } from '../../common/config'; -export class Context { +export class TestContext { private _testRunner: TestRunner | undefined; readonly configLocation: ConfigLocation; readonly options?: { muteConsole?: boolean }; diff --git a/packages/playwright/src/mcp/test/testTool.ts b/packages/playwright/src/mcp/test/testTool.ts new file mode 100644 index 0000000000000..05a3e5212bb39 --- /dev/null +++ b/packages/playwright/src/mcp/test/testTool.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { z } from 'zod'; +import type { TestContext } from './testContext.js'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { ToolSchema } from '../sdk/tool.js'; + +export type TestTool = { + schema: ToolSchema; + handle: (context: TestContext, params: z.output) => Promise; +}; + +export function defineTestTool(tool: TestTool): TestTool { + return tool; +} diff --git a/packages/playwright/src/mcp/test/tools.ts b/packages/playwright/src/mcp/test/testTools.ts similarity index 96% rename from packages/playwright/src/mcp/test/tools.ts rename to packages/playwright/src/mcp/test/testTools.ts index bae91402fd344..08c74e982c085 100644 --- a/packages/playwright/src/mcp/test/tools.ts +++ b/packages/playwright/src/mcp/test/testTools.ts @@ -21,10 +21,10 @@ import { terminalScreen } from '../../reporters/base'; import ListReporter from '../../reporters/list'; import ListModeReporter from '../../reporters/listModeReporter'; -import { defineTool } from './tool'; +import { defineTestTool } from './testTool'; import { StringWriteStream } from './streams'; -export const listTests = defineTool({ +export const listTests = defineTestTool({ schema: { name: 'playwright_test_list_tests', title: 'List tests', @@ -45,7 +45,7 @@ export const listTests = defineTool({ }, }); -export const runTests = defineTool({ +export const runTests = defineTestTool({ schema: { name: 'playwright_test_run_tests', title: 'Run tests', @@ -77,7 +77,7 @@ export const runTests = defineTool({ }, }); -export const debugTest = defineTool({ +export const debugTest = defineTestTool({ schema: { name: 'playwright_test_debug_test', title: 'Debug single test', diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index b23c389ca2ce3..70731b8cb49db 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -33,9 +33,8 @@ import * as testServer from './runner/testServer'; import { runWatchModeLoop } from './runner/watchMode'; import { runAllTestsWithConfig, TestRunner } from './runner/testRunner'; import { createErrorCollectingReporter } from './runner/reporters'; -import { ServerBackendFactory } from './mcp/sdk/server'; -import { TestServerBackend } from './mcp/test/backend'; -import { runMainBackend } from './mcp/sdk/mdb'; +import { ServerBackendFactory, runMainBackend } from './mcp/sdk/exports'; +import { TestServerBackend } from './mcp/test/testBackend'; import type { ConfigCLIOverrides } from './common/ipc'; import type { TraceMode } from '../types/test'; diff --git a/tests/config/testserver/index.ts b/tests/config/testserver/index.ts index ddd334e90e650..177ea131d02e7 100644 --- a/tests/config/testserver/index.ts +++ b/tests/config/testserver/index.ts @@ -56,6 +56,7 @@ export class TestServer { readonly EMPTY_PAGE: string; readonly HOST: string; readonly HOSTNAME: string; + readonly HELLO_WORLD: string; static async create(dirPath: string, port: number, loopback?: string): Promise { const server = new TestServer(dirPath, port, loopback); @@ -122,6 +123,7 @@ export class TestServer { this.EMPTY_PAGE = `${protocol}://${same_origin}:${port}/empty.html`; this.HOST = new URL(this.EMPTY_PAGE).host; this.HOSTNAME = new URL(this.EMPTY_PAGE).hostname; + this.HELLO_WORLD = `${this.PREFIX}/hello-world`; } async waitUntilReady() { @@ -165,6 +167,13 @@ export class TestServer { await new Promise(x => this._server.close(x)); } + setContent(path: string, content: string, mimeType: string) { + this.setRoute(path, (req, res) => { + res.writeHead(200, { 'Content-Type': mimeType }); + res.end(mimeType === 'text/html' ? `${content}` : content); + }); + } + setRoute(path: string, handler: (arg0: http.IncomingMessage & { postBody: Promise }, arg1: http.ServerResponse) => any) { this._routes.set(path, handler); } diff --git a/tests/mcp/capabilities.spec.ts b/tests/mcp/capabilities.spec.ts new file mode 100644 index 0000000000000..7ec699aa6a50e --- /dev/null +++ b/tests/mcp/capabilities.spec.ts @@ -0,0 +1,106 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('test snapshot tool list', async ({ client }) => { + const { tools } = await client.listTools(); + expect(new Set(tools.map(t => t.name))).toEqual(new Set([ + 'browser_click', + 'browser_console_messages', + 'browser_drag', + 'browser_evaluate', + 'browser_file_upload', + 'browser_fill_form', + 'browser_handle_dialog', + 'browser_hover', + 'browser_select_option', + 'browser_type', + 'browser_close', + 'browser_install', + 'browser_navigate_back', + 'browser_navigate', + 'browser_network_requests', + 'browser_press_key', + 'browser_resize', + 'browser_snapshot', + 'browser_tabs', + 'browser_take_screenshot', + 'browser_wait_for', + ])); +}); + +test('test tool list proxy mode', async ({ startClient }) => { + const { client } = await startClient({ + args: ['--connect-tool'], + }); + const { tools } = await client.listTools(); + expect(new Set(tools.map(t => t.name))).toEqual(new Set([ + 'browser_click', + 'browser_connect', // the extra tool + 'browser_console_messages', + 'browser_drag', + 'browser_evaluate', + 'browser_file_upload', + 'browser_fill_form', + 'browser_handle_dialog', + 'browser_hover', + 'browser_select_option', + 'browser_type', + 'browser_close', + 'browser_install', + 'browser_navigate_back', + 'browser_navigate', + 'browser_network_requests', + 'browser_press_key', + 'browser_resize', + 'browser_snapshot', + 'browser_tabs', + 'browser_take_screenshot', + 'browser_wait_for', + ])); +}); + +test('test capabilities (pdf)', async ({ startClient }) => { + const { client } = await startClient({ + args: ['--caps=pdf'], + }); + const { tools } = await client.listTools(); + const toolNames = tools.map(t => t.name); + expect(toolNames).toContain('browser_pdf_save'); +}); + +test('test capabilities (vision)', async ({ startClient }) => { + const { client } = await startClient({ + args: ['--caps=vision'], + }); + const { tools } = await client.listTools(); + const toolNames = tools.map(t => t.name); + expect(toolNames).toContain('browser_mouse_move_xy'); + expect(toolNames).toContain('browser_mouse_click_xy'); + expect(toolNames).toContain('browser_mouse_drag_xy'); +}); + +test('support for legacy --vision option', async ({ startClient }) => { + const { client } = await startClient({ + args: ['--vision'], + }); + const { tools } = await client.listTools(); + const toolNames = tools.map(t => t.name); + expect(toolNames).toContain('browser_mouse_move_xy'); + expect(toolNames).toContain('browser_mouse_click_xy'); + expect(toolNames).toContain('browser_mouse_drag_xy'); +}); diff --git a/tests/mcp/cdp.spec.ts b/tests/mcp/cdp.spec.ts new file mode 100644 index 0000000000000..90ae1620bd547 --- /dev/null +++ b/tests/mcp/cdp.spec.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawnSync } from 'child_process'; +import { test, expect, programPath } from './fixtures'; + +test('cdp server', async ({ cdpServer, startClient, server }) => { + await cdpServer.start(); + const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); +}); + +test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => { + const browserContext = await cdpServer.start(); + const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); + + const [page] = browserContext.pages(); + await page.goto(server.HELLO_WORLD); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Hello, world!', + ref: 'f0', + }, + })).toHaveResponse({ + result: `Error: No open pages available. Use the "browser_navigate" tool to navigate to a page first.`, + isError: true, + }); + + expect(await client.callTool({ + name: 'browser_snapshot', + })).toHaveResponse({ + pageState: expect.stringContaining(`- Page URL: ${server.HELLO_WORLD} +- Page Title: Title +- Page Snapshot: +\`\`\`yaml +- generic [active] [ref=e1]: Hello, world! +\`\`\``), + }); +}); + +test('should throw connection error and allow re-connecting', async ({ cdpServer, startClient, server }) => { + const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); + + server.setContent('/', ` + Title + Hello, world! + `, 'text/html'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + result: expect.stringContaining(`Error: browserType.connectOverCDP: connect ECONNREFUSED`), + isError: true, + }); + await cdpServer.start(); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); +}); + +test('does not support --device', async () => { + const result = spawnSync('node', [ + programPath, '--device=Pixel 5', '--cdp-endpoint=http://localhost:1234', + ]); + expect(result.error).toBeUndefined(); + expect(result.status).toBe(1); + expect(result.stderr.toString()).toContain('Device emulation is not supported with cdpEndpoint.'); +}); diff --git a/tests/mcp/click.spec.ts b/tests/mcp/click.spec.ts new file mode 100644 index 0000000000000..53db23152dccd --- /dev/null +++ b/tests/mcp/click.spec.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_click', async ({ client, server, mcpBrowser }) => { + server.setContent('/', ` + Title + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Submit button', + ref: 'e2', + }, + })).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Submit' }).click();`, + pageState: expect.stringContaining(`- button "Submit" ${mcpBrowser !== 'webkit' || process.platform === 'linux' ? '[active] ' : ''}[ref=e2]`), + }); +}); + +test('browser_click (double)', async ({ client, server }) => { + server.setContent('/', ` + Title + +

Click me

+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Click me', + ref: 'e2', + doubleClick: true, + }, + })).toHaveResponse({ + code: `await page.getByRole('heading', { name: 'Click me' }).dblclick();`, + pageState: expect.stringContaining(`- heading "Double clicked" [level=1] [ref=e3]`), + }); +}); + +test('browser_click (right)', async ({ client, server }) => { + server.setContent('/', ` + + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + const result = await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Menu', + ref: 'e2', + button: 'right', + }, + }); + expect(result).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Menu' }).click({ button: 'right' });`, + pageState: expect.stringContaining(`- button "Right clicked"`), + }); +}); diff --git a/tests/mcp/config.spec.ts b/tests/mcp/config.spec.ts new file mode 100644 index 0000000000000..a6d8151e12b30 --- /dev/null +++ b/tests/mcp/config.spec.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'node:fs'; + +import { test, expect } from './fixtures'; +import { configFromCLIOptions } from '../../packages/playwright/lib/mcp/browser/config'; +import type { Config } from '../../packages/playwright/src/mcp/config'; + +test('config user data dir', async ({ startClient, server, mcpMode }, testInfo) => { + server.setContent('/', ` + Title + Hello, world! + `, 'text/html'); + + const config: Config = { + browser: { + userDataDir: testInfo.outputPath('user-data-dir'), + }, + }; + const configPath = testInfo.outputPath('config.json'); + await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2)); + + const { client } = await startClient({ args: ['--config', configPath] }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`Hello, world!`), + }); + + const files = await fs.promises.readdir(config.browser!.userDataDir!); + expect(files.length).toBeGreaterThan(0); +}); + +test.describe(() => { + test.use({ mcpBrowser: '' }); + test('browserName', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/458' } }, async ({ startClient, mcpMode }, testInfo) => { + const config: Config = { + browser: { + browserName: 'firefox', + }, + }; + const configPath = testInfo.outputPath('config.json'); + await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2)); + + const { client } = await startClient({ args: ['--config', configPath] }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: 'data:text/html,' }, + })).toHaveResponse({ + pageState: expect.stringContaining(`Firefox`), + }); + }); +}); + +test.describe('sandbox configuration', () => { + test('should enable sandbox by default (no --no-sandbox flag)', async () => { + const config = configFromCLIOptions({ sandbox: undefined }); + expect(config.browser?.launchOptions?.chromiumSandbox).toBeUndefined(); + }); + + test('should disable sandbox when --no-sandbox flag is passed', async () => { + const config = configFromCLIOptions({ sandbox: false }); + expect(config.browser?.launchOptions?.chromiumSandbox).toBe(false); + }); +}); diff --git a/tests/mcp/console.spec.ts b/tests/mcp/console.spec.ts new file mode 100644 index 0000000000000..872e2730382dd --- /dev/null +++ b/tests/mcp/console.spec.ts @@ -0,0 +1,100 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_console_messages', async ({ client, server }) => { + server.setContent('/', ` + + + + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + const resource = await client.callTool({ + name: 'browser_console_messages', + }); + expect(resource).toHaveResponse({ + result: `[LOG] Hello, world! @ ${server.PREFIX}/:4 +[ERROR] Error @ ${server.PREFIX}/:5`, + }); +}); + +test('browser_console_messages (page error)', async ({ client, server }) => { + server.setContent('/', ` + + + + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + const resource = await client.callTool({ + name: 'browser_console_messages', + }); + expect(resource).toHaveResponse({ + result: expect.stringContaining(`Error: Error in script`), + }); + expect(resource).toHaveResponse({ + result: expect.stringContaining(server.PREFIX), + }); +}); + +test('recent console messages', async ({ client, server }) => { + server.setContent('/', ` + + + + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + const response = await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Click me', + ref: 'e2', + }, + }); + + expect(response).toHaveResponse({ + consoleMessages: expect.stringContaining(`- [LOG] Hello, world! @`), + }); +}); diff --git a/tests/mcp/core.spec.ts b/tests/mcp/core.spec.ts new file mode 100644 index 0000000000000..033a212245d5d --- /dev/null +++ b/tests/mcp/core.spec.ts @@ -0,0 +1,190 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_navigate', async ({ client, server }) => { + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: `await page.goto('${server.HELLO_WORLD}');`, + pageState: `- Page URL: ${server.HELLO_WORLD} +- Page Title: Title +- Page Snapshot: +\`\`\`yaml +- generic [active] [ref=e1]: Hello, world! +\`\`\``, + }); +}); + +test('browser_select_option', async ({ client, server }) => { + server.setContent('/', ` + Title + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_select_option', + arguments: { + element: 'Select', + ref: 'e2', + values: ['bar'], + }, + })).toHaveResponse({ + code: `await page.getByRole('combobox').selectOption(['bar']);`, + pageState: `- Page URL: ${server.PREFIX}/ +- Page Title: Title +- Page Snapshot: +\`\`\`yaml +- combobox [ref=e2]: + - option "Foo" + - option "Bar" [selected] +\`\`\``, + }); +}); + +test('browser_select_option (multiple)', async ({ client, server }) => { + server.setContent('/', ` + Title + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_select_option', + arguments: { + element: 'Select', + ref: 'e2', + values: ['bar', 'baz'], + }, + })).toHaveResponse({ + code: `await page.getByRole('listbox').selectOption(['bar', 'baz']);`, + pageState: expect.stringContaining(` +- listbox [ref=e2]: + - option "Foo" [ref=e3] + - option "Bar" [selected] [ref=e4] + - option "Baz" [selected] [ref=e5]`), + }); +}); + +test('browser_resize', async ({ client, server }) => { + server.setContent('/', ` + Resize Test + +
Waiting for resize...
+ + + `, 'text/html'); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + const response = await client.callTool({ + name: 'browser_resize', + arguments: { + width: 390, + height: 780, + }, + }); + expect(response).toHaveResponse({ + code: `await page.setViewportSize({ width: 390, height: 780 });`, + }); + await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toHaveResponse({ + pageState: expect.stringContaining(`Window size: 390x780`), + }); +}); + +test('old locator error message', async ({ client, server }) => { + server.setContent('/', ` + + + + `, 'text/html'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + })).toHaveResponse({ + pageState: expect.stringContaining(` + - button "Button 1" [ref=e2] + - button "Button 2" [ref=e3]`), + }); + + await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button 1', + ref: 'e2', + }, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button 2', + ref: 'e3', + }, + })).toHaveResponse({ + result: expect.stringContaining(`Ref e3 not found in the current page snapshot. Try capturing new snapshot.`), + isError: true, + }); +}); + +test('visibility: hidden > visible should be shown', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/535' } }, async ({ client, server }) => { + server.setContent('/', ` +
+
+ +
+
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_snapshot' + })).toHaveResponse({ + pageState: expect.stringContaining(`- button "Button"`), + }); +}); diff --git a/tests/mcp/device.spec.ts b/tests/mcp/device.spec.ts new file mode 100644 index 0000000000000..5c0f733101bd0 --- /dev/null +++ b/tests/mcp/device.spec.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('--device should work', async ({ startClient, server, mcpMode }) => { + const { client } = await startClient({ + args: ['--device', 'iPhone 15'], + }); + + server.setRoute('/', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + + + + + `); + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + })).toHaveResponse({ + pageState: expect.stringContaining(`393x659`), + }); +}); diff --git a/tests/mcp/dialogs.spec.ts b/tests/mcp/dialogs.spec.ts new file mode 100644 index 0000000000000..4c2bf98eb5025 --- /dev/null +++ b/tests/mcp/dialogs.spec.ts @@ -0,0 +1,255 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('alert dialog', async ({ client, server }) => { + server.setContent('/', ``, 'text/html'); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- button "Button" [ref=e2]`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button', + ref: 'e2', + }, + })).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Button' }).click();`, + modalState: `- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button', + ref: 'e2', + }, + })).toHaveResponse({ + code: undefined, + modalState: `- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`, + }); + + expect(await client.callTool({ + name: 'browser_handle_dialog', + arguments: { + accept: true, + }, + })).toHaveResponse({ + modalState: undefined, + pageState: expect.stringContaining(`- button "Button"`), + }); +}); + +test('two alert dialogs', async ({ client, server }) => { + server.setContent('/', ` + Title + + + + `, 'text/html'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- button "Button" [ref=e2]`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button', + ref: 'e2', + }, + })).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Button' }).click();`, + modalState: expect.stringContaining(`- ["alert" dialog with message "Alert 1"]: can be handled by the "browser_handle_dialog" tool`), + }); + + const result = await client.callTool({ + name: 'browser_handle_dialog', + arguments: { + accept: true, + }, + }); + + expect(result).toHaveResponse({ + modalState: expect.stringContaining(`- ["alert" dialog with message "Alert 2"]: can be handled by the "browser_handle_dialog" tool`), + }); + + const result2 = await client.callTool({ + name: 'browser_handle_dialog', + arguments: { + accept: true, + }, + }); + + expect(result2).not.toHaveResponse({ + modalState: expect.stringContaining(`- ["alert" dialog with message "Alert 2"]: can be handled by the "browser_handle_dialog" tool`), + }); +}); + +test('confirm dialog (true)', async ({ client, server }) => { + server.setContent('/', ` + Title + + + + `, 'text/html'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- button "Button" [ref=e2]`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button', + ref: 'e2', + }, + })).toHaveResponse({ + modalState: expect.stringContaining(`- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool`), + }); + + expect(await client.callTool({ + name: 'browser_handle_dialog', + arguments: { + accept: true, + }, + })).toHaveResponse({ + modalState: undefined, + pageState: expect.stringContaining(`- generic [active] [ref=e1]: "true"`), + }); +}); + +test('confirm dialog (false)', async ({ client, server }) => { + server.setContent('/', ` + Title + + + + `, 'text/html'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- button "Button" [ref=e2]`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button', + ref: 'e2', + }, + })).toHaveResponse({ + modalState: expect.stringContaining(`- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool`), + }); + + expect(await client.callTool({ + name: 'browser_handle_dialog', + arguments: { + accept: false, + }, + })).toHaveResponse({ + modalState: undefined, + pageState: expect.stringContaining(`- generic [active] [ref=e1]: "false"`), + }); +}); + +test('prompt dialog', async ({ client, server }) => { + server.setContent('/', ` + Title + + + + `, 'text/html'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- button "Button" [ref=e2]`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button', + ref: 'e2', + }, + })).toHaveResponse({ + modalState: expect.stringContaining(`- ["prompt" dialog with message "Prompt"]: can be handled by the "browser_handle_dialog" tool`), + }); + + const result = await client.callTool({ + name: 'browser_handle_dialog', + arguments: { + accept: true, + promptText: 'Answer', + }, + }); + + expect(result).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Answer`), + }); +}); + +test('alert dialog w/ race', async ({ client, server }) => { + server.setContent('/', ``, 'text/html'); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- button "Button" [ref=e2]`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button', + ref: 'e2', + }, + })).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Button' }).click();`, + modalState: expect.stringContaining(`- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`), + }); + + const result = await client.callTool({ + name: 'browser_handle_dialog', + arguments: { + accept: true, + }, + }); + + expect(result).toHaveResponse({ + modalState: undefined, + pageState: expect.stringContaining(`- Page URL: ${server.PREFIX}/ +- Page Title: +- Page Snapshot: +\`\`\`yaml +- button "Button"`), + }); +}); diff --git a/tests/mcp/evaluate.spec.ts b/tests/mcp/evaluate.spec.ts new file mode 100644 index 0000000000000..68c8a39a398db --- /dev/null +++ b/tests/mcp/evaluate.spec.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_evaluate', async ({ client, server }) => { + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- Page Title: Title`), + }); + + expect(await client.callTool({ + name: 'browser_evaluate', + arguments: { + function: '() => document.title', + }, + })).toHaveResponse({ + result: `"Title"`, + code: `await page.evaluate('() => document.title');`, + }); +}); + +test('browser_evaluate (element)', async ({ client, server }) => { + server.setContent('/', ` + Hello, world! + `, 'text/html'); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_evaluate', + arguments: { + function: 'element => element.style.backgroundColor', + element: 'body', + ref: 'e1', + }, + })).toHaveResponse({ + result: `"red"`, + code: `await page.getByText('Hello, world!').evaluate('element => element.style.backgroundColor');`, + }); +}); + +test('browser_evaluate object', async ({ client, server }) => { + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- Page Title: Title`), + }); + + expect(await client.callTool({ + name: 'browser_evaluate', + arguments: { + function: '() => ({ title: document.title, url: document.URL })', + }, + })).toHaveResponse({ + result: JSON.stringify({ title: 'Title', url: server.HELLO_WORLD }, null, 2), + code: `await page.evaluate('() => ({ title: document.title, url: document.URL })');`, + }); +}); + +test('browser_evaluate (error)', async ({ client, server }) => { + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- Page Title: Title`), + }); + + const result = await client.callTool({ + name: 'browser_evaluate', + arguments: { + function: '() => nonExistentVariable', + }, + }); + + expect(result.isError).toBe(true); + expect(result.content?.[0]?.text).toContain('nonExistentVariable'); + // Check for common error patterns across browsers + const errorText = result.content?.[0]?.text || ''; + expect(errorText).toMatch(/not defined|Can't find variable/); +}); diff --git a/tests/mcp/extension.spec.ts b/tests/mcp/extension.spec.ts new file mode 100644 index 0000000000000..10948baa60f01 --- /dev/null +++ b/tests/mcp/extension.spec.ts @@ -0,0 +1,306 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import { chromium } from 'playwright'; +import { test as base, expect } from './fixtures'; + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { BrowserContext } from 'playwright'; +import type { StartClient } from './fixtures'; + +type BrowserWithExtension = { + userDataDir: string; + launch: (mode?: 'disable-extension') => Promise; +}; + +type TestFixtures = { + browserWithExtension: BrowserWithExtension, + pathToExtension: string, + useShortConnectionTimeout: (timeoutMs: number) => void + overrideProtocolVersion: (version: number) => void +}; + +const test = base.extend({ + pathToExtension: async ({}, use) => { + await use(path.resolve(__dirname, '../../packages/mcp-extension/dist')); + }, + + browserWithExtension: async ({ mcpBrowser, pathToExtension }, use, testInfo) => { + // The flags no longer work in Chrome since + // https://chromium.googlesource.com/chromium/src/+/290ed8046692651ce76088914750cb659b65fb17%5E%21/chrome/browser/extensions/extension_service.cc?pli=1# + test.skip('chromium' !== mcpBrowser, '--load-extension is not supported for official builds of Chromium'); + + let browserContext: BrowserContext | undefined; + const userDataDir = testInfo.outputPath('extension-user-data-dir'); + await use({ + userDataDir, + launch: async (mode?: 'disable-extension') => { + browserContext = await chromium.launchPersistentContext(userDataDir, { + channel: mcpBrowser, + // Opening the browser singleton only works in headed. + headless: false, + // Automation disables singleton browser process behavior, which is necessary for the extension. + ignoreDefaultArgs: ['--enable-automation'], + args: mode === 'disable-extension' ? [] : [ + `--disable-extensions-except=${pathToExtension}`, + `--load-extension=${pathToExtension}`, + ], + }); + + // for manifest v3: + let [serviceWorker] = browserContext.serviceWorkers(); + if (!serviceWorker) + serviceWorker = await browserContext.waitForEvent('serviceworker'); + + return browserContext; + } + }); + await browserContext?.close(); + }, + + useShortConnectionTimeout: async ({}, use) => { + await use((timeoutMs: number) => { + process.env.PWMCP_TEST_CONNECTION_TIMEOUT = timeoutMs.toString(); + }); + process.env.PWMCP_TEST_CONNECTION_TIMEOUT = undefined; + }, + + overrideProtocolVersion: async ({}, use) => { + await use((version: number) => { + process.env.PWMCP_TEST_PROTOCOL_VERSION = version.toString(); + }); + process.env.PWMCP_TEST_PROTOCOL_VERSION = undefined; + } +}); + +async function startAndCallConnectTool(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise { + const { client } = await startClient({ + args: [`--connect-tool`], + config: { + browser: { + userDataDir: browserWithExtension.userDataDir, + } + }, + }); + + expect(await client.callTool({ + name: 'browser_connect', + arguments: { + name: 'extension' + } + })).toHaveResponse({ + result: 'Successfully changed connection method.', + }); + + return client; +} + +async function startWithExtensionFlag(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise { + const { client } = await startClient({ + args: [`--extension`], + config: { + browser: { + userDataDir: browserWithExtension.userDataDir, + } + }, + }); + return client; +} + +const testWithOldExtensionVersion = test.extend({ + pathToExtension: async ({}, use, testInfo) => { + const extensionDir = testInfo.outputPath('extension'); + const oldPath = path.resolve(__dirname, '../../packages/mcp-extension/dist'); + + await fs.promises.cp(oldPath, extensionDir, { recursive: true }); + const manifestPath = path.join(extensionDir, 'manifest.json'); + const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8')); + manifest.version = '0.0.1'; + await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n'); + + await use(extensionDir); + }, +}); + +for (const [mode, startClientMethod] of [ + ['connect-tool', startAndCallConnectTool], + ['extension-flag', startWithExtensionFlag], +] as const) { + + test(`navigate with extension (${mode})`, async ({ browserWithExtension, startClient, server }) => { + const browserContext = await browserWithExtension.launch(); + + const client = await startClientMethod(browserWithExtension, startClient); + + const confirmationPagePromise = browserContext.waitForEvent('page', page => { + return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); + }); + + const navigateResponse = client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const selectorPage = await confirmationPagePromise; + // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector + await selectorPage.getByRole('button', { name: 'Allow' }).click(); + + expect(await navigateResponse).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); + }); + + test(`snapshot of an existing page (${mode})`, async ({ browserWithExtension, startClient, server }) => { + const browserContext = await browserWithExtension.launch(); + + const page = await browserContext.newPage(); + await page.goto(server.HELLO_WORLD); + + // Another empty page. + await browserContext.newPage(); + expect(browserContext.pages()).toHaveLength(3); + + const client = await startClientMethod(browserWithExtension, startClient); + expect(browserContext.pages()).toHaveLength(3); + + const confirmationPagePromise = browserContext.waitForEvent('page', page => { + return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); + }); + + const navigateResponse = client.callTool({ + name: 'browser_snapshot', + arguments: { }, + }); + + const selectorPage = await confirmationPagePromise; + expect(browserContext.pages()).toHaveLength(4); + + await selectorPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click(); + + expect(await navigateResponse).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); + + expect(browserContext.pages()).toHaveLength(4); + }); + + test(`extension not installed timeout (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => { + useShortConnectionTimeout(100); + + const browserContext = await browserWithExtension.launch(); + + const client = await startClientMethod(browserWithExtension, startClient); + + const confirmationPagePromise = browserContext.waitForEvent('page', page => { + return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + result: expect.stringContaining('Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed.'), + isError: true, + }); + + await confirmationPagePromise; + }); + + testWithOldExtensionVersion(`works with old extension version (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => { + useShortConnectionTimeout(500); + + // Prelaunch the browser, so that it is properly closed after the test. + const browserContext = await browserWithExtension.launch(); + + const client = await startClientMethod(browserWithExtension, startClient); + + const confirmationPagePromise = browserContext.waitForEvent('page', page => { + return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); + }); + + const navigateResponse = client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const selectorPage = await confirmationPagePromise; + // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector + await selectorPage.getByRole('button', { name: 'Allow' }).click(); + + expect(await navigateResponse).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); + }); + + test(`extension needs update (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout, overrideProtocolVersion }) => { + useShortConnectionTimeout(500); + overrideProtocolVersion(1000); + + // Prelaunch the browser, so that it is properly closed after the test. + const browserContext = await browserWithExtension.launch(); + + const client = await startClientMethod(browserWithExtension, startClient); + + const confirmationPagePromise = browserContext.waitForEvent('page', page => { + return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); + }); + + const navigateResponse = client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const confirmationPage = await confirmationPagePromise; + await expect(confirmationPage.locator('.status-banner')).toContainText(`Playwright MCP version trying to connect requires newer extension version`); + + expect(await navigateResponse).toHaveResponse({ + result: expect.stringContaining('Extension connection timeout.'), + isError: true, + }); + }); + +} + +test(`custom executablePath`, async ({ startClient, server, useShortConnectionTimeout }) => { + useShortConnectionTimeout(1000); + + const executablePath = test.info().outputPath('echo.sh'); + await fs.promises.writeFile(executablePath, '#!/bin/bash\necho "Custom exec args: $@" > "$(dirname "$0")/output.txt"', { mode: 0o755 }); + + const { client } = await startClient({ + args: [`--extension`], + config: { + browser: { + launchOptions: { + executablePath, + }, + } + }, + }); + + const navigateResponse = await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + timeout: 1000, + }); + expect(await navigateResponse).toHaveResponse({ + result: expect.stringContaining('Extension connection timeout.'), + isError: true, + }); + expect(await fs.promises.readFile(test.info().outputPath('output.txt'), 'utf8')).toContain('Custom exec args: chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html?'); +}); diff --git a/tests/mcp/files.spec.ts b/tests/mcp/files.spec.ts new file mode 100644 index 0000000000000..5a1bb645d2f81 --- /dev/null +++ b/tests/mcp/files.spec.ts @@ -0,0 +1,151 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs/promises'; +import { test, expect } from './fixtures'; + +test('browser_file_upload', async ({ client, server }, testInfo) => { + server.setContent('/', ` + + + `, 'text/html'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: + - button "Choose File" [ref=e2] + - button "Button" [ref=e3]`), + }); + + { + expect(await client.callTool({ + name: 'browser_file_upload', + arguments: { paths: [] }, + })).toHaveResponse({ + isError: true, + result: expect.stringContaining(`The tool "browser_file_upload" can only be used when there is related modal state present.`), + modalState: expect.stringContaining(`- There is no modal state present`), + }); + } + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Textbox', + ref: 'e2', + }, + })).toHaveResponse({ + modalState: expect.stringContaining(`- [File chooser]: can be handled by the "browser_file_upload" tool`), + }); + + const filePath = testInfo.outputPath('test.txt'); + await fs.writeFile(filePath, 'Hello, world!'); + + { + const response = await client.callTool({ + name: 'browser_file_upload', + arguments: { + paths: [filePath], + }, + }); + + expect(response).toHaveResponse({ + code: expect.stringContaining(`await fileChooser.setFiles(`), + modalState: undefined, + }); + } + + { + const response = await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Textbox', + ref: 'e2', + }, + }); + + expect(response).toHaveResponse({ + modalState: `- [File chooser]: can be handled by the "browser_file_upload" tool`, + }); + } + + { + const response = await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button', + ref: 'e3', + }, + }); + + expect(response).toHaveResponse({ + result: `Error: Tool "browser_click" does not handle the modal state.`, + modalState: expect.stringContaining(`- [File chooser]: can be handled by the "browser_file_upload" tool`), + }); + } +}); + +test('clicking on download link emits download', async ({ startClient, server, mcpMode }, testInfo) => { + const { client } = await startClient({ + config: { outputDir: testInfo.outputPath('output') }, + }); + + server.setContent('/', `Download`, 'text/html'); + server.setContent('/download', 'Data', 'text/plain'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- link "Download" [ref=e2]`), + }); + await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Download link', + ref: 'e2', + }, + }); + await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toHaveResponse({ + downloads: `- Downloaded file test.txt to ${testInfo.outputPath('output', 'test.txt')}`, + }); +}); + +test('navigating to download link emits download', async ({ startClient, server, mcpBrowser, mcpMode }, testInfo) => { + const { client } = await startClient({ + config: { outputDir: testInfo.outputPath('output') }, + }); + + test.skip(mcpBrowser !== 'chromium', 'This test is racy'); + server.setRoute('/download', (req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Disposition': 'attachment; filename=test.txt', + }); + res.end('Hello world!'); + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX + '/download', + }, + })).toHaveResponse({ + downloads: expect.stringContaining(`- Downloaded file test.txt to`), + }); +}); diff --git a/tests/mcp/fixtures.ts b/tests/mcp/fixtures.ts new file mode 100644 index 0000000000000..7fb39c68071f3 --- /dev/null +++ b/tests/mcp/fixtures.ts @@ -0,0 +1,280 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import { chromium } from 'playwright'; + +import { test as baseTest, expect as baseExpect } from '@playwright/test'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { ListRootsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +import { TestServer } from '../config/testserver'; +import { serverFixtures } from '../config/serverFixtures'; + +import type { Config } from '../../packages/playwright/src/mcp/config'; +import type { BrowserContext } from 'playwright'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import type { Stream } from 'stream'; +import type { ServerFixtures, ServerWorkerOptions } from '../config/serverFixtures'; + +export type TestOptions = { + mcpArgs: string[] | undefined; + mcpBrowser: string | undefined; + mcpMode: 'docker' | undefined; +}; + +type CDPServer = { + endpoint: string; + start: () => Promise; +}; + +export type StartClient = (options?: { + clientName?: string, + args?: string[], + config?: Config, + roots?: { name: string, uri: string }[], + rootsResponseDelay?: number, +}) => Promise<{ client: Client, stderr: () => string }>; + + +type TestFixtures = { + client: Client; + startClient: StartClient; + wsEndpoint: string; + cdpServer: CDPServer; + server: TestServer; + httpsServer: TestServer; + mcpHeadless: boolean; +}; + +type WorkerFixtures = { + _workerServers: { server: TestServer, httpsServer: TestServer }; +}; + +export const serverTest = baseTest.extend(serverFixtures); + +export const test = serverTest.extend({ + mcpArgs: [undefined, { option: true }], + + client: async ({ startClient }, use) => { + const { client } = await startClient(); + await use(client); + }, + + startClient: async ({ mcpHeadless, mcpBrowser, mcpMode, mcpArgs }, use, testInfo) => { + const configDir = path.dirname(test.info().config.configFile!); + const clients: Client[] = []; + + await use(async options => { + const args: string[] = mcpArgs ?? []; + if (process.env.CI && process.platform === 'linux') + args.push('--no-sandbox'); + if (mcpHeadless) + args.push('--headless'); + if (mcpBrowser) + args.push(`--browser=${mcpBrowser}`); + if (options?.args) + args.push(...options.args); + if (options?.config) { + const configFile = testInfo.outputPath('config.json'); + await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2)); + args.push(`--config=${path.relative(configDir, configFile)}`); + } + + const client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined); + if (options?.roots) { + client.setRequestHandler(ListRootsRequestSchema, async request => { + if (options.rootsResponseDelay) + await new Promise(resolve => setTimeout(resolve, options.rootsResponseDelay)); + return { + roots: options.roots, + }; + }); + } + const { transport, stderr } = await createTransport(args, mcpMode, testInfo.outputPath('ms-playwright')); + let stderrBuffer = ''; + stderr?.on('data', data => { + if (process.env.PWMCP_DEBUG) + process.stderr.write(data); + stderrBuffer += data.toString(); + }); + clients.push(client); + await client.connect(transport); + await client.ping(); + return { client, stderr: () => stderrBuffer }; + }); + + await Promise.all(clients.map(client => client.close())); + }, + + wsEndpoint: async ({ }, use) => { + const browserServer = await chromium.launchServer(); + await use(browserServer.wsEndpoint()); + await browserServer.close(); + }, + + cdpServer: async ({ mcpBrowser }, use, testInfo) => { + test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser!), 'CDP is not supported for non-Chromium browsers'); + + let browserContext: BrowserContext | undefined; + const port = 3200 + test.info().parallelIndex; + await use({ + endpoint: `http://localhost:${port}`, + start: async () => { + if (browserContext) + throw new Error('CDP server already exists'); + browserContext = await chromium.launchPersistentContext(testInfo.outputPath('cdp-user-data-dir'), { + channel: mcpBrowser, + headless: true, + args: [ + `--remote-debugging-port=${port}`, + ], + }); + return browserContext; + } + }); + await browserContext?.close(); + }, + + mcpHeadless: async ({ headless }, use) => { + await use(headless); + }, + + server: async ({ server }, use) => { + server.setContent('/favicon.ico', '', 'image/x-icon'); + server.setContent('/', ``, 'text/html'); + server.setContent('/hello-world', ` + Title + Hello, world! + `, 'text/html'); + await use(server); + }, + + mcpBrowser: ['chrome', { option: true }], + + mcpMode: [undefined, { option: true }], +}); + +async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'], profilesDir: string): Promise<{ + transport: Transport, + stderr: Stream | null, +}> { + if (mcpMode === 'docker') { + const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`]; + const transport = new StdioClientTransport({ + command: 'docker', + args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args], + }); + return { + transport, + stderr: transport.stderr, + }; + } + + const transport = new StdioClientTransport({ + command: 'node', + args: [programPath, ...args], + cwd: path.dirname(test.info().config.configFile!), + stderr: 'pipe', + env: { + ...process.env, + DEBUG: 'pw:mcp:test', + DEBUG_COLORS: '0', + DEBUG_HIDE_DATE: '1', + PWMCP_PROFILES_DIR_FOR_TEST: profilesDir, + }, + }); + return { + transport, + stderr: transport.stderr!, + }; +} + +type Response = Awaited>; + +export const expect = baseExpect.extend({ + toHaveResponse(response: Response, object: any) { + const parsed = parseResponse(response); + const isNot = this.isNot; + try { + if (isNot) + expect(parsed).not.toEqual(expect.objectContaining(object)); + else + expect(parsed).toEqual(expect.objectContaining(object)); + } catch (e) { + return { + pass: isNot, + message: () => e.message, + }; + } + return { + pass: !isNot, + message: () => ``, + }; + }, +}); + +export function formatOutput(output: string): string[] { + return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean); +} + +function parseResponse(response: any) { + const text = response.content[0].text; + const sections = parseSections(text); + + const result = sections.get('Result'); + const code = sections.get('Ran Playwright code'); + const tabs = sections.get('Open tabs'); + const pageState = sections.get('Page state'); + const consoleMessages = sections.get('New console messages'); + const modalState = sections.get('Modal state'); + const downloads = sections.get('Downloads'); + const codeNoFrame = code?.replace(/^```js\n/, '').replace(/\n```$/, ''); + const isError = response.isError; + const attachments = response.content.slice(1); + + return { + result, + code: codeNoFrame, + tabs, + pageState, + consoleMessages, + modalState, + downloads, + isError, + attachments, + }; +} + +function parseSections(text: string): Map { + const sections = new Map(); + const sectionHeaders = text.split(/^### /m).slice(1); // Remove empty first element + + for (const section of sectionHeaders) { + const firstNewlineIndex = section.indexOf('\n'); + if (firstNewlineIndex === -1) + continue; + + const sectionName = section.substring(0, firstNewlineIndex); + const sectionContent = section.substring(firstNewlineIndex + 1).trim(); + sections.set(sectionName, sectionContent); + } + + return sections; +} + +export const programPath = path.join(__dirname, '../../packages/playwright/lib/mcp/program.js'); diff --git a/tests/mcp/form.spec.ts b/tests/mcp/form.spec.ts new file mode 100644 index 0000000000000..339d79dff58fa --- /dev/null +++ b/tests/mcp/form.spec.ts @@ -0,0 +1,123 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_fill_form (textbox)', async ({ client, server }) => { + server.setContent('/', ` + + + +
+ + + + + +
+ + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_fill_form', + arguments: { + fields: [ + { + name: 'Name textbox', + type: 'textbox', + ref: 'e4', + value: 'John Doe' + }, + { + name: 'Email textbox', + type: 'textbox', + ref: 'e6', + value: 'john.doe@example.com' + }, + { + name: 'Age textbox', + type: 'slider', + ref: 'e8', + value: '25' + }, + { + name: 'Country select', + type: 'combobox', + ref: 'e10', + value: 'United States' + }, + { + name: 'Subscribe checkbox', + type: 'checkbox', + ref: 'e12', + value: 'true' + }, + ] + }, + })).toHaveResponse({ + code: `await page.getByRole('textbox', { name: 'Name' }).fill('John Doe'); +await page.getByRole('textbox', { name: 'Email' }).fill('john.doe@example.com'); +await page.getByRole('slider', { name: 'Age' }).fill('25'); +await page.getByLabel('Choose a country United').selectOption('United States'); +await page.getByRole('checkbox', { name: 'Subscribe to newsletter' }).setChecked('true');`, + }); + + const response = await client.callTool({ + name: 'browser_snapshot', + arguments: { + }, + }); + expect.soft(response).toHaveResponse({ + pageState: expect.stringMatching(/textbox "Name".*John Doe/), + }); + expect.soft(response).toHaveResponse({ + pageState: expect.stringMatching(/textbox "Email".*john.doe@example.com/), + }); + expect.soft(response).toHaveResponse({ + pageState: expect.stringMatching(/slider "Age".*"25"/), + }); + expect.soft(response).toHaveResponse({ + pageState: expect.stringContaining('option \"United States\" [selected]'), + }); + expect.soft(response).toHaveResponse({ + pageState: expect.stringContaining('checkbox \"Subscribe to newsletter\" [checked]'), + }); +}); diff --git a/tests/mcp/headed.spec.ts b/tests/mcp/headed.spec.ts new file mode 100644 index 0000000000000..1654a30a50707 --- /dev/null +++ b/tests/mcp/headed.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +for (const mcpHeadless of [false, true]) { + test.describe(`mcpHeadless: ${mcpHeadless}`, () => { + test.use({ mcpHeadless }); + test.skip(process.platform === 'linux', 'Auto-detection wont let this test run on linux'); + test.skip(({ mcpMode, mcpHeadless }) => mcpMode === 'docker' && !mcpHeadless, 'Headed mode is not supported in docker'); + + test('browser', async ({ client, server, mcpBrowser }) => { + test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser ?? ''), 'Only chrome is supported for this test'); + server.setRoute('/', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + + `); + }); + + const response = await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + expect(response).toHaveResponse({ + pageState: (mcpHeadless ? expect : expect.not).stringContaining(`HeadlessChrome`), + }); + }); + }); +} diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts new file mode 100644 index 0000000000000..2289a2bb2d81a --- /dev/null +++ b/tests/mcp/http.spec.ts @@ -0,0 +1,254 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; + +import { ChildProcess, spawn } from 'child_process'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { test as baseTest, expect, programPath } from './fixtures'; + +import type { Config } from '../../packages/playwright/src/mcp/config'; + +const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ + serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { + let cp: ChildProcess | undefined; + const userDataDir = testInfo.outputPath('user-data-dir'); + await use(async (options?: { args?: string[], noPort?: boolean }) => { + if (cp) + throw new Error('Process already running'); + + cp = spawn('node', [ + programPath, + ...(options?.noPort ? [] : ['--port=0']), + '--user-data-dir=' + userDataDir, + ...(mcpHeadless ? ['--headless'] : []), + ...(options?.args || []), + ], { + stdio: 'pipe', + env: { + ...process.env, + DEBUG: 'pw:mcp:test', + DEBUG_COLORS: '0', + DEBUG_HIDE_DATE: '1', + }, + }); + let stderr = ''; + const url = await new Promise(resolve => cp!.stderr?.on('data', data => { + stderr += data.toString(); + const match = stderr.match(/Listening on (http:\/\/.*)/); + if (match) + resolve(match[1]); + })); + + return { url: new URL(url), stderr: () => stderr }; + }); + cp?.kill('SIGTERM'); + }, +}); + +test('http transport', async ({ serverEndpoint }) => { + const { url } = await serverEndpoint(); + const transport = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client = new Client({ name: 'test', version: '1.0.0' }); + await client.connect(transport); + await client.ping(); +}); + +test('http transport (config)', async ({ serverEndpoint }) => { + const config: Config = { + server: { + port: 0, + } + }; + const configFile = test.info().outputPath('config.json'); + await fs.promises.writeFile(configFile, JSON.stringify(config, null, 2)); + + const { url } = await serverEndpoint({ noPort: true, args: ['--config=' + configFile] }); + const transport = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client = new Client({ name: 'test', version: '1.0.0' }); + await client.connect(transport); + await client.ping(); +}); + +test('http transport browser lifecycle (isolated)', async ({ serverEndpoint, server }) => { + const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); + + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + /** + * src/client/streamableHttp.ts + * Clients that no longer need a particular session + * (e.g., because the user is leaving the client application) SHOULD send an + * HTTP DELETE to the MCP endpoint with the Mcp-Session-Id header to explicitly + * terminate the session. + */ + await transport1.terminateSession(); + await client1.close(); + + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await transport2.terminateSession(); + await client2.close(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create http session/)).length).toBe(2); + expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create context/)).length).toBe(2); + expect(lines.filter(line => line.match(/close context/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(2); + expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(2); + + expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(2); + expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(2); + }).toPass(); +}); + +test('http transport browser lifecycle (isolated, multiclient)', async ({ serverEndpoint, server }) => { + const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); + + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await transport1.terminateSession(); + await client1.close(); + + const transport3 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client3 = new Client({ name: 'test', version: '1.0.0' }); + await client3.connect(transport3); + await client3.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + await transport2.terminateSession(); + await client2.close(); + await transport3.terminateSession(); + await client3.close(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create http session/)).length).toBe(3); + expect(lines.filter(line => line.match(/delete http session/)).length).toBe(3); + + expect(lines.filter(line => line.match(/create context/)).length).toBe(3); + expect(lines.filter(line => line.match(/close context/)).length).toBe(3); + + expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(3); + expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(3); + + expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(1); + expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(1); + }).toPass(); +}); + +test('http transport browser lifecycle (persistent)', async ({ serverEndpoint, server }) => { + const { url, stderr } = await serverEndpoint(); + + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await transport1.terminateSession(); + await client1.close(); + + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await transport2.terminateSession(); + await client2.close(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create http session/)).length).toBe(2); + expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create context/)).length).toBe(2); + expect(lines.filter(line => line.match(/close context/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create browser context \(persistent\)/)).length).toBe(2); + expect(lines.filter(line => line.match(/close browser context \(persistent\)/)).length).toBe(2); + + expect(lines.filter(line => line.match(/lock user data dir/)).length).toBe(2); + expect(lines.filter(line => line.match(/release user data dir/)).length).toBe(2); + }).toPass(); +}); + +test('http transport browser lifecycle (persistent, multiclient)', async ({ serverEndpoint, server }) => { + const { url } = await serverEndpoint(); + + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + const response = await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + expect(response.isError).toBe(true); + expect(response.content?.[0].text).toContain('use --isolated to run multiple instances of the same browser'); + + await client1.close(); + await client2.close(); +}); + +test('http transport (default)', async ({ serverEndpoint }) => { + const { url } = await serverEndpoint(); + const transport = new StreamableHTTPClientTransport(url); + const client = new Client({ name: 'test', version: '1.0.0' }); + await client.connect(transport); + await client.ping(); + expect(transport.sessionId, 'has session support').toBeDefined(); +}); diff --git a/tests/mcp/iframes.spec.ts b/tests/mcp/iframes.spec.ts new file mode 100644 index 0000000000000..3c53de170b718 --- /dev/null +++ b/tests/mcp/iframes.spec.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('stitched aria frames', async ({ client }) => { + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: `data:text/html,

Hello

`, + }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: + - heading "Hello" [level=1] [ref=e2] + - iframe [ref=e3]: + - generic [active] [ref=f1e1]: + - button "World" [ref=f1e2] + - main [ref=f1e3]: + - iframe [ref=f1e4]: + - paragraph [ref=f2e2]: Nested +\`\`\``), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'World', + ref: 'f1e2', + }, + })).toHaveResponse({ + code: `await page.locator('iframe').first().contentFrame().getByRole('button', { name: 'World' }).click();`, + }); +}); diff --git a/tests/mcp/install.spec.ts b/tests/mcp/install.spec.ts new file mode 100644 index 0000000000000..70fde07597ee7 --- /dev/null +++ b/tests/mcp/install.spec.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_install', async ({ client, mcpBrowser }) => { + test.skip(mcpBrowser !== 'chromium', 'Test only chromium'); + expect(await client.callTool({ + name: 'browser_install', + })).toHaveResponse({ + tabs: expect.stringContaining(`No open tabs`), + }); +}); diff --git a/tests/mcp/launch.spec.ts b/tests/mcp/launch.spec.ts new file mode 100644 index 0000000000000..f7f0171aa9efd --- /dev/null +++ b/tests/mcp/launch.spec.ts @@ -0,0 +1,169 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; + +import { test, expect, formatOutput } from './fixtures'; + +test('test reopen browser', async ({ startClient, server, mcpMode }) => { + const { client, stderr } = await startClient(); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + expect(await client.callTool({ + name: 'browser_close', + })).toHaveResponse({ + code: `await page.close()`, + tabs: `No open tabs. Use the "browser_navigate" tool to navigate to a page first.`, + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); + + await client.close(); + + if (process.platform === 'win32') + return; + + await expect.poll(() => formatOutput(stderr()), { timeout: 0 }).toEqual([ + 'create context', + 'create browser context (persistent)', + 'lock user data dir', + 'close context', + 'close browser context (persistent)', + 'release user data dir', + 'close browser context complete (persistent)', + 'create browser context (persistent)', + 'lock user data dir', + 'close context', + 'close browser context (persistent)', + 'release user data dir', + 'close browser context complete (persistent)', + ]); +}); + +test('executable path', async ({ startClient, server }) => { + const { client } = await startClient({ args: [`--executable-path=bogus`] }); + const response = await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + expect(response).toHaveResponse({ + result: expect.stringContaining(`executable doesn't exist`), + isError: true, + }); +}); + +test('persistent context', async ({ startClient, server }) => { + server.setContent('/', ` + + + + `, 'text/html'); + + const { client } = await startClient(); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`Storage: NO`), + }); + + await new Promise(resolve => setTimeout(resolve, 3000)); + + await client.callTool({ + name: 'browser_close', + }); + + const { client: client2 } = await startClient(); + expect(await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`Storage: YES`), + }); +}); + +test('isolated context', async ({ startClient, server }) => { + server.setContent('/', ` + + + + `, 'text/html'); + + const { client: client1 } = await startClient({ args: [`--isolated`] }); + expect(await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`Storage: NO`), + }); + + await client1.callTool({ + name: 'browser_close', + }); + + const { client: client2 } = await startClient({ args: [`--isolated`] }); + expect(await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`Storage: NO`), + }); +}); + +test('isolated context with storage state', async ({ startClient, server }, testInfo) => { + const storageStatePath = testInfo.outputPath('storage-state.json'); + await fs.promises.writeFile(storageStatePath, JSON.stringify({ + origins: [ + { + origin: server.PREFIX, + localStorage: [{ name: 'test', value: 'session-value' }], + }, + ], + })); + + server.setContent('/', ` + + + + `, 'text/html'); + + const { client } = await startClient({ args: [ + `--isolated`, + `--storage-state=${storageStatePath}`, + ] }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`Storage: session-value`), + }); +}); diff --git a/tests/mcp/library.spec.ts b/tests/mcp/library.spec.ts new file mode 100644 index 0000000000000..24eaf2d05e31a --- /dev/null +++ b/tests/mcp/library.spec.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import child_process from 'child_process'; +import fs from 'fs/promises'; +import { test, expect } from './fixtures'; + +test('library can be used from CommonJS', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/456' } }, async ({}, testInfo) => { + const file = testInfo.outputPath('main.cjs'); + await fs.writeFile(file, ` + import('playwright/lib/mcp/index') + .then(playwrightMCP => playwrightMCP.createConnection()) + .then(() => console.log('OK')); + `); + expect(child_process.execSync(`node ${file}`, { encoding: 'utf-8' })).toContain('OK'); +}); diff --git a/tests/mcp/mdb.spec.ts b/tests/mcp/mdb.spec.ts new file mode 100644 index 0000000000000..85229a7864b36 --- /dev/null +++ b/tests/mcp/mdb.spec.ts @@ -0,0 +1,208 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; + +import { runMainBackend, runOnPauseBackendLoop } from '../../packages/playwright/lib/mcp/sdk/mdb'; + +import { test, expect } from './fixtures'; + +test('call top level tool', async () => { + const { mdbUrl } = await startMDBAndCLI(); + const mdbClient = await createMDBClient(mdbUrl); + + const { tools } = await mdbClient.client.listTools(); + expect(tools).toEqual([{ + name: 'cli_echo', + description: 'Echo a message', + inputSchema: expect.any(Object), + }, { + name: 'cli_pause_in_gdb', + description: 'Pause in gdb', + inputSchema: expect.any(Object), + }, { + name: 'cli_pause_in_gdb_twice', + description: 'Pause in gdb twice', + inputSchema: expect.any(Object), + } + ]); + + const echoResult = await mdbClient.client.callTool({ + name: 'cli_echo', + arguments: { + message: 'Hello, world!', + }, + }); + expect(echoResult.content).toEqual([{ type: 'text', text: 'Echo: Hello, world!' }]); + + await mdbClient.close(); +}); + +test('pause on error', async () => { + const { mdbUrl } = await startMDBAndCLI(); + const mdbClient = await createMDBClient(mdbUrl); + + // Make a call that results in a recoverable error. + const interruptResult = await mdbClient.client.callTool({ + name: 'cli_pause_in_gdb', + arguments: {}, + }); + expect(interruptResult.content).toEqual([{ type: 'text', text: 'Paused on exception' }]); + + // List new inner tools. + const { tools } = await mdbClient.client.listTools(); + expect(tools).toEqual([ + expect.objectContaining({ + name: 'gdb_bt', + }), + expect.objectContaining({ + name: 'gdb_continue', + }), + ]); + + // Call the new inner tool. + const btResult = await mdbClient.client.callTool({ + name: 'gdb_bt', + arguments: {}, + }); + expect(btResult.content).toEqual([{ type: 'text', text: 'Backtrace' }]); + + // Continue execution. + const continueResult = await mdbClient.client.callTool({ + name: 'gdb_continue', + arguments: {}, + }); + expect(continueResult.content).toEqual([{ type: 'text', text: 'Done' }]); + + await mdbClient.close(); +}); + +test('pause on error twice', async () => { + const { mdbUrl } = await startMDBAndCLI(); + const mdbClient = await createMDBClient(mdbUrl); + + // Make a call that results in a recoverable error. + const result = await mdbClient.client.callTool({ + name: 'cli_pause_in_gdb_twice', + arguments: {}, + }); + expect(result.content).toEqual([{ type: 'text', text: 'Paused on exception 1' }]); + + // Continue execution. + const continueResult1 = await mdbClient.client.callTool({ + name: 'gdb_continue', + arguments: {}, + }); + expect(continueResult1.content).toEqual([{ type: 'text', text: 'Paused on exception 2' }]); + + const continueResult2 = await mdbClient.client.callTool({ + name: 'gdb_continue', + arguments: {}, + }); + expect(continueResult2.content).toEqual([{ type: 'text', text: 'Done' }]); + + await mdbClient.close(); +}); + +async function startMDBAndCLI(): Promise<{ mdbUrl: string }> { + const mdbUrlBox = { mdbUrl: undefined as string | undefined }; + const cliBackendFactory = { + name: 'CLI', + nameInConfig: 'cli', + version: '0.0.0', + create: () => new CLIBackend(mdbUrlBox) + }; + + const mdbUrl = (await runMainBackend(cliBackendFactory, { port: 0 }))!; + mdbUrlBox.mdbUrl = mdbUrl; + return { mdbUrl }; +} + +async function createMDBClient(mdbUrl: string): Promise<{ client: Client, close: () => Promise }> { + const client = new Client({ name: 'Internal client', version: '0.0.0' }); + const transport = new StreamableHTTPClientTransport(new URL(mdbUrl)); + await client.connect(transport); + return { + client, + close: async () => { + await transport.terminateSession(); + await client.close(); + } + }; +} + +class CLIBackend { + constructor(private readonly mdbUrlBox: { mdbUrl: string | undefined }) {} + + async listTools() { + return [{ + name: 'cli_echo', + description: 'Echo a message', + inputSchema: zodToJsonSchema(z.object({ message: z.string() })) as any, + }, { + name: 'cli_pause_in_gdb', + description: 'Pause in gdb', + inputSchema: zodToJsonSchema(z.object({})) as any, + }, { + name: 'cli_pause_in_gdb_twice', + description: 'Pause in gdb twice', + inputSchema: zodToJsonSchema(z.object({})) as any, + }]; + } + + async callTool(name: string, args: any) { + if (name === 'cli_echo') + return { content: [{ type: 'text', text: 'Echo: ' + (args?.message as string) }] }; + if (name === 'cli_pause_in_gdb') { + await runOnPauseBackendLoop(this.mdbUrlBox.mdbUrl!, new GDBBackend(), 'Paused on exception'); + return { content: [{ type: 'text', text: 'Done' }] }; + } + if (name === 'cli_pause_in_gdb_twice') { + await runOnPauseBackendLoop(this.mdbUrlBox.mdbUrl!, new GDBBackend(), 'Paused on exception 1'); + await runOnPauseBackendLoop(this.mdbUrlBox.mdbUrl!, new GDBBackend(), 'Paused on exception 2'); + return { content: [{ type: 'text', text: 'Done' }] }; + } + throw new Error(`Unknown tool: ${name}`); + } +} + +class GDBBackend { + async listTools() { + return [{ + name: 'gdb_bt', + description: 'Print backtrace', + inputSchema: zodToJsonSchema(z.object({})) as any, + }, { + name: 'gdb_continue', + description: 'Continue execution', + inputSchema: zodToJsonSchema(z.object({})) as any, + }]; + } + + async callTool(name: string) { + if (name === 'gdb_bt') + return { content: [{ type: 'text', text: 'Backtrace' }] }; + if (name === 'gdb_continue') { + (this as any).requestSelfDestruct?.(); + // Stall + await new Promise(f => setTimeout(f, 1000)); + } + throw new Error(`Unknown tool: ${name}`); + } +} diff --git a/tests/mcp/network.spec.ts b/tests/mcp/network.spec.ts new file mode 100644 index 0000000000000..a47bcce4ae96d --- /dev/null +++ b/tests/mcp/network.spec.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_network_requests', async ({ client, server }) => { + server.setContent('/', ` + + `, 'text/html'); + + server.setContent('/json', JSON.stringify({ name: 'John Doe' }), 'application/json'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Click me button', + ref: 'e2', + }, + }); + + await expect.poll(() => client.callTool({ + name: 'browser_network_requests', + })).toHaveResponse({ + result: expect.stringContaining(`[GET] ${`${server.PREFIX}/`} => [200] OK +[GET] ${`${server.PREFIX}/json`} => [200] OK`), + }); +}); diff --git a/tests/mcp/pdf.spec.ts b/tests/mcp/pdf.spec.ts new file mode 100644 index 0000000000000..937eb16c879d2 --- /dev/null +++ b/tests/mcp/pdf.spec.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; + +import { test, expect } from './fixtures'; + +test('save as pdf unavailable', async ({ startClient, server }) => { + const { client } = await startClient(); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + expect(await client.callTool({ + name: 'browser_pdf_save', + })).toHaveResponse({ + result: 'Error: Tool "browser_pdf_save" not found', + isError: true, + }); +}); + +test('save as pdf', async ({ startClient, mcpBrowser, server }, testInfo) => { + const { client } = await startClient({ + config: { outputDir: testInfo.outputPath('output'), capabilities: ['pdf'] }, + }); + + test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); + + expect(await client.callTool({ + name: 'browser_pdf_save', + })).toHaveResponse({ + code: expect.stringContaining(`await page.pdf(`), + result: expect.stringMatching(/Saved page as.*page-[^:]+.pdf/), + }); +}); + +test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.'); + const { client } = await startClient({ + config: { outputDir, capabilities: ['pdf'] }, + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); + + expect(await client.callTool({ + name: 'browser_pdf_save', + arguments: { + filename: 'output.pdf', + }, + })).toHaveResponse({ + result: expect.stringContaining(`output.pdf`), + code: expect.stringContaining(`await page.pdf(`), + }); + + const files = [...fs.readdirSync(outputDir)]; + + expect(fs.existsSync(outputDir)).toBeTruthy(); + const pdfFiles = files.filter(f => f.endsWith('.pdf')); + expect(pdfFiles).toHaveLength(1); + expect(pdfFiles[0]).toMatch(/^output.pdf$/); +}); diff --git a/tests/mcp/playwright.config.extension.ts b/tests/mcp/playwright.config.extension.ts new file mode 100644 index 0000000000000..526fda2001f51 --- /dev/null +++ b/tests/mcp/playwright.config.extension.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineConfig } from '@playwright/test'; + +import type { TestOptions } from './fixtures'; + +export default defineConfig({ + testDir: './', + grep: /extension/, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'list', + projects: [ + { name: 'chromium', use: { mcpBrowser: 'chromium' } }, + ], +}); diff --git a/tests/mcp/playwright.config.ts b/tests/mcp/playwright.config.ts new file mode 100644 index 0000000000000..923a0f8034d6a --- /dev/null +++ b/tests/mcp/playwright.config.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineConfig } from '@playwright/test'; + +import type { TestOptions } from './fixtures'; + +export default defineConfig({ + testDir: './', + grepInvert: /extension/, + fullyParallel: true, + forbidOnly: !!process.env.CI, + workers: process.env.CI ? 2 : undefined, + reporter: 'list', + projects: [ + { name: 'chrome' }, + { name: 'chromium', use: { mcpBrowser: 'chromium' } }, + ...process.env.MCP_IN_DOCKER ? [{ + name: 'chromium-docker', + grep: /browser_navigate|browser_click/, + use: { + mcpBrowser: 'chromium', + mcpMode: 'docker' as const + } + }] : [], + { name: 'firefox', use: { mcpBrowser: 'firefox' } }, + { name: 'webkit', use: { mcpBrowser: 'webkit' } }, + ... process.platform === 'win32' ? [{ name: 'msedge', use: { mcpBrowser: 'msedge' } }] : [], + ], +}); diff --git a/tests/mcp/request-blocking.spec.ts b/tests/mcp/request-blocking.spec.ts new file mode 100644 index 0000000000000..0bcfd343010af --- /dev/null +++ b/tests/mcp/request-blocking.spec.ts @@ -0,0 +1,82 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { test, expect } from './fixtures'; + +const BLOCK_MESSAGE = /Blocked by Web Inspector|NS_ERROR_FAILURE|net::ERR_BLOCKED_BY_CLIENT/g; + +const fetchPage = async (client: Client, url: string) => { + const result = await client.callTool({ + name: 'browser_navigate', + arguments: { + url, + }, + }); + + return JSON.stringify(result, null, 2); +}; + +test('default to allow all', async ({ server, client }) => { + server.setContent('/ppp', 'content:PPP', 'text/html'); + const result = await fetchPage(client, server.PREFIX + '/ppp'); + expect(result).toContain('content:PPP'); +}); + +test('blocked works', async ({ startClient }) => { + const { client } = await startClient({ + args: ['--blocked-origins', 'microsoft.com;example.com;playwright.dev'] + }); + const result = await fetchPage(client, 'https://example.com/'); + expect(result).toMatch(BLOCK_MESSAGE); +}); + +test('allowed works', async ({ server, startClient }) => { + server.setContent('/ppp', 'content:PPP', 'text/html'); + const { client } = await startClient({ + args: ['--allowed-origins', `microsoft.com;${new URL(server.PREFIX).host};playwright.dev`] + }); + const result = await fetchPage(client, server.PREFIX + '/ppp'); + expect(result).toContain('content:PPP'); +}); + +test('blocked takes precedence', async ({ startClient }) => { + const { client } = await startClient({ + args: [ + '--blocked-origins', 'example.com', + '--allowed-origins', 'example.com', + ], + }); + const result = await fetchPage(client, 'https://example.com/'); + expect(result).toMatch(BLOCK_MESSAGE); +}); + +test('allowed without blocked blocks all non-explicitly specified origins', async ({ startClient }) => { + const { client } = await startClient({ + args: ['--allowed-origins', 'playwright.dev'], + }); + const result = await fetchPage(client, 'https://example.com/'); + expect(result).toMatch(BLOCK_MESSAGE); +}); + +test('blocked without allowed allows non-explicitly specified origins', async ({ server, startClient }) => { + server.setContent('/ppp', 'content:PPP', 'text/html'); + const { client } = await startClient({ + args: ['--blocked-origins', 'example.com'], + }); + const result = await fetchPage(client, server.PREFIX + '/ppp'); + expect(result).toContain('content:PPP'); +}); diff --git a/tests/mcp/roots.spec.ts b/tests/mcp/roots.spec.ts new file mode 100644 index 0000000000000..904a3265be221 --- /dev/null +++ b/tests/mcp/roots.spec.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import { pathToFileURL } from 'url'; + +import { test, expect } from './fixtures'; + +const p = process.platform === 'win32' ? 'c:\\non\\existent\\folder' : '/non/existent/folder'; + +test('should use separate user data by root path', async ({ startClient, server }, testInfo) => { + const { client } = await startClient({ + clientName: 'Visual Studio Code', + roots: [ + { + name: 'test', + uri: 'file://' + p.replace(/\\/g, '/'), + } + ], + }); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const hash = createHash(p); + const [file] = await fs.promises.readdir(testInfo.outputPath('ms-playwright')); + expect(file).toContain(hash); +}); + +test('check that trace is saved in workspace', async ({ startClient, server }, testInfo) => { + const rootPath = testInfo.outputPath('workspace'); + const { client } = await startClient({ + args: ['--save-trace'], + clientName: 'My client', + roots: [ + { + name: 'workspace', + uri: pathToFileURL(rootPath).toString(), + }, + ], + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + const [file] = await fs.promises.readdir(path.join(rootPath, '.playwright-mcp')); + expect(file).toContain('traces'); +}); + +test('should list all tools when listRoots is slow', async ({ startClient }) => { + const { client } = await startClient({ + clientName: 'Another custom client', + roots: [], + rootsResponseDelay: 1000, + }); + const tools = await client.listTools(); + expect(tools.tools.length).toBeGreaterThan(10); +}); + +function createHash(data: string): string { + return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); +} diff --git a/tests/mcp/screenshot.spec.ts b/tests/mcp/screenshot.spec.ts new file mode 100644 index 0000000000000..50b2eaeaff5f3 --- /dev/null +++ b/tests/mcp/screenshot.spec.ts @@ -0,0 +1,327 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; + +import { test, expect } from './fixtures'; + +test('browser_take_screenshot (viewport)', async ({ startClient, server }, testInfo) => { + const { client } = await startClient({ + config: { outputDir: testInfo.outputPath('output') }, + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + expect(await client.callTool({ + name: 'browser_take_screenshot', + })).toHaveResponse({ + code: expect.stringContaining(`await page.screenshot`), + attachments: [{ + data: expect.any(String), + mimeType: 'image/png', + type: 'image', + }], + }); +}); + +test('browser_take_screenshot (element)', async ({ startClient, server }, testInfo) => { + const { client } = await startClient({ + config: { outputDir: testInfo.outputPath('output') }, + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + pageState: expect.stringContaining(`[ref=e1]`), + }); + + expect(await client.callTool({ + name: 'browser_take_screenshot', + arguments: { + element: 'hello button', + ref: 'e1', + }, + })).toEqual({ + content: [ + { + text: expect.stringContaining(`page.getByText('Hello, world!').screenshot`), + type: 'text', + }, + { + data: expect.any(String), + mimeType: 'image/png', + type: 'image', + }, + ], + }); +}); + +test('--output-dir should work', async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + const { client } = await startClient({ + config: { outputDir }, + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + await client.callTool({ + name: 'browser_take_screenshot', + }); + + expect(fs.existsSync(outputDir)).toBeTruthy(); + const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith('.png')); + expect(files).toHaveLength(1); + expect(files[0]).toMatch(/^page-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.png$/); +}); + +for (const type of ['png', 'jpeg']) { + test(`browser_take_screenshot (type: ${type})`, async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + const { client } = await startClient({ + config: { outputDir }, + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + expect(await client.callTool({ + name: 'browser_take_screenshot', + arguments: { type }, + })).toEqual({ + content: [ + { + text: expect.stringMatching( + new RegExp(`page-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}\\-\\d{3}Z\\.${type}`) + ), + type: 'text', + }, + { + data: expect.any(String), + mimeType: `image/${type}`, + type: 'image', + }, + ], + }); + + const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith(`.${type}`)); + + expect(fs.existsSync(outputDir)).toBeTruthy(); + expect(files).toHaveLength(1); + expect(files[0]).toMatch( + new RegExp(`^page-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${type}$`) + ); + }); + +} + +test('browser_take_screenshot (default type should be png)', async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + const { client } = await startClient({ + config: { outputDir }, + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + code: `await page.goto('${server.PREFIX}');`, + }); + + expect(await client.callTool({ + name: 'browser_take_screenshot', + })).toEqual({ + content: [ + { + text: expect.stringMatching( + new RegExp(`page-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}\\-\\d{3}Z\\.png`) + ), + type: 'text', + }, + { + data: expect.any(String), + mimeType: 'image/png', + type: 'image', + }, + ], + }); + + const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith('.png')); + + expect(fs.existsSync(outputDir)).toBeTruthy(); + expect(files).toHaveLength(1); + expect(files[0]).toMatch(/^page-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.png$/); +}); + +test('browser_take_screenshot (filename: "output.png")', async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + const { client } = await startClient({ + config: { outputDir }, + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + expect(await client.callTool({ + name: 'browser_take_screenshot', + arguments: { + filename: 'output.png', + }, + })).toEqual({ + content: [ + { + text: expect.stringContaining(`output.png`), + type: 'text', + }, + { + data: expect.any(String), + mimeType: 'image/png', + type: 'image', + }, + ], + }); + + const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith('.png')); + + expect(fs.existsSync(outputDir)).toBeTruthy(); + expect(files).toHaveLength(1); + expect(files[0]).toMatch(/^output\.png$/); +}); + +test('browser_take_screenshot (imageResponses=omit)', async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + const { client } = await startClient({ + config: { + outputDir, + imageResponses: 'omit', + }, + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + await client.callTool({ + name: 'browser_take_screenshot', + }); + + expect(await client.callTool({ + name: 'browser_take_screenshot', + })).toEqual({ + content: [ + { + text: expect.stringContaining(`await page.screenshot`), + type: 'text', + }, + ], + }); +}); + +test('browser_take_screenshot (fullPage: true)', async ({ startClient, server }, testInfo) => { + const { client } = await startClient({ + config: { outputDir: testInfo.outputPath('output') }, + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + expect(await client.callTool({ + name: 'browser_take_screenshot', + arguments: { fullPage: true }, + })).toEqual({ + content: [ + { + text: expect.stringContaining('fullPage: true'), + type: 'text', + } + ], + }); +}); + +test('browser_take_screenshot (fullPage with element should error)', async ({ startClient, server }, testInfo) => { + const { client } = await startClient({ + config: { outputDir: testInfo.outputPath('output') }, + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + pageState: expect.stringContaining(`[ref=e1]`), + }); + + const result = await client.callTool({ + name: 'browser_take_screenshot', + arguments: { + fullPage: true, + element: 'hello button', + ref: 'e1', + }, + }); + + expect(result.isError).toBe(true); + expect(result.content?.[0]?.text).toContain('fullPage cannot be used with element screenshots'); +}); + +test('browser_take_screenshot (viewport without snapshot)', async ({ startClient, server }, testInfo) => { + const { client } = await startClient({ + config: { outputDir: testInfo.outputPath('output') }, + }); + + // Ensure we have a tab but don't navigate anywhere (no snapshot captured) + expect(await client.callTool({ + name: 'browser_tabs', + arguments: { + action: 'list', + }, + })).toHaveResponse({ + tabs: `- 0: (current) [] (about:blank)`, + }); + + // This should work without requiring a snapshot since it's a viewport screenshot + expect(await client.callTool({ + name: 'browser_take_screenshot', + })).toEqual({ + content: [ + { + text: expect.stringContaining(`page.screenshot`), + type: 'text', + }, + { + data: expect.any(String), + mimeType: 'image/png', + type: 'image', + }, + ], + }); +}); diff --git a/tests/mcp/session-log.spec.ts b/tests/mcp/session-log.spec.ts new file mode 100644 index 0000000000000..6f9a98ee66cb6 --- /dev/null +++ b/tests/mcp/session-log.spec.ts @@ -0,0 +1,275 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; + +import { test, expect } from './fixtures'; + +test('session log should record tool calls', async ({ startClient, server }, testInfo) => { + const { client, stderr } = await startClient({ + args: [ + '--save-session', + '--output-dir', testInfo.outputPath('output'), + ], + }); + + server.setContent('/', `Title`, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Submit button', + ref: 'e2', + }, + })).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Submit' }).click();`, + pageState: expect.stringContaining(`- button "Submit"`), + }); + + const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0]; + const sessionFolder = output.substring('Session: '.length); + await expect.poll(() => readSessionLog(sessionFolder)).toBe(` +### Tool call: browser_navigate +- Args +\`\`\`json +{ + "url": "http://localhost:${server.PORT}" +} +\`\`\` +- Code +\`\`\`js +await page.goto('http://localhost:${server.PORT}'); +\`\`\` +- Snapshot: 001.snapshot.yml + + +### Tool call: browser_click +- Args +\`\`\`json +{ + "element": "Submit button", + "ref": "e2" +} +\`\`\` +- Code +\`\`\`js +await page.getByRole('button', { name: 'Submit' }).click(); +\`\`\` +- Snapshot: 002.snapshot.yml + +`); +}); + +test('session log should record user action', async ({ cdpServer, startClient }, testInfo) => { + const browserContext = await cdpServer.start(); + const { client, stderr } = await startClient({ + args: [ + '--save-session', + '--output-dir', testInfo.outputPath('output'), + `--cdp-endpoint=${cdpServer.endpoint}`, + ], + }); + + // Force browser context creation. + await client.callTool({ + name: 'browser_snapshot', + }); + + const [page] = browserContext.pages(); + await page.setContent(` + + + `); + + await page.getByRole('button', { name: 'Button 1' }).click(); + + const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0]; + const sessionFolder = output.substring('Session: '.length); + + await expect.poll(() => readSessionLog(sessionFolder)).toBe(` +### Tool call: browser_snapshot +- Args +\`\`\`json +{} +\`\`\` +- Snapshot: 001.snapshot.yml + + +### User action: click +- Args +\`\`\`json +{ + "name": "click", + "ref": "e2", + "button": "left", + "modifiers": 0, + "clickCount": 1 +} +\`\`\` +- Code +\`\`\`js +await page.getByRole('button', { name: 'Button 1' }).click(); +\`\`\` +- Snapshot: 002.snapshot.yml + +`); +}); + +test('session log should update user action', async ({ cdpServer, startClient }, testInfo) => { + const browserContext = await cdpServer.start(); + const { client, stderr } = await startClient({ + args: [ + '--save-session', + '--output-dir', testInfo.outputPath('output'), + `--cdp-endpoint=${cdpServer.endpoint}`, + ], + }); + + // Force browser context creation. + await client.callTool({ + name: 'browser_snapshot', + }); + + const [page] = browserContext.pages(); + await page.setContent(` + + + `); + + await page.getByRole('button', { name: 'Button 1' }).dblclick(); + + const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0]; + const sessionFolder = output.substring('Session: '.length); + + await expect.poll(() => readSessionLog(sessionFolder)).toBe(` +### Tool call: browser_snapshot +- Args +\`\`\`json +{} +\`\`\` +- Snapshot: 001.snapshot.yml + + +### User action: click +- Args +\`\`\`json +{ + "name": "click", + "ref": "e2", + "button": "left", + "modifiers": 0, + "clickCount": 2 +} +\`\`\` +- Code +\`\`\`js +await page.getByRole('button', { name: 'Button 1' }).dblclick(); +\`\`\` +- Snapshot: 002.snapshot.yml + +`); +}); + +test('session log should record tool calls and user actions', async ({ cdpServer, startClient }, testInfo) => { + const browserContext = await cdpServer.start(); + const { client, stderr } = await startClient({ + args: [ + '--save-session', + '--output-dir', testInfo.outputPath('output'), + `--cdp-endpoint=${cdpServer.endpoint}`, + ], + }); + + const [page] = browserContext.pages(); + await page.setContent(` + + + `); + + await client.callTool({ + name: 'browser_snapshot', + }); + + // Manual action. + await page.getByRole('button', { name: 'Button 1' }).click(); + + // This is to simulate a delay after the user action before the tool action. + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Tool action. + await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button 2', + ref: 'e3', + }, + }); + + const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0]; + const sessionFolder = output.substring('Session: '.length); + await expect.poll(() => readSessionLog(sessionFolder)).toBe(` +### Tool call: browser_snapshot +- Args +\`\`\`json +{} +\`\`\` +- Snapshot: 001.snapshot.yml + + +### User action: click +- Args +\`\`\`json +{ + "name": "click", + "ref": "e2", + "button": "left", + "modifiers": 0, + "clickCount": 1 +} +\`\`\` +- Code +\`\`\`js +await page.getByRole('button', { name: 'Button 1' }).click(); +\`\`\` +- Snapshot: 002.snapshot.yml + + +### Tool call: browser_click +- Args +\`\`\`json +{ + "element": "Button 2", + "ref": "e3" +} +\`\`\` +- Code +\`\`\`js +await page.getByRole('button', { name: 'Button 2' }).click(); +\`\`\` +- Snapshot: 003.snapshot.yml + +`); +}); + +async function readSessionLog(sessionFolder: string): Promise { + return await fs.promises.readFile(path.join(sessionFolder, 'session.md'), 'utf8').catch(() => ''); +} diff --git a/tests/mcp/sse.spec.ts b/tests/mcp/sse.spec.ts new file mode 100644 index 0000000000000..42bdc4c228292 --- /dev/null +++ b/tests/mcp/sse.spec.ts @@ -0,0 +1,231 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; + +import { ChildProcess, spawn } from 'child_process'; +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { test as baseTest, expect, programPath } from './fixtures'; + +import type { Config } from '../../packages/playwright/src/mcp/config'; + +const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ + serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { + let cp: ChildProcess | undefined; + const userDataDir = testInfo.outputPath('user-data-dir'); + await use(async (options?: { args?: string[], noPort?: boolean }) => { + if (cp) + throw new Error('Process already running'); + + cp = spawn('node', [ + programPath, + ...(options?.noPort ? [] : ['--port=0']), + '--user-data-dir=' + userDataDir, + ...(mcpHeadless ? ['--headless'] : []), + ...(options?.args || []), + ], { + stdio: 'pipe', + env: { + ...process.env, + DEBUG: 'pw:mcp:test', + DEBUG_COLORS: '0', + DEBUG_HIDE_DATE: '1', + }, + }); + let stderr = ''; + const url = await new Promise(resolve => cp!.stderr?.on('data', data => { + stderr += data.toString(); + const match = stderr.match(/Listening on (http:\/\/.*)/); + if (match) + resolve(match[1]); + })); + + return { url: new URL(url), stderr: () => stderr }; + }); + cp?.kill('SIGTERM'); + }, +}); + +test('sse transport', async ({ serverEndpoint }) => { + const { url } = await serverEndpoint(); + const transport = new SSEClientTransport(new URL('/sse', url)); + const client = new Client({ name: 'test', version: '1.0.0' }); + await client.connect(transport); + await client.ping(); +}); + +test('sse transport (config)', async ({ serverEndpoint }) => { + const config: Config = { + server: { + port: 0, + } + }; + const configFile = test.info().outputPath('config.json'); + await fs.promises.writeFile(configFile, JSON.stringify(config, null, 2)); + + const { url } = await serverEndpoint({ noPort: true, args: ['--config=' + configFile] }); + const transport = new SSEClientTransport(new URL('/sse', url)); + const client = new Client({ name: 'test', version: '1.0.0' }); + await client.connect(transport); + await client.ping(); +}); + +test('sse transport browser lifecycle (isolated)', async ({ serverEndpoint, server }) => { + const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); + + const transport1 = new SSEClientTransport(new URL('/sse', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await client1.close(); + + const transport2 = new SSEClientTransport(new URL('/sse', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await client2.close(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create SSE session/)).length).toBe(2); + expect(lines.filter(line => line.match(/delete SSE session/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create context/)).length).toBe(2); + expect(lines.filter(line => line.match(/close context/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(2); + expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(2); + + expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(2); + expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(2); + }).toPass(); +}); + +test('sse transport browser lifecycle (isolated, multiclient)', async ({ serverEndpoint, server }) => { + const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); + + const transport1 = new SSEClientTransport(new URL('/sse', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const transport2 = new SSEClientTransport(new URL('/sse', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await client1.close(); + + const transport3 = new SSEClientTransport(new URL('/sse', url)); + const client3 = new Client({ name: 'test', version: '1.0.0' }); + await client3.connect(transport3); + await client3.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + await client2.close(); + await client3.close(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create SSE session/)).length).toBe(3); + expect(lines.filter(line => line.match(/delete SSE session/)).length).toBe(3); + + expect(lines.filter(line => line.match(/create context/)).length).toBe(3); + expect(lines.filter(line => line.match(/close context/)).length).toBe(3); + + expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(3); + expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(3); + + expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(1); + expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(1); + }).toPass(); +}); + +test('sse transport browser lifecycle (persistent)', async ({ serverEndpoint, server }) => { + const { url, stderr } = await serverEndpoint(); + + const transport1 = new SSEClientTransport(new URL('/sse', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await client1.close(); + + const transport2 = new SSEClientTransport(new URL('/sse', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await client2.close(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create SSE session/)).length).toBe(2); + expect(lines.filter(line => line.match(/delete SSE session/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create context/)).length).toBe(2); + expect(lines.filter(line => line.match(/close context/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create browser context \(persistent\)/)).length).toBe(2); + expect(lines.filter(line => line.match(/close browser context \(persistent\)/)).length).toBe(2); + + expect(lines.filter(line => line.match(/lock user data dir/)).length).toBe(2); + expect(lines.filter(line => line.match(/release user data dir/)).length).toBe(2); + }).toPass(); +}); + +test('sse transport browser lifecycle (persistent, multiclient)', async ({ serverEndpoint, server }) => { + const { url } = await serverEndpoint(); + + const transport1 = new SSEClientTransport(new URL('/sse', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const transport2 = new SSEClientTransport(new URL('/sse', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + const response = await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + expect(response.isError).toBe(true); + expect(response.content?.[0].text).toContain('use --isolated to run multiple instances of the same browser'); + + await client1.close(); + await client2.close(); +}); diff --git a/tests/mcp/tabs.spec.ts b/tests/mcp/tabs.spec.ts new file mode 100644 index 0000000000000..a0b40d824b783 --- /dev/null +++ b/tests/mcp/tabs.spec.ts @@ -0,0 +1,155 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; + +async function createTab(client: Client, title: string, body: string) { + await client.callTool({ + name: 'browser_tabs', + arguments: { + action: 'new', + }, + }); + return await client.callTool({ + name: 'browser_navigate', + arguments: { + url: `data:text/html,${title}${body}`, + }, + }); +} + +test('list initial tabs', async ({ client }) => { + expect(await client.callTool({ + name: 'browser_tabs', + arguments: { + action: 'list', + }, + })).toHaveResponse({ + tabs: `- 0: (current) [] (about:blank)`, + }); +}); + +test('list first tab', async ({ client }) => { + await createTab(client, 'Tab one', 'Body one'); + expect(await client.callTool({ + name: 'browser_tabs', + arguments: { + action: 'list', + }, + })).toHaveResponse({ + tabs: `- 0: [] (about:blank) +- 1: (current) [Tab one] (data:text/html,Tab oneBody one)`, + }); +}); + +test('create new tab', async ({ client }) => { + expect(await createTab(client, 'Tab one', 'Body one')).toHaveResponse({ + tabs: `- 0: [] (about:blank) +- 1: (current) [Tab one] (data:text/html,Tab oneBody one)`, + pageState: expect.stringContaining(`- Page URL: data:text/html,Tab oneBody one +- Page Title: Tab one +- Page Snapshot: +\`\`\`yaml +- generic [active] [ref=e1]: Body one +\`\`\``), + }); + + expect(await createTab(client, 'Tab two', 'Body two')).toHaveResponse({ + tabs: `- 0: [] (about:blank) +- 1: [Tab one] (data:text/html,Tab oneBody one) +- 2: (current) [Tab two] (data:text/html,Tab twoBody two)`, + pageState: expect.stringContaining(`- Page URL: data:text/html,Tab twoBody two +- Page Title: Tab two +- Page Snapshot: +\`\`\`yaml +- generic [active] [ref=e1]: Body two +\`\`\``), + }); +}); + +test('select tab', async ({ client }) => { + await createTab(client, 'Tab one', 'Body one'); + await createTab(client, 'Tab two', 'Body two'); + + expect(await client.callTool({ + name: 'browser_tabs', + arguments: { + action: 'select', + index: 1, + }, + })).toHaveResponse({ + tabs: `- 0: [] (about:blank) +- 1: (current) [Tab one] (data:text/html,Tab oneBody one) +- 2: [Tab two] (data:text/html,Tab twoBody two)`, + pageState: expect.stringContaining(`- Page URL: data:text/html,Tab oneBody one +- Page Title: Tab one +- Page Snapshot: +\`\`\`yaml +- generic [active] [ref=e1]: Body one +\`\`\``), + }); + + expect(await client.callTool({ + name: 'browser_tabs', + arguments: { + action: 'select', + index: 0, + }, + })).toHaveResponse({ + tabs: `- 0: (current) [] (about:blank) +- 1: [Tab one] (data:text/html,Tab oneBody one) +- 2: [Tab two] (data:text/html,Tab twoBody two)`, + pageState: expect.stringContaining(`- Page URL: about:blank`), + }); +}); + +test('close tab', async ({ client }) => { + await createTab(client, 'Tab one', 'Body one'); + await createTab(client, 'Tab two', 'Body two'); + + expect(await client.callTool({ + name: 'browser_tabs', + arguments: { + action: 'close', + index: 2, + }, + })).toHaveResponse({ + tabs: `- 0: [] (about:blank) +- 1: (current) [Tab one] (data:text/html,Tab oneBody one)`, + pageState: expect.stringContaining(`- Page URL: data:text/html,Tab oneBody one +- Page Title: Tab one +- Page Snapshot: +\`\`\`yaml +- generic [active] [ref=e1]: Body one +\`\`\``), + }); +}); + +test('reuse first tab when navigating', async ({ startClient, cdpServer, server }) => { + const browserContext = await cdpServer.start(); + const pages = browserContext.pages(); + + const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + expect(pages.length).toBe(1); + expect(await pages[0].title()).toBe('Title'); +}); diff --git a/tests/mcp/trace.spec.ts b/tests/mcp/trace.spec.ts new file mode 100644 index 0000000000000..927f23a798ab3 --- /dev/null +++ b/tests/mcp/trace.spec.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; + +import { test, expect } from './fixtures'; + +test('check that trace is saved', async ({ startClient, server, mcpMode }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + + const { client } = await startClient({ + args: ['--save-trace', `--output-dir=${outputDir}`], + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + const [file] = await fs.promises.readdir(outputDir); + expect(file).toContain('traces'); +}); diff --git a/tests/mcp/type.spec.ts b/tests/mcp/type.spec.ts new file mode 100644 index 0000000000000..7ac23141a3bb2 --- /dev/null +++ b/tests/mcp/type.spec.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_type', async ({ client, server }) => { + server.setContent('/', ` + + + + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + { + const response = await client.callTool({ + name: 'browser_type', + arguments: { + element: 'textbox', + ref: 'e2', + text: 'Hi!', + submit: true, + }, + }); + expect(response).toHaveResponse({ + code: `await page.getByRole('textbox').fill('Hi!'); +await page.getByRole('textbox').press('Enter');`, + pageState: expect.stringContaining(`- textbox`), + }); + } + + expect(await client.callTool({ + name: 'browser_console_messages', + })).toHaveResponse({ + result: expect.stringContaining(`[LOG] Key pressed: Enter , Text: Hi!`), + }); +}); + +test('browser_type (slowly)', async ({ client, server }) => { + server.setContent('/', ` + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + { + const response = await client.callTool({ + name: 'browser_type', + arguments: { + element: 'textbox', + ref: 'e2', + text: 'Hi!', + slowly: true, + }, + }); + + expect(response).toHaveResponse({ + code: `await page.getByRole('textbox').pressSequentially('Hi!');`, + pageState: expect.stringContaining(`- textbox`), + }); + } + const response = await client.callTool({ + name: 'browser_console_messages', + }); + expect(response).toHaveResponse({ + result: expect.stringContaining(`[LOG] Key pressed: H Text: `), + }); + expect(response).toHaveResponse({ + result: expect.stringContaining(`[LOG] Key pressed: i Text: H`), + }); + expect(response).toHaveResponse({ + result: expect.stringContaining(`[LOG] Key pressed: ! Text: Hi`), + }); +}); + +test('browser_type (no submit)', async ({ client, server }) => { + server.setContent('/', ` + + `, 'text/html'); + + { + const response = await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + expect(response).toHaveResponse({ + pageState: expect.stringContaining(`- textbox`), + }); + } + { + const response = await client.callTool({ + name: 'browser_type', + arguments: { + element: 'textbox', + ref: 'e2', + text: 'Hi!', + }, + }); + expect(response).toHaveResponse({ + code: expect.stringContaining(`fill('Hi!')`), + // Should yield no snapshot. + pageState: expect.not.stringContaining(`- textbox`), + }); + } + { + const response = await client.callTool({ + name: 'browser_console_messages', + }); + expect(response).toHaveResponse({ + result: expect.stringContaining(`[LOG] New value: Hi!`), + }); + } +}); diff --git a/tests/mcp/verify.spec.ts b/tests/mcp/verify.spec.ts new file mode 100644 index 0000000000000..58c9b36d3d4c0 --- /dev/null +++ b/tests/mcp/verify.spec.ts @@ -0,0 +1,522 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test.use({ mcpArgs: ['--caps=verify'] }); + +test('browser_verify_element_visible', async ({ client, server }) => { + server.setContent('/', ` + Test Page + +

Welcome

+
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_element_visible', + arguments: { + role: 'button', + accessibleName: 'Submit', + }, + })).toHaveResponse({ + result: 'Done', + code: `await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();`, + }); + + expect(await client.callTool({ + name: 'browser_verify_element_visible', + arguments: { + role: 'heading', + accessibleName: 'Welcome', + }, + })).toHaveResponse({ + result: 'Done', + code: `await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();`, + }); + + expect(await client.callTool({ + name: 'browser_verify_element_visible', + arguments: { + role: 'alert', + accessibleName: 'Success message', + }, + })).toHaveResponse({ + result: 'Done', + code: `await expect(page.getByRole('alert', { name: 'Success message' })).toBeVisible();`, + }); +}); + +test('browser_verify_element_visible (not found)', async ({ client, server }) => { + server.setContent('/', ` + Test Page + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_element_visible', + arguments: { + role: 'button', + accessibleName: 'Cancel', + }, + })).toHaveResponse({ + isError: true, + result: 'Element with role "button" and accessible name "Cancel" not found', + }); +}); + +test('browser_verify_text_visible', async ({ client, server }) => { + server.setContent('/', ` + Test Page +

Hello world

+
Welcome to our site
+ Status: Active + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_text_visible', + arguments: { + text: 'Hello world', + }, + })).toHaveResponse({ + result: 'Done', + code: `await expect(page.getByText('Hello world')).toBeVisible();`, + }); + + expect(await client.callTool({ + name: 'browser_verify_text_visible', + arguments: { + text: 'Welcome to our site', + }, + })).toHaveResponse({ + result: 'Done', + code: `await expect(page.getByText('Welcome to our site')).toBeVisible();`, + }); + + expect(await client.callTool({ + name: 'browser_verify_text_visible', + arguments: { + text: 'Status: Active', + }, + })).toHaveResponse({ + result: 'Done', + code: `await expect(page.getByText('Status: Active')).toBeVisible();`, + }); +}); + +test('browser_verify_text_visible (not found)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +

Hello world

+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_text_visible', + arguments: { + text: 'Goodbye world', + }, + })).toHaveResponse({ + isError: true, + result: 'Text not found', + }); +}); + +test('browser_verify_text_visible (with quotes)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +

She said "Hello world"

+
It's a beautiful day
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_text_visible', + arguments: { + text: 'She said "Hello world"', + }, + })).toHaveResponse({ + result: 'Done', + code: `await expect(page.getByText('She said "Hello world"')).toBeVisible();`, + }); + + expect(await client.callTool({ + name: 'browser_verify_text_visible', + arguments: { + text: "It's a beautiful day", + }, + })).toHaveResponse({ + result: 'Done', + code: `await expect(page.getByText('It\\'s a beautiful day')).toBeVisible();`, + }); +}); + +test('browser_verify_list_visible', async ({ client, server }) => { + server.setContent('/', ` + Test Page +
    +
  • Apple
  • +
  • Banana
  • +
  • Cherry
  • +
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_list_visible', + arguments: { + element: 'Fruit list', + ref: 'e2', + items: ['Apple', 'Banana', 'Cherry'], + }, + })).toHaveResponse({ + result: 'Done', + code: expect.stringContaining(`await expect(page.locator('body')).toMatchAriaSnapshot(\` +- list: + - listitem: "Apple" + - listitem: "Banana" + - listitem: "Cherry" +\`);`), + }); +}); + +test('browser_verify_list_visible (partial items)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +
    +
  • Apple
  • +
  • Banana
  • +
  • Cherry
  • +
  • Date
  • +
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_list_visible', + arguments: { + element: 'Fruit list', + ref: 'e2', + items: ['Apple', 'Cherry'], + }, + })).toHaveResponse({ + result: 'Done', + code: expect.stringContaining(`await expect(page.locator('body')).toMatchAriaSnapshot(\` +- list: + - listitem: "Apple" + - listitem: "Cherry" +\`);`), + }); +}); + +test('browser_verify_list_visible (item not found)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +
    +
  • Apple
  • +
  • Banana
  • +
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_list_visible', + arguments: { + element: 'Fruit list', + ref: 'e2', + items: ['Apple', 'Cherry'], + }, + })).toHaveResponse({ + isError: true, + result: 'Item "Cherry" not found', + }); +}); + +test('browser_verify_value (textbox)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +
+ + +
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_value', + arguments: { + type: 'textbox', + element: 'Name textbox', + ref: 'e3', + value: 'John Doe', + }, + })).toHaveResponse({ + result: 'Done', + code: expect.stringContaining(`await expect(page.getByRole('textbox', { name: 'Name' })).toHaveValue('John Doe');`), + }); + + expect(await client.callTool({ + name: 'browser_verify_value', + arguments: { + type: 'textbox', + element: 'Email textbox', + ref: 'e4', + value: 'john@example.com', + }, + })).toHaveResponse({ + result: 'Done', + code: expect.stringContaining(`await expect(page.getByRole('textbox', { name: 'Email' })).toHaveValue('john@example.com');`), + }); +}); + +test('browser_verify_value (textbox wrong value)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +
+ +
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_value', + arguments: { + type: 'textbox', + element: 'Name textbox', + ref: 'e3', + value: 'Jane Smith', + }, + })).toHaveResponse({ + isError: true, + result: 'Expected value "Jane Smith", but got "John Doe"', + }); +}); + +test('browser_verify_value (checkbox checked)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +
+ + +
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_value', + arguments: { + type: 'checkbox', + element: 'Subscribe checkbox', + ref: 'e3', + value: 'true', + }, + })).toHaveResponse({ + result: 'Done', + code: expect.stringContaining(`await expect(page.getByRole('checkbox')).toBeChecked();`), + }); +}); + +test('browser_verify_value (checkbox unchecked)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +
+ + +
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_value', + arguments: { + type: 'checkbox', + element: 'Subscribe checkbox', + ref: 'e3', + value: 'false', + }, + })).toHaveResponse({ + result: 'Done', + code: expect.stringContaining(`await expect(page.getByRole('checkbox')).not.toBeChecked();`), + }); +}); + +test('browser_verify_value (checkbox wrong value)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +
+ + +
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_value', + arguments: { + type: 'checkbox', + element: 'Subscribe checkbox', + ref: 'e3', + value: 'false', + }, + })).toHaveResponse({ + isError: true, + result: 'Expected value "false", but got "true"', + }); +}); + +test('browser_verify_value (radio checked)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +
+ + + + +
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_value', + arguments: { + type: 'radio', + element: 'Color radio', + ref: 'e3', + value: 'true', + }, + })).toHaveResponse({ + result: 'Done', + code: expect.stringContaining(`await expect(page.getByRole('radio', { name: 'Red' })).toBeChecked();`), + }); +}); + +test('browser_verify_value (slider)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +
+ + +
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_value', + arguments: { + type: 'slider', + element: 'Volume slider', + ref: 'e3', + value: '75', + }, + })).toHaveResponse({ + result: 'Done', + code: expect.stringContaining(`await expect(page.getByRole('slider')).toHaveValue('75');`), + }); +}); + +test('browser_verify_value (combobox)', async ({ client, server }) => { + server.setContent('/', ` + Test Page +
+ +
+ `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_verify_value', + arguments: { + type: 'combobox', + element: 'Country select', + ref: 'e3', + value: 'United States', + }, + })).toHaveResponse({ + result: 'Done', + code: expect.stringContaining(`await expect(page.getByRole('combobox')).toHaveValue('United States');`), + }); +}); diff --git a/tests/mcp/wait.spec.ts b/tests/mcp/wait.spec.ts new file mode 100644 index 0000000000000..0388faf259b57 --- /dev/null +++ b/tests/mcp/wait.spec.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_wait_for(text)', async ({ client, server }) => { + server.setContent('/', ` + + + +
Text to disappear
+ + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Click me', + ref: 'e2', + }, + }); + + expect(await client.callTool({ + name: 'browser_wait_for', + arguments: { text: 'Text to appear' }, + code: `await page.getByText("Text to appear").first().waitFor({ state: 'visible' });`, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`), + }); +}); + +test('browser_wait_for(textGone)', async ({ client, server }) => { + server.setContent('/', ` + + + +
Text to disappear
+ + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Click me', + ref: 'e2', + }, + }); + + expect(await client.callTool({ + name: 'browser_wait_for', + arguments: { textGone: 'Text to disappear' }, + code: `await page.getByText("Text to disappear").first().waitFor({ state: 'hidden' });`, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`), + }); +}); + +test('browser_wait_for(time)', async ({ client, server }) => { + server.setContent('/', `
Hello World
`, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_wait_for', + arguments: { time: 1 }, + })).toHaveResponse({ + code: `await new Promise(f => setTimeout(f, 1 * 1000));`, + }); +}); diff --git a/tests/mcp/webdriver.spec.ts b/tests/mcp/webdriver.spec.ts new file mode 100644 index 0000000000000..9467056f9efff --- /dev/null +++ b/tests/mcp/webdriver.spec.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('do not falsely advertise user agent as a test driver', async ({ client, server, mcpBrowser }) => { + test.skip(mcpBrowser === 'firefox'); + test.skip(mcpBrowser === 'webkit'); + server.setRoute('/', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + + `); + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + })).toHaveResponse({ + pageState: expect.stringContaining(`webdriver: false`), + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 1fb09a13a00b4..9f66b9c2122de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,5 +36,6 @@ "exclude": [ "packages/*/lib", "packages/html-reporter/", + "packages/mcp-extension/", ] } From da6f1333420eeabd5b1a6d0547e6aca6d894c8c9 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 4 Sep 2025 09:16:03 +0100 Subject: [PATCH 072/329] feat(fixtures): support `box: 'self'` (#37279) --- docs/src/test-fixtures-js.md | 2 ++ packages/playwright/src/common/fixtures.ts | 6 ++-- .../playwright/src/worker/fixtureRunner.ts | 22 ++++++++----- packages/playwright/types/test.d.ts | 8 ++--- tests/playwright-test/test-step.spec.ts | 33 ++++++++++++++----- utils/generate_types/overrides-test.d.ts | 8 ++--- 6 files changed, 52 insertions(+), 27 deletions(-) diff --git a/docs/src/test-fixtures-js.md b/docs/src/test-fixtures-js.md index 19fbf687c1b30..16afe6f25ead8 100644 --- a/docs/src/test-fixtures-js.md +++ b/docs/src/test-fixtures-js.md @@ -760,6 +760,8 @@ export const test = base.extend({ This is useful for non-interesting helper fixtures. For example, an [automatic](./test-fixtures.md#automatic-fixtures) fixture that sets up some common data can be safely hidden from a test report. +You can also mark the fixture as `box: 'self'` to only hide that particular fixture, but include all the steps inside the fixture in the test report. + ## Custom fixture title Instead of the usual fixture name, you can give fixtures a custom title that will be shown in test reports and error messages. diff --git a/packages/playwright/src/common/fixtures.ts b/packages/playwright/src/common/fixtures.ts index 470741302d733..30b144e663465 100644 --- a/packages/playwright/src/common/fixtures.ts +++ b/packages/playwright/src/common/fixtures.ts @@ -25,7 +25,7 @@ import type { Location } from '../../types/testReporter'; export type FixtureScope = 'test' | 'worker'; type FixtureAuto = boolean | 'all-hooks-included'; const kScopeOrder: FixtureScope[] = ['test', 'worker']; -type FixtureOptions = { auto?: FixtureAuto, scope?: FixtureScope, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }; +type FixtureOptions = { auto?: FixtureAuto, scope?: FixtureScope, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean | 'self' }; type FixtureTuple = [ value: any, options: FixtureOptions ]; export type FixtureRegistration = { // Fixture registration location. @@ -52,7 +52,7 @@ export type FixtureRegistration = { // Whether this fixture is an option override value set from the config. optionOverride?: boolean; // Do not generate the step for this fixture, consider it internal. - box?: boolean; + box?: boolean | 'self'; }; export type LoadError = { message: string; @@ -105,7 +105,7 @@ export class FixturePool { for (const entry of Object.entries(fixtures)) { const name = entry[0]; let value = entry[1]; - let options: { auto: FixtureAuto, scope: FixtureScope, option: boolean, timeout: number | undefined, customTitle?: string, box?: boolean } | undefined; + let options: { auto: FixtureAuto, scope: FixtureScope, option: boolean, timeout: number | undefined, customTitle?: string, box?: boolean | 'self' } | undefined; if (isFixtureTuple(value)) { options = { auto: value[1].auto ?? false, diff --git a/packages/playwright/src/worker/fixtureRunner.ts b/packages/playwright/src/worker/fixtureRunner.ts index 6e236b4486e33..413c86c0eeb49 100644 --- a/packages/playwright/src/worker/fixtureRunner.ts +++ b/packages/playwright/src/worker/fixtureRunner.ts @@ -35,7 +35,7 @@ class Fixture { private _selfTeardownComplete: Promise | undefined; private _setupDescription: FixtureDescription; private _teardownDescription: FixtureDescription; - private _stepInfo: { title: string, category: 'fixture', location?: Location, group?: string }; + private _stepInfo: { title: string, category: 'fixture', location?: Location, group?: string } | undefined; _deps = new Set(); _usages = new Set(); @@ -47,7 +47,9 @@ class Fixture { const title = this.registration.customTitle || this.registration.name; const location = isUserFixture ? this.registration.location : undefined; this._stepInfo = { title: `Fixture ${escapeWithQuotes(title, '"')}`, category: 'fixture', location }; - if (this.registration.box) + if (this.registration.box === 'self') + this._stepInfo = undefined; + else if (this.registration.box) this._stepInfo.group = isUserFixture ? 'configuration' : 'internal'; this._setupDescription = { title, @@ -69,9 +71,11 @@ class Fixture { return; } - await testInfo._runAsStep(this._stepInfo, async () => { - await testInfo._runWithTimeout({ ...runnable, fixture: this._setupDescription }, () => this._setupInternal(testInfo)); - }); + const run = () => testInfo._runWithTimeout({ ...runnable, fixture: this._setupDescription }, () => this._setupInternal(testInfo)); + if (this._stepInfo) + await testInfo._runAsStep(this._stepInfo, run); + else + await run(); } private async _setupInternal(testInfo: TestInfoImpl) { @@ -130,9 +134,11 @@ class Fixture { // Do not even start the teardown for a fixture that does not have any // time remaining in the time slot. This avoids cascading timeouts. if (!testInfo._timeoutManager.isTimeExhaustedFor(fixtureRunnable)) { - await testInfo._runAsStep(this._stepInfo, async () => { - await testInfo._runWithTimeout(fixtureRunnable, () => this._teardownInternal()); - }); + const run = () => testInfo._runWithTimeout(fixtureRunnable, () => this._teardownInternal()); + if (this._stepInfo) + await testInfo._runAsStep(this._stepInfo, run); + else + await run(); } } finally { // To preserve fixtures integrity, forcefully cleanup fixtures diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index ae1a6ba88b5a2..87363f3a0fb85 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -6574,13 +6574,13 @@ export type WorkerFixture = (args: Args, use: (r: R) => Prom type TestFixtureValue = Exclude | TestFixture; type WorkerFixtureValue = Exclude | WorkerFixture; export type Fixtures = { - [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }]; + [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean | 'self' }]; } & { - [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }]; + [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean | 'self' }]; } & { - [K in Exclude]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; + [K in Exclude]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean | 'self' }]; } & { - [K in Exclude]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; + [K in Exclude]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean | 'self' }]; }; type BrowserName = 'chromium' | 'firefox' | 'webkit'; diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index 3e472d4311193..c49820735cdc7 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -1770,7 +1770,7 @@ pw:api | Close context `); }); -test('should box fixtures with everything inside them', async ({ runInlineTest }) => { +test('should box fixtures', async ({ runInlineTest }) => { const result = await runInlineTest({ 'reporter.ts': stepIndentReporter, 'playwright.config.ts': `module.exports = { reporter: './reporter' };`, @@ -1784,6 +1784,7 @@ test('should box fixtures with everything inside them', async ({ runInlineTest } await page.goto('data:text/html,
here we go
'); }); await use(1); + await page.setContent('
here we go
'); }, { box: true }], bar: [async ({ page }, use) => { await page.setContent('
here we go
'); @@ -1791,13 +1792,23 @@ test('should box fixtures with everything inside them', async ({ runInlineTest } await page.goto('data:text/html,
here we go
'); }); await use(2); + await page.setContent('
here we go
'); }, { box: false }], + baz: [async ({ page }, use) => { + await page.setContent('
here we go
'); + await test.step('inner step', async () => { + await page.goto('data:text/html,
here we go
'); + }); + await use(3); + await page.setContent('
here we go
'); + }, { box: 'self' }], }); - test('test', async ({ foo, bar, page }) => { + test('test', async ({ foo, bar, baz, page }) => { await expect(page.locator('body')).toBeVisible(); expect(foo).toBe(1); expect(bar).toBe(2); + expect(baz).toBe(3); }); ` }, { reporter: '' }); @@ -1812,14 +1823,20 @@ pw:api | Create context fixture | Fixture "page" pw:api | Create page fixture | Fixture "bar" @ a.test.ts:4 -pw:api | Set content @ a.test.ts:13 -test.step | inner step @ a.test.ts:14 -pw:api | Navigate to "data:" @ a.test.ts:15 -expect |Expect "toBeVisible" @ a.test.ts:22 -expect |Expect "toBe" @ a.test.ts:23 -expect |Expect "toBe" @ a.test.ts:24 +pw:api | Set content @ a.test.ts:14 +test.step | inner step @ a.test.ts:15 +pw:api | Navigate to "data:" @ a.test.ts:16 +pw:api | Set content @ a.test.ts:22 +test.step | inner step @ a.test.ts:23 +pw:api | Navigate to "data:" @ a.test.ts:24 +expect |Expect "toBeVisible" @ a.test.ts:32 +expect |Expect "toBe" @ a.test.ts:33 +expect |Expect "toBe" @ a.test.ts:34 +expect |Expect "toBe" @ a.test.ts:35 hook |After Hooks +pw:api | Set content @ a.test.ts:27 fixture | Fixture "bar" @ a.test.ts:4 +pw:api | Set content @ a.test.ts:19 fixture | Fixture "page" fixture | Fixture "context" pw:api | Close context diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 374f66f179e35..03f9babb43c77 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -191,13 +191,13 @@ export type WorkerFixture = (args: Args, use: (r: R) => Prom type TestFixtureValue = Exclude | TestFixture; type WorkerFixtureValue = Exclude | WorkerFixture; export type Fixtures = { - [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }]; + [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean | 'self' }]; } & { - [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }]; + [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean | 'self' }]; } & { - [K in Exclude]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; + [K in Exclude]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean | 'self' }]; } & { - [K in Exclude]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }]; + [K in Exclude]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean | 'self' }]; }; type BrowserName = 'chromium' | 'firefox' | 'webkit'; From 23e0dd19a1713d1a0d218435549a54ecf5af4626 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 4 Sep 2025 10:50:50 +0100 Subject: [PATCH 073/329] chore: regenerate package-lock.json and update deps (#37293) --- eslint.config.mjs | 1 + package-lock.json | 4250 +++++++++++------ package.json | 2 +- packages/injected/src/reactSelectorEngine.ts | 14 +- .../playwright/src/transform/transform.ts | 6 +- 5 files changed, 2905 insertions(+), 1368 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 02dbc2f1a453c..0aa507cf85d9e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -50,6 +50,7 @@ const ignores = [ "packages/playwright-core/src/third_party/", "packages/playwright-core/types/*", "packages/playwright-ct-core/src/generated/*", + "packages/playwright/bundles/expect/third_party/", "packages/html-reporter/bundle.ts", "packages/html-reporter/playwright.config.ts", "packages/html-reporter/playwright/*", diff --git a/package-lock.json b/package-lock.json index 55ebf2271fb2e..0792b3c2711f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@types/codemirror": "^5.60.7", "@types/formidable": "^2.0.4", "@types/immutable": "^3.8.7", - "@types/node": "^18.19.68", + "@types/node": "18.19.76", "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", "@types/ws": "^8.5.3", @@ -71,8 +71,9 @@ }, "node_modules/@actions/core": { "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "dev": true, - "license": "MIT", "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" @@ -80,27 +81,33 @@ }, "node_modules/@actions/exec": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "dev": true, - "license": "MIT", "dependencies": { "@actions/io": "^1.0.1" } }, "node_modules/@actions/github": { - "version": "6.0.0", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", + "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", "dev": true, - "license": "MIT", "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", - "@octokit/plugin-paginate-rest": "^9.0.0", - "@octokit/plugin-rest-endpoint-methods": "^10.0.0" + "@octokit/plugin-paginate-rest": "^9.2.2", + "@octokit/plugin-rest-endpoint-methods": "^10.4.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "undici": "^5.28.5" } }, "node_modules/@actions/http-client": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", "dev": true, - "license": "MIT", "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" @@ -108,12 +115,14 @@ }, "node_modules/@actions/io": { "version": "1.1.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true }, "node_modules/@ampproject/remapping": { "version": "2.3.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -123,38 +132,41 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "license": "MIT", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.26.9", - "license": "MIT", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.9", - "@babel/types": "^7.26.9", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -169,14 +181,23 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { - "version": "7.26.9", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -184,11 +205,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "license": "MIT", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -197,24 +219,42 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "license": "MIT", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -224,49 +264,55 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "license": "MIT", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "license": "MIT", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -276,10 +322,11 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.25.9", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -289,10 +336,11 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.25.9", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -302,50 +350,51 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.10", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "dev": true, - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.26.9", - "license": "MIT", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.9", - "license": "MIT", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.26.10", - "license": "MIT", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -353,8 +402,9 @@ }, "node_modules/@electron/get": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", @@ -371,365 +421,765 @@ "global-agent": "^3.0.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ - "x64" + "ppc64" ], - "license": "MIT", "optional": true, "os": [ - "linux" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "dev": true, - "license": "MIT", + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@eslint/compat": { - "version": "1.3.2", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^8.40 || 9" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/core": { - "version": "0.15.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "dev": true, - "license": "MIT", + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/js": { - "version": "9.34.0", - "dev": true, - "license": "MIT", + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": ">=18" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.15.2", - "levn": "^0.4.1" - }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "dev": true, - "license": "MIT", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14" + "node": ">=18" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "license": "MIT", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "license": "MIT", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "license": "MIT" + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.5", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.6", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">=18" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", + "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==", "dev": true, - "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">= 8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, "engines": { - "node": ">= 8" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@octokit/auth-token": { - "version": "4.0.0", + "node_modules/@eslint/compat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.2.tgz", + "integrity": "sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.40 || 9" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/@octokit/core": { - "version": "5.2.1", + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, - "license": "MIT", "dependencies": { - "@octokit/auth-token": "^4.0.0", - "@octokit/graphql": "^7.1.0", - "@octokit/request": "^8.4.1", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" }, "engines": { - "node": ">= 18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@octokit/endpoint": { - "version": "9.0.6", + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" - }, "engines": { - "node": ">= 18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@octokit/graphql": { - "version": "7.1.1", + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", + "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", + "dev": true, + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "dev": true, + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", @@ -741,8 +1191,9 @@ }, "node_modules/@octokit/graphql-schema": { "version": "15.26.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql-schema/-/graphql-schema-15.26.0.tgz", + "integrity": "sha512-SoVbh+sXe9nsoweFbLT3tAk3XWYbYLs5ku05wij1zhyQ2U3lewdrhjo5Tb7lfaOGWNHSkPZT4uuPZp8neF7P7A==", "dev": true, - "license": "MIT", "dependencies": { "graphql": "^16.0.0", "graphql-tag": "^2.10.3" @@ -750,13 +1201,15 @@ }, "node_modules/@octokit/openapi-types": { "version": "24.2.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true }, "node_modules/@octokit/plugin-paginate-rest": { "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/types": "^12.6.0" }, @@ -769,21 +1222,24 @@ }, "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { "version": "20.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true }, "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/types": "^12.6.0" }, @@ -796,21 +1252,24 @@ }, "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { "version": "20.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true }, "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "node_modules/@octokit/request": { "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", @@ -823,8 +1282,9 @@ }, "node_modules/@octokit/request-error": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", @@ -836,12 +1296,22 @@ }, "node_modules/@octokit/types": { "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", "dev": true, - "license": "MIT", "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@playwright/browser-chromium": { "resolved": "packages/playwright-browser-chromium", "link": true @@ -886,37 +1356,274 @@ "resolved": "packages/playwright-test", "link": true }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.1", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.0.tgz", + "integrity": "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==", "cpu": [ - "x64" + "arm" ], - "license": "MIT", "optional": true, "os": [ - "linux" + "android" ] }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.1", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.0.tgz", + "integrity": "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==", "cpu": [ - "x64" + "arm64" ], - "license": "MIT", "optional": true, "os": [ - "linux" + "android" ] }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "dev": true, - "license": "MIT" + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.0.tgz", + "integrity": "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@sindresorhus/is": { + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.0.tgz", + "integrity": "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.0.tgz", + "integrity": "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.0.tgz", + "integrity": "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.0.tgz", + "integrity": "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.0.tgz", + "integrity": "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.0.tgz", + "integrity": "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.0.tgz", + "integrity": "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.0.tgz", + "integrity": "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.0.tgz", + "integrity": "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.0.tgz", + "integrity": "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.0.tgz", + "integrity": "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.0.tgz", + "integrity": "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.0.tgz", + "integrity": "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.0.tgz", + "integrity": "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.0.tgz", + "integrity": "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.0.tgz", + "integrity": "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.0.tgz", + "integrity": "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.0.tgz", + "integrity": "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@sindresorhus/is": { "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -925,12 +1632,13 @@ } }, "node_modules/@stylistic/eslint-plugin": { - "version": "5.2.3", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.3.1.tgz", + "integrity": "sha512-Ykums1VYonM0TgkD0VteVq9mrlO2FhF48MDJnPyv3MktIB2ydtuhlO0AfWm7xnW1kyf5bjOqA6xc7JjviuVTxg==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/types": "^8.38.0", + "@typescript-eslint/types": "^8.41.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", @@ -945,15 +1653,17 @@ }, "node_modules/@sveltejs/acorn-typescript": { "version": "1.0.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", "peerDependencies": { "acorn": "^8.9.0" } }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "dev": true, - "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.0" }, @@ -963,7 +1673,8 @@ }, "node_modules/@types/babel__core": { "version": "7.20.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -973,31 +1684,35 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.8", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { "version": "7.4.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "license": "MIT", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/cacheable-request": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dev": true, - "license": "MIT", "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", @@ -1007,163 +1722,186 @@ }, "node_modules/@types/chrome": { "version": "0.0.315", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.315.tgz", + "integrity": "sha512-Oy1dYWkr6BCmgwBtOngLByCHstQ3whltZg7/7lubgIZEYvKobDneqplgc6LKERNRBwckFviV4UU5AZZNUFrJ4A==", "dev": true, - "license": "MIT", "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "node_modules/@types/codemirror": { - "version": "5.60.15", + "version": "5.60.16", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.16.tgz", + "integrity": "sha512-V/yHdamffSS075jit+fDxaOAmdP2liok8NSNJnAZfDJErzOheuygHZEhAJrfmk5TEyM32MhkZjwo/idX791yxw==", "dev": true, - "license": "MIT", "dependencies": { "@types/tern": "*" } }, "node_modules/@types/estree": { - "version": "1.0.7", - "license": "MIT" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "node_modules/@types/filesystem": { "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", "dev": true, - "license": "MIT", "dependencies": { "@types/filewriter": "*" } }, "node_modules/@types/filewriter": { "version": "0.0.33", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true }, "node_modules/@types/formidable": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.6.tgz", + "integrity": "sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/har-format": { "version": "1.2.16", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true }, "node_modules/@types/immutable": { "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@types/immutable/-/immutable-3.8.7.tgz", + "integrity": "sha512-nsHFDX48Tl3RaP4BF47HHe5njx40Pcp+0a8CIqzJata80Fp7JzkcuGB7UhZBGjH9aA1fMEahIqvPQQNmro5YLg==", + "deprecated": "This is a stub types definition for Facebook's Immutable (https://github.com/facebook/immutable-js). Facebook's Immutable provides its own type definitions, so you don't need @types/immutable installed!", "dev": true, - "license": "MIT", "dependencies": { "immutable": "*" } }, "node_modules/@types/json-schema": { "version": "7.0.15", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "node_modules/@types/json5": { "version": "0.0.29", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true }, "node_modules/@types/keyv": { "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { "version": "18.19.76", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz", + "integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==", "devOptional": true, - "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/prop-types": { - "version": "15.7.14", - "dev": true, - "license": "MIT" + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true }, "node_modules/@types/react": { - "version": "18.3.18", + "version": "18.3.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", + "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "dev": true, - "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.5", + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, - "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" } }, "node_modules/@types/responselike": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/tern": { "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "*" } }, "node_modules/@types/ws": { - "version": "8.5.14", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/xml2js": { "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/yauzl": { "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.41.0", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz", + "integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.41.0", - "@typescript-eslint/type-utils": "8.41.0", - "@typescript-eslint/utils": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", + "@typescript-eslint/scope-manager": "8.42.0", + "@typescript-eslint/type-utils": "8.42.0", + "@typescript-eslint/utils": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1177,28 +1915,30 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.41.0", + "@typescript-eslint/parser": "^8.42.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.41.0", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz", + "integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", + "@typescript-eslint/scope-manager": "8.42.0", + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/typescript-estree": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0", "debug": "^4.3.4" }, "engines": { @@ -1214,12 +1954,13 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.41.0", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz", + "integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.41.0", - "@typescript-eslint/types": "^8.41.0", + "@typescript-eslint/tsconfig-utils": "^8.42.0", + "@typescript-eslint/types": "^8.42.0", "debug": "^4.3.4" }, "engines": { @@ -1234,12 +1975,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.41.0", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz", + "integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0" + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1250,9 +1992,10 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.41.0", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz", + "integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1265,13 +2008,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.41.0", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz", + "integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0", - "@typescript-eslint/utils": "8.41.0", + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/typescript-estree": "8.42.0", + "@typescript-eslint/utils": "8.42.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1288,9 +2032,10 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.41.0", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz", + "integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1300,14 +2045,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.41.0", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz", + "integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.41.0", - "@typescript-eslint/tsconfig-utils": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/visitor-keys": "8.41.0", + "@typescript-eslint/project-service": "8.42.0", + "@typescript-eslint/tsconfig-utils": "8.42.0", + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1328,16 +2074,18 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1348,26 +2096,16 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.2", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "8.41.0", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.42.0.tgz", + "integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.41.0", - "@typescript-eslint/types": "8.41.0", - "@typescript-eslint/typescript-estree": "8.41.0" + "@typescript-eslint/scope-manager": "8.42.0", + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/typescript-estree": "8.42.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1382,11 +2120,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.41.0", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz", + "integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/types": "8.42.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1399,8 +2138,9 @@ }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", + "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.21.3" }, @@ -1409,25 +2149,28 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.4", - "license": "MIT", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dependencies": { - "@babel/core": "^7.26.0", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "react-refresh": "^0.17.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@vitejs/plugin-vue": { - "version": "5.2.1", - "license": "MIT", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", "engines": { "node": "^18.0.0 || >=20.0.0" }, @@ -1437,110 +2180,110 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.13", - "license": "MIT", + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz", + "integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==", "peer": true, "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.21", "entities": "^4.5.0", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" + "source-map-js": "^1.2.1" } }, - "node_modules/@vue/compiler-core/node_modules/estree-walker": { - "version": "2.0.2", - "license": "MIT", - "peer": true - }, "node_modules/@vue/compiler-dom": { - "version": "3.5.13", - "license": "MIT", + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz", + "integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==", "peer": true, "dependencies": { - "@vue/compiler-core": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-core": "3.5.21", + "@vue/shared": "3.5.21" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.13", - "license": "MIT", + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz", + "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==", "peer": true, "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.13", - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.21", + "@vue/compiler-dom": "3.5.21", + "@vue/compiler-ssr": "3.5.21", + "@vue/shared": "3.5.21", "estree-walker": "^2.0.2", - "magic-string": "^0.30.11", - "postcss": "^8.4.48", - "source-map-js": "^1.2.0" + "magic-string": "^0.30.18", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" } }, - "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { - "version": "2.0.2", - "license": "MIT", - "peer": true - }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.13", - "license": "MIT", + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz", + "integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==", "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.21", + "@vue/shared": "3.5.21" } }, "node_modules/@vue/reactivity": { - "version": "3.5.13", - "license": "MIT", + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz", + "integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==", "peer": true, "dependencies": { - "@vue/shared": "3.5.13" + "@vue/shared": "3.5.21" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.13", - "license": "MIT", + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.21.tgz", + "integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==", "peer": true, "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/reactivity": "3.5.21", + "@vue/shared": "3.5.21" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.13", - "license": "MIT", + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz", + "integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==", "peer": true, "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/runtime-core": "3.5.13", - "@vue/shared": "3.5.13", + "@vue/reactivity": "3.5.21", + "@vue/runtime-core": "3.5.21", + "@vue/shared": "3.5.21", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.13", - "license": "MIT", + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.21.tgz", + "integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==", "peer": true, "dependencies": { - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-ssr": "3.5.21", + "@vue/shared": "3.5.21" }, "peerDependencies": { - "vue": "3.5.13" + "vue": "3.5.21" } }, "node_modules/@vue/shared": { - "version": "3.5.13", - "license": "MIT", + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz", + "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", "peer": true }, "node_modules/@zip.js/zip.js": { - "version": "2.7.57", + "version": "2.7.73", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.73.tgz", + "integrity": "sha512-I2UP8/rdQE5hTtVVL08B7P8XuwXiKuuMUPjNuFOVL/9b+8IsExR9S5jz2H58u0rJjU4M1BikLgqEMG8gZJZVBw==", "dev": true, - "license": "BSD-3-Clause", "engines": { "bun": ">=0.7.0", "deno": ">=1.0.0", @@ -1549,13 +2292,15 @@ }, "node_modules/abbrev": { "version": "1.1.1", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true }, "node_modules/accepts": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, - "license": "MIT", "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -1566,7 +2311,8 @@ }, "node_modules/acorn": { "version": "8.15.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "bin": { "acorn": "bin/acorn" }, @@ -1576,16 +2322,18 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1599,16 +2347,18 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1621,8 +2371,9 @@ }, "node_modules/anymatch": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, - "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1633,8 +2384,9 @@ }, "node_modules/anymatch/node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1644,20 +2396,23 @@ }, "node_modules/argparse": { "version": "2.0.1", - "dev": true, - "license": "Python-2.0" + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/aria-query": { "version": "5.3.2", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "engines": { "node": ">= 0.4" } }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" @@ -1671,16 +2426,18 @@ }, "node_modules/array-find-index": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/array-includes": { "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -1700,8 +2457,9 @@ }, "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, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -1719,8 +2477,9 @@ }, "node_modules/array.prototype.findlastindex": { "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -1739,8 +2498,9 @@ }, "node_modules/array.prototype.flat": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -1756,8 +2516,9 @@ }, "node_modules/array.prototype.flatmap": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -1773,8 +2534,9 @@ }, "node_modules/array.prototype.tosorted": { "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, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -1788,8 +2550,9 @@ }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, - "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", @@ -1808,21 +2571,24 @@ }, "node_modules/asap": { "version": "2.0.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true }, "node_modules/async-function": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "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, - "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -1835,25 +2601,29 @@ }, "node_modules/axobject-query": { "version": "4.1.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "engines": { "node": ">= 0.4" } }, "node_modules/balanced-match": { "version": "1.0.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/before-after-hook": { "version": "2.2.3", - "dev": true, - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true }, "node_modules/binary-extensions": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -1863,8 +2633,9 @@ }, "node_modules/body-parser": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "dev": true, - "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", @@ -1882,14 +2653,17 @@ }, "node_modules/boolean": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, - "license": "MIT", "optional": true }, "node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1897,8 +2671,9 @@ }, "node_modules/braces": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -1907,7 +2682,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", "funding": [ { "type": "opencollective", @@ -1922,12 +2699,11 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -1938,32 +2714,36 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, - "license": "MIT", "engines": { "node": "*" } }, "node_modules/bytes": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/cacheable-lookup": { "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.6.0" } }, "node_modules/cacheable-request": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dev": true, - "license": "MIT", "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", @@ -1979,8 +2759,9 @@ }, "node_modules/call-bind": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -1996,8 +2777,9 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -2008,8 +2790,9 @@ }, "node_modules/call-bound": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -2023,14 +2806,17 @@ }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001700", + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", "funding": [ { "type": "opencollective", @@ -2044,13 +2830,13 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2064,8 +2850,9 @@ }, "node_modules/chalk/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2075,8 +2862,9 @@ }, "node_modules/chokidar": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2097,9 +2885,10 @@ } }, "node_modules/chromium-bidi": { - "version": "7.2.0", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.3.2.tgz", + "integrity": "sha512-80J9hiQvSxqQAGhC6vsOa5yfG8lrusrSrR7eAIViDkoK5wnXi15z5iLQYegUTc4ELDUIkq37Ob1Xx8VRwYUDRg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" @@ -2110,8 +2899,9 @@ }, "node_modules/cliui": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, - "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -2120,8 +2910,9 @@ }, "node_modules/clone-response": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dev": true, - "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" }, @@ -2131,19 +2922,22 @@ }, "node_modules/clsx": { "version": "2.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "engines": { "node": ">=6" } }, "node_modules/codemirror": { "version": "5.65.18", - "license": "MIT" + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz", + "integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==" }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -2153,26 +2947,30 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/colors": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.1.90" } }, "node_modules/concat-map": { "version": "0.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/concurrently": { "version": "6.5.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz", + "integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==", "dev": true, - "license": "MIT", "dependencies": { "chalk": "^4.1.0", "date-fns": "^2.16.1", @@ -2192,8 +2990,9 @@ }, "node_modules/content-disposition": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -2203,36 +3002,41 @@ }, "node_modules/content-type": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/convert-source-map": { "version": "2.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/cookie": { "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.6.0" } }, "node_modules/cors": { "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, - "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -2243,8 +3047,9 @@ }, "node_modules/cross-env": { "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, - "license": "MIT", "dependencies": { "cross-spawn": "^7.0.1" }, @@ -2260,8 +3065,9 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2273,12 +3079,14 @@ }, "node_modules/csstype": { "version": "3.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/data-view-buffer": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -2293,8 +3101,9 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -2309,8 +3118,9 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -2325,8 +3135,9 @@ }, "node_modules/date-fns": { "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -2340,7 +3151,8 @@ }, "node_modules/debug": { "version": "4.4.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dependencies": { "ms": "^2.1.3" }, @@ -2355,16 +3167,19 @@ }, "node_modules/debuglog": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, - "license": "MIT", "engines": { "node": "*" } }, "node_modules/decompress-response": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, - "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, @@ -2377,8 +3192,9 @@ }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -2388,28 +3204,24 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "node_modules/defer-to-connect": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/define-data-property": { "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, - "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2424,8 +3236,9 @@ }, "node_modules/define-properties": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, - "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -2440,33 +3253,38 @@ }, "node_modules/depd": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/deprecation": { "version": "2.3.1", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true }, "node_modules/detect-node": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/devtools-protocol": { - "version": "0.0.1421213", + "version": "0.0.1510116", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1510116.tgz", + "integrity": "sha512-OJyQgco4wCKi1qk//4+ymbKuxsAEH6YCNeCjebUqSIre4SjgHjE2RiO4uY4byQIqDGqJ+8ZtLbnO2ZzNC2rjnw==", "dev": true, - "license": "BSD-3-Clause", "peer": true }, "node_modules/dezalgo": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "dev": true, - "license": "ISC", "dependencies": { "asap": "^2.0.0", "wrappy": "1" @@ -2474,8 +3292,9 @@ }, "node_modules/doctrine": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -2484,9 +3303,10 @@ } }, "node_modules/dotenv": { - "version": "16.5.0", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -2496,8 +3316,9 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -2509,14 +3330,16 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true }, "node_modules/electron": { "version": "30.5.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-30.5.1.tgz", + "integrity": "sha512-AhL7+mZ8Lg14iaNfoYTkXQ2qee8mmsQyllKdqxlpv/zrKgfxz6jNVtcRRbQtLxtF8yzcImWdfTQROpYiPumdbw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^20.9.0", @@ -2530,46 +3353,53 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.102", - "license": "ISC" + "version": "1.5.214", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz", + "integrity": "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==" }, "node_modules/electron/node_modules/@types/node": { - "version": "20.17.19", + "version": "20.19.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.12.tgz", + "integrity": "sha512-lSOjyS6vdO2G2g2CWrETTV3Jz2zlCXHpu1rcubLKpz9oj+z/1CceHlj+yq53W+9zgb98nSov/wjEKYDNauD+Hw==", "dev": true, - "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/electron/node_modules/undici-types": { - "version": "6.19.8", - "dev": true, - "license": "MIT" + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/encodeurl": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/end-of-stream": { - "version": "1.4.4", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, - "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/entities": { "version": "4.5.0", - "license": "BSD-2-Clause", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "peer": true, "engines": { "node": ">=0.12" @@ -2580,16 +3410,18 @@ }, "node_modules/env-paths": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/es-abstract": { "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, - "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", @@ -2655,24 +3487,27 @@ }, "node_modules/es-define-property": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "license": "MIT", "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, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-iterator-helpers": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -2697,8 +3532,9 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -2708,8 +3544,9 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -2722,8 +3559,9 @@ }, "node_modules/es-shim-unscopables": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, - "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -2733,8 +3571,9 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, - "license": "MIT", "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", @@ -2749,14 +3588,16 @@ }, "node_modules/es6-error": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/esbuild": { - "version": "0.25.0", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -2764,49 +3605,53 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { "version": "3.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "engines": { "node": ">=6" } }, "node_modules/escape-html": { "version": "1.0.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -2816,8 +3661,9 @@ }, "node_modules/eslint": { "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -2875,8 +3721,9 @@ }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -2885,16 +3732,18 @@ }, "node_modules/eslint-import-resolver-node/node_modules/debug": { "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-module-utils": { "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -2909,16 +3758,18 @@ }, "node_modules/eslint-module-utils/node_modules/debug": { "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import": { "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, - "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -2949,16 +3800,27 @@ }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-notice": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-notice/-/eslint-plugin-notice-1.0.0.tgz", + "integrity": "sha512-M3VLQMZzZpvfTZ/vy9FmClIKq5rLBbQpM0KgfLZPJPrVXpmJYeobmmb+lfJzHWdNm8PWwvw8KlafQWo2N9xx1Q==", "dev": true, - "license": "MIT", "dependencies": { "find-root": "^1.1.0", "lodash": "^4.17.21", @@ -2970,8 +3832,9 @@ }, "node_modules/eslint-plugin-react": { "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, - "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -3001,8 +3864,9 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -3012,8 +3876,9 @@ }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, - "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -3026,10 +3891,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -3043,8 +3918,9 @@ }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3054,8 +3930,9 @@ }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -3065,12 +3942,14 @@ }, "node_modules/esm-env": { "version": "1.2.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" }, "node_modules/espree": { "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", @@ -3085,8 +3964,9 @@ }, "node_modules/esquery": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -3096,15 +3976,17 @@ }, "node_modules/esrap": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", + "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -3114,32 +3996,42 @@ }, "node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "peer": true + }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/etag": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/eventsource": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "dev": true, - "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" }, @@ -3148,17 +4040,19 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.3", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "dev": true, - "license": "MIT", "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, "node_modules/express": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, - "license": "MIT", "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -3198,8 +4092,9 @@ }, "node_modules/express-rate-limit": { "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 16" }, @@ -3212,8 +4107,9 @@ }, "node_modules/extract-zip": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -3231,13 +4127,15 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-glob": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -3251,33 +4149,41 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "node_modules/fastq": { "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, - "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/fd-slicer": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, - "license": "MIT", "dependencies": { "pend": "~1.2.0" } }, "node_modules/fdir": { - "version": "6.4.6", - "license": "MIT", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -3289,8 +4195,9 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, - "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, @@ -3300,8 +4207,9 @@ }, "node_modules/fill-range": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3311,8 +4219,9 @@ }, "node_modules/finalhandler": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -3327,13 +4236,15 @@ }, "node_modules/find-root": { "version": "1.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true }, "node_modules/find-up": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3347,8 +4258,9 @@ }, "node_modules/flat-cache": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, - "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -3359,13 +4271,15 @@ }, "node_modules/flatted": { "version": "3.3.3", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true }, "node_modules/for-each": { "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, - "license": "MIT", "dependencies": { "is-callable": "^1.2.7" }, @@ -3377,12 +4291,13 @@ } }, "node_modules/formidable": { - "version": "2.1.2", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", "dev": true, - "license": "MIT", "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", "once": "^1.4.0", "qs": "^6.11.0" }, @@ -3392,24 +4307,27 @@ }, "node_modules/forwarded": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/fresh": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/fs-extra": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", @@ -3421,15 +4339,15 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -3440,16 +4358,18 @@ }, "node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/function.prototype.name": { "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -3467,31 +4387,35 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "engines": { "node": ">=6.9.0" } }, "node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-intrinsic": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -3513,8 +4437,9 @@ }, "node_modules/get-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, - "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -3525,8 +4450,9 @@ }, "node_modules/get-stream": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, - "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -3539,8 +4465,9 @@ }, "node_modules/get-symbol-description": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -3555,8 +4482,10 @@ }, "node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3574,8 +4503,9 @@ }, "node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -3585,8 +4515,9 @@ }, "node_modules/global-agent": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "dev": true, - "license": "BSD-3-Clause", "optional": true, "dependencies": { "boolean": "^3.0.1", @@ -3600,29 +4531,23 @@ "node": ">=10.0" } }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.7.1", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/globals": { - "version": "11.12.0", - "license": "MIT", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, - "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -3636,8 +4561,9 @@ }, "node_modules/gopd": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3647,8 +4573,9 @@ }, "node_modules/got": { "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "dev": true, - "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -3671,26 +4598,30 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "node_modules/graphemer": { "version": "1.4.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "node_modules/graphql": { - "version": "16.10.0", + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", "dev": true, - "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, "node_modules/graphql-tag": { "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", "dev": true, - "license": "MIT", "dependencies": { "tslib": "^2.1.0" }, @@ -3701,15 +4632,11 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/graphql-tag/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "license": "0BSD" - }, "node_modules/has-bigints": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3719,16 +4646,18 @@ }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/has-property-descriptors": { "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, - "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -3738,8 +4667,9 @@ }, "node_modules/has-proto": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, - "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" }, @@ -3752,8 +4682,9 @@ }, "node_modules/has-symbols": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3763,8 +4694,9 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, - "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -3777,8 +4709,9 @@ }, "node_modules/hasown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, - "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -3786,32 +4719,27 @@ "node": ">= 0.4" } }, - "node_modules/hexoid": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/hosted-git-info": { "version": "2.8.9", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true }, "node_modules/html-reporter": { "resolved": "packages/html-reporter", "link": true }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "dev": true, - "license": "BSD-2-Clause" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true }, "node_modules/http-errors": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, - "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -3825,16 +4753,18 @@ }, "node_modules/http-errors/node_modules/statuses": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/http2-wrapper": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "dev": true, - "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" @@ -3845,8 +4775,9 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, - "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -3856,21 +4787,24 @@ }, "node_modules/ignore": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/immutable": { "version": "4.3.7", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true }, "node_modules/import-fresh": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, - "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3884,16 +4818,19 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.8.19" } }, "node_modules/inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3901,13 +4838,15 @@ }, "node_modules/inherits": { "version": "2.0.4", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/internal-slot": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", @@ -3919,16 +4858,18 @@ }, "node_modules/ipaddr.js": { "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/is-array-buffer": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -3943,8 +4884,9 @@ }, "node_modules/is-async-function": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, - "license": "MIT", "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", @@ -3961,8 +4903,9 @@ }, "node_modules/is-bigint": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, - "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" }, @@ -3975,8 +4918,9 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, - "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -3986,8 +4930,9 @@ }, "node_modules/is-boolean-object": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -4001,8 +4946,9 @@ }, "node_modules/is-callable": { "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4012,8 +4958,9 @@ }, "node_modules/is-core-module": { "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, - "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -4026,8 +4973,9 @@ }, "node_modules/is-data-view": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", @@ -4042,8 +4990,9 @@ }, "node_modules/is-date-object": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -4057,16 +5006,18 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-finalizationregistry": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -4079,16 +5030,18 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-generator-function": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", @@ -4104,8 +5057,9 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -4115,8 +5069,9 @@ }, "node_modules/is-map": { "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, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4126,8 +5081,9 @@ }, "node_modules/is-negative-zero": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4137,16 +5093,18 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-number-object": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -4160,20 +5118,23 @@ }, "node_modules/is-promise": { "version": "4.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true }, "node_modules/is-reference": { "version": "3.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "dependencies": { "@types/estree": "^1.0.6" } }, "node_modules/is-regex": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -4189,8 +5150,9 @@ }, "node_modules/is-set": { "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, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4200,8 +5162,9 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -4214,8 +5177,9 @@ }, "node_modules/is-string": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -4229,8 +5193,9 @@ }, "node_modules/is-symbol": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", @@ -4245,8 +5210,9 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, - "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" }, @@ -4259,8 +5225,9 @@ }, "node_modules/is-weakmap": { "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, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4270,8 +5237,9 @@ }, "node_modules/is-weakref": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -4284,8 +5252,9 @@ }, "node_modules/is-weakset": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" @@ -4299,18 +5268,21 @@ }, "node_modules/isarray": { "version": "2.0.5", - "dev": true, - "license": "MIT" + "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", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/iterator.prototype": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, - "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", @@ -4325,12 +5297,14 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -4340,7 +5314,8 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "bin": { "jsesc": "bin/jsesc" }, @@ -4350,33 +5325,39 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true }, "node_modules/json-stringify-safe": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true, - "license": "ISC", "optional": true }, "node_modules/json5": { "version": "2.2.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "bin": { "json5": "lib/cli.js" }, @@ -4386,16 +5367,18 @@ }, "node_modules/jsonfile": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, - "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/jsx-ast-utils": { "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, - "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -4408,23 +5391,18 @@ }, "node_modules/keyv": { "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, - "node_modules/kleur": { - "version": "4.1.5", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4435,8 +5413,9 @@ }, "node_modules/license-checker": { "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "chalk": "^2.4.1", "debug": "^3.1.0", @@ -4455,8 +5434,9 @@ }, "node_modules/license-checker/node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -4466,8 +5446,9 @@ }, "node_modules/license-checker/node_modules/chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -4479,53 +5460,60 @@ }, "node_modules/license-checker/node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "1.1.3" } }, "node_modules/license-checker/node_modules/color-name": { "version": "1.1.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/license-checker/node_modules/debug": { "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/license-checker/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/license-checker/node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/license-checker/node_modules/semver": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver" } }, "node_modules/license-checker/node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -4535,12 +5523,14 @@ }, "node_modules/locate-character": { "version": "3.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, "node_modules/locate-path": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -4553,18 +5543,21 @@ }, "node_modules/lodash": { "version": "4.17.21", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.merge": { "version": "4.6.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, "node_modules/loose-envify": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, - "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -4574,30 +5567,34 @@ }, "node_modules/lowercase-keys": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/lru-cache": { "version": "5.1.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dependencies": { "yallist": "^3.0.2" } }, "node_modules/magic-string": { - "version": "0.30.17", - "license": "MIT", + "version": "0.30.18", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", + "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/matcher": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "escape-string-regexp": "^4.0.0" @@ -4608,24 +5605,27 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/media-typer": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/merge-descriptors": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -4635,21 +5635,24 @@ }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/metric-lcs": { "version": "0.1.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/metric-lcs/-/metric-lcs-0.1.2.tgz", + "integrity": "sha512-+TZ5dUDPKPJaU/rscTzxyN8ZkX7eAVLAiQU/e+YINleXPv03SCmJShaMT1If1liTH8OcmWXZs0CmzCBRBLcMpA==", + "dev": true }, "node_modules/micromatch": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -4660,8 +5663,9 @@ }, "node_modules/micromatch/node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -4671,8 +5675,9 @@ }, "node_modules/mime": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true, - "license": "MIT", "bin": { "mime": "cli.js" }, @@ -4682,16 +5687,18 @@ }, "node_modules/mime-db": { "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dev": true, - "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, @@ -4701,16 +5708,18 @@ }, "node_modules/mimic-response": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4720,21 +5729,24 @@ }, "node_modules/minimist": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/mitt": { "version": "3.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true }, "node_modules/mkdirp": { "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, - "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -4744,17 +5756,19 @@ }, "node_modules/ms": { "version": "2.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -4764,25 +5778,29 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "node_modules/negotiator": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/node-releases": { "version": "2.0.19", - "license": "MIT" + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" }, "node_modules/node-stream-zip": { "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.12.0" }, @@ -4793,8 +5811,9 @@ }, "node_modules/nopt": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", "dev": true, - "license": "ISC", "dependencies": { "abbrev": "1", "osenv": "^0.1.4" @@ -4805,8 +5824,9 @@ }, "node_modules/normalize-package-data": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -4816,24 +5836,27 @@ }, "node_modules/normalize-package-data/node_modules/semver": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver" } }, "node_modules/normalize-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/normalize-url": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -4843,21 +5866,24 @@ }, "node_modules/npm-normalize-package-bin": { "version": "1.0.1", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true }, "node_modules/object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object-inspect": { "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4867,16 +5893,18 @@ }, "node_modules/object-keys": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/object.assign": { "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4894,8 +5922,9 @@ }, "node_modules/object.entries": { "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -4908,8 +5937,9 @@ }, "node_modules/object.fromentries": { "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4925,8 +5955,9 @@ }, "node_modules/object.groupby": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4938,8 +5969,9 @@ }, "node_modules/object.values": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4955,8 +5987,9 @@ }, "node_modules/on-finished": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, - "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -4966,16 +5999,18 @@ }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, - "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/optionator": { "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, - "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -4990,24 +6025,28 @@ }, "node_modules/os-homedir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/os-tmpdir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/osenv": { "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", "dev": true, - "license": "ISC", "dependencies": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -5015,8 +6054,9 @@ }, "node_modules/own-keys": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, - "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", @@ -5031,16 +6071,18 @@ }, "node_modules/p-cancelable": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -5053,8 +6095,9 @@ }, "node_modules/p-locate": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -5067,8 +6110,9 @@ }, "node_modules/p-map": { "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -5078,8 +6122,9 @@ }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -5089,61 +6134,71 @@ }, "node_modules/parseurl": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-to-regexp": { - "version": "8.2.0", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/pend": { "version": "1.2.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true }, "node_modules/picocolors": { "version": "1.1.1", - "license": "ISC" + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "4.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "engines": { "node": ">=12" }, @@ -5153,8 +6208,9 @@ }, "node_modules/pkce-challenge": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=16.20.0" } @@ -5181,14 +6237,17 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/postcss": { "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -5203,7 +6262,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5215,24 +6273,27 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/progress": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/prop-types": { "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, - "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -5241,8 +6302,9 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, - "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -5252,9 +6314,10 @@ } }, "node_modules/pump": { - "version": "3.0.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "dev": true, - "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -5262,16 +6325,18 @@ }, "node_modules/punycode": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/qs": { "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" }, @@ -5284,6 +6349,8 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -5298,13 +6365,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/quick-lru": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -5314,30 +6381,49 @@ }, "node_modules/range-parser": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { - "version": "3.0.0", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", "dev": true, - "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.0", "unpipe": "1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/react": { "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, - "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -5347,8 +6433,9 @@ }, "node_modules/react-dom": { "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, - "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5359,20 +6446,24 @@ }, "node_modules/react-is": { "version": "16.13.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true }, "node_modules/react-refresh": { - "version": "0.14.2", - "license": "MIT", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "engines": { "node": ">=0.10.0" } }, "node_modules/read-installed": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", + "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", + "deprecated": "This package is no longer supported.", "dev": true, - "license": "ISC", "dependencies": { "debuglog": "^1.0.1", "read-package-json": "^2.0.0", @@ -5387,16 +6478,19 @@ }, "node_modules/read-installed/node_modules/semver": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver" } }, "node_modules/read-package-json": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", "dev": true, - "license": "ISC", "dependencies": { "glob": "^7.1.1", "json-parse-even-better-errors": "^2.3.0", @@ -5406,8 +6500,10 @@ }, "node_modules/readdir-scoped-modules": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "deprecated": "This functionality has been moved to @npmcli/fs", "dev": true, - "license": "ISC", "dependencies": { "debuglog": "^1.0.1", "dezalgo": "^1.0.0", @@ -5417,8 +6513,9 @@ }, "node_modules/readdirp": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, - "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -5428,8 +6525,9 @@ }, "node_modules/readdirp/node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -5443,8 +6541,9 @@ }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -5462,15 +6561,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "dev": true, - "license": "MIT" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -5488,16 +6583,18 @@ }, "node_modules/require-directory": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, - "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", @@ -5515,21 +6612,24 @@ }, "node_modules/resolve-alpn": { "version": "1.2.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true }, "node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/responselike": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "dev": true, - "license": "MIT", "dependencies": { "lowercase-keys": "^2.0.0" }, @@ -5539,8 +6639,9 @@ }, "node_modules/reusify": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, - "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -5548,8 +6649,9 @@ }, "node_modules/roarr": { "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", "dev": true, - "license": "BSD-3-Clause", "optional": true, "dependencies": { "boolean": "^3.0.1", @@ -5564,10 +6666,11 @@ } }, "node_modules/rollup": { - "version": "4.40.1", - "license": "MIT", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz", + "integrity": "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -5577,33 +6680,35 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.1", - "@rollup/rollup-android-arm64": "4.40.1", - "@rollup/rollup-darwin-arm64": "4.40.1", - "@rollup/rollup-darwin-x64": "4.40.1", - "@rollup/rollup-freebsd-arm64": "4.40.1", - "@rollup/rollup-freebsd-x64": "4.40.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", - "@rollup/rollup-linux-arm64-gnu": "4.40.1", - "@rollup/rollup-linux-arm64-musl": "4.40.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-musl": "4.40.1", - "@rollup/rollup-linux-s390x-gnu": "4.40.1", - "@rollup/rollup-linux-x64-gnu": "4.40.1", - "@rollup/rollup-linux-x64-musl": "4.40.1", - "@rollup/rollup-win32-arm64-msvc": "4.40.1", - "@rollup/rollup-win32-ia32-msvc": "4.40.1", - "@rollup/rollup-win32-x64-msvc": "4.40.1", + "@rollup/rollup-android-arm-eabi": "4.50.0", + "@rollup/rollup-android-arm64": "4.50.0", + "@rollup/rollup-darwin-arm64": "4.50.0", + "@rollup/rollup-darwin-x64": "4.50.0", + "@rollup/rollup-freebsd-arm64": "4.50.0", + "@rollup/rollup-freebsd-x64": "4.50.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", + "@rollup/rollup-linux-arm-musleabihf": "4.50.0", + "@rollup/rollup-linux-arm64-gnu": "4.50.0", + "@rollup/rollup-linux-arm64-musl": "4.50.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", + "@rollup/rollup-linux-ppc64-gnu": "4.50.0", + "@rollup/rollup-linux-riscv64-gnu": "4.50.0", + "@rollup/rollup-linux-riscv64-musl": "4.50.0", + "@rollup/rollup-linux-s390x-gnu": "4.50.0", + "@rollup/rollup-linux-x64-gnu": "4.50.0", + "@rollup/rollup-linux-x64-musl": "4.50.0", + "@rollup/rollup-openharmony-arm64": "4.50.0", + "@rollup/rollup-win32-arm64-msvc": "4.50.0", + "@rollup/rollup-win32-ia32-msvc": "4.50.0", + "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" } }, "node_modules/router": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -5617,6 +6722,8 @@ }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -5632,15 +6739,15 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/rxjs": { "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "tslib": "^1.9.0" }, @@ -5648,10 +6755,17 @@ "npm": ">=2.0.0" } }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/safe-array-concat": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -5668,6 +6782,8 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { @@ -5682,13 +6798,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/safe-push-apply": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" @@ -5702,8 +6818,9 @@ }, "node_modules/safe-regex-test": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5718,39 +6835,49 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true }, "node_modules/sax": { "version": "1.4.1", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true }, "node_modules/scheduler": { "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dev": true, - "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } }, "node_modules/semver": { - "version": "6.3.1", - "license": "ISC", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/semver-compare": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/send": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", @@ -5770,8 +6897,9 @@ }, "node_modules/serialize-error": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "type-fest": "^0.13.1" @@ -5785,8 +6913,9 @@ }, "node_modules/serve-static": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "dev": true, - "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -5799,8 +6928,9 @@ }, "node_modules/set-function-length": { "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, - "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5815,8 +6945,9 @@ }, "node_modules/set-function-name": { "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, - "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5829,8 +6960,9 @@ }, "node_modules/set-proto": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, - "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", @@ -5842,13 +6974,15 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5858,16 +6992,18 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/side-channel": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -5884,8 +7020,9 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -5899,8 +7036,9 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5916,8 +7054,9 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5934,27 +7073,32 @@ }, "node_modules/slide": { "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", "dev": true, - "license": "ISC", "engines": { "node": "*" } }, "node_modules/source-map-js": { "version": "1.2.1", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } }, "node_modules/spawn-command": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, "node_modules/spdx-compare": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", "dev": true, - "license": "MIT", "dependencies": { "array-find-index": "^1.0.2", "spdx-expression-parse": "^3.0.0", @@ -5963,8 +7107,9 @@ }, "node_modules/spdx-correct": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -5972,32 +7117,37 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", - "dev": true, - "license": "CC-BY-3.0" + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true }, "node_modules/spdx-expression-parse": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, - "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-license-ids": { - "version": "3.0.21", - "dev": true, - "license": "CC0-1.0" + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true }, "node_modules/spdx-ranges": { "version": "2.1.1", - "dev": true, - "license": "(MIT AND CC-BY-3.0)" + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "dev": true }, "node_modules/spdx-satisfies": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", + "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", "dev": true, - "license": "MIT", "dependencies": { "spdx-compare": "^1.0.0", "spdx-expression-parse": "^3.0.0", @@ -6006,27 +7156,31 @@ }, "node_modules/sprintf-js": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true, - "license": "BSD-3-Clause", "optional": true }, "node_modules/ssim.js": { "version": "3.5.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ssim.js/-/ssim.js-3.5.0.tgz", + "integrity": "sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==", + "dev": true }, "node_modules/statuses": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" @@ -6037,8 +7191,9 @@ }, "node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -6050,8 +7205,9 @@ }, "node_modules/string.prototype.matchall": { "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -6076,8 +7232,9 @@ }, "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, - "license": "MIT", "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -6085,8 +7242,9 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -6105,8 +7263,9 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -6122,8 +7281,9 @@ }, "node_modules/string.prototype.trimstart": { "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, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -6138,8 +7298,9 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6149,16 +7310,18 @@ }, "node_modules/strip-bom": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/strip-json-comments": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -6168,8 +7331,9 @@ }, "node_modules/sumchecker": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" }, @@ -6179,8 +7343,9 @@ }, "node_modules/supports-color": { "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -6193,8 +7358,9 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -6203,10 +7369,11 @@ } }, "node_modules/svelte": { - "version": "5.37.3", - "license": "MIT", + "version": "5.38.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.6.tgz", + "integrity": "sha512-ltBPlkvqk3bgCK7/N323atUpP3O3Y+DrGV4dcULrsSn4fZaaNnOmdplNznwfdWclAgvSr5rxjtzn/zJhRm6TKg==", "dependencies": { - "@ampproject/remapping": "^2.3.0", + "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", @@ -6227,7 +7394,8 @@ }, "node_modules/tinyglobby": { "version": "0.2.14", - "license": "MIT", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" @@ -6241,8 +7409,9 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -6252,8 +7421,9 @@ }, "node_modules/toidentifier": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.6" } @@ -6264,24 +7434,27 @@ }, "node_modules/tree-kill": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, - "license": "MIT", "bin": { "tree-kill": "cli.js" } }, "node_modules/treeify": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.6" } }, "node_modules/ts-api-utils": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18.12" }, @@ -6291,8 +7464,9 @@ }, "node_modules/tsconfig-paths": { "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, - "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -6302,8 +7476,9 @@ }, "node_modules/tsconfig-paths/node_modules/json5": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, - "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -6312,22 +7487,25 @@ } }, "node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true }, "node_modules/tunnel": { "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, "node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -6337,8 +7515,9 @@ }, "node_modules/type-fest": { "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true, - "license": "(MIT OR CC0-1.0)", "optional": true, "engines": { "node": ">=10" @@ -6349,8 +7528,9 @@ }, "node_modules/type-is": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dev": true, - "license": "MIT", "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -6362,8 +7542,9 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -6375,8 +7556,9 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", @@ -6393,8 +7575,9 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, - "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -6413,8 +7596,9 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", @@ -6432,8 +7616,9 @@ }, "node_modules/typescript": { "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6444,8 +7629,9 @@ }, "node_modules/unbox-primitive": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", @@ -6461,8 +7647,9 @@ }, "node_modules/undici": { "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", "dev": true, - "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -6472,32 +7659,38 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true }, "node_modules/universal-user-agent": { "version": "6.0.1", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true }, "node_modules/universalify": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4.0.0" } }, "node_modules/unpipe": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "funding": [ { "type": "opencollective", @@ -6512,7 +7705,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -6526,21 +7718,24 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/util-extend": { "version": "1.0.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", + "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", + "dev": true }, "node_modules/validate-npm-package-license": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, - "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -6548,15 +7743,17 @@ }, "node_modules/vary": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/vite": { "version": "6.3.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -6628,8 +7825,9 @@ }, "node_modules/vite-plugin-static-copy": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.2.tgz", + "integrity": "sha512-aVmYOzptLVOI2b1jL+cmkF7O6uhRv1u5fvOkQgbohWZp2CbR22kn9ZqkCUIt9umKF7UhdbsEpshn1rf4720QFg==", "dev": true, - "license": "MIT", "dependencies": { "chokidar": "^3.6.0", "fs-extra": "^11.3.0", @@ -6646,8 +7844,9 @@ }, "node_modules/vite-plugin-static-copy/node_modules/fs-extra": { "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -6659,8 +7858,9 @@ }, "node_modules/vite-plugin-static-copy/node_modules/jsonfile": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, - "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -6670,53 +7870,24 @@ }, "node_modules/vite-plugin-static-copy/node_modules/universalify": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 10.0.0" } }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/vitefu": { - "version": "1.1.1", - "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, "node_modules/vue": { - "version": "3.5.13", - "license": "MIT", + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz", + "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-sfc": "3.5.13", - "@vue/runtime-dom": "3.5.13", - "@vue/server-renderer": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.21", + "@vue/compiler-sfc": "3.5.21", + "@vue/runtime-dom": "3.5.21", + "@vue/server-renderer": "3.5.21", + "@vue/shared": "3.5.21" }, "peerDependencies": { "typescript": "*" @@ -6733,8 +7904,9 @@ }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -6747,8 +7919,9 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, - "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", @@ -6765,8 +7938,9 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, - "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", @@ -6791,8 +7965,9 @@ }, "node_modules/which-collection": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, - "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -6808,8 +7983,9 @@ }, "node_modules/which-typed-array": { "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, - "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -6828,203 +8004,575 @@ }, "node_modules/word-wrap": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead." + }, + "node_modules/xterm-addon-fit": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz", + "integrity": "sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", + "peerDependencies": { + "xterm": "^5.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "dev": true, + "peerDependencies": { + "zod": "^3.24.1" + } + }, + "packages/html-reporter": { + "version": "0.0.0" + }, + "packages/mcp-extension": { + "name": "@playwright/mcp-extension", + "version": "0.0.36", + "license": "Apache-2.0", + "devDependencies": { + "@types/chrome": "^0.0.315", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.8.2", + "vite": "^5.0.0", + "vite-plugin-static-copy": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "packages/mcp-extension/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "packages/mcp-extension/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/mcp-extension/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/mcp-extension/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/mcp-extension/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "packages/mcp-extension/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "packages/mcp-extension/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/mcp-extension/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/mcp-extension/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.2", + "packages/mcp-extension/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "node": ">=12" } }, - "node_modules/xml2js": { - "version": "0.5.0", + "packages/mcp-extension/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4.0.0" + "node": ">=12" } }, - "node_modules/xmlbuilder": { - "version": "11.0.1", + "packages/mcp-extension/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4.0" + "node": ">=12" } }, - "node_modules/xterm": { - "version": "5.3.0", - "license": "MIT" - }, - "node_modules/xterm-addon-fit": { - "version": "0.7.0", - "license": "MIT", - "peerDependencies": { - "xterm": "^5.0.0" + "packages/mcp-extension/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/y18n": { - "version": "5.0.8", + "packages/mcp-extension/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "ISC", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/yallist": { - "version": "3.1.1", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.6.0", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, + "packages/mcp-extension/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 14" + "node": ">=12" } }, - "node_modules/yargs": { - "version": "16.2.0", + "packages/mcp-extension/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/yargs-parser": { - "version": "20.2.9", + "packages/mcp-extension/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/yauzl": { - "version": "2.10.0", + "packages/mcp-extension/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", + "packages/mcp-extension/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/zimmerframe": { - "version": "1.1.2", - "license": "MIT" - }, - "node_modules/zod": { - "version": "3.25.76", + "packages/mcp-extension/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" } }, - "node_modules/zod-to-json-schema": { - "version": "3.24.6", + "packages/mcp-extension/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "packages/html-reporter": { - "version": "0.0.0" - }, - "packages/mcp-extension": { - "name": "@playwright/mcp-extension", - "version": "0.0.36", - "license": "Apache-2.0", - "devDependencies": { - "@types/chrome": "^0.0.315", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@vitejs/plugin-react": "^4.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^5.8.2", - "vite": "^5.0.0", - "vite-plugin-static-copy": "^3.1.1" - }, + "packages/mcp-extension/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "packages/mcp-extension/node_modules/@esbuild/linux-x64": { + "packages/mcp-extension/node_modules/@esbuild/win32-x64": { "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { "node": ">=12" @@ -7067,21 +8615,6 @@ "@esbuild/win32-x64": "0.21.5" } }, - "packages/mcp-extension/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "packages/mcp-extension/node_modules/vite": { "version": "5.4.19", "dev": true, @@ -7379,6 +8912,17 @@ "node": ">=18" } }, + "packages/playwright/node_modules/fsevents": { + "version": "2.3.2", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "packages/recorder": { "version": "0.0.0", "dependencies": { diff --git a/package.json b/package.json index 80dd33e8119ac..bdc5f6149cc80 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@types/codemirror": "^5.60.7", "@types/formidable": "^2.0.4", "@types/immutable": "^3.8.7", - "@types/node": "^18.19.68", + "@types/node": "18.19.76", "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", "@types/ws": "^8.5.3", diff --git a/packages/injected/src/reactSelectorEngine.ts b/packages/injected/src/reactSelectorEngine.ts index a9185057c7cdc..eb27da6947b7e 100644 --- a/packages/injected/src/reactSelectorEngine.ts +++ b/packages/injected/src/reactSelectorEngine.ts @@ -108,11 +108,7 @@ function getChildren(reactElement: ReactVNode): ReactVNode[] { } function getProps(reactElement: ReactVNode) { - const props = - // React 16+ - reactElement.memoizedProps || - // React 15 - reactElement._currentElement?.props; + const props = /* React 16+ */ reactElement.memoizedProps || /* React 15 */ reactElement._currentElement?.props; if (!props || typeof props === 'string') return props; const result = { ...props }; @@ -130,12 +126,8 @@ function buildComponentsTree(reactElement: ReactVNode): ComponentNode { props: getProps(reactElement), }; - const rootElement = - // React 16+ - // @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L29 - reactElement.stateNode || - // React 15 - reactElement._hostNode || reactElement._renderedComponent?._hostNode; + // @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L29 + const rootElement = /* React 16+ */ reactElement.stateNode || /* React 15 */ reactElement._hostNode || /* React 15 */ reactElement._renderedComponent?._hostNode; if (rootElement instanceof Element) { treeNode.rootElements.push(rootElement); } else { diff --git a/packages/playwright/src/transform/transform.ts b/packages/playwright/src/transform/transform.ts index 566e0ff119be1..18a99b2b24753 100644 --- a/packages/playwright/src/transform/transform.ts +++ b/packages/playwright/src/transform/transform.ts @@ -221,9 +221,9 @@ export function setTransformData(pluginName: string, value: any) { export function transformHook(originalCode: string, filename: string, moduleUrl?: string): { code: string, serializedCache?: any } { const hasPreprocessor = - process.env.PW_TEST_SOURCE_TRANSFORM && - process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE && - process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE.split(pathSeparator).some(f => filename.startsWith(f)); + process.env.PW_TEST_SOURCE_TRANSFORM && + process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE && + process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE.split(pathSeparator).some(f => filename.startsWith(f)); const pluginsPrologue = _transformConfig.babelPlugins; const pluginsEpilogue = hasPreprocessor ? [[process.env.PW_TEST_SOURCE_TRANSFORM!]] as BabelPlugin[] : []; const hash = calculateHash(originalCode, filename, !!moduleUrl, pluginsPrologue, pluginsEpilogue); From b5322349e47649a279ea6e52efb93e92d8915163 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 4 Sep 2025 05:35:13 -0700 Subject: [PATCH 074/329] feat(ui-mode): support macOS alt key to expand/collapse sections of tree (#37284) --- packages/web/src/components/treeView.tsx | 23 ++++++- .../playwright-test/ui-mode-test-tree.spec.ts | 60 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx index f478ba402512a..d4f955fb9cfbd 100644 --- a/packages/web/src/components/treeView.tsx +++ b/packages/web/src/components/treeView.tsx @@ -112,6 +112,20 @@ export function TreeView({ setTreeState({ ...treeState }); }, [treeItems, selectedItem, onSelected, treeState, setTreeState]); + const toggleSubtree = React.useCallback((item: T) => { + const { expanded } = treeItems.get(item)!; + + const stack: TreeItem[] = [item]; + while (stack.length) { + const current = stack.pop()!; + stack.push(...current.children); + + treeState.expandedItems.set(current.id, !expanded); + } + + setTreeState({ ...treeState }); + }, [treeItems, treeState, setTreeState]); + return
({ onAccepted={onAccepted} isError={isError} toggleExpanded={toggleExpanded} + toggleSubtree={toggleSubtree} highlightedItem={highlightedItem} setHighlightedItem={setHighlightedItem} render={render} @@ -205,6 +220,7 @@ type TreeItemHeaderProps = { selectedItem: T | undefined, onSelected?: (item: T) => void, toggleExpanded: (item: T) => void, + toggleSubtree: (item: T) => void, highlightedItem: T | undefined, isError?: (item: T) => boolean, onAccepted?: (item: T) => void, @@ -226,6 +242,7 @@ export function TreeItemHeader({ isError, onAccepted, toggleExpanded, + toggleSubtree, render, title, icon, @@ -276,7 +293,10 @@ export function TreeItemHeader({ onClick={e => { e.stopPropagation(); e.preventDefault(); - toggleExpanded(item); + if (e.altKey) + toggleSubtree(item); + else + toggleExpanded(item); }} /> {icon &&
} @@ -294,6 +314,7 @@ export function TreeItemHeader({ onAccepted={onAccepted} isError={isError} toggleExpanded={toggleExpanded} + toggleSubtree={toggleSubtree} highlightedItem={highlightedItem} setHighlightedItem={setHighlightedItem} render={render} diff --git a/tests/playwright-test/ui-mode-test-tree.spec.ts b/tests/playwright-test/ui-mode-test-tree.spec.ts index e663b986b6a28..36942507b62cc 100644 --- a/tests/playwright-test/ui-mode-test-tree.spec.ts +++ b/tests/playwright-test/ui-mode-test-tree.spec.ts @@ -480,6 +480,66 @@ test('should expand all', { `); }); +test('should allow expanding entire subtrees', async ({ runUITest }) => { + const { page } = await runUITest(basicTestTree); + + await page.getByTestId('test-tree').getByText('suite').click(); + await page.getByTitle('Collapse all').click(); + await expect.poll(dumpTestTree(page)).toContain(` + ► ◯ a.test.ts + ► ◯ b.test.ts + `); + + const firstTestClosedLocator = page.getByTitle('a.test.ts').locator('.codicon-chevron-right').first(); + const firstTestOpenLocator = page.getByTitle('a.test.ts').locator('.codicon-chevron-down').first(); + + await firstTestClosedLocator.click(); + await expect.poll(dumpTestTree(page)).toContain(` + ▼ ◯ a.test.ts + ◯ passes + ◯ fails + ► ◯ suite + ► ◯ b.test.ts + `); + + await firstTestOpenLocator.click(); + await firstTestClosedLocator.click({ modifiers: ['Alt'] }); + + await expect.poll(dumpTestTree(page)).toContain(` + ▼ ◯ a.test.ts + ◯ passes + ◯ fails + ▼ ◯ suite + ◯ inner passes + ◯ inner fails + ► ◯ b.test.ts + `); + + await firstTestOpenLocator.click(); + await firstTestClosedLocator.click(); + + await expect.poll(dumpTestTree(page)).toContain(` + ▼ ◯ a.test.ts + ◯ passes + ◯ fails + ▼ ◯ suite + ◯ inner passes + ◯ inner fails + ► ◯ b.test.ts + `); + + await firstTestOpenLocator.click({ modifiers: ['Alt'] }); + await firstTestClosedLocator.click(); + + await expect.poll(dumpTestTree(page)).toContain(` + ▼ ◯ a.test.ts + ◯ passes + ◯ fails + ► ◯ suite + ► ◯ b.test.ts + `); +}); + test('should resolve title conflicts', async ({ runUITest }) => { const { page } = await runUITest({ 'a.test.ts': ` From 05ae1d456596cdc30e9d047d839d66d68de55a9f Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 4 Sep 2025 13:59:28 +0100 Subject: [PATCH 075/329] chore: update package-lock.json to fix component tests (#37298) --- package-lock.json | 118 ++++++++++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0792b3c2711f8..f4f605b1c59a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1659,6 +1659,42 @@ "acorn": "^8.9.0" } }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -3208,6 +3244,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -3358,9 +3402,9 @@ "integrity": "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==" }, "node_modules/electron/node_modules/@types/node": { - "version": "20.19.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.12.tgz", - "integrity": "sha512-lSOjyS6vdO2G2g2CWrETTV3Jz2zlCXHpu1rcubLKpz9oj+z/1CceHlj+yq53W+9zgb98nSov/wjEKYDNauD+Hw==", + "version": "20.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz", + "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", "dev": true, "dependencies": { "undici-types": "~6.21.0" @@ -5398,6 +5442,14 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7877,6 +7929,19 @@ "node": ">= 10.0.0" } }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vue": { "version": "3.5.21", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz", @@ -8277,11 +8342,12 @@ }, "packages/mcp-extension/node_modules/@esbuild/darwin-arm64": { "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -8580,9 +8646,10 @@ }, "packages/mcp-extension/node_modules/esbuild": { "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -8617,8 +8684,9 @@ }, "packages/mcp-extension/node_modules/vite": { "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -8821,40 +8889,6 @@ "node": ">=18" } }, - "packages/playwright-ct-svelte/node_modules/@sveltejs/vite-plugin-svelte": { - "version": "5.1.1", - "license": "MIT", - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", - "debug": "^4.4.1", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.17", - "vitefu": "^1.0.6" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "svelte": "^5.0.0", - "vite": "^6.0.0" - } - }, - "packages/playwright-ct-svelte/node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "debug": "^4.3.7" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "svelte": "^5.0.0", - "vite": "^6.0.0" - } - }, "packages/playwright-ct-vue": { "name": "@playwright/experimental-ct-vue", "version": "1.56.0-next", @@ -8914,7 +8948,9 @@ }, "packages/playwright/node_modules/fsevents": { "version": "2.3.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, "optional": true, "os": [ "darwin" From 18afd41ece9767f4eaf45d4b2ce024f4717b3d6d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 4 Sep 2025 14:00:07 +0100 Subject: [PATCH 076/329] chore(expect): explicit errorMessage (#37253) --- packages/playwright-core/src/client/frame.ts | 2 +- .../playwright-core/src/client/locator.ts | 2 +- .../playwright-core/src/protocol/validator.ts | 1 + packages/playwright-core/src/server/frames.ts | 21 ++++++++----- .../playwright/src/matchers/matcherHint.ts | 2 -- packages/playwright/src/matchers/matchers.ts | 4 +-- .../playwright/src/matchers/toBeTruthy.ts | 15 ++++------ packages/playwright/src/matchers/toEqual.ts | 13 ++++---- .../src/matchers/toMatchAriaSnapshot.ts | 16 +++++----- .../playwright/src/matchers/toMatchText.ts | 30 +++++++------------ packages/protocol/src/channels.d.ts | 1 + packages/protocol/src/protocol.yml | 1 + tests/page/expect-boolean.spec.ts | 4 +-- tests/page/expect-matcher-result.spec.ts | 3 +- tests/page/expect-misc.spec.ts | 14 +++++++++ tests/page/expect-timeout.spec.ts | 4 +-- tests/page/expect-to-have-text.spec.ts | 2 +- tests/page/matchers.misc.spec.ts | 2 +- tests/playwright-test/expect.spec.ts | 2 +- .../update-aria-snapshot.spec.ts | 2 +- 20 files changed, 73 insertions(+), 68 deletions(-) diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index 7aaa8353f1165..9470ebc7129d1 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -466,7 +466,7 @@ export class Frame extends ChannelOwner implements api.Fr return (await this._channel.title()).value; } - async _expect(expression: string, options: Omit): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> { + async _expect(expression: string, options: Omit): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string }> { const params: channels.FrameExpectParams = { expression, ...options, isNot: !!options.isNot }; params.expectedValue = serializeArgument(options.expectedValue); const result = (await this._channel.expect(params)); diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index f7afdbb5fe550..fd889c75f36f2 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -377,7 +377,7 @@ export class Locator implements api.Locator { await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options, timeout: this._frame._timeout(options) }); } - async _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> { + async _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string }> { return this._frame._expect(expression, { ...options, selector: this._selector, diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index f290f49e2dd6e..27c6d635ebcbe 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1922,6 +1922,7 @@ scheme.FrameExpectResult = tObject({ matches: tBoolean, received: tOptional(tType('SerializedValue')), timedOut: tOptional(tBoolean), + errorMessage: tOptional(tString), log: tOptional(tArray(tString)), }); scheme.WorkerInitializer = tObject({ diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 891de22817dfa..4fd2e7368bd9a 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -90,7 +90,7 @@ export class NavigationAbortedError extends Error { } } -type ExpectResult = { matches: boolean, received?: any, log?: string[], timedOut?: boolean }; +type ExpectResult = { matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string }; const kDummyFrameId = ''; @@ -1356,7 +1356,7 @@ export class Frame extends SdkObject { async expect(progress: Progress, selector: string | undefined, options: FrameExpectParams, timeout?: number): Promise { progress.log(`${renderTitleForCall(progress.metadata)}${timeout ? ` with timeout ${timeout}ms` : ''}`); - const lastIntermediateResult: { received?: any, isSet: boolean } = { isSet: false }; + const lastIntermediateResult: { received?: any, isSet: boolean, errorMessage?: string } = { isSet: false }; const fixupMetadataError = (result: ExpectResult) => { // Library mode special case for the expect errors which are return values, not exceptions. if (result.matches === options.isNot) @@ -1398,11 +1398,15 @@ export class Frame extends SdkObject { } catch (e) { // Q: Why not throw upon isNonRetriableError(e) as in other places? // A: We want user to receive a friendly message containing the last intermediate result. - if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e)) - throw e; const result: ExpectResult = { matches: options.isNot, log: compressCallLog(progress.metadata.log) }; - if (lastIntermediateResult.isSet) + if (isInvalidSelectorError(e)) { + result.errorMessage = 'Error: ' + e.message; + } else if (js.isJavaScriptErrorInEvaluate(e)) { + result.errorMessage = e.message; + } else if (lastIntermediateResult.isSet) { result.received = lastIntermediateResult.received; + result.errorMessage = lastIntermediateResult.errorMessage; + } if (e instanceof TimeoutError) result.timedOut = true; fixupMetadataError(result); @@ -1410,7 +1414,7 @@ export class Frame extends SdkObject { } } - private async _expectInternal(progress: Progress, selector: string | undefined, options: FrameExpectParams, lastIntermediateResult: { received?: any, isSet: boolean }, noAbort: boolean) { + private async _expectInternal(progress: Progress, selector: string | undefined, options: FrameExpectParams, lastIntermediateResult: { received?: any, isSet: boolean, errorMessage?: string }, noAbort: boolean) { // The first expect check, a.k.a. one-shot, always finishes - even when progress is aborted. const race = (p: Promise) => noAbort ? p : progress.race(p); const selectorInFrame = selector ? await race(this.selectors.resolveFrameForSelector(selector, { strict: true })) : undefined; @@ -1439,7 +1443,10 @@ export class Frame extends SdkObject { progress.log(log); // Note: missingReceived avoids `unexpected value "undefined"` when element was not found. if (matches === options.isNot) { - lastIntermediateResult.received = missingReceived ? '' : received; + if (missingReceived) + lastIntermediateResult.errorMessage = 'Error: element(s) not found'; + else + lastIntermediateResult.received = received; lastIntermediateResult.isSet = true; if (!missingReceived && !Array.isArray(received)) progress.log(` unexpected value "${renderUnexpectedValue(options.expression, received)}"`); diff --git a/packages/playwright/src/matchers/matcherHint.ts b/packages/playwright/src/matchers/matcherHint.ts index 0505f0676caaf..65328ed68dd24 100644 --- a/packages/playwright/src/matchers/matcherHint.ts +++ b/packages/playwright/src/matchers/matcherHint.ts @@ -20,8 +20,6 @@ import type { ExpectMatcherState } from '../../types/test'; import type { StackFrame } from '@protocol/channels'; import type { Locator } from 'playwright-core'; -export const kNoElementsFoundError = ''; - export function matcherHint(state: ExpectMatcherState, locator: Locator | undefined, matcherName: string, expression: any, actual: any, matcherOptions: any, timeout: number | undefined, expectedReceivedString?: string, preventExtraStatIndent: boolean = false) { let header = state.utils.matcherHint(matcherName, expression, actual, matcherOptions).replace(/ \/\/ deep equality/, '') + ' failed\n\n'; // Extra space added after locator and timeout to match Jest's received/expected output diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index bc4e3a7f3e38e..78db00e9c5c07 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -34,11 +34,11 @@ import type { FrameExpectParams } from 'playwright-core/lib/client/types'; export type ExpectMatcherStateInternal = ExpectMatcherState & { _stepInfo?: TestStepInfoImpl }; export interface LocatorEx extends Locator { - _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>; + _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string }>; } export interface FrameEx extends Frame { - _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>; + _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string }>; } interface APIResponseEx extends APIResponse { diff --git a/packages/playwright/src/matchers/toBeTruthy.ts b/packages/playwright/src/matchers/toBeTruthy.ts index 3f6144eaf8819..94da12644cb57 100644 --- a/packages/playwright/src/matchers/toBeTruthy.ts +++ b/packages/playwright/src/matchers/toBeTruthy.ts @@ -15,7 +15,7 @@ */ import { callLogText, expectTypes } from '../util'; -import { kNoElementsFoundError, matcherHint } from './matcherHint'; +import { matcherHint } from './matcherHint'; import { runBrowserBackendOnError } from '../mcp/test/browserBackend'; import type { MatcherResult } from './matcherHint'; @@ -29,7 +29,7 @@ export async function toBeTruthy( receiverType: string, expected: string, arg: string, - query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, log?: string[], received?: any, timedOut?: boolean }>, + query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, log?: string[], received?: any, timedOut?: boolean, errorMessage?: string }>, options: { timeout?: number } = {}, ): Promise> { expectTypes(receiver, [receiverType], matcherName); @@ -41,11 +41,7 @@ export async function toBeTruthy( const timeout = options.timeout ?? this.timeout; - const { matches: pass, log, timedOut, received } = await query(!!this.isNot, timeout).catch(async error => { - // FIXME: query should not throw, but it does for strict mode violations for example. - await runBrowserBackendOnError(receiver.page(), () => error.message); - throw error; - }); + const { matches: pass, log, timedOut, received, errorMessage } = await query(!!this.isNot, timeout); if (pass === !this.isNot) { return { @@ -56,15 +52,14 @@ export async function toBeTruthy( }; } - const notFound = received === kNoElementsFoundError ? received : undefined; let printedReceived: string | undefined; let printedExpected: string | undefined; if (pass) { printedExpected = `Expected: not ${expected}`; - printedReceived = `Received: ${notFound ? kNoElementsFoundError : expected}`; + printedReceived = errorMessage ?? `Received: ${expected}`; } else { printedExpected = `Expected: ${expected}`; - printedReceived = `Received: ${notFound ? kNoElementsFoundError : received}`; + printedReceived = errorMessage ?? `Received: ${received}`; } const message = () => { const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined, `${printedExpected}\n${printedReceived}`); diff --git a/packages/playwright/src/matchers/toEqual.ts b/packages/playwright/src/matchers/toEqual.ts index 1c4a0bf6c94da..967778db5d45d 100644 --- a/packages/playwright/src/matchers/toEqual.ts +++ b/packages/playwright/src/matchers/toEqual.ts @@ -33,7 +33,7 @@ export async function toEqual( matcherName: string, receiver: Locator, receiverType: string, - query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>, + query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string }>, expected: T, options: { timeout?: number, contains?: boolean } = {}, messagePreventExtraStatIndent?: boolean @@ -48,11 +48,7 @@ export async function toEqual( const timeout = options.timeout ?? this.timeout; - const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout).catch(async error => { - // FIXME: query should not throw, but it does for strict mode violations for example. - await runBrowserBackendOnError(receiver.page(), () => error.message); - throw error; - }); + const { matches: pass, received, log, timedOut, errorMessage } = await query(!!this.isNot, timeout); if (pass === !this.isNot) { return { @@ -68,7 +64,10 @@ export async function toEqual( let printedDiff: string | undefined; if (pass) { printedExpected = `Expected: not ${this.utils.printExpected(expected)}`; - printedReceived = `Received: ${this.utils.printReceived(received)}`; + printedReceived = errorMessage ?? `Received: ${this.utils.printReceived(received)}`; + } else if (errorMessage) { + printedExpected = `Expected: ${this.utils.printExpected(expected)}`; + printedReceived = errorMessage; } else if (Array.isArray(expected) && Array.isArray(received)) { const normalizedExpected = expected.map((exp, index) => { const rec = received[index]; diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts index edb7a860ab9c4..ef304fad4654b 100644 --- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts @@ -20,8 +20,7 @@ import path from 'path'; import { escapeTemplateString, isString } from 'playwright-core/lib/utils'; -import { kNoElementsFoundError, matcherHint } from './matcherHint'; -import { EXPECTED_COLOR } from '../common/expectBundle'; +import { matcherHint } from './matcherHint'; import { callLogText, fileExistsAsync } from '../util'; import { printReceivedStringContainExpectedSubstring } from './expect'; import { currentTestInfo } from '../common/globals'; @@ -89,18 +88,17 @@ export async function toMatchAriaSnapshot( } expected = unshift(expected); - const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout }); - const typedReceived = received as MatcherReceived | typeof kNoElementsFoundError; + const { matches: pass, received, log, timedOut, errorMessage } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout }); + const typedReceived = received as MatcherReceived; const matcherHintWithExpect = (expectedReceivedString: string) => { return matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined, expectedReceivedString); }; - const notFound = typedReceived === kNoElementsFoundError; - if (notFound) { + if (errorMessage) { return { pass: this.isNot, - message: () => matcherHintWithExpect(`Expected: ${this.utils.printExpected(expected)}\nReceived: ${EXPECTED_COLOR('')}`) + callLogText(log), + message: () => matcherHintWithExpect(`Expected: ${this.utils.printExpected(expected)}\n${errorMessage}`) + callLogText(log), name: 'toMatchAriaSnapshot', expected, }; @@ -109,12 +107,12 @@ export async function toMatchAriaSnapshot( const receivedText = typedReceived.raw; const message = () => { if (pass) { - const receivedString = notFound ? receivedText : printReceivedStringContainExpectedSubstring(receivedText, receivedText.indexOf(expected), expected.length); + const receivedString = printReceivedStringContainExpectedSubstring(receivedText, receivedText.indexOf(expected), expected.length); const expectedReceivedString = `Expected: not ${this.utils.printExpected(expected)}\nReceived: ${receivedString}`; return matcherHintWithExpect(expectedReceivedString) + callLogText(log); } else { const labelExpected = `Expected`; - const expectedReceivedString = notFound ? `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${receivedText}` : this.utils.printDiffOrStringify(expected, receivedText, labelExpected, 'Received', false); + const expectedReceivedString = this.utils.printDiffOrStringify(expected, receivedText, labelExpected, 'Received', false); return matcherHintWithExpect(expectedReceivedString) + callLogText(log); } }; diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index 04b4c84a74577..1552022d98eeb 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -22,7 +22,7 @@ import { printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring } from './expect'; -import { kNoElementsFoundError, matcherHint } from './matcherHint'; +import { matcherHint } from './matcherHint'; import { EXPECTED_COLOR } from '../common/expectBundle'; import { runBrowserBackendOnError } from '../mcp/test/browserBackend'; @@ -35,7 +35,7 @@ export async function toMatchText( matcherName: string, receiver: Locator | Page, receiverType: 'Locator' | 'Page', - query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: string, log?: string[], timedOut?: boolean }>, + query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: string, log?: string[], timedOut?: boolean, errorMessage?: string }>, expected: string | RegExp, options: { timeout?: number, matchSubstring?: boolean, receiverLabel?: string } = {}, ): Promise> { @@ -60,12 +60,7 @@ export async function toMatchText( const timeout = options.timeout ?? this.timeout; - const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout).catch(async error => { - // FIXME: query should not throw, but it does for strict mode violations for example. - if (receiverType === 'Locator') - await runBrowserBackendOnError((receiver as Locator).page(), () => error.message); - throw error; - }); + const { matches: pass, received, log, timedOut, errorMessage } = await query(!!this.isNot, timeout); if (pass === !this.isNot) { return { @@ -78,36 +73,33 @@ export async function toMatchText( const stringSubstring = options.matchSubstring ? 'substring' : 'string'; const receivedString = received || ''; - const notFound = received === kNoElementsFoundError; let printedReceived: string | undefined; let printedExpected: string | undefined; let printedDiff: string | undefined; if (pass) { if (typeof expected === 'string') { - if (notFound) { - printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`; - printedReceived = `Received: ${received}`; + printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`; + if (errorMessage) { + printedReceived = errorMessage; } else { - printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`; const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length); printedReceived = `Received string: ${formattedReceived}`; } } else { - if (notFound) { - printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`; - printedReceived = `Received: ${received}`; + printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`; + if (errorMessage) { + printedReceived = errorMessage; } else { - printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`; const formattedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null); printedReceived = `Received string: ${formattedReceived}`; } } } else { const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`; - if (notFound) { + if (errorMessage) { printedExpected = `${labelExpected}: ${this.utils.printExpected(expected)}`; - printedReceived = `Received: ${received}`; + printedReceived = errorMessage; } else { printedDiff = this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false); } diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index a4ff80f6ad462..b1b179f8cb671 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -3296,6 +3296,7 @@ export type FrameExpectResult = { matches: boolean, received?: SerializedValue, timedOut?: boolean, + errorMessage?: string, log?: string[], }; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 337488643baed..fbe6aeda51860 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2803,6 +2803,7 @@ Frame: matches: boolean received: SerializedValue? timedOut: boolean? + errorMessage: string? log: type: array? items: string diff --git a/tests/page/expect-boolean.spec.ts b/tests/page/expect-boolean.spec.ts index 8f8a58c9d9a81..d9174b4b25354 100644 --- a/tests/page/expect-boolean.spec.ts +++ b/tests/page/expect-boolean.spec.ts @@ -125,7 +125,7 @@ Timeout: 1000ms`); Locator: locator('input2') Expected: not checked -Received: +Error: element(s) not found Timeout: 1000ms`); expect(stripAnsi(error.message)).toContain(`- Expect "not toBeChecked" with timeout 1000ms`); expect(stripAnsi(error.message)).toContain(`- waiting for locator(\'input2\')`); @@ -468,7 +468,7 @@ test.describe('toBeHidden', () => { Locator: locator('button') Expected: not hidden -Received: +Error: element(s) not found Timeout: 1000ms`); expect(stripAnsi(error.message)).toContain(`- Expect "not toBeHidden" with timeout 1000ms`); }); diff --git a/tests/page/expect-matcher-result.spec.ts b/tests/page/expect-matcher-result.spec.ts index 277a0efbcbf8a..549df88e465c4 100644 --- a/tests/page/expect-matcher-result.spec.ts +++ b/tests/page/expect-matcher-result.spec.ts @@ -77,7 +77,6 @@ test('toBeTruthy-based assertions should have matcher result', async ({ page }) const e = await expect(page.locator('#node2')).toBeVisible({ timeout: 1 }).catch(e => e); e.matcherResult.message = stripAnsi(e.matcherResult.message); expect.soft(e.matcherResult).toEqual({ - actual: '', expected: 'visible', message: expect.stringContaining(`expect(locator).toBeVisible() failed`), name: 'toBeVisible', @@ -90,7 +89,7 @@ test('toBeTruthy-based assertions should have matcher result', async ({ page }) Locator: locator('#node2') Expected: visible -Received: +Error: element(s) not found Timeout: 1ms Call log`); diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index 7aa5a2f2a831e..e7a91dd10f027 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -582,3 +582,17 @@ test('toHaveText that does not match should not produce logs twice', async ({ pa expect(error.message).not.toContain('locator resolved to'); expect(error.message.replace(waitingForMessage, '')).not.toContain(waitingForMessage); }); + +test('strict mode violation error format', async ({ page }) => { + await page.setContent('
hello
hi
'); + const error = await expect(page.locator('div')).toBeVisible().catch(e => e); + expect(error.message).toContain('Expected: visible'); + expect(error.message).toContain(`Error: strict mode violation: locator('div') resolved to 2 elements:`); +}); + +test('invalid selector error format', async ({ page }) => { + await page.setContent('
hello
hi
'); + const error = await expect(page.locator('##')).toBeVisible().catch(e => e); + expect(error.message).toContain('Expected: visible'); + expect(error.message).toContain(`Error: Unexpected token "#" while parsing css selector "##".`); +}); diff --git a/tests/page/expect-timeout.spec.ts b/tests/page/expect-timeout.spec.ts index 7143d309f8af6..b45a378aa56dc 100644 --- a/tests/page/expect-timeout.spec.ts +++ b/tests/page/expect-timeout.spec.ts @@ -24,7 +24,7 @@ test('should print timed out error message', async ({ page }) => { Locator: locator('no-such-thing') Expected string: "hey" -Received: +Error: element(s) not found Timeout: 1000ms`); }); @@ -46,7 +46,7 @@ test('should print timed out error message with impossible timeout', async ({ pa Locator: locator('no-such-thing') Expected string: "hey" -Received: +Error: element(s) not found Timeout: 1ms`); }); diff --git a/tests/page/expect-to-have-text.spec.ts b/tests/page/expect-to-have-text.spec.ts index b1b19cc02a294..17f510e665de5 100644 --- a/tests/page/expect-to-have-text.spec.ts +++ b/tests/page/expect-to-have-text.spec.ts @@ -160,7 +160,7 @@ test.describe('not.toHaveText', () => { await page.setContent('
hello
'); const error = await expect(page.locator('span')).not.toHaveText('hello', { timeout: 1000 }).catch(e => e); expect(stripAnsi(error.message)).toContain('Expected string: not "hello"'); - expect(stripAnsi(error.message)).toContain('Received: '); + expect(stripAnsi(error.message)).toContain('Error: element(s) not found'); expect(stripAnsi(error.message)).toContain('waiting for locator(\'span\')'); }); }); diff --git a/tests/page/matchers.misc.spec.ts b/tests/page/matchers.misc.spec.ts index 5f785f49a3a98..eb3b092a1eb8b 100644 --- a/tests/page/matchers.misc.spec.ts +++ b/tests/page/matchers.misc.spec.ts @@ -41,7 +41,7 @@ it('should print no-locator-resolved error when locator matcher did not resolve const error = await matcher().catch(e => e); expect(error).toBeInstanceOf(Error); expect(error.message).toContain(`waiting for locator('.nonexisting')`); - expect(stripAnsi(error.message)).toMatch(/Received: ?"?/); + expect(stripAnsi(error.message)).toContain('Error: element(s) not found'); }); } }); diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index a5e5b40c2084c..fca4d09e39d10 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -635,7 +635,7 @@ test('should print pending operations for toHaveText', async ({ runInlineTest }) const output = result.output; expect(output).toContain(`expect(locator).toHaveText(expected)`); expect(output).toContain('Expected string: "Text"'); - expect(output).toContain('Received: '); + expect(output).toContain('Error: element(s) not found'); expect(output).toContain('waiting for locator(\'no-such-thing\')'); }); diff --git a/tests/playwright-test/update-aria-snapshot.spec.ts b/tests/playwright-test/update-aria-snapshot.spec.ts index 78cdab3bab7e5..203b5c27e43a8 100644 --- a/tests/playwright-test/update-aria-snapshot.spec.ts +++ b/tests/playwright-test/update-aria-snapshot.spec.ts @@ -519,7 +519,7 @@ test('should not update snapshots when locator did not match', async ({ runInlin expect(fs.existsSync(patchPath)).toBe(false); expect(result.output).not.toContain('New baselines created'); expect(result.output).toContain('Expected: "- heading"'); - expect(result.output).toContain('Received: '); + expect(result.output).toContain('Error: element(s) not found'); }); test.describe('update-snapshots none', () => { From 7cbda37ab00f540a89a8a9b1cc06239b275764af Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 4 Sep 2025 16:08:14 +0200 Subject: [PATCH 077/329] chore(testserver): allow overriding timeout (#37294) --- .../src/isomorphic/testServerInterface.ts | 1 + tests/playwright-test/test-server.spec.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 66136bab2e386..0761f6cdfb27b 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -102,6 +102,7 @@ export interface TestServerInterface { projects?: string[]; reuseContext?: boolean; connectWsEndpoint?: string; + timeout?: number; }): Promise<{ status: reporterTypes.FullResult['status']; }>; diff --git a/tests/playwright-test/test-server.spec.ts b/tests/playwright-test/test-server.spec.ts index 157989c47e688..5e0c3f030c609 100644 --- a/tests/playwright-test/test-server.spec.ts +++ b/tests/playwright-test/test-server.spec.ts @@ -227,3 +227,18 @@ test('clear cache', async ({ startTestServer, writeFiles }) => { await testServerConnection.clearCache({}); expect((await testServerConnection.runGlobalTeardown({})).status).toBe('passed'); }); + +test('timeout override', async ({ startTestServer, writeFiles }) => { + const testServerConnection = await startTestServer(); + await testServerConnection.initialize({}); + await writeFiles({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('foo', () => { + expect(test.info().timeout).toEqual(42); + }); + `, + }); + + expect(await testServerConnection.runTests({ timeout: 42 })).toEqual({ status: 'passed' }); +}); From dea31d86d647353ab68ea2e61e425077cbfc200b Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 4 Sep 2025 12:11:53 -0700 Subject: [PATCH 078/329] feat(mcp): allow passing secrets (#37304) --- packages/playwright/src/mcp/browser/config.ts | 10 ++ .../playwright/src/mcp/browser/context.ts | 10 ++ .../playwright/src/mcp/browser/response.ts | 13 ++ .../playwright/src/mcp/browser/tools/form.ts | 11 +- .../src/mcp/browser/tools/keyboard.ts | 10 +- packages/playwright/src/mcp/config.d.ts | 7 + packages/playwright/src/mcp/program.ts | 3 +- tests/mcp/form.spec.ts | 2 +- tests/mcp/secrets.spec.ts | 128 ++++++++++++++++++ 9 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 tests/mcp/secrets.spec.ts diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index 1d26b13bb4f52..e931fef11ac36 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -18,6 +18,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import { devices } from 'playwright-core'; +import { dotenv } from 'playwright-core/lib/utilsBundle'; import type * as playwright from '../../../types/test'; import type { Config, ToolCapability } from '../config'; @@ -44,6 +45,7 @@ export type CLIOptions = { proxyServer?: string; saveSession?: boolean; saveTrace?: boolean; + secrets?: Record; storageState?: string; userAgent?: string; userDataDir?: string; @@ -189,6 +191,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { }, saveSession: cliOptions.saveSession, saveTrace: cliOptions.saveTrace, + secrets: cliOptions.secrets, outputDir: cliOptions.outputDir, imageResponses: cliOptions.imageResponses, }; @@ -219,6 +222,7 @@ function configFromEnv(): Config { options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS); options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER); options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE); + options.secrets = dotenvFileLoader(process.env.PLAYWRIGHT_MCP_SECRETS_FILE); options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE); options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT); options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR); @@ -300,6 +304,12 @@ export function commaSeparatedList(value: string | undefined): string[] | undefi return value.split(',').map(v => v.trim()); } +export function dotenvFileLoader(value: string | undefined): Record | undefined { + if (!value) + return undefined; + return dotenv.parse(fs.readFileSync(value, 'utf8')); +} + function envToNumber(value: string | undefined): number | undefined { if (!value) return undefined; diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts index c886c88a80bcc..a4bd59088799f 100644 --- a/packages/playwright/src/mcp/browser/context.ts +++ b/packages/playwright/src/mcp/browser/context.ts @@ -19,6 +19,7 @@ import { debug } from 'playwright-core/lib/utilsBundle'; import { logUnhandledError } from '../log'; import { Tab } from './tab'; import { outputFile } from './config'; +import * as codegen from './codegen'; import type * as playwright from '../../../types/test'; import type { FullConfig } from './config'; @@ -220,6 +221,15 @@ export class Context { } return result; } + + lookupSecret(secretName: string): { value: string, code: string } { + if (!this.config.secrets?.[secretName]) + return { value: secretName, code: codegen.quote(secretName) }; + return { + value: this.config.secrets[secretName]!, + code: `process.env['${secretName}']`, + }; + } } export class InputRecorder { diff --git a/packages/playwright/src/mcp/browser/response.ts b/packages/playwright/src/mcp/browser/response.ts index 3dc95ab826284..db910cc2227c8 100644 --- a/packages/playwright/src/mcp/browser/response.ts +++ b/packages/playwright/src/mcp/browser/response.ts @@ -136,8 +136,21 @@ ${this._code.join('\n')} content.push({ type: 'image', data: image.data.toString('base64'), mimeType: image.contentType }); } + this._redactSecrets(content); return { content, isError: this._isError }; } + + private _redactSecrets(content: (TextContent | ImageContent)[]) { + if (!this._context.config.secrets) + return; + + for (const item of content) { + if (item.type !== 'text') + continue; + for (const [secretName, secretValue] of Object.entries(this._context.config.secrets)) + item.text = item.text.replaceAll(secretValue, `${secretName}`); + } + } } function renderTabSnapshot(tabSnapshot: TabSnapshot): string { diff --git a/packages/playwright/src/mcp/browser/tools/form.ts b/packages/playwright/src/mcp/browser/tools/form.ts index aad12ad919c69..4f7beadcedc7d 100644 --- a/packages/playwright/src/mcp/browser/tools/form.ts +++ b/packages/playwright/src/mcp/browser/tools/form.ts @@ -17,7 +17,7 @@ import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; import { generateLocator } from './utils'; -import * as javascript from '../codegen'; +import * as codegen from '../codegen'; const fillForm = defineTabTool({ capability: 'core', @@ -42,14 +42,15 @@ const fillForm = defineTabTool({ const locator = await tab.refLocator({ element: field.name, ref: field.ref }); const locatorSource = `await page.${await generateLocator(locator)}`; if (field.type === 'textbox' || field.type === 'slider') { - await locator.fill(field.value); - response.addCode(`${locatorSource}.fill(${javascript.quote(field.value)});`); + const secret = tab.context.lookupSecret(field.value); + await locator.fill(secret.value); + response.addCode(`${locatorSource}.fill(${secret.code});`); } else if (field.type === 'checkbox' || field.type === 'radio') { await locator.setChecked(field.value === 'true'); - response.addCode(`${locatorSource}.setChecked(${javascript.quote(field.value)});`); + response.addCode(`${locatorSource}.setChecked(${field.value});`); } else if (field.type === 'combobox') { await locator.selectOption({ label: field.value }); - response.addCode(`${locatorSource}.selectOption(${javascript.quote(field.value)});`); + response.addCode(`${locatorSource}.selectOption(${codegen.quote(field.value)});`); } } }, diff --git a/packages/playwright/src/mcp/browser/tools/keyboard.ts b/packages/playwright/src/mcp/browser/tools/keyboard.ts index e567bcb65bb03..8b08a1ec68ee7 100644 --- a/packages/playwright/src/mcp/browser/tools/keyboard.ts +++ b/packages/playwright/src/mcp/browser/tools/keyboard.ts @@ -18,7 +18,6 @@ import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; import { elementSchema } from './snapshot'; import { generateLocator } from './utils'; -import * as javascript from '../codegen'; const pressKey = defineTabTool({ capability: 'core', @@ -62,15 +61,16 @@ const type = defineTabTool({ handle: async (tab, params, response) => { const locator = await tab.refLocator(params); + const secret = tab.context.lookupSecret(params.text); await tab.waitForCompletion(async () => { if (params.slowly) { response.setIncludeSnapshot(); - response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`); - await locator.pressSequentially(params.text); + response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${secret.code});`); + await locator.pressSequentially(secret.value); } else { - response.addCode(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`); - await locator.fill(params.text); + response.addCode(`await page.${await generateLocator(locator)}.fill(${secret.code});`); + await locator.fill(secret.value); } if (params.submit) { diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index ffeb8120b1485..815329f0abbe0 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -95,6 +95,13 @@ export type Config = { */ saveTrace?: boolean; + /** + * Secrets are used to prevent LLM from getting sensitive data while + * automating scenarios such as authentication. + * Prefer the browser.contextOptions.storageState over secrets file as a more secure alternative. + */ + secrets?: Record; + /** * The directory to save output files. */ diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index 561472e85aa43..6e046453a951a 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -16,7 +16,7 @@ import { program, ProgramOption } from 'playwright-core/lib/utilsBundle'; import * as mcpServer from './sdk/server'; -import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; +import { commaSeparatedList, dotenvFileLoader, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; import { Context } from './browser/context'; import { contextFactory } from './browser/browserContextFactory'; import { ProxyBackend } from './sdk/proxyBackend'; @@ -52,6 +52,7 @@ program .option('--proxy-server ', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"') .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.') .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.') + .option('--secrets ', 'path to a file containing secrets in the dotenv format', dotenvFileLoader) .option('--storage-state ', 'path to the storage state file for isolated sessions.') .option('--user-agent ', 'specify user agent string') .option('--user-data-dir ', 'path to the user data directory. If not specified, a temporary directory will be created.') diff --git a/tests/mcp/form.spec.ts b/tests/mcp/form.spec.ts index 339d79dff58fa..893c4517889e2 100644 --- a/tests/mcp/form.spec.ts +++ b/tests/mcp/form.spec.ts @@ -97,7 +97,7 @@ test('browser_fill_form (textbox)', async ({ client, server }) => { await page.getByRole('textbox', { name: 'Email' }).fill('john.doe@example.com'); await page.getByRole('slider', { name: 'Age' }).fill('25'); await page.getByLabel('Choose a country United').selectOption('United States'); -await page.getByRole('checkbox', { name: 'Subscribe to newsletter' }).setChecked('true');`, +await page.getByRole('checkbox', { name: 'Subscribe to newsletter' }).setChecked(true);`, }); const response = await client.callTool({ diff --git a/tests/mcp/secrets.spec.ts b/tests/mcp/secrets.spec.ts new file mode 100644 index 0000000000000..0f8cb47095908 --- /dev/null +++ b/tests/mcp/secrets.spec.ts @@ -0,0 +1,128 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'node:fs'; + +import { test, expect } from './fixtures'; + +test('browser_type', async ({ startClient, server }) => { + const secretsFile = test.info().outputPath('secrets.env'); + await fs.promises.writeFile(secretsFile, 'X-PASSWORD=password123'); + + const { client } = await startClient({ + args: ['--secrets', secretsFile], + }); + + server.setContent('/', ` + + + + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + { + const response = await client.callTool({ + name: 'browser_type', + arguments: { + element: 'textbox', + ref: 'e2', + text: 'X-PASSWORD', + submit: true, + }, + }); + expect(response).toHaveResponse({ + code: `await page.getByRole('textbox').fill(process.env['X-PASSWORD']); +await page.getByRole('textbox').press('Enter');`, + pageState: expect.stringContaining(`- textbox`), + }); + } + + expect(await client.callTool({ + name: 'browser_console_messages', + })).toHaveResponse({ + result: expect.stringContaining(`[LOG] Key pressed: Enter , Text: X-PASSWORD`), + }); +}); + + +test('browser_fill_form', async ({ startClient, server }) => { + const secretsFile = test.info().outputPath('secrets.env'); + await fs.promises.writeFile(secretsFile, 'X-PASSWORD=password123'); + + const { client } = await startClient({ + args: ['--secrets', secretsFile], + }); + + server.setContent('/', ` + + + +
+ + +
+ + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_fill_form', + arguments: { + fields: [ + { + name: 'Email textbox', + type: 'textbox', + ref: 'e4', + value: 'John Doe' + }, + { + name: 'Password textbox', + type: 'textbox', + ref: 'e6', + value: 'X-PASSWORD' + }, + ] + }, + })).toHaveResponse({ + code: `await page.getByRole('textbox', { name: 'Email' }).fill('John Doe'); +await page.getByRole('textbox', { name: 'Password' }).fill(process.env['X-PASSWORD']);`, + }); + + expect(await client.callTool({ + name: 'browser_snapshot', + arguments: {}, + })).toHaveResponse({ + pageState: expect.stringContaining(`- textbox \"Password\" [active] [ref=e6]: X-PASSWORD`), + }); +}); From 3e4efe9d7a304918ad643801bb40003ea1481183 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 4 Sep 2025 13:27:30 -0700 Subject: [PATCH 079/329] feat(mcp): allow passing cdp headers (#37309) --- .../src/mcp/browser/browserContextFactory.ts | 2 +- packages/playwright/src/mcp/browser/config.ts | 12 ++++++++++++ packages/playwright/src/mcp/config.d.ts | 5 +++++ packages/playwright/src/mcp/program.ts | 3 ++- tests/mcp/cdp.spec.ts | 17 +++++++++++++++++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index 6021896cbbe2f..e4320b3a1a246 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -126,7 +126,7 @@ class CdpContextFactory extends BaseContextFactory { } protected override async _doObtainBrowser(): Promise { - return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint!); + return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint!, { headers: this.config.browser.cdpHeaders }); } protected override async _doCreateContext(browser: playwright.Browser): Promise { diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index e931fef11ac36..e06543f5ff24f 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -30,6 +30,7 @@ export type CLIOptions = { browser?: string; caps?: string[]; cdpEndpoint?: string; + cdpHeader?: Record; config?: string; device?: string; executablePath?: string; @@ -179,6 +180,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { launchOptions, contextOptions, cdpEndpoint: cliOptions.cdpEndpoint, + cdpHeaders: cliOptions.cdpHeader, }, server: { port: cliOptions.port, @@ -207,6 +209,7 @@ function configFromEnv(): Config { options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER); options.caps = commaSeparatedList(process.env.PLAYWRIGHT_MCP_CAPS); options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT); + options.cdpHeader = headerParser(process.env.PLAYWRIGHT_MCP_CDP_HEADERS, {}); options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG); options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE); options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH); @@ -310,6 +313,15 @@ export function dotenvFileLoader(value: string | undefined): Record): Record { + if (!arg) + return previous || {}; + const result: Record = previous || {}; + const [name, value] = arg.split(':').map(v => v.trim()); + result[name] = value; + return result; +} + function envToNumber(value: string | undefined): number | undefined { if (!value) return undefined; diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index 815329f0abbe0..49fe40226e116 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -59,6 +59,11 @@ export type Config = { */ cdpEndpoint?: string; + /** + * CDP headers to send with the connect request. + */ + cdpHeaders?: Record; + /** * Remote endpoint to connect to an existing Playwright server. */ diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index 6e046453a951a..bc01c31806a02 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -16,7 +16,7 @@ import { program, ProgramOption } from 'playwright-core/lib/utilsBundle'; import * as mcpServer from './sdk/server'; -import { commaSeparatedList, dotenvFileLoader, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; +import { commaSeparatedList, dotenvFileLoader, headerParser, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; import { Context } from './browser/context'; import { contextFactory } from './browser/browserContextFactory'; import { ProxyBackend } from './sdk/proxyBackend'; @@ -36,6 +36,7 @@ program .option('--browser ', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.') .option('--caps ', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList) .option('--cdp-endpoint ', 'CDP endpoint to connect to.') + .option('--cdp-header ', 'CDP headers to send with the connect request, multiple can be specified.', headerParser) .option('--config ', 'path to the configuration file.') .option('--device ', 'device to emulate, for example: "iPhone 15"') .option('--executable-path ', 'path to the browser executable.') diff --git a/tests/mcp/cdp.spec.ts b/tests/mcp/cdp.spec.ts index 90ae1620bd547..5ff0930bba4a2 100644 --- a/tests/mcp/cdp.spec.ts +++ b/tests/mcp/cdp.spec.ts @@ -90,3 +90,20 @@ test('does not support --device', async () => { expect(result.status).toBe(1); expect(result.stderr.toString()).toContain('Device emulation is not supported with cdpEndpoint.'); }); + +test('cdp server with headers', async ({ startClient, server }) => { + let authHeader = ''; + server.setRoute('/json/version/', (req, res) => { + authHeader = req.headers['authorization']; + res.end(); + }); + + const { client } = await startClient({ args: [`--cdp-endpoint=${server.PREFIX}`, '--cdp-header', 'Authorization: Bearer 1234567890'] }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + isError: true, + }); + expect(authHeader).toBe('Bearer 1234567890'); +}); From 82320145f7d297492eb4660ce12e6236a7b37144 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 4 Sep 2025 14:27:50 -0700 Subject: [PATCH 080/329] fix(mcp): wait if profile is still in use (#37303) --- .../src/mcp/browser/browserContextFactory.ts | 50 +++++++------ .../src/mcp/browser/processUtils.ts | 75 +++++++++++++++++++ tests/mcp/launch.spec.ts | 43 +++++++++++ 3 files changed, 147 insertions(+), 21 deletions(-) create mode 100644 packages/playwright/src/mcp/browser/processUtils.ts diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index e4320b3a1a246..d2e9f7de31c0e 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -22,6 +22,7 @@ import path from 'path'; import * as playwright from 'playwright-core'; import { registryDirectory } from 'playwright-core/lib/server/registry/index'; import { startTraceViewerServer } from 'playwright-core/lib/server'; +import { findBrowserProcess, getBrowserExecPath } from './processUtils'; import { logUnhandledError, testDebug } from '../log'; import { outputFile } from './config'; @@ -174,28 +175,28 @@ class PersistentContextFactory implements BrowserContextFactory { const browserType = playwright[this.config.browser.browserName]; for (let i = 0; i < 5; i++) { - try { - const browserContext = await browserType.launchPersistentContext(userDataDir, { - tracesDir, - ...this.config.browser.launchOptions, - ...this.config.browser.contextOptions, - handleSIGINT: false, - handleSIGTERM: false, - }); - const close = () => this._closeBrowserContext(browserContext, userDataDir); - return { browserContext, close }; - } catch (error: any) { - if (error.message.includes('Executable doesn\'t exist')) - throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); - if (error.message.includes('ProcessSingleton') || error.message.includes('Invalid URL')) { - // User data directory is already in use, try again. - await new Promise(resolve => setTimeout(resolve, 1000)); - continue; - } - throw error; - } + if (!await alreadyRunning(this.config, browserType, userDataDir)) + break; + // User data directory is already in use, wait for the previous browser instance to close. + await new Promise(resolve => setTimeout(resolve, 1000)); + } + try { + const browserContext = await browserType.launchPersistentContext(userDataDir, { + tracesDir, + ...this.config.browser.launchOptions, + ...this.config.browser.contextOptions, + handleSIGINT: false, + handleSIGTERM: false, + }); + const close = () => this._closeBrowserContext(browserContext, userDataDir); + return { browserContext, close }; + } catch (error: any) { + if (error.message.includes('Executable doesn\'t exist')) + throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); + if (error.message.includes('ProcessSingleton') || error.message.includes('Invalid URL')) + throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); + throw error; } - throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); } private async _closeBrowserContext(browserContext: playwright.BrowserContext, userDataDir: string) { @@ -217,6 +218,13 @@ class PersistentContextFactory implements BrowserContextFactory { } } +async function alreadyRunning(config: FullConfig, browserType: playwright.BrowserType, userDataDir: string) { + const execPath = config.browser.launchOptions.executablePath ?? getBrowserExecPath(config.browser.launchOptions.channel ?? browserType.name()); + if (!execPath) + return false; + return !!findBrowserProcess(execPath, userDataDir); +} + async function injectCdpPort(browserConfig: FullConfig['browser']) { if (browserConfig.browserName === 'chromium') (browserConfig.launchOptions as any).cdpPort = await findFreePort(); diff --git a/packages/playwright/src/mcp/browser/processUtils.ts b/packages/playwright/src/mcp/browser/processUtils.ts new file mode 100644 index 0000000000000..45584b4861e50 --- /dev/null +++ b/packages/playwright/src/mcp/browser/processUtils.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import childProcess from 'child_process'; + +import { registry } from 'playwright-core/lib/server/registry/index'; + +// TODO: make browserType.executablePath() return it. +export function getBrowserExecPath(channelOrName: string): string | undefined { + return registry.findExecutable(channelOrName)?.executablePath('javascript'); +} + +export function findBrowserProcess(execPath: string, arg: string): string | null { + switch (process.platform) { + case 'darwin': + return findProcessMacos(execPath, arg); + case 'linux': + return findProcessLinux(execPath, arg); + case 'win32': + return findProcessWindows(execPath, arg); + default: + return null; + } +} + +function findProcessLinux(execPath: string, arg: string): string | null { + const psResult = childProcess.spawnSync('ps', ['-eo', 'pid=,args=']); + return findMatchingLine(psResult.stdout.toString(), execPath, arg); +} + +function findProcessMacos(execPath: string, arg: string): string | null { + const psResult = childProcess.spawnSync('ps', ['-axo', 'pid=,command=']); + return findMatchingLine(psResult.stdout.toString(), execPath, arg); +} + +function findProcessWindows(execPath: string, arg: string): string | null { + const filter = `$_.ExecutablePath -eq '${execPath}' -and $_.CommandLine.Contains('${arg}') -and $_.CommandLine -notmatch '--type'`; + const ps = childProcess.spawnSync( + 'powershell.exe', + [ + '-NoProfile', + '-Command', + `Get-CimInstance Win32_Process | Where-Object { ${filter} } | Select-Object -Property ProcessId,CommandLine | ForEach-Object { "$($_.ProcessId) $($_.CommandLine)" }` + ], + { encoding: 'utf8' } + ); + + if (ps.status !== 0) + return null; + + return findMatchingLine(ps.stdout.toString(), execPath, arg); +} + +function findMatchingLine(psOutput: string, execPath: string, arg: string): string | null { + const lines = psOutput.split('\n').map(l => l.trim()).filter(Boolean); + for (const line of lines) { + // Chrome child process have --type argument, we only return the browser process. + if (line.includes(execPath) && line.includes(arg) && !line.includes('--type')) + return line; + } + return null; +} diff --git a/tests/mcp/launch.spec.ts b/tests/mcp/launch.spec.ts index f7f0171aa9efd..deca384b7e192 100644 --- a/tests/mcp/launch.spec.ts +++ b/tests/mcp/launch.spec.ts @@ -167,3 +167,46 @@ test('isolated context with storage state', async ({ startClient, server }, test pageState: expect.stringContaining(`Storage: session-value`), }); }); + +test('persistent context already running', async ({ startClient, server, mcpBrowser }, testInfo) => { + const userDataDir = testInfo.outputPath('user-data-dir'); + const { client } = await startClient({ + args: [`--user-data-dir=${userDataDir}`], + }); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const { client: client2, stderr } = await startClient({ + args: [`--user-data-dir=${userDataDir}`], + }); + const navigationPromise = client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const wait = await Promise.race([ + navigationPromise.then(() => 'done'), + new Promise(resolve => setTimeout(resolve, 1_000)).then(() => 'timeout'), + ]); + expect(wait).toBe('timeout'); + + // Check that the second client is trying to launch the browser. + await expect.poll(() => formatOutput(stderr()), { timeout: 0 }).toEqual([ + 'create context', + 'create browser context (persistent)', + 'lock user data dir' + ]); + + // Close first client's browser. + await client.callTool({ + name: 'browser_close', + arguments: { url: server.HELLO_WORLD }, + }); + + const result = await navigationPromise; + expect(result).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), + }); +}); From cc8c05b10444895d0f3bfc9d1710f4c0e2f5fad6 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 4 Sep 2025 14:28:30 -0700 Subject: [PATCH 081/329] test(mcp): add a test for custom executable path (#37312) --- .github/workflows/tests_components.yml | 2 ++ .github/workflows/tests_primary.yml | 2 ++ tests/mcp/config.spec.ts | 11 +++++++++++ 3 files changed, 15 insertions(+) diff --git a/.github/workflows/tests_components.yml b/.github/workflows/tests_components.yml index 39f33c9455fd0..020e60d37c5d9 100644 --- a/.github/workflows/tests_components.yml +++ b/.github/workflows/tests_components.yml @@ -9,6 +9,8 @@ on: paths-ignore: - 'browser_patches/**' - 'docs/**' + - 'packages/playwright/src/mcp/**' + - 'tests/mcp/**' branches: - main - release-* diff --git a/.github/workflows/tests_primary.yml b/.github/workflows/tests_primary.yml index 0a6f2e185749f..1673f2f104868 100644 --- a/.github/workflows/tests_primary.yml +++ b/.github/workflows/tests_primary.yml @@ -9,6 +9,8 @@ on: paths-ignore: - 'browser_patches/**' - 'docs/**' + - 'packages/playwright/src/mcp/**' + - 'tests/mcp/**' branches: - main - release-* diff --git a/tests/mcp/config.spec.ts b/tests/mcp/config.spec.ts index a6d8151e12b30..b7109f9cd957f 100644 --- a/tests/mcp/config.spec.ts +++ b/tests/mcp/config.spec.ts @@ -46,6 +46,17 @@ test('config user data dir', async ({ startClient, server, mcpMode }, testInfo) expect(files.length).toBeGreaterThan(0); }); +test('executable path', async ({ startClient, server }, testInfo) => { + const { client } = await startClient({ args: ['--executable-path', testInfo.outputPath('missing-executable')] }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + isError: true, + result: expect.stringMatching(/Failed to launch.*missing-executable/), + }); +}); + test.describe(() => { test.use({ mcpBrowser: '' }); test('browserName', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/458' } }, async ({ startClient, mcpMode }, testInfo) => { From bf86541aad6e501cdbaf4a2f2e583330440fb2f9 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 4 Sep 2025 15:12:54 -0700 Subject: [PATCH 082/329] chore: move mcp extension back to the mcp repo (#37313) --- .../workflows/{test_mcp.yml => tests_mcp.yml} | 39 -- .gitignore | 2 - eslint.config.mjs | 1 - package-lock.json | 585 ------------------ packages/mcp-extension/README.md | 48 -- packages/mcp-extension/icons/icon-128.png | Bin 6352 -> 0 bytes packages/mcp-extension/icons/icon-16.png | Bin 571 -> 0 bytes packages/mcp-extension/icons/icon-32.png | Bin 1258 -> 0 bytes packages/mcp-extension/icons/icon-48.png | Bin 2043 -> 0 bytes packages/mcp-extension/manifest.json | 35 -- packages/mcp-extension/package.json | 35 -- packages/mcp-extension/src/background.ts | 222 ------- packages/mcp-extension/src/relayConnection.ts | 178 ------ packages/mcp-extension/src/ui/connect.css | 206 ------ packages/mcp-extension/src/ui/connect.html | 29 - packages/mcp-extension/src/ui/connect.tsx | 233 ------- packages/mcp-extension/src/ui/status.html | 13 - packages/mcp-extension/src/ui/status.tsx | 110 ---- packages/mcp-extension/src/ui/tabItem.tsx | 67 -- packages/mcp-extension/src/ui/tsconfig.json | 4 - packages/mcp-extension/tsconfig.json | 22 - packages/mcp-extension/tsconfig.ui.json | 19 - packages/mcp-extension/vite.config.mts | 54 -- packages/mcp-extension/vite.sw.config.mts | 31 - tests/mcp/extension.spec.ts | 306 --------- tsconfig.json | 1 - 26 files changed, 2240 deletions(-) rename .github/workflows/{test_mcp.yml => tests_mcp.yml} (52%) delete mode 100644 packages/mcp-extension/README.md delete mode 100644 packages/mcp-extension/icons/icon-128.png delete mode 100644 packages/mcp-extension/icons/icon-16.png delete mode 100644 packages/mcp-extension/icons/icon-32.png delete mode 100644 packages/mcp-extension/icons/icon-48.png delete mode 100644 packages/mcp-extension/manifest.json delete mode 100644 packages/mcp-extension/package.json delete mode 100644 packages/mcp-extension/src/background.ts delete mode 100644 packages/mcp-extension/src/relayConnection.ts delete mode 100644 packages/mcp-extension/src/ui/connect.css delete mode 100644 packages/mcp-extension/src/ui/connect.html delete mode 100644 packages/mcp-extension/src/ui/connect.tsx delete mode 100644 packages/mcp-extension/src/ui/status.html delete mode 100644 packages/mcp-extension/src/ui/status.tsx delete mode 100644 packages/mcp-extension/src/ui/tabItem.tsx delete mode 100644 packages/mcp-extension/src/ui/tsconfig.json delete mode 100644 packages/mcp-extension/tsconfig.json delete mode 100644 packages/mcp-extension/tsconfig.ui.json delete mode 100644 packages/mcp-extension/vite.config.mts delete mode 100644 packages/mcp-extension/vite.sw.config.mts delete mode 100644 tests/mcp/extension.spec.ts diff --git a/.github/workflows/test_mcp.yml b/.github/workflows/tests_mcp.yml similarity index 52% rename from .github/workflows/test_mcp.yml rename to .github/workflows/tests_mcp.yml index 91b569f8da757..36120d5d9d68a 100644 --- a/.github/workflows/test_mcp.yml +++ b/.github/workflows/tests_mcp.yml @@ -44,42 +44,3 @@ jobs: - run: npm run build - run: npx playwright install --with-deps - run: npm run mcp-test - - test_mcp_extension: - name: Extension - strategy: - fail-fast: false - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: "Extension: Install dependencies" - run: npm ci - working-directory: ./packages/mcp-extension - - name: "Extension: Build" - run: npm run build - working-directory: ./packages/mcp-extension - - name: "Extension: Upload artifact" - uses: actions/upload-artifact@v4 - with: - name: extension - path: ./packages/mcp-extension/dist - retention-days: 7 - - - run: npm ci - - run: npm run build - - run: npx playwright install chromium - - - name: Run tests - run: | - if [[ "$(uname)" == "Linux" ]]; then - xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run mcp-etest - else - npm run mcp-etest - fi - shell: bash diff --git a/.gitignore b/.gitignore index 07cfae4d196a6..8e41e318108ae 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,6 @@ yarn.lock /packages/playwright-core/src/generated /packages/playwright-ct-core/src/generated packages/*/lib/ -packages/mcp-extension/dist/ -packages/mcp-extension/lib/ drivers/ .android-sdk/ .gradle/ diff --git a/eslint.config.mjs b/eslint.config.mjs index 0aa507cf85d9e..05346f56eef49 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -55,7 +55,6 @@ const ignores = [ "packages/html-reporter/playwright.config.ts", "packages/html-reporter/playwright/*", "packages/html-reporter/vite.config.ts", - "packages/mcp-extension/", "test-results/", "tests/assets/", "tests/components/", diff --git a/package-lock.json b/package-lock.json index f4f605b1c59a7..c8b89499feda5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1348,10 +1348,6 @@ "resolved": "packages/playwright-ct-vue", "link": true }, - "node_modules/@playwright/mcp-extension": { - "resolved": "packages/mcp-extension", - "link": true - }, "node_modules/@playwright/test": { "resolved": "packages/playwright-test", "link": true @@ -1756,16 +1752,6 @@ "@types/responselike": "^1.0.0" } }, - "node_modules/@types/chrome": { - "version": "0.0.315", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.315.tgz", - "integrity": "sha512-Oy1dYWkr6BCmgwBtOngLByCHstQ3whltZg7/7lubgIZEYvKobDneqplgc6LKERNRBwckFviV4UU5AZZNUFrJ4A==", - "dev": true, - "dependencies": { - "@types/filesystem": "*", - "@types/har-format": "*" - } - }, "node_modules/@types/codemirror": { "version": "5.60.16", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.16.tgz", @@ -1780,21 +1766,6 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, - "node_modules/@types/filesystem": { - "version": "0.0.36", - "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", - "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", - "dev": true, - "dependencies": { - "@types/filewriter": "*" - } - }, - "node_modules/@types/filewriter": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", - "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", - "dev": true - }, "node_modules/@types/formidable": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-2.0.6.tgz", @@ -1804,12 +1775,6 @@ "@types/node": "*" } }, - "node_modules/@types/har-format": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", - "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", - "dev": true - }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -6160,18 +6125,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", - "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7875,60 +7828,6 @@ } } }, - "node_modules/vite-plugin-static-copy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.2.tgz", - "integrity": "sha512-aVmYOzptLVOI2b1jL+cmkF7O6uhRv1u5fvOkQgbohWZp2CbR22kn9ZqkCUIt9umKF7UhdbsEpshn1rf4720QFg==", - "dev": true, - "dependencies": { - "chokidar": "^3.6.0", - "fs-extra": "^11.3.0", - "p-map": "^7.0.3", - "picocolors": "^1.1.1", - "tinyglobby": "^0.2.14" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/vite-plugin-static-copy/node_modules/fs-extra": { - "version": "11.3.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", - "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/vite-plugin-static-copy/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/vite-plugin-static-copy/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/vitefu": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", @@ -8257,490 +8156,6 @@ "packages/html-reporter": { "version": "0.0.0" }, - "packages/mcp-extension": { - "name": "@playwright/mcp-extension", - "version": "0.0.36", - "license": "Apache-2.0", - "devDependencies": { - "@types/chrome": "^0.0.315", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@vitejs/plugin-react": "^4.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^5.8.2", - "vite": "^5.0.0", - "vite-plugin-static-copy": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "packages/mcp-extension/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "packages/mcp-extension/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "packages/mcp-extension/node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", - "dev": true, - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, "packages/playwright": { "version": "1.56.0-next", "license": "Apache-2.0", diff --git a/packages/mcp-extension/README.md b/packages/mcp-extension/README.md deleted file mode 100644 index 6421798e03a68..0000000000000 --- a/packages/mcp-extension/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Playwright MCP Chrome Extension - -## Introduction - -The Playwright MCP Chrome Extension allows you to connect to pages in your existing browser and leverage the state of your default user profile. This means the AI assistant can interact with websites where you're already logged in, using your existing cookies, sessions, and browser state, providing a seamless experience without requiring separate authentication or setup. - -## Prerequisites - -- Chrome/Edge/Chromium browser - -## Installation Steps - -### Download the Extension - -Download the latest Chrome extension from GitHub: -- **Download link**: https://github.com/microsoft/playwright-mcp/releases - -### Load Chrome Extension - -1. Open Chrome and navigate to `chrome://extensions/` -2. Enable "Developer mode" (toggle in the top right corner) -3. Click "Load unpacked" and select the extension directory - -### Configure Playwright MCP server - -Configure Playwright MCP server to connect to the browser using the extension by passing the `--extension` option when running the MCP server: - -```json -{ - "mcpServers": { - "playwright-extension": { - "command": "npx", - "args": [ - "@playwright/mcp@latest", - "--extension" - ] - } - } -} -``` - -## Usage - -### Browser Tab Selection - -When the LLM interacts with the browser for the first time, it will load a page where you can select which browser tab the LLM will connect to. This allows you to control which specific page the AI assistant will interact with during the session. - - diff --git a/packages/mcp-extension/icons/icon-128.png b/packages/mcp-extension/icons/icon-128.png deleted file mode 100644 index c4bc8b02508b40cec8a0d73465324a9612744172..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6352 zcmb7JWmnXV)Bf$U^a7HCASFn5m#lQBba#i+y|jdYAX2h~l%#Y>xrB5}r*tFT`}h6^ z&ogso=FPmAGv~})6EPZU@_0DUZ~y?nQ&f=A`e)()2^Pjb-Sx_1@Snl3RF;VMe&-7<*EP~0QQ-e zyPEv64$;N8t5@Htw|oavKfM1-X3m~l`|-KlSi0S_&s^fcK)*57v>O(C>Pr#51Gt?1w1oSgyZ zedR~@)(h_;^n;ic3U<4En>p%V?e!pfb?7B!W!m_c2ES%)$S$j#Pis^j{P`2Tp_~;* zJ9e4N%F*$2Bcv`M{4dqIXz8}>yTYjn(FDN1yxDxgJ3u_h7@Q%Qo^qaqx`dNq53CY4js>CT{=bOAlkx~C2O^GnfL8-s^NA-@d2 z12s8#X<0Fe$-|GX9H(I01GP_nmHN1}UYpcFW&S_{f+y&@Gf5UJYZCc5Hd)(Yw3Txg zjxL#l>bhz8gZD2!Cy;JM%hhlB^VQgyr=IJ2DWle=0DTGPWA58bO#V+f2@_JmB8$^7MeThKdvSR7&RO$-;f)BFiBerbg$JfxA)!{6<- z|4wHBDEGT)u**VQnK z=e+{3&E7%)cmZZsvMwnqU^W_l@7CnkY?4tc2Qrjww-wezTV;tjta{IL`(x!G0tq{z9$P5qgosqxaD6STSl7GuM3(OSF4wsxobSb#Rx??j z@4I$ZRMM})hnVoEDh1Ikl4$*5JC;$5Sau*7Z#3ki5$|bCQWrZDPYMCVEnPL#;whse z)GdtXj}wjT$(I{RZaT}WXM9prXE~bW-ybY{KegnyT>iA{>Rkz$7C0TYh1HYaz=trc z@Bw{I9*VfHqDO0}?)P)tXvq{|a!2dzwh@I8;D80En@D2a-ZiLIR+s?yCU2>R=L2)- z@p00IstLoLJ`V{416RCrX5de#f)$NG96KDSpXH*A)|F;aQ2o-)Z583wH9fZA<_I?% zM(~H^VPSCuP!U-S5ICoO@JfOwyzW$$;q|IMptE9z?R-I-T^mIFE5uT!faPGo?y_=W z!KTq&r9WdFA54w~?o%}2r{pFlBOhS2GQoJ^S&5Q7;v;fRBe6_1&NfsZp%~9rrZ3o; zcv?MVDXC&+9_BzES$&_PZ#{V8h9#P=uq#|TC=Zf)-+zo@Cj$aHlcBQ4<-kYg?Nx=~ zS#Kj5NLog~)Wu$TKZ$NEh=hQK7?|41djl?`U8HQ^_2s5VV~iOnYp&@BpDfEL{W@wQ z#{{j`6~xOK%blXppGt9gPC0Qw%1e}l-9{G$uLeD~HS}Y4hggFu>5r9d7)6Si8k1-j z8esM~5t$MXlEh4(2FSF%3QL1&3nKlc*D^SbJa8Q>3T_@0rm%8{Nmnp61YA0QxCEyo z=heOkW}qjq5WSAG&!=U#UB&1Ayq?@t+Te34Zl*V)7_vF;z%FuRov}6Y@{HiSOc4F% zy6I~EUM6v*l$tNE(5LaWTzSo0nI1IJFggf3t`<7=qx}H|EH74J#0d9MJ$Q^J<>$etp7f`<$7MI_~2UO=rJHdt1+{eH$ZV7MWTYQ>vpZ&YCo zo$l=@8#?D92K~Dow+^vKxC|DKoGvxXIJJEiVVUtHTNVL-d+W`*ObMF1rcm)wmbkk1 z5|17pA@ksE$bss{8k#o=44UzYTgEL}swd*TH6(u@%Epu*(zt<+o@Aabe!dzY@t3qS zUjJq-^yCGa3EBs~4sB84?HF{o)K@x>kp`${q=eQ@luH%@ZOsbc#PRd@86l~5E*L{s z=r8>gD@0WBL=O~Eumre|X~_u`#=xMT8AhnAE>3);D~b0<{L7)f}( z{Da@1cD86ZODn&jX-F9V`mH%;rMB~hTPt9W9|Y;-7pdbLS9)8u8qQ&KA6M#hDQHj^ zA0>71c3V7#dFt2HMMGW$0@sl-I5!La9B@gSm>sVR8oX)u+Kv5D+5Gw`=g-rcq*^~U zP&xCku$2|W{OMqwICsS+@YyH!q|r8{qg=wCdVbwMpx0r1of88nyvgius`rS9Q(=B| zdk33~BUpNvstV(<&+khTXZaI3S`H-PWBv$6wRF!2&bNT+{j)RD&}m!5lTzflFxN*h z#2?qm6v1t1C*BPkY^q`iNnXR}OPl^DafqM0FJ#x)KLg#ek8m@}uO)TXywY25h_|Lr zYQi<%FqRuus#R3b%Eb2BD4R~1lyZ{t50u-GfJBpP<5pN(@IlqkCTu zu)>g6p>gugJs6N+i46Y+5|~uGTCU`R1M_3*{0>eh^f0S=tyUhX)MLWc`oyQ*yC;?u zJ#icVk%ebFVS<{0ue^jd$*f;R9LdX(7TI}zG6irWS=KL6gyY?mMI_Z`Z5*~D()~Y2 zkcpOVrUX?^RZNNX{1ko$rw!J7b2t{w##{J6XijAPE{TU64KO%o!+F;A^{S%zg>H3t zeHmL$Yp+KiUP5!xie-BV?unCCD^~Z*>Zp)F-Iabxgsxn0eQHJr_G+2TIXV0oG#kl` z=8nka1xa!mgmKPf#S9?hgX3Oc&!7=0;F`VBsilYr2>DENppzbVFr^83PMmATJ@mXN zHK30p>`Zm60vd?4ba1lyAO*HVemrXiLV`WS=H(PM`blKACpzR?0zfuTgAHqzYNluo zLTtKX2$gc*sSfiy{5gZ%6T;&va?HbD`ITb3W_0??EYE8E*lve#t|501$QWLaqQO#F zS`MeH8Af>v?me_)U2;3CpPZpG*-`UHU6?>Wk?s^Ov81rrWeK&{xt@F<$0;sNuwC81 zo{wiFl;pC6vgAMCnk4P0s?6r8I~F16kslEv$v$^u)7VYel(I`uo7 zm|ffpj)d2{&mWiWy(}^hQz_Hh^_q8Es)DT7FN9_uYFxWNPKG|uO2C|2Rf0E|dYZ#B zkQK{5F0sU9)-Njn>^AjO9Raa)_l1y6^nf=f@2%FWbH%7ca+pt3jpoPm65XCA2?=nG zLsvoErdEHRV$Cb#)&Jl$lov)F{Rr~c2%_vno=I-?ar*CAgW$r;FWnqUmORwykuoV7 z4B^-)yw!$7!g`-nS8ElJ@Bnrzzb3ZhcmIZu0MM-fp&JMLdE42% zi_tx9*>R8tY|3hul^`}oyyT#W%E>^*j~1bPq!guKPcK%25rR*9l9D7C&Fk>tYYULW z)9!KXfQ2H0?*os@wykpu`^UzuZ+E$Rw$<75z*b$dj7DvL?|}{nbfIE;6{*t2fNOVO zEr**xjL(ZD{nO$#LeCUZ;eJ1{%X6Md-3cdv7yT-C1mL@!Xy$aGN3SD0!YcPAWwoV0 zaLWqam&R>j6m^%}IcqPqfB5!Ae2tBeNS%8v1;xA5vK7$Y*(_^lg`2$(!Gge}km2jy z>BS+qh^XW-epYOdbaVPwB!l*H1;ZNU+{r+Z4{)$&y>XW}YYI=oVIXm(C1`iv_{zdm zjgT)v))Cg!FOZRNd>!l%|N7iq%EM}SxlvoyHsU8du(woI*yd3UP07Q ztA9T!DcWl*m{kXU*VsLxpakR&b0?xK0p16*JR%N9^=OdANz3t7oZ)A0d6`(=-=oOg zB`@6uEANcAC)E!`yh`WT5k{uC=G1_D``g}=n?tc2C{fCg?xywQjYS#xRO{Z~FsX+z znZ$jC>cXEv`Xqg((*;?DR9+u*8vAgG)@foHPJAe-eNly;b*Xi#9v3RUf?87#Gs8u^ zKb{W6(ChP@9UsubM{=TC8}9@6d&!)G9+9B@hBf!xiM0z$N6W(~oIBe+#~5sFWA8Qw zMI9rY!Ku$uO(6Hz8!8*~mDIH*9i5*((tlV6mJ}&KTM$5BSWN6s#BT8M?9OM8MkO#7 zTNI?tqjV)t*Yg!I#5O^odft+#5@p+*$abS!mB|J!)w`gL4ET(ne*Npt@#>Mm?>ae{ z<;pm~0%!|_wo7;Jh-?uRP;sPh`r8~)Jv7%41=~jTSE8iOzc{3LK~@GBmr9Q3$8FmQPBNnF)=1brfo!)xBJ zY-WGd(-2hOe2w3v`Ok|fW>tqQJra)ktbg!Az!L6DAiANx0`PZ6cx+e}wz(8D_}4rl z`ckaPx8ARz^ia)Z{U3MsqLiv};4ce}K9cZns3b~<46dOjHpi%O+F(()b!7rP)bRdH zS@biX5Gkx1MPame47<*_x3wr12REvd~$A(rq|e zmRp`E0hjH{Nlx|JH=P!r@Jssm245>U_jl-07>EJ9GH62cTYQj81bpKLw zDwKggRT`|HrXz$v>_$ZfE@pq_inz@l1)0M8}h=8Ts%JH zETDpMhBY=zGZ~YsW68^aMF=e1=yGEIl58VOsN2b0hWvvpao`MN%ho8T1A$ZOiYcGA zjs}4_k~MAamO{^3x5XC9b&3Yu54w3-g=7lGF@SWJZUZJ%Lp%?z@LN+H18^m4OBIa8 zuGP~iS`wLe4_K68i`T)2M@!!Z_?}r2&6Kx(y~MCx#;4sTrvAF=I{f{Rq!;OyzI}xa zfltT0eDu~d63u)?K$zQ9hp*KmcgccE-|<0bLzoGg=ub-nLC-oDKUsr9tjrD(RYenW z_a5?~|Fte~-)~^^NE84%lK=&9;k!q=-5-ZqurSgU*bx&AIzm)P4nq zi1&JWf89+T&VuQqk7Z;>@jeWOBWjbG3xm!i272g23QXifMFc17)*ElrXa>8xFnN;< ze>b^IQfkE-k=u(nXpW=VNkj&4REQiA)oQwzvE~y8>Vrc9J9Yc$EP5G15r4~~G9^|< zrgnLGYk&WqUleFQzqJFb!0A_n<&C+KXhrbb10xoe2L`wdOk@2c7h*uNlp41X2mT!0OeSS8zC-%fYrFVRM zrM=r?n$qU_$9T3*p_G0k5d$kCN#m{PA0B+ttHQ&7S1t@awbr)5Nu{UkrORiXNAVj5 z_eL(kk*@NdGcw+O=^bb#M608lDq~@Wts#rSuxKP-Se#bZ#@eXd! z^Djm%G^L{c3D_z1nM;Z;CrPa9Ije$0w-=V480 z(ERSuk?fo(ze;+2tTXeWh=M1Dy5l3g2Dij+;zuSVEpDlk@!j(Dwd}8`15G%?w-VDk z4)DXq*3#I_H2L@HH_M@Jxuu1;hFSuiEhSJ%N)+Ve>2?XSxCPcrZ#muoq{*o8vEQTU* zIK}cZ<$O;S%!>On($OTCRLy|e9foC2@!{ciS4MUkUsV0}Bw`|!RWBP1{ST<@UY{S6 zZ)yt}Q|(}Jh>#{Q=k1PX!N^obFszGLSa$FmfRDoSlq;y%Z==+sfci0 z=k@u{9hY0(19s!z)lyx!1({w&n=F11xuwk_vIUyY;2W=ul(5v2K|0;$*5F49fHTGaT6w)FOK_rd&O{f<{wIdjUXE6nKRwtbq=a*V$ceu4^P)5vgd!sD#D!20RJ7_s!G%Fhg;rAR15BY}BoejF>z;e>85fmMp>Zn?%x31} z$H&YE{P#tj7Rl+cCoIAN$C8=iRI%DgCw3ZMA3vEE+Ryqeb+A_V%E_Cf_tUr2gF8CN zWHQFBJ_o|mGAadCh=nnOfOz_2TK6p1kr-q>?xSq{lH8ih)!d$F;0ZL`BV8O07w9+ zloo5Lk#uWVa+bh}!xdxi!^BVc((6YL&pY2@*RW={u+KX{1Ya8ZO!;1Uvkd@%XCIf} z#D+#oNvTs2E%duS6;uu75HM^F(OK&Eju62F=-m9o`$GGTodPnJo0wF`OP3J$;!ez# zP;v!zg?>s-}L{cegPiHz@Uarj;R0u002ov JPDHLkV1gAA4qyNP diff --git a/packages/mcp-extension/icons/icon-32.png b/packages/mcp-extension/icons/icon-32.png deleted file mode 100644 index 1f9a8ccb89bdbccd6bdc1c7f945f72eed9b94017..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1258 zcmVvS_*I>m_KT&x^fBtGyo{vygB`pAML~lkiZCl zFf-D?$W*Ys3K9gyUC;dcrg?P$@B=7epk(@p9G}{ohz9_lo3_~Duup;mPzcN{s+i$0 zqdPq|IjJTrO_?Dj?R3X>5;^)fz?|lSZ%zYPWQA^zz&8{G2a_f+!%_+z8RG!Dq22SJ zDgKZgu>{}`qMN!0#$jWj0)0f4%?I;Y2#2`WLP9MJtimggcQyB%i4 zT!lV!9Kg1m?2*cruSa&Y?ul->e!~1UFDNu}r7j?}iM?hHlS1Fj?gVb=-qM|rGI0xz zNA!#F;|ha*))l>wrSrJ}RK8+{%Zy>2CslD>!x^=n9#fHn!{x1+tXOEC3HgWg)9Ra* z$JJLWhxy0sb}&NmJcg*73X}l2q2#d`i2(rcv8rI9I+de7Q_KPw-vb4eR8wXKE0FG) zkUwbq&kduHn!MFqR8oXDy{ z&C&tD^;-e0EBAfR)!e_#>)DI-2Wes9YS-X*24HXyz|8yDXPDUA|82iL4R9vW+6}Kh ze>5079)MZ_02-);iC0G)*X;DiaPXNsiCE3h!osI-yRonJ!kY5!1ws>*%&26E60!AO z6LQDaPd1`=ukEa_nWfMy%>X=Gv2%Y-VDyj&oE!HkW(?RC_Lts9RH$hcG>>*GeQEl~ zil~=ZJqATPA^GLWApWJP`ARts&E8@;(*Xc|H`*I6EnCx9tYdYS;3WVA5y4dqQ)sF& zI64iSq{uV_#w!Hg4Z?N^6is#ZDArLw* zE2FMO23bPs)EpYbtiU#)fkwy0I&2yVgZN-jKFdudbhrD*teZ!yQjj}`*z%h)<+kD5{ zfGbNrZLY1oIbW@Z0sw&O>T22XK&@w}3PGYkfpr4fq4usnWC$RR~H#Xhj^UO2PJoEe?L#Y&7d1z%`*vs9* zl-F{EXH&!X8vLo$PlQO7&|{l+WuxEy7iB4r#Ki|su+n#NePvg?>GGwEp_8Xz2%9gR9SlS?843zufP{cCp&U>Om+}L1Uc8V3!kF_V7;L_F zVJJo?@fer%VcQWUztm#A4)%4_|?47h(`$hI0ej4T_;MGJHuHW z(eq8`+;@P9&L%YgfIkRR(pEyCz$nYkI@?hX0E&UIW{kRGb`(Jxd(K;RY4GN^2G04a z+cuBw>)cCBe{%Ktk zX>K^Q;x#@{c6dGFTYNyNP~)@)G~?qz7SAstRSW7#c^pv{ z!oWw-{Xas?J~mf8Qul7q^hHgA1-)z34zy|Dvc+5(eZJBRo2ECA2-7A1{euv|*M`IV z!mW$uGd(XmZs)BA@F%{S`2@UXOuZB6Xd69Qb z8McrZKPA?+;RWzoFV(*#zkll-GVRO!S$1aBw6N*mzs_s`fM)$#gC%=svls#90SDr5 z!_&0lWZj1dnE+L5=-<=6-bZPNHzP{#NJCYFH}M*#aP{QTmf!juT||+JP0T37CqMLM z9(v0WZ#Hc_uRXo@p&L$0yCqE-iXNMF{}(*@GQ_N3OL|(OgMP-(Pg_P;vPlZ!C+m;A z|Fn;AT8^Z-rj7@*?i88V@^8A2uPn$oR_4t5Y}K&Kzo6YT9A3jI@6OJ_B#lkg7e7fh zC=(qV?rQ0~S-7@hk4rsF;FtwU5~tT3dH=fr@Z-{w9Xmt(jdO)}DI9e%vu3H+-s$yp z=aebiI7>kzX!K;Qa9qz{ipCrhea;qP@-=wWI3zE?H18$E{QThC{T)filMR$_s>)m9 zmY@F06DrZnlL1UAf31~%iV0=5)-G=-jKih(@b`uBiFLTmGaW;1n{f~}0F-U5E+$|* z3v7qxEg)pTsYhtlcq^V5Sp`CF^n51NHtlJc_5~>64gf%Z_jSK(S!p|n*OyA28!@Yy zH!!e-0fUGTQX07-11A=b#%U;97v`kr9VnaZV^W>|x zEBFyD3<*?>EGLtvegLopQ_bMpeFJgAQOx-0R<>>Tb2Se2a*Z=w!U@hFw`>Ho!iyXG zF=QrxFOa4TXFQzsCb7Q4fFDFlhKV%|M?v z)~z>H8i1HKA_c*3E362*aH?P|sgB{4(&=%m184_ndyVZQI_&j3OZfL8B9zyI9gmogLKU8{4w99d9D@p-f2UjQQ%+ z0^6?cZb;g79@s!K9@(~g3o{+pIiXSC#&)-|Y|ms6A&^~|HN8NX>Q#UGv&}Eo-k3|= zlztm6+giO9#D`fTEA(V{cgS7k58m3Wsn{TB1xz&AAJ6ykMj`{-fKQ$1c zbbC#S(0U)h+rcOUA@XqMW8fkI$1Kfnw0?N>_J3*(DW#;cGP8VZ%?ua9JoC&m&piKS Z{soKhKGhw*_GbV9002ovPDHLkV1gda&R_rl diff --git a/packages/mcp-extension/manifest.json b/packages/mcp-extension/manifest.json deleted file mode 100644 index ffc9e9d3fdbed..0000000000000 --- a/packages/mcp-extension/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "manifest_version": 3, - "name": "Playwright MCP Bridge", - "version": "0.0.36", - "description": "Share browser tabs with Playwright MCP server", - "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nMS2b0WCohjVHPGb8D9qAdkbIngDqoAjTeSccHJijgcONejge+OJxOQOMLu7b0ovt1c9BiEJa5JcpM+EHFVGL1vluBxK71zmBy1m2f9vZF3HG0LSCp7YRkum9rAIEthDwbkxx6XTvpmAY5rjFa/NON6b9Hlbo+8peUSkoOK7HTwYnnI36asZ9eUTiveIf+DMPLojW2UX33vDWG2UKvMVDewzclb4+uLxAYshY7Mx8we/b44xu+Anb/EBLKjOPk9Yh541xJ5Ozc8EiP/5yxOp9c/lRiYUHaRW+4r0HKZyFt0eZ52ti2iM4Nfk7jRXR7an3JPsUIf5deC/1cVM/+1ZQIDAQAB", - "permissions": [ - "debugger", - "activeTab", - "tabs", - "storage" - ], - "host_permissions": [ - "" - ], - "background": { - "service_worker": "lib/background.mjs", - "type": "module" - }, - "action": { - "default_title": "Playwright MCP Bridge", - "default_icon": { - "16": "icons/icon-16.png", - "32": "icons/icon-32.png", - "48": "icons/icon-48.png", - "128": "icons/icon-128.png" - } - }, - "icons": { - "16": "icons/icon-16.png", - "32": "icons/icon-32.png", - "48": "icons/icon-48.png", - "128": "icons/icon-128.png" - } -} diff --git a/packages/mcp-extension/package.json b/packages/mcp-extension/package.json deleted file mode 100644 index 89b75ebf3d7ea..0000000000000 --- a/packages/mcp-extension/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@playwright/mcp-extension", - "version": "0.0.36", - "description": "Playwright MCP Browser Extension", - "private": true, - "repository": { - "type": "git", - "url": "git+https://github.com/microsoft/playwright-mcp.git" - }, - "homepage": "https://playwright.dev", - "engines": { - "node": ">=18" - }, - "author": { - "name": "Microsoft Corporation" - }, - "license": "Apache-2.0", - "scripts": { - "build": "tsc --project . && tsc --project tsconfig.ui.json && vite build && vite build --config vite.sw.config.mts", - "watch": "tsc --watch --project . & tsc --watch --project tsconfig.ui.json & vite build --watch & vite build --watch --config vite.sw.config.mts", - "test": "playwright test", - "clean": "rm -rf dist" - }, - "devDependencies": { - "@types/chrome": "^0.0.315", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@vitejs/plugin-react": "^4.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^5.8.2", - "vite": "^5.0.0", - "vite-plugin-static-copy": "^3.1.1" - } -} diff --git a/packages/mcp-extension/src/background.ts b/packages/mcp-extension/src/background.ts deleted file mode 100644 index e74ebb80cdcff..0000000000000 --- a/packages/mcp-extension/src/background.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { RelayConnection, debugLog } from './relayConnection'; - -type PageMessage = { - type: 'connectToMCPRelay'; - mcpRelayUrl: string; -} | { - type: 'getTabs'; -} | { - type: 'connectToTab'; - tabId?: number; - windowId?: number; - mcpRelayUrl: string; -} | { - type: 'getConnectionStatus'; -} | { - type: 'disconnect'; -}; - -class TabShareExtension { - private _activeConnection: RelayConnection | undefined; - private _connectedTabId: number | null = null; - private _pendingTabSelection = new Map(); - - constructor() { - chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this)); - chrome.tabs.onUpdated.addListener(this._onTabUpdated.bind(this)); - chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this)); - chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); - chrome.action.onClicked.addListener(this._onActionClicked.bind(this)); - } - - // Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031 - private _onMessage(message: PageMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) { - switch (message.type) { - case 'connectToMCPRelay': - this._connectToRelay(sender.tab!.id!, message.mcpRelayUrl).then( - () => sendResponse({ success: true }), - (error: any) => sendResponse({ success: false, error: error.message })); - return true; - case 'getTabs': - this._getTabs().then( - tabs => sendResponse({ success: true, tabs, currentTabId: sender.tab?.id }), - (error: any) => sendResponse({ success: false, error: error.message })); - return true; - case 'connectToTab': - const tabId = message.tabId || sender.tab?.id!; - const windowId = message.windowId || sender.tab?.windowId!; - this._connectTab(sender.tab!.id!, tabId, windowId, message.mcpRelayUrl!).then( - () => sendResponse({ success: true }), - (error: any) => sendResponse({ success: false, error: error.message })); - return true; // Return true to indicate that the response will be sent asynchronously - case 'getConnectionStatus': - sendResponse({ - connectedTabId: this._connectedTabId - }); - return false; - case 'disconnect': - this._disconnect().then( - () => sendResponse({ success: true }), - (error: any) => sendResponse({ success: false, error: error.message })); - return true; - } - return false; - } - - private async _connectToRelay(selectorTabId: number, mcpRelayUrl: string): Promise { - try { - debugLog(`Connecting to relay at ${mcpRelayUrl}`); - const socket = new WebSocket(mcpRelayUrl); - await new Promise((resolve, reject) => { - socket.onopen = () => resolve(); - socket.onerror = () => reject(new Error('WebSocket error')); - setTimeout(() => reject(new Error('Connection timeout')), 5000); - }); - - const connection = new RelayConnection(socket); - connection.onclose = () => { - debugLog('Connection closed'); - this._pendingTabSelection.delete(selectorTabId); - // TODO: show error in the selector tab? - }; - this._pendingTabSelection.set(selectorTabId, { connection }); - debugLog(`Connected to MCP relay`); - } catch (error: any) { - const message = `Failed to connect to MCP relay: ${error.message}`; - debugLog(message); - throw new Error(message); - } - } - - private async _connectTab(selectorTabId: number, tabId: number, windowId: number, mcpRelayUrl: string): Promise { - try { - debugLog(`Connecting tab ${tabId} to relay at ${mcpRelayUrl}`); - try { - this._activeConnection?.close('Another connection is requested'); - } catch (error: any) { - debugLog(`Error closing active connection:`, error); - } - await this._setConnectedTabId(null); - - this._activeConnection = this._pendingTabSelection.get(selectorTabId)?.connection; - if (!this._activeConnection) - throw new Error('No active MCP relay connection'); - this._pendingTabSelection.delete(selectorTabId); - - this._activeConnection.setTabId(tabId); - this._activeConnection.onclose = () => { - debugLog('MCP connection closed'); - this._activeConnection = undefined; - void this._setConnectedTabId(null); - }; - - await Promise.all([ - this._setConnectedTabId(tabId), - chrome.tabs.update(tabId, { active: true }), - chrome.windows.update(windowId, { focused: true }), - ]); - debugLog(`Connected to MCP bridge`); - } catch (error: any) { - await this._setConnectedTabId(null); - debugLog(`Failed to connect tab ${tabId}:`, error.message); - throw error; - } - } - - private async _setConnectedTabId(tabId: number | null): Promise { - const oldTabId = this._connectedTabId; - this._connectedTabId = tabId; - if (oldTabId && oldTabId !== tabId) - await this._updateBadge(oldTabId, { text: '' }); - if (tabId) - await this._updateBadge(tabId, { text: '✓', color: '#4CAF50', title: 'Connected to MCP client' }); - } - - private async _updateBadge(tabId: number, { text, color, title }: { text: string; color?: string, title?: string }): Promise { - try { - await chrome.action.setBadgeText({ tabId, text }); - await chrome.action.setTitle({ tabId, title: title || '' }); - if (color) - await chrome.action.setBadgeBackgroundColor({ tabId, color }); - } catch (error: any) { - // Ignore errors as the tab may be closed already. - } - } - - private async _onTabRemoved(tabId: number): Promise { - const pendingConnection = this._pendingTabSelection.get(tabId)?.connection; - if (pendingConnection) { - this._pendingTabSelection.delete(tabId); - pendingConnection.close('Browser tab closed'); - return; - } - if (this._connectedTabId !== tabId) - return; - this._activeConnection?.close('Browser tab closed'); - this._activeConnection = undefined; - this._connectedTabId = null; - } - - private _onTabActivated(activeInfo: chrome.tabs.TabActiveInfo) { - for (const [tabId, pending] of this._pendingTabSelection) { - if (tabId === activeInfo.tabId) { - if (pending.timerId) { - clearTimeout(pending.timerId); - pending.timerId = undefined; - } - continue; - } - if (!pending.timerId) { - pending.timerId = setTimeout(() => { - const existed = this._pendingTabSelection.delete(tabId); - if (existed) { - pending.connection.close('Tab has been inactive for 5 seconds'); - chrome.tabs.sendMessage(tabId, { type: 'connectionTimeout' }); - } - }, 5000); - return; - } - } - } - - private _onTabUpdated(tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) { - if (this._connectedTabId === tabId) - void this._setConnectedTabId(tabId); - } - - private async _getTabs(): Promise { - const tabs = await chrome.tabs.query({}); - return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme))); - } - - private async _onActionClicked(): Promise { - await chrome.tabs.create({ - url: chrome.runtime.getURL('status.html'), - active: true - }); - } - - private async _disconnect(): Promise { - this._activeConnection?.close('User disconnected'); - this._activeConnection = undefined; - await this._setConnectedTabId(null); - } -} - -new TabShareExtension(); diff --git a/packages/mcp-extension/src/relayConnection.ts b/packages/mcp-extension/src/relayConnection.ts deleted file mode 100644 index b203af4222df5..0000000000000 --- a/packages/mcp-extension/src/relayConnection.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export function debugLog(...args: unknown[]): void { - const enabled = true; - if (enabled) { - // eslint-disable-next-line no-console - console.log('[Extension]', ...args); - } -} - -type ProtocolCommand = { - id: number; - method: string; - params?: any; -}; - -type ProtocolResponse = { - id?: number; - method?: string; - params?: any; - result?: any; - error?: string; -}; - -export class RelayConnection { - private _debuggee: chrome.debugger.Debuggee; - private _ws: WebSocket; - private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void; - private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void; - private _tabPromise: Promise; - private _tabPromiseResolve!: () => void; - private _closed = false; - - onclose?: () => void; - - constructor(ws: WebSocket) { - this._debuggee = { }; - this._tabPromise = new Promise(resolve => this._tabPromiseResolve = resolve); - this._ws = ws; - this._ws.onmessage = this._onMessage.bind(this); - this._ws.onclose = () => this._onClose(); - // Store listeners for cleanup - this._eventListener = this._onDebuggerEvent.bind(this); - this._detachListener = this._onDebuggerDetach.bind(this); - chrome.debugger.onEvent.addListener(this._eventListener); - chrome.debugger.onDetach.addListener(this._detachListener); - } - - // Either setTabId or close is called after creating the connection. - setTabId(tabId: number): void { - this._debuggee = { tabId }; - this._tabPromiseResolve(); - } - - close(message: string): void { - this._ws.close(1000, message); - // ws.onclose is called asynchronously, so we call it here to avoid forwarding - // CDP events to the closed connection. - this._onClose(); - } - - private _onClose() { - if (this._closed) - return; - this._closed = true; - chrome.debugger.onEvent.removeListener(this._eventListener); - chrome.debugger.onDetach.removeListener(this._detachListener); - chrome.debugger.detach(this._debuggee).catch(() => {}); - this.onclose?.(); - } - - private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void { - if (source.tabId !== this._debuggee.tabId) - return; - debugLog('Forwarding CDP event:', method, params); - const sessionId = source.sessionId; - this._sendMessage({ - method: 'forwardCDPEvent', - params: { - sessionId, - method, - params, - }, - }); - } - - private _onDebuggerDetach(source: chrome.debugger.Debuggee, reason: string): void { - if (source.tabId !== this._debuggee.tabId) - return; - this.close(`Debugger detached: ${reason}`); - this._debuggee = { }; - } - - private _onMessage(event: MessageEvent): void { - this._onMessageAsync(event).catch(e => debugLog('Error handling message:', e)); - } - - private async _onMessageAsync(event: MessageEvent): Promise { - let message: ProtocolCommand; - try { - message = JSON.parse(event.data); - } catch (error: any) { - debugLog('Error parsing message:', error); - this._sendError(-32700, `Error parsing message: ${error.message}`); - return; - } - - debugLog('Received message:', message); - - const response: ProtocolResponse = { - id: message.id, - }; - try { - response.result = await this._handleCommand(message); - } catch (error: any) { - debugLog('Error handling command:', error); - response.error = error.message; - } - debugLog('Sending response:', response); - this._sendMessage(response); - } - - private async _handleCommand(message: ProtocolCommand): Promise { - if (message.method === 'attachToTab') { - await this._tabPromise; - debugLog('Attaching debugger to tab:', this._debuggee); - await chrome.debugger.attach(this._debuggee, '1.3'); - const result: any = await chrome.debugger.sendCommand(this._debuggee, 'Target.getTargetInfo'); - return { - targetInfo: result?.targetInfo, - }; - } - if (!this._debuggee.tabId) - throw new Error('No tab is connected. Please go to the Playwright MCP extension and select the tab you want to connect to.'); - if (message.method === 'forwardCDPCommand') { - const { sessionId, method, params } = message.params; - debugLog('CDP command:', method, params); - const debuggerSession: chrome.debugger.DebuggerSession = { - ...this._debuggee, - sessionId, - }; - // Forward CDP command to chrome.debugger - return await chrome.debugger.sendCommand( - debuggerSession, - method, - params - ); - } - } - - private _sendError(code: number, message: string): void { - this._sendMessage({ - error: { - code, - message, - }, - }); - } - - private _sendMessage(message: any): void { - if (this._ws.readyState === WebSocket.OPEN) - this._ws.send(JSON.stringify(message)); - } -} diff --git a/packages/mcp-extension/src/ui/connect.css b/packages/mcp-extension/src/ui/connect.css deleted file mode 100644 index 60945d4d4bd09..0000000000000 --- a/packages/mcp-extension/src/ui/connect.css +++ /dev/null @@ -1,206 +0,0 @@ -/* - Copyright (c) Microsoft Corporation. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -body { - margin: 0; - padding: 0; -} - -/* Base styles */ -.app-container { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif; - background-color: #ffffff; - color: #1f2328; - margin: 0; - padding: 16px; - min-height: 100vh; - font-size: 14px; -} - -.content-wrapper { - max-width: 600px; - margin: 0 auto; -} - -/* Status Banner */ -.status-container { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 16px; - padding-right: 12px; -} - -.status-banner { - padding: 12px; - font-size: 14px; - font-weight: 500; - display: flex; - align-items: center; - gap: 8px; - flex: 1; -} - -.status-banner.connected { - color: #1f2328; -} - -.status-banner.connected::before { - content: "\2705"; - margin-right: 8px; -} - -.status-banner.error { - color: #1f2328; -} - -.status-banner.error::before { - content: "\274C"; - margin-right: 8px; -} - -/* Buttons */ -.button-container { - margin-bottom: 16px; - display: flex; - justify-content: flex-end; - padding-right: 12px; -} - -.button { - padding: 8px 16px; - border-radius: 6px; - border: none; - font-size: 14px; - font-weight: 500; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - text-decoration: none; - margin-right: 8px; - min-width: 90px; -} - -.button.primary { - background-color: #f8f9fa; - color: #3c4043; - border: 1px solid #dadce0; -} - -.button.primary:hover { - background-color: #f1f3f4; - border-color: #dadce0; - box-shadow: 0 1px 2px 0 rgba(60,64,67,.1); -} - -.button.default { - background-color: #f6f8fa; - color: #24292f; -} - -.button.default:hover { - background-color: #f3f4f6; -} - -.button.reject { - background-color: #da3633; - color: #ffffff; - border: 1px solid #da3633; -} - -.button.reject:hover { - background-color: #c73836; - border-color: #c73836; -} - -/* Tab selection */ -.tab-section-title { - padding-left: 12px; - font-size: 12px; - font-weight: 400; - margin-bottom: 12px; - color: #656d76; -} - -.tab-item { - display: flex; - align-items: center; - padding: 12px; - margin-bottom: 8px; - background-color: #ffffff; - cursor: pointer; - border-radius: 6px; - transition: background-color 0.2s ease; -} - -.tab-item:hover { - background-color: #f8f9fa; -} - -.tab-item.selected { - background-color: #f6f8fa; -} - -.tab-item.disabled { - cursor: not-allowed; - opacity: 0.5; -} - -.tab-radio { - margin-right: 12px; - flex-shrink: 0; -} - -.tab-favicon { - width: 16px; - height: 16px; - margin-right: 8px; - flex-shrink: 0; -} - -.tab-content { - flex: 1; - min-width: 0; -} - -.tab-title { - font-weight: 500; - color: #1f2328; - margin-bottom: 2px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.tab-url { - font-size: 12px; - color: #656d76; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Link-style button */ -.link-button { - background: none; - border: none; - color: #0066cc; - text-decoration: underline; - cursor: pointer; - padding: 0; - font: inherit; -} \ No newline at end of file diff --git a/packages/mcp-extension/src/ui/connect.html b/packages/mcp-extension/src/ui/connect.html deleted file mode 100644 index 3f20e4b36010e..0000000000000 --- a/packages/mcp-extension/src/ui/connect.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Playwright MCP extension - - - - - - -
- - - \ No newline at end of file diff --git a/packages/mcp-extension/src/ui/connect.tsx b/packages/mcp-extension/src/ui/connect.tsx deleted file mode 100644 index 153017b5bbf95..0000000000000 --- a/packages/mcp-extension/src/ui/connect.tsx +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useState, useEffect, useCallback } from 'react'; -import { createRoot } from 'react-dom/client'; -import { Button, TabItem } from './tabItem'; -import type { TabInfo } from './tabItem'; - -type Status = - | { type: 'connecting'; message: string } - | { type: 'connected'; message: string } - | { type: 'error'; message: string } - | { type: 'error'; versionMismatch: { extensionVersion: string; } }; - -const SUPPORTED_PROTOCOL_VERSION = 1; - -const ConnectApp: React.FC = () => { - const [tabs, setTabs] = useState([]); - const [status, setStatus] = useState(null); - const [showButtons, setShowButtons] = useState(true); - const [showTabList, setShowTabList] = useState(true); - const [clientInfo, setClientInfo] = useState('unknown'); - const [mcpRelayUrl, setMcpRelayUrl] = useState(''); - const [newTab, setNewTab] = useState(false); - - useEffect(() => { - const params = new URLSearchParams(window.location.search); - const relayUrl = params.get('mcpRelayUrl'); - - if (!relayUrl) { - setShowButtons(false); - setStatus({ type: 'error', message: 'Missing mcpRelayUrl parameter in URL.' }); - return; - } - - setMcpRelayUrl(relayUrl); - - try { - const client = JSON.parse(params.get('client') || '{}'); - const info = `${client.name}/${client.version}`; - setClientInfo(info); - setStatus({ - type: 'connecting', - message: `🎭 Playwright MCP started from "${info}" is trying to connect. Do you want to continue?` - }); - } catch (e) { - setStatus({ type: 'error', message: 'Failed to parse client version.' }); - return; - } - - const parsedVersion = parseInt(params.get('protocolVersion') ?? '', 10); - const requiredVersion = isNaN(parsedVersion) ? 1 : parsedVersion; - if (requiredVersion > SUPPORTED_PROTOCOL_VERSION) { - const extensionVersion = chrome.runtime.getManifest().version; - setShowButtons(false); - setShowTabList(false); - setStatus({ - type: 'error', - versionMismatch: { - extensionVersion, - } - }); - return; - } - - void connectToMCPRelay(relayUrl); - - // If this is a browser_navigate command, hide the tab list and show simple allow/reject - if (params.get('newTab') === 'true') { - setNewTab(true); - setShowTabList(false); - } else { - void loadTabs(); - } - }, []); - - const handleReject = useCallback((message: string) => { - setShowButtons(false); - setShowTabList(false); - setStatus({ type: 'error', message }); - }, []); - - const connectToMCPRelay = useCallback(async (mcpRelayUrl: string) => { - - const response = await chrome.runtime.sendMessage({ type: 'connectToMCPRelay', mcpRelayUrl }); - if (!response.success) - handleReject(response.error); - }, [handleReject]); - - const loadTabs = useCallback(async () => { - const response = await chrome.runtime.sendMessage({ type: 'getTabs' }); - if (response.success) - setTabs(response.tabs); - else - setStatus({ type: 'error', message: 'Failed to load tabs: ' + response.error }); - }, []); - - const handleConnectToTab = useCallback(async (tab?: TabInfo) => { - setShowButtons(false); - setShowTabList(false); - - try { - const response = await chrome.runtime.sendMessage({ - type: 'connectToTab', - mcpRelayUrl, - tabId: tab?.id, - windowId: tab?.windowId, - }); - - if (response?.success) { - setStatus({ type: 'connected', message: `MCP client "${clientInfo}" connected.` }); - } else { - setStatus({ - type: 'error', - message: response?.error || `MCP client "${clientInfo}" failed to connect.` - }); - } - } catch (e) { - setStatus({ - type: 'error', - message: `MCP client "${clientInfo}" failed to connect: ${e}` - }); - } - }, [clientInfo, mcpRelayUrl]); - - useEffect(() => { - const listener = (message: any) => { - if (message.type === 'connectionTimeout') - handleReject('Connection timed out.'); - }; - chrome.runtime.onMessage.addListener(listener); - return () => { - chrome.runtime.onMessage.removeListener(listener); - }; - }, [handleReject]); - - return ( -
-
- {status && ( -
- - {showButtons && ( -
- {newTab ? ( - <> - - - - ) : ( - - )} -
- )} -
- )} - - {showTabList && ( -
-
- Select page to expose to MCP server: -
-
- {tabs.map(tab => ( - handleConnectToTab(tab)}> - Connect - - } - /> - ))} -
-
- )} -
-
- ); -}; - -const VersionMismatchError: React.FC<{ extensionVersion: string }> = ({ extensionVersion }) => { - const readmeUrl = 'https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md'; - const latestReleaseUrl = 'https://github.com/microsoft/playwright-mcp/releases/latest'; - return ( -
- Playwright MCP version trying to connect requires newer extension version (current version: {extensionVersion}).{' '} - Click here to download latest version of the extension, then drag and drop it into the Chrome Extensions page.{' '} - See installation instructions for more details. -
- ); -}; - -const StatusBanner: React.FC<{ status: Status }> = ({ status }) => { - return ( -
- {'versionMismatch' in status ? ( - - ) : ( - status.message - )} -
- ); -}; - -// Initialize the React app -const container = document.getElementById('root'); -if (container) { - const root = createRoot(container); - root.render(); -} diff --git a/packages/mcp-extension/src/ui/status.html b/packages/mcp-extension/src/ui/status.html deleted file mode 100644 index ccc1f04aa91b9..0000000000000 --- a/packages/mcp-extension/src/ui/status.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Playwright MCP Bridge Status - - - -
- - - \ No newline at end of file diff --git a/packages/mcp-extension/src/ui/status.tsx b/packages/mcp-extension/src/ui/status.tsx deleted file mode 100644 index f8de78880707d..0000000000000 --- a/packages/mcp-extension/src/ui/status.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useState, useEffect } from 'react'; -import { createRoot } from 'react-dom/client'; -import { Button, TabItem } from './tabItem'; - -import type { TabInfo } from './tabItem'; - -interface ConnectionStatus { - isConnected: boolean; - connectedTabId: number | null; - connectedTab?: TabInfo; -} - -const StatusApp: React.FC = () => { - const [status, setStatus] = useState({ - isConnected: false, - connectedTabId: null - }); - - useEffect(() => { - void loadStatus(); - }, []); - - const loadStatus = async () => { - // Get current connection status from background script - const { connectedTabId } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' }); - if (connectedTabId) { - const tab = await chrome.tabs.get(connectedTabId); - setStatus({ - isConnected: true, - connectedTabId, - connectedTab: { - id: tab.id!, - windowId: tab.windowId!, - title: tab.title!, - url: tab.url!, - favIconUrl: tab.favIconUrl - } - }); - } else { - setStatus({ - isConnected: false, - connectedTabId: null - }); - } - }; - - const openConnectedTab = async () => { - if (!status.connectedTabId) - return; - await chrome.tabs.update(status.connectedTabId, { active: true }); - window.close(); - }; - - const disconnect = async () => { - await chrome.runtime.sendMessage({ type: 'disconnect' }); - window.close(); - }; - - return ( -
-
- {status.isConnected && status.connectedTab ? ( -
-
- Page with connected MCP client: -
-
- - Disconnect - - } - onClick={openConnectedTab} - /> -
-
- ) : ( -
- No MCP clients are currently connected. -
- )} -
-
- ); -}; - -// Initialize the React app -const container = document.getElementById('root'); -if (container) { - const root = createRoot(container); - root.render(); -} diff --git a/packages/mcp-extension/src/ui/tabItem.tsx b/packages/mcp-extension/src/ui/tabItem.tsx deleted file mode 100644 index 148374292f5b9..0000000000000 --- a/packages/mcp-extension/src/ui/tabItem.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; - -export interface TabInfo { - id: number; - windowId: number; - title: string; - url: string; - favIconUrl?: string; -} - -export const Button: React.FC<{ variant: 'primary' | 'default' | 'reject'; onClick: () => void; children: React.ReactNode }> = ({ - variant, - onClick, - children -}) => { - return ( - - ); -}; - - -export interface TabItemProps { - tab: TabInfo; - onClick?: () => void; - button?: React.ReactNode; -} - -export const TabItem: React.FC = ({ - tab, - onClick, - button -}) => { - return ( -
- '} - alt='' - className='tab-favicon' - /> -
-
- {tab.title || 'Untitled'} -
-
{tab.url}
-
- {button} -
- ); -}; diff --git a/packages/mcp-extension/src/ui/tsconfig.json b/packages/mcp-extension/src/ui/tsconfig.json deleted file mode 100644 index 77839b62d222b..0000000000000 --- a/packages/mcp-extension/src/ui/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -// Help VSCode to find right tsconfig file. -{ - "extends": "../../tsconfig.ui.json" -} diff --git a/packages/mcp-extension/tsconfig.json b/packages/mcp-extension/tsconfig.json deleted file mode 100644 index 9c22b0bca0018..0000000000000 --- a/packages/mcp-extension/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "esModuleInterop": true, - "moduleResolution": "node", - "strict": true, - "module": "ESNext", - "rootDir": "src", - "outDir": "./dist/lib", - "resolveJsonModule": true, - "types": ["chrome"], - "jsx": "react-jsx", - "jsxImportSource": "react", - "noEmit": true - }, - "include": [ - "src", - ], - "exclude": [ - "src/ui", - ] -} diff --git a/packages/mcp-extension/tsconfig.ui.json b/packages/mcp-extension/tsconfig.ui.json deleted file mode 100644 index f62dd8a3f74c3..0000000000000 --- a/packages/mcp-extension/tsconfig.ui.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "esModuleInterop": true, - "moduleResolution": "node", - "strict": true, - "module": "ESNext", - "rootDir": "src", - "outDir": "./lib", - "resolveJsonModule": true, - "types": ["chrome"], - "jsx": "react-jsx", - "jsxImportSource": "react", - "noEmit": true, - }, - "include": [ - "src/ui", - ], -} diff --git a/packages/mcp-extension/vite.config.mts b/packages/mcp-extension/vite.config.mts deleted file mode 100644 index 89ec56c6898a2..0000000000000 --- a/packages/mcp-extension/vite.config.mts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { resolve } from 'path'; -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import { viteStaticCopy } from 'vite-plugin-static-copy'; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [ - react(), - viteStaticCopy({ - targets: [ - { - src: '../../icons/*', - dest: 'icons' - }, - { - src: '../../manifest.json', - dest: '.' - } - ] - }) - ], - root: resolve(__dirname, 'src/ui'), - build: { - outDir: resolve(__dirname, 'dist/'), - emptyOutDir: false, - minify: false, - rollupOptions: { - input: ['src/ui/connect.html', 'src/ui/status.html'], - output: { - manualChunks: undefined, - entryFileNames: 'lib/ui/[name].js', - chunkFileNames: 'lib/ui/[name].js', - assetFileNames: 'lib/ui/[name].[ext]' - } - } - } -}); diff --git a/packages/mcp-extension/vite.sw.config.mts b/packages/mcp-extension/vite.sw.config.mts deleted file mode 100644 index a383e4b4a50e9..0000000000000 --- a/packages/mcp-extension/vite.sw.config.mts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { resolve } from 'path'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - build: { - lib: { - entry: resolve(__dirname, 'src/background.ts'), - fileName: 'lib/background', - formats: ['es'] - }, - outDir: 'dist', - emptyOutDir: false, - minify: false - } -}); diff --git a/tests/mcp/extension.spec.ts b/tests/mcp/extension.spec.ts deleted file mode 100644 index 10948baa60f01..0000000000000 --- a/tests/mcp/extension.spec.ts +++ /dev/null @@ -1,306 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; -import { chromium } from 'playwright'; -import { test as base, expect } from './fixtures'; - -import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import type { BrowserContext } from 'playwright'; -import type { StartClient } from './fixtures'; - -type BrowserWithExtension = { - userDataDir: string; - launch: (mode?: 'disable-extension') => Promise; -}; - -type TestFixtures = { - browserWithExtension: BrowserWithExtension, - pathToExtension: string, - useShortConnectionTimeout: (timeoutMs: number) => void - overrideProtocolVersion: (version: number) => void -}; - -const test = base.extend({ - pathToExtension: async ({}, use) => { - await use(path.resolve(__dirname, '../../packages/mcp-extension/dist')); - }, - - browserWithExtension: async ({ mcpBrowser, pathToExtension }, use, testInfo) => { - // The flags no longer work in Chrome since - // https://chromium.googlesource.com/chromium/src/+/290ed8046692651ce76088914750cb659b65fb17%5E%21/chrome/browser/extensions/extension_service.cc?pli=1# - test.skip('chromium' !== mcpBrowser, '--load-extension is not supported for official builds of Chromium'); - - let browserContext: BrowserContext | undefined; - const userDataDir = testInfo.outputPath('extension-user-data-dir'); - await use({ - userDataDir, - launch: async (mode?: 'disable-extension') => { - browserContext = await chromium.launchPersistentContext(userDataDir, { - channel: mcpBrowser, - // Opening the browser singleton only works in headed. - headless: false, - // Automation disables singleton browser process behavior, which is necessary for the extension. - ignoreDefaultArgs: ['--enable-automation'], - args: mode === 'disable-extension' ? [] : [ - `--disable-extensions-except=${pathToExtension}`, - `--load-extension=${pathToExtension}`, - ], - }); - - // for manifest v3: - let [serviceWorker] = browserContext.serviceWorkers(); - if (!serviceWorker) - serviceWorker = await browserContext.waitForEvent('serviceworker'); - - return browserContext; - } - }); - await browserContext?.close(); - }, - - useShortConnectionTimeout: async ({}, use) => { - await use((timeoutMs: number) => { - process.env.PWMCP_TEST_CONNECTION_TIMEOUT = timeoutMs.toString(); - }); - process.env.PWMCP_TEST_CONNECTION_TIMEOUT = undefined; - }, - - overrideProtocolVersion: async ({}, use) => { - await use((version: number) => { - process.env.PWMCP_TEST_PROTOCOL_VERSION = version.toString(); - }); - process.env.PWMCP_TEST_PROTOCOL_VERSION = undefined; - } -}); - -async function startAndCallConnectTool(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise { - const { client } = await startClient({ - args: [`--connect-tool`], - config: { - browser: { - userDataDir: browserWithExtension.userDataDir, - } - }, - }); - - expect(await client.callTool({ - name: 'browser_connect', - arguments: { - name: 'extension' - } - })).toHaveResponse({ - result: 'Successfully changed connection method.', - }); - - return client; -} - -async function startWithExtensionFlag(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise { - const { client } = await startClient({ - args: [`--extension`], - config: { - browser: { - userDataDir: browserWithExtension.userDataDir, - } - }, - }); - return client; -} - -const testWithOldExtensionVersion = test.extend({ - pathToExtension: async ({}, use, testInfo) => { - const extensionDir = testInfo.outputPath('extension'); - const oldPath = path.resolve(__dirname, '../../packages/mcp-extension/dist'); - - await fs.promises.cp(oldPath, extensionDir, { recursive: true }); - const manifestPath = path.join(extensionDir, 'manifest.json'); - const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8')); - manifest.version = '0.0.1'; - await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n'); - - await use(extensionDir); - }, -}); - -for (const [mode, startClientMethod] of [ - ['connect-tool', startAndCallConnectTool], - ['extension-flag', startWithExtensionFlag], -] as const) { - - test(`navigate with extension (${mode})`, async ({ browserWithExtension, startClient, server }) => { - const browserContext = await browserWithExtension.launch(); - - const client = await startClientMethod(browserWithExtension, startClient); - - const confirmationPagePromise = browserContext.waitForEvent('page', page => { - return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); - }); - - const navigateResponse = client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const selectorPage = await confirmationPagePromise; - // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector - await selectorPage.getByRole('button', { name: 'Allow' }).click(); - - expect(await navigateResponse).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); - }); - - test(`snapshot of an existing page (${mode})`, async ({ browserWithExtension, startClient, server }) => { - const browserContext = await browserWithExtension.launch(); - - const page = await browserContext.newPage(); - await page.goto(server.HELLO_WORLD); - - // Another empty page. - await browserContext.newPage(); - expect(browserContext.pages()).toHaveLength(3); - - const client = await startClientMethod(browserWithExtension, startClient); - expect(browserContext.pages()).toHaveLength(3); - - const confirmationPagePromise = browserContext.waitForEvent('page', page => { - return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); - }); - - const navigateResponse = client.callTool({ - name: 'browser_snapshot', - arguments: { }, - }); - - const selectorPage = await confirmationPagePromise; - expect(browserContext.pages()).toHaveLength(4); - - await selectorPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click(); - - expect(await navigateResponse).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); - - expect(browserContext.pages()).toHaveLength(4); - }); - - test(`extension not installed timeout (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => { - useShortConnectionTimeout(100); - - const browserContext = await browserWithExtension.launch(); - - const client = await startClientMethod(browserWithExtension, startClient); - - const confirmationPagePromise = browserContext.waitForEvent('page', page => { - return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - result: expect.stringContaining('Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed.'), - isError: true, - }); - - await confirmationPagePromise; - }); - - testWithOldExtensionVersion(`works with old extension version (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => { - useShortConnectionTimeout(500); - - // Prelaunch the browser, so that it is properly closed after the test. - const browserContext = await browserWithExtension.launch(); - - const client = await startClientMethod(browserWithExtension, startClient); - - const confirmationPagePromise = browserContext.waitForEvent('page', page => { - return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); - }); - - const navigateResponse = client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const selectorPage = await confirmationPagePromise; - // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector - await selectorPage.getByRole('button', { name: 'Allow' }).click(); - - expect(await navigateResponse).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); - }); - - test(`extension needs update (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout, overrideProtocolVersion }) => { - useShortConnectionTimeout(500); - overrideProtocolVersion(1000); - - // Prelaunch the browser, so that it is properly closed after the test. - const browserContext = await browserWithExtension.launch(); - - const client = await startClientMethod(browserWithExtension, startClient); - - const confirmationPagePromise = browserContext.waitForEvent('page', page => { - return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html'); - }); - - const navigateResponse = client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const confirmationPage = await confirmationPagePromise; - await expect(confirmationPage.locator('.status-banner')).toContainText(`Playwright MCP version trying to connect requires newer extension version`); - - expect(await navigateResponse).toHaveResponse({ - result: expect.stringContaining('Extension connection timeout.'), - isError: true, - }); - }); - -} - -test(`custom executablePath`, async ({ startClient, server, useShortConnectionTimeout }) => { - useShortConnectionTimeout(1000); - - const executablePath = test.info().outputPath('echo.sh'); - await fs.promises.writeFile(executablePath, '#!/bin/bash\necho "Custom exec args: $@" > "$(dirname "$0")/output.txt"', { mode: 0o755 }); - - const { client } = await startClient({ - args: [`--extension`], - config: { - browser: { - launchOptions: { - executablePath, - }, - } - }, - }); - - const navigateResponse = await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - timeout: 1000, - }); - expect(await navigateResponse).toHaveResponse({ - result: expect.stringContaining('Extension connection timeout.'), - isError: true, - }); - expect(await fs.promises.readFile(test.info().outputPath('output.txt'), 'utf8')).toContain('Custom exec args: chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html?'); -}); diff --git a/tsconfig.json b/tsconfig.json index 9f66b9c2122de..1fb09a13a00b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,6 +36,5 @@ "exclude": [ "packages/*/lib", "packages/html-reporter/", - "packages/mcp-extension/", ] } From cc69e07738a810476816642649b9d79533147db7 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 4 Sep 2025 15:53:12 -0700 Subject: [PATCH 083/329] feat(mcp): allow clicking with modifiers (#37315) --- .../playwright/src/mcp/browser/codegen.ts | 6 ++- .../src/mcp/browser/tools/snapshot.ts | 18 ++++--- tests/mcp/click.spec.ts | 54 +++++++++++++++++++ 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/packages/playwright/src/mcp/browser/codegen.ts b/packages/playwright/src/mcp/browser/codegen.ts index a3bc8d0e02b27..a67700fc4f2e9 100644 --- a/packages/playwright/src/mcp/browser/codegen.ts +++ b/packages/playwright/src/mcp/browser/codegen.ts @@ -35,7 +35,7 @@ export function quote(text: string) { return escapeWithQuotes(text, '\''); } -export function formatObject(value: any, indent = ' '): string { +export function formatObject(value: any, indent = ' ', mode: 'multiline' | 'oneline' = 'multiline'): string { if (typeof value === 'string') return quote(value); if (Array.isArray(value)) @@ -47,7 +47,9 @@ export function formatObject(value: any, indent = ' '): string { const tokens: string[] = []; for (const key of keys) tokens.push(`${key}: ${formatObject(value[key])}`); - return `{\n${indent}${tokens.join(`,\n${indent}`)}\n}`; + if (mode === 'multiline') + return `{\n${tokens.join(`,\n${indent}`)}\n}`; + return `{ ${tokens.join(', ')} }`; } return String(value); } diff --git a/packages/playwright/src/mcp/browser/tools/snapshot.ts b/packages/playwright/src/mcp/browser/tools/snapshot.ts index 03c6852e14813..5f2cb790d003d 100644 --- a/packages/playwright/src/mcp/browser/tools/snapshot.ts +++ b/packages/playwright/src/mcp/browser/tools/snapshot.ts @@ -43,6 +43,7 @@ export const elementSchema = z.object({ const clickSchema = elementSchema.extend({ doubleClick: z.boolean().optional().describe('Whether to perform a double click instead of a single click'), button: z.enum(['left', 'right', 'middle']).optional().describe('Button to click, defaults to left'), + modifiers: z.array(z.enum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift'])).optional().describe('Modifier keys to press'), }); const click = defineTabTool({ @@ -59,20 +60,23 @@ const click = defineTabTool({ response.setIncludeSnapshot(); const locator = await tab.refLocator(params); - const button = params.button; - const buttonAttr = button ? `{ button: '${button}' }` : ''; + const options = { + button: params.button, + modifiers: params.modifiers, + }; + const formatted = javascript.formatObject(options, ' ', 'oneline'); + const optionsAttr = formatted !== '{}' ? formatted : ''; if (params.doubleClick) - response.addCode(`await page.${await generateLocator(locator)}.dblclick(${buttonAttr});`); + response.addCode(`await page.${await generateLocator(locator)}.dblclick(${optionsAttr});`); else - response.addCode(`await page.${await generateLocator(locator)}.click(${buttonAttr});`); - + response.addCode(`await page.${await generateLocator(locator)}.click(${optionsAttr});`); await tab.waitForCompletion(async () => { if (params.doubleClick) - await locator.dblclick({ button }); + await locator.dblclick(options); else - await locator.click({ button }); + await locator.click(options); }); }, }); diff --git a/tests/mcp/click.spec.ts b/tests/mcp/click.spec.ts index 53db23152dccd..f7de50e3e701a 100644 --- a/tests/mcp/click.spec.ts +++ b/tests/mcp/click.spec.ts @@ -97,3 +97,57 @@ test('browser_click (right)', async ({ client, server }) => { pageState: expect.stringContaining(`- button "Right clicked"`), }); }); + +test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => { + server.setContent('/', ` + Title + +
+ + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Submit button', + ref: 'e2', + modifiers: ['Control'], + }, + })).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Control'] });`, + pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:true metaKey:false shiftKey:false altKey:false`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Submit button', + ref: 'e2', + modifiers: ['Shift'], + }, + })).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Shift'] });`, + pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:false`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Submit button', + ref: 'e2', + modifiers: ['Shift', 'Alt'], + }, + })).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Shift', 'Alt'] });`, + pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:true`), + }); +}); From f8f3e07efb4ea56bf77e90cf90bd6af754a6d2c3 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 4 Sep 2025 21:48:17 -0700 Subject: [PATCH 084/329] chore: expose npx playwright run-mpc-server (#37316) --- packages/playwright/src/mcp/program.ts | 166 ++++++++++++------------- packages/playwright/src/program.ts | 16 ++- tests/mcp/cdp.spec.ts | 2 +- tests/mcp/click.spec.ts | 24 ++-- tests/mcp/fixtures.ts | 4 +- tests/mcp/http.spec.ts | 2 +- tests/mcp/sse.spec.ts | 2 +- 7 files changed, 112 insertions(+), 104 deletions(-) diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index bc01c31806a02..aa9904e16b60f 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { program, ProgramOption } from 'playwright-core/lib/utilsBundle'; +import { ProgramOption } from 'playwright-core/lib/utilsBundle'; import * as mcpServer from './sdk/server'; import { commaSeparatedList, dotenvFileLoader, headerParser, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; import { Context } from './browser/context'; @@ -23,98 +23,96 @@ import { ProxyBackend } from './sdk/proxyBackend'; import { BrowserServerBackend } from './browser/browserServerBackend'; import { ExtensionContextFactory } from './extension/extensionContextFactory'; +import type { Command } from 'playwright-core/lib/utilsBundle'; import type { MCPProvider } from './sdk/proxyBackend'; -const packageJSON = require('../../package.json'); +export function decorateCommand(command: Command, version: string) { + command.option('--allowed-origins ', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList) + .option('--blocked-origins ', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList) + .option('--block-service-workers', 'block service workers') + .option('--browser ', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.') + .option('--caps ', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList) + .option('--cdp-endpoint ', 'CDP endpoint to connect to.') + .option('--cdp-header ', 'CDP headers to send with the connect request, multiple can be specified.', headerParser) + .option('--config ', 'path to the configuration file.') + .option('--device ', 'device to emulate, for example: "iPhone 15"') + .option('--executable-path ', 'path to the browser executable.') + .option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.') + .option('--headless', 'run browser in headless mode, headed by default') + .option('--host ', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.') + .option('--ignore-https-errors', 'ignore https errors') + .option('--isolated', 'keep the browser profile in memory, do not save it to disk.') + .option('--image-responses ', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".') + .option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.') + .option('--output-dir ', 'path to the directory for output files.') + .option('--port ', 'port to listen on for SSE transport.') + .option('--proxy-bypass ', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"') + .option('--proxy-server ', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"') + .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.') + .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.') + .option('--secrets ', 'path to a file containing secrets in the dotenv format', dotenvFileLoader) + .option('--storage-state ', 'path to the storage state file for isolated sessions.') + .option('--user-agent ', 'specify user agent string') + .option('--user-data-dir ', 'path to the user data directory. If not specified, a temporary directory will be created.') + .option('--viewport-size ', 'specify browser viewport size in pixels, for example "1280, 720"') + .addOption(new ProgramOption('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp()) + .addOption(new ProgramOption('--vision', 'Legacy option, use --caps=vision instead').hideHelp()) + .action(async options => { + setupExitWatchdog(); -program - .version('Version ' + packageJSON.version) - .name('Playwright MCP') - .option('--allowed-origins ', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList) - .option('--blocked-origins ', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList) - .option('--block-service-workers', 'block service workers') - .option('--browser ', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.') - .option('--caps ', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList) - .option('--cdp-endpoint ', 'CDP endpoint to connect to.') - .option('--cdp-header ', 'CDP headers to send with the connect request, multiple can be specified.', headerParser) - .option('--config ', 'path to the configuration file.') - .option('--device ', 'device to emulate, for example: "iPhone 15"') - .option('--executable-path ', 'path to the browser executable.') - .option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.') - .option('--headless', 'run browser in headless mode, headed by default') - .option('--host ', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.') - .option('--ignore-https-errors', 'ignore https errors') - .option('--isolated', 'keep the browser profile in memory, do not save it to disk.') - .option('--image-responses ', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".') - .option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.') - .option('--output-dir ', 'path to the directory for output files.') - .option('--port ', 'port to listen on for SSE transport.') - .option('--proxy-bypass ', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"') - .option('--proxy-server ', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"') - .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.') - .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.') - .option('--secrets ', 'path to a file containing secrets in the dotenv format', dotenvFileLoader) - .option('--storage-state ', 'path to the storage state file for isolated sessions.') - .option('--user-agent ', 'specify user agent string') - .option('--user-data-dir ', 'path to the user data directory. If not specified, a temporary directory will be created.') - .option('--viewport-size ', 'specify browser viewport size in pixels, for example "1280, 720"') - .addOption(new ProgramOption('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp()) - .addOption(new ProgramOption('--vision', 'Legacy option, use --caps=vision instead').hideHelp()) - .action(async options => { - setupExitWatchdog(); + if (options.vision) { + // eslint-disable-next-line no-console + console.error('The --vision option is deprecated, use --caps=vision instead'); + options.caps = 'vision'; + } - if (options.vision) { - // eslint-disable-next-line no-console - console.error('The --vision option is deprecated, use --caps=vision instead'); - options.caps = 'vision'; - } + const config = await resolveCLIConfig(options); + const browserContextFactory = contextFactory(config); + const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir, config.browser.launchOptions.executablePath); - const config = await resolveCLIConfig(options); - const browserContextFactory = contextFactory(config); - const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir, config.browser.launchOptions.executablePath); + if (options.extension) { + const serverBackendFactory: mcpServer.ServerBackendFactory = { + name: 'Playwright w/ extension', + nameInConfig: 'playwright-extension', + version, + create: () => new BrowserServerBackend(config, extensionContextFactory) + }; + await mcpServer.start(serverBackendFactory, config.server); + return; + } - if (options.extension) { - const serverBackendFactory: mcpServer.ServerBackendFactory = { - name: 'Playwright w/ extension', - nameInConfig: 'playwright-extension', - version: packageJSON.version, - create: () => new BrowserServerBackend(config, extensionContextFactory) - }; - await mcpServer.start(serverBackendFactory, config.server); - return; - } + if (options.connectTool) { + const providers: MCPProvider[] = [ + { + name: 'default', + description: 'Starts standalone browser', + connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, browserContextFactory)), + }, + { + name: 'extension', + description: 'Connect to a browser using the Playwright MCP extension', + connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, extensionContextFactory)), + }, + ]; + const factory: mcpServer.ServerBackendFactory = { + name: 'Playwright w/ switch', + nameInConfig: 'playwright-switch', + version, + create: () => new ProxyBackend(providers), + }; + await mcpServer.start(factory, config.server); + return; + } - if (options.connectTool) { - const providers: MCPProvider[] = [ - { - name: 'default', - description: 'Starts standalone browser', - connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, browserContextFactory)), - }, - { - name: 'extension', - description: 'Connect to a browser using the Playwright MCP extension', - connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, extensionContextFactory)), - }, - ]; const factory: mcpServer.ServerBackendFactory = { - name: 'Playwright w/ switch', - nameInConfig: 'playwright-switch', - version: packageJSON.version, - create: () => new ProxyBackend(providers), + name: 'Playwright', + nameInConfig: 'playwright', + version, + create: () => new BrowserServerBackend(config, browserContextFactory) }; await mcpServer.start(factory, config.server); - return; - } - - const factory: mcpServer.ServerBackendFactory = { - name: 'Playwright', - nameInConfig: 'playwright', - version: packageJSON.version, - create: () => new BrowserServerBackend(config, browserContextFactory) - }; - await mcpServer.start(factory, config.server); - }); + }); +} function setupExitWatchdog() { let isExiting = false; @@ -133,5 +131,3 @@ function setupExitWatchdog() { process.on('SIGINT', handleExit); process.on('SIGTERM', handleExit); } - -void program.parseAsync(process.argv); diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 70731b8cb49db..62d02f04d85c7 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -35,12 +35,15 @@ import { runAllTestsWithConfig, TestRunner } from './runner/testRunner'; import { createErrorCollectingReporter } from './runner/reporters'; import { ServerBackendFactory, runMainBackend } from './mcp/sdk/exports'; import { TestServerBackend } from './mcp/test/testBackend'; +import { decorateCommand } from './mcp/program'; import type { ConfigCLIOverrides } from './common/ipc'; import type { TraceMode } from '../types/test'; import type { ReporterDescription } from '../types/test'; import type { Command } from 'playwright-core/lib/utilsBundle'; +const packageJSON = require('../package.json'); + function addTestCommand(program: Command) { const command = program.command('test [test-filter...]'); command.description('run tests with Playwright Test'); @@ -142,8 +145,14 @@ Examples: $ npx playwright merge-reports playwright-report`); } -function addMCPServerCommand(program: Command) { +function addBrowserMCPServerCommand(program: Command) { const command = program.command('run-mcp-server', { hidden: true }); + command.description('Interact with the browser over MCP'); + decorateCommand(command, packageJSON.version); +} + +function addTestMCPServerCommand(program: Command) { + const command = program.command('run-test-mcp-server', { hidden: true }); command.description('Interact with the test runner over MCP'); command.option('-c, --config ', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`); command.option('--host ', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.'); @@ -153,7 +162,7 @@ function addMCPServerCommand(program: Command) { const backendFactory: ServerBackendFactory = { name: 'Playwright Test Runner', nameInConfig: 'playwright-test-runner', - version: '0.0.0', + version: packageJSON.version, create: () => new TestServerBackend(resolvedLocation, { muteConsole: options.port === undefined }), }; const mdbUrl = await runMainBackend(backendFactory, { port: options.port === undefined ? undefined : +options.port }); @@ -390,6 +399,7 @@ addTestCommand(program); addShowReportCommand(program); addMergeReportsCommand(program); addClearCacheCommand(program); -addMCPServerCommand(program); +addBrowserMCPServerCommand(program); +addTestMCPServerCommand(program); addDevServerCommand(program); addTestServerCommand(program); diff --git a/tests/mcp/cdp.spec.ts b/tests/mcp/cdp.spec.ts index 5ff0930bba4a2..fe209b7404718 100644 --- a/tests/mcp/cdp.spec.ts +++ b/tests/mcp/cdp.spec.ts @@ -84,7 +84,7 @@ test('should throw connection error and allow re-connecting', async ({ cdpServer test('does not support --device', async () => { const result = spawnSync('node', [ - programPath, '--device=Pixel 5', '--cdp-endpoint=http://localhost:1234', + ...programPath, '--device=Pixel 5', '--cdp-endpoint=http://localhost:1234', ]); expect(result.error).toBeUndefined(); expect(result.status).toBe(1); diff --git a/tests/mcp/click.spec.ts b/tests/mcp/click.spec.ts index f7de50e3e701a..6f6a06dbd017c 100644 --- a/tests/mcp/click.spec.ts +++ b/tests/mcp/click.spec.ts @@ -115,17 +115,19 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => { arguments: { url: server.PREFIX }, }); - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Submit button', - ref: 'e2', - modifiers: ['Control'], - }, - })).toHaveResponse({ - code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Control'] });`, - pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:true metaKey:false shiftKey:false altKey:false`), - }); + if (process.platform !== 'darwin') { + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Submit button', + ref: 'e2', + modifiers: ['Control'], + }, + })).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Control'] });`, + pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:true metaKey:false shiftKey:false altKey:false`), + }); + } expect(await client.callTool({ name: 'browser_click', diff --git a/tests/mcp/fixtures.ts b/tests/mcp/fixtures.ts index 7fb39c68071f3..47dc2e7e7c2de 100644 --- a/tests/mcp/fixtures.ts +++ b/tests/mcp/fixtures.ts @@ -187,7 +187,7 @@ async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'], const transport = new StdioClientTransport({ command: 'node', - args: [programPath, ...args], + args: [...programPath, ...args], cwd: path.dirname(test.info().config.configFile!), stderr: 'pipe', env: { @@ -277,4 +277,4 @@ function parseSections(text: string): Map { return sections; } -export const programPath = path.join(__dirname, '../../packages/playwright/lib/mcp/program.js'); +export const programPath = [path.join(__dirname, '../../packages/playwright/cli.js'), 'run-mcp-server']; diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index 2289a2bb2d81a..69cc63d41c4ab 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -32,7 +32,7 @@ const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noP throw new Error('Process already running'); cp = spawn('node', [ - programPath, + ...programPath, ...(options?.noPort ? [] : ['--port=0']), '--user-data-dir=' + userDataDir, ...(mcpHeadless ? ['--headless'] : []), diff --git a/tests/mcp/sse.spec.ts b/tests/mcp/sse.spec.ts index 42bdc4c228292..5d8d706c0d8cd 100644 --- a/tests/mcp/sse.spec.ts +++ b/tests/mcp/sse.spec.ts @@ -32,7 +32,7 @@ const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noP throw new Error('Process already running'); cp = spawn('node', [ - programPath, + ...programPath, ...(options?.noPort ? [] : ['--port=0']), '--user-data-dir=' + userDataDir, ...(mcpHeadless ? ['--headless'] : []), From f6e4101ec1d7a3fbd9b3be366f24407a1eeb32e0 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:23:14 +0200 Subject: [PATCH 085/329] feat(webkit): roll to r2208 (#37305) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 39ed0bf87398d..89711b68db78c 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -39,7 +39,7 @@ }, { "name": "webkit", - "revision": "2207", + "revision": "2208", "installByDefault": true, "revisionOverrides": { "debian11-x64": "2105", From b37caaaf5c9a46a5c328c8736ec85502590f0287 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 5 Sep 2025 07:17:37 -0700 Subject: [PATCH 086/329] feat(mcp): allow configuring timeouts (#37311) --- packages/playwright/src/mcp/browser/config.ts | 34 +++++-- packages/playwright/src/mcp/browser/tab.ts | 4 +- packages/playwright/src/mcp/config.d.ts | 12 +++ packages/playwright/src/mcp/program.ts | 4 +- tests/mcp/timeouts.spec.ts | 97 +++++++++++++++++++ 5 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 tests/mcp/timeouts.spec.ts diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index e06543f5ff24f..da22d0e38023f 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -48,6 +48,8 @@ export type CLIOptions = { saveTrace?: boolean; secrets?: Record; storageState?: string; + timeoutAction?: number; + timeoutNavigation?: number; userAgent?: string; userDataDir?: string; viewportSize?: string; @@ -71,6 +73,10 @@ const defaultConfig: FullConfig = { }, server: {}, saveTrace: false, + timeouts: { + action: 5000, + navigation: 60000, + }, }; type BrowserUserConfig = NonNullable; @@ -84,6 +90,10 @@ export type FullConfig = Config & { network: NonNullable, saveTrace: boolean; server: NonNullable, + timeouts: { + action: number; + navigation: number; + }, }; export async function resolveConfig(config: Config): Promise { @@ -196,6 +206,10 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { secrets: cliOptions.secrets, outputDir: cliOptions.outputDir, imageResponses: cliOptions.imageResponses, + timeouts: { + action: cliOptions.timeoutAction, + navigation: cliOptions.timeoutNavigation, + }, }; return result; @@ -221,12 +235,14 @@ function configFromEnv(): Config { options.imageResponses = 'omit'; options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX); options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR); - options.port = envToNumber(process.env.PLAYWRIGHT_MCP_PORT); + options.port = numberParser(process.env.PLAYWRIGHT_MCP_PORT); options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS); options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER); options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE); options.secrets = dotenvFileLoader(process.env.PLAYWRIGHT_MCP_SECRETS_FILE); options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE); + options.timeoutAction = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION); + options.timeoutNavigation = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION); options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT); options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR); options.viewportSize = envToString(process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE); @@ -292,6 +308,10 @@ function mergeConfig(base: FullConfig, overrides: Config): FullConfig { ...pickDefined(base.server), ...pickDefined(overrides.server), }, + timeouts: { + ...pickDefined(base.timeouts), + ...pickDefined(overrides.timeouts), + }, } as FullConfig; } @@ -313,6 +333,12 @@ export function dotenvFileLoader(value: string | undefined): Record): Record { if (!arg) return previous || {}; @@ -322,12 +348,6 @@ export function headerParser(arg: string | undefined, previous?: Record { page.on('download', download => { void this._downloadStarted(download); }); - page.setDefaultNavigationTimeout(60000); - page.setDefaultTimeout(5000); + page.setDefaultNavigationTimeout(this.context.config.timeouts.navigation); + page.setDefaultTimeout(this.context.config.timeouts.action); (page as any)[tabSymbol] = this; } diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index 49fe40226e116..09a999d88ca4d 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -124,6 +124,18 @@ export type Config = { blockedOrigins?: string[]; }; + timeouts?: { + /* + * Configures default action timeout: https://playwright.dev/docs/api/class-page#page-set-default-timeout. Defaults to 5000ms. + */ + action?: number; + + /* + * Configures default navigation timeout: https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout. Defaults to 60000ms. + */ + navigation?: number; + }; + /** * Whether to send image responses to the client. Can be "allow", "omit", or "auto". Defaults to "auto", which sends images if the client can display them. */ diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index aa9904e16b60f..25dcb12c406cd 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -16,7 +16,7 @@ import { ProgramOption } from 'playwright-core/lib/utilsBundle'; import * as mcpServer from './sdk/server'; -import { commaSeparatedList, dotenvFileLoader, headerParser, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; +import { commaSeparatedList, dotenvFileLoader, headerParser, numberParser, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; import { Context } from './browser/context'; import { contextFactory } from './browser/browserContextFactory'; import { ProxyBackend } from './sdk/proxyBackend'; @@ -52,6 +52,8 @@ export function decorateCommand(command: Command, version: string) { .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.') .option('--secrets ', 'path to a file containing secrets in the dotenv format', dotenvFileLoader) .option('--storage-state ', 'path to the storage state file for isolated sessions.') + .option('--timeout-action ', 'specify action timeout in milliseconds, defaults to 5000ms', numberParser) + .option('--timeout-navigation ', 'specify navigation timeout in milliseconds, defaults to 60000ms', numberParser) .option('--user-agent ', 'specify user agent string') .option('--user-data-dir ', 'path to the user data directory. If not specified, a temporary directory will be created.') .option('--viewport-size ', 'specify browser viewport size in pixels, for example "1280, 720"') diff --git a/tests/mcp/timeouts.spec.ts b/tests/mcp/timeouts.spec.ts new file mode 100644 index 0000000000000..57c459812194b --- /dev/null +++ b/tests/mcp/timeouts.spec.ts @@ -0,0 +1,97 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('action timeout (default)', async ({ client, server }) => { + server.setContent('/', ` + + + + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + expect(await client.callTool({ + name: 'browser_type', + arguments: { + element: 'textbox', + ref: 'e2', + text: 'Hi!', + submit: true, + }, + })).toHaveResponse({ + result: expect.stringContaining(`Timeout 5000ms exceeded.`), + }); +}); + +test('action timeout (custom)', async ({ startClient, server }) => { + const { client } = await startClient({ args: [`--timeout-action=1234`] }); + server.setContent('/', ` + + + + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + }); + + expect(await client.callTool({ + name: 'browser_type', + arguments: { + element: 'textbox', + ref: 'e2', + text: 'Hi!', + submit: true, + }, + })).toHaveResponse({ + result: expect.stringContaining(`Timeout 1234ms exceeded.`), + }); +}); + +test('navigation timeout', async ({ startClient, server }) => { + const { client } = await startClient({ args: [`--timeout-navigation=1234`] }); + server.setRoute('/slow', async () => { + await new Promise(f => setTimeout(f, 1500)); + return new Response('OK'); + }); + server.setContent('/', ` + + + + + `, 'text/html'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX + '/slow', + }, + })).toHaveResponse({ + result: expect.stringContaining(`Timeout 1234ms exceeded.`), + }); +}); From 2f91078cb3e6cdb78292b14d19660eb9ae2a381e Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Fri, 5 Sep 2025 18:33:04 +0200 Subject: [PATCH 087/329] test(bidi): remove some timeout expectations (#37174) --- .../expectations/moz-firefox-nightly-library.txt | 9 --------- .../bidi/expectations/moz-firefox-nightly-page.txt | 14 -------------- 2 files changed, 23 deletions(-) diff --git a/tests/bidi/expectations/moz-firefox-nightly-library.txt b/tests/bidi/expectations/moz-firefox-nightly-library.txt index e0d301763a88c..fd6aafdb55ffc 100644 --- a/tests/bidi/expectations/moz-firefox-nightly-library.txt +++ b/tests/bidi/expectations/moz-firefox-nightly-library.txt @@ -1,20 +1,13 @@ -library/browsercontext-credentials.spec.ts › should fail with wrong credentials [timeout] library/browsercontext-credentials.spec.ts › should fail without credentials [timeout] library/browsercontext-credentials.spec.ts › should work with setHTTPCredentials [timeout] -library/browsercontext-events.spec.ts › dialog event should work in immediately closed popup [timeout] -library/browsercontext-events.spec.ts › dialog event should work in popup [timeout] library/browsercontext-har.spec.ts › should change document URL after redirected navigation [timeout] library/browsercontext-har.spec.ts › should goBack to redirected navigation [timeout] library/browsercontext-har.spec.ts › should goForward to redirected navigation [timeout] library/browsercontext-har.spec.ts › should ignore boundary when matching multipart/form-data body [timeout] library/browsercontext-har.spec.ts › should record overridden requests to har [timeout] library/browsercontext-har.spec.ts › should reload redirected navigation [timeout] -library/browsercontext-locale.spec.ts › should format number in popups [timeout] library/browsercontext-network-event.spec.ts › should reject response.finished if context closes [timeout] library/browsercontext-page-event.spec.ts › should have about:blank for empty url with domcontentloaded [timeout] -library/browsercontext-proxy.spec.ts › should use proxy for https urls [timeout] -library/browsercontext-service-worker-policy.spec.ts › block › blocks service worker registration [timeout] -library/browsercontext-timezone-id.spec.ts › should work for multiple pages sharing same process [timeout] library/browsertype-connect.spec.ts › launchServer › should be able to connect 20 times to a single server without warnings [timeout] library/browsertype-connect.spec.ts › launchServer › should error when saving download after deletion [timeout] library/browsertype-connect.spec.ts › launchServer › should save download [timeout] @@ -98,8 +91,6 @@ library/geolocation.spec.ts › should use context options [timeout] library/geolocation.spec.ts › should use context options for popup [timeout] library/geolocation.spec.ts › should work @smoke [timeout] library/geolocation.spec.ts › watchPosition should be notified [timeout] -library/har.spec.ts › should record failed request overrides [timeout] -library/har.spec.ts › should record request overrides [timeout] library/headful.spec.ts › should click bottom row w/ infobar in OOPIF [timeout] library/inspector/cli-codegen-2.spec.ts › cli codegen › should --test-id-attribute [timeout] library/inspector/cli-codegen-2.spec.ts › cli codegen › should not clash pages [timeout] diff --git a/tests/bidi/expectations/moz-firefox-nightly-page.txt b/tests/bidi/expectations/moz-firefox-nightly-page.txt index a28f0715df20a..ac99dbe73089f 100644 --- a/tests/bidi/expectations/moz-firefox-nightly-page.txt +++ b/tests/bidi/expectations/moz-firefox-nightly-page.txt @@ -2,8 +2,6 @@ page/frame-hierarchy.spec.ts › should send "framenavigated" when navigating on page/interception.spec.ts › should disable memory cache when intercepting [timeout] page/interception.spec.ts › should intercept worker requests when enabled after worker creation [timeout] page/page-add-init-script.spec.ts › init script should run only once in popup [timeout] -page/page-basic.spec.ts › page.close should work with window.close [timeout] -page/page-basic.spec.ts › should provide access to the opener page [timeout] page/page-basic.spec.ts › should return null if parent page has been closed [timeout] page/page-event-console.spec.ts › should trigger correct Log [timeout] page/page-event-pageerror.spec.ts › should contain sourceURL [timeout] @@ -14,8 +12,6 @@ page/page-event-pageerror.spec.ts › should handle odd values [timeout] page/page-event-pageerror.spec.ts › should handle window [timeout] page/page-event-pageerror.spec.ts › should not receive console message for pageError [timeout] page/page-event-pageerror.spec.ts › should support an empty Error.name property [timeout] -page/page-event-popup.spec.ts › should be able to capture alert [timeout] -page/page-event-popup.spec.ts › should work @smoke [timeout] page/page-event-request.spec.ts › should report navigation requests and responses handled by service worker [timeout] page/page-event-request.spec.ts › should report navigation requests and responses handled by service worker with routing [timeout] page/page-filechooser.spec.ts › should upload multiple large files [timeout] @@ -37,21 +33,11 @@ page/page-filechooser.spec.ts › should work for "multiple" [timeout] page/page-filechooser.spec.ts › should work for "webkitdirectory" [timeout] page/page-filechooser.spec.ts › should emit event after navigation [timeout] page/page-filechooser.spec.ts › should trigger listener added before navigation [timeout] -page/page-goto.spec.ts › should fail when server returns 204 [timeout] page/page-goto.spec.ts › should work with anchor navigation [timeout] -page/page-history.spec.ts › should reload proper page [timeout] page/page-network-response.spec.ts › should reject response.finished if context closes [timeout] page/page-request-continue.spec.ts › should not throw if request was cancelled by the page [timeout] page/page-route.spec.ts › should not throw if request was cancelled by the page [timeout] -page/page-set-input-files.spec.ts › should upload a file after popup [timeout] -page/page-wait-for-function.spec.ts › should fail with ReferenceError on wrong page [timeout] -page/page-wait-for-function.spec.ts › should fail with predicate throwing on first call [timeout] -page/page-wait-for-function.spec.ts › should not be called after finishing unsuccessfully [timeout] -page/page-wait-for-load-state.spec.ts › should wait for load state of about:blank popup [timeout] page/page-wait-for-load-state.spec.ts › should wait for load state of empty url popup [timeout] -page/page-wait-for-load-state.spec.ts › should work with pages that have loaded before being connected to [timeout] -page/page-wait-for-navigation.spec.ts › should work with url match for same document navigations [timeout] -page/page-wait-for-url.spec.ts › should work with url match for same document navigations [timeout] page/workers.spec.ts › Page.workers @smoke [timeout] page/workers.spec.ts › should attribute network activity for worker inside iframe to the iframe [timeout] page/workers.spec.ts › should clear upon cross-process navigation [timeout] From edbdb2dcc1fbc9f3674b35b21cc1102449f439a7 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Fri, 5 Sep 2025 18:34:19 +0200 Subject: [PATCH 088/329] chore(deps): bump chromium-bidi to ^8.1.0 (#37318) --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c8b89499feda5..26835f3ed1d15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "@zip.js/zip.js": "^2.7.29", "ansi-styles": "^4.3.0", "chokidar": "^3.5.3", - "chromium-bidi": "^7.2.0", + "chromium-bidi": "^8.1.0", "colors": "^1.4.0", "concurrently": "^6.2.1", "cross-env": "^7.0.3", @@ -2886,10 +2886,11 @@ } }, "node_modules/chromium-bidi": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.3.2.tgz", - "integrity": "sha512-80J9hiQvSxqQAGhC6vsOa5yfG8lrusrSrR7eAIViDkoK5wnXi15z5iLQYegUTc4ELDUIkq37Ob1Xx8VRwYUDRg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-8.1.0.tgz", + "integrity": "sha512-tkzq2Y+sMwg3M0rtZersWIYHVR0amVXNQlYHxrwvAseO+LvSm9Z19lBsZpaA0GMwEzZx1/HJiaoyMVuy7qzOdQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" diff --git a/package.json b/package.json index bdc5f6149cc80..e9b74e2430485 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "@zip.js/zip.js": "^2.7.29", "ansi-styles": "^4.3.0", "chokidar": "^3.5.3", - "chromium-bidi": "^7.2.0", + "chromium-bidi": "^8.1.0", "colors": "^1.4.0", "concurrently": "^6.2.1", "cross-env": "^7.0.3", From a14094d36446662cdaf67c3245fc766932f322e5 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 5 Sep 2025 09:35:31 -0700 Subject: [PATCH 089/329] Revert "fix(mcp): wait if profile is still in use (#37303)" (#37319) --- .../src/mcp/browser/browserContextFactory.ts | 50 ++++++------- .../src/mcp/browser/processUtils.ts | 75 ------------------- tests/mcp/launch.spec.ts | 43 ----------- 3 files changed, 21 insertions(+), 147 deletions(-) delete mode 100644 packages/playwright/src/mcp/browser/processUtils.ts diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index d2e9f7de31c0e..e4320b3a1a246 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -22,7 +22,6 @@ import path from 'path'; import * as playwright from 'playwright-core'; import { registryDirectory } from 'playwright-core/lib/server/registry/index'; import { startTraceViewerServer } from 'playwright-core/lib/server'; -import { findBrowserProcess, getBrowserExecPath } from './processUtils'; import { logUnhandledError, testDebug } from '../log'; import { outputFile } from './config'; @@ -175,28 +174,28 @@ class PersistentContextFactory implements BrowserContextFactory { const browserType = playwright[this.config.browser.browserName]; for (let i = 0; i < 5; i++) { - if (!await alreadyRunning(this.config, browserType, userDataDir)) - break; - // User data directory is already in use, wait for the previous browser instance to close. - await new Promise(resolve => setTimeout(resolve, 1000)); - } - try { - const browserContext = await browserType.launchPersistentContext(userDataDir, { - tracesDir, - ...this.config.browser.launchOptions, - ...this.config.browser.contextOptions, - handleSIGINT: false, - handleSIGTERM: false, - }); - const close = () => this._closeBrowserContext(browserContext, userDataDir); - return { browserContext, close }; - } catch (error: any) { - if (error.message.includes('Executable doesn\'t exist')) - throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); - if (error.message.includes('ProcessSingleton') || error.message.includes('Invalid URL')) - throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); - throw error; + try { + const browserContext = await browserType.launchPersistentContext(userDataDir, { + tracesDir, + ...this.config.browser.launchOptions, + ...this.config.browser.contextOptions, + handleSIGINT: false, + handleSIGTERM: false, + }); + const close = () => this._closeBrowserContext(browserContext, userDataDir); + return { browserContext, close }; + } catch (error: any) { + if (error.message.includes('Executable doesn\'t exist')) + throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); + if (error.message.includes('ProcessSingleton') || error.message.includes('Invalid URL')) { + // User data directory is already in use, try again. + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + throw error; + } } + throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); } private async _closeBrowserContext(browserContext: playwright.BrowserContext, userDataDir: string) { @@ -218,13 +217,6 @@ class PersistentContextFactory implements BrowserContextFactory { } } -async function alreadyRunning(config: FullConfig, browserType: playwright.BrowserType, userDataDir: string) { - const execPath = config.browser.launchOptions.executablePath ?? getBrowserExecPath(config.browser.launchOptions.channel ?? browserType.name()); - if (!execPath) - return false; - return !!findBrowserProcess(execPath, userDataDir); -} - async function injectCdpPort(browserConfig: FullConfig['browser']) { if (browserConfig.browserName === 'chromium') (browserConfig.launchOptions as any).cdpPort = await findFreePort(); diff --git a/packages/playwright/src/mcp/browser/processUtils.ts b/packages/playwright/src/mcp/browser/processUtils.ts deleted file mode 100644 index 45584b4861e50..0000000000000 --- a/packages/playwright/src/mcp/browser/processUtils.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import childProcess from 'child_process'; - -import { registry } from 'playwright-core/lib/server/registry/index'; - -// TODO: make browserType.executablePath() return it. -export function getBrowserExecPath(channelOrName: string): string | undefined { - return registry.findExecutable(channelOrName)?.executablePath('javascript'); -} - -export function findBrowserProcess(execPath: string, arg: string): string | null { - switch (process.platform) { - case 'darwin': - return findProcessMacos(execPath, arg); - case 'linux': - return findProcessLinux(execPath, arg); - case 'win32': - return findProcessWindows(execPath, arg); - default: - return null; - } -} - -function findProcessLinux(execPath: string, arg: string): string | null { - const psResult = childProcess.spawnSync('ps', ['-eo', 'pid=,args=']); - return findMatchingLine(psResult.stdout.toString(), execPath, arg); -} - -function findProcessMacos(execPath: string, arg: string): string | null { - const psResult = childProcess.spawnSync('ps', ['-axo', 'pid=,command=']); - return findMatchingLine(psResult.stdout.toString(), execPath, arg); -} - -function findProcessWindows(execPath: string, arg: string): string | null { - const filter = `$_.ExecutablePath -eq '${execPath}' -and $_.CommandLine.Contains('${arg}') -and $_.CommandLine -notmatch '--type'`; - const ps = childProcess.spawnSync( - 'powershell.exe', - [ - '-NoProfile', - '-Command', - `Get-CimInstance Win32_Process | Where-Object { ${filter} } | Select-Object -Property ProcessId,CommandLine | ForEach-Object { "$($_.ProcessId) $($_.CommandLine)" }` - ], - { encoding: 'utf8' } - ); - - if (ps.status !== 0) - return null; - - return findMatchingLine(ps.stdout.toString(), execPath, arg); -} - -function findMatchingLine(psOutput: string, execPath: string, arg: string): string | null { - const lines = psOutput.split('\n').map(l => l.trim()).filter(Boolean); - for (const line of lines) { - // Chrome child process have --type argument, we only return the browser process. - if (line.includes(execPath) && line.includes(arg) && !line.includes('--type')) - return line; - } - return null; -} diff --git a/tests/mcp/launch.spec.ts b/tests/mcp/launch.spec.ts index deca384b7e192..f7f0171aa9efd 100644 --- a/tests/mcp/launch.spec.ts +++ b/tests/mcp/launch.spec.ts @@ -167,46 +167,3 @@ test('isolated context with storage state', async ({ startClient, server }, test pageState: expect.stringContaining(`Storage: session-value`), }); }); - -test('persistent context already running', async ({ startClient, server, mcpBrowser }, testInfo) => { - const userDataDir = testInfo.outputPath('user-data-dir'); - const { client } = await startClient({ - args: [`--user-data-dir=${userDataDir}`], - }); - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const { client: client2, stderr } = await startClient({ - args: [`--user-data-dir=${userDataDir}`], - }); - const navigationPromise = client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const wait = await Promise.race([ - navigationPromise.then(() => 'done'), - new Promise(resolve => setTimeout(resolve, 1_000)).then(() => 'timeout'), - ]); - expect(wait).toBe('timeout'); - - // Check that the second client is trying to launch the browser. - await expect.poll(() => formatOutput(stderr()), { timeout: 0 }).toEqual([ - 'create context', - 'create browser context (persistent)', - 'lock user data dir' - ]); - - // Close first client's browser. - await client.callTool({ - name: 'browser_close', - arguments: { url: server.HELLO_WORLD }, - }); - - const result = await navigationPromise; - expect(result).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); -}); From 8972ec811766b996f39e8292ed803e7d3e1def97 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Fri, 5 Sep 2025 18:42:51 +0200 Subject: [PATCH 090/329] test(bidi): add missing snapshots for Firefox (#37189) --- ...ot-element-mobile-dsf-moz-firefox-nightly.png | Bin 0 -> 1544 bytes ...enshot-element-mobile-moz-firefox-nightly.png | Bin 0 -> 442 bytes .../mock-binary-response-moz-firefox-nightly.png | Bin 0 -> 5679 bytes .../mock-svg-moz-firefox-nightly.png | Bin 0 -> 245 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/library/screenshot.spec.ts-snapshots/screenshot-element-mobile-dsf-moz-firefox-nightly.png create mode 100644 tests/library/screenshot.spec.ts-snapshots/screenshot-element-mobile-moz-firefox-nightly.png create mode 100644 tests/page/page-request-fulfill.spec.ts-snapshots/mock-binary-response-moz-firefox-nightly.png create mode 100644 tests/page/page-request-fulfill.spec.ts-snapshots/mock-svg-moz-firefox-nightly.png diff --git a/tests/library/screenshot.spec.ts-snapshots/screenshot-element-mobile-dsf-moz-firefox-nightly.png b/tests/library/screenshot.spec.ts-snapshots/screenshot-element-mobile-dsf-moz-firefox-nightly.png new file mode 100644 index 0000000000000000000000000000000000000000..e5003ec20298c5a677cf117ee1cf38443812e057 GIT binary patch literal 1544 zcmcJPXE+-O9L8ghN^mt|6hUhxX|0nxtBDw~qM8(yIA^ujYDKIV7p*M>p}11CO?%R) z8nsGLTUF~+5gy}^w46~|BYljX`+OhoxexFAKF|9+@3;T|m+9f|2$WWp1^@s+j1$`P zID`I?r1-Iu77K9zfY=BIjl@y>)~8po7h}{1wy+Bx)0g3?AQo4t!P~I7iNG0F)M;gv zzP08SAx<{i`eU2E`G4!Ghx*j(!Cll1d}lak`t=bw^t%3Tw;QZ@sL`XY@5DpsM++-s zP_gy7t@xR}9iHyTd%LeJJHJ>S9h zNE)lBfI%o32}oM9Tn{p1L96$_*B65G(sMO+?v@YC;#4CdA}HI-3`7NkAyj?o1amYa zDM^?I-u@#v(;E?BD3BlxpAtqUUNoi@~6rww2?UJ|K z*KnVBKz7GOk^k>PhVeaGR-yBl%J zz{0{>)t$ld5QBO9!fQ#xR)m*3PNMVszVd6Q;mK!2yGJ#$ zbpr5kUmv$U-XbAN!_3>|=U3*2^Nz=BLb>aGJ@tO9?LLDOBWqJ3OE<@}>WTuQs2!9b z7!>Ls9j$cxszPV^^&TEUT2WDv>*-7XRZXq7BS**s(wZo$IlY%z)j_tlnXNlD&X4A6uyH4O!xUi#NtD{D>0lu8 z6O}4yQ{(75;q$uSt107we1qSz98yVH*LXf(AWrZ%qMNO6IoA4`jZvP zIY1^qp-r;Bp&_NDF<704cY#9#s8o%yv9Ua}TW>s*td@ofBE1l_u*1#_t&ry`rPtYr z!PPwKCLnN3Dl$?cT(GwU#=h-ZkIrg-+D_kLc@rPbxQweQ2Kfgm=)t2GQChb)ND8rH zV%*L|+a~k(T}02604DN9G222r!^))pr0Fv4;ILJVSZ?_}qD&y@k*$5gmR2Gb85ViW zWoRWc!|%1uq~>hg+(z%;XXtLRrWd1_Q>zZCkJ08?*=!IwjOzFF29dtINEtZ2-JiC# zy>ju6Pc?URI|r8g5a0YeBOaAQS`jN|X&c!Ko+H49a_jW2QOiZ;7h?^br1(5k8IRM6f<0wjHeFs=tSamR!QVC>z|&rtqJe*-p|!_WW# literal 0 HcmV?d00001 diff --git a/tests/library/screenshot.spec.ts-snapshots/screenshot-element-mobile-moz-firefox-nightly.png b/tests/library/screenshot.spec.ts-snapshots/screenshot-element-mobile-moz-firefox-nightly.png new file mode 100644 index 0000000000000000000000000000000000000000..4d61adc510128af131ca72414021871ff4670e43 GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fFgAL+IEG|6zrAAXeK7-(CSo;G7 z5yi{>dv2onr#SCP>XGjrs$9$3y6*%3k){cAcP^1w^y1ymR%efg0$CO< zQhW>BoL9h@ZkZ=uzO`L-HLGHMrc~BesUqRz!`q8Axo4Vwd$3FI)KjCNl_FkCg=+4% zXU@8?T>WVICJj!*`1=#$&p%#x_gI*b;x`4oxqgTDSP0M7@p9UA&zk4(f#Z`^w;ecm z)zs#Ex8I3;H{;bO(l%e@n|WXVK%q>;wAQH~1vj4lq^{`QntWqhZ`Izu$#*v0NDY#k z6mi>S{dMQX7Y`(+1-{v&$=jL9{c!8$mkG&fSvyuO)KRgx@wZNXexI({)F~}7c4dJp zLk|5;+SqY0!Jvlu$m7B}@AJCTBd4^;nC2fc{wN%KeevJlp}h#NBKaD~6jV&=+>n2( Xvr^1M_`5tXP8d90{an^LB{Ts5i^IeS literal 0 HcmV?d00001 diff --git a/tests/page/page-request-fulfill.spec.ts-snapshots/mock-binary-response-moz-firefox-nightly.png b/tests/page/page-request-fulfill.spec.ts-snapshots/mock-binary-response-moz-firefox-nightly.png new file mode 100644 index 0000000000000000000000000000000000000000..ef2b004d4ea795b7664a9b05bda80b826d793cae GIT binary patch literal 5679 zcmV+~7SQR5P)7000&CNklvJ5(QACasUj#~aIVLEZHf_>p zpM9o1d-kYj&z`DLqekGp<4F!5KCExP`9_l_O;YXJwbiLpC!Kuq$$|ItlCYovW6YQ_ zdhfmW^urH7h>x6c#u*wgV1Nb=9C+No@nF33&O7@2^Ut+y+cw3-#HeMXi9zEJ%{P^RKaw?XVdbX5`L`!34OKZ03^wUpQ|Ni|A&RJ)j75MDGDTfXn($`;q z9e}ZY`*!Kv8uE2Zkn{6;X=1WsewSLbXkljuJNm0_~?Q$kBltxT9IMu3EQ{TRQHE7Tv)vQ@FaISw+{`lh$End9XU`&`W zL1}4eQsWx(4d^JftS^-+CLKH^txl2i*<5MDT*d4@sDy+BgVVKZR~0W_Ja9fwvO&RL zTzv7x8b5x#`$E4q@?FzQs#r!k>`0l1-PfahN8n6aAdQ$It=+0BRjL>q&T-B;=eWoI z&61g!sRaubXz0+P`r?Z(v}4B(sd4oqFiIBZAj!M6r~>YP*KLyX>R9Qs`6^VXknXzc zE9x)8?ezK3LkGfi5Xs?$z8 zO?~?GF*rE8ze9k5PkZ;>cQsXXFeiWrw}R!~6K- zj};XaPHw`FgjN>YX9QM2Cj6Db*sCQ>Tu6K8`y!0mk>=e{a5R?AWnp-fD2Ve1kgY4uk7kyGhQ3dD7U~ z($?J)ezAES1Ju7qqJ|G29v+moZQE*@GUR*bPHER(IiqK}Ck0NyD5*;m`TBH_8dR4G z$K(zNpR+Di6)RRWID-cdR^7UF!-C_%Kr?``Wy=<++9~q&>nNR{AeAW@8U`l*%SJil zk|h4PZeVn6E?<0IX?wbyd)`*xo{8aU%I@8}?c%?h_LbD4wtR!SNavm{ZQUt{6=&=$ zDa{Y3a})Xch5_eGzviqkLFJ8X*I+D*PXTdUQQnIvuyKcSpRz35~Gl5fhk_`&7 zdi82k)lWbDRO^0QFD2BHZ$M`$uBNmtT@GIeptL+uyuU@@+>C#ItRsH()4Gu8A`|i5{jICR@8Voyk*Ep#{tb4A^aA7o8f#!AP_Surd?*XXei&uL&-^Uh2qJtzy7M{pMPF&zWJsyQQNqh@(t|hI;=E59Q=+4 zr&A+8oHL~&IlPkB@ zg5YGha60|0n3dp+=1wr2+-~F-tBIHk2oxctUsIeYcu7Z?2bLe%WlEhV3(u#?oH zp0sn1tInfmN~swVjH=)HzAmQ0}?s z9zFi}o2@5FThjeY`ML7lIZ!&a)X`va5`d)d zku!9HtAY58YGtL)af)hE(}gkbdui$-i$O?CNi+DXTD7X3;{N;Z*CUTSVh@w+M$7Y( zu%MuVBz#1X{1@BwmQq%XZ@>N4;E);+=cryaS91vdl9$T4Z>Y5Q52;X;d?Ox|>eo2h zlyVYO{kxTN`aI-nOQ8bN<=y40Syj%0rP5c6O&iJBxh`Pv|G~nw;#|)>^Gwa2Jv&zi z>q%Hp$U=!N$Utwt`DQ)$+;f5VJn_RK0q2gF+CQmxKi+BO}8y z%6aqVnHfF$=%a!6vI<&r`|Y=D=FFMy3$?4rcS#o)MycZ2H3SEG=npeDl`JL|DCjy= zPXL3T*^nyd^EuLpnP!eSOk$1u?z_+Otegasp&&*!Jbn6fbG~^=SWw6{ND^1BTxpuX z#RQ8Nj^lXoFVtaFr>g5*+clD6ON0)`44Sb50h_U+rJ$&)9mRjXEk_i~c3plsN%L9JW2)|xeIEXpMk zL@*gNgXkBNv2Yw113E};!+_SZE_~glSa;x zHtsN=ff-To!c;t2wrrU_(7k*2n$~b1c}ZAM@D&6<`}gnHmtTHqD-iVzv<5$8&b2O@ z(NSvKFc%nyrBxf;rIl3)7_r5qG9{#y8?7dy%{%NS18@@CNHubSld?_D7s=A7nN||O z&j1EMXwkZL>#Q7^l9FQ4Ij`p>VL?esN>Z0DU2GY?_uhN;(@#GgDf`u}=4u8$BR3eU zH@GvLO;*pmBv&XnOdKaM2&9oh4XW#KzxHz4GzbHR-%N-=4Mv=zTGV%8%=^L3 zO;R%~&}!7Ek@+XoozUIapE5ynJGb-2JUO3Emp1Km;Uuu`#K*^L$dDmA@4WL=vSdm3xFE>}g|wa8Dt^QSGeM=TI-Wz>F90JbQNw|D ziBnX98y6UiDT^#w^y;_3s8y?$|3pWG`mhIw2Sf&JnsXj-9M`OdO?0zkN(?yoE^$(M z63HCkP_mf1NY49{3@UDbiavQ9MeJb728CFJwcKhN2nD38E|AWyA{8olB=`!zpq}Bc z$c2J2cZsVR+xJ+#*3*noFtSQWIQ`mNR;)ey<&2sxjh!RyJ(x2bm~gH84oC}E$a!&u zw0wh|pH|0|DN}UPNhkTg(Up);Sl2gaq)FvtrPZ6I=mJuYc(>?7MW3=jpq}A}Q6>lm z^^CcGGx5VfGsr>0fss{+HGo60%iv%-SN4?JH*$+z1b=Ah9zPrkVV#@0a7Yt?@yS$a z&QkY;Rm)22x2a`93yWRCf9Eo*)%3D(8V=YSE;bMY(xGS-*b01!rs4ud_y&dbe|ftEBIxkER4vPi2bR(is(B z-~3GGoF#77nzl!kDpd+-#@_)Wt9UU7S^2JAyWIS05O5kuX}ev`p@s|`ghirjyc_3i z*lMep7Oyl%%j$~r4VG+B2;GTVUw!pe&7C_}2MT6N-Qy)pr$lk-%BL;F_C0)=d@(Vu znM?=9-hUiMun>F9pFiKagqT2jdb$gzAAw{8IkT5YcMp@=)RS*;H)+jAIaKk#{Y9lp zlr$hW+;D>$H*Op_rzhE^S+t-FhjQQ6bT^!*U9fYXrQ^Xc z!s+S!T>_lsXwf1|25D(fDs6qY>(TnM7h zJ@;JmF}dXo$EyGZ$I8NiTyxDedgYZ@a(t+{LV+kI!f*^VS_(|zv7$P*n@Gm*hJ)VVIWMw45MTkoP#3B(&a)d%phhnvt1Ko1VEqeCZ zXZ`QyKViZkxC?>iP(}@<`8h&)^2sM{{K=CSUwqMmo%|{O!T-+%wzYRu$iStTSCoDdCZ zD&Pnbd8iv)t#6*Wa+l)xbdmO`G0+>um#t}n>N+0x7=z$&ge0tZD8lL%BSg;TW?XhvgP#PgAbZh&aY4?eD~?w$GV2B zy!LulD437;Aj-R;Z`~1=%Cxe&~c2A464#wQ6ro((pNGR;soSigVB}-(V**H;! zCAoF0);7=+f#R14ln@eu5<((SLP!Kk2#G)mArUAcBmyObM4*I_2$T>Kff7QF849!V zD_5e&& zn4vuIzymgt!PXWk`m`JQ{R7ID-eyKhlbnK1zSk1rkJoqY-ldr67;B~>UV8+fyEAsH zNZ}&c275{6IWBmNjGhd`Lrg(1#qEhV0h#spm8g;M$Ap15f5ILmu8*=RlaiE2xW+#P zg}D^Aw*Y~nHGf4t~G_CnvB<4TZiAw8(4F+@<=|A(U6W$&heyfCCsMK{CDx_U< z=bd-jMryi~RMYt^fEm=g>86`BdCC;sbmNUSJj5^`KNrM!Dx(!lLLiKuF~^$=-FO-?X@;I*uH&xgGX_g_ZWu&DmTY)?1CTW5Lhkg_q866X?mMvP^t~ENEKmjNd1#G0`rw0JTXe+%dG=m4i!{fZ1FWB6U z84BjZk0+q&tU5eZ?2gr#LqAefx2S`B}$gmu%SbB>5w7n+ozA299qk2hzZdU z$DeR4nnU3kv%;xj#*Cri!twtehNUeh1q4oHUA|jtO(3?aAT-#u&~6j`H?T+%UR}FM!6u& z4$Z)Jab{i<9OLl&7Zf=0jdcq!P#>zPtpX0hX7APF+E&B+*ixq}* z6I*eMm>BoK3KcBJ4CT)Vnt(>&6wf~U?7-^?Q1Bgm&+#&?@UK82rokby^kv=N3MkBs z73S}bO#iI>D^T*AM4*I_2$T>Kff7O@P(nxqN(iC6(W+&u2$W!nKnWocC?O;QC4}S& zh4HSLvt~x1cu=T;P&H@jn&IOAFT!{|r3XrwluD2Q6ev*(Wm`*r-T`*3Lb;Rz6vZ^G z1qnc*HbM3pT`1Z%FVw2-{)U17?SEAM%^s2`+!q-=n-r+yP+<28X#pq*rh~T78KiWV zpEo__B^0UHr9uH6Eus4F$x%=cG=_>dKhe$yPgG%B4(%HHR>7rKBpZ~7M4*I}{{p^4 V+^5A2pM?Mb002ovPDHLkV1hLh+n4|V literal 0 HcmV?d00001 diff --git a/tests/page/page-request-fulfill.spec.ts-snapshots/mock-svg-moz-firefox-nightly.png b/tests/page/page-request-fulfill.spec.ts-snapshots/mock-svg-moz-firefox-nightly.png new file mode 100644 index 0000000000000000000000000000000000000000..064d497091449bcd339d9bf2aab0950b07d2bf67 GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETJ)SO(AsNnZZyE}(9Y%e7H`*A^jC6GYZXv- zN(tgH#KT-1?o<_j)P2QYRtY&HZbfw`xdg72vb!Ryru!H_nGSY=z@xM5o2FQI&h`Ap Q4)h0ur>mdKI;Vst0OkExRsaA1 literal 0 HcmV?d00001 From 033d36e66510ed7343b771864764d65da1fe3af4 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 5 Sep 2025 16:43:21 -0700 Subject: [PATCH 091/329] feat(mcp): allow saving trace as a command (#37323) --- .../src/server/har/harTracer.ts | 13 ++-- .../src/mcp/browser/browserContextFactory.ts | 15 ++-- .../playwright/src/mcp/browser/context.ts | 14 +++- packages/playwright/src/mcp/browser/tools.ts | 4 +- .../src/mcp/browser/tools/tracing.ts | 76 +++++++++++++++++++ packages/playwright/src/mcp/config.d.ts | 2 +- tests/mcp/trace.spec.ts | 37 --------- tests/mcp/tracing.spec.ts | 69 +++++++++++++++++ 8 files changed, 174 insertions(+), 56 deletions(-) create mode 100644 packages/playwright/src/mcp/browser/tools/tracing.ts delete mode 100644 tests/mcp/trace.spec.ts create mode 100644 tests/mcp/tracing.spec.ts diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index dc70ad2079e5e..601f1821e554d 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -193,7 +193,7 @@ export class HarTracer { private _onAPIRequest(event: APIRequestEvent) { if (!this._shouldIncludeEntryWithUrl(event.url.toString())) return; - const harEntry = createHarEntry(event.method, event.url, undefined, this._options); + const harEntry = createHarEntry(undefined, event.method, event.url, undefined, this._options); harEntry._apiRequest = true; if (!this._options.omitCookies) harEntry.request.cookies = event.cookies; @@ -265,9 +265,7 @@ export class HarTracer { return; const pageEntry = this._createPageEntryIfNeeded(page); - const harEntry = createHarEntry(request.method(), url, request.frame()?.guid, this._options); - if (pageEntry) - harEntry.pageref = pageEntry.id; + const harEntry = createHarEntry(pageEntry?.id, request.method(), url, request.frame()?.guid, this._options); this._recordRequestHeadersAndCookies(harEntry, request.headers()); harEntry.request.postData = this._postDataForRequest(request, this._options.content); if (!this._options.omitSizes) @@ -608,10 +606,9 @@ export class HarTracer { } -function createHarEntry(method: string, url: URL, frameref: string | undefined, options: HarTracerOptions): har.Entry { +function createHarEntry(pageRef: string | undefined, method: string, url: URL, frameref: string | undefined, options: HarTracerOptions): har.Entry { const harEntry: har.Entry = { - _frameref: options.includeTraceInfo ? frameref : undefined, - _monotonicTime: options.includeTraceInfo ? monotonicTime() : undefined, + pageref: pageRef, startedDateTime: new Date().toISOString(), time: -1, request: { @@ -645,6 +642,8 @@ function createHarEntry(method: string, url: URL, frameref: string | undefined, wait: -1, receive: -1 }, + _frameref: options.includeTraceInfo ? frameref : undefined, + _monotonicTime: options.includeTraceInfo ? monotonicTime() : undefined, }; return harEntry; } diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index e4320b3a1a246..813e56e3ad967 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -103,8 +103,11 @@ class IsolatedContextFactory extends BaseContextFactory { protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise { await injectCdpPort(this.config.browser); const browserType = playwright[this.config.browser.browserName]; + const tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces`); + if (this.config.saveTrace) + await startTraceServer(this.config, tracesDir); return browserType.launch({ - tracesDir: await startTraceServer(this.config, clientInfo.rootPath), + tracesDir, ...this.config.browser.launchOptions, handleSIGINT: false, handleSIGTERM: false, @@ -167,7 +170,9 @@ class PersistentContextFactory implements BrowserContextFactory { await injectCdpPort(this.config.browser); testDebug('create browser context (persistent)'); const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath); - const tracesDir = await startTraceServer(this.config, clientInfo.rootPath); + const tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces`); + if (this.config.saveTrace) + await startTraceServer(this.config, tracesDir); this._userDataDirs.add(userDataDir); testDebug('lock user data dir', userDataDir); @@ -233,17 +238,15 @@ async function findFreePort(): Promise { }); } -async function startTraceServer(config: FullConfig, rootPath: string | undefined): Promise { +async function startTraceServer(config: FullConfig, tracesDir: string): Promise { if (!config.saveTrace) - return undefined; + return; - const tracesDir = await outputFile(config, rootPath, `traces-${Date.now()}`); const server = await startTraceViewerServer(); const urlPrefix = server.urlPrefix('human-readable'); const url = urlPrefix + '/trace/index.html?trace=' + tracesDir + '/trace.json'; // eslint-disable-next-line no-console console.error('\nTrace viewer listening on ' + url); - return tracesDir; } function createHash(data: string): string { diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts index a4bd59088799f..18b13d5682942 100644 --- a/packages/playwright/src/mcp/browser/context.ts +++ b/packages/playwright/src/mcp/browser/context.ts @@ -27,6 +27,7 @@ import type { Tool } from './tools/tool'; import type { BrowserContextFactory, ClientInfo } from './browserContextFactory'; import type * as actions from './actions'; import type { SessionLog } from './sessionLog'; +import type { Tracing } from '../../../../playwright-core/src/client/tracing'; const testDebug = debug('pw:mcp:test'); @@ -189,6 +190,11 @@ export class Context { } } + async ensureBrowserContext(): Promise { + const { browserContext } = await this._ensureBrowserContext(); + return browserContext; + } + private _ensureBrowserContext() { if (!this._browserContextPromise) { this._browserContextPromise = this._setupBrowserContext(); @@ -212,11 +218,11 @@ export class Context { this._onPageCreated(page); browserContext.on('page', page => this._onPageCreated(page)); if (this.config.saveTrace) { - await browserContext.tracing.start({ - name: 'trace', - screenshots: false, + await (browserContext.tracing as Tracing).start({ + name: 'trace-' + Date.now(), + screenshots: true, snapshots: true, - sources: false, + _live: true, }); } return result; diff --git a/packages/playwright/src/mcp/browser/tools.ts b/packages/playwright/src/mcp/browser/tools.ts index b1e8059f914cb..408058ab9978d 100644 --- a/packages/playwright/src/mcp/browser/tools.ts +++ b/packages/playwright/src/mcp/browser/tools.ts @@ -27,8 +27,9 @@ import navigate from './tools/navigate'; import network from './tools/network'; import pdf from './tools/pdf'; import snapshot from './tools/snapshot'; -import tabs from './tools/tabs'; import screenshot from './tools/screenshot'; +import tabs from './tools/tabs'; +import tracing from './tools/tracing'; import wait from './tools/wait'; import verify from './tools/verify'; @@ -51,6 +52,7 @@ export const allTools: Tool[] = [ ...screenshot, ...snapshot, ...tabs, + ...tracing, ...wait, ...verify, ]; diff --git a/packages/playwright/src/mcp/browser/tools/tracing.ts b/packages/playwright/src/mcp/browser/tools/tracing.ts new file mode 100644 index 0000000000000..828386c40d29c --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/tracing.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from '../../sdk/bundle'; +import { defineTool } from './tool'; + +import type { Tracing } from '../../../../../playwright-core/src/client/tracing'; + +const tracingStart = defineTool({ + capability: 'tracing', + + schema: { + name: 'browser_start_tracing', + title: 'Start tracing', + description: 'Start trace recording', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async (context, params, response) => { + const browserContext = await context.ensureBrowserContext(); + const tracesDir = await context.outputFile(`traces`); + const name = 'trace-' + Date.now(); + await (browserContext.tracing as Tracing).start({ + name, + screenshots: true, + snapshots: true, + _live: true, + }); + const traceLegend = `- Action log: ${tracesDir}/${name}.trace +- Network log: ${tracesDir}/${name}.network +- Resources with content by sha1: ${tracesDir}/resources`; + + response.addResult(`Tracing started, saving to ${tracesDir}.\n${traceLegend}`); + (browserContext.tracing as any)[traceLegendSymbol] = traceLegend; + }, +}); + +const tracingStop = defineTool({ + capability: 'tracing', + + schema: { + name: 'browser_stop_tracing', + title: 'Stop tracing', + description: 'Stop trace recording', + inputSchema: z.object({}), + type: 'readOnly', + }, + + handle: async (context, params, response) => { + const browserContext = await context.ensureBrowserContext(); + await browserContext.tracing.stop(); + const traceLegend = (browserContext.tracing as any)[traceLegendSymbol]; + response.addResult(`Tracing stopped.\n${traceLegend}`); + }, +}); + +export default [ + tracingStart, + tracingStop, +]; + +const traceLegendSymbol = Symbol('tracesDir'); diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index 09a999d88ca4d..fd42445a6e646 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -16,7 +16,7 @@ import type * as playwright from 'playwright-core'; -export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'verify'; +export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'verify' | 'tracing'; export type Config = { /** diff --git a/tests/mcp/trace.spec.ts b/tests/mcp/trace.spec.ts deleted file mode 100644 index 927f23a798ab3..0000000000000 --- a/tests/mcp/trace.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; - -import { test, expect } from './fixtures'; - -test('check that trace is saved', async ({ startClient, server, mcpMode }, testInfo) => { - const outputDir = testInfo.outputPath('output'); - - const { client } = await startClient({ - args: ['--save-trace', `--output-dir=${outputDir}`], - }); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - code: expect.stringContaining(`page.goto('http://localhost`), - }); - - const [file] = await fs.promises.readdir(outputDir); - expect(file).toContain('traces'); -}); diff --git a/tests/mcp/tracing.spec.ts b/tests/mcp/tracing.spec.ts new file mode 100644 index 0000000000000..23bc94fee5054 --- /dev/null +++ b/tests/mcp/tracing.spec.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import { test, expect } from './fixtures'; + +test('check that trace is saved with --save-trace', async ({ startClient, server, mcpMode }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + + const { client } = await startClient({ + args: ['--save-trace', `--output-dir=${outputDir}`], + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + const [file] = await fs.promises.readdir(outputDir); + expect(file).toContain('traces'); +}); + +test('check that trace is saved with browser_start_tracing', async ({ startClient, server, mcpMode }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + + const { client } = await startClient({ args: [`--output-dir=${outputDir}`, '--caps=tracing'] }); + + expect(await client.callTool({ + name: 'browser_start_tracing', + })).toHaveResponse({ + result: expect.stringContaining(`Tracing started, saving to ${outputDir}`), + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + expect(await client.callTool({ + name: 'browser_stop_tracing', + })).toHaveResponse({ + result: expect.stringMatching(/trace-\d+.trace/) + }); + + const files = await fs.promises.readdir(path.join(outputDir, 'traces')); + expect(files).toEqual([ + 'resources', + expect.stringMatching(/trace-\d+\.network/), + expect.stringMatching(/trace-\d+\.trace/), + ]); +}); From 44327df00d45b5d54d878cee4f0ff8a01b29f851 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 5 Sep 2025 19:13:11 -0700 Subject: [PATCH 092/329] fix(mcp): retry list roots request with streaming http (#37324) --- packages/playwright/src/mcp/sdk/server.ts | 13 ++++++++++-- tests/mcp/http.spec.ts | 26 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/playwright/src/mcp/sdk/server.ts b/packages/playwright/src/mcp/sdk/server.ts index bf80d80494050..a50f2d80f5a65 100644 --- a/packages/playwright/src/mcp/sdk/server.ts +++ b/packages/playwright/src/mcp/sdk/server.ts @@ -95,8 +95,17 @@ export function createServer(name: string, version: string, backend: ServerBacke const capabilities = server.getClientCapabilities(); let clientRoots: Root[] = []; if (capabilities?.roots) { - const { roots } = await server.listRoots(undefined, { timeout: 2_000 }).catch(() => ({ roots: [] })); - clientRoots = roots; + for (let i = 0; i < 2; i++) { + try { + // In the @modelcontextprotocol TypeScript SDK (and Cursor) in the streaming http + // mode, the SSE channel is not ready yet, when `initialized` notification arrives, + // `listRoots` times out in that case and we retry once. + const { roots } = await server.listRoots(undefined, { timeout: 2_000 }); + clientRoots = roots; + } catch (e) { + continue; + } + } } const clientVersion = server.getClientVersion() ?? { name: 'unknown', version: 'unknown' }; await backend.initialize?.(server, clientVersion, clientRoots); diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index 69cc63d41c4ab..83ed6b86f56d2 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -22,6 +22,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { test as baseTest, expect, programPath } from './fixtures'; import type { Config } from '../../packages/playwright/src/mcp/config'; +import { ListRootsRequestSchema } from 'packages/playwright/lib/mcp/sdk/bundle'; const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { @@ -252,3 +253,28 @@ test('http transport (default)', async ({ serverEndpoint }) => { await client.ping(); expect(transport.sessionId, 'has session support').toBeDefined(); }); + +test('client should receive list roots request', async ({ serverEndpoint }) => { + const { url } = await serverEndpoint(); + const transport = new StreamableHTTPClientTransport(url); + const client = new Client({ name: 'test', version: '1.0.0' }, { capabilities: { roots: {} } }); + let rootsListedCallback; + const rootsListedPromise = new Promise((resolve, reject) => { + rootsListedCallback = resolve; + setTimeout(() => reject(new Error('timeout waiting for ListRootsRequestSchema')), 5_000); + }); + client.setRequestHandler(ListRootsRequestSchema, async request => { + rootsListedCallback('success'); + return { + roots: [ + { + name: 'test', + uri: 'file://tmp/', + } + ], + }; + }); + await client.connect(transport); + await client.ping(); + expect(await rootsListedPromise).toBe('success'); +}); From 4d9e815ebb3cc8be5fda0c18b221ad6d9fe47a43 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 5 Sep 2025 19:43:23 -0700 Subject: [PATCH 093/329] fix(mcp): do not disable extensions in persistent mode (#37325) --- docs/src/api/params.md | 11 +++++++--- packages/playwright-client/types/types.d.ts | 15 +++----------- .../playwright-core/src/browserServerImpl.ts | 4 ++-- .../src/client/clientHelper.ts | 3 +-- .../playwright-core/src/client/electron.ts | 4 ++-- packages/playwright-core/src/client/types.ts | 3 +-- .../src/server/bidi/bidiChromium.ts | 3 +-- .../src/server/bidi/bidiFirefox.ts | 3 +-- .../playwright-core/src/server/browserType.ts | 3 +-- .../src/server/chromium/chromium.ts | 3 +-- .../src/server/firefox/firefox.ts | 3 +-- .../src/server/utils/processLauncher.ts | 10 ++++------ .../src/server/webkit/webkit.ts | 3 +-- packages/playwright-core/types/types.d.ts | 15 +++----------- .../src/mcp/browser/browserContextFactory.ts | 20 ++++++++++++------- utils/generate_types/test/test.ts | 4 ++-- 16 files changed, 45 insertions(+), 62 deletions(-) diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 8cb60f038f65a..993b8bc2964e6 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -235,14 +235,18 @@ Dangerous option; use with care. Defaults to `false`. Network proxy settings. +## js-browser-option-env +* langs: js +- `env` <[Object]<[string], [string]|[undefined]>> + ## csharp-java-browser-option-env * langs: csharp, java - `env` <[Object]<[string], [string]>> Specify environment variables that will be visible to the browser. Defaults to `process.env`. -## js-python-browser-option-env -* langs: js, python +## python-browser-option-env +* langs: python - `env` <[Object]<[string], [string]|[float]|[boolean]>> Specify environment variables that will be visible to the browser. Defaults to `process.env`. @@ -1122,7 +1126,8 @@ Slows down Playwright operations by the specified amount of milliseconds. Useful - %%-browser-option-devtools-%% - %%-browser-option-downloadspath-%% - %%-csharp-java-browser-option-env-%% -- %%-js-python-browser-option-env-%% +- %%-js-browser-option-env-%% +- %%-python-browser-option-env-%% - %%-browser-option-executablepath-%% - %%-browser-option-handlesigint-%% - %%-browser-option-handlesigterm-%% diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 9d759a2bc2103..2f6edbe3b372e 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -14918,10 +14918,7 @@ export interface BrowserType { */ downloadsPath?: string; - /** - * Specify environment variables that will be visible to the browser. Defaults to `process.env`. - */ - env?: { [key: string]: string|number|boolean; }; + env?: { [key: string]: string|undefined; }; /** * Path to a browser executable to run instead of the bundled one. If @@ -15351,10 +15348,7 @@ export interface BrowserType { */ downloadsPath?: string; - /** - * Specify environment variables that will be visible to the browser. Defaults to `process.env`. - */ - env?: { [key: string]: string|number|boolean; }; + env?: { [key: string]: string|undefined; }; /** * Path to a browser executable to run instead of the bundled one. If @@ -21766,10 +21760,7 @@ export interface LaunchOptions { */ downloadsPath?: string; - /** - * Specify environment variables that will be visible to the browser. Defaults to `process.env`. - */ - env?: { [key: string]: string|number|boolean; }; + env?: { [key: string]: string|undefined; }; /** * Path to a browser executable to run instead of the bundled one. If diff --git a/packages/playwright-core/src/browserServerImpl.ts b/packages/playwright-core/src/browserServerImpl.ts index c789991584afc..a9b1e97ae956c 100644 --- a/packages/playwright-core/src/browserServerImpl.ts +++ b/packages/playwright-core/src/browserServerImpl.ts @@ -26,7 +26,7 @@ import * as validatorPrimitives from './protocol/validatorPrimitives'; import { ProgressController } from './server/progress'; import type { BrowserServer, BrowserServerLauncher } from './client/browserType'; -import type { LaunchOptions, LaunchServerOptions, Logger, Env } from './client/types'; +import type { LaunchOptions, LaunchServerOptions, Logger } from './client/types'; import type { ProtocolLogger } from './server/types'; import type { WebSocketEventEmitter } from './utilsBundle'; import type { Browser } from './server/browser'; @@ -112,7 +112,7 @@ function toProtocolLogger(logger: Logger | undefined): ProtocolLogger | undefine } : undefined; } -function envObjectToArray(env: Env): { name: string, value: string }[] { +function envObjectToArray(env: NodeJS.ProcessEnv): { name: string, value: string }[] { const result: { name: string, value: string }[] = []; for (const name in env) { if (!Object.is(env[name], undefined)) diff --git a/packages/playwright-core/src/client/clientHelper.ts b/packages/playwright-core/src/client/clientHelper.ts index aecc92a156380..6c9b8aad1bfe4 100644 --- a/packages/playwright-core/src/client/clientHelper.ts +++ b/packages/playwright-core/src/client/clientHelper.ts @@ -17,10 +17,9 @@ import { isString } from '../utils/isomorphic/rtti'; -import type * as types from './types'; import type { Platform } from './platform'; -export function envObjectToArray(env: types.Env): { name: string, value: string }[] { +export function envObjectToArray(env: NodeJS.ProcessEnv): { name: string, value: string }[] { const result: { name: string, value: string }[] = []; for (const name in env) { if (!Object.is(env[name], undefined)) diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index fc33350d0750f..083e7b6f9f22b 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -25,7 +25,7 @@ import { Waiter } from './waiter'; import { TimeoutSettings } from './timeoutSettings'; import type { Page } from './page'; -import type { BrowserContextOptions, Env, Headers, WaitForEventOptions } from './types'; +import type { BrowserContextOptions, Headers, WaitForEventOptions } from './types'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type * as channels from '@protocol/channels'; @@ -34,7 +34,7 @@ import type { BrowserWindow } from 'electron'; import type { Playwright } from './playwright'; type ElectronOptions = Omit & { - env?: Env, + env?: NodeJS.ProcessEnv, extraHTTPHeaders?: Headers, recordHar?: BrowserContextOptions['recordHar'], colorScheme?: 'dark' | 'light' | 'no-preference' | null, diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index bcdf728423a89..5b0b10b5949b1 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -28,7 +28,6 @@ export interface Logger { export type TimeoutOptions = { timeout?: number }; export type StrictOptions = { strict?: boolean }; export type Headers = { [key: string]: string }; -export type Env = { [key: string]: string | number | boolean | undefined }; export type WaitForEventOptions = Function | TimeoutOptions & { predicate?: Function }; export type WaitForFunctionOptions = TimeoutOptions & { polling?: 'raf' | number }; @@ -88,7 +87,7 @@ export type BrowserContextOptions = Omit; - abstract amendEnvironment(env: Env, userDataDir: string, isPersistent: boolean): Env; + abstract amendEnvironment(env: NodeJS.ProcessEnv, userDataDir: string, isPersistent: boolean): NodeJS.ProcessEnv; abstract doRewriteStartupLog(error: ProtocolError): ProtocolError; abstract attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void; } diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index 60d9ccdbe3cb3..c406cc7e87438 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -41,7 +41,6 @@ import { gracefullyCloseSet } from '../utils/processLauncher'; import type { HTTPRequestParams } from '../utils/network'; import type { BrowserOptions, BrowserProcess } from '../browser'; import type { SdkObject } from '../instrumentation'; -import type { Env } from '../utils/processLauncher'; import type { Progress } from '../progress'; import type { ProtocolError } from '../protocolError'; import type { ConnectionTransport, ProtocolRequest } from '../transport'; @@ -165,7 +164,7 @@ export class Chromium extends BrowserType { return error; } - override amendEnvironment(env: Env): Env { + override amendEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { return env; } diff --git a/packages/playwright-core/src/server/firefox/firefox.ts b/packages/playwright-core/src/server/firefox/firefox.ts index 79dca474658a1..c9ffbecc74f39 100644 --- a/packages/playwright-core/src/server/firefox/firefox.ts +++ b/packages/playwright-core/src/server/firefox/firefox.ts @@ -26,7 +26,6 @@ import { ManualPromise } from '../../utils/isomorphic/manualPromise'; import type { BrowserOptions } from '../browser'; import type { SdkObject } from '../instrumentation'; -import type { Env } from '../utils/processLauncher'; import type { ProtocolError } from '../protocolError'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; @@ -52,7 +51,7 @@ export class Firefox extends BrowserType { return error; } - override amendEnvironment(env: Env): Env { + override amendEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { if (!path.isAbsolute(os.homedir())) throw new Error(`Cannot launch Firefox with relative home directory. Did you set ${os.platform() === 'win32' ? 'USERPROFILE' : 'HOME'} to a relative path?`); if (os.platform() === 'linux') { diff --git a/packages/playwright-core/src/server/utils/processLauncher.ts b/packages/playwright-core/src/server/utils/processLauncher.ts index e027708c1b54b..148b340ec7aa0 100644 --- a/packages/playwright-core/src/server/utils/processLauncher.ts +++ b/packages/playwright-core/src/server/utils/processLauncher.ts @@ -22,12 +22,10 @@ import * as readline from 'readline'; import { removeFolders } from './fileUtils'; import { isUnderTest } from '../../utils'; -export type Env = {[key: string]: string | number | boolean | undefined}; - export type LaunchProcessOptions = { command: string, args?: string[], - env?: Env, + env?: NodeJS.ProcessEnv, shell?: boolean, handleSIGINT?: boolean, @@ -138,7 +136,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise { */ downloadsPath?: string; - /** - * Specify environment variables that will be visible to the browser. Defaults to `process.env`. - */ - env?: { [key: string]: string|number|boolean; }; + env?: { [key: string]: string|undefined; }; /** * Path to a browser executable to run instead of the bundled one. If @@ -15351,10 +15348,7 @@ export interface BrowserType { */ downloadsPath?: string; - /** - * Specify environment variables that will be visible to the browser. Defaults to `process.env`. - */ - env?: { [key: string]: string|number|boolean; }; + env?: { [key: string]: string|undefined; }; /** * Path to a browser executable to run instead of the bundled one. If @@ -21766,10 +21760,7 @@ export interface LaunchOptions { */ downloadsPath?: string; - /** - * Specify environment variables that will be visible to the browser. Defaults to `process.env`. - */ - env?: { [key: string]: string|number|boolean; }; + env?: { [key: string]: string|undefined; }; /** * Path to a browser executable to run instead of the bundled one. If diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index 813e56e3ad967..2c39ba47f5917 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -26,6 +26,7 @@ import { logUnhandledError, testDebug } from '../log'; import { outputFile } from './config'; import type { FullConfig } from './config'; +import type { LaunchOptions } from '../../../../playwright-core/src/client/types'; export function contextFactory(config: FullConfig): BrowserContextFactory { if (config.browser.remoteEndpoint) @@ -179,14 +180,19 @@ class PersistentContextFactory implements BrowserContextFactory { const browserType = playwright[this.config.browser.browserName]; for (let i = 0; i < 5; i++) { + const launchOptions: LaunchOptions = { + tracesDir, + ...this.config.browser.launchOptions, + ...this.config.browser.contextOptions, + handleSIGINT: false, + handleSIGTERM: false, + ignoreDefaultArgs: [ + '--disable-extensions', + ], + assistantMode: true, + }; try { - const browserContext = await browserType.launchPersistentContext(userDataDir, { - tracesDir, - ...this.config.browser.launchOptions, - ...this.config.browser.contextOptions, - handleSIGINT: false, - handleSIGTERM: false, - }); + const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions); const close = () => this._closeBrowserContext(browserContext, userDataDir); return { browserContext, close }; } catch (error: any) { diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index 7aec26a418f88..a71dab64b0b6a 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -228,9 +228,9 @@ playwright.chromium.launch().then(async browser => { const launchOptions: playwright.LaunchOptions = { devtools: true, env: { - TIMEOUT: 52, + TIMEOUT: '52', SOMETHING: '/some/path', - JEST_TEST: true + JEST_TEST: 'true' } }; const browser = await playwright.chromium.launch(launchOptions); From 3577d195e5a9974000b31435f78a5de383dd5ef3 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 09:29:50 +0200 Subject: [PATCH 094/329] feat(chromium-tip-of-tree): roll to r1366 (#37330) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 89711b68db78c..ae5677bcf47da 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,15 +15,15 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1365", + "revision": "1366", "installByDefault": false, - "browserVersion": "141.0.7390.0" + "browserVersion": "142.0.7393.0" }, { "name": "chromium-tip-of-tree-headless-shell", - "revision": "1365", + "revision": "1366", "installByDefault": false, - "browserVersion": "141.0.7390.0" + "browserVersion": "142.0.7393.0" }, { "name": "firefox", From f94a28e469d1508d7b4d2af4a9524ba916c5a954 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 09:43:39 +0200 Subject: [PATCH 095/329] feat(chromium): roll to r1190 (#37329) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- README.md | 4 +- packages/playwright-core/browsers.json | 8 +- .../src/server/chromium/protocol.d.ts | 13954 ++++++++-------- .../src/server/deviceDescriptorsSource.json | 108 +- packages/playwright-core/types/protocol.d.ts | 13954 ++++++++-------- 5 files changed, 14228 insertions(+), 13800 deletions(-) diff --git a/README.md b/README.md index 4be427c5ae676..ec21ec50b5ff4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-140.0.7339.41-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-141.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-141.0.7390.7-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-141.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 140.0.7339.41 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 141.0.7390.7 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 26.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 141.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index ae5677bcf47da..b34293e5e6794 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,15 +3,15 @@ "browsers": [ { "name": "chromium", - "revision": "1189", + "revision": "1190", "installByDefault": true, - "browserVersion": "140.0.7339.41" + "browserVersion": "141.0.7390.7" }, { "name": "chromium-headless-shell", - "revision": "1189", + "revision": "1190", "installByDefault": true, - "browserVersion": "140.0.7339.41" + "browserVersion": "141.0.7390.7" }, { "name": "chromium-tip-of-tree", diff --git a/packages/playwright-core/src/server/chromium/protocol.d.ts b/packages/playwright-core/src/server/chromium/protocol.d.ts index 1fab022bfb268..5d3c9d26c1308 100644 --- a/packages/playwright-core/src/server/chromium/protocol.d.ts +++ b/packages/playwright-core/src/server/chromium/protocol.d.ts @@ -1050,7 +1050,7 @@ registrations being ignored. */ propertyValue?: string; } - export type UserReidentificationIssueType = "BlockedFrameNavigation"|"BlockedSubresource"; + export type UserReidentificationIssueType = "BlockedFrameNavigation"|"BlockedSubresource"|"NoisedCanvasReadback"; /** * This issue warns about uses of APIs that may be considered misuse to re-identify users. @@ -1061,6 +1061,10 @@ re-identify users. * Applies to BlockedFrameNavigation and BlockedSubresource issue types. */ request?: AffectedRequest; + /** + * Applies to NoisedCanvasReadback issue type. + */ + sourceCodeLocation?: SourceCodeLocation; } /** * A unique identifier for the type of issue. Each type may use one of the @@ -1197,125 +1201,6 @@ using Audits.issueAdded event. } } - /** - * Defines commands and events for browser extensions. - */ - export module Extensions { - /** - * Storage areas. - */ - export type StorageArea = "session"|"local"|"sync"|"managed"; - - - /** - * Installs an unpacked extension from the filesystem similar to ---load-extension CLI flags. Returns extension ID once the extension -has been installed. Available if the client is connected using the ---remote-debugging-pipe flag and the --enable-unsafe-extension-debugging -flag is set. - */ - export type loadUnpackedParameters = { - /** - * Absolute file path. - */ - path: string; - } - export type loadUnpackedReturnValue = { - /** - * Extension id. - */ - id: string; - } - /** - * Uninstalls an unpacked extension (others not supported) from the profile. -Available if the client is connected using the --remote-debugging-pipe flag -and the --enable-unsafe-extension-debugging. - */ - export type uninstallParameters = { - /** - * Extension id. - */ - id: string; - } - export type uninstallReturnValue = { - } - /** - * Gets data from extension storage in the given `storageArea`. If `keys` is -specified, these are used to filter the result. - */ - export type getStorageItemsParameters = { - /** - * ID of extension. - */ - id: string; - /** - * StorageArea to retrieve data from. - */ - storageArea: StorageArea; - /** - * Keys to retrieve. - */ - keys?: string[]; - } - export type getStorageItemsReturnValue = { - data: { [key: string]: string }; - } - /** - * Removes `keys` from extension storage in the given `storageArea`. - */ - export type removeStorageItemsParameters = { - /** - * ID of extension. - */ - id: string; - /** - * StorageArea to remove data from. - */ - storageArea: StorageArea; - /** - * Keys to remove. - */ - keys: string[]; - } - export type removeStorageItemsReturnValue = { - } - /** - * Clears extension storage in the given `storageArea`. - */ - export type clearStorageItemsParameters = { - /** - * ID of extension. - */ - id: string; - /** - * StorageArea to remove data from. - */ - storageArea: StorageArea; - } - export type clearStorageItemsReturnValue = { - } - /** - * Sets `values` in extension storage in the given `storageArea`. The provided `values` -will be merged with existing values in the storage area. - */ - export type setStorageItemsParameters = { - /** - * ID of extension. - */ - id: string; - /** - * StorageArea to set data in. - */ - storageArea: StorageArea; - /** - * Values to set. - */ - values: { [key: string]: string }; - } - export type setStorageItemsReturnValue = { - } - } - /** * Defines commands and events for Autofill. */ @@ -1580,4404 +1465,4669 @@ events afterwards if enabled and recording. } /** - * The Browser domain defines methods and events for browser managing. + * This domain allows configuring virtual Bluetooth devices to test +the web-bluetooth API. */ - export module Browser { - export type BrowserContextID = string; - export type WindowID = number; + export module BluetoothEmulation { /** - * The state of the browser window. + * Indicates the various states of Central. */ - export type WindowState = "normal"|"minimized"|"maximized"|"fullscreen"; + export type CentralState = "absent"|"powered-off"|"powered-on"; /** - * Browser window bounds information + * Indicates the various types of GATT event. */ - export interface Bounds { - /** - * The offset from the left edge of the screen to the window in pixels. - */ - left?: number; - /** - * The offset from the top edge of the screen to the window in pixels. - */ - top?: number; - /** - * The window width in pixels. - */ - width?: number; - /** - * The window height in pixels. - */ - height?: number; - /** - * The window state. Default to normal. - */ - windowState?: WindowState; - } - export type PermissionType = "ar"|"audioCapture"|"automaticFullscreen"|"backgroundFetch"|"backgroundSync"|"cameraPanTiltZoom"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"geolocation"|"handTracking"|"idleDetection"|"keyboardLock"|"localFonts"|"localNetworkAccess"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"pointerLock"|"protectedMediaIdentifier"|"sensors"|"smartCard"|"speakerSelection"|"storageAccess"|"topLevelStorageAccess"|"videoCapture"|"vr"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"webPrinting"|"windowManagement"; - export type PermissionSetting = "granted"|"denied"|"prompt"; + export type GATTOperationType = "connection"|"discovery"; /** - * Definition of PermissionDescriptor defined in the Permissions API: -https://w3c.github.io/permissions/#dom-permissiondescriptor. + * Indicates the various types of characteristic write. */ - export interface PermissionDescriptor { - /** - * Name of permission. -See https://cs.chromium.org/chromium/src/third_party/blink/renderer/modules/permissions/permission_descriptor.idl for valid permission names. - */ - name: string; - /** - * For "midi" permission, may also specify sysex control. - */ - sysex?: boolean; - /** - * For "push" permission, may specify userVisibleOnly. -Note that userVisibleOnly = true is the only currently supported type. - */ - userVisibleOnly?: boolean; - /** - * For "clipboard" permission, may specify allowWithoutSanitization. - */ - allowWithoutSanitization?: boolean; - /** - * For "fullscreen" permission, must specify allowWithoutGesture:true. - */ - allowWithoutGesture?: boolean; - /** - * For "camera" permission, may specify panTiltZoom. - */ - panTiltZoom?: boolean; - } + export type CharacteristicWriteType = "write-default-deprecated"|"write-with-response"|"write-without-response"; /** - * Browser command ids used by executeBrowserCommand. + * Indicates the various types of characteristic operation. */ - export type BrowserCommandId = "openTabSearch"|"closeTabSearch"|"openGlic"; + export type CharacteristicOperationType = "read"|"write"|"subscribe-to-notifications"|"unsubscribe-from-notifications"; /** - * Chrome histogram bucket. + * Indicates the various types of descriptor operation. */ - export interface Bucket { - /** - * Minimum value (inclusive). - */ - low: number; + export type DescriptorOperationType = "read"|"write"; + /** + * Stores the manufacturer data + */ + export interface ManufacturerData { /** - * Maximum value (exclusive). + * Company identifier +https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml +https://usb.org/developers */ - high: number; + key: number; /** - * Number of samples. + * Manufacturer-specific data */ - count: number; + data: binary; } /** - * Chrome histogram. + * Stores the byte data of the advertisement packet sent by a Bluetooth device. */ - export interface Histogram { - /** - * Name. - */ - name: string; + export interface ScanRecord { + name?: string; + uuids?: string[]; /** - * Sum of sample values. + * Stores the external appearance description of the device. */ - sum: number; + appearance?: number; /** - * Total number of samples. + * Stores the transmission power of a broadcasting device. */ - count: number; + txPower?: number; /** - * Buckets. + * Key is the company identifier and the value is an array of bytes of +manufacturer specific data. */ - buckets: Bucket[]; + manufacturerData?: ManufacturerData[]; } - export type PrivacySandboxAPI = "BiddingAndAuctionServices"|"TrustedKeyValue"; - /** - * Fired when page is about to start a download. + * Stores the advertisement packet information that is sent by a Bluetooth device. */ - export type downloadWillBeginPayload = { - /** - * Id of the frame that caused the download to begin. - */ - frameId: Page.FrameId; - /** - * Global unique identifier of the download. - */ - guid: string; - /** - * URL of the resource being downloaded. - */ - url: string; - /** - * Suggested file name of the resource (the actual name of the file saved on disk may differ). - */ - suggestedFilename: string; + export interface ScanEntry { + deviceAddress: string; + rssi: number; + scanRecord: ScanRecord; } /** - * Fired when download makes progress. Last call has |done| == true. + * Describes the properties of a characteristic. This follows Bluetooth Core +Specification BT 4.2 Vol 3 Part G 3.3.1. Characteristic Properties. */ - export type downloadProgressPayload = { - /** - * Global unique identifier of the download. - */ - guid: string; - /** - * Total expected bytes to download. - */ - totalBytes: number; - /** - * Total bytes received. - */ - receivedBytes: number; - /** - * Download status. - */ - state: "inProgress"|"completed"|"canceled"; - /** - * If download is "completed", provides the path of the downloaded file. -Depending on the platform, it is not guaranteed to be set, nor the file -is guaranteed to exist. - */ - filePath?: string; + export interface CharacteristicProperties { + broadcast?: boolean; + read?: boolean; + writeWithoutResponse?: boolean; + write?: boolean; + notify?: boolean; + indicate?: boolean; + authenticatedSignedWrites?: boolean; + extendedProperties?: boolean; } /** - * Set permission settings for given origin. + * Event for when a GATT operation of |type| to the peripheral with |address| +happened. */ - export type setPermissionParameters = { - /** - * Descriptor of permission to override. - */ - permission: PermissionDescriptor; - /** - * Setting of the permission. - */ - setting: PermissionSetting; - /** - * Origin the permission applies to, all origins if not specified. - */ - origin?: string; - /** - * Context to override. When omitted, default browser context is used. - */ - browserContextId?: BrowserContextID; + export type gattOperationReceivedPayload = { + address: string; + type: GATTOperationType; } - export type setPermissionReturnValue = { + /** + * Event for when a characteristic operation of |type| to the characteristic +respresented by |characteristicId| happened. |data| and |writeType| is +expected to exist when |type| is write. + */ + export type characteristicOperationReceivedPayload = { + characteristicId: string; + type: CharacteristicOperationType; + data?: binary; + writeType?: CharacteristicWriteType; } /** - * Grant specific permissions to the given origin and reject all others. + * Event for when a descriptor operation of |type| to the descriptor +respresented by |descriptorId| happened. |data| is expected to exist when +|type| is write. */ - export type grantPermissionsParameters = { - permissions: PermissionType[]; + export type descriptorOperationReceivedPayload = { + descriptorId: string; + type: DescriptorOperationType; + data?: binary; + } + + /** + * Enable the BluetoothEmulation domain. + */ + export type enableParameters = { /** - * Origin the permission applies to, all origins if not specified. + * State of the simulated central. */ - origin?: string; + state: CentralState; /** - * BrowserContext to override permissions. When omitted, default browser context is used. + * If the simulated central supports low-energy. */ - browserContextId?: BrowserContextID; + leSupported: boolean; } - export type grantPermissionsReturnValue = { + export type enableReturnValue = { } /** - * Reset all permission management for all origins. + * Set the state of the simulated central. */ - export type resetPermissionsParameters = { + export type setSimulatedCentralStateParameters = { /** - * BrowserContext to reset permissions. When omitted, default browser context is used. + * State of the simulated central. */ - browserContextId?: BrowserContextID; + state: CentralState; } - export type resetPermissionsReturnValue = { + export type setSimulatedCentralStateReturnValue = { } /** - * Set the behavior when downloading a file. + * Disable the BluetoothEmulation domain. */ - export type setDownloadBehaviorParameters = { - /** - * Whether to allow all or deny all download requests, or use default Chrome behavior if -available (otherwise deny). |allowAndName| allows download and names files according to -their download guids. - */ - behavior: "deny"|"allow"|"allowAndName"|"default"; - /** - * BrowserContext to set download behavior. When omitted, default browser context is used. - */ - browserContextId?: BrowserContextID; - /** - * The default path to save downloaded files to. This is required if behavior is set to 'allow' -or 'allowAndName'. - */ - downloadPath?: string; - /** - * Whether to emit download events (defaults to false). - */ - eventsEnabled?: boolean; + export type disableParameters = { } - export type setDownloadBehaviorReturnValue = { + export type disableReturnValue = { } /** - * Cancel a download if in progress + * Simulates a peripheral with |address|, |name| and |knownServiceUuids| +that has already been connected to the system. */ - export type cancelDownloadParameters = { - /** - * Global unique identifier of the download. - */ - guid: string; - /** - * BrowserContext to perform the action in. When omitted, default browser context is used. - */ - browserContextId?: BrowserContextID; + export type simulatePreconnectedPeripheralParameters = { + address: string; + name: string; + manufacturerData: ManufacturerData[]; + knownServiceUuids: string[]; } - export type cancelDownloadReturnValue = { + export type simulatePreconnectedPeripheralReturnValue = { } /** - * Close browser gracefully. + * Simulates an advertisement packet described in |entry| being received by +the central. */ - export type closeParameters = { + export type simulateAdvertisementParameters = { + entry: ScanEntry; } - export type closeReturnValue = { + export type simulateAdvertisementReturnValue = { } /** - * Crashes browser on the main thread. + * Simulates the response code from the peripheral with |address| for a +GATT operation of |type|. The |code| value follows the HCI Error Codes from +Bluetooth Core Specification Vol 2 Part D 1.3 List Of Error Codes. */ - export type crashParameters = { + export type simulateGATTOperationResponseParameters = { + address: string; + type: GATTOperationType; + code: number; } - export type crashReturnValue = { + export type simulateGATTOperationResponseReturnValue = { } /** - * Crashes GPU process. + * Simulates the response from the characteristic with |characteristicId| for a +characteristic operation of |type|. The |code| value follows the Error +Codes from Bluetooth Core Specification Vol 3 Part F 3.4.1.1 Error Response. +The |data| is expected to exist when simulating a successful read operation +response. */ - export type crashGpuProcessParameters = { + export type simulateCharacteristicOperationResponseParameters = { + characteristicId: string; + type: CharacteristicOperationType; + code: number; + data?: binary; } - export type crashGpuProcessReturnValue = { + export type simulateCharacteristicOperationResponseReturnValue = { } /** - * Returns version information. + * Simulates the response from the descriptor with |descriptorId| for a +descriptor operation of |type|. The |code| value follows the Error +Codes from Bluetooth Core Specification Vol 3 Part F 3.4.1.1 Error Response. +The |data| is expected to exist when simulating a successful read operation +response. */ - export type getVersionParameters = { + export type simulateDescriptorOperationResponseParameters = { + descriptorId: string; + type: DescriptorOperationType; + code: number; + data?: binary; } - export type getVersionReturnValue = { - /** - * Protocol version. - */ - protocolVersion: string; - /** - * Product name. - */ - product: string; - /** - * Product revision. - */ - revision: string; - /** - * User-Agent. - */ - userAgent: string; - /** - * V8 version. - */ - jsVersion: string; + export type simulateDescriptorOperationResponseReturnValue = { } /** - * Returns the command line switches for the browser process if, and only if ---enable-automation is on the commandline. + * Adds a service with |serviceUuid| to the peripheral with |address|. */ - export type getBrowserCommandLineParameters = { + export type addServiceParameters = { + address: string; + serviceUuid: string; } - export type getBrowserCommandLineReturnValue = { + export type addServiceReturnValue = { /** - * Commandline parameters + * An identifier that uniquely represents this service. */ - arguments: string[]; + serviceId: string; } /** - * Get Chrome histograms. + * Removes the service respresented by |serviceId| from the simulated central. */ - export type getHistogramsParameters = { - /** - * Requested substring in name. Only histograms which have query as a -substring in their name are extracted. An empty or absent query returns -all histograms. - */ - query?: string; - /** - * If true, retrieve delta since last delta call. - */ - delta?: boolean; + export type removeServiceParameters = { + serviceId: string; } - export type getHistogramsReturnValue = { - /** - * Histograms. - */ - histograms: Histogram[]; + export type removeServiceReturnValue = { } /** - * Get a Chrome histogram by name. + * Adds a characteristic with |characteristicUuid| and |properties| to the +service represented by |serviceId|. */ - export type getHistogramParameters = { - /** - * Requested histogram name. - */ - name: string; - /** - * If true, retrieve delta since last delta call. - */ - delta?: boolean; + export type addCharacteristicParameters = { + serviceId: string; + characteristicUuid: string; + properties: CharacteristicProperties; } - export type getHistogramReturnValue = { + export type addCharacteristicReturnValue = { /** - * Histogram. + * An identifier that uniquely represents this characteristic. */ - histogram: Histogram; + characteristicId: string; } /** - * Get position and size of the browser window. + * Removes the characteristic respresented by |characteristicId| from the +simulated central. */ - export type getWindowBoundsParameters = { - /** - * Browser window id. - */ - windowId: WindowID; + export type removeCharacteristicParameters = { + characteristicId: string; } - export type getWindowBoundsReturnValue = { - /** - * Bounds information of the window. When window state is 'minimized', the restored window -position and size are returned. - */ - bounds: Bounds; + export type removeCharacteristicReturnValue = { } /** - * Get the browser window that contains the devtools target. + * Adds a descriptor with |descriptorUuid| to the characteristic respresented +by |characteristicId|. */ - export type getWindowForTargetParameters = { - /** - * Devtools agent host id. If called as a part of the session, associated targetId is used. - */ - targetId?: Target.TargetID; + export type addDescriptorParameters = { + characteristicId: string; + descriptorUuid: string; } - export type getWindowForTargetReturnValue = { - /** - * Browser window id. - */ - windowId: WindowID; + export type addDescriptorReturnValue = { /** - * Bounds information of the window. When window state is 'minimized', the restored window -position and size are returned. + * An identifier that uniquely represents this descriptor. */ - bounds: Bounds; + descriptorId: string; } /** - * Set position and/or size of the browser window. + * Removes the descriptor with |descriptorId| from the simulated central. */ - export type setWindowBoundsParameters = { - /** - * Browser window id. - */ - windowId: WindowID; - /** - * New window bounds. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined -with 'left', 'top', 'width' or 'height'. Leaves unspecified fields unchanged. - */ - bounds: Bounds; + export type removeDescriptorParameters = { + descriptorId: string; } - export type setWindowBoundsReturnValue = { + export type removeDescriptorReturnValue = { } /** - * Set size of the browser contents resizing browser window as necessary. + * Simulates a GATT disconnection from the peripheral with |address|. */ - export type setContentsSizeParameters = { + export type simulateGATTDisconnectionParameters = { + address: string; + } + export type simulateGATTDisconnectionReturnValue = { + } + } + + /** + * The Browser domain defines methods and events for browser managing. + */ + export module Browser { + export type BrowserContextID = string; + export type WindowID = number; + /** + * The state of the browser window. + */ + export type WindowState = "normal"|"minimized"|"maximized"|"fullscreen"; + /** + * Browser window bounds information + */ + export interface Bounds { /** - * Browser window id. + * The offset from the left edge of the screen to the window in pixels. */ - windowId: WindowID; + left?: number; /** - * The window contents width in DIP. Assumes current width if omitted. -Must be specified if 'height' is omitted. + * The offset from the top edge of the screen to the window in pixels. + */ + top?: number; + /** + * The window width in pixels. */ width?: number; /** - * The window contents height in DIP. Assumes current height if omitted. -Must be specified if 'width' is omitted. + * The window height in pixels. */ height?: number; - } - export type setContentsSizeReturnValue = { - } - /** - * Set dock tile details, platform-specific. - */ - export type setDockTileParameters = { - badgeLabel?: string; /** - * Png encoded image. + * The window state. Default to normal. */ - image?: binary; - } - export type setDockTileReturnValue = { - } - /** - * Invoke custom browser commands used by telemetry. - */ - export type executeBrowserCommandParameters = { - commandId: BrowserCommandId; - } - export type executeBrowserCommandReturnValue = { - } - /** - * Allows a site to use privacy sandbox features that require enrollment -without the site actually being enrolled. Only supported on page targets. - */ - export type addPrivacySandboxEnrollmentOverrideParameters = { - url: string; - } - export type addPrivacySandboxEnrollmentOverrideReturnValue = { + windowState?: WindowState; } + export type PermissionType = "ar"|"audioCapture"|"automaticFullscreen"|"backgroundFetch"|"backgroundSync"|"cameraPanTiltZoom"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"geolocation"|"handTracking"|"idleDetection"|"keyboardLock"|"localFonts"|"localNetworkAccess"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"pointerLock"|"protectedMediaIdentifier"|"sensors"|"smartCard"|"speakerSelection"|"storageAccess"|"topLevelStorageAccess"|"videoCapture"|"vr"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"webPrinting"|"windowManagement"; + export type PermissionSetting = "granted"|"denied"|"prompt"; /** - * Configures encryption keys used with a given privacy sandbox API to talk -to a trusted coordinator. Since this is intended for test automation only, -coordinatorOrigin must be a .test domain. No existing coordinator -configuration for the origin may exist. + * Definition of PermissionDescriptor defined in the Permissions API: +https://w3c.github.io/permissions/#dom-permissiondescriptor. */ - export type addPrivacySandboxCoordinatorKeyConfigParameters = { - api: PrivacySandboxAPI; - coordinatorOrigin: string; - keyConfig: string; + export interface PermissionDescriptor { /** - * BrowserContext to perform the action in. When omitted, default browser -context is used. + * Name of permission. +See https://cs.chromium.org/chromium/src/third_party/blink/renderer/modules/permissions/permission_descriptor.idl for valid permission names. */ - browserContextId?: BrowserContextID; - } - export type addPrivacySandboxCoordinatorKeyConfigReturnValue = { + name: string; + /** + * For "midi" permission, may also specify sysex control. + */ + sysex?: boolean; + /** + * For "push" permission, may specify userVisibleOnly. +Note that userVisibleOnly = true is the only currently supported type. + */ + userVisibleOnly?: boolean; + /** + * For "clipboard" permission, may specify allowWithoutSanitization. + */ + allowWithoutSanitization?: boolean; + /** + * For "fullscreen" permission, must specify allowWithoutGesture:true. + */ + allowWithoutGesture?: boolean; + /** + * For "camera" permission, may specify panTiltZoom. + */ + panTiltZoom?: boolean; } - } - - /** - * This domain exposes CSS read/write operations. All CSS objects (stylesheets, rules, and styles) -have an associated `id` used in subsequent operations on the related object. Each object type has -a specific `id` structure, and those are not interchangeable between objects of different kinds. -CSS objects can be loaded using the `get*ForNode()` calls (which accept a DOM node id). A client -can also keep track of stylesheets via the `styleSheetAdded`/`styleSheetRemoved` events and -subsequently load the required stylesheet contents using the `getStyleSheet[Text]()` methods. - */ - export module CSS { - export type StyleSheetId = string; /** - * Stylesheet type: "injected" for stylesheets injected via extension, "user-agent" for user-agent -stylesheets, "inspector" for stylesheets created by the inspector (i.e. those holding the "via -inspector" rules), "regular" for regular stylesheets. + * Browser command ids used by executeBrowserCommand. */ - export type StyleSheetOrigin = "injected"|"user-agent"|"inspector"|"regular"; + export type BrowserCommandId = "openTabSearch"|"closeTabSearch"|"openGlic"; /** - * CSS rule collection for a single pseudo style. + * Chrome histogram bucket. */ - export interface PseudoElementMatches { + export interface Bucket { /** - * Pseudo element type. + * Minimum value (inclusive). */ - pseudoType: DOM.PseudoType; + low: number; /** - * Pseudo element custom ident. + * Maximum value (exclusive). */ - pseudoIdentifier?: string; + high: number; /** - * Matches of CSS rules applicable to the pseudo style. + * Number of samples. */ - matches: RuleMatch[]; + count: number; } /** - * CSS style coming from animations with the name of the animation. + * Chrome histogram. */ - export interface CSSAnimationStyle { + export interface Histogram { /** - * The name of the animation. + * Name. */ - name?: string; + name: string; /** - * The style coming from the animation. + * Sum of sample values. */ - style: CSSStyle; - } - /** - * Inherited CSS rule collection from ancestor node. - */ - export interface InheritedStyleEntry { + sum: number; /** - * The ancestor node's inline style, if any, in the style inheritance chain. + * Total number of samples. */ - inlineStyle?: CSSStyle; + count: number; /** - * Matches of CSS rules matching the ancestor node in the style inheritance chain. + * Buckets. */ - matchedCSSRules: RuleMatch[]; + buckets: Bucket[]; } + export type PrivacySandboxAPI = "BiddingAndAuctionServices"|"TrustedKeyValue"; + /** - * Inherited CSS style collection for animated styles from ancestor node. + * Fired when page is about to start a download. */ - export interface InheritedAnimatedStyleEntry { + export type downloadWillBeginPayload = { /** - * Styles coming from the animations of the ancestor, if any, in the style inheritance chain. + * Id of the frame that caused the download to begin. */ - animationStyles?: CSSAnimationStyle[]; + frameId: Page.FrameId; /** - * The style coming from the transitions of the ancestor, if any, in the style inheritance chain. + * Global unique identifier of the download. */ - transitionsStyle?: CSSStyle; - } - /** - * Inherited pseudo element matches from pseudos of an ancestor node. - */ - export interface InheritedPseudoElementMatches { + guid: string; /** - * Matches of pseudo styles from the pseudos of an ancestor node. + * URL of the resource being downloaded. */ - pseudoElements: PseudoElementMatches[]; + url: string; + /** + * Suggested file name of the resource (the actual name of the file saved on disk may differ). + */ + suggestedFilename: string; } /** - * Match data for a CSS rule. + * Fired when download makes progress. Last call has |done| == true. */ - export interface RuleMatch { + export type downloadProgressPayload = { /** - * CSS rule in the match. + * Global unique identifier of the download. */ - rule: CSSRule; + guid: string; /** - * Matching selector indices in the rule's selectorList selectors (0-based). + * Total expected bytes to download. */ - matchingSelectors: number[]; - } - /** - * Data for a simple selector (these are delimited by commas in a selector list). - */ - export interface Value { + totalBytes: number; /** - * Value text. + * Total bytes received. */ - text: string; + receivedBytes: number; /** - * Value range in the underlying resource (if available). + * Download status. */ - range?: SourceRange; + state: "inProgress"|"completed"|"canceled"; /** - * Specificity of the selector. + * If download is "completed", provides the path of the downloaded file. +Depending on the platform, it is not guaranteed to be set, nor the file +is guaranteed to exist. */ - specificity?: Specificity; + filePath?: string; } + /** - * Specificity: -https://drafts.csswg.org/selectors/#specificity-rules + * Set permission settings for given requesting and embedding origins. */ - export interface Specificity { + export type setPermissionParameters = { /** - * The a component, which represents the number of ID selectors. + * Descriptor of permission to override. */ - a: number; + permission: PermissionDescriptor; /** - * The b component, which represents the number of class selectors, attributes selectors, and -pseudo-classes. + * Setting of the permission. */ - b: number; + setting: PermissionSetting; /** - * The c component, which represents the number of type selectors and pseudo-elements. + * Requesting origin the permission applies to, all origins if not specified. */ - c: number; - } - /** - * Selector list data. - */ - export interface SelectorList { + origin?: string; /** - * Selectors in the list. + * Embedding origin the permission applies to. It is ignored unless the requesting origin is +present and valid. If the requesting origin is provided but the embedding origin isn't, the +requesting origin is used as the embedding origin. */ - selectors: Value[]; + embeddingOrigin?: string; /** - * Rule selector text. + * Context to override. When omitted, default browser context is used. */ - text: string; + browserContextId?: BrowserContextID; + } + export type setPermissionReturnValue = { } /** - * CSS stylesheet metainformation. + * Grant specific permissions to the given origin and reject all others. */ - export interface CSSStyleSheetHeader { + export type grantPermissionsParameters = { + permissions: PermissionType[]; /** - * The stylesheet identifier. + * Origin the permission applies to, all origins if not specified. */ - styleSheetId: StyleSheetId; + origin?: string; /** - * Owner frame identifier. + * BrowserContext to override permissions. When omitted, default browser context is used. */ - frameId: Page.FrameId; + browserContextId?: BrowserContextID; + } + export type grantPermissionsReturnValue = { + } + /** + * Reset all permission management for all origins. + */ + export type resetPermissionsParameters = { /** - * Stylesheet resource URL. Empty if this is a constructed stylesheet created using -new CSSStyleSheet() (but non-empty if this is a constructed stylesheet imported -as a CSS module script). + * BrowserContext to reset permissions. When omitted, default browser context is used. */ - sourceURL: string; + browserContextId?: BrowserContextID; + } + export type resetPermissionsReturnValue = { + } + /** + * Set the behavior when downloading a file. + */ + export type setDownloadBehaviorParameters = { /** - * URL of source map associated with the stylesheet (if any). + * Whether to allow all or deny all download requests, or use default Chrome behavior if +available (otherwise deny). |allowAndName| allows download and names files according to +their download guids. */ - sourceMapURL?: string; + behavior: "deny"|"allow"|"allowAndName"|"default"; /** - * Stylesheet origin. + * BrowserContext to set download behavior. When omitted, default browser context is used. */ - origin: StyleSheetOrigin; + browserContextId?: BrowserContextID; /** - * Stylesheet title. + * The default path to save downloaded files to. This is required if behavior is set to 'allow' +or 'allowAndName'. */ - title: string; + downloadPath?: string; /** - * The backend id for the owner node of the stylesheet. + * Whether to emit download events (defaults to false). */ - ownerNode?: DOM.BackendNodeId; + eventsEnabled?: boolean; + } + export type setDownloadBehaviorReturnValue = { + } + /** + * Cancel a download if in progress + */ + export type cancelDownloadParameters = { /** - * Denotes whether the stylesheet is disabled. + * Global unique identifier of the download. */ - disabled: boolean; + guid: string; /** - * Whether the sourceURL field value comes from the sourceURL comment. + * BrowserContext to perform the action in. When omitted, default browser context is used. */ - hasSourceURL?: boolean; + browserContextId?: BrowserContextID; + } + export type cancelDownloadReturnValue = { + } + /** + * Close browser gracefully. + */ + export type closeParameters = { + } + export type closeReturnValue = { + } + /** + * Crashes browser on the main thread. + */ + export type crashParameters = { + } + export type crashReturnValue = { + } + /** + * Crashes GPU process. + */ + export type crashGpuProcessParameters = { + } + export type crashGpuProcessReturnValue = { + } + /** + * Returns version information. + */ + export type getVersionParameters = { + } + export type getVersionReturnValue = { /** - * Whether this stylesheet is created for STYLE tag by parser. This flag is not set for -document.written STYLE tags. + * Protocol version. */ - isInline: boolean; + protocolVersion: string; /** - * Whether this stylesheet is mutable. Inline stylesheets become mutable -after they have been modified via CSSOM API. -`` element's stylesheets become mutable only if DevTools modifies them. -Constructed stylesheets (new CSSStyleSheet()) are mutable immediately after creation. + * Product name. */ - isMutable: boolean; + product: string; /** - * True if this stylesheet is created through new CSSStyleSheet() or imported as a -CSS module script. + * Product revision. */ - isConstructed: boolean; + revision: string; /** - * Line offset of the stylesheet within the resource (zero based). + * User-Agent. */ - startLine: number; + userAgent: string; /** - * Column offset of the stylesheet within the resource (zero based). + * V8 version. */ - startColumn: number; + jsVersion: string; + } + /** + * Returns the command line switches for the browser process if, and only if +--enable-automation is on the commandline. + */ + export type getBrowserCommandLineParameters = { + } + export type getBrowserCommandLineReturnValue = { /** - * Size of the content (in characters). + * Commandline parameters */ - length: number; + arguments: string[]; + } + /** + * Get Chrome histograms. + */ + export type getHistogramsParameters = { /** - * Line offset of the end of the stylesheet within the resource (zero based). + * Requested substring in name. Only histograms which have query as a +substring in their name are extracted. An empty or absent query returns +all histograms. */ - endLine: number; + query?: string; /** - * Column offset of the end of the stylesheet within the resource (zero based). + * If true, retrieve delta since last delta call. */ - endColumn: number; + delta?: boolean; + } + export type getHistogramsReturnValue = { /** - * If the style sheet was loaded from a network resource, this indicates when the resource failed to load + * Histograms. */ - loadingFailed?: boolean; + histograms: Histogram[]; } /** - * CSS rule representation. + * Get a Chrome histogram by name. */ - export interface CSSRule { + export type getHistogramParameters = { /** - * The css style sheet identifier (absent for user agent stylesheet and user-specified -stylesheet rules) this rule came from. + * Requested histogram name. */ - styleSheetId?: StyleSheetId; + name: string; /** - * Rule selector data. + * If true, retrieve delta since last delta call. */ - selectorList: SelectorList; + delta?: boolean; + } + export type getHistogramReturnValue = { /** - * Array of selectors from ancestor style rules, sorted by distance from the current rule. + * Histogram. */ - nestingSelectors?: string[]; - /** - * Parent stylesheet's origin. - */ - origin: StyleSheetOrigin; - /** - * Associated style declaration. - */ - style: CSSStyle; + histogram: Histogram; + } + /** + * Get position and size of the browser window. + */ + export type getWindowBoundsParameters = { /** - * Media list array (for rules involving media queries). The array enumerates media queries -starting with the innermost one, going outwards. + * Browser window id. */ - media?: CSSMedia[]; + windowId: WindowID; + } + export type getWindowBoundsReturnValue = { /** - * Container query list array (for rules involving container queries). -The array enumerates container queries starting with the innermost one, going outwards. + * Bounds information of the window. When window state is 'minimized', the restored window +position and size are returned. */ - containerQueries?: CSSContainerQuery[]; + bounds: Bounds; + } + /** + * Get the browser window that contains the devtools target. + */ + export type getWindowForTargetParameters = { /** - * @supports CSS at-rule array. -The array enumerates @supports at-rules starting with the innermost one, going outwards. + * Devtools agent host id. If called as a part of the session, associated targetId is used. */ - supports?: CSSSupports[]; + targetId?: Target.TargetID; + } + export type getWindowForTargetReturnValue = { /** - * Cascade layer array. Contains the layer hierarchy that this rule belongs to starting -with the innermost layer and going outwards. + * Browser window id. */ - layers?: CSSLayer[]; + windowId: WindowID; /** - * @scope CSS at-rule array. -The array enumerates @scope at-rules starting with the innermost one, going outwards. + * Bounds information of the window. When window state is 'minimized', the restored window +position and size are returned. */ - scopes?: CSSScope[]; + bounds: Bounds; + } + /** + * Set position and/or size of the browser window. + */ + export type setWindowBoundsParameters = { /** - * The array keeps the types of ancestor CSSRules from the innermost going outwards. + * Browser window id. */ - ruleTypes?: CSSRuleType[]; + windowId: WindowID; /** - * @starting-style CSS at-rule array. -The array enumerates @starting-style at-rules starting with the innermost one, going outwards. + * New window bounds. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined +with 'left', 'top', 'width' or 'height'. Leaves unspecified fields unchanged. */ - startingStyles?: CSSStartingStyle[]; + bounds: Bounds; + } + export type setWindowBoundsReturnValue = { } /** - * Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors. -This list only contains rule types that are collected during the ancestor rule collection. - */ - export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule"|"StartingStyleRule"; - /** - * CSS coverage information. + * Set size of the browser contents resizing browser window as necessary. */ - export interface RuleUsage { + export type setContentsSizeParameters = { /** - * The css style sheet identifier (absent for user agent stylesheet and user-specified -stylesheet rules) this rule came from. + * Browser window id. */ - styleSheetId: StyleSheetId; + windowId: WindowID; /** - * Offset of the start of the rule (including selector) from the beginning of the stylesheet. + * The window contents width in DIP. Assumes current width if omitted. +Must be specified if 'height' is omitted. */ - startOffset: number; + width?: number; /** - * Offset of the end of the rule body from the beginning of the stylesheet. + * The window contents height in DIP. Assumes current height if omitted. +Must be specified if 'width' is omitted. */ - endOffset: number; + height?: number; + } + export type setContentsSizeReturnValue = { + } + /** + * Set dock tile details, platform-specific. + */ + export type setDockTileParameters = { + badgeLabel?: string; /** - * Indicates whether the rule was actually used by some element in the page. + * Png encoded image. */ - used: boolean; + image?: binary; + } + export type setDockTileReturnValue = { } /** - * Text range within a resource. All numbers are zero-based. + * Invoke custom browser commands used by telemetry. */ - export interface SourceRange { + export type executeBrowserCommandParameters = { + commandId: BrowserCommandId; + } + export type executeBrowserCommandReturnValue = { + } + /** + * Allows a site to use privacy sandbox features that require enrollment +without the site actually being enrolled. Only supported on page targets. + */ + export type addPrivacySandboxEnrollmentOverrideParameters = { + url: string; + } + export type addPrivacySandboxEnrollmentOverrideReturnValue = { + } + /** + * Configures encryption keys used with a given privacy sandbox API to talk +to a trusted coordinator. Since this is intended for test automation only, +coordinatorOrigin must be a .test domain. No existing coordinator +configuration for the origin may exist. + */ + export type addPrivacySandboxCoordinatorKeyConfigParameters = { + api: PrivacySandboxAPI; + coordinatorOrigin: string; + keyConfig: string; /** - * Start line of range. + * BrowserContext to perform the action in. When omitted, default browser +context is used. */ - startLine: number; + browserContextId?: BrowserContextID; + } + export type addPrivacySandboxCoordinatorKeyConfigReturnValue = { + } + } + + /** + * This domain exposes CSS read/write operations. All CSS objects (stylesheets, rules, and styles) +have an associated `id` used in subsequent operations on the related object. Each object type has +a specific `id` structure, and those are not interchangeable between objects of different kinds. +CSS objects can be loaded using the `get*ForNode()` calls (which accept a DOM node id). A client +can also keep track of stylesheets via the `styleSheetAdded`/`styleSheetRemoved` events and +subsequently load the required stylesheet contents using the `getStyleSheet[Text]()` methods. + */ + export module CSS { + export type StyleSheetId = string; + /** + * Stylesheet type: "injected" for stylesheets injected via extension, "user-agent" for user-agent +stylesheets, "inspector" for stylesheets created by the inspector (i.e. those holding the "via +inspector" rules), "regular" for regular stylesheets. + */ + export type StyleSheetOrigin = "injected"|"user-agent"|"inspector"|"regular"; + /** + * CSS rule collection for a single pseudo style. + */ + export interface PseudoElementMatches { /** - * Start column of range (inclusive). + * Pseudo element type. */ - startColumn: number; + pseudoType: DOM.PseudoType; /** - * End line of range + * Pseudo element custom ident. */ - endLine: number; + pseudoIdentifier?: string; /** - * End column of range (exclusive). + * Matches of CSS rules applicable to the pseudo style. */ - endColumn: number; + matches: RuleMatch[]; } - export interface ShorthandEntry { - /** - * Shorthand name. - */ - name: string; + /** + * CSS style coming from animations with the name of the animation. + */ + export interface CSSAnimationStyle { /** - * Shorthand value. + * The name of the animation. */ - value: string; + name?: string; /** - * Whether the property has "!important" annotation (implies `false` if absent). + * The style coming from the animation. */ - important?: boolean; + style: CSSStyle; } - export interface CSSComputedStyleProperty { + /** + * Inherited CSS rule collection from ancestor node. + */ + export interface InheritedStyleEntry { /** - * Computed style property name. + * The ancestor node's inline style, if any, in the style inheritance chain. */ - name: string; + inlineStyle?: CSSStyle; /** - * Computed style property value. + * Matches of CSS rules matching the ancestor node in the style inheritance chain. */ - value: string; + matchedCSSRules: RuleMatch[]; } /** - * CSS style representation. + * Inherited CSS style collection for animated styles from ancestor node. */ - export interface CSSStyle { + export interface InheritedAnimatedStyleEntry { /** - * The css style sheet identifier (absent for user agent stylesheet and user-specified -stylesheet rules) this rule came from. + * Styles coming from the animations of the ancestor, if any, in the style inheritance chain. */ - styleSheetId?: StyleSheetId; + animationStyles?: CSSAnimationStyle[]; /** - * CSS properties in the style. + * The style coming from the transitions of the ancestor, if any, in the style inheritance chain. */ - cssProperties: CSSProperty[]; + transitionsStyle?: CSSStyle; + } + /** + * Inherited pseudo element matches from pseudos of an ancestor node. + */ + export interface InheritedPseudoElementMatches { /** - * Computed values for all shorthands found in the style. + * Matches of pseudo styles from the pseudos of an ancestor node. */ - shorthandEntries: ShorthandEntry[]; + pseudoElements: PseudoElementMatches[]; + } + /** + * Match data for a CSS rule. + */ + export interface RuleMatch { /** - * Style declaration text (if available). + * CSS rule in the match. */ - cssText?: string; + rule: CSSRule; /** - * Style declaration range in the enclosing stylesheet (if available). + * Matching selector indices in the rule's selectorList selectors (0-based). */ - range?: SourceRange; + matchingSelectors: number[]; } /** - * CSS property declaration data. + * Data for a simple selector (these are delimited by commas in a selector list). */ - export interface CSSProperty { - /** - * The property name. - */ - name: string; + export interface Value { /** - * The property value. + * Value text. */ - value: string; + text: string; /** - * Whether the property has "!important" annotation (implies `false` if absent). + * Value range in the underlying resource (if available). */ - important?: boolean; + range?: SourceRange; /** - * Whether the property is implicit (implies `false` if absent). + * Specificity of the selector. */ - implicit?: boolean; + specificity?: Specificity; + } + /** + * Specificity: +https://drafts.csswg.org/selectors/#specificity-rules + */ + export interface Specificity { /** - * The full property text as specified in the style. + * The a component, which represents the number of ID selectors. */ - text?: string; + a: number; /** - * Whether the property is understood by the browser (implies `true` if absent). + * The b component, which represents the number of class selectors, attributes selectors, and +pseudo-classes. */ - parsedOk?: boolean; + b: number; /** - * Whether the property is disabled by the user (present for source-based properties only). + * The c component, which represents the number of type selectors and pseudo-elements. */ - disabled?: boolean; + c: number; + } + /** + * Selector list data. + */ + export interface SelectorList { /** - * The entire property range in the enclosing style declaration (if available). + * Selectors in the list. */ - range?: SourceRange; + selectors: Value[]; /** - * Parsed longhand components of this property if it is a shorthand. -This field will be empty if the given property is not a shorthand. + * Rule selector text. */ - longhandProperties?: CSSProperty[]; + text: string; } /** - * CSS media rule descriptor. + * CSS stylesheet metainformation. */ - export interface CSSMedia { + export interface CSSStyleSheetHeader { /** - * Media query text. + * The stylesheet identifier. */ - text: string; + styleSheetId: StyleSheetId; /** - * Source of the media query: "mediaRule" if specified by a @media rule, "importRule" if -specified by an @import rule, "linkedSheet" if specified by a "media" attribute in a linked -stylesheet's LINK tag, "inlineSheet" if specified by a "media" attribute in an inline -stylesheet's STYLE tag. + * Owner frame identifier. */ - source: "mediaRule"|"importRule"|"linkedSheet"|"inlineSheet"; + frameId: Page.FrameId; /** - * URL of the document containing the media query description. + * Stylesheet resource URL. Empty if this is a constructed stylesheet created using +new CSSStyleSheet() (but non-empty if this is a constructed stylesheet imported +as a CSS module script). */ - sourceURL?: string; + sourceURL: string; /** - * The associated rule (@media or @import) header range in the enclosing stylesheet (if -available). + * URL of source map associated with the stylesheet (if any). */ - range?: SourceRange; + sourceMapURL?: string; /** - * Identifier of the stylesheet containing this object (if exists). + * Stylesheet origin. */ - styleSheetId?: StyleSheetId; + origin: StyleSheetOrigin; /** - * Array of media queries. + * Stylesheet title. */ - mediaList?: MediaQuery[]; - } - /** - * Media query descriptor. - */ - export interface MediaQuery { + title: string; /** - * Array of media query expressions. + * The backend id for the owner node of the stylesheet. */ - expressions: MediaQueryExpression[]; + ownerNode?: DOM.BackendNodeId; /** - * Whether the media query condition is satisfied. + * Denotes whether the stylesheet is disabled. */ - active: boolean; - } - /** - * Media query expression descriptor. - */ - export interface MediaQueryExpression { + disabled: boolean; /** - * Media query expression value. + * Whether the sourceURL field value comes from the sourceURL comment. */ - value: number; + hasSourceURL?: boolean; /** - * Media query expression units. + * Whether this stylesheet is created for STYLE tag by parser. This flag is not set for +document.written STYLE tags. */ - unit: string; + isInline: boolean; /** - * Media query expression feature. + * Whether this stylesheet is mutable. Inline stylesheets become mutable +after they have been modified via CSSOM API. +`` element's stylesheets become mutable only if DevTools modifies them. +Constructed stylesheets (new CSSStyleSheet()) are mutable immediately after creation. */ - feature: string; + isMutable: boolean; /** - * The associated range of the value text in the enclosing stylesheet (if available). + * True if this stylesheet is created through new CSSStyleSheet() or imported as a +CSS module script. */ - valueRange?: SourceRange; + isConstructed: boolean; /** - * Computed length of media query expression (if applicable). + * Line offset of the stylesheet within the resource (zero based). */ - computedLength?: number; + startLine: number; + /** + * Column offset of the stylesheet within the resource (zero based). + */ + startColumn: number; + /** + * Size of the content (in characters). + */ + length: number; + /** + * Line offset of the end of the stylesheet within the resource (zero based). + */ + endLine: number; + /** + * Column offset of the end of the stylesheet within the resource (zero based). + */ + endColumn: number; + /** + * If the style sheet was loaded from a network resource, this indicates when the resource failed to load + */ + loadingFailed?: boolean; } /** - * CSS container query rule descriptor. + * CSS rule representation. */ - export interface CSSContainerQuery { + export interface CSSRule { /** - * Container query text. + * The css style sheet identifier (absent for user agent stylesheet and user-specified +stylesheet rules) this rule came from. */ - text: string; + styleSheetId?: StyleSheetId; /** - * The associated rule header range in the enclosing stylesheet (if -available). + * Rule selector data. */ - range?: SourceRange; + selectorList: SelectorList; /** - * Identifier of the stylesheet containing this object (if exists). + * Array of selectors from ancestor style rules, sorted by distance from the current rule. */ - styleSheetId?: StyleSheetId; + nestingSelectors?: string[]; /** - * Optional name for the container. + * Parent stylesheet's origin. */ - name?: string; + origin: StyleSheetOrigin; /** - * Optional physical axes queried for the container. + * Associated style declaration. */ - physicalAxes?: DOM.PhysicalAxes; + style: CSSStyle; /** - * Optional logical axes queried for the container. + * Media list array (for rules involving media queries). The array enumerates media queries +starting with the innermost one, going outwards. */ - logicalAxes?: DOM.LogicalAxes; + media?: CSSMedia[]; /** - * true if the query contains scroll-state() queries. + * Container query list array (for rules involving container queries). +The array enumerates container queries starting with the innermost one, going outwards. */ - queriesScrollState?: boolean; + containerQueries?: CSSContainerQuery[]; /** - * true if the query contains anchored() queries. + * @supports CSS at-rule array. +The array enumerates @supports at-rules starting with the innermost one, going outwards. */ - queriesAnchored?: boolean; - } - /** - * CSS Supports at-rule descriptor. - */ - export interface CSSSupports { + supports?: CSSSupports[]; /** - * Supports rule text. + * Cascade layer array. Contains the layer hierarchy that this rule belongs to starting +with the innermost layer and going outwards. */ - text: string; + layers?: CSSLayer[]; /** - * Whether the supports condition is satisfied. + * @scope CSS at-rule array. +The array enumerates @scope at-rules starting with the innermost one, going outwards. */ - active: boolean; + scopes?: CSSScope[]; /** - * The associated rule header range in the enclosing stylesheet (if -available). + * The array keeps the types of ancestor CSSRules from the innermost going outwards. */ - range?: SourceRange; + ruleTypes?: CSSRuleType[]; /** - * Identifier of the stylesheet containing this object (if exists). + * @starting-style CSS at-rule array. +The array enumerates @starting-style at-rules starting with the innermost one, going outwards. */ - styleSheetId?: StyleSheetId; + startingStyles?: CSSStartingStyle[]; } /** - * CSS Scope at-rule descriptor. + * Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors. +This list only contains rule types that are collected during the ancestor rule collection. */ - export interface CSSScope { + export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule"|"StartingStyleRule"; + /** + * CSS coverage information. + */ + export interface RuleUsage { /** - * Scope rule text. + * The css style sheet identifier (absent for user agent stylesheet and user-specified +stylesheet rules) this rule came from. */ - text: string; + styleSheetId: StyleSheetId; /** - * The associated rule header range in the enclosing stylesheet (if -available). + * Offset of the start of the rule (including selector) from the beginning of the stylesheet. */ - range?: SourceRange; + startOffset: number; /** - * Identifier of the stylesheet containing this object (if exists). + * Offset of the end of the rule body from the beginning of the stylesheet. */ - styleSheetId?: StyleSheetId; + endOffset: number; + /** + * Indicates whether the rule was actually used by some element in the page. + */ + used: boolean; } /** - * CSS Layer at-rule descriptor. + * Text range within a resource. All numbers are zero-based. */ - export interface CSSLayer { + export interface SourceRange { /** - * Layer name. + * Start line of range. */ - text: string; + startLine: number; /** - * The associated rule header range in the enclosing stylesheet (if -available). + * Start column of range (inclusive). */ - range?: SourceRange; + startColumn: number; /** - * Identifier of the stylesheet containing this object (if exists). + * End line of range */ - styleSheetId?: StyleSheetId; - } - /** - * CSS Starting Style at-rule descriptor. - */ - export interface CSSStartingStyle { + endLine: number; /** - * The associated rule header range in the enclosing stylesheet (if -available). + * End column of range (exclusive). */ - range?: SourceRange; + endColumn: number; + } + export interface ShorthandEntry { /** - * Identifier of the stylesheet containing this object (if exists). - */ - styleSheetId?: StyleSheetId; - } - /** - * CSS Layer data. - */ - export interface CSSLayerData { - /** - * Layer name. + * Shorthand name. */ name: string; /** - * Direct sub-layers + * Shorthand value. */ - subLayers?: CSSLayerData[]; + value: string; /** - * Layer order. The order determines the order of the layer in the cascade order. -A higher number has higher priority in the cascade order. + * Whether the property has "!important" annotation (implies `false` if absent). */ - order: number; + important?: boolean; } - /** - * Information about amount of glyphs that were rendered with given font. - */ - export interface PlatformFontUsage { - /** - * Font's family name reported by platform. - */ - familyName: string; + export interface CSSComputedStyleProperty { /** - * Font's PostScript name reported by platform. + * Computed style property name. */ - postScriptName: string; + name: string; /** - * Indicates if the font was downloaded or resolved locally. + * Computed style property value. */ - isCustomFont: boolean; + value: string; + } + export interface ComputedStyleExtraFields { /** - * Amount of glyphs that were rendered with this font. + * Returns whether or not this node is being rendered with base appearance, +which happens when it has its appearance property set to base/base-select +or it is in the subtree of an element being rendered with base appearance. */ - glyphCount: number; + isAppearanceBase: boolean; } /** - * Information about font variation axes for variable fonts + * CSS style representation. */ - export interface FontVariationAxis { + export interface CSSStyle { /** - * The font-variation-setting tag (a.k.a. "axis tag"). + * The css style sheet identifier (absent for user agent stylesheet and user-specified +stylesheet rules) this rule came from. */ - tag: string; + styleSheetId?: StyleSheetId; /** - * Human-readable variation name in the default language (normally, "en"). + * CSS properties in the style. */ - name: string; + cssProperties: CSSProperty[]; /** - * The minimum value (inclusive) the font supports for this tag. + * Computed values for all shorthands found in the style. */ - minValue: number; + shorthandEntries: ShorthandEntry[]; /** - * The maximum value (inclusive) the font supports for this tag. + * Style declaration text (if available). */ - maxValue: number; + cssText?: string; /** - * The default value. + * Style declaration range in the enclosing stylesheet (if available). */ - defaultValue: number; + range?: SourceRange; } /** - * Properties of a web font: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#font-descriptions -and additional information such as platformFontFamily and fontVariationAxes. + * CSS property declaration data. */ - export interface FontFace { - /** - * The font-family. - */ - fontFamily: string; + export interface CSSProperty { /** - * The font-style. + * The property name. */ - fontStyle: string; + name: string; /** - * The font-variant. + * The property value. */ - fontVariant: string; + value: string; /** - * The font-weight. + * Whether the property has "!important" annotation (implies `false` if absent). */ - fontWeight: string; + important?: boolean; /** - * The font-stretch. + * Whether the property is implicit (implies `false` if absent). */ - fontStretch: string; + implicit?: boolean; /** - * The font-display. + * The full property text as specified in the style. */ - fontDisplay: string; + text?: string; /** - * The unicode-range. + * Whether the property is understood by the browser (implies `true` if absent). */ - unicodeRange: string; + parsedOk?: boolean; /** - * The src. + * Whether the property is disabled by the user (present for source-based properties only). */ - src: string; + disabled?: boolean; /** - * The resolved platform font family + * The entire property range in the enclosing style declaration (if available). */ - platformFontFamily: string; + range?: SourceRange; /** - * Available variation settings (a.k.a. "axes"). + * Parsed longhand components of this property if it is a shorthand. +This field will be empty if the given property is not a shorthand. */ - fontVariationAxes?: FontVariationAxis[]; + longhandProperties?: CSSProperty[]; } /** - * CSS try rule representation. + * CSS media rule descriptor. */ - export interface CSSTryRule { + export interface CSSMedia { /** - * The css style sheet identifier (absent for user agent stylesheet and user-specified -stylesheet rules) this rule came from. + * Media query text. */ - styleSheetId?: StyleSheetId; + text: string; /** - * Parent stylesheet's origin. + * Source of the media query: "mediaRule" if specified by a @media rule, "importRule" if +specified by an @import rule, "linkedSheet" if specified by a "media" attribute in a linked +stylesheet's LINK tag, "inlineSheet" if specified by a "media" attribute in an inline +stylesheet's STYLE tag. */ - origin: StyleSheetOrigin; + source: "mediaRule"|"importRule"|"linkedSheet"|"inlineSheet"; /** - * Associated style declaration. + * URL of the document containing the media query description. */ - style: CSSStyle; - } - /** - * CSS @position-try rule representation. - */ - export interface CSSPositionTryRule { + sourceURL?: string; /** - * The prelude dashed-ident name + * The associated rule (@media or @import) header range in the enclosing stylesheet (if +available). */ - name: Value; + range?: SourceRange; /** - * The css style sheet identifier (absent for user agent stylesheet and user-specified -stylesheet rules) this rule came from. + * Identifier of the stylesheet containing this object (if exists). */ styleSheetId?: StyleSheetId; /** - * Parent stylesheet's origin. - */ - origin: StyleSheetOrigin; - /** - * Associated style declaration. + * Array of media queries. */ - style: CSSStyle; - active: boolean; + mediaList?: MediaQuery[]; } /** - * CSS keyframes rule representation. + * Media query descriptor. */ - export interface CSSKeyframesRule { + export interface MediaQuery { /** - * Animation name. + * Array of media query expressions. */ - animationName: Value; + expressions: MediaQueryExpression[]; /** - * List of keyframes. + * Whether the media query condition is satisfied. */ - keyframes: CSSKeyframeRule[]; - } - /** - * Representation of a custom property registration through CSS.registerProperty - */ - export interface CSSPropertyRegistration { - propertyName: string; - initialValue?: Value; - inherits: boolean; - syntax: string; + active: boolean; } /** - * CSS font-palette-values rule representation. + * Media query expression descriptor. */ - export interface CSSFontPaletteValuesRule { + export interface MediaQueryExpression { /** - * The css style sheet identifier (absent for user agent stylesheet and user-specified -stylesheet rules) this rule came from. + * Media query expression value. */ - styleSheetId?: StyleSheetId; + value: number; /** - * Parent stylesheet's origin. + * Media query expression units. */ - origin: StyleSheetOrigin; + unit: string; /** - * Associated font palette name. + * Media query expression feature. */ - fontPaletteName: Value; + feature: string; /** - * Associated style declaration. + * The associated range of the value text in the enclosing stylesheet (if available). */ - style: CSSStyle; + valueRange?: SourceRange; + /** + * Computed length of media query expression (if applicable). + */ + computedLength?: number; } /** - * CSS property at-rule representation. + * CSS container query rule descriptor. */ - export interface CSSPropertyRule { + export interface CSSContainerQuery { /** - * The css style sheet identifier (absent for user agent stylesheet and user-specified -stylesheet rules) this rule came from. + * Container query text. + */ + text: string; + /** + * The associated rule header range in the enclosing stylesheet (if +available). + */ + range?: SourceRange; + /** + * Identifier of the stylesheet containing this object (if exists). */ styleSheetId?: StyleSheetId; /** - * Parent stylesheet's origin. + * Optional name for the container. */ - origin: StyleSheetOrigin; + name?: string; /** - * Associated property name. + * Optional physical axes queried for the container. */ - propertyName: Value; + physicalAxes?: DOM.PhysicalAxes; /** - * Associated style declaration. + * Optional logical axes queried for the container. */ - style: CSSStyle; - } - /** - * CSS function argument representation. - */ - export interface CSSFunctionParameter { + logicalAxes?: DOM.LogicalAxes; /** - * The parameter name. + * true if the query contains scroll-state() queries. */ - name: string; + queriesScrollState?: boolean; /** - * The parameter type. + * true if the query contains anchored() queries. */ - type: string; + queriesAnchored?: boolean; } /** - * CSS function conditional block representation. + * CSS Supports at-rule descriptor. */ - export interface CSSFunctionConditionNode { + export interface CSSSupports { /** - * Media query for this conditional block. Only one type of condition should be set. + * Supports rule text. */ - media?: CSSMedia; + text: string; /** - * Container query for this conditional block. Only one type of condition should be set. + * Whether the supports condition is satisfied. */ - containerQueries?: CSSContainerQuery; + active: boolean; /** - * @supports CSS at-rule condition. Only one type of condition should be set. + * The associated rule header range in the enclosing stylesheet (if +available). */ - supports?: CSSSupports; + range?: SourceRange; /** - * Block body. + * Identifier of the stylesheet containing this object (if exists). */ - children: CSSFunctionNode[]; + styleSheetId?: StyleSheetId; + } + /** + * CSS Scope at-rule descriptor. + */ + export interface CSSScope { /** - * The condition text. + * Scope rule text. */ - conditionText: string; + text: string; + /** + * The associated rule header range in the enclosing stylesheet (if +available). + */ + range?: SourceRange; + /** + * Identifier of the stylesheet containing this object (if exists). + */ + styleSheetId?: StyleSheetId; } /** - * Section of the body of a CSS function rule. + * CSS Layer at-rule descriptor. */ - export interface CSSFunctionNode { + export interface CSSLayer { /** - * A conditional block. If set, style should not be set. + * Layer name. */ - condition?: CSSFunctionConditionNode; + text: string; /** - * Values set by this node. If set, condition should not be set. + * The associated rule header range in the enclosing stylesheet (if +available). */ - style?: CSSStyle; + range?: SourceRange; + /** + * Identifier of the stylesheet containing this object (if exists). + */ + styleSheetId?: StyleSheetId; } /** - * CSS function at-rule representation. + * CSS Starting Style at-rule descriptor. */ - export interface CSSFunctionRule { + export interface CSSStartingStyle { /** - * Name of the function. + * The associated rule header range in the enclosing stylesheet (if +available). */ - name: Value; + range?: SourceRange; /** - * The css style sheet identifier (absent for user agent stylesheet and user-specified -stylesheet rules) this rule came from. + * Identifier of the stylesheet containing this object (if exists). */ styleSheetId?: StyleSheetId; + } + /** + * CSS Layer data. + */ + export interface CSSLayerData { /** - * Parent stylesheet's origin. + * Layer name. */ - origin: StyleSheetOrigin; + name: string; /** - * List of parameters. + * Direct sub-layers */ - parameters: CSSFunctionParameter[]; + subLayers?: CSSLayerData[]; /** - * Function body. + * Layer order. The order determines the order of the layer in the cascade order. +A higher number has higher priority in the cascade order. */ - children: CSSFunctionNode[]; + order: number; } /** - * CSS keyframe rule representation. + * Information about amount of glyphs that were rendered with given font. */ - export interface CSSKeyframeRule { + export interface PlatformFontUsage { /** - * The css style sheet identifier (absent for user agent stylesheet and user-specified -stylesheet rules) this rule came from. + * Font's family name reported by platform. */ - styleSheetId?: StyleSheetId; + familyName: string; /** - * Parent stylesheet's origin. + * Font's PostScript name reported by platform. */ - origin: StyleSheetOrigin; + postScriptName: string; /** - * Associated key text. + * Indicates if the font was downloaded or resolved locally. */ - keyText: Value; + isCustomFont: boolean; /** - * Associated style declaration. + * Amount of glyphs that were rendered with this font. */ - style: CSSStyle; + glyphCount: number; } /** - * A descriptor of operation to mutate style declaration text. + * Information about font variation axes for variable fonts */ - export interface StyleDeclarationEdit { + export interface FontVariationAxis { /** - * The css style sheet identifier. + * The font-variation-setting tag (a.k.a. "axis tag"). */ - styleSheetId: StyleSheetId; + tag: string; /** - * The range of the style text in the enclosing stylesheet. + * Human-readable variation name in the default language (normally, "en"). */ - range: SourceRange; + name: string; /** - * New style text. + * The minimum value (inclusive) the font supports for this tag. */ - text: string; - } - - /** - * Fires whenever a web font is updated. A non-empty font parameter indicates a successfully loaded -web font. - */ - export type fontsUpdatedPayload = { + minValue: number; /** - * The web font that has loaded. + * The maximum value (inclusive) the font supports for this tag. */ - font?: FontFace; - } - /** - * Fires whenever a MediaQuery result changes (for example, after a browser window has been -resized.) The current implementation considers only viewport-dependent media features. - */ - export type mediaQueryResultChangedPayload = void; - /** - * Fired whenever an active document stylesheet is added. - */ - export type styleSheetAddedPayload = { + maxValue: number; /** - * Added stylesheet metainfo. + * The default value. */ - header: CSSStyleSheetHeader; - } - /** - * Fired whenever a stylesheet is changed as a result of the client operation. - */ - export type styleSheetChangedPayload = { - styleSheetId: StyleSheetId; + defaultValue: number; } /** - * Fired whenever an active document stylesheet is removed. + * Properties of a web font: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#font-descriptions +and additional information such as platformFontFamily and fontVariationAxes. */ - export type styleSheetRemovedPayload = { + export interface FontFace { /** - * Identifier of the removed stylesheet. + * The font-family. */ - styleSheetId: StyleSheetId; - } - export type computedStyleUpdatedPayload = { + fontFamily: string; /** - * The node id that has updated computed styles. + * The font-style. */ - nodeId: DOM.NodeId; - } - - /** - * Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the -position specified by `location`. - */ - export type addRuleParameters = { + fontStyle: string; /** - * The css style sheet identifier where a new rule should be inserted. + * The font-variant. */ - styleSheetId: StyleSheetId; + fontVariant: string; /** - * The text of a new rule. + * The font-weight. */ - ruleText: string; + fontWeight: string; /** - * Text position of a new rule in the target style sheet. + * The font-stretch. */ - location: SourceRange; + fontStretch: string; /** - * NodeId for the DOM node in whose context custom property declarations for registered properties should be -validated. If omitted, declarations in the new rule text can only be validated statically, which may produce -incorrect results if the declaration contains a var() for example. + * The font-display. */ - nodeForPropertySyntaxValidation?: DOM.NodeId; - } - export type addRuleReturnValue = { + fontDisplay: string; /** - * The newly created rule. + * The unicode-range. */ - rule: CSSRule; + unicodeRange: string; + /** + * The src. + */ + src: string; + /** + * The resolved platform font family + */ + platformFontFamily: string; + /** + * Available variation settings (a.k.a. "axes"). + */ + fontVariationAxes?: FontVariationAxis[]; } /** - * Returns all class names from specified stylesheet. + * CSS try rule representation. */ - export type collectClassNamesParameters = { - styleSheetId: StyleSheetId; - } - export type collectClassNamesReturnValue = { + export interface CSSTryRule { /** - * Class name list. + * The css style sheet identifier (absent for user agent stylesheet and user-specified +stylesheet rules) this rule came from. */ - classNames: string[]; + styleSheetId?: StyleSheetId; + /** + * Parent stylesheet's origin. + */ + origin: StyleSheetOrigin; + /** + * Associated style declaration. + */ + style: CSSStyle; } /** - * Creates a new special "via-inspector" stylesheet in the frame with given `frameId`. + * CSS @position-try rule representation. */ - export type createStyleSheetParameters = { + export interface CSSPositionTryRule { /** - * Identifier of the frame where "via-inspector" stylesheet should be created. + * The prelude dashed-ident name */ - frameId: Page.FrameId; + name: Value; /** - * If true, creates a new stylesheet for every call. If false, -returns a stylesheet previously created by a call with force=false -for the frame's document if it exists or creates a new stylesheet -(default: false). + * The css style sheet identifier (absent for user agent stylesheet and user-specified +stylesheet rules) this rule came from. */ - force?: boolean; - } - export type createStyleSheetReturnValue = { + styleSheetId?: StyleSheetId; /** - * Identifier of the created "via-inspector" stylesheet. + * Parent stylesheet's origin. */ - styleSheetId: StyleSheetId; + origin: StyleSheetOrigin; + /** + * Associated style declaration. + */ + style: CSSStyle; + active: boolean; } /** - * Disables the CSS agent for the given page. + * CSS keyframes rule representation. */ - export type disableParameters = { - } - export type disableReturnValue = { + export interface CSSKeyframesRule { + /** + * Animation name. + */ + animationName: Value; + /** + * List of keyframes. + */ + keyframes: CSSKeyframeRule[]; } /** - * Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been -enabled until the result of this command is received. + * Representation of a custom property registration through CSS.registerProperty */ - export type enableParameters = { - } - export type enableReturnValue = { + export interface CSSPropertyRegistration { + propertyName: string; + initialValue?: Value; + inherits: boolean; + syntax: string; } /** - * Ensures that the given node will have specified pseudo-classes whenever its style is computed by -the browser. + * CSS font-palette-values rule representation. */ - export type forcePseudoStateParameters = { + export interface CSSFontPaletteValuesRule { /** - * The element id for which to force the pseudo state. + * The css style sheet identifier (absent for user agent stylesheet and user-specified +stylesheet rules) this rule came from. */ - nodeId: DOM.NodeId; + styleSheetId?: StyleSheetId; /** - * Element pseudo classes to force when computing the element's style. + * Parent stylesheet's origin. */ - forcedPseudoClasses: string[]; - } - export type forcePseudoStateReturnValue = { - } - /** - * Ensures that the given node is in its starting-style state. - */ - export type forceStartingStyleParameters = { + origin: StyleSheetOrigin; /** - * The element id for which to force the starting-style state. + * Associated font palette name. */ - nodeId: DOM.NodeId; + fontPaletteName: Value; /** - * Boolean indicating if this is on or off. + * Associated style declaration. */ - forced: boolean; - } - export type forceStartingStyleReturnValue = { + style: CSSStyle; } - export type getBackgroundColorsParameters = { + /** + * CSS property at-rule representation. + */ + export interface CSSPropertyRule { /** - * Id of the node to get background colors for. + * The css style sheet identifier (absent for user agent stylesheet and user-specified +stylesheet rules) this rule came from. */ - nodeId: DOM.NodeId; - } - export type getBackgroundColorsReturnValue = { + styleSheetId?: StyleSheetId; /** - * The range of background colors behind this element, if it contains any visible text. If no -visible text is present, this will be undefined. In the case of a flat background color, -this will consist of simply that color. In the case of a gradient, this will consist of each -of the color stops. For anything more complicated, this will be an empty array. Images will -be ignored (as if the image had failed to load). + * Parent stylesheet's origin. */ - backgroundColors?: string[]; + origin: StyleSheetOrigin; /** - * The computed font size for this node, as a CSS computed value string (e.g. '12px'). + * Associated property name. */ - computedFontSize?: string; + propertyName: Value; /** - * The computed font weight for this node, as a CSS computed value string (e.g. 'normal' or -'100'). + * Associated style declaration. */ - computedFontWeight?: string; + style: CSSStyle; } /** - * Returns the computed style for a DOM node identified by `nodeId`. + * CSS function argument representation. */ - export type getComputedStyleForNodeParameters = { - nodeId: DOM.NodeId; - } - export type getComputedStyleForNodeReturnValue = { + export interface CSSFunctionParameter { /** - * Computed style for the specified DOM node. + * The parameter name. */ - computedStyle: CSSComputedStyleProperty[]; + name: string; + /** + * The parameter type. + */ + type: string; } /** - * Resolve the specified values in the context of the provided element. -For example, a value of '1em' is evaluated according to the computed -'font-size' of the element and a value 'calc(1px + 2px)' will be -resolved to '3px'. -If the `propertyName` was specified the `values` are resolved as if -they were property's declaration. If a value cannot be parsed according -to the provided property syntax, the value is parsed using combined -syntax as if null `propertyName` was provided. If the value cannot be -resolved even then, return the provided value without any changes. + * CSS function conditional block representation. */ - export type resolveValuesParameters = { - /** - * Substitution functions (var()/env()/attr()) and cascade-dependent -keywords (revert/revert-layer) do not work. - */ - values: string[]; - /** - * Id of the node in whose context the expression is evaluated - */ - nodeId: DOM.NodeId; + export interface CSSFunctionConditionNode { /** - * Only longhands and custom property names are accepted. + * Media query for this conditional block. Only one type of condition should be set. */ - propertyName?: string; + media?: CSSMedia; /** - * Pseudo element type, only works for pseudo elements that generate -elements in the tree, such as ::before and ::after. + * Container query for this conditional block. Only one type of condition should be set. */ - pseudoType?: DOM.PseudoType; + containerQueries?: CSSContainerQuery; /** - * Pseudo element custom ident. + * @supports CSS at-rule condition. Only one type of condition should be set. */ - pseudoIdentifier?: string; - } - export type resolveValuesReturnValue = { - results: string[]; - } - export type getLonghandPropertiesParameters = { - shorthandName: string; - value: string; - } - export type getLonghandPropertiesReturnValue = { - longhandProperties: CSSProperty[]; - } - /** - * Returns the styles defined inline (explicitly in the "style" attribute and implicitly, using DOM -attributes) for a DOM node identified by `nodeId`. - */ - export type getInlineStylesForNodeParameters = { - nodeId: DOM.NodeId; - } - export type getInlineStylesForNodeReturnValue = { + supports?: CSSSupports; /** - * Inline style for the specified DOM node. + * Block body. */ - inlineStyle?: CSSStyle; + children: CSSFunctionNode[]; /** - * Attribute-defined element style (e.g. resulting from "width=20 height=100%"). + * The condition text. */ - attributesStyle?: CSSStyle; + conditionText: string; } /** - * Returns the styles coming from animations & transitions -including the animation & transition styles coming from inheritance chain. + * Section of the body of a CSS function rule. */ - export type getAnimatedStylesForNodeParameters = { - nodeId: DOM.NodeId; - } - export type getAnimatedStylesForNodeReturnValue = { - /** - * Styles coming from animations. - */ - animationStyles?: CSSAnimationStyle[]; + export interface CSSFunctionNode { /** - * Style coming from transitions. + * A conditional block. If set, style should not be set. */ - transitionsStyle?: CSSStyle; + condition?: CSSFunctionConditionNode; /** - * Inherited style entries for animationsStyle and transitionsStyle from -the inheritance chain of the element. + * Values set by this node. If set, condition should not be set. */ - inherited?: InheritedAnimatedStyleEntry[]; + style?: CSSStyle; } /** - * Returns requested styles for a DOM node identified by `nodeId`. + * CSS function at-rule representation. */ - export type getMatchedStylesForNodeParameters = { - nodeId: DOM.NodeId; - } - export type getMatchedStylesForNodeReturnValue = { - /** - * Inline style for the specified DOM node. - */ - inlineStyle?: CSSStyle; - /** - * Attribute-defined element style (e.g. resulting from "width=20 height=100%"). - */ - attributesStyle?: CSSStyle; + export interface CSSFunctionRule { /** - * CSS rules matching this node, from all applicable stylesheets. + * Name of the function. */ - matchedCSSRules?: RuleMatch[]; + name: Value; /** - * Pseudo style matches for this node. + * The css style sheet identifier (absent for user agent stylesheet and user-specified +stylesheet rules) this rule came from. */ - pseudoElements?: PseudoElementMatches[]; + styleSheetId?: StyleSheetId; /** - * A chain of inherited styles (from the immediate node parent up to the DOM tree root). + * Parent stylesheet's origin. */ - inherited?: InheritedStyleEntry[]; + origin: StyleSheetOrigin; /** - * A chain of inherited pseudo element styles (from the immediate node parent up to the DOM tree root). + * List of parameters. */ - inheritedPseudoElements?: InheritedPseudoElementMatches[]; + parameters: CSSFunctionParameter[]; /** - * A list of CSS keyframed animations matching this node. + * Function body. */ - cssKeyframesRules?: CSSKeyframesRule[]; + children: CSSFunctionNode[]; + } + /** + * CSS keyframe rule representation. + */ + export interface CSSKeyframeRule { /** - * A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property. + * The css style sheet identifier (absent for user agent stylesheet and user-specified +stylesheet rules) this rule came from. */ - cssPositionTryRules?: CSSPositionTryRule[]; + styleSheetId?: StyleSheetId; /** - * Index of the active fallback in the applied position-try-fallback property, -will not be set if there is no active position-try fallback. + * Parent stylesheet's origin. */ - activePositionFallbackIndex?: number; + origin: StyleSheetOrigin; /** - * A list of CSS at-property rules matching this node. + * Associated key text. */ - cssPropertyRules?: CSSPropertyRule[]; + keyText: Value; /** - * A list of CSS property registrations matching this node. + * Associated style declaration. */ - cssPropertyRegistrations?: CSSPropertyRegistration[]; + style: CSSStyle; + } + /** + * A descriptor of operation to mutate style declaration text. + */ + export interface StyleDeclarationEdit { /** - * A font-palette-values rule matching this node. + * The css style sheet identifier. */ - cssFontPaletteValuesRule?: CSSFontPaletteValuesRule; + styleSheetId: StyleSheetId; /** - * Id of the first parent element that does not have display: contents. + * The range of the style text in the enclosing stylesheet. */ - parentLayoutNodeId?: DOM.NodeId; + range: SourceRange; /** - * A list of CSS at-function rules referenced by styles of this node. + * New style text. */ - cssFunctionRules?: CSSFunctionRule[]; + text: string; } + /** - * Returns the values of the default UA-defined environment variables used in env() + * Fires whenever a web font is updated. A non-empty font parameter indicates a successfully loaded +web font. */ - export type getEnvironmentVariablesParameters = { - } - export type getEnvironmentVariablesReturnValue = { - environmentVariables: { [key: string]: string }; + export type fontsUpdatedPayload = { + /** + * The web font that has loaded. + */ + font?: FontFace; } /** - * Returns all media queries parsed by the rendering engine. - */ - export type getMediaQueriesParameters = { - } - export type getMediaQueriesReturnValue = { - medias: CSSMedia[]; - } - /** - * Requests information about platform fonts which we used to render child TextNodes in the given -node. + * Fires whenever a MediaQuery result changes (for example, after a browser window has been +resized.) The current implementation considers only viewport-dependent media features. */ - export type getPlatformFontsForNodeParameters = { - nodeId: DOM.NodeId; - } - export type getPlatformFontsForNodeReturnValue = { - /** - * Usage statistics for every employed platform font. - */ - fonts: PlatformFontUsage[]; - } + export type mediaQueryResultChangedPayload = void; /** - * Returns the current textual content for a stylesheet. + * Fired whenever an active document stylesheet is added. */ - export type getStyleSheetTextParameters = { - styleSheetId: StyleSheetId; - } - export type getStyleSheetTextReturnValue = { + export type styleSheetAddedPayload = { /** - * The stylesheet text. + * Added stylesheet metainfo. */ - text: string; - } - /** - * Returns all layers parsed by the rendering engine for the tree scope of a node. -Given a DOM element identified by nodeId, getLayersForNode returns the root -layer for the nearest ancestor document or shadow root. The layer root contains -the full layer tree for the tree scope and their ordering. - */ - export type getLayersForNodeParameters = { - nodeId: DOM.NodeId; - } - export type getLayersForNodeReturnValue = { - rootLayer: CSSLayerData; + header: CSSStyleSheetHeader; } /** - * Given a CSS selector text and a style sheet ID, getLocationForSelector -returns an array of locations of the CSS selector in the style sheet. + * Fired whenever a stylesheet is changed as a result of the client operation. */ - export type getLocationForSelectorParameters = { + export type styleSheetChangedPayload = { styleSheetId: StyleSheetId; - selectorText: string; - } - export type getLocationForSelectorReturnValue = { - ranges: SourceRange[]; - } - /** - * Starts tracking the given node for the computed style updates -and whenever the computed style is updated for node, it queues -a `computedStyleUpdated` event with throttling. -There can only be 1 node tracked for computed style updates -so passing a new node id removes tracking from the previous node. -Pass `undefined` to disable tracking. - */ - export type trackComputedStyleUpdatesForNodeParameters = { - nodeId?: DOM.NodeId; - } - export type trackComputedStyleUpdatesForNodeReturnValue = { - } - /** - * Starts tracking the given computed styles for updates. The specified array of properties -replaces the one previously specified. Pass empty array to disable tracking. -Use takeComputedStyleUpdates to retrieve the list of nodes that had properties modified. -The changes to computed style properties are only tracked for nodes pushed to the front-end -by the DOM agent. If no changes to the tracked properties occur after the node has been pushed -to the front-end, no updates will be issued for the node. - */ - export type trackComputedStyleUpdatesParameters = { - propertiesToTrack: CSSComputedStyleProperty[]; - } - export type trackComputedStyleUpdatesReturnValue = { } /** - * Polls the next batch of computed style updates. + * Fired whenever an active document stylesheet is removed. */ - export type takeComputedStyleUpdatesParameters = { - } - export type takeComputedStyleUpdatesReturnValue = { + export type styleSheetRemovedPayload = { /** - * The list of node Ids that have their tracked computed styles updated. + * Identifier of the removed stylesheet. */ - nodeIds: DOM.NodeId[]; + styleSheetId: StyleSheetId; } - /** - * Find a rule with the given active property for the given node and set the new value for this -property - */ - export type setEffectivePropertyValueForNodeParameters = { + export type computedStyleUpdatedPayload = { /** - * The element id for which to set property. + * The node id that has updated computed styles. */ nodeId: DOM.NodeId; - propertyName: string; - value: string; - } - export type setEffectivePropertyValueForNodeReturnValue = { } + /** - * Modifies the property rule property name. + * Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the +position specified by `location`. */ - export type setPropertyRulePropertyNameParameters = { - styleSheetId: StyleSheetId; - range: SourceRange; - propertyName: string; - } - export type setPropertyRulePropertyNameReturnValue = { + export type addRuleParameters = { /** - * The resulting key text after modification. + * The css style sheet identifier where a new rule should be inserted. */ - propertyName: Value; - } - /** - * Modifies the keyframe rule key text. - */ - export type setKeyframeKeyParameters = { styleSheetId: StyleSheetId; - range: SourceRange; - keyText: string; - } - export type setKeyframeKeyReturnValue = { /** - * The resulting key text after modification. + * The text of a new rule. */ - keyText: Value; - } - /** - * Modifies the rule selector. - */ - export type setMediaTextParameters = { - styleSheetId: StyleSheetId; - range: SourceRange; - text: string; + ruleText: string; + /** + * Text position of a new rule in the target style sheet. + */ + location: SourceRange; + /** + * NodeId for the DOM node in whose context custom property declarations for registered properties should be +validated. If omitted, declarations in the new rule text can only be validated statically, which may produce +incorrect results if the declaration contains a var() for example. + */ + nodeForPropertySyntaxValidation?: DOM.NodeId; } - export type setMediaTextReturnValue = { + export type addRuleReturnValue = { /** - * The resulting CSS media rule after modification. + * The newly created rule. */ - media: CSSMedia; + rule: CSSRule; } /** - * Modifies the expression of a container query. + * Returns all class names from specified stylesheet. */ - export type setContainerQueryTextParameters = { + export type collectClassNamesParameters = { styleSheetId: StyleSheetId; - range: SourceRange; - text: string; } - export type setContainerQueryTextReturnValue = { + export type collectClassNamesReturnValue = { /** - * The resulting CSS container query rule after modification. + * Class name list. */ - containerQuery: CSSContainerQuery; + classNames: string[]; } /** - * Modifies the expression of a supports at-rule. + * Creates a new special "via-inspector" stylesheet in the frame with given `frameId`. */ - export type setSupportsTextParameters = { - styleSheetId: StyleSheetId; - range: SourceRange; - text: string; + export type createStyleSheetParameters = { + /** + * Identifier of the frame where "via-inspector" stylesheet should be created. + */ + frameId: Page.FrameId; + /** + * If true, creates a new stylesheet for every call. If false, +returns a stylesheet previously created by a call with force=false +for the frame's document if it exists or creates a new stylesheet +(default: false). + */ + force?: boolean; } - export type setSupportsTextReturnValue = { + export type createStyleSheetReturnValue = { /** - * The resulting CSS Supports rule after modification. + * Identifier of the created "via-inspector" stylesheet. */ - supports: CSSSupports; + styleSheetId: StyleSheetId; } /** - * Modifies the expression of a scope at-rule. + * Disables the CSS agent for the given page. */ - export type setScopeTextParameters = { - styleSheetId: StyleSheetId; - range: SourceRange; - text: string; + export type disableParameters = { } - export type setScopeTextReturnValue = { - /** - * The resulting CSS Scope rule after modification. - */ - scope: CSSScope; + export type disableReturnValue = { } /** - * Modifies the rule selector. + * Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been +enabled until the result of this command is received. */ - export type setRuleSelectorParameters = { - styleSheetId: StyleSheetId; - range: SourceRange; - selector: string; + export type enableParameters = { } - export type setRuleSelectorReturnValue = { - /** - * The resulting selector list after modification. - */ - selectorList: SelectorList; + export type enableReturnValue = { } /** - * Sets the new stylesheet text. + * Ensures that the given node will have specified pseudo-classes whenever its style is computed by +the browser. */ - export type setStyleSheetTextParameters = { - styleSheetId: StyleSheetId; - text: string; - } - export type setStyleSheetTextReturnValue = { + export type forcePseudoStateParameters = { /** - * URL of source map associated with script (if any). + * The element id for which to force the pseudo state. */ - sourceMapURL?: string; + nodeId: DOM.NodeId; + /** + * Element pseudo classes to force when computing the element's style. + */ + forcedPseudoClasses: string[]; + } + export type forcePseudoStateReturnValue = { } /** - * Applies specified style edits one after another in the given order. + * Ensures that the given node is in its starting-style state. */ - export type setStyleTextsParameters = { - edits: StyleDeclarationEdit[]; + export type forceStartingStyleParameters = { /** - * NodeId for the DOM node in whose context custom property declarations for registered properties should be -validated. If omitted, declarations in the new rule text can only be validated statically, which may produce -incorrect results if the declaration contains a var() for example. + * The element id for which to force the starting-style state. */ - nodeForPropertySyntaxValidation?: DOM.NodeId; - } - export type setStyleTextsReturnValue = { + nodeId: DOM.NodeId; /** - * The resulting styles after modification. + * Boolean indicating if this is on or off. */ - styles: CSSStyle[]; + forced: boolean; } - /** - * Enables the selector recording. - */ - export type startRuleUsageTrackingParameters = { + export type forceStartingStyleReturnValue = { } - export type startRuleUsageTrackingReturnValue = { - } - /** - * Stop tracking rule usage and return the list of rules that were used since last call to -`takeCoverageDelta` (or since start of coverage instrumentation). - */ - export type stopRuleUsageTrackingParameters = { - } - export type stopRuleUsageTrackingReturnValue = { - ruleUsage: RuleUsage[]; - } - /** - * Obtain list of rules that became used since last call to this method (or since start of coverage -instrumentation). - */ - export type takeCoverageDeltaParameters = { - } - export type takeCoverageDeltaReturnValue = { - coverage: RuleUsage[]; - /** - * Monotonically increasing time, in seconds. - */ - timestamp: number; - } - /** - * Enables/disables rendering of local CSS fonts (enabled by default). - */ - export type setLocalFontsEnabledParameters = { + export type getBackgroundColorsParameters = { /** - * Whether rendering of local fonts is enabled. + * Id of the node to get background colors for. */ - enabled: boolean; - } - export type setLocalFontsEnabledReturnValue = { + nodeId: DOM.NodeId; } - } - - export module CacheStorage { - /** - * Unique identifier of the Cache object. - */ - export type CacheId = string; - /** - * type of HTTP response cached - */ - export type CachedResponseType = "basic"|"cors"|"default"|"error"|"opaqueResponse"|"opaqueRedirect"; - /** - * Data entry. - */ - export interface DataEntry { - /** - * Request URL. - */ - requestURL: string; - /** - * Request method. - */ - requestMethod: string; - /** - * Request headers - */ - requestHeaders: Header[]; + export type getBackgroundColorsReturnValue = { /** - * Number of seconds since epoch. + * The range of background colors behind this element, if it contains any visible text. If no +visible text is present, this will be undefined. In the case of a flat background color, +this will consist of simply that color. In the case of a gradient, this will consist of each +of the color stops. For anything more complicated, this will be an empty array. Images will +be ignored (as if the image had failed to load). */ - responseTime: number; + backgroundColors?: string[]; /** - * HTTP response status code. + * The computed font size for this node, as a CSS computed value string (e.g. '12px'). */ - responseStatus: number; + computedFontSize?: string; /** - * HTTP response status text. + * The computed font weight for this node, as a CSS computed value string (e.g. 'normal' or +'100'). */ - responseStatusText: string; + computedFontWeight?: string; + } + /** + * Returns the computed style for a DOM node identified by `nodeId`. + */ + export type getComputedStyleForNodeParameters = { + nodeId: DOM.NodeId; + } + export type getComputedStyleForNodeReturnValue = { /** - * HTTP response type + * Computed style for the specified DOM node. */ - responseType: CachedResponseType; + computedStyle: CSSComputedStyleProperty[]; /** - * Response headers + * A list of non-standard "extra fields" which blink stores alongside each +computed style. */ - responseHeaders: Header[]; + extraFields: ComputedStyleExtraFields; } /** - * Cache identifier. + * Resolve the specified values in the context of the provided element. +For example, a value of '1em' is evaluated according to the computed +'font-size' of the element and a value 'calc(1px + 2px)' will be +resolved to '3px'. +If the `propertyName` was specified the `values` are resolved as if +they were property's declaration. If a value cannot be parsed according +to the provided property syntax, the value is parsed using combined +syntax as if null `propertyName` was provided. If the value cannot be +resolved even then, return the provided value without any changes. */ - export interface Cache { + export type resolveValuesParameters = { /** - * An opaque unique id of the cache. + * Cascade-dependent keywords (revert/revert-layer) do not work. */ - cacheId: CacheId; + values: string[]; /** - * Security origin of the cache. + * Id of the node in whose context the expression is evaluated */ - securityOrigin: string; + nodeId: DOM.NodeId; /** - * Storage key of the cache. + * Only longhands and custom property names are accepted. */ - storageKey: string; + propertyName?: string; /** - * Storage bucket of the cache. + * Pseudo element type, only works for pseudo elements that generate +elements in the tree, such as ::before and ::after. */ - storageBucket?: Storage.StorageBucket; + pseudoType?: DOM.PseudoType; /** - * The name of the cache. + * Pseudo element custom ident. */ - cacheName: string; + pseudoIdentifier?: string; } - export interface Header { - name: string; + export type resolveValuesReturnValue = { + results: string[]; + } + export type getLonghandPropertiesParameters = { + shorthandName: string; value: string; } + export type getLonghandPropertiesReturnValue = { + longhandProperties: CSSProperty[]; + } /** - * Cached response + * Returns the styles defined inline (explicitly in the "style" attribute and implicitly, using DOM +attributes) for a DOM node identified by `nodeId`. */ - export interface CachedResponse { + export type getInlineStylesForNodeParameters = { + nodeId: DOM.NodeId; + } + export type getInlineStylesForNodeReturnValue = { /** - * Entry content, base64-encoded. + * Inline style for the specified DOM node. */ - body: binary; - } - - - /** - * Deletes a cache. - */ - export type deleteCacheParameters = { + inlineStyle?: CSSStyle; /** - * Id of cache for deletion. + * Attribute-defined element style (e.g. resulting from "width=20 height=100%"). */ - cacheId: CacheId; - } - export type deleteCacheReturnValue = { + attributesStyle?: CSSStyle; } /** - * Deletes a cache entry. + * Returns the styles coming from animations & transitions +including the animation & transition styles coming from inheritance chain. */ - export type deleteEntryParameters = { + export type getAnimatedStylesForNodeParameters = { + nodeId: DOM.NodeId; + } + export type getAnimatedStylesForNodeReturnValue = { /** - * Id of cache where the entry will be deleted. + * Styles coming from animations. */ - cacheId: CacheId; + animationStyles?: CSSAnimationStyle[]; /** - * URL spec of the request. + * Style coming from transitions. */ - request: string; - } - export type deleteEntryReturnValue = { + transitionsStyle?: CSSStyle; + /** + * Inherited style entries for animationsStyle and transitionsStyle from +the inheritance chain of the element. + */ + inherited?: InheritedAnimatedStyleEntry[]; } /** - * Requests cache names. + * Returns requested styles for a DOM node identified by `nodeId`. */ - export type requestCacheNamesParameters = { + export type getMatchedStylesForNodeParameters = { + nodeId: DOM.NodeId; + } + export type getMatchedStylesForNodeReturnValue = { /** - * At least and at most one of securityOrigin, storageKey, storageBucket must be specified. -Security origin. + * Inline style for the specified DOM node. */ - securityOrigin?: string; + inlineStyle?: CSSStyle; /** - * Storage key. + * Attribute-defined element style (e.g. resulting from "width=20 height=100%"). */ - storageKey?: string; + attributesStyle?: CSSStyle; /** - * Storage bucket. If not specified, it uses the default bucket. + * CSS rules matching this node, from all applicable stylesheets. */ - storageBucket?: Storage.StorageBucket; - } - export type requestCacheNamesReturnValue = { + matchedCSSRules?: RuleMatch[]; /** - * Caches for the security origin. + * Pseudo style matches for this node. */ - caches: Cache[]; - } - /** - * Fetches cache entry. - */ - export type requestCachedResponseParameters = { + pseudoElements?: PseudoElementMatches[]; /** - * Id of cache that contains the entry. + * A chain of inherited styles (from the immediate node parent up to the DOM tree root). */ - cacheId: CacheId; + inherited?: InheritedStyleEntry[]; /** - * URL spec of the request. + * A chain of inherited pseudo element styles (from the immediate node parent up to the DOM tree root). */ - requestURL: string; + inheritedPseudoElements?: InheritedPseudoElementMatches[]; /** - * headers of the request. + * A list of CSS keyframed animations matching this node. */ - requestHeaders: Header[]; - } - export type requestCachedResponseReturnValue = { + cssKeyframesRules?: CSSKeyframesRule[]; /** - * Response read from the cache. + * A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property. */ - response: CachedResponse; - } - /** - * Requests data from cache. - */ - export type requestEntriesParameters = { + cssPositionTryRules?: CSSPositionTryRule[]; /** - * ID of cache to get entries from. + * Index of the active fallback in the applied position-try-fallback property, +will not be set if there is no active position-try fallback. */ - cacheId: CacheId; + activePositionFallbackIndex?: number; /** - * Number of records to skip. + * A list of CSS at-property rules matching this node. */ - skipCount?: number; + cssPropertyRules?: CSSPropertyRule[]; /** - * Number of records to fetch. + * A list of CSS property registrations matching this node. */ - pageSize?: number; + cssPropertyRegistrations?: CSSPropertyRegistration[]; /** - * If present, only return the entries containing this substring in the path + * A font-palette-values rule matching this node. */ - pathFilter?: string; - } - export type requestEntriesReturnValue = { + cssFontPaletteValuesRule?: CSSFontPaletteValuesRule; /** - * Array of object store data entries. - */ - cacheDataEntries: DataEntry[]; - /** - * Count of returned entries from this storage. If pathFilter is empty, it -is the count of all entries from this storage. + * Id of the first parent element that does not have display: contents. */ - returnCount: number; - } - } - - /** - * A domain for interacting with Cast, Presentation API, and Remote Playback API -functionalities. - */ - export module Cast { - export interface Sink { - name: string; - id: string; + parentLayoutNodeId?: DOM.NodeId; /** - * Text describing the current session. Present only if there is an active -session on the sink. + * A list of CSS at-function rules referenced by styles of this node. */ - session?: string; + cssFunctionRules?: CSSFunctionRule[]; } - /** - * This is fired whenever the list of available sinks changes. A sink is a -device or a software surface that you can cast to. + * Returns the values of the default UA-defined environment variables used in env() */ - export type sinksUpdatedPayload = { - sinks: Sink[]; + export type getEnvironmentVariablesParameters = { } - /** - * This is fired whenever the outstanding issue/error message changes. -|issueMessage| is empty if there is no issue. - */ - export type issueUpdatedPayload = { - issueMessage: string; + export type getEnvironmentVariablesReturnValue = { + environmentVariables: { [key: string]: string }; } - /** - * Starts observing for sinks that can be used for tab mirroring, and if set, -sinks compatible with |presentationUrl| as well. When sinks are found, a -|sinksUpdated| event is fired. -Also starts observing for issue messages. When an issue is added or removed, -an |issueUpdated| event is fired. + * Returns all media queries parsed by the rendering engine. */ - export type enableParameters = { - presentationUrl?: string; + export type getMediaQueriesParameters = { } - export type enableReturnValue = { + export type getMediaQueriesReturnValue = { + medias: CSSMedia[]; } /** - * Stops observing for sinks and issues. + * Requests information about platform fonts which we used to render child TextNodes in the given +node. */ - export type disableParameters = { + export type getPlatformFontsForNodeParameters = { + nodeId: DOM.NodeId; } - export type disableReturnValue = { + export type getPlatformFontsForNodeReturnValue = { + /** + * Usage statistics for every employed platform font. + */ + fonts: PlatformFontUsage[]; } /** - * Sets a sink to be used when the web page requests the browser to choose a -sink via Presentation API, Remote Playback API, or Cast SDK. + * Returns the current textual content for a stylesheet. */ - export type setSinkToUseParameters = { - sinkName: string; + export type getStyleSheetTextParameters = { + styleSheetId: StyleSheetId; } - export type setSinkToUseReturnValue = { + export type getStyleSheetTextReturnValue = { + /** + * The stylesheet text. + */ + text: string; } /** - * Starts mirroring the desktop to the sink. + * Returns all layers parsed by the rendering engine for the tree scope of a node. +Given a DOM element identified by nodeId, getLayersForNode returns the root +layer for the nearest ancestor document or shadow root. The layer root contains +the full layer tree for the tree scope and their ordering. */ - export type startDesktopMirroringParameters = { - sinkName: string; + export type getLayersForNodeParameters = { + nodeId: DOM.NodeId; } - export type startDesktopMirroringReturnValue = { + export type getLayersForNodeReturnValue = { + rootLayer: CSSLayerData; } /** - * Starts mirroring the tab to the sink. + * Given a CSS selector text and a style sheet ID, getLocationForSelector +returns an array of locations of the CSS selector in the style sheet. */ - export type startTabMirroringParameters = { - sinkName: string; + export type getLocationForSelectorParameters = { + styleSheetId: StyleSheetId; + selectorText: string; } - export type startTabMirroringReturnValue = { + export type getLocationForSelectorReturnValue = { + ranges: SourceRange[]; } /** - * Stops the active Cast session on the sink. + * Starts tracking the given node for the computed style updates +and whenever the computed style is updated for node, it queues +a `computedStyleUpdated` event with throttling. +There can only be 1 node tracked for computed style updates +so passing a new node id removes tracking from the previous node. +Pass `undefined` to disable tracking. */ - export type stopCastingParameters = { - sinkName: string; + export type trackComputedStyleUpdatesForNodeParameters = { + nodeId?: DOM.NodeId; } - export type stopCastingReturnValue = { + export type trackComputedStyleUpdatesForNodeReturnValue = { } - } - - /** - * This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror object -that has an `id`. This `id` can be used to get additional information on the Node, resolve it into -the JavaScript object wrapper, etc. It is important that client receives DOM events only for the -nodes that are known to the client. Backend keeps track of the nodes that were sent to the client -and never sends the same node twice. It is client's responsibility to collect information about -the nodes that were sent to the client. Note that `iframe` owner elements will return -corresponding document elements as their child nodes. - */ - export module DOM { - /** - * Unique DOM node identifier. - */ - export type NodeId = number; /** - * Unique DOM node identifier used to reference a node that may not have been pushed to the -front-end. + * Starts tracking the given computed styles for updates. The specified array of properties +replaces the one previously specified. Pass empty array to disable tracking. +Use takeComputedStyleUpdates to retrieve the list of nodes that had properties modified. +The changes to computed style properties are only tracked for nodes pushed to the front-end +by the DOM agent. If no changes to the tracked properties occur after the node has been pushed +to the front-end, no updates will be issued for the node. */ - export type BackendNodeId = number; + export type trackComputedStyleUpdatesParameters = { + propertiesToTrack: CSSComputedStyleProperty[]; + } + export type trackComputedStyleUpdatesReturnValue = { + } /** - * Backend node with a friendly name. + * Polls the next batch of computed style updates. */ - export interface BackendNode { - /** - * `Node`'s nodeType. - */ - nodeType: number; + export type takeComputedStyleUpdatesParameters = { + } + export type takeComputedStyleUpdatesReturnValue = { /** - * `Node`'s nodeName. + * The list of node Ids that have their tracked computed styles updated. */ - nodeName: string; - backendNodeId: BackendNodeId; + nodeIds: DOM.NodeId[]; } /** - * Pseudo element type. + * Find a rule with the given active property for the given node and set the new value for this +property */ - export type PseudoType = "first-line"|"first-letter"|"checkmark"|"before"|"after"|"picker-icon"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-group-children"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker"|"permission-icon"; + export type setEffectivePropertyValueForNodeParameters = { + /** + * The element id for which to set property. + */ + nodeId: DOM.NodeId; + propertyName: string; + value: string; + } + export type setEffectivePropertyValueForNodeReturnValue = { + } /** - * Shadow root type. + * Modifies the property rule property name. */ - export type ShadowRootType = "user-agent"|"open"|"closed"; + export type setPropertyRulePropertyNameParameters = { + styleSheetId: StyleSheetId; + range: SourceRange; + propertyName: string; + } + export type setPropertyRulePropertyNameReturnValue = { + /** + * The resulting key text after modification. + */ + propertyName: Value; + } /** - * Document compatibility mode. + * Modifies the keyframe rule key text. */ - export type CompatibilityMode = "QuirksMode"|"LimitedQuirksMode"|"NoQuirksMode"; + export type setKeyframeKeyParameters = { + styleSheetId: StyleSheetId; + range: SourceRange; + keyText: string; + } + export type setKeyframeKeyReturnValue = { + /** + * The resulting key text after modification. + */ + keyText: Value; + } /** - * ContainerSelector physical axes + * Modifies the rule selector. */ - export type PhysicalAxes = "Horizontal"|"Vertical"|"Both"; + export type setMediaTextParameters = { + styleSheetId: StyleSheetId; + range: SourceRange; + text: string; + } + export type setMediaTextReturnValue = { + /** + * The resulting CSS media rule after modification. + */ + media: CSSMedia; + } /** - * ContainerSelector logical axes + * Modifies the expression of a container query. */ - export type LogicalAxes = "Inline"|"Block"|"Both"; + export type setContainerQueryTextParameters = { + styleSheetId: StyleSheetId; + range: SourceRange; + text: string; + } + export type setContainerQueryTextReturnValue = { + /** + * The resulting CSS container query rule after modification. + */ + containerQuery: CSSContainerQuery; + } /** - * Physical scroll orientation + * Modifies the expression of a supports at-rule. */ - export type ScrollOrientation = "horizontal"|"vertical"; + export type setSupportsTextParameters = { + styleSheetId: StyleSheetId; + range: SourceRange; + text: string; + } + export type setSupportsTextReturnValue = { + /** + * The resulting CSS Supports rule after modification. + */ + supports: CSSSupports; + } /** - * DOM interaction is implemented in terms of mirror objects that represent the actual DOM nodes. -DOMNode is a base node mirror type. + * Modifies the expression of a scope at-rule. */ - export interface Node { + export type setScopeTextParameters = { + styleSheetId: StyleSheetId; + range: SourceRange; + text: string; + } + export type setScopeTextReturnValue = { /** - * Node identifier that is passed into the rest of the DOM messages as the `nodeId`. Backend -will only push node with given `id` once. It is aware of all requested nodes and will only -fire DOM events for nodes known to the client. + * The resulting CSS Scope rule after modification. */ - nodeId: NodeId; - /** - * The id of the parent node if any. - */ - parentId?: NodeId; + scope: CSSScope; + } + /** + * Modifies the rule selector. + */ + export type setRuleSelectorParameters = { + styleSheetId: StyleSheetId; + range: SourceRange; + selector: string; + } + export type setRuleSelectorReturnValue = { /** - * The BackendNodeId for this node. + * The resulting selector list after modification. */ - backendNodeId: BackendNodeId; + selectorList: SelectorList; + } + /** + * Sets the new stylesheet text. + */ + export type setStyleSheetTextParameters = { + styleSheetId: StyleSheetId; + text: string; + } + export type setStyleSheetTextReturnValue = { /** - * `Node`'s nodeType. + * URL of source map associated with script (if any). */ - nodeType: number; + sourceMapURL?: string; + } + /** + * Applies specified style edits one after another in the given order. + */ + export type setStyleTextsParameters = { + edits: StyleDeclarationEdit[]; /** - * `Node`'s nodeName. + * NodeId for the DOM node in whose context custom property declarations for registered properties should be +validated. If omitted, declarations in the new rule text can only be validated statically, which may produce +incorrect results if the declaration contains a var() for example. */ - nodeName: string; + nodeForPropertySyntaxValidation?: DOM.NodeId; + } + export type setStyleTextsReturnValue = { /** - * `Node`'s localName. + * The resulting styles after modification. */ - localName: string; + styles: CSSStyle[]; + } + /** + * Enables the selector recording. + */ + export type startRuleUsageTrackingParameters = { + } + export type startRuleUsageTrackingReturnValue = { + } + /** + * Stop tracking rule usage and return the list of rules that were used since last call to +`takeCoverageDelta` (or since start of coverage instrumentation). + */ + export type stopRuleUsageTrackingParameters = { + } + export type stopRuleUsageTrackingReturnValue = { + ruleUsage: RuleUsage[]; + } + /** + * Obtain list of rules that became used since last call to this method (or since start of coverage +instrumentation). + */ + export type takeCoverageDeltaParameters = { + } + export type takeCoverageDeltaReturnValue = { + coverage: RuleUsage[]; /** - * `Node`'s nodeValue. + * Monotonically increasing time, in seconds. */ - nodeValue: string; + timestamp: number; + } + /** + * Enables/disables rendering of local CSS fonts (enabled by default). + */ + export type setLocalFontsEnabledParameters = { /** - * Child count for `Container` nodes. + * Whether rendering of local fonts is enabled. */ - childNodeCount?: number; + enabled: boolean; + } + export type setLocalFontsEnabledReturnValue = { + } + } + + export module CacheStorage { + /** + * Unique identifier of the Cache object. + */ + export type CacheId = string; + /** + * type of HTTP response cached + */ + export type CachedResponseType = "basic"|"cors"|"default"|"error"|"opaqueResponse"|"opaqueRedirect"; + /** + * Data entry. + */ + export interface DataEntry { /** - * Child nodes of this node when requested with children. + * Request URL. */ - children?: Node[]; + requestURL: string; /** - * Attributes of the `Element` node in the form of flat array `[name1, value1, name2, value2]`. + * Request method. */ - attributes?: string[]; + requestMethod: string; /** - * Document URL that `Document` or `FrameOwner` node points to. + * Request headers */ - documentURL?: string; + requestHeaders: Header[]; /** - * Base URL that `Document` or `FrameOwner` node uses for URL completion. + * Number of seconds since epoch. */ - baseURL?: string; + responseTime: number; /** - * `DocumentType`'s publicId. + * HTTP response status code. */ - publicId?: string; + responseStatus: number; /** - * `DocumentType`'s systemId. + * HTTP response status text. */ - systemId?: string; + responseStatusText: string; /** - * `DocumentType`'s internalSubset. + * HTTP response type */ - internalSubset?: string; + responseType: CachedResponseType; /** - * `Document`'s XML version in case of XML documents. + * Response headers */ - xmlVersion?: string; + responseHeaders: Header[]; + } + /** + * Cache identifier. + */ + export interface Cache { /** - * `Attr`'s name. + * An opaque unique id of the cache. */ - name?: string; + cacheId: CacheId; /** - * `Attr`'s value. + * Security origin of the cache. */ - value?: string; + securityOrigin: string; /** - * Pseudo element type for this node. + * Storage key of the cache. */ - pseudoType?: PseudoType; + storageKey: string; /** - * Pseudo element identifier for this node. Only present if there is a -valid pseudoType. + * Storage bucket of the cache. */ - pseudoIdentifier?: string; + storageBucket?: Storage.StorageBucket; /** - * Shadow root type. + * The name of the cache. */ - shadowRootType?: ShadowRootType; + cacheName: string; + } + export interface Header { + name: string; + value: string; + } + /** + * Cached response + */ + export interface CachedResponse { /** - * Frame ID for frame owner elements. + * Entry content, base64-encoded. */ - frameId?: Page.FrameId; + body: binary; + } + + + /** + * Deletes a cache. + */ + export type deleteCacheParameters = { /** - * Content document for frame owner elements. + * Id of cache for deletion. */ - contentDocument?: Node; + cacheId: CacheId; + } + export type deleteCacheReturnValue = { + } + /** + * Deletes a cache entry. + */ + export type deleteEntryParameters = { /** - * Shadow root list for given element host. + * Id of cache where the entry will be deleted. */ - shadowRoots?: Node[]; + cacheId: CacheId; /** - * Content document fragment for template elements. + * URL spec of the request. */ - templateContent?: Node; + request: string; + } + export type deleteEntryReturnValue = { + } + /** + * Requests cache names. + */ + export type requestCacheNamesParameters = { /** - * Pseudo elements associated with this node. + * At least and at most one of securityOrigin, storageKey, storageBucket must be specified. +Security origin. */ - pseudoElements?: Node[]; + securityOrigin?: string; /** - * Deprecated, as the HTML Imports API has been removed (crbug.com/937746). -This property used to return the imported document for the HTMLImport links. -The property is always undefined now. + * Storage key. */ - importedDocument?: Node; + storageKey?: string; /** - * Distributed nodes for given insertion point. + * Storage bucket. If not specified, it uses the default bucket. */ - distributedNodes?: BackendNode[]; + storageBucket?: Storage.StorageBucket; + } + export type requestCacheNamesReturnValue = { /** - * Whether the node is SVG. + * Caches for the security origin. */ - isSVG?: boolean; - compatibilityMode?: CompatibilityMode; - assignedSlot?: BackendNode; - isScrollable?: boolean; - } - /** - * A structure to hold the top-level node of a detached tree and an array of its retained descendants. - */ - export interface DetachedElementInfo { - treeNode: Node; - retainedNodeIds: NodeId[]; + caches: Cache[]; } /** - * A structure holding an RGBA color. + * Fetches cache entry. */ - export interface RGBA { + export type requestCachedResponseParameters = { /** - * The red component, in the [0-255] range. + * Id of cache that contains the entry. */ - r: number; + cacheId: CacheId; /** - * The green component, in the [0-255] range. + * URL spec of the request. */ - g: number; + requestURL: string; /** - * The blue component, in the [0-255] range. + * headers of the request. */ - b: number; + requestHeaders: Header[]; + } + export type requestCachedResponseReturnValue = { /** - * The alpha component, in the [0-1] range (default: 1). + * Response read from the cache. */ - a?: number; + response: CachedResponse; } /** - * An array of quad vertices, x immediately followed by y for each point, points clock-wise. - */ - export type Quad = number[]; - /** - * Box model. + * Requests data from cache. */ - export interface BoxModel { + export type requestEntriesParameters = { /** - * Content box + * ID of cache to get entries from. */ - content: Quad; + cacheId: CacheId; /** - * Padding box + * Number of records to skip. */ - padding: Quad; + skipCount?: number; /** - * Border box + * Number of records to fetch. */ - border: Quad; + pageSize?: number; /** - * Margin box + * If present, only return the entries containing this substring in the path */ - margin: Quad; + pathFilter?: string; + } + export type requestEntriesReturnValue = { /** - * Node width + * Array of object store data entries. */ - width: number; + cacheDataEntries: DataEntry[]; /** - * Node height + * Count of returned entries from this storage. If pathFilter is empty, it +is the count of all entries from this storage. */ - height: number; + returnCount: number; + } + } + + /** + * A domain for interacting with Cast, Presentation API, and Remote Playback API +functionalities. + */ + export module Cast { + export interface Sink { + name: string; + id: string; /** - * Shape outside coordinates + * Text describing the current session. Present only if there is an active +session on the sink. */ - shapeOutside?: ShapeOutsideInfo; + session?: string; } + /** - * CSS Shape Outside details. + * This is fired whenever the list of available sinks changes. A sink is a +device or a software surface that you can cast to. */ - export interface ShapeOutsideInfo { - /** - * Shape bounds - */ - bounds: Quad; - /** - * Shape coordinate details - */ - shape: any[]; - /** - * Margin shape bounds - */ - marginShape: any[]; + export type sinksUpdatedPayload = { + sinks: Sink[]; } /** - * Rectangle. + * This is fired whenever the outstanding issue/error message changes. +|issueMessage| is empty if there is no issue. */ - export interface Rect { - /** - * X coordinate - */ - x: number; - /** - * Y coordinate - */ - y: number; - /** - * Rectangle width - */ - width: number; - /** - * Rectangle height - */ - height: number; - } - export interface CSSComputedStyleProperty { - /** - * Computed style property name. - */ - name: string; - /** - * Computed style property value. - */ - value: string; + export type issueUpdatedPayload = { + issueMessage: string; } /** - * Fired when `Element`'s attribute is modified. + * Starts observing for sinks that can be used for tab mirroring, and if set, +sinks compatible with |presentationUrl| as well. When sinks are found, a +|sinksUpdated| event is fired. +Also starts observing for issue messages. When an issue is added or removed, +an |issueUpdated| event is fired. */ - export type attributeModifiedPayload = { - /** - * Id of the node that has changed. - */ - nodeId: NodeId; - /** - * Attribute name. - */ - name: string; - /** - * Attribute value. - */ - value: string; + export type enableParameters = { + presentationUrl?: string; + } + export type enableReturnValue = { } /** - * Fired when `Element`'s attribute is removed. + * Stops observing for sinks and issues. */ - export type attributeRemovedPayload = { - /** - * Id of the node that has changed. - */ - nodeId: NodeId; - /** - * A ttribute name. - */ - name: string; + export type disableParameters = { + } + export type disableReturnValue = { } /** - * Mirrors `DOMCharacterDataModified` event. + * Sets a sink to be used when the web page requests the browser to choose a +sink via Presentation API, Remote Playback API, or Cast SDK. */ - export type characterDataModifiedPayload = { - /** - * Id of the node that has changed. - */ - nodeId: NodeId; - /** - * New text value. - */ - characterData: string; + export type setSinkToUseParameters = { + sinkName: string; + } + export type setSinkToUseReturnValue = { } /** - * Fired when `Container`'s child node count has changed. + * Starts mirroring the desktop to the sink. */ - export type childNodeCountUpdatedPayload = { - /** - * Id of the node that has changed. - */ - nodeId: NodeId; - /** - * New node count. - */ - childNodeCount: number; + export type startDesktopMirroringParameters = { + sinkName: string; + } + export type startDesktopMirroringReturnValue = { } /** - * Mirrors `DOMNodeInserted` event. + * Starts mirroring the tab to the sink. */ - export type childNodeInsertedPayload = { - /** - * Id of the node that has changed. - */ - parentNodeId: NodeId; - /** - * Id of the previous sibling. - */ - previousNodeId: NodeId; - /** - * Inserted node data. - */ - node: Node; + export type startTabMirroringParameters = { + sinkName: string; + } + export type startTabMirroringReturnValue = { } /** - * Mirrors `DOMNodeRemoved` event. + * Stops the active Cast session on the sink. */ - export type childNodeRemovedPayload = { - /** - * Parent id. - */ - parentNodeId: NodeId; - /** - * Id of the node that has been removed. - */ - nodeId: NodeId; + export type stopCastingParameters = { + sinkName: string; + } + export type stopCastingReturnValue = { } + } + + /** + * This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror object +that has an `id`. This `id` can be used to get additional information on the Node, resolve it into +the JavaScript object wrapper, etc. It is important that client receives DOM events only for the +nodes that are known to the client. Backend keeps track of the nodes that were sent to the client +and never sends the same node twice. It is client's responsibility to collect information about +the nodes that were sent to the client. Note that `iframe` owner elements will return +corresponding document elements as their child nodes. + */ + export module DOM { /** - * Called when distribution is changed. + * Unique DOM node identifier. */ - export type distributedNodesUpdatedPayload = { + export type NodeId = number; + /** + * Unique DOM node identifier used to reference a node that may not have been pushed to the +front-end. + */ + export type BackendNodeId = number; + /** + * Backend node with a friendly name. + */ + export interface BackendNode { /** - * Insertion point where distributed nodes were updated. + * `Node`'s nodeType. */ - insertionPointId: NodeId; + nodeType: number; /** - * Distributed nodes for given insertion point. + * `Node`'s nodeName. */ - distributedNodes: BackendNode[]; + nodeName: string; + backendNodeId: BackendNodeId; } /** - * Fired when `Document` has been totally updated. Node ids are no longer valid. + * Pseudo element type. */ - export type documentUpdatedPayload = void; + export type PseudoType = "first-line"|"first-letter"|"checkmark"|"before"|"after"|"picker-icon"|"interest-hint"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-group-children"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker"|"permission-icon"; /** - * Fired when `Element`'s inline style is modified via a CSS property modification. + * Shadow root type. */ - export type inlineStyleInvalidatedPayload = { - /** - * Ids of the nodes for which the inline styles have been invalidated. - */ - nodeIds: NodeId[]; - } + export type ShadowRootType = "user-agent"|"open"|"closed"; /** - * Called when a pseudo element is added to an element. + * Document compatibility mode. */ - export type pseudoElementAddedPayload = { - /** - * Pseudo element's parent element id. - */ - parentId: NodeId; - /** - * The added pseudo element. - */ - pseudoElement: Node; - } + export type CompatibilityMode = "QuirksMode"|"LimitedQuirksMode"|"NoQuirksMode"; /** - * Called when top layer elements are changed. + * ContainerSelector physical axes */ - export type topLayerElementsUpdatedPayload = void; + export type PhysicalAxes = "Horizontal"|"Vertical"|"Both"; /** - * Fired when a node's scrollability state changes. + * ContainerSelector logical axes */ - export type scrollableFlagUpdatedPayload = { + export type LogicalAxes = "Inline"|"Block"|"Both"; + /** + * Physical scroll orientation + */ + export type ScrollOrientation = "horizontal"|"vertical"; + /** + * DOM interaction is implemented in terms of mirror objects that represent the actual DOM nodes. +DOMNode is a base node mirror type. + */ + export interface Node { /** - * The id of the node. + * Node identifier that is passed into the rest of the DOM messages as the `nodeId`. Backend +will only push node with given `id` once. It is aware of all requested nodes and will only +fire DOM events for nodes known to the client. */ - nodeId: DOM.NodeId; + nodeId: NodeId; /** - * If the node is scrollable. + * The id of the parent node if any. */ - isScrollable: boolean; - } - /** - * Called when a pseudo element is removed from an element. - */ - export type pseudoElementRemovedPayload = { + parentId?: NodeId; /** - * Pseudo element's parent element id. + * The BackendNodeId for this node. */ - parentId: NodeId; + backendNodeId: BackendNodeId; /** - * The removed pseudo element id. + * `Node`'s nodeType. */ - pseudoElementId: NodeId; - } - /** - * Fired when backend wants to provide client with the missing DOM structure. This happens upon -most of the calls requesting node ids. - */ - export type setChildNodesPayload = { + nodeType: number; /** - * Parent node id to populate with children. + * `Node`'s nodeName. */ - parentId: NodeId; + nodeName: string; /** - * Child nodes array. + * `Node`'s localName. */ - nodes: Node[]; - } - /** - * Called when shadow root is popped from the element. - */ - export type shadowRootPoppedPayload = { + localName: string; /** - * Host element id. + * `Node`'s nodeValue. */ - hostId: NodeId; + nodeValue: string; /** - * Shadow root id. + * Child count for `Container` nodes. */ - rootId: NodeId; - } - /** - * Called when shadow root is pushed into the element. - */ - export type shadowRootPushedPayload = { + childNodeCount?: number; /** - * Host element id. + * Child nodes of this node when requested with children. */ - hostId: NodeId; + children?: Node[]; /** - * Shadow root. + * Attributes of the `Element` node in the form of flat array `[name1, value1, name2, value2]`. */ - root: Node; - } - - /** - * Collects class names for the node with given id and all of it's child nodes. - */ - export type collectClassNamesFromSubtreeParameters = { + attributes?: string[]; /** - * Id of the node to collect class names. + * Document URL that `Document` or `FrameOwner` node points to. */ - nodeId: NodeId; - } - export type collectClassNamesFromSubtreeReturnValue = { + documentURL?: string; /** - * Class name list. + * Base URL that `Document` or `FrameOwner` node uses for URL completion. */ - classNames: string[]; - } - /** - * Creates a deep copy of the specified node and places it into the target container before the -given anchor. - */ - export type copyToParameters = { + baseURL?: string; /** - * Id of the node to copy. + * `DocumentType`'s publicId. */ - nodeId: NodeId; + publicId?: string; /** - * Id of the element to drop the copy into. + * `DocumentType`'s systemId. */ - targetNodeId: NodeId; + systemId?: string; /** - * Drop the copy before this node (if absent, the copy becomes the last child of -`targetNodeId`). + * `DocumentType`'s internalSubset. */ - insertBeforeNodeId?: NodeId; - } - export type copyToReturnValue = { + internalSubset?: string; /** - * Id of the node clone. + * `Document`'s XML version in case of XML documents. */ - nodeId: NodeId; - } - /** - * Describes node given its id, does not require domain to be enabled. Does not start tracking any -objects, can be used for automation. - */ - export type describeNodeParameters = { + xmlVersion?: string; /** - * Identifier of the node. + * `Attr`'s name. */ - nodeId?: NodeId; + name?: string; /** - * Identifier of the backend node. + * `Attr`'s value. */ - backendNodeId?: BackendNodeId; + value?: string; /** - * JavaScript object id of the node wrapper. + * Pseudo element type for this node. */ - objectId?: Runtime.RemoteObjectId; + pseudoType?: PseudoType; /** - * The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the -entire subtree or provide an integer larger than 0. + * Pseudo element identifier for this node. Only present if there is a +valid pseudoType. */ - depth?: number; + pseudoIdentifier?: string; /** - * Whether or not iframes and shadow roots should be traversed when returning the subtree -(default is false). + * Shadow root type. */ - pierce?: boolean; - } - export type describeNodeReturnValue = { + shadowRootType?: ShadowRootType; /** - * Node description. + * Frame ID for frame owner elements. */ - node: Node; - } - /** - * Scrolls the specified rect of the given node into view if not already visible. -Note: exactly one between nodeId, backendNodeId and objectId should be passed -to identify the node. - */ - export type scrollIntoViewIfNeededParameters = { + frameId?: Page.FrameId; /** - * Identifier of the node. + * Content document for frame owner elements. */ - nodeId?: NodeId; + contentDocument?: Node; /** - * Identifier of the backend node. + * Shadow root list for given element host. */ - backendNodeId?: BackendNodeId; + shadowRoots?: Node[]; /** - * JavaScript object id of the node wrapper. + * Content document fragment for template elements. */ - objectId?: Runtime.RemoteObjectId; + templateContent?: Node; /** - * The rect to be scrolled into view, relative to the node's border box, in CSS pixels. -When omitted, center of the node will be used, similar to Element.scrollIntoView. + * Pseudo elements associated with this node. */ - rect?: Rect; - } - export type scrollIntoViewIfNeededReturnValue = { - } - /** - * Disables DOM agent for the given page. - */ - export type disableParameters = { - } - export type disableReturnValue = { - } - /** - * Discards search results from the session with the given id. `getSearchResults` should no longer -be called for that search. - */ - export type discardSearchResultsParameters = { + pseudoElements?: Node[]; /** - * Unique search session identifier. + * Deprecated, as the HTML Imports API has been removed (crbug.com/937746). +This property used to return the imported document for the HTMLImport links. +The property is always undefined now. */ - searchId: string; - } - export type discardSearchResultsReturnValue = { - } - /** - * Enables DOM agent for the given page. - */ - export type enableParameters = { + importedDocument?: Node; /** - * Whether to include whitespaces in the children array of returned Nodes. + * Distributed nodes for given insertion point. */ - includeWhitespace?: "none"|"all"; + distributedNodes?: BackendNode[]; + /** + * Whether the node is SVG. + */ + isSVG?: boolean; + compatibilityMode?: CompatibilityMode; + assignedSlot?: BackendNode; + isScrollable?: boolean; } - export type enableReturnValue = { + /** + * A structure to hold the top-level node of a detached tree and an array of its retained descendants. + */ + export interface DetachedElementInfo { + treeNode: Node; + retainedNodeIds: NodeId[]; } /** - * Focuses the given element. + * A structure holding an RGBA color. */ - export type focusParameters = { + export interface RGBA { /** - * Identifier of the node. + * The red component, in the [0-255] range. */ - nodeId?: NodeId; + r: number; /** - * Identifier of the backend node. + * The green component, in the [0-255] range. */ - backendNodeId?: BackendNodeId; + g: number; /** - * JavaScript object id of the node wrapper. + * The blue component, in the [0-255] range. */ - objectId?: Runtime.RemoteObjectId; - } - export type focusReturnValue = { + b: number; + /** + * The alpha component, in the [0-1] range (default: 1). + */ + a?: number; } /** - * Returns attributes for the specified node. + * An array of quad vertices, x immediately followed by y for each point, points clock-wise. */ - export type getAttributesParameters = { + export type Quad = number[]; + /** + * Box model. + */ + export interface BoxModel { /** - * Id of the node to retrieve attributes for. + * Content box */ - nodeId: NodeId; - } - export type getAttributesReturnValue = { + content: Quad; /** - * An interleaved array of node attribute names and values. + * Padding box */ - attributes: string[]; - } - /** - * Returns boxes for the given node. - */ - export type getBoxModelParameters = { + padding: Quad; /** - * Identifier of the node. + * Border box */ - nodeId?: NodeId; + border: Quad; /** - * Identifier of the backend node. + * Margin box */ - backendNodeId?: BackendNodeId; + margin: Quad; /** - * JavaScript object id of the node wrapper. + * Node width */ - objectId?: Runtime.RemoteObjectId; - } - export type getBoxModelReturnValue = { + width: number; /** - * Box model for the node. + * Node height */ - model: BoxModel; + height: number; + /** + * Shape outside coordinates + */ + shapeOutside?: ShapeOutsideInfo; } /** - * Returns quads that describe node position on the page. This method -might return multiple quads for inline nodes. + * CSS Shape Outside details. */ - export type getContentQuadsParameters = { - /** - * Identifier of the node. - */ - nodeId?: NodeId; + export interface ShapeOutsideInfo { /** - * Identifier of the backend node. + * Shape bounds */ - backendNodeId?: BackendNodeId; + bounds: Quad; /** - * JavaScript object id of the node wrapper. + * Shape coordinate details */ - objectId?: Runtime.RemoteObjectId; - } - export type getContentQuadsReturnValue = { + shape: any[]; /** - * Quads that describe node layout relative to viewport. + * Margin shape bounds */ - quads: Quad[]; + marginShape: any[]; } /** - * Returns the root DOM node (and optionally the subtree) to the caller. -Implicitly enables the DOM domain events for the current target. + * Rectangle. */ - export type getDocumentParameters = { + export interface Rect { /** - * The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the -entire subtree or provide an integer larger than 0. + * X coordinate */ - depth?: number; + x: number; /** - * Whether or not iframes and shadow roots should be traversed when returning the subtree -(default is false). + * Y coordinate */ - pierce?: boolean; - } - export type getDocumentReturnValue = { + y: number; /** - * Resulting node. + * Rectangle width */ - root: Node; - } - /** - * Returns the root DOM node (and optionally the subtree) to the caller. -Deprecated, as it is not designed to work well with the rest of the DOM agent. -Use DOMSnapshot.captureSnapshot instead. - */ - export type getFlattenedDocumentParameters = { + width: number; /** - * The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the -entire subtree or provide an integer larger than 0. + * Rectangle height */ - depth?: number; + height: number; + } + export interface CSSComputedStyleProperty { /** - * Whether or not iframes and shadow roots should be traversed when returning the subtree -(default is false). + * Computed style property name. */ - pierce?: boolean; - } - export type getFlattenedDocumentReturnValue = { + name: string; /** - * Resulting node. + * Computed style property value. */ - nodes: Node[]; + value: string; } + /** - * Finds nodes with a given computed style in a subtree. + * Fired when `Element`'s attribute is modified. */ - export type getNodesForSubtreeByStyleParameters = { + export type attributeModifiedPayload = { /** - * Node ID pointing to the root of a subtree. + * Id of the node that has changed. */ nodeId: NodeId; /** - * The style to filter nodes by (includes nodes if any of properties matches). - */ - computedStyles: CSSComputedStyleProperty[]; - /** - * Whether or not iframes and shadow roots in the same target should be traversed when returning the -results (default is false). + * Attribute name. */ - pierce?: boolean; - } - export type getNodesForSubtreeByStyleReturnValue = { + name: string; /** - * Resulting nodes. + * Attribute value. */ - nodeIds: NodeId[]; + value: string; } /** - * Returns node id at given location. Depending on whether DOM domain is enabled, nodeId is -either returned or not. + * Fired when `Element`'s attribute is removed. */ - export type getNodeForLocationParameters = { - /** - * X coordinate. - */ - x: number; - /** - * Y coordinate. - */ - y: number; + export type attributeRemovedPayload = { /** - * False to skip to the nearest non-UA shadow root ancestor (default: false). + * Id of the node that has changed. */ - includeUserAgentShadowDOM?: boolean; + nodeId: NodeId; /** - * Whether to ignore pointer-events: none on elements and hit test them. + * A ttribute name. */ - ignorePointerEventsNone?: boolean; + name: string; } - export type getNodeForLocationReturnValue = { - /** - * Resulting node. - */ - backendNodeId: BackendNodeId; + /** + * Mirrors `DOMCharacterDataModified` event. + */ + export type characterDataModifiedPayload = { /** - * Frame this node belongs to. + * Id of the node that has changed. */ - frameId: Page.FrameId; + nodeId: NodeId; /** - * Id of the node at given coordinates, only when enabled and requested document. + * New text value. */ - nodeId?: NodeId; + characterData: string; } /** - * Returns node's HTML markup. + * Fired when `Container`'s child node count has changed. */ - export type getOuterHTMLParameters = { + export type childNodeCountUpdatedPayload = { /** - * Identifier of the node. + * Id of the node that has changed. */ - nodeId?: NodeId; + nodeId: NodeId; /** - * Identifier of the backend node. + * New node count. */ - backendNodeId?: BackendNodeId; + childNodeCount: number; + } + /** + * Mirrors `DOMNodeInserted` event. + */ + export type childNodeInsertedPayload = { /** - * JavaScript object id of the node wrapper. + * Id of the node that has changed. */ - objectId?: Runtime.RemoteObjectId; + parentNodeId: NodeId; /** - * Include all shadow roots. Equals to false if not specified. + * Id of the previous sibling. */ - includeShadowDOM?: boolean; - } - export type getOuterHTMLReturnValue = { + previousNodeId: NodeId; /** - * Outer HTML markup. + * Inserted node data. */ - outerHTML: string; + node: Node; } /** - * Returns the id of the nearest ancestor that is a relayout boundary. + * Mirrors `DOMNodeRemoved` event. */ - export type getRelayoutBoundaryParameters = { + export type childNodeRemovedPayload = { /** - * Id of the node. + * Parent id. */ - nodeId: NodeId; - } - export type getRelayoutBoundaryReturnValue = { + parentNodeId: NodeId; /** - * Relayout boundary node id for the given node. + * Id of the node that has been removed. */ nodeId: NodeId; } /** - * Returns search results from given `fromIndex` to given `toIndex` from the search with the given -identifier. + * Called when distribution is changed. */ - export type getSearchResultsParameters = { - /** - * Unique search session identifier. - */ - searchId: string; + export type distributedNodesUpdatedPayload = { /** - * Start index of the search result to be returned. + * Insertion point where distributed nodes were updated. */ - fromIndex: number; + insertionPointId: NodeId; /** - * End index of the search result to be returned. + * Distributed nodes for given insertion point. */ - toIndex: number; + distributedNodes: BackendNode[]; } - export type getSearchResultsReturnValue = { + /** + * Fired when `Document` has been totally updated. Node ids are no longer valid. + */ + export type documentUpdatedPayload = void; + /** + * Fired when `Element`'s inline style is modified via a CSS property modification. + */ + export type inlineStyleInvalidatedPayload = { /** - * Ids of the search result nodes. + * Ids of the nodes for which the inline styles have been invalidated. */ nodeIds: NodeId[]; } /** - * Hides any highlight. + * Called when a pseudo element is added to an element. */ - export type hideHighlightParameters = { - } - export type hideHighlightReturnValue = { - } - /** - * Highlights DOM node. - */ - export type highlightNodeParameters = { - } - export type highlightNodeReturnValue = { - } - /** - * Highlights given rectangle. - */ - export type highlightRectParameters = { - } - export type highlightRectReturnValue = { + export type pseudoElementAddedPayload = { + /** + * Pseudo element's parent element id. + */ + parentId: NodeId; + /** + * The added pseudo element. + */ + pseudoElement: Node; } /** - * Marks last undoable state. + * Called when top layer elements are changed. */ - export type markUndoableStateParameters = { - } - export type markUndoableStateReturnValue = { - } + export type topLayerElementsUpdatedPayload = void; /** - * Moves node into the new container, places it before the given anchor. + * Fired when a node's scrollability state changes. */ - export type moveToParameters = { + export type scrollableFlagUpdatedPayload = { /** - * Id of the node to move. + * The id of the node. */ - nodeId: NodeId; + nodeId: DOM.NodeId; /** - * Id of the element to drop the moved node into. + * If the node is scrollable. */ - targetNodeId: NodeId; + isScrollable: boolean; + } + /** + * Called when a pseudo element is removed from an element. + */ + export type pseudoElementRemovedPayload = { /** - * Drop node before this one (if absent, the moved node becomes the last child of -`targetNodeId`). + * Pseudo element's parent element id. */ - insertBeforeNodeId?: NodeId; - } - export type moveToReturnValue = { + parentId: NodeId; /** - * New id of the moved node. + * The removed pseudo element id. */ - nodeId: NodeId; + pseudoElementId: NodeId; } /** - * Searches for a given string in the DOM tree. Use `getSearchResults` to access search results or -`cancelSearch` to end this search session. + * Fired when backend wants to provide client with the missing DOM structure. This happens upon +most of the calls requesting node ids. */ - export type performSearchParameters = { + export type setChildNodesPayload = { /** - * Plain text or query selector or XPath search query. + * Parent node id to populate with children. */ - query: string; + parentId: NodeId; /** - * True to search in user agent shadow DOM. + * Child nodes array. */ - includeUserAgentShadowDOM?: boolean; + nodes: Node[]; } - export type performSearchReturnValue = { + /** + * Called when shadow root is popped from the element. + */ + export type shadowRootPoppedPayload = { /** - * Unique search session identifier. + * Host element id. */ - searchId: string; + hostId: NodeId; /** - * Number of search results. + * Shadow root id. */ - resultCount: number; + rootId: NodeId; } /** - * Requests that the node is sent to the caller given its path. // FIXME, use XPath + * Called when shadow root is pushed into the element. */ - export type pushNodeByPathToFrontendParameters = { + export type shadowRootPushedPayload = { /** - * Path to node in the proprietary format. + * Host element id. */ - path: string; - } - export type pushNodeByPathToFrontendReturnValue = { + hostId: NodeId; /** - * Id of the node for given path. + * Shadow root. */ - nodeId: NodeId; + root: Node; } + /** - * Requests that a batch of nodes is sent to the caller given their backend node ids. + * Collects class names for the node with given id and all of it's child nodes. */ - export type pushNodesByBackendIdsToFrontendParameters = { + export type collectClassNamesFromSubtreeParameters = { /** - * The array of backend node ids. + * Id of the node to collect class names. */ - backendNodeIds: BackendNodeId[]; + nodeId: NodeId; } - export type pushNodesByBackendIdsToFrontendReturnValue = { + export type collectClassNamesFromSubtreeReturnValue = { /** - * The array of ids of pushed nodes that correspond to the backend ids specified in -backendNodeIds. + * Class name list. */ - nodeIds: NodeId[]; + classNames: string[]; } /** - * Executes `querySelector` on a given node. + * Creates a deep copy of the specified node and places it into the target container before the +given anchor. */ - export type querySelectorParameters = { + export type copyToParameters = { /** - * Id of the node to query upon. + * Id of the node to copy. */ nodeId: NodeId; /** - * Selector string. + * Id of the element to drop the copy into. */ - selector: string; + targetNodeId: NodeId; + /** + * Drop the copy before this node (if absent, the copy becomes the last child of +`targetNodeId`). + */ + insertBeforeNodeId?: NodeId; } - export type querySelectorReturnValue = { + export type copyToReturnValue = { /** - * Query selector result. + * Id of the node clone. */ nodeId: NodeId; } /** - * Executes `querySelectorAll` on a given node. + * Describes node given its id, does not require domain to be enabled. Does not start tracking any +objects, can be used for automation. */ - export type querySelectorAllParameters = { + export type describeNodeParameters = { /** - * Id of the node to query upon. + * Identifier of the node. */ - nodeId: NodeId; + nodeId?: NodeId; /** - * Selector string. + * Identifier of the backend node. */ - selector: string; - } - export type querySelectorAllReturnValue = { + backendNodeId?: BackendNodeId; /** - * Query selector result. + * JavaScript object id of the node wrapper. */ - nodeIds: NodeId[]; - } - /** - * Returns NodeIds of current top layer elements. -Top layer is rendered closest to the user within a viewport, therefore its elements always -appear on top of all other content. - */ - export type getTopLayerElementsParameters = { + objectId?: Runtime.RemoteObjectId; + /** + * The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the +entire subtree or provide an integer larger than 0. + */ + depth?: number; + /** + * Whether or not iframes and shadow roots should be traversed when returning the subtree +(default is false). + */ + pierce?: boolean; } - export type getTopLayerElementsReturnValue = { + export type describeNodeReturnValue = { /** - * NodeIds of top layer elements + * Node description. */ - nodeIds: NodeId[]; + node: Node; } /** - * Returns the NodeId of the matched element according to certain relations. + * Scrolls the specified rect of the given node into view if not already visible. +Note: exactly one between nodeId, backendNodeId and objectId should be passed +to identify the node. */ - export type getElementByRelationParameters = { + export type scrollIntoViewIfNeededParameters = { /** - * Id of the node from which to query the relation. + * Identifier of the node. */ - nodeId: NodeId; + nodeId?: NodeId; /** - * Type of relation to get. + * Identifier of the backend node. */ - relation: "PopoverTarget"|"InterestTarget"|"CommandFor"; - } - export type getElementByRelationReturnValue = { + backendNodeId?: BackendNodeId; /** - * NodeId of the element matching the queried relation. + * JavaScript object id of the node wrapper. */ - nodeId: NodeId; + objectId?: Runtime.RemoteObjectId; + /** + * The rect to be scrolled into view, relative to the node's border box, in CSS pixels. +When omitted, center of the node will be used, similar to Element.scrollIntoView. + */ + rect?: Rect; + } + export type scrollIntoViewIfNeededReturnValue = { } /** - * Re-does the last undone action. + * Disables DOM agent for the given page. */ - export type redoParameters = { + export type disableParameters = { } - export type redoReturnValue = { + export type disableReturnValue = { } /** - * Removes attribute with given name from an element with given id. + * Discards search results from the session with the given id. `getSearchResults` should no longer +be called for that search. */ - export type removeAttributeParameters = { - /** - * Id of the element to remove attribute from. - */ - nodeId: NodeId; + export type discardSearchResultsParameters = { /** - * Name of the attribute to remove. + * Unique search session identifier. */ - name: string; + searchId: string; } - export type removeAttributeReturnValue = { + export type discardSearchResultsReturnValue = { } /** - * Removes node with given id. + * Enables DOM agent for the given page. */ - export type removeNodeParameters = { + export type enableParameters = { /** - * Id of the node to remove. + * Whether to include whitespaces in the children array of returned Nodes. */ - nodeId: NodeId; + includeWhitespace?: "none"|"all"; } - export type removeNodeReturnValue = { + export type enableReturnValue = { } /** - * Requests that children of the node with given id are returned to the caller in form of -`setChildNodes` events where not only immediate children are retrieved, but all children down to -the specified depth. + * Focuses the given element. */ - export type requestChildNodesParameters = { + export type focusParameters = { /** - * Id of the node to get children for. + * Identifier of the node. */ - nodeId: NodeId; + nodeId?: NodeId; /** - * The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the -entire subtree or provide an integer larger than 0. + * Identifier of the backend node. */ - depth?: number; + backendNodeId?: BackendNodeId; /** - * Whether or not iframes and shadow roots should be traversed when returning the sub-tree -(default is false). + * JavaScript object id of the node wrapper. */ - pierce?: boolean; + objectId?: Runtime.RemoteObjectId; } - export type requestChildNodesReturnValue = { + export type focusReturnValue = { } /** - * Requests that the node is sent to the caller given the JavaScript node object reference. All -nodes that form the path from the node to the root are also sent to the client as a series of -`setChildNodes` notifications. + * Returns attributes for the specified node. */ - export type requestNodeParameters = { + export type getAttributesParameters = { /** - * JavaScript object id to convert into node. + * Id of the node to retrieve attributes for. */ - objectId: Runtime.RemoteObjectId; + nodeId: NodeId; } - export type requestNodeReturnValue = { + export type getAttributesReturnValue = { /** - * Node id for given object. + * An interleaved array of node attribute names and values. */ - nodeId: NodeId; + attributes: string[]; } /** - * Resolves the JavaScript node object for a given NodeId or BackendNodeId. + * Returns boxes for the given node. */ - export type resolveNodeParameters = { + export type getBoxModelParameters = { /** - * Id of the node to resolve. + * Identifier of the node. */ nodeId?: NodeId; /** - * Backend identifier of the node to resolve. - */ - backendNodeId?: DOM.BackendNodeId; - /** - * Symbolic group name that can be used to release multiple objects. + * Identifier of the backend node. */ - objectGroup?: string; + backendNodeId?: BackendNodeId; /** - * Execution context in which to resolve the node. + * JavaScript object id of the node wrapper. */ - executionContextId?: Runtime.ExecutionContextId; + objectId?: Runtime.RemoteObjectId; } - export type resolveNodeReturnValue = { + export type getBoxModelReturnValue = { /** - * JavaScript object wrapper for given node. + * Box model for the node. */ - object: Runtime.RemoteObject; + model: BoxModel; } /** - * Sets attribute for an element with given id. + * Returns quads that describe node position on the page. This method +might return multiple quads for inline nodes. */ - export type setAttributeValueParameters = { + export type getContentQuadsParameters = { /** - * Id of the element to set attribute for. + * Identifier of the node. */ - nodeId: NodeId; + nodeId?: NodeId; /** - * Attribute name. + * Identifier of the backend node. */ - name: string; + backendNodeId?: BackendNodeId; /** - * Attribute value. + * JavaScript object id of the node wrapper. */ - value: string; + objectId?: Runtime.RemoteObjectId; } - export type setAttributeValueReturnValue = { + export type getContentQuadsReturnValue = { + /** + * Quads that describe node layout relative to viewport. + */ + quads: Quad[]; } /** - * Sets attributes on element with given id. This method is useful when user edits some existing -attribute value and types in several attribute name/value pairs. + * Returns the root DOM node (and optionally the subtree) to the caller. +Implicitly enables the DOM domain events for the current target. */ - export type setAttributesAsTextParameters = { + export type getDocumentParameters = { /** - * Id of the element to set attributes for. + * The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the +entire subtree or provide an integer larger than 0. */ - nodeId: NodeId; + depth?: number; /** - * Text with a number of attributes. Will parse this text using HTML parser. + * Whether or not iframes and shadow roots should be traversed when returning the subtree +(default is false). */ - text: string; + pierce?: boolean; + } + export type getDocumentReturnValue = { /** - * Attribute name to replace with new attributes derived from text in case text parsed -successfully. + * Resulting node. */ - name?: string; - } - export type setAttributesAsTextReturnValue = { + root: Node; } /** - * Sets files for the given file input element. + * Returns the root DOM node (and optionally the subtree) to the caller. +Deprecated, as it is not designed to work well with the rest of the DOM agent. +Use DOMSnapshot.captureSnapshot instead. */ - export type setFileInputFilesParameters = { - /** - * Array of file paths to set. - */ - files: string[]; + export type getFlattenedDocumentParameters = { /** - * Identifier of the node. + * The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the +entire subtree or provide an integer larger than 0. */ - nodeId?: NodeId; + depth?: number; /** - * Identifier of the backend node. + * Whether or not iframes and shadow roots should be traversed when returning the subtree +(default is false). */ - backendNodeId?: BackendNodeId; + pierce?: boolean; + } + export type getFlattenedDocumentReturnValue = { /** - * JavaScript object id of the node wrapper. + * Resulting node. */ - objectId?: Runtime.RemoteObjectId; - } - export type setFileInputFilesReturnValue = { + nodes: Node[]; } /** - * Sets if stack traces should be captured for Nodes. See `Node.getNodeStackTraces`. Default is disabled. + * Finds nodes with a given computed style in a subtree. */ - export type setNodeStackTracesEnabledParameters = { + export type getNodesForSubtreeByStyleParameters = { /** - * Enable or disable. + * Node ID pointing to the root of a subtree. */ - enable: boolean; + nodeId: NodeId; + /** + * The style to filter nodes by (includes nodes if any of properties matches). + */ + computedStyles: CSSComputedStyleProperty[]; + /** + * Whether or not iframes and shadow roots in the same target should be traversed when returning the +results (default is false). + */ + pierce?: boolean; } - export type setNodeStackTracesEnabledReturnValue = { + export type getNodesForSubtreeByStyleReturnValue = { + /** + * Resulting nodes. + */ + nodeIds: NodeId[]; } /** - * Gets stack traces associated with a Node. As of now, only provides stack trace for Node creation. + * Returns node id at given location. Depending on whether DOM domain is enabled, nodeId is +either returned or not. */ - export type getNodeStackTracesParameters = { + export type getNodeForLocationParameters = { /** - * Id of the node to get stack traces for. + * X coordinate. */ - nodeId: NodeId; + x: number; + /** + * Y coordinate. + */ + y: number; + /** + * False to skip to the nearest non-UA shadow root ancestor (default: false). + */ + includeUserAgentShadowDOM?: boolean; + /** + * Whether to ignore pointer-events: none on elements and hit test them. + */ + ignorePointerEventsNone?: boolean; } - export type getNodeStackTracesReturnValue = { + export type getNodeForLocationReturnValue = { /** - * Creation stack trace, if available. + * Resulting node. */ - creation?: Runtime.StackTrace; + backendNodeId: BackendNodeId; + /** + * Frame this node belongs to. + */ + frameId: Page.FrameId; + /** + * Id of the node at given coordinates, only when enabled and requested document. + */ + nodeId?: NodeId; } /** - * Returns file information for the given -File wrapper. + * Returns node's HTML markup. */ - export type getFileInfoParameters = { + export type getOuterHTMLParameters = { + /** + * Identifier of the node. + */ + nodeId?: NodeId; + /** + * Identifier of the backend node. + */ + backendNodeId?: BackendNodeId; /** * JavaScript object id of the node wrapper. */ - objectId: Runtime.RemoteObjectId; - } - export type getFileInfoReturnValue = { - path: string; - } - /** - * Returns list of detached nodes - */ - export type getDetachedDomNodesParameters = { - } - export type getDetachedDomNodesReturnValue = { + objectId?: Runtime.RemoteObjectId; /** - * The list of detached nodes + * Include all shadow roots. Equals to false if not specified. */ - detachedNodes: DetachedElementInfo[]; + includeShadowDOM?: boolean; } - /** - * Enables console to refer to the node with given id via $x (see Command Line API for more details -$x functions). - */ - export type setInspectedNodeParameters = { + export type getOuterHTMLReturnValue = { /** - * DOM node id to be accessible by means of $x command line API. + * Outer HTML markup. */ - nodeId: NodeId; - } - export type setInspectedNodeReturnValue = { + outerHTML: string; } /** - * Sets node name for a node with given id. + * Returns the id of the nearest ancestor that is a relayout boundary. */ - export type setNodeNameParameters = { + export type getRelayoutBoundaryParameters = { /** - * Id of the node to set name for. + * Id of the node. */ nodeId: NodeId; - /** - * New node's name. - */ - name: string; } - export type setNodeNameReturnValue = { + export type getRelayoutBoundaryReturnValue = { /** - * New node's id. + * Relayout boundary node id for the given node. */ nodeId: NodeId; } /** - * Sets node value for a node with given id. + * Returns search results from given `fromIndex` to given `toIndex` from the search with the given +identifier. */ - export type setNodeValueParameters = { + export type getSearchResultsParameters = { /** - * Id of the node to set value for. + * Unique search session identifier. */ - nodeId: NodeId; + searchId: string; /** - * New node's value. + * Start index of the search result to be returned. */ - value: string; + fromIndex: number; + /** + * End index of the search result to be returned. + */ + toIndex: number; } - export type setNodeValueReturnValue = { + export type getSearchResultsReturnValue = { + /** + * Ids of the search result nodes. + */ + nodeIds: NodeId[]; } /** - * Sets node HTML markup, returns new node id. + * Hides any highlight. */ - export type setOuterHTMLParameters = { - /** - * Id of the node to set markup for. - */ - nodeId: NodeId; - /** - * Outer HTML markup to set. - */ - outerHTML: string; + export type hideHighlightParameters = { } - export type setOuterHTMLReturnValue = { + export type hideHighlightReturnValue = { } /** - * Undoes the last performed action. + * Highlights DOM node. */ - export type undoParameters = { + export type highlightNodeParameters = { } - export type undoReturnValue = { + export type highlightNodeReturnValue = { } /** - * Returns iframe node that owns iframe with the given domain. + * Highlights given rectangle. */ - export type getFrameOwnerParameters = { - frameId: Page.FrameId; + export type highlightRectParameters = { } - export type getFrameOwnerReturnValue = { - /** - * Resulting node. - */ - backendNodeId: BackendNodeId; - /** - * Id of the node at given coordinates, only when enabled and requested document. - */ - nodeId?: NodeId; + export type highlightRectReturnValue = { } /** - * Returns the query container of the given node based on container query -conditions: containerName, physical and logical axes, and whether it queries -scroll-state or anchored elements. If no axes are provided and -queriesScrollState is false, the style container is returned, which is the -direct parent or the closest element with a matching container-name. + * Marks last undoable state. */ - export type getContainerForNodeParameters = { - nodeId: NodeId; - containerName?: string; - physicalAxes?: PhysicalAxes; - logicalAxes?: LogicalAxes; - queriesScrollState?: boolean; - queriesAnchored?: boolean; + export type markUndoableStateParameters = { } - export type getContainerForNodeReturnValue = { - /** - * The container node for the given node, or null if not found. - */ - nodeId?: NodeId; + export type markUndoableStateReturnValue = { } /** - * Returns the descendants of a container query container that have -container queries against this container. + * Moves node into the new container, places it before the given anchor. */ - export type getQueryingDescendantsForContainerParameters = { + export type moveToParameters = { /** - * Id of the container node to find querying descendants from. + * Id of the node to move. */ nodeId: NodeId; - } - export type getQueryingDescendantsForContainerReturnValue = { - /** - * Descendant nodes with container queries against the given container. - */ - nodeIds: NodeId[]; - } - /** - * Returns the target anchor element of the given anchor query according to -https://www.w3.org/TR/css-anchor-position-1/#target. - */ - export type getAnchorElementParameters = { /** - * Id of the positioned element from which to find the anchor. + * Id of the element to drop the moved node into. */ - nodeId: NodeId; + targetNodeId: NodeId; /** - * An optional anchor specifier, as defined in -https://www.w3.org/TR/css-anchor-position-1/#anchor-specifier. -If not provided, it will return the implicit anchor element for -the given positioned element. + * Drop node before this one (if absent, the moved node becomes the last child of +`targetNodeId`). */ - anchorSpecifier?: string; + insertBeforeNodeId?: NodeId; } - export type getAnchorElementReturnValue = { + export type moveToReturnValue = { /** - * The anchor element of the given anchor query. + * New id of the moved node. */ nodeId: NodeId; } /** - * When enabling, this API force-opens the popover identified by nodeId -and keeps it open until disabled. + * Searches for a given string in the DOM tree. Use `getSearchResults` to access search results or +`cancelSearch` to end this search session. */ - export type forceShowPopoverParameters = { + export type performSearchParameters = { /** - * Id of the popover HTMLElement + * Plain text or query selector or XPath search query. */ - nodeId: NodeId; + query: string; /** - * If true, opens the popover and keeps it open. If false, closes the -popover if it was previously force-opened. + * True to search in user agent shadow DOM. */ - enable: boolean; + includeUserAgentShadowDOM?: boolean; } - export type forceShowPopoverReturnValue = { + export type performSearchReturnValue = { /** - * List of popovers that were closed in order to respect popover stacking order. + * Unique search session identifier. */ - nodeIds: NodeId[]; + searchId: string; + /** + * Number of search results. + */ + resultCount: number; } - } - - /** - * DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript -execution will stop on these operations as if there was a regular breakpoint set. - */ - export module DOMDebugger { - /** - * DOM breakpoint type. - */ - export type DOMBreakpointType = "subtree-modified"|"attribute-modified"|"node-removed"; - /** - * CSP Violation type. - */ - export type CSPViolationType = "trustedtype-sink-violation"|"trustedtype-policy-violation"; /** - * Object event listener. + * Requests that the node is sent to the caller given its path. // FIXME, use XPath */ - export interface EventListener { + export type pushNodeByPathToFrontendParameters = { /** - * `EventListener`'s type. + * Path to node in the proprietary format. */ - type: string; + path: string; + } + export type pushNodeByPathToFrontendReturnValue = { /** - * `EventListener`'s useCapture. + * Id of the node for given path. */ - useCapture: boolean; + nodeId: NodeId; + } + /** + * Requests that a batch of nodes is sent to the caller given their backend node ids. + */ + export type pushNodesByBackendIdsToFrontendParameters = { /** - * `EventListener`'s passive flag. + * The array of backend node ids. */ - passive: boolean; + backendNodeIds: BackendNodeId[]; + } + export type pushNodesByBackendIdsToFrontendReturnValue = { /** - * `EventListener`'s once flag. + * The array of ids of pushed nodes that correspond to the backend ids specified in +backendNodeIds. */ - once: boolean; + nodeIds: NodeId[]; + } + /** + * Executes `querySelector` on a given node. + */ + export type querySelectorParameters = { /** - * Script id of the handler code. + * Id of the node to query upon. */ - scriptId: Runtime.ScriptId; + nodeId: NodeId; /** - * Line number in the script (0-based). + * Selector string. */ - lineNumber: number; + selector: string; + } + export type querySelectorReturnValue = { /** - * Column number in the script (0-based). + * Query selector result. */ - columnNumber: number; + nodeId: NodeId; + } + /** + * Executes `querySelectorAll` on a given node. + */ + export type querySelectorAllParameters = { /** - * Event handler function value. + * Id of the node to query upon. */ - handler?: Runtime.RemoteObject; + nodeId: NodeId; /** - * Event original handler function value. + * Selector string. */ - originalHandler?: Runtime.RemoteObject; + selector: string; + } + export type querySelectorAllReturnValue = { /** - * Node the listener is added to (if any). + * Query selector result. */ - backendNodeId?: DOM.BackendNodeId; + nodeIds: NodeId[]; } - - /** - * Returns event listeners of the given object. + * Returns NodeIds of current top layer elements. +Top layer is rendered closest to the user within a viewport, therefore its elements always +appear on top of all other content. */ - export type getEventListenersParameters = { + export type getTopLayerElementsParameters = { + } + export type getTopLayerElementsReturnValue = { /** - * Identifier of the object to return listeners for. + * NodeIds of top layer elements */ - objectId: Runtime.RemoteObjectId; + nodeIds: NodeId[]; + } + /** + * Returns the NodeId of the matched element according to certain relations. + */ + export type getElementByRelationParameters = { /** - * The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the -entire subtree or provide an integer larger than 0. + * Id of the node from which to query the relation. */ - depth?: number; + nodeId: NodeId; /** - * Whether or not iframes and shadow roots should be traversed when returning the subtree -(default is false). Reports listeners for all contexts if pierce is enabled. + * Type of relation to get. */ - pierce?: boolean; + relation: "PopoverTarget"|"InterestTarget"|"CommandFor"; } - export type getEventListenersReturnValue = { + export type getElementByRelationReturnValue = { /** - * Array of relevant listeners. + * NodeId of the element matching the queried relation. */ - listeners: EventListener[]; + nodeId: NodeId; } /** - * Removes DOM breakpoint that was set using `setDOMBreakpoint`. + * Re-does the last undone action. */ - export type removeDOMBreakpointParameters = { + export type redoParameters = { + } + export type redoReturnValue = { + } + /** + * Removes attribute with given name from an element with given id. + */ + export type removeAttributeParameters = { /** - * Identifier of the node to remove breakpoint from. + * Id of the element to remove attribute from. */ - nodeId: DOM.NodeId; + nodeId: NodeId; /** - * Type of the breakpoint to remove. + * Name of the attribute to remove. */ - type: DOMBreakpointType; + name: string; } - export type removeDOMBreakpointReturnValue = { + export type removeAttributeReturnValue = { } /** - * Removes breakpoint on particular DOM event. + * Removes node with given id. */ - export type removeEventListenerBreakpointParameters = { + export type removeNodeParameters = { /** - * Event name. - */ - eventName: string; - /** - * EventTarget interface name. + * Id of the node to remove. */ - targetName?: string; + nodeId: NodeId; } - export type removeEventListenerBreakpointReturnValue = { + export type removeNodeReturnValue = { } /** - * Removes breakpoint on particular native event. + * Requests that children of the node with given id are returned to the caller in form of +`setChildNodes` events where not only immediate children are retrieved, but all children down to +the specified depth. */ - export type removeInstrumentationBreakpointParameters = { + export type requestChildNodesParameters = { /** - * Instrumentation name to stop on. + * Id of the node to get children for. */ - eventName: string; + nodeId: NodeId; + /** + * The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the +entire subtree or provide an integer larger than 0. + */ + depth?: number; + /** + * Whether or not iframes and shadow roots should be traversed when returning the sub-tree +(default is false). + */ + pierce?: boolean; } - export type removeInstrumentationBreakpointReturnValue = { + export type requestChildNodesReturnValue = { } /** - * Removes breakpoint from XMLHttpRequest. + * Requests that the node is sent to the caller given the JavaScript node object reference. All +nodes that form the path from the node to the root are also sent to the client as a series of +`setChildNodes` notifications. */ - export type removeXHRBreakpointParameters = { + export type requestNodeParameters = { /** - * Resource URL substring. + * JavaScript object id to convert into node. */ - url: string; + objectId: Runtime.RemoteObjectId; } - export type removeXHRBreakpointReturnValue = { + export type requestNodeReturnValue = { + /** + * Node id for given object. + */ + nodeId: NodeId; } /** - * Sets breakpoint on particular CSP violations. + * Resolves the JavaScript node object for a given NodeId or BackendNodeId. */ - export type setBreakOnCSPViolationParameters = { + export type resolveNodeParameters = { /** - * CSP Violations to stop upon. + * Id of the node to resolve. */ - violationTypes: CSPViolationType[]; + nodeId?: NodeId; + /** + * Backend identifier of the node to resolve. + */ + backendNodeId?: DOM.BackendNodeId; + /** + * Symbolic group name that can be used to release multiple objects. + */ + objectGroup?: string; + /** + * Execution context in which to resolve the node. + */ + executionContextId?: Runtime.ExecutionContextId; } - export type setBreakOnCSPViolationReturnValue = { + export type resolveNodeReturnValue = { + /** + * JavaScript object wrapper for given node. + */ + object: Runtime.RemoteObject; } /** - * Sets breakpoint on particular operation with DOM. + * Sets attribute for an element with given id. */ - export type setDOMBreakpointParameters = { + export type setAttributeValueParameters = { /** - * Identifier of the node to set breakpoint on. + * Id of the element to set attribute for. */ - nodeId: DOM.NodeId; + nodeId: NodeId; /** - * Type of the operation to stop upon. + * Attribute name. */ - type: DOMBreakpointType; + name: string; + /** + * Attribute value. + */ + value: string; } - export type setDOMBreakpointReturnValue = { + export type setAttributeValueReturnValue = { } /** - * Sets breakpoint on particular DOM event. + * Sets attributes on element with given id. This method is useful when user edits some existing +attribute value and types in several attribute name/value pairs. */ - export type setEventListenerBreakpointParameters = { + export type setAttributesAsTextParameters = { /** - * DOM Event name to stop on (any DOM event will do). + * Id of the element to set attributes for. */ - eventName: string; + nodeId: NodeId; /** - * EventTarget interface name to stop on. If equal to `"*"` or not provided, will stop on any -EventTarget. + * Text with a number of attributes. Will parse this text using HTML parser. */ - targetName?: string; + text: string; + /** + * Attribute name to replace with new attributes derived from text in case text parsed +successfully. + */ + name?: string; } - export type setEventListenerBreakpointReturnValue = { + export type setAttributesAsTextReturnValue = { } /** - * Sets breakpoint on particular native event. + * Sets files for the given file input element. */ - export type setInstrumentationBreakpointParameters = { + export type setFileInputFilesParameters = { /** - * Instrumentation name to stop on. + * Array of file paths to set. */ - eventName: string; + files: string[]; + /** + * Identifier of the node. + */ + nodeId?: NodeId; + /** + * Identifier of the backend node. + */ + backendNodeId?: BackendNodeId; + /** + * JavaScript object id of the node wrapper. + */ + objectId?: Runtime.RemoteObjectId; } - export type setInstrumentationBreakpointReturnValue = { + export type setFileInputFilesReturnValue = { } /** - * Sets breakpoint on XMLHttpRequest. + * Sets if stack traces should be captured for Nodes. See `Node.getNodeStackTraces`. Default is disabled. */ - export type setXHRBreakpointParameters = { + export type setNodeStackTracesEnabledParameters = { /** - * Resource URL substring. All XHRs having this substring in the URL will get stopped upon. + * Enable or disable. */ - url: string; + enable: boolean; } - export type setXHRBreakpointReturnValue = { + export type setNodeStackTracesEnabledReturnValue = { } - } - - /** - * EventBreakpoints permits setting JavaScript breakpoints on operations and events -occurring in native code invoked from JavaScript. Once breakpoint is hit, it is -reported through Debugger domain, similarly to regular breakpoints being hit. - */ - export module EventBreakpoints { - - /** - * Sets breakpoint on particular native event. + * Gets stack traces associated with a Node. As of now, only provides stack trace for Node creation. */ - export type setInstrumentationBreakpointParameters = { + export type getNodeStackTracesParameters = { /** - * Instrumentation name to stop on. + * Id of the node to get stack traces for. */ - eventName: string; + nodeId: NodeId; } - export type setInstrumentationBreakpointReturnValue = { + export type getNodeStackTracesReturnValue = { + /** + * Creation stack trace, if available. + */ + creation?: Runtime.StackTrace; } /** - * Removes breakpoint on particular native event. + * Returns file information for the given +File wrapper. */ - export type removeInstrumentationBreakpointParameters = { + export type getFileInfoParameters = { /** - * Instrumentation name to stop on. + * JavaScript object id of the node wrapper. */ - eventName: string; + objectId: Runtime.RemoteObjectId; } - export type removeInstrumentationBreakpointReturnValue = { + export type getFileInfoReturnValue = { + path: string; } /** - * Removes all breakpoints + * Returns list of detached nodes */ - export type disableParameters = { + export type getDetachedDomNodesParameters = { } - export type disableReturnValue = { + export type getDetachedDomNodesReturnValue = { + /** + * The list of detached nodes + */ + detachedNodes: DetachedElementInfo[]; } - } - - /** - * This domain facilitates obtaining document snapshots with DOM, layout, and style information. - */ - export module DOMSnapshot { /** - * A Node in the DOM tree. + * Enables console to refer to the node with given id via $x (see Command Line API for more details +$x functions). */ - export interface DOMNode { + export type setInspectedNodeParameters = { /** - * `Node`'s nodeType. + * DOM node id to be accessible by means of $x command line API. */ - nodeType: number; + nodeId: NodeId; + } + export type setInspectedNodeReturnValue = { + } + /** + * Sets node name for a node with given id. + */ + export type setNodeNameParameters = { /** - * `Node`'s nodeName. + * Id of the node to set name for. */ - nodeName: string; + nodeId: NodeId; /** - * `Node`'s nodeValue. + * New node's name. */ - nodeValue: string; + name: string; + } + export type setNodeNameReturnValue = { /** - * Only set for textarea elements, contains the text value. + * New node's id. */ - textValue?: string; + nodeId: NodeId; + } + /** + * Sets node value for a node with given id. + */ + export type setNodeValueParameters = { /** - * Only set for input elements, contains the input's associated text value. + * Id of the node to set value for. */ - inputValue?: string; + nodeId: NodeId; /** - * Only set for radio and checkbox input elements, indicates if the element has been checked + * New node's value. */ - inputChecked?: boolean; + value: string; + } + export type setNodeValueReturnValue = { + } + /** + * Sets node HTML markup, returns new node id. + */ + export type setOuterHTMLParameters = { /** - * Only set for option elements, indicates if the element has been selected + * Id of the node to set markup for. */ - optionSelected?: boolean; + nodeId: NodeId; /** - * `Node`'s id, corresponds to DOM.Node.backendNodeId. + * Outer HTML markup to set. */ - backendNodeId: DOM.BackendNodeId; - /** - * The indexes of the node's child nodes in the `domNodes` array returned by `getSnapshot`, if -any. - */ - childNodeIndexes?: number[]; + outerHTML: string; + } + export type setOuterHTMLReturnValue = { + } + /** + * Undoes the last performed action. + */ + export type undoParameters = { + } + export type undoReturnValue = { + } + /** + * Returns iframe node that owns iframe with the given domain. + */ + export type getFrameOwnerParameters = { + frameId: Page.FrameId; + } + export type getFrameOwnerReturnValue = { /** - * Attributes of an `Element` node. + * Resulting node. */ - attributes?: NameValue[]; + backendNodeId: BackendNodeId; /** - * Indexes of pseudo elements associated with this node in the `domNodes` array returned by -`getSnapshot`, if any. + * Id of the node at given coordinates, only when enabled and requested document. */ - pseudoElementIndexes?: number[]; + nodeId?: NodeId; + } + /** + * Returns the query container of the given node based on container query +conditions: containerName, physical and logical axes, and whether it queries +scroll-state or anchored elements. If no axes are provided and +queriesScrollState is false, the style container is returned, which is the +direct parent or the closest element with a matching container-name. + */ + export type getContainerForNodeParameters = { + nodeId: NodeId; + containerName?: string; + physicalAxes?: PhysicalAxes; + logicalAxes?: LogicalAxes; + queriesScrollState?: boolean; + queriesAnchored?: boolean; + } + export type getContainerForNodeReturnValue = { /** - * The index of the node's related layout tree node in the `layoutTreeNodes` array returned by -`getSnapshot`, if any. + * The container node for the given node, or null if not found. */ - layoutNodeIndex?: number; + nodeId?: NodeId; + } + /** + * Returns the descendants of a container query container that have +container queries against this container. + */ + export type getQueryingDescendantsForContainerParameters = { /** - * Document URL that `Document` or `FrameOwner` node points to. + * Id of the container node to find querying descendants from. */ - documentURL?: string; + nodeId: NodeId; + } + export type getQueryingDescendantsForContainerReturnValue = { /** - * Base URL that `Document` or `FrameOwner` node uses for URL completion. + * Descendant nodes with container queries against the given container. */ - baseURL?: string; + nodeIds: NodeId[]; + } + /** + * Returns the target anchor element of the given anchor query according to +https://www.w3.org/TR/css-anchor-position-1/#target. + */ + export type getAnchorElementParameters = { /** - * Only set for documents, contains the document's content language. + * Id of the positioned element from which to find the anchor. */ - contentLanguage?: string; + nodeId: NodeId; /** - * Only set for documents, contains the document's character set encoding. + * An optional anchor specifier, as defined in +https://www.w3.org/TR/css-anchor-position-1/#anchor-specifier. +If not provided, it will return the implicit anchor element for +the given positioned element. */ - documentEncoding?: string; + anchorSpecifier?: string; + } + export type getAnchorElementReturnValue = { /** - * `DocumentType` node's publicId. + * The anchor element of the given anchor query. */ - publicId?: string; + nodeId: NodeId; + } + /** + * When enabling, this API force-opens the popover identified by nodeId +and keeps it open until disabled. + */ + export type forceShowPopoverParameters = { /** - * `DocumentType` node's systemId. + * Id of the popover HTMLElement */ - systemId?: string; + nodeId: NodeId; /** - * Frame ID for frame owner elements and also for the document node. + * If true, opens the popover and keeps it open. If false, closes the +popover if it was previously force-opened. */ - frameId?: Page.FrameId; + enable: boolean; + } + export type forceShowPopoverReturnValue = { /** - * The index of a frame owner element's content document in the `domNodes` array returned by -`getSnapshot`, if any. + * List of popovers that were closed in order to respect popover stacking order. */ - contentDocumentIndex?: number; + nodeIds: NodeId[]; + } + } + + /** + * DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript +execution will stop on these operations as if there was a regular breakpoint set. + */ + export module DOMDebugger { + /** + * DOM breakpoint type. + */ + export type DOMBreakpointType = "subtree-modified"|"attribute-modified"|"node-removed"; + /** + * CSP Violation type. + */ + export type CSPViolationType = "trustedtype-sink-violation"|"trustedtype-policy-violation"; + /** + * Object event listener. + */ + export interface EventListener { /** - * Type of a pseudo element node. + * `EventListener`'s type. */ - pseudoType?: DOM.PseudoType; + type: string; /** - * Shadow root type. + * `EventListener`'s useCapture. */ - shadowRootType?: DOM.ShadowRootType; + useCapture: boolean; /** - * Whether this DOM node responds to mouse clicks. This includes nodes that have had click -event listeners attached via JavaScript as well as anchor tags that naturally navigate when -clicked. + * `EventListener`'s passive flag. */ - isClickable?: boolean; + passive: boolean; /** - * Details of the node's event listeners, if any. + * `EventListener`'s once flag. */ - eventListeners?: DOMDebugger.EventListener[]; + once: boolean; /** - * The selected url for nodes with a srcset attribute. + * Script id of the handler code. */ - currentSourceURL?: string; + scriptId: Runtime.ScriptId; /** - * The url of the script (if any) that generates this node. + * Line number in the script (0-based). */ - originURL?: string; + lineNumber: number; /** - * Scroll offsets, set when this node is a Document. + * Column number in the script (0-based). */ - scrollOffsetX?: number; - scrollOffsetY?: number; - } - /** - * Details of post layout rendered text positions. The exact layout should not be regarded as -stable and may change between versions. - */ - export interface InlineTextBox { + columnNumber: number; /** - * The bounding box in document coordinates. Note that scroll offset of the document is ignored. + * Event handler function value. */ - boundingBox: DOM.Rect; + handler?: Runtime.RemoteObject; /** - * The starting index in characters, for this post layout textbox substring. Characters that -would be represented as a surrogate pair in UTF-16 have length 2. + * Event original handler function value. */ - startCharacterIndex: number; + originalHandler?: Runtime.RemoteObject; /** - * The number of characters in this post layout textbox substring. Characters that would be -represented as a surrogate pair in UTF-16 have length 2. + * Node the listener is added to (if any). */ - numCharacters: number; + backendNodeId?: DOM.BackendNodeId; } + + /** - * Details of an element in the DOM tree with a LayoutObject. + * Returns event listeners of the given object. */ - export interface LayoutTreeNode { - /** - * The index of the related DOM node in the `domNodes` array returned by `getSnapshot`. - */ - domNodeIndex: number; - /** - * The bounding box in document coordinates. Note that scroll offset of the document is ignored. - */ - boundingBox: DOM.Rect; - /** - * Contents of the LayoutText, if any. - */ - layoutText?: string; + export type getEventListenersParameters = { /** - * The post-layout inline text nodes, if any. + * Identifier of the object to return listeners for. */ - inlineTextNodes?: InlineTextBox[]; + objectId: Runtime.RemoteObjectId; /** - * Index into the `computedStyles` array returned by `getSnapshot`. + * The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the +entire subtree or provide an integer larger than 0. */ - styleIndex?: number; + depth?: number; /** - * Global paint order index, which is determined by the stacking order of the nodes. Nodes -that are painted together will have the same index. Only provided if includePaintOrder in -getSnapshot was true. + * Whether or not iframes and shadow roots should be traversed when returning the subtree +(default is false). Reports listeners for all contexts if pierce is enabled. */ - paintOrder?: number; + pierce?: boolean; + } + export type getEventListenersReturnValue = { /** - * Set to true to indicate the element begins a new stacking context. + * Array of relevant listeners. */ - isStackingContext?: boolean; + listeners: EventListener[]; } /** - * A subset of the full ComputedStyle as defined by the request whitelist. + * Removes DOM breakpoint that was set using `setDOMBreakpoint`. */ - export interface ComputedStyle { + export type removeDOMBreakpointParameters = { /** - * Name/value pairs of computed style properties. + * Identifier of the node to remove breakpoint from. */ - properties: NameValue[]; + nodeId: DOM.NodeId; + /** + * Type of the breakpoint to remove. + */ + type: DOMBreakpointType; + } + export type removeDOMBreakpointReturnValue = { } /** - * A name/value pair. + * Removes breakpoint on particular DOM event. */ - export interface NameValue { + export type removeEventListenerBreakpointParameters = { /** - * Attribute/property name. + * Event name. */ - name: string; + eventName: string; /** - * Attribute/property value. + * EventTarget interface name. */ - value: string; + targetName?: string; + } + export type removeEventListenerBreakpointReturnValue = { } /** - * Index of the string in the strings table. + * Removes breakpoint on particular native event. */ - export type StringIndex = number; - /** - * Index of the string in the strings table. - */ - export type ArrayOfStrings = StringIndex[]; + export type removeInstrumentationBreakpointParameters = { + /** + * Instrumentation name to stop on. + */ + eventName: string; + } + export type removeInstrumentationBreakpointReturnValue = { + } /** - * Data that is only present on rare nodes. + * Removes breakpoint from XMLHttpRequest. */ - export interface RareStringData { - index: number[]; - value: StringIndex[]; - } - export interface RareBooleanData { - index: number[]; + export type removeXHRBreakpointParameters = { + /** + * Resource URL substring. + */ + url: string; } - export interface RareIntegerData { - index: number[]; - value: number[]; + export type removeXHRBreakpointReturnValue = { } - export type Rectangle = number[]; /** - * Document snapshot. + * Sets breakpoint on particular CSP violations. */ - export interface DocumentSnapshot { + export type setBreakOnCSPViolationParameters = { /** - * Document URL that `Document` or `FrameOwner` node points to. + * CSP Violations to stop upon. */ - documentURL: StringIndex; + violationTypes: CSPViolationType[]; + } + export type setBreakOnCSPViolationReturnValue = { + } + /** + * Sets breakpoint on particular operation with DOM. + */ + export type setDOMBreakpointParameters = { /** - * Document title. + * Identifier of the node to set breakpoint on. */ - title: StringIndex; + nodeId: DOM.NodeId; /** - * Base URL that `Document` or `FrameOwner` node uses for URL completion. + * Type of the operation to stop upon. */ - baseURL: StringIndex; + type: DOMBreakpointType; + } + export type setDOMBreakpointReturnValue = { + } + /** + * Sets breakpoint on particular DOM event. + */ + export type setEventListenerBreakpointParameters = { /** - * Contains the document's content language. + * DOM Event name to stop on (any DOM event will do). */ - contentLanguage: StringIndex; + eventName: string; /** - * Contains the document's character set encoding. + * EventTarget interface name to stop on. If equal to `"*"` or not provided, will stop on any +EventTarget. */ - encodingName: StringIndex; + targetName?: string; + } + export type setEventListenerBreakpointReturnValue = { + } + /** + * Sets breakpoint on particular native event. + */ + export type setInstrumentationBreakpointParameters = { /** - * `DocumentType` node's publicId. + * Instrumentation name to stop on. */ - publicId: StringIndex; + eventName: string; + } + export type setInstrumentationBreakpointReturnValue = { + } + /** + * Sets breakpoint on XMLHttpRequest. + */ + export type setXHRBreakpointParameters = { /** - * `DocumentType` node's systemId. + * Resource URL substring. All XHRs having this substring in the URL will get stopped upon. */ - systemId: StringIndex; + url: string; + } + export type setXHRBreakpointReturnValue = { + } + } + + /** + * This domain facilitates obtaining document snapshots with DOM, layout, and style information. + */ + export module DOMSnapshot { + /** + * A Node in the DOM tree. + */ + export interface DOMNode { /** - * Frame ID for frame owner elements and also for the document node. + * `Node`'s nodeType. */ - frameId: StringIndex; + nodeType: number; /** - * A table with dom nodes. + * `Node`'s nodeName. */ - nodes: NodeTreeSnapshot; + nodeName: string; /** - * The nodes in the layout tree. + * `Node`'s nodeValue. */ - layout: LayoutTreeSnapshot; + nodeValue: string; /** - * The post-layout inline text nodes. + * Only set for textarea elements, contains the text value. */ - textBoxes: TextBoxSnapshot; + textValue?: string; /** - * Horizontal scroll offset. + * Only set for input elements, contains the input's associated text value. */ - scrollOffsetX?: number; + inputValue?: string; /** - * Vertical scroll offset. + * Only set for radio and checkbox input elements, indicates if the element has been checked */ - scrollOffsetY?: number; + inputChecked?: boolean; /** - * Document content width. + * Only set for option elements, indicates if the element has been selected */ - contentWidth?: number; + optionSelected?: boolean; /** - * Document content height. + * `Node`'s id, corresponds to DOM.Node.backendNodeId. */ - contentHeight?: number; - } - /** - * Table containing nodes. - */ - export interface NodeTreeSnapshot { + backendNodeId: DOM.BackendNodeId; /** - * Parent node index. + * The indexes of the node's child nodes in the `domNodes` array returned by `getSnapshot`, if +any. */ - parentIndex?: number[]; + childNodeIndexes?: number[]; /** - * `Node`'s nodeType. + * Attributes of an `Element` node. */ - nodeType?: number[]; + attributes?: NameValue[]; /** - * Type of the shadow root the `Node` is in. String values are equal to the `ShadowRootType` enum. + * Indexes of pseudo elements associated with this node in the `domNodes` array returned by +`getSnapshot`, if any. */ - shadowRootType?: RareStringData; + pseudoElementIndexes?: number[]; /** - * `Node`'s nodeName. + * The index of the node's related layout tree node in the `layoutTreeNodes` array returned by +`getSnapshot`, if any. */ - nodeName?: StringIndex[]; + layoutNodeIndex?: number; /** - * `Node`'s nodeValue. + * Document URL that `Document` or `FrameOwner` node points to. */ - nodeValue?: StringIndex[]; + documentURL?: string; /** - * `Node`'s id, corresponds to DOM.Node.backendNodeId. + * Base URL that `Document` or `FrameOwner` node uses for URL completion. */ - backendNodeId?: DOM.BackendNodeId[]; + baseURL?: string; /** - * Attributes of an `Element` node. Flatten name, value pairs. + * Only set for documents, contains the document's content language. */ - attributes?: ArrayOfStrings[]; + contentLanguage?: string; /** - * Only set for textarea elements, contains the text value. + * Only set for documents, contains the document's character set encoding. */ - textValue?: RareStringData; + documentEncoding?: string; /** - * Only set for input elements, contains the input's associated text value. + * `DocumentType` node's publicId. */ - inputValue?: RareStringData; + publicId?: string; /** - * Only set for radio and checkbox input elements, indicates if the element has been checked + * `DocumentType` node's systemId. */ - inputChecked?: RareBooleanData; + systemId?: string; /** - * Only set for option elements, indicates if the element has been selected + * Frame ID for frame owner elements and also for the document node. */ - optionSelected?: RareBooleanData; + frameId?: Page.FrameId; /** - * The index of the document in the list of the snapshot documents. + * The index of a frame owner element's content document in the `domNodes` array returned by +`getSnapshot`, if any. */ - contentDocumentIndex?: RareIntegerData; + contentDocumentIndex?: number; /** * Type of a pseudo element node. */ - pseudoType?: RareStringData; + pseudoType?: DOM.PseudoType; /** - * Pseudo element identifier for this node. Only present if there is a -valid pseudoType. + * Shadow root type. */ - pseudoIdentifier?: RareStringData; + shadowRootType?: DOM.ShadowRootType; /** * Whether this DOM node responds to mouse clicks. This includes nodes that have had click event listeners attached via JavaScript as well as anchor tags that naturally navigate when clicked. */ - isClickable?: RareBooleanData; + isClickable?: boolean; + /** + * Details of the node's event listeners, if any. + */ + eventListeners?: DOMDebugger.EventListener[]; /** * The selected url for nodes with a srcset attribute. */ - currentSourceURL?: RareStringData; + currentSourceURL?: string; /** * The url of the script (if any) that generates this node. */ - originURL?: RareStringData; + originURL?: string; + /** + * Scroll offsets, set when this node is a Document. + */ + scrollOffsetX?: number; + scrollOffsetY?: number; } /** - * Table of details of an element in the DOM tree with a LayoutObject. + * Details of post layout rendered text positions. The exact layout should not be regarded as +stable and may change between versions. */ - export interface LayoutTreeSnapshot { + export interface InlineTextBox { /** - * Index of the corresponding node in the `NodeTreeSnapshot` array returned by `captureSnapshot`. + * The bounding box in document coordinates. Note that scroll offset of the document is ignored. */ - nodeIndex: number[]; + boundingBox: DOM.Rect; /** - * Array of indexes specifying computed style strings, filtered according to the `computedStyles` parameter passed to `captureSnapshot`. + * The starting index in characters, for this post layout textbox substring. Characters that +would be represented as a surrogate pair in UTF-16 have length 2. */ - styles: ArrayOfStrings[]; + startCharacterIndex: number; /** - * The absolute position bounding box. + * The number of characters in this post layout textbox substring. Characters that would be +represented as a surrogate pair in UTF-16 have length 2. */ - bounds: Rectangle[]; + numCharacters: number; + } + /** + * Details of an element in the DOM tree with a LayoutObject. + */ + export interface LayoutTreeNode { /** - * Contents of the LayoutText, if any. + * The index of the related DOM node in the `domNodes` array returned by `getSnapshot`. */ - text: StringIndex[]; + domNodeIndex: number; /** - * Stacking context information. + * The bounding box in document coordinates. Note that scroll offset of the document is ignored. */ - stackingContexts: RareBooleanData; + boundingBox: DOM.Rect; /** - * Global paint order index, which is determined by the stacking order of the nodes. Nodes -that are painted together will have the same index. Only provided if includePaintOrder in -captureSnapshot was true. + * Contents of the LayoutText, if any. */ - paintOrders?: number[]; + layoutText?: string; /** - * The offset rect of nodes. Only available when includeDOMRects is set to true + * The post-layout inline text nodes, if any. */ - offsetRects?: Rectangle[]; + inlineTextNodes?: InlineTextBox[]; + /** + * Index into the `computedStyles` array returned by `getSnapshot`. + */ + styleIndex?: number; + /** + * Global paint order index, which is determined by the stacking order of the nodes. Nodes +that are painted together will have the same index. Only provided if includePaintOrder in +getSnapshot was true. + */ + paintOrder?: number; + /** + * Set to true to indicate the element begins a new stacking context. + */ + isStackingContext?: boolean; + } + /** + * A subset of the full ComputedStyle as defined by the request whitelist. + */ + export interface ComputedStyle { + /** + * Name/value pairs of computed style properties. + */ + properties: NameValue[]; + } + /** + * A name/value pair. + */ + export interface NameValue { + /** + * Attribute/property name. + */ + name: string; + /** + * Attribute/property value. + */ + value: string; + } + /** + * Index of the string in the strings table. + */ + export type StringIndex = number; + /** + * Index of the string in the strings table. + */ + export type ArrayOfStrings = StringIndex[]; + /** + * Data that is only present on rare nodes. + */ + export interface RareStringData { + index: number[]; + value: StringIndex[]; + } + export interface RareBooleanData { + index: number[]; + } + export interface RareIntegerData { + index: number[]; + value: number[]; + } + export type Rectangle = number[]; + /** + * Document snapshot. + */ + export interface DocumentSnapshot { + /** + * Document URL that `Document` or `FrameOwner` node points to. + */ + documentURL: StringIndex; + /** + * Document title. + */ + title: StringIndex; + /** + * Base URL that `Document` or `FrameOwner` node uses for URL completion. + */ + baseURL: StringIndex; + /** + * Contains the document's content language. + */ + contentLanguage: StringIndex; + /** + * Contains the document's character set encoding. + */ + encodingName: StringIndex; + /** + * `DocumentType` node's publicId. + */ + publicId: StringIndex; + /** + * `DocumentType` node's systemId. + */ + systemId: StringIndex; + /** + * Frame ID for frame owner elements and also for the document node. + */ + frameId: StringIndex; + /** + * A table with dom nodes. + */ + nodes: NodeTreeSnapshot; + /** + * The nodes in the layout tree. + */ + layout: LayoutTreeSnapshot; + /** + * The post-layout inline text nodes. + */ + textBoxes: TextBoxSnapshot; + /** + * Horizontal scroll offset. + */ + scrollOffsetX?: number; + /** + * Vertical scroll offset. + */ + scrollOffsetY?: number; + /** + * Document content width. + */ + contentWidth?: number; + /** + * Document content height. + */ + contentHeight?: number; + } + /** + * Table containing nodes. + */ + export interface NodeTreeSnapshot { + /** + * Parent node index. + */ + parentIndex?: number[]; + /** + * `Node`'s nodeType. + */ + nodeType?: number[]; + /** + * Type of the shadow root the `Node` is in. String values are equal to the `ShadowRootType` enum. + */ + shadowRootType?: RareStringData; + /** + * `Node`'s nodeName. + */ + nodeName?: StringIndex[]; + /** + * `Node`'s nodeValue. + */ + nodeValue?: StringIndex[]; + /** + * `Node`'s id, corresponds to DOM.Node.backendNodeId. + */ + backendNodeId?: DOM.BackendNodeId[]; + /** + * Attributes of an `Element` node. Flatten name, value pairs. + */ + attributes?: ArrayOfStrings[]; + /** + * Only set for textarea elements, contains the text value. + */ + textValue?: RareStringData; + /** + * Only set for input elements, contains the input's associated text value. + */ + inputValue?: RareStringData; + /** + * Only set for radio and checkbox input elements, indicates if the element has been checked + */ + inputChecked?: RareBooleanData; + /** + * Only set for option elements, indicates if the element has been selected + */ + optionSelected?: RareBooleanData; + /** + * The index of the document in the list of the snapshot documents. + */ + contentDocumentIndex?: RareIntegerData; + /** + * Type of a pseudo element node. + */ + pseudoType?: RareStringData; + /** + * Pseudo element identifier for this node. Only present if there is a +valid pseudoType. + */ + pseudoIdentifier?: RareStringData; + /** + * Whether this DOM node responds to mouse clicks. This includes nodes that have had click +event listeners attached via JavaScript as well as anchor tags that naturally navigate when +clicked. + */ + isClickable?: RareBooleanData; + /** + * The selected url for nodes with a srcset attribute. + */ + currentSourceURL?: RareStringData; + /** + * The url of the script (if any) that generates this node. + */ + originURL?: RareStringData; + } + /** + * Table of details of an element in the DOM tree with a LayoutObject. + */ + export interface LayoutTreeSnapshot { + /** + * Index of the corresponding node in the `NodeTreeSnapshot` array returned by `captureSnapshot`. + */ + nodeIndex: number[]; + /** + * Array of indexes specifying computed style strings, filtered according to the `computedStyles` parameter passed to `captureSnapshot`. + */ + styles: ArrayOfStrings[]; + /** + * The absolute position bounding box. + */ + bounds: Rectangle[]; + /** + * Contents of the LayoutText, if any. + */ + text: StringIndex[]; + /** + * Stacking context information. + */ + stackingContexts: RareBooleanData; + /** + * Global paint order index, which is determined by the stacking order of the nodes. Nodes +that are painted together will have the same index. Only provided if includePaintOrder in +captureSnapshot was true. + */ + paintOrders?: number[]; + /** + * The offset rect of nodes. Only available when includeDOMRects is set to true + */ + offsetRects?: Rectangle[]; /** * The scroll rect of nodes. Only available when includeDOMRects is set to true */ @@ -6203,22 +6353,84 @@ The final text color opacity is computed based on the opacity of all overlapping } } - export module DeviceOrientation { - - + export module DeviceAccess { /** - * Clears the overridden Device Orientation. + * Device request id. */ - export type clearDeviceOrientationOverrideParameters = { - } - export type clearDeviceOrientationOverrideReturnValue = { - } + export type RequestId = string; /** - * Overrides the Device Orientation. + * A device id. */ - export type setDeviceOrientationOverrideParameters = { + export type DeviceId = string; + /** + * Device information displayed in a user prompt to select a device. + */ + export interface PromptDevice { + id: DeviceId; /** - * Mock alpha + * Display name as it appears in a device request user prompt. + */ + name: string; + } + + /** + * A device request opened a user prompt to select a device. Respond with the +selectPrompt or cancelPrompt command. + */ + export type deviceRequestPromptedPayload = { + id: RequestId; + devices: PromptDevice[]; + } + + /** + * Enable events in this domain. + */ + export type enableParameters = { + } + export type enableReturnValue = { + } + /** + * Disable events in this domain. + */ + export type disableParameters = { + } + export type disableReturnValue = { + } + /** + * Select a device in response to a DeviceAccess.deviceRequestPrompted event. + */ + export type selectPromptParameters = { + id: RequestId; + deviceId: DeviceId; + } + export type selectPromptReturnValue = { + } + /** + * Cancel a prompt in response to a DeviceAccess.deviceRequestPrompted event. + */ + export type cancelPromptParameters = { + id: RequestId; + } + export type cancelPromptReturnValue = { + } + } + + export module DeviceOrientation { + + + /** + * Clears the overridden Device Orientation. + */ + export type clearDeviceOrientationOverrideParameters = { + } + export type clearDeviceOrientationOverrideReturnValue = { + } + /** + * Overrides the Device Orientation. + */ + export type setDeviceOrientationOverrideParameters = { + /** + * Mock alpha */ alpha: number; /** @@ -6387,6 +6599,95 @@ See https://w3c.github.io/sensors/#automation for more information. export interface PressureMetadata { available?: boolean; } + export interface WorkAreaInsets { + /** + * Work area top inset in pixels. Default is 0; + */ + top?: number; + /** + * Work area left inset in pixels. Default is 0; + */ + left?: number; + /** + * Work area bottom inset in pixels. Default is 0; + */ + bottom?: number; + /** + * Work area right inset in pixels. Default is 0; + */ + right?: number; + } + export type ScreenId = string; + /** + * Screen information similar to the one returned by window.getScreenDetails() method, +see https://w3c.github.io/window-management/#screendetailed. + */ + export interface ScreenInfo { + /** + * Offset of the left edge of the screen. + */ + left: number; + /** + * Offset of the top edge of the screen. + */ + top: number; + /** + * Width of the screen. + */ + width: number; + /** + * Height of the screen. + */ + height: number; + /** + * Offset of the left edge of the available screen area. + */ + availLeft: number; + /** + * Offset of the top edge of the available screen area. + */ + availTop: number; + /** + * Width of the available screen area. + */ + availWidth: number; + /** + * Height of the available screen area. + */ + availHeight: number; + /** + * Specifies the screen's device pixel ratio. + */ + devicePixelRatio: number; + /** + * Specifies the screen's orientation. + */ + orientation: ScreenOrientation; + /** + * Specifies the screen's color depth in bits. + */ + colorDepth: number; + /** + * Indicates whether the device has multiple screens. + */ + isExtended: boolean; + /** + * Indicates whether the screen is internal to the device or external, attached to the device. + */ + isInternal: boolean; + /** + * Indicates whether the screen is set as the the operating system primary screen. + */ + isPrimary: boolean; + /** + * Specifies the descriptive label for the screen. + */ + label: string; + /** + * Specifies the unique identifier of the screen. + */ + id: ScreenId; + } /** * Enum of image types that can be disabled. */ @@ -6974,3243 +7275,3768 @@ of size 100lvh. } export type setSmallViewportHeightDifferenceOverrideReturnValue = { } - } - - /** - * This domain provides experimental commands only supported in headless mode. - */ - export module HeadlessExperimental { /** - * Encoding options for a screenshot. + * Returns device's screen configuration. */ - export interface ScreenshotParams { + export type getScreenInfosParameters = { + } + export type getScreenInfosReturnValue = { + screenInfos: ScreenInfo[]; + } + /** + * Add a new screen to the device. Only supported in headless mode. + */ + export type addScreenParameters = { /** - * Image compression format (defaults to png). + * Offset of the left edge of the screen in pixels. */ - format?: "jpeg"|"png"|"webp"; + left: number; /** - * Compression quality from range [0..100] (jpeg and webp only). + * Offset of the top edge of the screen in pixels. */ - quality?: number; + top: number; /** - * Optimize image encoding for speed, not for resulting size (defaults to false) + * The width of the screen in pixels. */ - optimizeForSpeed?: boolean; - } - - - /** - * Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a -screenshot from the resulting frame. Requires that the target was created with enabled -BeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also -https://goo.gle/chrome-headless-rendering for more background. - */ - export type beginFrameParameters = { + width: number; /** - * Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set, -the current time will be used. + * The height of the screen in pixels. */ - frameTimeTicks?: number; + height: number; /** - * The interval between BeginFrames that is reported to the compositor, in milliseconds. -Defaults to a 60 frames/second interval, i.e. about 16.666 milliseconds. + * Specifies the screen's work area. Default is entire screen. */ - interval?: number; + workAreaInsets?: WorkAreaInsets; /** - * Whether updates should not be committed and drawn onto the display. False by default. If -true, only side effects of the BeginFrame will be run, such as layout and animations, but -any visual updates may not be visible on the display or in screenshots. + * Specifies the screen's device pixel ratio. Default is 1. */ - noDisplayUpdates?: boolean; + devicePixelRatio?: number; /** - * If set, a screenshot of the frame will be captured and returned in the response. Otherwise, -no screenshot will be captured. Note that capturing a screenshot can fail, for example, -during renderer initialization. In such a case, no screenshot data will be returned. + * Specifies the screen's rotation angle. Available values are 0, 90, 180 and 270. Default is 0. */ - screenshot?: ScreenshotParams; - } - export type beginFrameReturnValue = { + rotation?: number; /** - * Whether the BeginFrame resulted in damage and, thus, a new frame was committed to the -display. Reported for diagnostic uses, may be removed in the future. + * Specifies the screen's color depth in bits. Default is 24. */ - hasDamage: boolean; + colorDepth?: number; /** - * Base64-encoded image data of the screenshot, if one was requested and successfully taken. + * Specifies the descriptive label for the screen. Default is none. */ - screenshotData?: binary; - } - /** - * Disables headless events for the target. - */ - export type disableParameters = { + label?: string; + /** + * Indicates whether the screen is internal to the device or external, attached to the device. Default is false. + */ + isInternal?: boolean; } - export type disableReturnValue = { + export type addScreenReturnValue = { + screenInfo: ScreenInfo; } /** - * Enables headless events for the target. + * Remove screen from the device. Only supported in headless mode. */ - export type enableParameters = { + export type removeScreenParameters = { + screenId: ScreenId; } - export type enableReturnValue = { + export type removeScreenReturnValue = { } } /** - * Input/Output operations for streams produced by DevTools. + * EventBreakpoints permits setting JavaScript breakpoints on operations and events +occurring in native code invoked from JavaScript. Once breakpoint is hit, it is +reported through Debugger domain, similarly to regular breakpoints being hit. */ - export module IO { - /** - * This is either obtained from another method or specified as `blob:` where -`` is an UUID of a Blob. - */ - export type StreamHandle = string; + export module EventBreakpoints { /** - * Close the stream, discard any temporary backing storage. + * Sets breakpoint on particular native event. */ - export type closeParameters = { + export type setInstrumentationBreakpointParameters = { /** - * Handle of the stream to close. + * Instrumentation name to stop on. */ - handle: StreamHandle; + eventName: string; } - export type closeReturnValue = { + export type setInstrumentationBreakpointReturnValue = { } /** - * Read a chunk of the stream + * Removes breakpoint on particular native event. */ - export type readParameters = { - /** - * Handle of the stream to read. - */ - handle: StreamHandle; - /** - * Seek to the specified offset before reading (if not specified, proceed with offset -following the last read). Some types of streams may only support sequential reads. - */ - offset?: number; + export type removeInstrumentationBreakpointParameters = { /** - * Maximum number of bytes to read (left upon the agent discretion if not specified). + * Instrumentation name to stop on. */ - size?: number; + eventName: string; } - export type readReturnValue = { - /** - * Set if the data is base64-encoded - */ - base64Encoded?: boolean; - /** - * Data that were read. - */ - data: string; - /** - * Set if the end-of-file condition occurred while reading. - */ - eof: boolean; + export type removeInstrumentationBreakpointReturnValue = { } /** - * Return UUID of Blob object specified by a remote object id. + * Removes all breakpoints */ - export type resolveBlobParameters = { - /** - * Object id of a Blob object wrapper. - */ - objectId: Runtime.RemoteObjectId; + export type disableParameters = { } - export type resolveBlobReturnValue = { - /** - * UUID of the specified Blob. - */ - uuid: string; + export type disableReturnValue = { } } - export module FileSystem { - export interface File { - name: string; - /** - * Timestamp - */ - lastModified: Network.TimeSinceEpoch; - /** - * Size in bytes - */ - size: number; - type: string; - } - export interface Directory { - name: string; - nestedDirectories: string[]; - /** - * Files that are directly nested under this directory. - */ - nestedFiles: File[]; - } - export interface BucketFileSystemLocator { - /** - * Storage key - */ - storageKey: Storage.SerializedStorageKey; - /** - * Bucket name. Not passing a `bucketName` will retrieve the default Bucket. (https://developer.mozilla.org/en-US/docs/Web/API/Storage_API#storage_buckets) - */ - bucketName?: string; - /** - * Path to the directory using each path component as an array item. - */ - pathComponents: string[]; - } + /** + * Defines commands and events for browser extensions. + */ + export module Extensions { + /** + * Storage areas. + */ + export type StorageArea = "session"|"local"|"sync"|"managed"; - export type getDirectoryParameters = { - bucketFileSystemLocator: BucketFileSystemLocator; - } - export type getDirectoryReturnValue = { - /** - * Returns the directory object at the path. - */ - directory: Directory; - } - } - - export module IndexedDB { /** - * Database with an array of object stores. + * Installs an unpacked extension from the filesystem similar to +--load-extension CLI flags. Returns extension ID once the extension +has been installed. Available if the client is connected using the +--remote-debugging-pipe flag and the --enable-unsafe-extension-debugging +flag is set. */ - export interface DatabaseWithObjectStores { - /** - * Database name. - */ - name: string; + export type loadUnpackedParameters = { /** - * Database version (type is not 'integer', as the standard -requires the version number to be 'unsigned long long') + * Absolute file path. */ - version: number; + path: string; + } + export type loadUnpackedReturnValue = { /** - * Object stores in this database. + * Extension id. */ - objectStores: ObjectStore[]; + id: string; } /** - * Object store. + * Uninstalls an unpacked extension (others not supported) from the profile. +Available if the client is connected using the --remote-debugging-pipe flag +and the --enable-unsafe-extension-debugging. */ - export interface ObjectStore { - /** - * Object store name. - */ - name: string; - /** - * Object store key path. - */ - keyPath: KeyPath; - /** - * If true, object store has auto increment flag set. - */ - autoIncrement: boolean; + export type uninstallParameters = { /** - * Indexes in this object store. + * Extension id. */ - indexes: ObjectStoreIndex[]; + id: string; + } + export type uninstallReturnValue = { } /** - * Object store index. + * Gets data from extension storage in the given `storageArea`. If `keys` is +specified, these are used to filter the result. */ - export interface ObjectStoreIndex { - /** - * Index name. - */ - name: string; + export type getStorageItemsParameters = { /** - * Index key path. + * ID of extension. */ - keyPath: KeyPath; + id: string; /** - * If true, index is unique. + * StorageArea to retrieve data from. */ - unique: boolean; + storageArea: StorageArea; /** - * If true, index allows multiple entries for a key. + * Keys to retrieve. */ - multiEntry: boolean; + keys?: string[]; + } + export type getStorageItemsReturnValue = { + data: { [key: string]: string }; } /** - * Key. + * Removes `keys` from extension storage in the given `storageArea`. */ - export interface Key { - /** - * Key type. - */ - type: "number"|"string"|"date"|"array"; - /** - * Number value. - */ - number?: number; + export type removeStorageItemsParameters = { /** - * String value. + * ID of extension. */ - string?: string; + id: string; /** - * Date value. + * StorageArea to remove data from. */ - date?: number; + storageArea: StorageArea; /** - * Array value. + * Keys to remove. */ - array?: Key[]; + keys: string[]; + } + export type removeStorageItemsReturnValue = { } /** - * Key range. + * Clears extension storage in the given `storageArea`. */ - export interface KeyRange { - /** - * Lower bound. - */ - lower?: Key; - /** - * Upper bound. - */ - upper?: Key; + export type clearStorageItemsParameters = { /** - * If true lower bound is open. + * ID of extension. */ - lowerOpen: boolean; + id: string; /** - * If true upper bound is open. + * StorageArea to remove data from. */ - upperOpen: boolean; + storageArea: StorageArea; + } + export type clearStorageItemsReturnValue = { } /** - * Data entry. + * Sets `values` in extension storage in the given `storageArea`. The provided `values` +will be merged with existing values in the storage area. */ - export interface DataEntry { + export type setStorageItemsParameters = { /** - * Key object. + * ID of extension. */ - key: Runtime.RemoteObject; + id: string; /** - * Primary key object. + * StorageArea to set data in. */ - primaryKey: Runtime.RemoteObject; + storageArea: StorageArea; /** - * Value object. + * Values to set. */ - value: Runtime.RemoteObject; + values: { [key: string]: string }; + } + export type setStorageItemsReturnValue = { } + } + + /** + * This domain allows interacting with the FedCM dialog. + */ + export module FedCm { /** - * Key path. + * Whether this is a sign-up or sign-in action for this account, i.e. +whether this account has ever been used to sign in to this RP before. */ - export interface KeyPath { - /** - * Key path type. - */ - type: "null"|"string"|"array"; + export type LoginState = "SignIn"|"SignUp"; + /** + * The types of FedCM dialogs. + */ + export type DialogType = "AccountChooser"|"AutoReauthn"|"ConfirmIdpLogin"|"Error"; + /** + * The buttons on the FedCM dialog. + */ + export type DialogButton = "ConfirmIdpLoginContinue"|"ErrorGotIt"|"ErrorMoreDetails"; + /** + * The URLs that each account has + */ + export type AccountUrlType = "TermsOfService"|"PrivacyPolicy"; + /** + * Corresponds to IdentityRequestAccount + */ + export interface Account { + accountId: string; + email: string; + name: string; + givenName: string; + pictureUrl: string; + idpConfigUrl: string; + idpLoginUrl: string; + loginState: LoginState; /** - * String value. + * These two are only set if the loginState is signUp */ - string?: string; + termsOfServiceUrl?: string; + privacyPolicyUrl?: string; + } + + export type dialogShownPayload = { + dialogId: string; + dialogType: DialogType; + accounts: Account[]; /** - * Array value. + * These exist primarily so that the caller can verify the +RP context was used appropriately. */ - array?: string[]; + title: string; + subtitle?: string; } - - /** - * Clears all entries from an object store. + * Triggered when a dialog is closed, either by user action, JS abort, +or a command below. */ - export type clearObjectStoreParameters = { - /** - * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. -Security origin. - */ - securityOrigin?: string; + export type dialogClosedPayload = { + dialogId: string; + } + + export type enableParameters = { /** - * Storage key. + * Allows callers to disable the promise rejection delay that would +normally happen, if this is unimportant to what's being tested. +(step 4 of https://fedidcg.github.io/FedCM/#browser-api-rp-sign-in) */ - storageKey?: string; + disableRejectionDelay?: boolean; + } + export type enableReturnValue = { + } + export type disableParameters = { + } + export type disableReturnValue = { + } + export type selectAccountParameters = { + dialogId: string; + accountIndex: number; + } + export type selectAccountReturnValue = { + } + export type clickDialogButtonParameters = { + dialogId: string; + dialogButton: DialogButton; + } + export type clickDialogButtonReturnValue = { + } + export type openUrlParameters = { + dialogId: string; + accountIndex: number; + accountUrlType: AccountUrlType; + } + export type openUrlReturnValue = { + } + export type dismissDialogParameters = { + dialogId: string; + triggerCooldown?: boolean; + } + export type dismissDialogReturnValue = { + } + /** + * Resets the cooldown time, if any, to allow the next FedCM call to show +a dialog even if one was recently dismissed by the user. + */ + export type resetCooldownParameters = { + } + export type resetCooldownReturnValue = { + } + } + + /** + * A domain for letting clients substitute browser's network layer with client code. + */ + export module Fetch { + /** + * Unique request identifier. +Note that this does not identify individual HTTP requests that are part of +a network request. + */ + export type RequestId = string; + /** + * Stages of the request to handle. Request will intercept before the request is +sent. Response will intercept after the response is received (but before response +body is received). + */ + export type RequestStage = "Request"|"Response"; + export interface RequestPattern { /** - * Storage bucket. If not specified, it uses the default bucket. + * Wildcards (`'*'` -> zero or more, `'?'` -> exactly one) are allowed. Escape character is +backslash. Omitting is equivalent to `"*"`. */ - storageBucket?: Storage.StorageBucket; + urlPattern?: string; /** - * Database name. + * If set, only requests for matching resource types will be intercepted. */ - databaseName: string; + resourceType?: Network.ResourceType; /** - * Object store name. + * Stage at which to begin intercepting requests. Default is Request. */ - objectStoreName: string; + requestStage?: RequestStage; } - export type clearObjectStoreReturnValue = { + /** + * Response HTTP header entry + */ + export interface HeaderEntry { + name: string; + value: string; } /** - * Deletes a database. + * Authorization challenge for HTTP status code 401 or 407. */ - export type deleteDatabaseParameters = { + export interface AuthChallenge { /** - * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. -Security origin. + * Source of the authentication challenge. */ - securityOrigin?: string; + source?: "Server"|"Proxy"; /** - * Storage key. + * Origin of the challenger. */ - storageKey?: string; + origin: string; /** - * Storage bucket. If not specified, it uses the default bucket. + * The authentication scheme used, such as basic or digest */ - storageBucket?: Storage.StorageBucket; + scheme: string; /** - * Database name. + * The realm of the challenge. May be empty. */ - databaseName: string; - } - export type deleteDatabaseReturnValue = { + realm: string; } /** - * Delete a range of entries from an object store + * Response to an AuthChallenge. */ - export type deleteObjectStoreEntriesParameters = { - /** - * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. -Security origin. - */ - securityOrigin?: string; + export interface AuthChallengeResponse { /** - * Storage key. + * The decision on what to do in response to the authorization challenge. Default means +deferring to the default behavior of the net stack, which will likely either the Cancel +authentication or display a popup dialog box. */ - storageKey?: string; + response: "Default"|"CancelAuth"|"ProvideCredentials"; /** - * Storage bucket. If not specified, it uses the default bucket. + * The username to provide, possibly empty. Should only be set if response is +ProvideCredentials. */ - storageBucket?: Storage.StorageBucket; - databaseName: string; - objectStoreName: string; + username?: string; /** - * Range of entry keys to delete + * The password to provide, possibly empty. Should only be set if response is +ProvideCredentials. */ - keyRange: KeyRange; - } - export type deleteObjectStoreEntriesReturnValue = { - } - /** - * Disables events from backend. - */ - export type disableParameters = { - } - export type disableReturnValue = { - } - /** - * Enables events from backend. - */ - export type enableParameters = { - } - export type enableReturnValue = { + password?: string; } + /** - * Requests data from object store or index. + * Issued when the domain is enabled and the request URL matches the +specified filter. The request is paused until the client responds +with one of continueRequest, failRequest or fulfillRequest. +The stage of the request can be determined by presence of responseErrorReason +and responseStatusCode -- the request is at the response stage if either +of these fields is present and in the request stage otherwise. +Redirect responses and subsequent requests are reported similarly to regular +responses and requests. Redirect responses may be distinguished by the value +of `responseStatusCode` (which is one of 301, 302, 303, 307, 308) along with +presence of the `location` header. Requests resulting from a redirect will +have `redirectedRequestId` field set. */ - export type requestDataParameters = { - /** - * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. -Security origin. - */ - securityOrigin?: string; + export type requestPausedPayload = { /** - * Storage key. + * Each request the page makes will have a unique id. */ - storageKey?: string; + requestId: RequestId; /** - * Storage bucket. If not specified, it uses the default bucket. + * The details of the request. */ - storageBucket?: Storage.StorageBucket; + request: Network.Request; /** - * Database name. + * The id of the frame that initiated the request. */ - databaseName: string; + frameId: Page.FrameId; /** - * Object store name. + * How the requested resource will be used. */ - objectStoreName: string; + resourceType: Network.ResourceType; /** - * Index name, empty string for object store data requests. + * Response error if intercepted at response stage. */ - indexName: string; + responseErrorReason?: Network.ErrorReason; /** - * Number of records to skip. + * Response code if intercepted at response stage. */ - skipCount: number; + responseStatusCode?: number; /** - * Number of records to fetch. + * Response status text if intercepted at response stage. */ - pageSize: number; + responseStatusText?: string; /** - * Key range. + * Response headers if intercepted at the response stage. */ - keyRange?: KeyRange; - } - export type requestDataReturnValue = { + responseHeaders?: HeaderEntry[]; /** - * Array of object store data entries. + * If the intercepted request had a corresponding Network.requestWillBeSent event fired for it, +then this networkId will be the same as the requestId present in the requestWillBeSent event. */ - objectStoreDataEntries: DataEntry[]; + networkId?: Network.RequestId; /** - * If true, there are more entries to fetch in the given range. + * If the request is due to a redirect response from the server, the id of the request that +has caused the redirect. */ - hasMore: boolean; + redirectedRequestId?: RequestId; } /** - * Gets metadata of an object store. + * Issued when the domain is enabled with handleAuthRequests set to true. +The request is paused until client responds with continueWithAuth. */ - export type getMetadataParameters = { - /** - * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. -Security origin. - */ - securityOrigin?: string; - /** - * Storage key. - */ - storageKey?: string; + export type authRequiredPayload = { /** - * Storage bucket. If not specified, it uses the default bucket. + * Each request the page makes will have a unique id. */ - storageBucket?: Storage.StorageBucket; + requestId: RequestId; /** - * Database name. + * The details of the request. */ - databaseName: string; + request: Network.Request; /** - * Object store name. + * The id of the frame that initiated the request. */ - objectStoreName: string; - } - export type getMetadataReturnValue = { + frameId: Page.FrameId; /** - * the entries count + * How the requested resource will be used. */ - entriesCount: number; + resourceType: Network.ResourceType; /** - * the current value of key generator, to become the next inserted -key into the object store. Valid if objectStore.autoIncrement -is true. + * Details of the Authorization Challenge encountered. +If this is set, client should respond with continueRequest that +contains AuthChallengeResponse. */ - keyGeneratorValue: number; + authChallenge: AuthChallenge; } + /** - * Requests database with given name in given frame. + * Disables the fetch domain. */ - export type requestDatabaseParameters = { - /** - * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. -Security origin. - */ - securityOrigin?: string; - /** - * Storage key. - */ - storageKey?: string; - /** - * Storage bucket. If not specified, it uses the default bucket. - */ - storageBucket?: Storage.StorageBucket; - /** - * Database name. - */ - databaseName: string; + export type disableParameters = { } - export type requestDatabaseReturnValue = { - /** - * Database with an array of object stores. - */ - databaseWithObjectStores: DatabaseWithObjectStores; + export type disableReturnValue = { } /** - * Requests database names for given security origin. + * Enables issuing of requestPaused events. A request will be paused until client +calls one of failRequest, fulfillRequest or continueRequest/continueWithAuth. */ - export type requestDatabaseNamesParameters = { - /** - * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. -Security origin. - */ - securityOrigin?: string; + export type enableParameters = { /** - * Storage key. + * If specified, only requests matching any of these patterns will produce +fetchRequested event and will be paused until clients response. If not set, +all requests will be affected. */ - storageKey?: string; + patterns?: RequestPattern[]; /** - * Storage bucket. If not specified, it uses the default bucket. + * If true, authRequired events will be issued and requests will be paused +expecting a call to continueWithAuth. */ - storageBucket?: Storage.StorageBucket; + handleAuthRequests?: boolean; } - export type requestDatabaseNamesReturnValue = { - /** - * Database names for origin. - */ - databaseNames: string[]; + export type enableReturnValue = { } - } - - export module Input { - export interface TouchPoint { - /** - * X coordinate of the event relative to the main frame's viewport in CSS pixels. - */ - x: number; - /** - * Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to -the top of the viewport and Y increases as it proceeds towards the bottom of the viewport. - */ - y: number; - /** - * X radius of the touch area (default: 1.0). - */ - radiusX?: number; + /** + * Causes the request to fail with specified reason. + */ + export type failRequestParameters = { /** - * Y radius of the touch area (default: 1.0). + * An id the client received in requestPaused event. */ - radiusY?: number; + requestId: RequestId; /** - * Rotation angle (default: 0.0). + * Causes the request to fail with the given reason. */ - rotationAngle?: number; + errorReason: Network.ErrorReason; + } + export type failRequestReturnValue = { + } + /** + * Provides response to the request. + */ + export type fulfillRequestParameters = { /** - * Force (default: 1.0). + * An id the client received in requestPaused event. */ - force?: number; + requestId: RequestId; /** - * The normalized tangential pressure, which has a range of [-1,1] (default: 0). + * An HTTP response code. */ - tangentialPressure?: number; + responseCode: number; /** - * The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0) + * Response headers. */ - tiltX?: number; + responseHeaders?: HeaderEntry[]; /** - * The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0). + * Alternative way of specifying response headers as a \0-separated +series of name: value pairs. Prefer the above method unless you +need to represent some non-UTF8 values that can't be transmitted +over the protocol as text. */ - tiltY?: number; + binaryResponseHeaders?: binary; /** - * The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0). + * A response body. If absent, original response body will be used if +the request is intercepted at the response stage and empty body +will be used if the request is intercepted at the request stage. */ - twist?: number; + body?: binary; /** - * Identifier used to track touch sources between events, must be unique within an event. + * A textual representation of responseCode. +If absent, a standard phrase matching responseCode is used. */ - id?: number; + responsePhrase?: string; + } + export type fulfillRequestReturnValue = { } - export type GestureSourceType = "default"|"touch"|"mouse"; - export type MouseButton = "none"|"left"|"middle"|"right"|"back"|"forward"; /** - * UTC time in seconds, counted from January 1, 1970. + * Continues the request, optionally modifying some of its parameters. */ - export type TimeSinceEpoch = number; - export interface DragDataItem { + export type continueRequestParameters = { /** - * Mime type of the dragged data. + * An id the client received in requestPaused event. */ - mimeType: string; + requestId: RequestId; /** - * Depending of the value of `mimeType`, it contains the dragged link, -text, HTML markup or any other data. + * If set, the request url will be modified in a way that's not observable by page. */ - data: string; + url?: string; /** - * Title associated with a link. Only valid when `mimeType` == "text/uri-list". + * If set, the request method is overridden. */ - title?: string; + method?: string; /** - * Stores the base URL for the contained markup. Only valid when `mimeType` -== "text/html". + * If set, overrides the post data in the request. */ - baseURL?: string; - } - export interface DragData { - items: DragDataItem[]; + postData?: binary; /** - * List of filenames that should be included when dropping + * If set, overrides the request headers. Note that the overrides do not +extend to subsequent redirect hops, if a redirect happens. Another override +may be applied to a different request produced by a redirect. */ - files?: string[]; + headers?: HeaderEntry[]; /** - * Bit field representing allowed drag operations. Copy = 1, Link = 2, Move = 16 + * If set, overrides response interception behavior for this request. */ - dragOperationsMask: number; + interceptResponse?: boolean; } - - /** - * Emitted only when `Input.setInterceptDrags` is enabled. Use this data with `Input.dispatchDragEvent` to -restore normal drag and drop behavior. - */ - export type dragInterceptedPayload = { - data: DragData; + export type continueRequestReturnValue = { } - /** - * Dispatches a drag event into the page. + * Continues a request supplying authChallengeResponse following authRequired event. */ - export type dispatchDragEventParameters = { - /** - * Type of the drag event. - */ - type: "dragEnter"|"dragOver"|"drop"|"dragCancel"; - /** - * X coordinate of the event relative to the main frame's viewport in CSS pixels. - */ - x: number; + export type continueWithAuthParameters = { /** - * Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to -the top of the viewport and Y increases as it proceeds towards the bottom of the viewport. + * An id the client received in authRequired event. */ - y: number; - data: DragData; + requestId: RequestId; /** - * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 -(default: 0). + * Response to with an authChallenge. */ - modifiers?: number; + authChallengeResponse: AuthChallengeResponse; } - export type dispatchDragEventReturnValue = { + export type continueWithAuthReturnValue = { } /** - * Dispatches a key event to the page. + * Continues loading of the paused response, optionally modifying the +response headers. If either responseCode or headers are modified, all of them +must be present. */ - export type dispatchKeyEventParameters = { - /** - * Type of the key event. - */ - type: "keyDown"|"keyUp"|"rawKeyDown"|"char"; + export type continueResponseParameters = { /** - * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 -(default: 0). + * An id the client received in requestPaused event. */ - modifiers?: number; + requestId: RequestId; /** - * Time at which the event occurred. + * An HTTP response code. If absent, original response code will be used. */ - timestamp?: TimeSinceEpoch; + responseCode?: number; /** - * Text as generated by processing a virtual key code with a keyboard layout. Not needed for -for `keyUp` and `rawKeyDown` events (default: "") + * A textual representation of responseCode. +If absent, a standard phrase matching responseCode is used. */ - text?: string; + responsePhrase?: string; /** - * Text that would have been generated by the keyboard if no modifiers were pressed (except for -shift). Useful for shortcut (accelerator) key handling (default: ""). + * Response headers. If absent, original response headers will be used. */ - unmodifiedText?: string; + responseHeaders?: HeaderEntry[]; /** - * Unique key identifier (e.g., 'U+0041') (default: ""). + * Alternative way of specifying response headers as a \0-separated +series of name: value pairs. Prefer the above method unless you +need to represent some non-UTF8 values that can't be transmitted +over the protocol as text. */ - keyIdentifier?: string; + binaryResponseHeaders?: binary; + } + export type continueResponseReturnValue = { + } + /** + * Causes the body of the response to be received from the server and +returned as a single string. May only be issued for a request that +is paused in the Response stage and is mutually exclusive with +takeResponseBodyForInterceptionAsStream. Calling other methods that +affect the request or disabling fetch domain before body is received +results in an undefined behavior. +Note that the response body is not available for redirects. Requests +paused in the _redirect received_ state may be differentiated by +`responseCode` and presence of `location` response header, see +comments to `requestPaused` for details. + */ + export type getResponseBodyParameters = { /** - * Unique DOM defined string value for each physical key (e.g., 'KeyA') (default: ""). + * Identifier for the intercepted request to get body for. */ - code?: string; + requestId: RequestId; + } + export type getResponseBodyReturnValue = { /** - * Unique DOM defined string value describing the meaning of the key in the context of active -modifiers, keyboard layout, etc (e.g., 'AltGr') (default: ""). + * Response body. */ - key?: string; + body: string; /** - * Windows virtual key code (default: 0). + * True, if content was sent as base64. */ - windowsVirtualKeyCode?: number; + base64Encoded: boolean; + } + /** + * Returns a handle to the stream representing the response body. +The request must be paused in the HeadersReceived stage. +Note that after this command the request can't be continued +as is -- client either needs to cancel it or to provide the +response body. +The stream only supports sequential read, IO.read will fail if the position +is specified. +This method is mutually exclusive with getResponseBody. +Calling other methods that affect the request or disabling fetch +domain before body is received results in an undefined behavior. + */ + export type takeResponseBodyAsStreamParameters = { + requestId: RequestId; + } + export type takeResponseBodyAsStreamReturnValue = { + stream: IO.StreamHandle; + } + } + + export module FileSystem { + export interface File { + name: string; /** - * Native virtual key code (default: 0). + * Timestamp */ - nativeVirtualKeyCode?: number; + lastModified: Network.TimeSinceEpoch; /** - * Whether the event was generated from auto repeat (default: false). + * Size in bytes */ - autoRepeat?: boolean; + size: number; + type: string; + } + export interface Directory { + name: string; + nestedDirectories: string[]; /** - * Whether the event was generated from the keypad (default: false). + * Files that are directly nested under this directory. */ - isKeypad?: boolean; + nestedFiles: File[]; + } + export interface BucketFileSystemLocator { /** - * Whether the event was a system key event (default: false). + * Storage key */ - isSystemKey?: boolean; + storageKey: Storage.SerializedStorageKey; /** - * Whether the event was from the left or right side of the keyboard. 1=Left, 2=Right (default: -0). + * Bucket name. Not passing a `bucketName` will retrieve the default Bucket. (https://developer.mozilla.org/en-US/docs/Web/API/Storage_API#storage_buckets) */ - location?: number; + bucketName?: string; /** - * Editing commands to send with the key event (e.g., 'selectAll') (default: []). -These are related to but not equal the command names used in `document.execCommand` and NSStandardKeyBindingResponding. -See https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h for valid command names. + * Path to the directory using each path component as an array item. */ - commands?: string[]; + pathComponents: string[]; } - export type dispatchKeyEventReturnValue = { + + + export type getDirectoryParameters = { + bucketFileSystemLocator: BucketFileSystemLocator; } - /** - * This method emulates inserting text that doesn't come from a key press, -for example an emoji keyboard or an IME. - */ - export type insertTextParameters = { + export type getDirectoryReturnValue = { /** - * The text to insert. + * Returns the directory object at the path. */ - text: string; - } - export type insertTextReturnValue = { + directory: Directory; } + } + + /** + * This domain provides experimental commands only supported in headless mode. + */ + export module HeadlessExperimental { /** - * This method sets the current candidate text for IME. -Use imeCommitComposition to commit the final text. -Use imeSetComposition with empty string as text to cancel composition. + * Encoding options for a screenshot. */ - export type imeSetCompositionParameters = { - /** - * The text to insert - */ - text: string; - /** - * selection start - */ - selectionStart: number; + export interface ScreenshotParams { /** - * selection end + * Image compression format (defaults to png). */ - selectionEnd: number; + format?: "jpeg"|"png"|"webp"; /** - * replacement start + * Compression quality from range [0..100] (jpeg and webp only). */ - replacementStart?: number; + quality?: number; /** - * replacement end + * Optimize image encoding for speed, not for resulting size (defaults to false) */ - replacementEnd?: number; - } - export type imeSetCompositionReturnValue = { + optimizeForSpeed?: boolean; } + + /** - * Dispatches a mouse event to the page. + * Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a +screenshot from the resulting frame. Requires that the target was created with enabled +BeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also +https://goo.gle/chrome-headless-rendering for more background. */ - export type dispatchMouseEventParameters = { - /** - * Type of the mouse event. - */ - type: "mousePressed"|"mouseReleased"|"mouseMoved"|"mouseWheel"; + export type beginFrameParameters = { /** - * X coordinate of the event relative to the main frame's viewport in CSS pixels. + * Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set, +the current time will be used. */ - x: number; + frameTimeTicks?: number; /** - * Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to -the top of the viewport and Y increases as it proceeds towards the bottom of the viewport. + * The interval between BeginFrames that is reported to the compositor, in milliseconds. +Defaults to a 60 frames/second interval, i.e. about 16.666 milliseconds. */ - y: number; + interval?: number; /** - * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 -(default: 0). + * Whether updates should not be committed and drawn onto the display. False by default. If +true, only side effects of the BeginFrame will be run, such as layout and animations, but +any visual updates may not be visible on the display or in screenshots. */ - modifiers?: number; + noDisplayUpdates?: boolean; /** - * Time at which the event occurred. + * If set, a screenshot of the frame will be captured and returned in the response. Otherwise, +no screenshot will be captured. Note that capturing a screenshot can fail, for example, +during renderer initialization. In such a case, no screenshot data will be returned. */ - timestamp?: TimeSinceEpoch; + screenshot?: ScreenshotParams; + } + export type beginFrameReturnValue = { /** - * Mouse button (default: "none"). + * Whether the BeginFrame resulted in damage and, thus, a new frame was committed to the +display. Reported for diagnostic uses, may be removed in the future. */ - button?: MouseButton; + hasDamage: boolean; /** - * A number indicating which buttons are pressed on the mouse when a mouse event is triggered. -Left=1, Right=2, Middle=4, Back=8, Forward=16, None=0. + * Base64-encoded image data of the screenshot, if one was requested and successfully taken. */ - buttons?: number; + screenshotData?: binary; + } + /** + * Disables headless events for the target. + */ + export type disableParameters = { + } + export type disableReturnValue = { + } + /** + * Enables headless events for the target. + */ + export type enableParameters = { + } + export type enableReturnValue = { + } + } + + /** + * Input/Output operations for streams produced by DevTools. + */ + export module IO { + /** + * This is either obtained from another method or specified as `blob:` where +`` is an UUID of a Blob. + */ + export type StreamHandle = string; + + + /** + * Close the stream, discard any temporary backing storage. + */ + export type closeParameters = { /** - * Number of times the mouse button was clicked (default: 0). + * Handle of the stream to close. */ - clickCount?: number; + handle: StreamHandle; + } + export type closeReturnValue = { + } + /** + * Read a chunk of the stream + */ + export type readParameters = { /** - * The normalized pressure, which has a range of [0,1] (default: 0). + * Handle of the stream to read. */ - force?: number; + handle: StreamHandle; /** - * The normalized tangential pressure, which has a range of [-1,1] (default: 0). + * Seek to the specified offset before reading (if not specified, proceed with offset +following the last read). Some types of streams may only support sequential reads. */ - tangentialPressure?: number; + offset?: number; /** - * The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0). + * Maximum number of bytes to read (left upon the agent discretion if not specified). */ - tiltX?: number; + size?: number; + } + export type readReturnValue = { /** - * The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0). + * Set if the data is base64-encoded */ - tiltY?: number; + base64Encoded?: boolean; /** - * The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0). + * Data that were read. */ - twist?: number; + data: string; /** - * X delta in CSS pixels for mouse wheel event (default: 0). + * Set if the end-of-file condition occurred while reading. */ - deltaX?: number; + eof: boolean; + } + /** + * Return UUID of Blob object specified by a remote object id. + */ + export type resolveBlobParameters = { /** - * Y delta in CSS pixels for mouse wheel event (default: 0). + * Object id of a Blob object wrapper. */ - deltaY?: number; + objectId: Runtime.RemoteObjectId; + } + export type resolveBlobReturnValue = { /** - * Pointer type (default: "mouse"). + * UUID of the specified Blob. */ - pointerType?: "mouse"|"pen"; - } - export type dispatchMouseEventReturnValue = { + uuid: string; } + } + + export module IndexedDB { /** - * Dispatches a touch event to the page. + * Database with an array of object stores. */ - export type dispatchTouchEventParameters = { + export interface DatabaseWithObjectStores { /** - * Type of the touch event. TouchEnd and TouchCancel must not contain any touch points, while -TouchStart and TouchMove must contains at least one. - */ - type: "touchStart"|"touchEnd"|"touchMove"|"touchCancel"; - /** - * Active touch points on the touch device. One event per any changed point (compared to -previous touch event in a sequence) is generated, emulating pressing/moving/releasing points -one by one. + * Database name. */ - touchPoints: TouchPoint[]; + name: string; /** - * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 -(default: 0). + * Database version (type is not 'integer', as the standard +requires the version number to be 'unsigned long long') */ - modifiers?: number; + version: number; /** - * Time at which the event occurred. + * Object stores in this database. */ - timestamp?: TimeSinceEpoch; - } - export type dispatchTouchEventReturnValue = { + objectStores: ObjectStore[]; } /** - * Cancels any active dragging in the page. + * Object store. */ - export type cancelDraggingParameters = { - } - export type cancelDraggingReturnValue = { + export interface ObjectStore { + /** + * Object store name. + */ + name: string; + /** + * Object store key path. + */ + keyPath: KeyPath; + /** + * If true, object store has auto increment flag set. + */ + autoIncrement: boolean; + /** + * Indexes in this object store. + */ + indexes: ObjectStoreIndex[]; } /** - * Emulates touch event from the mouse event parameters. + * Object store index. */ - export type emulateTouchFromMouseEventParameters = { + export interface ObjectStoreIndex { /** - * Type of the mouse event. + * Index name. */ - type: "mousePressed"|"mouseReleased"|"mouseMoved"|"mouseWheel"; + name: string; /** - * X coordinate of the mouse pointer in DIP. + * Index key path. */ - x: number; + keyPath: KeyPath; /** - * Y coordinate of the mouse pointer in DIP. + * If true, index is unique. */ - y: number; + unique: boolean; /** - * Mouse button. Only "none", "left", "right" are supported. + * If true, index allows multiple entries for a key. */ - button: MouseButton; + multiEntry: boolean; + } + /** + * Key. + */ + export interface Key { /** - * Time at which the event occurred (default: current time). + * Key type. */ - timestamp?: TimeSinceEpoch; + type: "number"|"string"|"date"|"array"; /** - * X delta in DIP for mouse wheel event (default: 0). + * Number value. */ - deltaX?: number; + number?: number; /** - * Y delta in DIP for mouse wheel event (default: 0). + * String value. */ - deltaY?: number; + string?: string; /** - * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 -(default: 0). + * Date value. */ - modifiers?: number; + date?: number; /** - * Number of times the mouse button was clicked (default: 0). + * Array value. */ - clickCount?: number; - } - export type emulateTouchFromMouseEventReturnValue = { + array?: Key[]; } /** - * Ignores input events (useful while auditing page). + * Key range. */ - export type setIgnoreInputEventsParameters = { + export interface KeyRange { /** - * Ignores input events processing when set to true. + * Lower bound. */ - ignore: boolean; - } - export type setIgnoreInputEventsReturnValue = { - } - /** - * Prevents default drag and drop behavior and instead emits `Input.dragIntercepted` events. -Drag and drop behavior can be directly controlled via `Input.dispatchDragEvent`. - */ - export type setInterceptDragsParameters = { - enabled: boolean; - } - export type setInterceptDragsReturnValue = { - } - /** - * Synthesizes a pinch gesture over a time period by issuing appropriate touch events. - */ - export type synthesizePinchGestureParameters = { + lower?: Key; /** - * X coordinate of the start of the gesture in CSS pixels. + * Upper bound. */ - x: number; + upper?: Key; /** - * Y coordinate of the start of the gesture in CSS pixels. + * If true lower bound is open. */ - y: number; + lowerOpen: boolean; /** - * Relative scale factor after zooming (>1.0 zooms in, <1.0 zooms out). + * If true upper bound is open. */ - scaleFactor: number; + upperOpen: boolean; + } + /** + * Data entry. + */ + export interface DataEntry { /** - * Relative pointer speed in pixels per second (default: 800). + * Key object. */ - relativeSpeed?: number; + key: Runtime.RemoteObject; /** - * Which type of input events to be generated (default: 'default', which queries the platform -for the preferred input type). + * Primary key object. */ - gestureSourceType?: GestureSourceType; - } - export type synthesizePinchGestureReturnValue = { + primaryKey: Runtime.RemoteObject; + /** + * Value object. + */ + value: Runtime.RemoteObject; } /** - * Synthesizes a scroll gesture over a time period by issuing appropriate touch events. + * Key path. */ - export type synthesizeScrollGestureParameters = { + export interface KeyPath { /** - * X coordinate of the start of the gesture in CSS pixels. + * Key path type. */ - x: number; + type: "null"|"string"|"array"; /** - * Y coordinate of the start of the gesture in CSS pixels. + * String value. */ - y: number; + string?: string; /** - * The distance to scroll along the X axis (positive to scroll left). + * Array value. */ - xDistance?: number; + array?: string[]; + } + + + /** + * Clears all entries from an object store. + */ + export type clearObjectStoreParameters = { /** - * The distance to scroll along the Y axis (positive to scroll up). + * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. +Security origin. */ - yDistance?: number; + securityOrigin?: string; /** - * The number of additional pixels to scroll back along the X axis, in addition to the given -distance. + * Storage key. */ - xOverscroll?: number; + storageKey?: string; /** - * The number of additional pixels to scroll back along the Y axis, in addition to the given -distance. + * Storage bucket. If not specified, it uses the default bucket. */ - yOverscroll?: number; + storageBucket?: Storage.StorageBucket; /** - * Prevent fling (default: true). + * Database name. */ - preventFling?: boolean; + databaseName: string; /** - * Swipe speed in pixels per second (default: 800). + * Object store name. */ - speed?: number; + objectStoreName: string; + } + export type clearObjectStoreReturnValue = { + } + /** + * Deletes a database. + */ + export type deleteDatabaseParameters = { /** - * Which type of input events to be generated (default: 'default', which queries the platform -for the preferred input type). + * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. +Security origin. */ - gestureSourceType?: GestureSourceType; + securityOrigin?: string; /** - * The number of times to repeat the gesture (default: 0). + * Storage key. */ - repeatCount?: number; + storageKey?: string; /** - * The number of milliseconds delay between each repeat. (default: 250). + * Storage bucket. If not specified, it uses the default bucket. */ - repeatDelayMs?: number; + storageBucket?: Storage.StorageBucket; /** - * The name of the interaction markers to generate, if not empty (default: ""). + * Database name. */ - interactionMarkerName?: string; + databaseName: string; } - export type synthesizeScrollGestureReturnValue = { + export type deleteDatabaseReturnValue = { } /** - * Synthesizes a tap gesture over a time period by issuing appropriate touch events. + * Delete a range of entries from an object store */ - export type synthesizeTapGestureParameters = { - /** - * X coordinate of the start of the gesture in CSS pixels. - */ - x: number; + export type deleteObjectStoreEntriesParameters = { /** - * Y coordinate of the start of the gesture in CSS pixels. + * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. +Security origin. */ - y: number; + securityOrigin?: string; /** - * Duration between touchdown and touchup events in ms (default: 50). + * Storage key. */ - duration?: number; + storageKey?: string; /** - * Number of times to perform the tap (e.g. 2 for double tap, default: 1). + * Storage bucket. If not specified, it uses the default bucket. */ - tapCount?: number; + storageBucket?: Storage.StorageBucket; + databaseName: string; + objectStoreName: string; /** - * Which type of input events to be generated (default: 'default', which queries the platform -for the preferred input type). + * Range of entry keys to delete */ - gestureSourceType?: GestureSourceType; + keyRange: KeyRange; } - export type synthesizeTapGestureReturnValue = { + export type deleteObjectStoreEntriesReturnValue = { } - } - - export module Inspector { - /** - * Fired when remote debugging connection is about to be terminated. Contains detach reason. - */ - export type detachedPayload = { - /** - * The reason why connection has been terminated. - */ - reason: string; - } - /** - * Fired when debugging target has crashed - */ - export type targetCrashedPayload = void; - /** - * Fired when debugging target has reloaded after crash - */ - export type targetReloadedAfterCrashPayload = void; - - /** - * Disables inspector domain notifications. + * Disables events from backend. */ export type disableParameters = { } export type disableReturnValue = { } /** - * Enables inspector domain notifications. + * Enables events from backend. */ export type enableParameters = { } export type enableReturnValue = { } - } - - export module LayerTree { - /** - * Unique Layer identifier. - */ - export type LayerId = string; - /** - * Unique snapshot identifier. - */ - export type SnapshotId = string; /** - * Rectangle where scrolling happens on the main thread. + * Requests data from object store or index. */ - export interface ScrollRect { + export type requestDataParameters = { /** - * Rectangle itself. + * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. +Security origin. */ - rect: DOM.Rect; + securityOrigin?: string; /** - * Reason for rectangle to force scrolling on the main thread + * Storage key. */ - type: "RepaintsOnScroll"|"TouchEventHandler"|"WheelEventHandler"; - } - /** - * Sticky position constraints. - */ - export interface StickyPositionConstraint { + storageKey?: string; /** - * Layout rectangle of the sticky element before being shifted + * Storage bucket. If not specified, it uses the default bucket. */ - stickyBoxRect: DOM.Rect; + storageBucket?: Storage.StorageBucket; /** - * Layout rectangle of the containing block of the sticky element + * Database name. */ - containingBlockRect: DOM.Rect; + databaseName: string; /** - * The nearest sticky layer that shifts the sticky box + * Object store name. */ - nearestLayerShiftingStickyBox?: LayerId; + objectStoreName: string; /** - * The nearest sticky layer that shifts the containing block + * Index name, empty string for object store data requests. */ - nearestLayerShiftingContainingBlock?: LayerId; - } - /** - * Serialized fragment of layer picture along with its offset within the layer. - */ - export interface PictureTile { + indexName: string; /** - * Offset from owning layer left boundary + * Number of records to skip. */ - x: number; + skipCount: number; /** - * Offset from owning layer top boundary + * Number of records to fetch. */ - y: number; + pageSize: number; /** - * Base64-encoded snapshot data. + * Key range. */ - picture: binary; + keyRange?: KeyRange; } - /** - * Information about a compositing layer. - */ - export interface Layer { - /** - * The unique id for this layer. - */ - layerId: LayerId; - /** - * The id of parent (not present for root). - */ - parentLayerId?: LayerId; + export type requestDataReturnValue = { /** - * The backend id for the node associated with this layer. + * Array of object store data entries. */ - backendNodeId?: DOM.BackendNodeId; + objectStoreDataEntries: DataEntry[]; /** - * Offset from parent layer, X coordinate. + * If true, there are more entries to fetch in the given range. */ - offsetX: number; + hasMore: boolean; + } + /** + * Gets metadata of an object store. + */ + export type getMetadataParameters = { /** - * Offset from parent layer, Y coordinate. + * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. +Security origin. */ - offsetY: number; + securityOrigin?: string; /** - * Layer width. + * Storage key. */ - width: number; + storageKey?: string; /** - * Layer height. + * Storage bucket. If not specified, it uses the default bucket. */ - height: number; + storageBucket?: Storage.StorageBucket; /** - * Transformation matrix for layer, default is identity matrix + * Database name. */ - transform?: number[]; + databaseName: string; /** - * Transform anchor point X, absent if no transform specified + * Object store name. */ - anchorX?: number; + objectStoreName: string; + } + export type getMetadataReturnValue = { /** - * Transform anchor point Y, absent if no transform specified + * the entries count */ - anchorY?: number; + entriesCount: number; /** - * Transform anchor point Z, absent if no transform specified + * the current value of key generator, to become the next inserted +key into the object store. Valid if objectStore.autoIncrement +is true. */ - anchorZ?: number; + keyGeneratorValue: number; + } + /** + * Requests database with given name in given frame. + */ + export type requestDatabaseParameters = { /** - * Indicates how many time this layer has painted. + * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. +Security origin. */ - paintCount: number; + securityOrigin?: string; /** - * Indicates whether this layer hosts any content, rather than being used for -transform/scrolling purposes only. + * Storage key. */ - drawsContent: boolean; + storageKey?: string; /** - * Set if layer is not visible. + * Storage bucket. If not specified, it uses the default bucket. */ - invisible?: boolean; + storageBucket?: Storage.StorageBucket; /** - * Rectangles scrolling on main thread only. + * Database name. */ - scrollRects?: ScrollRect[]; + databaseName: string; + } + export type requestDatabaseReturnValue = { /** - * Sticky position constraint information + * Database with an array of object stores. */ - stickyPositionConstraint?: StickyPositionConstraint; + databaseWithObjectStores: DatabaseWithObjectStores; } /** - * Array of timings, one per paint step. + * Requests database names for given security origin. */ - export type PaintProfile = number[]; - - export type layerPaintedPayload = { + export type requestDatabaseNamesParameters = { /** - * The id of the painted layer. + * At least and at most one of securityOrigin, storageKey, or storageBucket must be specified. +Security origin. */ - layerId: LayerId; + securityOrigin?: string; /** - * Clip rectangle. + * Storage key. */ - clip: DOM.Rect; - } - export type layerTreeDidChangePayload = { + storageKey?: string; /** - * Layer tree, absent if not in the compositing mode. + * Storage bucket. If not specified, it uses the default bucket. */ - layers?: Layer[]; + storageBucket?: Storage.StorageBucket; } - - /** - * Provides the reasons why the given layer was composited. - */ - export type compositingReasonsParameters = { + export type requestDatabaseNamesReturnValue = { /** - * The id of the layer for which we want to get the reasons it was composited. + * Database names for origin. */ - layerId: LayerId; + databaseNames: string[]; } - export type compositingReasonsReturnValue = { + } + + export module Input { + export interface TouchPoint { /** - * A list of strings specifying reasons for the given layer to become composited. + * X coordinate of the event relative to the main frame's viewport in CSS pixels. */ - compositingReasons: string[]; + x: number; /** - * A list of strings specifying reason IDs for the given layer to become composited. + * Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to +the top of the viewport and Y increases as it proceeds towards the bottom of the viewport. */ - compositingReasonIds: string[]; - } - /** - * Disables compositing tree inspection. - */ - export type disableParameters = { - } - export type disableReturnValue = { - } - /** - * Enables compositing tree inspection. - */ - export type enableParameters = { - } - export type enableReturnValue = { - } - /** - * Returns the snapshot identifier. - */ - export type loadSnapshotParameters = { + y: number; /** - * An array of tiles composing the snapshot. + * X radius of the touch area (default: 1.0). */ - tiles: PictureTile[]; - } - export type loadSnapshotReturnValue = { + radiusX?: number; /** - * The id of the snapshot. + * Y radius of the touch area (default: 1.0). */ - snapshotId: SnapshotId; - } - /** - * Returns the layer snapshot identifier. - */ - export type makeSnapshotParameters = { + radiusY?: number; /** - * The id of the layer. + * Rotation angle (default: 0.0). */ - layerId: LayerId; - } - export type makeSnapshotReturnValue = { + rotationAngle?: number; /** - * The id of the layer snapshot. + * Force (default: 1.0). */ - snapshotId: SnapshotId; - } - export type profileSnapshotParameters = { + force?: number; /** - * The id of the layer snapshot. + * The normalized tangential pressure, which has a range of [-1,1] (default: 0). */ - snapshotId: SnapshotId; + tangentialPressure?: number; /** - * The maximum number of times to replay the snapshot (1, if not specified). + * The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0) */ - minRepeatCount?: number; + tiltX?: number; /** - * The minimum duration (in seconds) to replay the snapshot. + * The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0). */ - minDuration?: number; + tiltY?: number; /** - * The clip rectangle to apply when replaying the snapshot. + * The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0). */ - clipRect?: DOM.Rect; - } - export type profileSnapshotReturnValue = { + twist?: number; /** - * The array of paint profiles, one per run. + * Identifier used to track touch sources between events, must be unique within an event. */ - timings: PaintProfile[]; + id?: number; } + export type GestureSourceType = "default"|"touch"|"mouse"; + export type MouseButton = "none"|"left"|"middle"|"right"|"back"|"forward"; /** - * Releases layer snapshot captured by the back-end. + * UTC time in seconds, counted from January 1, 1970. */ - export type releaseSnapshotParameters = { + export type TimeSinceEpoch = number; + export interface DragDataItem { /** - * The id of the layer snapshot. + * Mime type of the dragged data. */ - snapshotId: SnapshotId; - } - export type releaseSnapshotReturnValue = { - } - /** - * Replays the layer snapshot and returns the resulting bitmap. - */ - export type replaySnapshotParameters = { + mimeType: string; /** - * The id of the layer snapshot. + * Depending of the value of `mimeType`, it contains the dragged link, +text, HTML markup or any other data. */ - snapshotId: SnapshotId; + data: string; /** - * The first step to replay from (replay from the very start if not specified). + * Title associated with a link. Only valid when `mimeType` == "text/uri-list". */ - fromStep?: number; + title?: string; /** - * The last step to replay to (replay till the end if not specified). + * Stores the base URL for the contained markup. Only valid when `mimeType` +== "text/html". */ - toStep?: number; + baseURL?: string; + } + export interface DragData { + items: DragDataItem[]; /** - * The scale to apply while replaying (defaults to 1). + * List of filenames that should be included when dropping */ - scale?: number; - } - export type replaySnapshotReturnValue = { + files?: string[]; /** - * A data: URL for resulting image. + * Bit field representing allowed drag operations. Copy = 1, Link = 2, Move = 16 */ - dataURL: string; + dragOperationsMask: number; } + /** - * Replays the layer snapshot and returns canvas log. + * Emitted only when `Input.setInterceptDrags` is enabled. Use this data with `Input.dispatchDragEvent` to +restore normal drag and drop behavior. */ - export type snapshotCommandLogParameters = { + export type dragInterceptedPayload = { + data: DragData; + } + + /** + * Dispatches a drag event into the page. + */ + export type dispatchDragEventParameters = { /** - * The id of the layer snapshot. + * Type of the drag event. */ - snapshotId: SnapshotId; - } - export type snapshotCommandLogReturnValue = { + type: "dragEnter"|"dragOver"|"drop"|"dragCancel"; /** - * The array of canvas function calls. + * X coordinate of the event relative to the main frame's viewport in CSS pixels. */ - commandLog: { [key: string]: string }[]; + x: number; + /** + * Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to +the top of the viewport and Y increases as it proceeds towards the bottom of the viewport. + */ + y: number; + data: DragData; + /** + * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 +(default: 0). + */ + modifiers?: number; + } + export type dispatchDragEventReturnValue = { } - } - - /** - * Provides access to log entries. - */ - export module Log { /** - * Log entry. + * Dispatches a key event to the page. */ - export interface LogEntry { + export type dispatchKeyEventParameters = { /** - * Log entry source. + * Type of the key event. */ - source: "xml"|"javascript"|"network"|"storage"|"appcache"|"rendering"|"security"|"deprecation"|"worker"|"violation"|"intervention"|"recommendation"|"other"; + type: "keyDown"|"keyUp"|"rawKeyDown"|"char"; /** - * Log entry severity. + * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 +(default: 0). */ - level: "verbose"|"info"|"warning"|"error"; + modifiers?: number; /** - * Logged text. + * Time at which the event occurred. */ - text: string; - category?: "cors"; + timestamp?: TimeSinceEpoch; /** - * Timestamp when this entry was added. + * Text as generated by processing a virtual key code with a keyboard layout. Not needed for +for `keyUp` and `rawKeyDown` events (default: "") */ - timestamp: Runtime.Timestamp; + text?: string; /** - * URL of the resource if known. + * Text that would have been generated by the keyboard if no modifiers were pressed (except for +shift). Useful for shortcut (accelerator) key handling (default: ""). */ - url?: string; + unmodifiedText?: string; /** - * Line number in the resource. + * Unique key identifier (e.g., 'U+0041') (default: ""). */ - lineNumber?: number; + keyIdentifier?: string; /** - * JavaScript stack trace. + * Unique DOM defined string value for each physical key (e.g., 'KeyA') (default: ""). */ - stackTrace?: Runtime.StackTrace; + code?: string; /** - * Identifier of the network request associated with this entry. + * Unique DOM defined string value describing the meaning of the key in the context of active +modifiers, keyboard layout, etc (e.g., 'AltGr') (default: ""). */ - networkRequestId?: Network.RequestId; + key?: string; /** - * Identifier of the worker associated with this entry. + * Windows virtual key code (default: 0). */ - workerId?: string; + windowsVirtualKeyCode?: number; /** - * Call arguments. + * Native virtual key code (default: 0). */ - args?: Runtime.RemoteObject[]; - } - /** - * Violation configuration setting. - */ - export interface ViolationSetting { + nativeVirtualKeyCode?: number; /** - * Violation type. + * Whether the event was generated from auto repeat (default: false). */ - name: "longTask"|"longLayout"|"blockedEvent"|"blockedParser"|"discouragedAPIUse"|"handler"|"recurringHandler"; + autoRepeat?: boolean; /** - * Time threshold to trigger upon. + * Whether the event was generated from the keypad (default: false). */ - threshold: number; - } - - /** - * Issued when new message was logged. - */ - export type entryAddedPayload = { + isKeypad?: boolean; /** - * The entry. + * Whether the event was a system key event (default: false). */ - entry: LogEntry; - } - - /** - * Clears the log. - */ - export type clearParameters = { - } - export type clearReturnValue = { - } - /** - * Disables log domain, prevents further log entries from being reported to the client. - */ - export type disableParameters = { + isSystemKey?: boolean; + /** + * Whether the event was from the left or right side of the keyboard. 1=Left, 2=Right (default: +0). + */ + location?: number; + /** + * Editing commands to send with the key event (e.g., 'selectAll') (default: []). +These are related to but not equal the command names used in `document.execCommand` and NSStandardKeyBindingResponding. +See https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h for valid command names. + */ + commands?: string[]; } - export type disableReturnValue = { + export type dispatchKeyEventReturnValue = { } /** - * Enables log domain, sends the entries collected so far to the client by means of the -`entryAdded` notification. + * This method emulates inserting text that doesn't come from a key press, +for example an emoji keyboard or an IME. */ - export type enableParameters = { + export type insertTextParameters = { + /** + * The text to insert. + */ + text: string; } - export type enableReturnValue = { + export type insertTextReturnValue = { } /** - * start violation reporting. + * This method sets the current candidate text for IME. +Use imeCommitComposition to commit the final text. +Use imeSetComposition with empty string as text to cancel composition. */ - export type startViolationsReportParameters = { + export type imeSetCompositionParameters = { /** - * Configuration for violations. + * The text to insert */ - config: ViolationSetting[]; + text: string; + /** + * selection start + */ + selectionStart: number; + /** + * selection end + */ + selectionEnd: number; + /** + * replacement start + */ + replacementStart?: number; + /** + * replacement end + */ + replacementEnd?: number; } - export type startViolationsReportReturnValue = { + export type imeSetCompositionReturnValue = { } /** - * Stop violation reporting. + * Dispatches a mouse event to the page. */ - export type stopViolationsReportParameters = { - } - export type stopViolationsReportReturnValue = { - } - } - - export module Memory { - /** - * Memory pressure level. - */ - export type PressureLevel = "moderate"|"critical"; - /** - * Heap profile sample. - */ - export interface SamplingProfileNode { + export type dispatchMouseEventParameters = { /** - * Size of the sampled allocation. + * Type of the mouse event. */ - size: number; + type: "mousePressed"|"mouseReleased"|"mouseMoved"|"mouseWheel"; /** - * Total bytes attributed to this sample. + * X coordinate of the event relative to the main frame's viewport in CSS pixels. */ - total: number; + x: number; /** - * Execution stack at the point of allocation. + * Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to +the top of the viewport and Y increases as it proceeds towards the bottom of the viewport. */ - stack: string[]; - } - /** - * Array of heap profile samples. - */ - export interface SamplingProfile { - samples: SamplingProfileNode[]; - modules: Module[]; - } - /** - * Executable module information - */ - export interface Module { + y: number; /** - * Name of the module. + * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 +(default: 0). */ - name: string; + modifiers?: number; /** - * UUID of the module. + * Time at which the event occurred. */ - uuid: string; + timestamp?: TimeSinceEpoch; /** - * Base address where the module is loaded into memory. Encoded as a decimal -or hexadecimal (0x prefixed) string. + * Mouse button (default: "none"). */ - baseAddress: string; + button?: MouseButton; /** - * Size of the module in bytes. + * A number indicating which buttons are pressed on the mouse when a mouse event is triggered. +Left=1, Right=2, Middle=4, Back=8, Forward=16, None=0. */ - size: number; - } - /** - * DOM object counter data. - */ - export interface DOMCounter { + buttons?: number; /** - * Object name. Note: object names should be presumed volatile and clients should not expect -the returned names to be consistent across runs. + * Number of times the mouse button was clicked (default: 0). */ - name: string; + clickCount?: number; /** - * Object count. + * The normalized pressure, which has a range of [0,1] (default: 0). */ - count: number; - } - - - /** - * Retruns current DOM object counters. - */ - export type getDOMCountersParameters = { + force?: number; + /** + * The normalized tangential pressure, which has a range of [-1,1] (default: 0). + */ + tangentialPressure?: number; + /** + * The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0). + */ + tiltX?: number; + /** + * The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0). + */ + tiltY?: number; + /** + * The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0). + */ + twist?: number; + /** + * X delta in CSS pixels for mouse wheel event (default: 0). + */ + deltaX?: number; + /** + * Y delta in CSS pixels for mouse wheel event (default: 0). + */ + deltaY?: number; + /** + * Pointer type (default: "mouse"). + */ + pointerType?: "mouse"|"pen"; } - export type getDOMCountersReturnValue = { - documents: number; - nodes: number; - jsEventListeners: number; + export type dispatchMouseEventReturnValue = { } /** - * Retruns DOM object counters after preparing renderer for leak detection. + * Dispatches a touch event to the page. */ - export type getDOMCountersForLeakDetectionParameters = { - } - export type getDOMCountersForLeakDetectionReturnValue = { + export type dispatchTouchEventParameters = { /** - * DOM object counters. + * Type of the touch event. TouchEnd and TouchCancel must not contain any touch points, while +TouchStart and TouchMove must contains at least one. */ - counters: DOMCounter[]; - } - /** - * Prepares for leak detection by terminating workers, stopping spellcheckers, -dropping non-essential internal caches, running garbage collections, etc. - */ - export type prepareForLeakDetectionParameters = { + type: "touchStart"|"touchEnd"|"touchMove"|"touchCancel"; + /** + * Active touch points on the touch device. One event per any changed point (compared to +previous touch event in a sequence) is generated, emulating pressing/moving/releasing points +one by one. + */ + touchPoints: TouchPoint[]; + /** + * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 +(default: 0). + */ + modifiers?: number; + /** + * Time at which the event occurred. + */ + timestamp?: TimeSinceEpoch; } - export type prepareForLeakDetectionReturnValue = { + export type dispatchTouchEventReturnValue = { } /** - * Simulate OomIntervention by purging V8 memory. + * Cancels any active dragging in the page. */ - export type forciblyPurgeJavaScriptMemoryParameters = { + export type cancelDraggingParameters = { } - export type forciblyPurgeJavaScriptMemoryReturnValue = { + export type cancelDraggingReturnValue = { } /** - * Enable/disable suppressing memory pressure notifications in all processes. + * Emulates touch event from the mouse event parameters. */ - export type setPressureNotificationsSuppressedParameters = { + export type emulateTouchFromMouseEventParameters = { /** - * If true, memory pressure notifications will be suppressed. + * Type of the mouse event. */ - suppressed: boolean; - } - export type setPressureNotificationsSuppressedReturnValue = { - } - /** - * Simulate a memory pressure notification in all processes. - */ - export type simulatePressureNotificationParameters = { + type: "mousePressed"|"mouseReleased"|"mouseMoved"|"mouseWheel"; /** - * Memory pressure level of the notification. + * X coordinate of the mouse pointer in DIP. */ - level: PressureLevel; + x: number; + /** + * Y coordinate of the mouse pointer in DIP. + */ + y: number; + /** + * Mouse button. Only "none", "left", "right" are supported. + */ + button: MouseButton; + /** + * Time at which the event occurred (default: current time). + */ + timestamp?: TimeSinceEpoch; + /** + * X delta in DIP for mouse wheel event (default: 0). + */ + deltaX?: number; + /** + * Y delta in DIP for mouse wheel event (default: 0). + */ + deltaY?: number; + /** + * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8 +(default: 0). + */ + modifiers?: number; + /** + * Number of times the mouse button was clicked (default: 0). + */ + clickCount?: number; } - export type simulatePressureNotificationReturnValue = { + export type emulateTouchFromMouseEventReturnValue = { } /** - * Start collecting native memory profile. + * Ignores input events (useful while auditing page). */ - export type startSamplingParameters = { - /** - * Average number of bytes between samples. - */ - samplingInterval?: number; + export type setIgnoreInputEventsParameters = { /** - * Do not randomize intervals between samples. + * Ignores input events processing when set to true. */ - suppressRandomness?: boolean; + ignore: boolean; } - export type startSamplingReturnValue = { + export type setIgnoreInputEventsReturnValue = { } /** - * Stop collecting native memory profile. + * Prevents default drag and drop behavior and instead emits `Input.dragIntercepted` events. +Drag and drop behavior can be directly controlled via `Input.dispatchDragEvent`. */ - export type stopSamplingParameters = { + export type setInterceptDragsParameters = { + enabled: boolean; } - export type stopSamplingReturnValue = { + export type setInterceptDragsReturnValue = { } /** - * Retrieve native memory allocations profile -collected since renderer process startup. + * Synthesizes a pinch gesture over a time period by issuing appropriate touch events. */ - export type getAllTimeSamplingProfileParameters = { + export type synthesizePinchGestureParameters = { + /** + * X coordinate of the start of the gesture in CSS pixels. + */ + x: number; + /** + * Y coordinate of the start of the gesture in CSS pixels. + */ + y: number; + /** + * Relative scale factor after zooming (>1.0 zooms in, <1.0 zooms out). + */ + scaleFactor: number; + /** + * Relative pointer speed in pixels per second (default: 800). + */ + relativeSpeed?: number; + /** + * Which type of input events to be generated (default: 'default', which queries the platform +for the preferred input type). + */ + gestureSourceType?: GestureSourceType; } - export type getAllTimeSamplingProfileReturnValue = { - profile: SamplingProfile; + export type synthesizePinchGestureReturnValue = { } /** - * Retrieve native memory allocations profile -collected since browser process startup. + * Synthesizes a scroll gesture over a time period by issuing appropriate touch events. */ - export type getBrowserSamplingProfileParameters = { + export type synthesizeScrollGestureParameters = { + /** + * X coordinate of the start of the gesture in CSS pixels. + */ + x: number; + /** + * Y coordinate of the start of the gesture in CSS pixels. + */ + y: number; + /** + * The distance to scroll along the X axis (positive to scroll left). + */ + xDistance?: number; + /** + * The distance to scroll along the Y axis (positive to scroll up). + */ + yDistance?: number; + /** + * The number of additional pixels to scroll back along the X axis, in addition to the given +distance. + */ + xOverscroll?: number; + /** + * The number of additional pixels to scroll back along the Y axis, in addition to the given +distance. + */ + yOverscroll?: number; + /** + * Prevent fling (default: true). + */ + preventFling?: boolean; + /** + * Swipe speed in pixels per second (default: 800). + */ + speed?: number; + /** + * Which type of input events to be generated (default: 'default', which queries the platform +for the preferred input type). + */ + gestureSourceType?: GestureSourceType; + /** + * The number of times to repeat the gesture (default: 0). + */ + repeatCount?: number; + /** + * The number of milliseconds delay between each repeat. (default: 250). + */ + repeatDelayMs?: number; + /** + * The name of the interaction markers to generate, if not empty (default: ""). + */ + interactionMarkerName?: string; } - export type getBrowserSamplingProfileReturnValue = { - profile: SamplingProfile; + export type synthesizeScrollGestureReturnValue = { } /** - * Retrieve native memory allocations profile collected since last -`startSampling` call. + * Synthesizes a tap gesture over a time period by issuing appropriate touch events. */ - export type getSamplingProfileParameters = { + export type synthesizeTapGestureParameters = { + /** + * X coordinate of the start of the gesture in CSS pixels. + */ + x: number; + /** + * Y coordinate of the start of the gesture in CSS pixels. + */ + y: number; + /** + * Duration between touchdown and touchup events in ms (default: 50). + */ + duration?: number; + /** + * Number of times to perform the tap (e.g. 2 for double tap, default: 1). + */ + tapCount?: number; + /** + * Which type of input events to be generated (default: 'default', which queries the platform +for the preferred input type). + */ + gestureSourceType?: GestureSourceType; } - export type getSamplingProfileReturnValue = { - profile: SamplingProfile; + export type synthesizeTapGestureReturnValue = { } } - /** - * Network domain allows tracking network activities of the page. It exposes information about http, -file, data and other requests and responses, their headers, bodies, timing, etc. - */ - export module Network { - /** - * Resource type as it was perceived by the rendering engine. - */ - export type ResourceType = "Document"|"Stylesheet"|"Image"|"Media"|"Font"|"Script"|"TextTrack"|"XHR"|"Fetch"|"Prefetch"|"EventSource"|"WebSocket"|"Manifest"|"SignedExchange"|"Ping"|"CSPViolationReport"|"Preflight"|"FedCM"|"Other"; - /** - * Unique loader identifier. - */ - export type LoaderId = string; - /** - * Unique network request identifier. -Note that this does not identify individual HTTP requests that are part of -a network request. - */ - export type RequestId = string; - /** - * Unique intercepted request identifier. - */ - export type InterceptionId = string; - /** - * Network level fetch failure reason. - */ - export type ErrorReason = "Failed"|"Aborted"|"TimedOut"|"AccessDenied"|"ConnectionClosed"|"ConnectionReset"|"ConnectionRefused"|"ConnectionAborted"|"ConnectionFailed"|"NameNotResolved"|"InternetDisconnected"|"AddressUnreachable"|"BlockedByClient"|"BlockedByResponse"; + export module Inspector { + /** - * UTC time in seconds, counted from January 1, 1970. + * Fired when remote debugging connection is about to be terminated. Contains detach reason. */ - export type TimeSinceEpoch = number; + export type detachedPayload = { + /** + * The reason why connection has been terminated. + */ + reason: string; + } /** - * Monotonically increasing time in seconds since an arbitrary point in the past. + * Fired when debugging target has crashed */ - export type MonotonicTime = number; + export type targetCrashedPayload = void; /** - * Request / response headers as keys / values of JSON object. + * Fired when debugging target has reloaded after crash */ - export type Headers = { [key: string]: string }; + export type targetReloadedAfterCrashPayload = void; + /** - * The underlying connection technology that the browser is supposedly using. + * Disables inspector domain notifications. */ - export type ConnectionType = "none"|"cellular2g"|"cellular3g"|"cellular4g"|"bluetooth"|"ethernet"|"wifi"|"wimax"|"other"; + export type disableParameters = { + } + export type disableReturnValue = { + } /** - * Represents the cookie's 'SameSite' status: -https://tools.ietf.org/html/draft-west-first-party-cookies + * Enables inspector domain notifications. */ - export type CookieSameSite = "Strict"|"Lax"|"None"; + export type enableParameters = { + } + export type enableReturnValue = { + } + } + + export module LayerTree { /** - * Represents the cookie's 'Priority' status: -https://tools.ietf.org/html/draft-west-cookie-priority-00 + * Unique Layer identifier. */ - export type CookiePriority = "Low"|"Medium"|"High"; + export type LayerId = string; /** - * Represents the source scheme of the origin that originally set the cookie. -A value of "Unset" allows protocol clients to emulate legacy cookie scope for the scheme. -This is a temporary ability and it will be removed in the future. + * Unique snapshot identifier. */ - export type CookieSourceScheme = "Unset"|"NonSecure"|"Secure"; + export type SnapshotId = string; /** - * Timing information for the request. + * Rectangle where scrolling happens on the main thread. */ - export interface ResourceTiming { + export interface ScrollRect { /** - * Timing's requestTime is a baseline in seconds, while the other numbers are ticks in -milliseconds relatively to this requestTime. + * Rectangle itself. */ - requestTime: number; + rect: DOM.Rect; /** - * Started resolving proxy. + * Reason for rectangle to force scrolling on the main thread */ - proxyStart: number; + type: "RepaintsOnScroll"|"TouchEventHandler"|"WheelEventHandler"; + } + /** + * Sticky position constraints. + */ + export interface StickyPositionConstraint { /** - * Finished resolving proxy. + * Layout rectangle of the sticky element before being shifted */ - proxyEnd: number; + stickyBoxRect: DOM.Rect; /** - * Started DNS address resolve. + * Layout rectangle of the containing block of the sticky element */ - dnsStart: number; + containingBlockRect: DOM.Rect; /** - * Finished DNS address resolve. + * The nearest sticky layer that shifts the sticky box */ - dnsEnd: number; + nearestLayerShiftingStickyBox?: LayerId; /** - * Started connecting to the remote host. + * The nearest sticky layer that shifts the containing block */ - connectStart: number; + nearestLayerShiftingContainingBlock?: LayerId; + } + /** + * Serialized fragment of layer picture along with its offset within the layer. + */ + export interface PictureTile { /** - * Connected to the remote host. + * Offset from owning layer left boundary */ - connectEnd: number; + x: number; /** - * Started SSL handshake. + * Offset from owning layer top boundary */ - sslStart: number; + y: number; /** - * Finished SSL handshake. + * Base64-encoded snapshot data. */ - sslEnd: number; + picture: binary; + } + /** + * Information about a compositing layer. + */ + export interface Layer { /** - * Started running ServiceWorker. + * The unique id for this layer. */ - workerStart: number; + layerId: LayerId; /** - * Finished Starting ServiceWorker. + * The id of parent (not present for root). */ - workerReady: number; + parentLayerId?: LayerId; /** - * Started fetch event. + * The backend id for the node associated with this layer. */ - workerFetchStart: number; + backendNodeId?: DOM.BackendNodeId; /** - * Settled fetch event respondWith promise. + * Offset from parent layer, X coordinate. */ - workerRespondWithSettled: number; + offsetX: number; /** - * Started ServiceWorker static routing source evaluation. + * Offset from parent layer, Y coordinate. */ - workerRouterEvaluationStart?: number; + offsetY: number; /** - * Started cache lookup when the source was evaluated to `cache`. + * Layer width. */ - workerCacheLookupStart?: number; + width: number; /** - * Started sending request. + * Layer height. */ - sendStart: number; + height: number; /** - * Finished sending request. + * Transformation matrix for layer, default is identity matrix */ - sendEnd: number; + transform?: number[]; /** - * Time the server started pushing request. + * Transform anchor point X, absent if no transform specified */ - pushStart: number; + anchorX?: number; /** - * Time the server finished pushing request. + * Transform anchor point Y, absent if no transform specified */ - pushEnd: number; - /** - * Started receiving response headers. - */ - receiveHeadersStart: number; + anchorY?: number; /** - * Finished receiving response headers. + * Transform anchor point Z, absent if no transform specified */ - receiveHeadersEnd: number; - } - /** - * Loading priority of a resource request. - */ - export type ResourcePriority = "VeryLow"|"Low"|"Medium"|"High"|"VeryHigh"; - /** - * Post data entry for HTTP request - */ - export interface PostDataEntry { - bytes?: binary; - } - /** - * HTTP request data. - */ - export interface Request { + anchorZ?: number; /** - * Request URL (without fragment). + * Indicates how many time this layer has painted. */ - url: string; + paintCount: number; /** - * Fragment of the requested URL starting with hash, if present. + * Indicates whether this layer hosts any content, rather than being used for +transform/scrolling purposes only. */ - urlFragment?: string; + drawsContent: boolean; /** - * HTTP request method. + * Set if layer is not visible. */ - method: string; + invisible?: boolean; /** - * HTTP request headers. + * Rectangles scrolling on main thread only. */ - headers: Headers; + scrollRects?: ScrollRect[]; /** - * HTTP POST request data. -Use postDataEntries instead. + * Sticky position constraint information */ - postData?: string; + stickyPositionConstraint?: StickyPositionConstraint; + } + /** + * Array of timings, one per paint step. + */ + export type PaintProfile = number[]; + + export type layerPaintedPayload = { /** - * True when the request has POST data. Note that postData might still be omitted when this flag is true when the data is too long. + * The id of the painted layer. */ - hasPostData?: boolean; + layerId: LayerId; /** - * Request body elements (post data broken into individual entries). + * Clip rectangle. */ - postDataEntries?: PostDataEntry[]; + clip: DOM.Rect; + } + export type layerTreeDidChangePayload = { /** - * The mixed content type of the request. + * Layer tree, absent if not in the compositing mode. */ - mixedContentType?: Security.MixedContentType; + layers?: Layer[]; + } + + /** + * Provides the reasons why the given layer was composited. + */ + export type compositingReasonsParameters = { /** - * Priority of the resource request at the time request is sent. + * The id of the layer for which we want to get the reasons it was composited. */ - initialPriority: ResourcePriority; + layerId: LayerId; + } + export type compositingReasonsReturnValue = { /** - * The referrer policy of the request, as defined in https://www.w3.org/TR/referrer-policy/ + * A list of strings specifying reasons for the given layer to become composited. */ - referrerPolicy: "unsafe-url"|"no-referrer-when-downgrade"|"no-referrer"|"origin"|"origin-when-cross-origin"|"same-origin"|"strict-origin"|"strict-origin-when-cross-origin"; + compositingReasons: string[]; /** - * Whether is loaded via link preload. + * A list of strings specifying reason IDs for the given layer to become composited. */ - isLinkPreload?: boolean; + compositingReasonIds: string[]; + } + /** + * Disables compositing tree inspection. + */ + export type disableParameters = { + } + export type disableReturnValue = { + } + /** + * Enables compositing tree inspection. + */ + export type enableParameters = { + } + export type enableReturnValue = { + } + /** + * Returns the snapshot identifier. + */ + export type loadSnapshotParameters = { /** - * Set for requests when the TrustToken API is used. Contains the parameters -passed by the developer (e.g. via "fetch") as understood by the backend. + * An array of tiles composing the snapshot. */ - trustTokenParams?: TrustTokenParams; + tiles: PictureTile[]; + } + export type loadSnapshotReturnValue = { /** - * True if this resource request is considered to be the 'same site' as the -request corresponding to the main frame. + * The id of the snapshot. */ - isSameSite?: boolean; + snapshotId: SnapshotId; } /** - * Details of a signed certificate timestamp (SCT). + * Returns the layer snapshot identifier. */ - export interface SignedCertificateTimestamp { + export type makeSnapshotParameters = { /** - * Validation status. + * The id of the layer. */ - status: string; + layerId: LayerId; + } + export type makeSnapshotReturnValue = { /** - * Origin. + * The id of the layer snapshot. */ - origin: string; + snapshotId: SnapshotId; + } + export type profileSnapshotParameters = { /** - * Log name / description. + * The id of the layer snapshot. */ - logDescription: string; + snapshotId: SnapshotId; /** - * Log ID. + * The maximum number of times to replay the snapshot (1, if not specified). */ - logId: string; + minRepeatCount?: number; /** - * Issuance date. Unlike TimeSinceEpoch, this contains the number of -milliseconds since January 1, 1970, UTC, not the number of seconds. + * The minimum duration (in seconds) to replay the snapshot. */ - timestamp: number; + minDuration?: number; /** - * Hash algorithm. + * The clip rectangle to apply when replaying the snapshot. */ - hashAlgorithm: string; + clipRect?: DOM.Rect; + } + export type profileSnapshotReturnValue = { /** - * Signature algorithm. + * The array of paint profiles, one per run. */ - signatureAlgorithm: string; + timings: PaintProfile[]; + } + /** + * Releases layer snapshot captured by the back-end. + */ + export type releaseSnapshotParameters = { /** - * Signature data. + * The id of the layer snapshot. */ - signatureData: string; + snapshotId: SnapshotId; + } + export type releaseSnapshotReturnValue = { } /** - * Security details about a request. + * Replays the layer snapshot and returns the resulting bitmap. */ - export interface SecurityDetails { + export type replaySnapshotParameters = { /** - * Protocol name (e.g. "TLS 1.2" or "QUIC"). + * The id of the layer snapshot. */ - protocol: string; + snapshotId: SnapshotId; /** - * Key Exchange used by the connection, or the empty string if not applicable. + * The first step to replay from (replay from the very start if not specified). */ - keyExchange: string; + fromStep?: number; /** - * (EC)DH group used by the connection, if applicable. + * The last step to replay to (replay till the end if not specified). */ - keyExchangeGroup?: string; + toStep?: number; /** - * Cipher name. + * The scale to apply while replaying (defaults to 1). */ - cipher: string; + scale?: number; + } + export type replaySnapshotReturnValue = { /** - * TLS MAC. Note that AEAD ciphers do not have separate MACs. + * A data: URL for resulting image. */ - mac?: string; + dataURL: string; + } + /** + * Replays the layer snapshot and returns canvas log. + */ + export type snapshotCommandLogParameters = { /** - * Certificate ID value. + * The id of the layer snapshot. */ - certificateId: Security.CertificateId; + snapshotId: SnapshotId; + } + export type snapshotCommandLogReturnValue = { /** - * Certificate subject name. + * The array of canvas function calls. */ - subjectName: string; + commandLog: { [key: string]: string }[]; + } + } + + /** + * Provides access to log entries. + */ + export module Log { + /** + * Log entry. + */ + export interface LogEntry { /** - * Subject Alternative Name (SAN) DNS names and IP addresses. + * Log entry source. */ - sanList: string[]; + source: "xml"|"javascript"|"network"|"storage"|"appcache"|"rendering"|"security"|"deprecation"|"worker"|"violation"|"intervention"|"recommendation"|"other"; /** - * Name of the issuing CA. + * Log entry severity. */ - issuer: string; + level: "verbose"|"info"|"warning"|"error"; /** - * Certificate valid from date. + * Logged text. */ - validFrom: TimeSinceEpoch; + text: string; + category?: "cors"; /** - * Certificate valid to (expiration) date + * Timestamp when this entry was added. */ - validTo: TimeSinceEpoch; + timestamp: Runtime.Timestamp; /** - * List of signed certificate timestamps (SCTs). + * URL of the resource if known. */ - signedCertificateTimestampList: SignedCertificateTimestamp[]; + url?: string; /** - * Whether the request complied with Certificate Transparency policy + * Line number in the resource. */ - certificateTransparencyCompliance: CertificateTransparencyCompliance; + lineNumber?: number; /** - * The signature algorithm used by the server in the TLS server signature, -represented as a TLS SignatureScheme code point. Omitted if not -applicable or not known. + * JavaScript stack trace. */ - serverSignatureAlgorithm?: number; + stackTrace?: Runtime.StackTrace; /** - * Whether the connection used Encrypted ClientHello + * Identifier of the network request associated with this entry. */ - encryptedClientHello: boolean; - } - /** - * Whether the request complied with Certificate Transparency policy. + networkRequestId?: Network.RequestId; + /** + * Identifier of the worker associated with this entry. + */ + workerId?: string; + /** + * Call arguments. + */ + args?: Runtime.RemoteObject[]; + } + /** + * Violation configuration setting. */ - export type CertificateTransparencyCompliance = "unknown"|"not-compliant"|"compliant"; + export interface ViolationSetting { + /** + * Violation type. + */ + name: "longTask"|"longLayout"|"blockedEvent"|"blockedParser"|"discouragedAPIUse"|"handler"|"recurringHandler"; + /** + * Time threshold to trigger upon. + */ + threshold: number; + } + /** - * The reason why request was blocked. + * Issued when new message was logged. */ - export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"integrity"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"|"sri-message-signature-mismatch"; + export type entryAddedPayload = { + /** + * The entry. + */ + entry: LogEntry; + } + /** - * The reason why request was blocked. + * Clears the log. */ - export type CorsError = "DisallowedByMode"|"InvalidResponse"|"WildcardOriginNotAllowed"|"MissingAllowOriginHeader"|"MultipleAllowOriginValues"|"InvalidAllowOriginValue"|"AllowOriginMismatch"|"InvalidAllowCredentials"|"CorsDisabledScheme"|"PreflightInvalidStatus"|"PreflightDisallowedRedirect"|"PreflightWildcardOriginNotAllowed"|"PreflightMissingAllowOriginHeader"|"PreflightMultipleAllowOriginValues"|"PreflightInvalidAllowOriginValue"|"PreflightAllowOriginMismatch"|"PreflightInvalidAllowCredentials"|"PreflightMissingAllowExternal"|"PreflightInvalidAllowExternal"|"PreflightMissingAllowPrivateNetwork"|"PreflightInvalidAllowPrivateNetwork"|"InvalidAllowMethodsPreflightResponse"|"InvalidAllowHeadersPreflightResponse"|"MethodDisallowedByPreflightResponse"|"HeaderDisallowedByPreflightResponse"|"RedirectContainsCredentials"|"InsecurePrivateNetwork"|"InvalidPrivateNetworkAccess"|"UnexpectedPrivateNetworkAccess"|"NoCorsRedirectModeNotFollow"|"PreflightMissingPrivateNetworkAccessId"|"PreflightMissingPrivateNetworkAccessName"|"PrivateNetworkAccessPermissionUnavailable"|"PrivateNetworkAccessPermissionDenied"|"LocalNetworkAccessPermissionDenied"; - export interface CorsErrorStatus { - corsError: CorsError; - failedParameter: string; + export type clearParameters = { + } + export type clearReturnValue = { } /** - * Source of serviceworker response. + * Disables log domain, prevents further log entries from being reported to the client. */ - export type ServiceWorkerResponseSource = "cache-storage"|"http-cache"|"fallback-code"|"network"; + export type disableParameters = { + } + export type disableReturnValue = { + } /** - * Determines what type of Trust Token operation is executed and -depending on the type, some additional parameters. The values -are specified in third_party/blink/renderer/core/fetch/trust_token.idl. + * Enables log domain, sends the entries collected so far to the client by means of the +`entryAdded` notification. */ - export interface TrustTokenParams { - operation: TrustTokenOperationType; + export type enableParameters = { + } + export type enableReturnValue = { + } + /** + * start violation reporting. + */ + export type startViolationsReportParameters = { /** - * Only set for "token-redemption" operation and determine whether -to request a fresh SRR or use a still valid cached SRR. + * Configuration for violations. */ - refreshPolicy: "UseCached"|"Refresh"; + config: ViolationSetting[]; + } + export type startViolationsReportReturnValue = { + } + /** + * Stop violation reporting. + */ + export type stopViolationsReportParameters = { + } + export type stopViolationsReportReturnValue = { + } + } + + /** + * This domain allows detailed inspection of media elements. + */ + export module Media { + /** + * Players will get an ID that is unique within the agent context. + */ + export type PlayerId = string; + export type Timestamp = number; + /** + * Have one type per entry in MediaLogRecord::Type +Corresponds to kMessage + */ + export interface PlayerMessage { /** - * Origins of issuers from whom to request tokens or redemption -records. + * Keep in sync with MediaLogMessageLevel +We are currently keeping the message level 'error' separate from the +PlayerError type because right now they represent different things, +this one being a DVLOG(ERROR) style log message that gets printed +based on what log level is selected in the UI, and the other is a +representation of a media::PipelineStatus object. Soon however we're +going to be moving away from using PipelineStatus for errors and +introducing a new error type which should hopefully let us integrate +the error log level into the PlayerError type. */ - issuers?: string[]; + level: "error"|"warning"|"info"|"debug"; + message: string; } - export type TrustTokenOperationType = "Issuance"|"Redemption"|"Signing"; /** - * The reason why Chrome uses a specific transport protocol for HTTP semantics. + * Corresponds to kMediaPropertyChange */ - export type AlternateProtocolUsage = "alternativeJobWonWithoutRace"|"alternativeJobWonRace"|"mainJobWonRace"|"mappingMissing"|"broken"|"dnsAlpnH3JobWonWithoutRace"|"dnsAlpnH3JobWonRace"|"unspecifiedReason"; + export interface PlayerProperty { + name: string; + value: string; + } /** - * Source of service worker router. + * Corresponds to kMediaEventTriggered */ - export type ServiceWorkerRouterSource = "network"|"cache"|"fetch-event"|"race-network-and-fetch-handler"|"race-network-and-cache"; - export interface ServiceWorkerRouterInfo { + export interface PlayerEvent { + timestamp: Timestamp; + value: string; + } + /** + * Represents logged source line numbers reported in an error. +NOTE: file and line are from chromium c++ implementation code, not js. + */ + export interface PlayerErrorSourceLocation { + file: string; + line: number; + } + /** + * Corresponds to kMediaError + */ + export interface PlayerError { + errorType: string; /** - * ID of the rule matched. If there is a matched rule, this field will -be set, otherwiser no value will be set. + * Code is the numeric enum entry for a specific set of error codes, such +as PipelineStatusCodes in media/base/pipeline_status.h */ - ruleIdMatched?: number; + code: number; /** - * The router source of the matched rule. If there is a matched rule, this -field will be set, otherwise no value will be set. + * A trace of where this error was caused / where it passed through. */ - matchedSourceType?: ServiceWorkerRouterSource; + stack: PlayerErrorSourceLocation[]; /** - * The actual router source used. + * Errors potentially have a root cause error, ie, a DecoderError might be +caused by an WindowsError */ - actualSourceType?: ServiceWorkerRouterSource; + cause: PlayerError[]; + /** + * Extra data attached to an error, such as an HRESULT, Video Codec, etc. + */ + data: { [key: string]: string }; + } + export interface Player { + playerId: PlayerId; + domNodeId?: DOM.BackendNodeId; } + /** - * HTTP response data. + * This can be called multiple times, and can be used to set / override / +remove player properties. A null propValue indicates removal. */ - export interface Response { + export type playerPropertiesChangedPayload = { + playerId: PlayerId; + properties: PlayerProperty[]; + } + /** + * Send events as a list, allowing them to be batched on the browser for less +congestion. If batched, events must ALWAYS be in chronological order. + */ + export type playerEventsAddedPayload = { + playerId: PlayerId; + events: PlayerEvent[]; + } + /** + * Send a list of any messages that need to be delivered. + */ + export type playerMessagesLoggedPayload = { + playerId: PlayerId; + messages: PlayerMessage[]; + } + /** + * Send a list of any errors that need to be delivered. + */ + export type playerErrorsRaisedPayload = { + playerId: PlayerId; + errors: PlayerError[]; + } + /** + * Called whenever a player is created, or when a new agent joins and receives +a list of active players. If an agent is restored, it will receive one +event for each active player. + */ + export type playerCreatedPayload = { + player: Player; + } + + /** + * Enables the Media domain + */ + export type enableParameters = { + } + export type enableReturnValue = { + } + /** + * Disables the Media domain. + */ + export type disableParameters = { + } + export type disableReturnValue = { + } + } + + export module Memory { + /** + * Memory pressure level. + */ + export type PressureLevel = "moderate"|"critical"; + /** + * Heap profile sample. + */ + export interface SamplingProfileNode { /** - * Response URL. This URL can be different from CachedResource.url in case of redirect. + * Size of the sampled allocation. */ - url: string; + size: number; /** - * HTTP response status code. + * Total bytes attributed to this sample. */ - status: number; + total: number; /** - * HTTP response status text. + * Execution stack at the point of allocation. */ - statusText: string; + stack: string[]; + } + /** + * Array of heap profile samples. + */ + export interface SamplingProfile { + samples: SamplingProfileNode[]; + modules: Module[]; + } + /** + * Executable module information + */ + export interface Module { /** - * HTTP response headers. + * Name of the module. */ - headers: Headers; + name: string; /** - * HTTP response headers text. This has been replaced by the headers in Network.responseReceivedExtraInfo. + * UUID of the module. */ - headersText?: string; + uuid: string; /** - * Resource mimeType as determined by the browser. - */ - mimeType: string; - /** - * Resource charset as determined by the browser (if applicable). + * Base address where the module is loaded into memory. Encoded as a decimal +or hexadecimal (0x prefixed) string. */ - charset: string; + baseAddress: string; /** - * Refined HTTP request headers that were actually transmitted over the network. + * Size of the module in bytes. */ - requestHeaders?: Headers; + size: number; + } + /** + * DOM object counter data. + */ + export interface DOMCounter { /** - * HTTP request headers text. This has been replaced by the headers in Network.requestWillBeSentExtraInfo. + * Object name. Note: object names should be presumed volatile and clients should not expect +the returned names to be consistent across runs. */ - requestHeadersText?: string; + name: string; /** - * Specifies whether physical connection was actually reused for this request. + * Object count. */ - connectionReused: boolean; + count: number; + } + + + /** + * Retruns current DOM object counters. + */ + export type getDOMCountersParameters = { + } + export type getDOMCountersReturnValue = { + documents: number; + nodes: number; + jsEventListeners: number; + } + /** + * Retruns DOM object counters after preparing renderer for leak detection. + */ + export type getDOMCountersForLeakDetectionParameters = { + } + export type getDOMCountersForLeakDetectionReturnValue = { /** - * Physical connection id that was actually used for this request. + * DOM object counters. */ - connectionId: number; + counters: DOMCounter[]; + } + /** + * Prepares for leak detection by terminating workers, stopping spellcheckers, +dropping non-essential internal caches, running garbage collections, etc. + */ + export type prepareForLeakDetectionParameters = { + } + export type prepareForLeakDetectionReturnValue = { + } + /** + * Simulate OomIntervention by purging V8 memory. + */ + export type forciblyPurgeJavaScriptMemoryParameters = { + } + export type forciblyPurgeJavaScriptMemoryReturnValue = { + } + /** + * Enable/disable suppressing memory pressure notifications in all processes. + */ + export type setPressureNotificationsSuppressedParameters = { /** - * Remote IP address. + * If true, memory pressure notifications will be suppressed. */ - remoteIPAddress?: string; + suppressed: boolean; + } + export type setPressureNotificationsSuppressedReturnValue = { + } + /** + * Simulate a memory pressure notification in all processes. + */ + export type simulatePressureNotificationParameters = { /** - * Remote port. + * Memory pressure level of the notification. */ - remotePort?: number; + level: PressureLevel; + } + export type simulatePressureNotificationReturnValue = { + } + /** + * Start collecting native memory profile. + */ + export type startSamplingParameters = { /** - * Specifies that the request was served from the disk cache. + * Average number of bytes between samples. */ - fromDiskCache?: boolean; + samplingInterval?: number; /** - * Specifies that the request was served from the ServiceWorker. + * Do not randomize intervals between samples. */ - fromServiceWorker?: boolean; + suppressRandomness?: boolean; + } + export type startSamplingReturnValue = { + } + /** + * Stop collecting native memory profile. + */ + export type stopSamplingParameters = { + } + export type stopSamplingReturnValue = { + } + /** + * Retrieve native memory allocations profile +collected since renderer process startup. + */ + export type getAllTimeSamplingProfileParameters = { + } + export type getAllTimeSamplingProfileReturnValue = { + profile: SamplingProfile; + } + /** + * Retrieve native memory allocations profile +collected since browser process startup. + */ + export type getBrowserSamplingProfileParameters = { + } + export type getBrowserSamplingProfileReturnValue = { + profile: SamplingProfile; + } + /** + * Retrieve native memory allocations profile collected since last +`startSampling` call. + */ + export type getSamplingProfileParameters = { + } + export type getSamplingProfileReturnValue = { + profile: SamplingProfile; + } + } + + /** + * Network domain allows tracking network activities of the page. It exposes information about http, +file, data and other requests and responses, their headers, bodies, timing, etc. + */ + export module Network { + /** + * Resource type as it was perceived by the rendering engine. + */ + export type ResourceType = "Document"|"Stylesheet"|"Image"|"Media"|"Font"|"Script"|"TextTrack"|"XHR"|"Fetch"|"Prefetch"|"EventSource"|"WebSocket"|"Manifest"|"SignedExchange"|"Ping"|"CSPViolationReport"|"Preflight"|"FedCM"|"Other"; + /** + * Unique loader identifier. + */ + export type LoaderId = string; + /** + * Unique network request identifier. +Note that this does not identify individual HTTP requests that are part of +a network request. + */ + export type RequestId = string; + /** + * Unique intercepted request identifier. + */ + export type InterceptionId = string; + /** + * Network level fetch failure reason. + */ + export type ErrorReason = "Failed"|"Aborted"|"TimedOut"|"AccessDenied"|"ConnectionClosed"|"ConnectionReset"|"ConnectionRefused"|"ConnectionAborted"|"ConnectionFailed"|"NameNotResolved"|"InternetDisconnected"|"AddressUnreachable"|"BlockedByClient"|"BlockedByResponse"; + /** + * UTC time in seconds, counted from January 1, 1970. + */ + export type TimeSinceEpoch = number; + /** + * Monotonically increasing time in seconds since an arbitrary point in the past. + */ + export type MonotonicTime = number; + /** + * Request / response headers as keys / values of JSON object. + */ + export type Headers = { [key: string]: string }; + /** + * The underlying connection technology that the browser is supposedly using. + */ + export type ConnectionType = "none"|"cellular2g"|"cellular3g"|"cellular4g"|"bluetooth"|"ethernet"|"wifi"|"wimax"|"other"; + /** + * Represents the cookie's 'SameSite' status: +https://tools.ietf.org/html/draft-west-first-party-cookies + */ + export type CookieSameSite = "Strict"|"Lax"|"None"; + /** + * Represents the cookie's 'Priority' status: +https://tools.ietf.org/html/draft-west-cookie-priority-00 + */ + export type CookiePriority = "Low"|"Medium"|"High"; + /** + * Represents the source scheme of the origin that originally set the cookie. +A value of "Unset" allows protocol clients to emulate legacy cookie scope for the scheme. +This is a temporary ability and it will be removed in the future. + */ + export type CookieSourceScheme = "Unset"|"NonSecure"|"Secure"; + /** + * Timing information for the request. + */ + export interface ResourceTiming { /** - * Specifies that the request was served from the prefetch cache. + * Timing's requestTime is a baseline in seconds, while the other numbers are ticks in +milliseconds relatively to this requestTime. */ - fromPrefetchCache?: boolean; + requestTime: number; /** - * Specifies that the request was served from the prefetch cache. + * Started resolving proxy. */ - fromEarlyHints?: boolean; + proxyStart: number; /** - * Information about how ServiceWorker Static Router API was used. If this -field is set with `matchedSourceType` field, a matching rule is found. -If this field is set without `matchedSource`, no matching rule is found. -Otherwise, the API is not used. + * Finished resolving proxy. */ - serviceWorkerRouterInfo?: ServiceWorkerRouterInfo; + proxyEnd: number; /** - * Total number of bytes received for this request so far. + * Started DNS address resolve. */ - encodedDataLength: number; + dnsStart: number; /** - * Timing information for the given request. + * Finished DNS address resolve. */ - timing?: ResourceTiming; + dnsEnd: number; /** - * Response source of response from ServiceWorker. + * Started connecting to the remote host. */ - serviceWorkerResponseSource?: ServiceWorkerResponseSource; + connectStart: number; /** - * The time at which the returned response was generated. + * Connected to the remote host. */ - responseTime?: TimeSinceEpoch; + connectEnd: number; /** - * Cache Storage Cache Name. + * Started SSL handshake. */ - cacheStorageCacheName?: string; + sslStart: number; /** - * Protocol used to fetch this request. + * Finished SSL handshake. */ - protocol?: string; + sslEnd: number; /** - * The reason why Chrome uses a specific transport protocol for HTTP semantics. + * Started running ServiceWorker. */ - alternateProtocolUsage?: AlternateProtocolUsage; + workerStart: number; /** - * Security state of the request resource. + * Finished Starting ServiceWorker. */ - securityState: Security.SecurityState; + workerReady: number; /** - * Security details for the request. + * Started fetch event. */ - securityDetails?: SecurityDetails; + workerFetchStart: number; /** - * Indicates whether the request was sent through IP Protection proxies. If -set to true, the request used the IP Protection privacy feature. + * Settled fetch event respondWith promise. */ - isIpProtectionUsed?: boolean; + workerRespondWithSettled: number; + /** + * Started ServiceWorker static routing source evaluation. + */ + workerRouterEvaluationStart?: number; + /** + * Started cache lookup when the source was evaluated to `cache`. + */ + workerCacheLookupStart?: number; + /** + * Started sending request. + */ + sendStart: number; + /** + * Finished sending request. + */ + sendEnd: number; + /** + * Time the server started pushing request. + */ + pushStart: number; + /** + * Time the server finished pushing request. + */ + pushEnd: number; + /** + * Started receiving response headers. + */ + receiveHeadersStart: number; + /** + * Finished receiving response headers. + */ + receiveHeadersEnd: number; } /** - * WebSocket request data. + * Loading priority of a resource request. */ - export interface WebSocketRequest { - /** - * HTTP request headers. - */ - headers: Headers; + export type ResourcePriority = "VeryLow"|"Low"|"Medium"|"High"|"VeryHigh"; + /** + * Post data entry for HTTP request + */ + export interface PostDataEntry { + bytes?: binary; } /** - * WebSocket response data. + * HTTP request data. */ - export interface WebSocketResponse { + export interface Request { /** - * HTTP response status code. + * Request URL (without fragment). */ - status: number; + url: string; /** - * HTTP response status text. + * Fragment of the requested URL starting with hash, if present. */ - statusText: string; + urlFragment?: string; /** - * HTTP response headers. + * HTTP request method. */ - headers: Headers; + method: string; /** - * HTTP response headers text. + * HTTP request headers. */ - headersText?: string; + headers: Headers; /** - * HTTP request headers. + * HTTP POST request data. +Use postDataEntries instead. */ - requestHeaders?: Headers; + postData?: string; /** - * HTTP request headers text. + * True when the request has POST data. Note that postData might still be omitted when this flag is true when the data is too long. */ - requestHeadersText?: string; - } - /** - * WebSocket message data. This represents an entire WebSocket message, not just a fragmented frame as the name suggests. - */ - export interface WebSocketFrame { + hasPostData?: boolean; /** - * WebSocket message opcode. + * Request body elements (post data broken into individual entries). */ - opcode: number; + postDataEntries?: PostDataEntry[]; /** - * WebSocket message mask. + * The mixed content type of the request. */ - mask: boolean; + mixedContentType?: Security.MixedContentType; /** - * WebSocket message payload data. -If the opcode is 1, this is a text message and payloadData is a UTF-8 string. -If the opcode isn't 1, then payloadData is a base64 encoded string representing binary data. + * Priority of the resource request at the time request is sent. */ - payloadData: string; - } - /** - * Information about the cached resource. - */ - export interface CachedResource { + initialPriority: ResourcePriority; /** - * Resource URL. This is the url of the original network request. + * The referrer policy of the request, as defined in https://www.w3.org/TR/referrer-policy/ */ - url: string; + referrerPolicy: "unsafe-url"|"no-referrer-when-downgrade"|"no-referrer"|"origin"|"origin-when-cross-origin"|"same-origin"|"strict-origin"|"strict-origin-when-cross-origin"; /** - * Type of this resource. + * Whether is loaded via link preload. */ - type: ResourceType; + isLinkPreload?: boolean; /** - * Cached response data. + * Set for requests when the TrustToken API is used. Contains the parameters +passed by the developer (e.g. via "fetch") as understood by the backend. */ - response?: Response; + trustTokenParams?: TrustTokenParams; /** - * Cached response body size. + * True if this resource request is considered to be the 'same site' as the +request corresponding to the main frame. */ - bodySize: number; + isSameSite?: boolean; } /** - * Information about the request initiator. + * Details of a signed certificate timestamp (SCT). */ - export interface Initiator { + export interface SignedCertificateTimestamp { /** - * Type of this initiator. + * Validation status. */ - type: "parser"|"script"|"preload"|"SignedExchange"|"preflight"|"other"; + status: string; /** - * Initiator JavaScript stack trace, set for Script only. -Requires the Debugger domain to be enabled. + * Origin. */ - stack?: Runtime.StackTrace; + origin: string; /** - * Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type. + * Log name / description. */ - url?: string; + logDescription: string; /** - * Initiator line number, set for Parser type or for Script type (when script is importing -module) (0-based). + * Log ID. */ - lineNumber?: number; + logId: string; /** - * Initiator column number, set for Parser type or for Script type (when script is importing -module) (0-based). + * Issuance date. Unlike TimeSinceEpoch, this contains the number of +milliseconds since January 1, 1970, UTC, not the number of seconds. */ - columnNumber?: number; + timestamp: number; /** - * Set if another request triggered this request (e.g. preflight). + * Hash algorithm. */ - requestId?: RequestId; - } - /** - * cookiePartitionKey object -The representation of the components of the key that are created by the cookiePartitionKey class contained in net/cookies/cookie_partition_key.h. - */ - export interface CookiePartitionKey { + hashAlgorithm: string; /** - * The site of the top-level URL the browser was visiting at the start -of the request to the endpoint that set the cookie. + * Signature algorithm. */ - topLevelSite: string; + signatureAlgorithm: string; /** - * Indicates if the cookie has any ancestors that are cross-site to the topLevelSite. + * Signature data. */ - hasCrossSiteAncestor: boolean; + signatureData: string; } /** - * Cookie object + * Security details about a request. */ - export interface Cookie { - /** - * Cookie name. - */ - name: string; + export interface SecurityDetails { /** - * Cookie value. + * Protocol name (e.g. "TLS 1.2" or "QUIC"). */ - value: string; + protocol: string; /** - * Cookie domain. + * Key Exchange used by the connection, or the empty string if not applicable. */ - domain: string; + keyExchange: string; /** - * Cookie path. + * (EC)DH group used by the connection, if applicable. */ - path: string; + keyExchangeGroup?: string; /** - * Cookie expiration date as the number of seconds since the UNIX epoch. + * Cipher name. */ - expires: number; + cipher: string; /** - * Cookie size. + * TLS MAC. Note that AEAD ciphers do not have separate MACs. */ - size: number; + mac?: string; /** - * True if cookie is http-only. + * Certificate ID value. */ - httpOnly: boolean; + certificateId: Security.CertificateId; /** - * True if cookie is secure. + * Certificate subject name. */ - secure: boolean; + subjectName: string; /** - * True in case of session cookie. + * Subject Alternative Name (SAN) DNS names and IP addresses. */ - session: boolean; + sanList: string[]; /** - * Cookie SameSite type. + * Name of the issuing CA. */ - sameSite?: CookieSameSite; + issuer: string; /** - * Cookie Priority + * Certificate valid from date. */ - priority: CookiePriority; + validFrom: TimeSinceEpoch; /** - * True if cookie is SameParty. + * Certificate valid to (expiration) date */ - sameParty: boolean; + validTo: TimeSinceEpoch; /** - * Cookie source scheme type. + * List of signed certificate timestamps (SCTs). */ - sourceScheme: CookieSourceScheme; + signedCertificateTimestampList: SignedCertificateTimestamp[]; /** - * Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port. -An unspecified port value allows protocol clients to emulate legacy cookie scope for the port. -This is a temporary ability and it will be removed in the future. + * Whether the request complied with Certificate Transparency policy */ - sourcePort: number; + certificateTransparencyCompliance: CertificateTransparencyCompliance; /** - * Cookie partition key. + * The signature algorithm used by the server in the TLS server signature, +represented as a TLS SignatureScheme code point. Omitted if not +applicable or not known. */ - partitionKey?: CookiePartitionKey; + serverSignatureAlgorithm?: number; /** - * True if cookie partition key is opaque. + * Whether the connection used Encrypted ClientHello */ - partitionKeyOpaque?: boolean; + encryptedClientHello: boolean; } /** - * Types of reasons why a cookie may not be stored from a response. + * Whether the request complied with Certificate Transparency policy. */ - export type SetCookieBlockedReason = "SecureOnly"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"SyntaxError"|"SchemeNotSupported"|"OverwriteSecure"|"InvalidDomain"|"InvalidPrefix"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"SamePartyConflictsWithOtherAttributes"|"NameValuePairExceedsMaxSize"|"DisallowedCharacter"|"NoCookieContent"; + export type CertificateTransparencyCompliance = "unknown"|"not-compliant"|"compliant"; /** - * Types of reasons why a cookie may not be sent with a request. + * The reason why request was blocked. */ - export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize"|"PortMismatch"|"SchemeMismatch"|"AnonymousContext"; + export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"integrity"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"|"sri-message-signature-mismatch"; /** - * Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request. + * Sets Controls for IP Proxy of requests. +Page reload is required before the new behavior will be observed. */ - export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TopLevelTPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"Scheme"|"SameSiteNoneCookiesInSandbox"; + export type IpProxyStatus = "Available"|"FeatureNotEnabled"|"MaskedDomainListNotEnabled"|"MaskedDomainListNotPopulated"|"AuthTokensUnavailable"|"Unavailable"|"BypassedByDevTools"; /** - * A cookie which was not stored from a response with the corresponding reason. + * The reason why request was blocked. */ - export interface BlockedSetCookieWithReason { - /** - * The reason(s) this cookie was blocked. - */ - blockedReasons: SetCookieBlockedReason[]; - /** - * The string representing this individual cookie as it would appear in the header. -This is not the entire "cookie" or "set-cookie" header which could have multiple cookies. + export type CorsError = "DisallowedByMode"|"InvalidResponse"|"WildcardOriginNotAllowed"|"MissingAllowOriginHeader"|"MultipleAllowOriginValues"|"InvalidAllowOriginValue"|"AllowOriginMismatch"|"InvalidAllowCredentials"|"CorsDisabledScheme"|"PreflightInvalidStatus"|"PreflightDisallowedRedirect"|"PreflightWildcardOriginNotAllowed"|"PreflightMissingAllowOriginHeader"|"PreflightMultipleAllowOriginValues"|"PreflightInvalidAllowOriginValue"|"PreflightAllowOriginMismatch"|"PreflightInvalidAllowCredentials"|"PreflightMissingAllowExternal"|"PreflightInvalidAllowExternal"|"PreflightMissingAllowPrivateNetwork"|"PreflightInvalidAllowPrivateNetwork"|"InvalidAllowMethodsPreflightResponse"|"InvalidAllowHeadersPreflightResponse"|"MethodDisallowedByPreflightResponse"|"HeaderDisallowedByPreflightResponse"|"RedirectContainsCredentials"|"InsecurePrivateNetwork"|"InvalidPrivateNetworkAccess"|"UnexpectedPrivateNetworkAccess"|"NoCorsRedirectModeNotFollow"|"PreflightMissingPrivateNetworkAccessId"|"PreflightMissingPrivateNetworkAccessName"|"PrivateNetworkAccessPermissionUnavailable"|"PrivateNetworkAccessPermissionDenied"|"LocalNetworkAccessPermissionDenied"; + export interface CorsErrorStatus { + corsError: CorsError; + failedParameter: string; + } + /** + * Source of serviceworker response. + */ + export type ServiceWorkerResponseSource = "cache-storage"|"http-cache"|"fallback-code"|"network"; + /** + * Determines what type of Trust Token operation is executed and +depending on the type, some additional parameters. The values +are specified in third_party/blink/renderer/core/fetch/trust_token.idl. + */ + export interface TrustTokenParams { + operation: TrustTokenOperationType; + /** + * Only set for "token-redemption" operation and determine whether +to request a fresh SRR or use a still valid cached SRR. */ - cookieLine: string; + refreshPolicy: "UseCached"|"Refresh"; /** - * The cookie object which represents the cookie which was not stored. It is optional because -sometimes complete cookie information is not available, such as in the case of parsing -errors. + * Origins of issuers from whom to request tokens or redemption +records. */ - cookie?: Cookie; + issuers?: string[]; } + export type TrustTokenOperationType = "Issuance"|"Redemption"|"Signing"; /** - * A cookie should have been blocked by 3PCD but is exempted and stored from a response with the -corresponding reason. A cookie could only have at most one exemption reason. + * The reason why Chrome uses a specific transport protocol for HTTP semantics. */ - export interface ExemptedSetCookieWithReason { + export type AlternateProtocolUsage = "alternativeJobWonWithoutRace"|"alternativeJobWonRace"|"mainJobWonRace"|"mappingMissing"|"broken"|"dnsAlpnH3JobWonWithoutRace"|"dnsAlpnH3JobWonRace"|"unspecifiedReason"; + /** + * Source of service worker router. + */ + export type ServiceWorkerRouterSource = "network"|"cache"|"fetch-event"|"race-network-and-fetch-handler"|"race-network-and-cache"; + export interface ServiceWorkerRouterInfo { /** - * The reason the cookie was exempted. + * ID of the rule matched. If there is a matched rule, this field will +be set, otherwiser no value will be set. */ - exemptionReason: CookieExemptionReason; + ruleIdMatched?: number; /** - * The string representing this individual cookie as it would appear in the header. + * The router source of the matched rule. If there is a matched rule, this +field will be set, otherwise no value will be set. */ - cookieLine: string; + matchedSourceType?: ServiceWorkerRouterSource; /** - * The cookie object representing the cookie. + * The actual router source used. */ - cookie: Cookie; + actualSourceType?: ServiceWorkerRouterSource; } /** - * A cookie associated with the request which may or may not be sent with it. -Includes the cookies itself and reasons for blocking or exemption. + * HTTP response data. */ - export interface AssociatedCookie { + export interface Response { /** - * The cookie object representing the cookie which was not sent. + * Response URL. This URL can be different from CachedResource.url in case of redirect. */ - cookie: Cookie; + url: string; /** - * The reason(s) the cookie was blocked. If empty means the cookie is included. + * HTTP response status code. */ - blockedReasons: CookieBlockedReason[]; + status: number; /** - * The reason the cookie should have been blocked by 3PCD but is exempted. A cookie could -only have at most one exemption reason. + * HTTP response status text. */ - exemptionReason?: CookieExemptionReason; - } - /** - * Cookie parameter object - */ - export interface CookieParam { + statusText: string; /** - * Cookie name. + * HTTP response headers. */ - name: string; + headers: Headers; /** - * Cookie value. + * HTTP response headers text. This has been replaced by the headers in Network.responseReceivedExtraInfo. */ - value: string; + headersText?: string; /** - * The request-URI to associate with the setting of the cookie. This value can affect the -default domain, path, source port, and source scheme values of the created cookie. + * Resource mimeType as determined by the browser. */ - url?: string; + mimeType: string; /** - * Cookie domain. + * Resource charset as determined by the browser (if applicable). */ - domain?: string; + charset: string; /** - * Cookie path. + * Refined HTTP request headers that were actually transmitted over the network. */ - path?: string; + requestHeaders?: Headers; /** - * True if cookie is secure. + * HTTP request headers text. This has been replaced by the headers in Network.requestWillBeSentExtraInfo. */ - secure?: boolean; + requestHeadersText?: string; /** - * True if cookie is http-only. + * Specifies whether physical connection was actually reused for this request. */ - httpOnly?: boolean; + connectionReused: boolean; /** - * Cookie SameSite type. + * Physical connection id that was actually used for this request. */ - sameSite?: CookieSameSite; + connectionId: number; /** - * Cookie expiration date, session cookie if not set + * Remote IP address. */ - expires?: TimeSinceEpoch; + remoteIPAddress?: string; /** - * Cookie Priority. + * Remote port. */ - priority?: CookiePriority; + remotePort?: number; /** - * True if cookie is SameParty. + * Specifies that the request was served from the disk cache. */ - sameParty?: boolean; + fromDiskCache?: boolean; /** - * Cookie source scheme type. + * Specifies that the request was served from the ServiceWorker. */ - sourceScheme?: CookieSourceScheme; + fromServiceWorker?: boolean; /** - * Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port. -An unspecified port value allows protocol clients to emulate legacy cookie scope for the port. -This is a temporary ability and it will be removed in the future. + * Specifies that the request was served from the prefetch cache. */ - sourcePort?: number; + fromPrefetchCache?: boolean; /** - * Cookie partition key. If not set, the cookie will be set as not partitioned. + * Specifies that the request was served from the prefetch cache. */ - partitionKey?: CookiePartitionKey; - } - /** - * Authorization challenge for HTTP status code 401 or 407. - */ - export interface AuthChallenge { + fromEarlyHints?: boolean; /** - * Source of the authentication challenge. + * Information about how ServiceWorker Static Router API was used. If this +field is set with `matchedSourceType` field, a matching rule is found. +If this field is set without `matchedSource`, no matching rule is found. +Otherwise, the API is not used. */ - source?: "Server"|"Proxy"; + serviceWorkerRouterInfo?: ServiceWorkerRouterInfo; /** - * Origin of the challenger. + * Total number of bytes received for this request so far. */ - origin: string; + encodedDataLength: number; /** - * The authentication scheme used, such as basic or digest + * Timing information for the given request. */ - scheme: string; + timing?: ResourceTiming; /** - * The realm of the challenge. May be empty. + * Response source of response from ServiceWorker. */ - realm: string; - } - /** - * Response to an AuthChallenge. - */ - export interface AuthChallengeResponse { + serviceWorkerResponseSource?: ServiceWorkerResponseSource; /** - * The decision on what to do in response to the authorization challenge. Default means -deferring to the default behavior of the net stack, which will likely either the Cancel -authentication or display a popup dialog box. + * The time at which the returned response was generated. */ - response: "Default"|"CancelAuth"|"ProvideCredentials"; + responseTime?: TimeSinceEpoch; /** - * The username to provide, possibly empty. Should only be set if response is -ProvideCredentials. + * Cache Storage Cache Name. */ - username?: string; + cacheStorageCacheName?: string; /** - * The password to provide, possibly empty. Should only be set if response is -ProvideCredentials. + * Protocol used to fetch this request. */ - password?: string; - } - /** - * Stages of the interception to begin intercepting. Request will intercept before the request is -sent. Response will intercept after the response is received. - */ - export type InterceptionStage = "Request"|"HeadersReceived"; - /** - * Request pattern for interception. - */ - export interface RequestPattern { + protocol?: string; /** - * Wildcards (`'*'` -> zero or more, `'?'` -> exactly one) are allowed. Escape character is -backslash. Omitting is equivalent to `"*"`. + * The reason why Chrome uses a specific transport protocol for HTTP semantics. */ - urlPattern?: string; + alternateProtocolUsage?: AlternateProtocolUsage; /** - * If set, only requests for matching resource types will be intercepted. + * Security state of the request resource. */ - resourceType?: ResourceType; + securityState: Security.SecurityState; /** - * Stage at which to begin intercepting requests. Default is Request. + * Security details for the request. */ - interceptionStage?: InterceptionStage; + securityDetails?: SecurityDetails; + /** + * Indicates whether the request was sent through IP Protection proxies. If +set to true, the request used the IP Protection privacy feature. + */ + isIpProtectionUsed?: boolean; } /** - * Information about a signed exchange signature. -https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#rfc.section.3.1 + * WebSocket request data. */ - export interface SignedExchangeSignature { + export interface WebSocketRequest { /** - * Signed exchange signature label. + * HTTP request headers. */ - label: string; + headers: Headers; + } + /** + * WebSocket response data. + */ + export interface WebSocketResponse { /** - * The hex string of signed exchange signature. + * HTTP response status code. */ - signature: string; + status: number; /** - * Signed exchange signature integrity. + * HTTP response status text. */ - integrity: string; + statusText: string; /** - * Signed exchange signature cert Url. + * HTTP response headers. */ - certUrl?: string; + headers: Headers; /** - * The hex string of signed exchange signature cert sha256. + * HTTP response headers text. */ - certSha256?: string; + headersText?: string; /** - * Signed exchange signature validity Url. + * HTTP request headers. */ - validityUrl: string; + requestHeaders?: Headers; /** - * Signed exchange signature date. + * HTTP request headers text. */ - date: number; + requestHeadersText?: string; + } + /** + * WebSocket message data. This represents an entire WebSocket message, not just a fragmented frame as the name suggests. + */ + export interface WebSocketFrame { /** - * Signed exchange signature expires. + * WebSocket message opcode. */ - expires: number; + opcode: number; /** - * The encoded certificates. + * WebSocket message mask. */ - certificates?: string[]; + mask: boolean; + /** + * WebSocket message payload data. +If the opcode is 1, this is a text message and payloadData is a UTF-8 string. +If the opcode isn't 1, then payloadData is a base64 encoded string representing binary data. + */ + payloadData: string; } /** - * Information about a signed exchange header. -https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#cbor-representation + * Information about the cached resource. */ - export interface SignedExchangeHeader { - /** - * Signed exchange request URL. - */ - requestUrl: string; + export interface CachedResource { /** - * Signed exchange response code. + * Resource URL. This is the url of the original network request. */ - responseCode: number; + url: string; /** - * Signed exchange response headers. + * Type of this resource. */ - responseHeaders: Headers; + type: ResourceType; /** - * Signed exchange response signature. + * Cached response data. */ - signatures: SignedExchangeSignature[]; + response?: Response; /** - * Signed exchange header integrity hash in the form of `sha256-`. + * Cached response body size. */ - headerIntegrity: string; + bodySize: number; } /** - * Field type for a signed exchange related error. - */ - export type SignedExchangeErrorField = "signatureSig"|"signatureIntegrity"|"signatureCertUrl"|"signatureCertSha256"|"signatureValidityUrl"|"signatureTimestamps"; - /** - * Information about a signed exchange response. + * Information about the request initiator. */ - export interface SignedExchangeError { + export interface Initiator { /** - * Error message. + * Type of this initiator. */ - message: string; + type: "parser"|"script"|"preload"|"SignedExchange"|"preflight"|"other"; /** - * The index of the signature which caused the error. + * Initiator JavaScript stack trace, set for Script only. +Requires the Debugger domain to be enabled. */ - signatureIndex?: number; + stack?: Runtime.StackTrace; /** - * The field which caused the error. + * Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type. */ - errorField?: SignedExchangeErrorField; - } - /** - * Information about a signed exchange response. - */ - export interface SignedExchangeInfo { + url?: string; /** - * The outer response of signed HTTP exchange which was received from network. + * Initiator line number, set for Parser type or for Script type (when script is importing +module) (0-based). */ - outerResponse: Response; + lineNumber?: number; /** - * Whether network response for the signed exchange was accompanied by -extra headers. + * Initiator column number, set for Parser type or for Script type (when script is importing +module) (0-based). */ - hasExtraInfo: boolean; + columnNumber?: number; /** - * Information about the signed exchange header. + * Set if another request triggered this request (e.g. preflight). */ - header?: SignedExchangeHeader; + requestId?: RequestId; + } + /** + * cookiePartitionKey object +The representation of the components of the key that are created by the cookiePartitionKey class contained in net/cookies/cookie_partition_key.h. + */ + export interface CookiePartitionKey { /** - * Security details for the signed exchange header. + * The site of the top-level URL the browser was visiting at the start +of the request to the endpoint that set the cookie. */ - securityDetails?: SecurityDetails; + topLevelSite: string; /** - * Errors occurred while handling the signed exchange. + * Indicates if the cookie has any ancestors that are cross-site to the topLevelSite. */ - errors?: SignedExchangeError[]; + hasCrossSiteAncestor: boolean; } /** - * List of content encodings supported by the backend. + * Cookie object */ - export type ContentEncoding = "deflate"|"gzip"|"br"|"zstd"; - export type DirectSocketDnsQueryType = "ipv4"|"ipv6"; - export interface DirectTCPSocketOptions { + export interface Cookie { /** - * TCP_NODELAY option + * Cookie name. */ - noDelay: boolean; + name: string; /** - * Expected to be unsigned integer. + * Cookie value. */ - keepAliveDelay?: number; + value: string; /** - * Expected to be unsigned integer. + * Cookie domain. */ - sendBufferSize?: number; + domain: string; /** - * Expected to be unsigned integer. + * Cookie path. */ - receiveBufferSize?: number; - dnsQueryType?: DirectSocketDnsQueryType; - } - export interface DirectUDPSocketOptions { - remoteAddr?: string; + path: string; /** - * Unsigned int 16. + * Cookie expiration date as the number of seconds since the UNIX epoch. */ - remotePort?: number; - localAddr?: string; + expires: number; /** - * Unsigned int 16. + * Cookie size. */ - localPort?: number; - dnsQueryType?: DirectSocketDnsQueryType; + size: number; /** - * Expected to be unsigned integer. + * True if cookie is http-only. */ - sendBufferSize?: number; + httpOnly: boolean; /** - * Expected to be unsigned integer. + * True if cookie is secure. */ - receiveBufferSize?: number; - } - export interface DirectUDPMessage { - data: binary; + secure: boolean; /** - * Null for connected mode. + * True in case of session cookie. */ - remoteAddr?: string; + session: boolean; /** - * Null for connected mode. -Expected to be unsigned integer. + * Cookie SameSite type. */ - remotePort?: number; - } - export type PrivateNetworkRequestPolicy = "Allow"|"BlockFromInsecureToMorePrivate"|"WarnFromInsecureToMorePrivate"|"PreflightBlock"|"PreflightWarn"|"PermissionBlock"|"PermissionWarn"; - export type IPAddressSpace = "Loopback"|"Local"|"Public"|"Unknown"; - export interface ConnectTiming { + sameSite?: CookieSameSite; /** - * Timing's requestTime is a baseline in seconds, while the other numbers are ticks in -milliseconds relatively to this requestTime. Matches ResourceTiming's requestTime for -the same request (but not for redirected requests). + * Cookie Priority */ - requestTime: number; - } - export interface ClientSecurityState { - initiatorIsSecureContext: boolean; - initiatorIPAddressSpace: IPAddressSpace; - privateNetworkRequestPolicy: PrivateNetworkRequestPolicy; - } - export type CrossOriginOpenerPolicyValue = "SameOrigin"|"SameOriginAllowPopups"|"RestrictProperties"|"UnsafeNone"|"SameOriginPlusCoep"|"RestrictPropertiesPlusCoep"|"NoopenerAllowPopups"; - export interface CrossOriginOpenerPolicyStatus { - value: CrossOriginOpenerPolicyValue; - reportOnlyValue: CrossOriginOpenerPolicyValue; - reportingEndpoint?: string; - reportOnlyReportingEndpoint?: string; - } - export type CrossOriginEmbedderPolicyValue = "None"|"Credentialless"|"RequireCorp"; - export interface CrossOriginEmbedderPolicyStatus { - value: CrossOriginEmbedderPolicyValue; - reportOnlyValue: CrossOriginEmbedderPolicyValue; - reportingEndpoint?: string; - reportOnlyReportingEndpoint?: string; - } - export type ContentSecurityPolicySource = "HTTP"|"Meta"; - export interface ContentSecurityPolicyStatus { - effectiveDirectives: string; - isEnforced: boolean; - source: ContentSecurityPolicySource; - } - export interface SecurityIsolationStatus { - coop?: CrossOriginOpenerPolicyStatus; - coep?: CrossOriginEmbedderPolicyStatus; - csp?: ContentSecurityPolicyStatus[]; - } - /** - * The status of a Reporting API report. - */ - export type ReportStatus = "Queued"|"Pending"|"MarkedForRemoval"|"Success"; - export type ReportId = string; - /** - * An object representing a report generated by the Reporting API. - */ - export interface ReportingApiReport { - id: ReportId; + priority: CookiePriority; /** - * The URL of the document that triggered the report. + * True if cookie is SameParty. */ - initiatorUrl: string; + sameParty: boolean; /** - * The name of the endpoint group that should be used to deliver the report. + * Cookie source scheme type. */ - destination: string; + sourceScheme: CookieSourceScheme; /** - * The type of the report (specifies the set of data that is contained in the report body). + * Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port. +An unspecified port value allows protocol clients to emulate legacy cookie scope for the port. +This is a temporary ability and it will be removed in the future. */ - type: string; + sourcePort: number; /** - * When the report was generated. + * Cookie partition key. */ - timestamp: Network.TimeSinceEpoch; + partitionKey?: CookiePartitionKey; /** - * How many uploads deep the related request was. + * True if cookie partition key is opaque. */ - depth: number; + partitionKeyOpaque?: boolean; + } + /** + * Types of reasons why a cookie may not be stored from a response. + */ + export type SetCookieBlockedReason = "SecureOnly"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"SyntaxError"|"SchemeNotSupported"|"OverwriteSecure"|"InvalidDomain"|"InvalidPrefix"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"SamePartyConflictsWithOtherAttributes"|"NameValuePairExceedsMaxSize"|"DisallowedCharacter"|"NoCookieContent"; + /** + * Types of reasons why a cookie may not be sent with a request. + */ + export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize"|"PortMismatch"|"SchemeMismatch"|"AnonymousContext"; + /** + * Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request. + */ + export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TopLevelTPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"Scheme"|"SameSiteNoneCookiesInSandbox"; + /** + * A cookie which was not stored from a response with the corresponding reason. + */ + export interface BlockedSetCookieWithReason { /** - * The number of delivery attempts made so far, not including an active attempt. + * The reason(s) this cookie was blocked. */ - completedAttempts: number; - body: { [key: string]: string }; - status: ReportStatus; - } - export interface ReportingApiEndpoint { + blockedReasons: SetCookieBlockedReason[]; /** - * The URL of the endpoint to which reports may be delivered. + * The string representing this individual cookie as it would appear in the header. +This is not the entire "cookie" or "set-cookie" header which could have multiple cookies. */ - url: string; + cookieLine: string; /** - * Name of the endpoint group. + * The cookie object which represents the cookie which was not stored. It is optional because +sometimes complete cookie information is not available, such as in the case of parsing +errors. */ - groupName: string; + cookie?: Cookie; } /** - * An object providing the result of a network resource load. + * A cookie should have been blocked by 3PCD but is exempted and stored from a response with the +corresponding reason. A cookie could only have at most one exemption reason. */ - export interface LoadNetworkResourcePageResult { - success: boolean; + export interface ExemptedSetCookieWithReason { /** - * Optional values used for error reporting. + * The reason the cookie was exempted. */ - netError?: number; - netErrorName?: string; - httpStatusCode?: number; + exemptionReason: CookieExemptionReason; /** - * If successful, one of the following two fields holds the result. + * The string representing this individual cookie as it would appear in the header. */ - stream?: IO.StreamHandle; + cookieLine: string; /** - * Response headers. + * The cookie object representing the cookie. */ - headers?: Network.Headers; + cookie: Cookie; } /** - * An options object that may be extended later to better support CORS, -CORB and streaming. + * A cookie associated with the request which may or may not be sent with it. +Includes the cookies itself and reasons for blocking or exemption. */ - export interface LoadNetworkResourceOptions { - disableCache: boolean; - includeCredentials: boolean; + export interface AssociatedCookie { + /** + * The cookie object representing the cookie which was not sent. + */ + cookie: Cookie; + /** + * The reason(s) the cookie was blocked. If empty means the cookie is included. + */ + blockedReasons: CookieBlockedReason[]; + /** + * The reason the cookie should have been blocked by 3PCD but is exempted. A cookie could +only have at most one exemption reason. + */ + exemptionReason?: CookieExemptionReason; } - /** - * Fired when data chunk was received over the network. + * Cookie parameter object */ - export type dataReceivedPayload = { + export interface CookieParam { /** - * Request identifier. + * Cookie name. */ - requestId: RequestId; + name: string; /** - * Timestamp. + * Cookie value. */ - timestamp: MonotonicTime; + value: string; /** - * Data chunk length. + * The request-URI to associate with the setting of the cookie. This value can affect the +default domain, path, source port, and source scheme values of the created cookie. */ - dataLength: number; + url?: string; /** - * Actual bytes received (might be less than dataLength for compressed encodings). + * Cookie domain. */ - encodedDataLength: number; + domain?: string; /** - * Data that was received. + * Cookie path. */ - data?: binary; - } - /** - * Fired when EventSource message is received. - */ - export type eventSourceMessageReceivedPayload = { + path?: string; /** - * Request identifier. + * True if cookie is secure. */ - requestId: RequestId; + secure?: boolean; /** - * Timestamp. + * True if cookie is http-only. */ - timestamp: MonotonicTime; + httpOnly?: boolean; /** - * Message type. + * Cookie SameSite type. */ - eventName: string; + sameSite?: CookieSameSite; /** - * Message identifier. + * Cookie expiration date, session cookie if not set */ - eventId: string; + expires?: TimeSinceEpoch; /** - * Message content. + * Cookie Priority. */ - data: string; - } - /** - * Fired when HTTP request has failed to load. - */ - export type loadingFailedPayload = { + priority?: CookiePriority; /** - * Request identifier. + * True if cookie is SameParty. */ - requestId: RequestId; + sameParty?: boolean; /** - * Timestamp. + * Cookie source scheme type. */ - timestamp: MonotonicTime; + sourceScheme?: CookieSourceScheme; /** - * Resource type. + * Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port. +An unspecified port value allows protocol clients to emulate legacy cookie scope for the port. +This is a temporary ability and it will be removed in the future. */ - type: ResourceType; + sourcePort?: number; /** - * Error message. List of network errors: https://cs.chromium.org/chromium/src/net/base/net_error_list.h + * Cookie partition key. If not set, the cookie will be set as not partitioned. */ - errorText: string; + partitionKey?: CookiePartitionKey; + } + /** + * Authorization challenge for HTTP status code 401 or 407. + */ + export interface AuthChallenge { /** - * True if loading was canceled. + * Source of the authentication challenge. */ - canceled?: boolean; + source?: "Server"|"Proxy"; /** - * The reason why loading was blocked, if any. + * Origin of the challenger. */ - blockedReason?: BlockedReason; + origin: string; /** - * The reason why loading was blocked by CORS, if any. + * The authentication scheme used, such as basic or digest */ - corsErrorStatus?: CorsErrorStatus; + scheme: string; + /** + * The realm of the challenge. May be empty. + */ + realm: string; } /** - * Fired when HTTP request has finished loading. + * Response to an AuthChallenge. */ - export type loadingFinishedPayload = { + export interface AuthChallengeResponse { /** - * Request identifier. + * The decision on what to do in response to the authorization challenge. Default means +deferring to the default behavior of the net stack, which will likely either the Cancel +authentication or display a popup dialog box. */ - requestId: RequestId; + response: "Default"|"CancelAuth"|"ProvideCredentials"; /** - * Timestamp. + * The username to provide, possibly empty. Should only be set if response is +ProvideCredentials. */ - timestamp: MonotonicTime; + username?: string; /** - * Total number of bytes received for this request. + * The password to provide, possibly empty. Should only be set if response is +ProvideCredentials. */ - encodedDataLength: number; + password?: string; } /** - * Details of an intercepted HTTP request, which must be either allowed, blocked, modified or -mocked. -Deprecated, use Fetch.requestPaused instead. + * Stages of the interception to begin intercepting. Request will intercept before the request is +sent. Response will intercept after the response is received. */ - export type requestInterceptedPayload = { + export type InterceptionStage = "Request"|"HeadersReceived"; + /** + * Request pattern for interception. + */ + export interface RequestPattern { /** - * Each request the page makes will have a unique id, however if any redirects are encountered -while processing that fetch, they will be reported with the same id as the original fetch. -Likewise if HTTP authentication is needed then the same fetch id will be used. + * Wildcards (`'*'` -> zero or more, `'?'` -> exactly one) are allowed. Escape character is +backslash. Omitting is equivalent to `"*"`. */ - interceptionId: InterceptionId; - request: Request; + urlPattern?: string; /** - * The id of the frame that initiated the request. + * If set, only requests for matching resource types will be intercepted. */ - frameId: Page.FrameId; + resourceType?: ResourceType; /** - * How the requested resource will be used. + * Stage at which to begin intercepting requests. Default is Request. */ - resourceType: ResourceType; + interceptionStage?: InterceptionStage; + } + /** + * Information about a signed exchange signature. +https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#rfc.section.3.1 + */ + export interface SignedExchangeSignature { /** - * Whether this is a navigation request, which can abort the navigation completely. + * Signed exchange signature label. */ - isNavigationRequest: boolean; + label: string; /** - * Set if the request is a navigation that will result in a download. -Only present after response is received from the server (i.e. HeadersReceived stage). + * The hex string of signed exchange signature. */ - isDownload?: boolean; + signature: string; /** - * Redirect location, only sent if a redirect was intercepted. + * Signed exchange signature integrity. */ - redirectUrl?: string; + integrity: string; /** - * Details of the Authorization Challenge encountered. If this is set then -continueInterceptedRequest must contain an authChallengeResponse. + * Signed exchange signature cert Url. */ - authChallenge?: AuthChallenge; + certUrl?: string; /** - * Response error if intercepted at response stage or if redirect occurred while intercepting -request. + * The hex string of signed exchange signature cert sha256. */ - responseErrorReason?: ErrorReason; + certSha256?: string; /** - * Response code if intercepted at response stage or if redirect occurred while intercepting -request or auth retry occurred. + * Signed exchange signature validity Url. */ - responseStatusCode?: number; + validityUrl: string; /** - * Response headers if intercepted at the response stage or if redirect occurred while -intercepting request or auth retry occurred. + * Signed exchange signature date. */ - responseHeaders?: Headers; + date: number; /** - * If the intercepted request had a corresponding requestWillBeSent event fired for it, then -this requestId will be the same as the requestId present in the requestWillBeSent event. + * Signed exchange signature expires. */ - requestId?: RequestId; - } - /** - * Fired if request ended up loading from cache. - */ - export type requestServedFromCachePayload = { + expires: number; /** - * Request identifier. + * The encoded certificates. */ - requestId: RequestId; + certificates?: string[]; } /** - * Fired when page is about to send HTTP request. + * Information about a signed exchange header. +https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#cbor-representation */ - export type requestWillBeSentPayload = { + export interface SignedExchangeHeader { /** - * Request identifier. + * Signed exchange request URL. */ - requestId: RequestId; + requestUrl: string; /** - * Loader identifier. Empty string if the request is fetched from worker. + * Signed exchange response code. */ - loaderId: LoaderId; + responseCode: number; /** - * URL of the document this request is loaded for. + * Signed exchange response headers. */ - documentURL: string; + responseHeaders: Headers; /** - * Request data. + * Signed exchange response signature. */ - request: Request; + signatures: SignedExchangeSignature[]; /** - * Timestamp. + * Signed exchange header integrity hash in the form of `sha256-`. */ - timestamp: MonotonicTime; + headerIntegrity: string; + } + /** + * Field type for a signed exchange related error. + */ + export type SignedExchangeErrorField = "signatureSig"|"signatureIntegrity"|"signatureCertUrl"|"signatureCertSha256"|"signatureValidityUrl"|"signatureTimestamps"; + /** + * Information about a signed exchange response. + */ + export interface SignedExchangeError { /** - * Timestamp. + * Error message. */ - wallTime: TimeSinceEpoch; + message: string; /** - * Request initiator. + * The index of the signature which caused the error. */ - initiator: Initiator; + signatureIndex?: number; /** - * In the case that redirectResponse is populated, this flag indicates whether -requestWillBeSentExtraInfo and responseReceivedExtraInfo events will be or were emitted -for the request which was just redirected. + * The field which caused the error. */ - redirectHasExtraInfo: boolean; + errorField?: SignedExchangeErrorField; + } + /** + * Information about a signed exchange response. + */ + export interface SignedExchangeInfo { /** - * Redirect response data. + * The outer response of signed HTTP exchange which was received from network. */ - redirectResponse?: Response; + outerResponse: Response; /** - * Type of this resource. + * Whether network response for the signed exchange was accompanied by +extra headers. */ - type?: ResourceType; + hasExtraInfo: boolean; /** - * Frame identifier. + * Information about the signed exchange header. */ - frameId?: Page.FrameId; + header?: SignedExchangeHeader; /** - * Whether the request is initiated by a user gesture. Defaults to false. + * Security details for the signed exchange header. */ - hasUserGesture?: boolean; + securityDetails?: SecurityDetails; + /** + * Errors occurred while handling the signed exchange. + */ + errors?: SignedExchangeError[]; } /** - * Fired when resource loading priority is changed + * List of content encodings supported by the backend. */ - export type resourceChangedPriorityPayload = { + export type ContentEncoding = "deflate"|"gzip"|"br"|"zstd"; + export type DirectSocketDnsQueryType = "ipv4"|"ipv6"; + export interface DirectTCPSocketOptions { /** - * Request identifier. + * TCP_NODELAY option */ - requestId: RequestId; + noDelay: boolean; /** - * New priority + * Expected to be unsigned integer. */ - newPriority: ResourcePriority; + keepAliveDelay?: number; /** - * Timestamp. + * Expected to be unsigned integer. */ - timestamp: MonotonicTime; + sendBufferSize?: number; + /** + * Expected to be unsigned integer. + */ + receiveBufferSize?: number; + dnsQueryType?: DirectSocketDnsQueryType; } - /** - * Fired when a signed exchange was received over the network - */ - export type signedExchangeReceivedPayload = { + export interface DirectUDPSocketOptions { + remoteAddr?: string; /** - * Request identifier. + * Unsigned int 16. */ - requestId: RequestId; + remotePort?: number; + localAddr?: string; /** - * Information about the signed exchange response. + * Unsigned int 16. */ - info: SignedExchangeInfo; + localPort?: number; + dnsQueryType?: DirectSocketDnsQueryType; + /** + * Expected to be unsigned integer. + */ + sendBufferSize?: number; + /** + * Expected to be unsigned integer. + */ + receiveBufferSize?: number; } - /** - * Fired when HTTP response is available. - */ - export type responseReceivedPayload = { + export interface DirectUDPMessage { + data: binary; /** - * Request identifier. + * Null for connected mode. */ - requestId: RequestId; + remoteAddr?: string; /** - * Loader identifier. Empty string if the request is fetched from worker. + * Null for connected mode. +Expected to be unsigned integer. */ - loaderId: LoaderId; + remotePort?: number; + } + export type PrivateNetworkRequestPolicy = "Allow"|"BlockFromInsecureToMorePrivate"|"WarnFromInsecureToMorePrivate"|"PreflightBlock"|"PreflightWarn"|"PermissionBlock"|"PermissionWarn"; + export type IPAddressSpace = "Loopback"|"Local"|"Public"|"Unknown"; + export interface ConnectTiming { /** - * Timestamp. + * Timing's requestTime is a baseline in seconds, while the other numbers are ticks in +milliseconds relatively to this requestTime. Matches ResourceTiming's requestTime for +the same request (but not for redirected requests). */ - timestamp: MonotonicTime; + requestTime: number; + } + export interface ClientSecurityState { + initiatorIsSecureContext: boolean; + initiatorIPAddressSpace: IPAddressSpace; + privateNetworkRequestPolicy: PrivateNetworkRequestPolicy; + } + export type CrossOriginOpenerPolicyValue = "SameOrigin"|"SameOriginAllowPopups"|"RestrictProperties"|"UnsafeNone"|"SameOriginPlusCoep"|"RestrictPropertiesPlusCoep"|"NoopenerAllowPopups"; + export interface CrossOriginOpenerPolicyStatus { + value: CrossOriginOpenerPolicyValue; + reportOnlyValue: CrossOriginOpenerPolicyValue; + reportingEndpoint?: string; + reportOnlyReportingEndpoint?: string; + } + export type CrossOriginEmbedderPolicyValue = "None"|"Credentialless"|"RequireCorp"; + export interface CrossOriginEmbedderPolicyStatus { + value: CrossOriginEmbedderPolicyValue; + reportOnlyValue: CrossOriginEmbedderPolicyValue; + reportingEndpoint?: string; + reportOnlyReportingEndpoint?: string; + } + export type ContentSecurityPolicySource = "HTTP"|"Meta"; + export interface ContentSecurityPolicyStatus { + effectiveDirectives: string; + isEnforced: boolean; + source: ContentSecurityPolicySource; + } + export interface SecurityIsolationStatus { + coop?: CrossOriginOpenerPolicyStatus; + coep?: CrossOriginEmbedderPolicyStatus; + csp?: ContentSecurityPolicyStatus[]; + } + /** + * The status of a Reporting API report. + */ + export type ReportStatus = "Queued"|"Pending"|"MarkedForRemoval"|"Success"; + export type ReportId = string; + /** + * An object representing a report generated by the Reporting API. + */ + export interface ReportingApiReport { + id: ReportId; /** - * Resource type. + * The URL of the document that triggered the report. */ - type: ResourceType; + initiatorUrl: string; /** - * Response data. + * The name of the endpoint group that should be used to deliver the report. */ - response: Response; + destination: string; /** - * Indicates whether requestWillBeSentExtraInfo and responseReceivedExtraInfo events will be -or were emitted for this request. + * The type of the report (specifies the set of data that is contained in the report body). */ - hasExtraInfo: boolean; + type: string; /** - * Frame identifier. + * When the report was generated. */ - frameId?: Page.FrameId; + timestamp: Network.TimeSinceEpoch; + /** + * How many uploads deep the related request was. + */ + depth: number; + /** + * The number of delivery attempts made so far, not including an active attempt. + */ + completedAttempts: number; + body: { [key: string]: string }; + status: ReportStatus; } - /** - * Fired when WebSocket is closed. - */ - export type webSocketClosedPayload = { + export interface ReportingApiEndpoint { /** - * Request identifier. + * The URL of the endpoint to which reports may be delivered. */ - requestId: RequestId; + url: string; /** - * Timestamp. + * Name of the endpoint group. */ - timestamp: MonotonicTime; + groupName: string; } /** - * Fired upon WebSocket creation. + * An object providing the result of a network resource load. */ - export type webSocketCreatedPayload = { + export interface LoadNetworkResourcePageResult { + success: boolean; /** - * Request identifier. + * Optional values used for error reporting. */ - requestId: RequestId; + netError?: number; + netErrorName?: string; + httpStatusCode?: number; /** - * WebSocket request URL. + * If successful, one of the following two fields holds the result. */ - url: string; + stream?: IO.StreamHandle; /** - * Request initiator. + * Response headers. */ - initiator?: Initiator; + headers?: Network.Headers; } /** - * Fired when WebSocket message error occurs. + * An options object that may be extended later to better support CORS, +CORB and streaming. */ - export type webSocketFrameErrorPayload = { + export interface LoadNetworkResourceOptions { + disableCache: boolean; + includeCredentials: boolean; + } + + /** + * Fired when data chunk was received over the network. + */ + export type dataReceivedPayload = { /** * Request identifier. */ @@ -10220,14 +11046,22 @@ or were emitted for this request. */ timestamp: MonotonicTime; /** - * WebSocket error message. + * Data chunk length. */ - errorMessage: string; + dataLength: number; + /** + * Actual bytes received (might be less than dataLength for compressed encodings). + */ + encodedDataLength: number; + /** + * Data that was received. + */ + data?: binary; } /** - * Fired when WebSocket message is received. + * Fired when EventSource message is received. */ - export type webSocketFrameReceivedPayload = { + export type eventSourceMessageReceivedPayload = { /** * Request identifier. */ @@ -10237,14 +11071,22 @@ or were emitted for this request. */ timestamp: MonotonicTime; /** - * WebSocket response data. + * Message type. */ - response: WebSocketFrame; + eventName: string; + /** + * Message identifier. + */ + eventId: string; + /** + * Message content. + */ + data: string; } /** - * Fired when WebSocket message is sent. + * Fired when HTTP request has failed to load. */ - export type webSocketFrameSentPayload = { + export type loadingFailedPayload = { /** * Request identifier. */ @@ -10254,14 +11096,30 @@ or were emitted for this request. */ timestamp: MonotonicTime; /** - * WebSocket response data. + * Resource type. */ - response: WebSocketFrame; + type: ResourceType; + /** + * Error message. List of network errors: https://cs.chromium.org/chromium/src/net/base/net_error_list.h + */ + errorText: string; + /** + * True if loading was canceled. + */ + canceled?: boolean; + /** + * The reason why loading was blocked, if any. + */ + blockedReason?: BlockedReason; + /** + * The reason why loading was blocked by CORS, if any. + */ + corsErrorStatus?: CorsErrorStatus; } /** - * Fired when WebSocket handshake response becomes available. + * Fired when HTTP request has finished loading. */ - export type webSocketHandshakeResponseReceivedPayload = { + export type loadingFinishedPayload = { /** * Request identifier. */ @@ -10271,111 +11129,397 @@ or were emitted for this request. */ timestamp: MonotonicTime; /** - * WebSocket response data. + * Total number of bytes received for this request. */ - response: WebSocketResponse; + encodedDataLength: number; } /** - * Fired when WebSocket is about to initiate handshake. + * Details of an intercepted HTTP request, which must be either allowed, blocked, modified or +mocked. +Deprecated, use Fetch.requestPaused instead. */ - export type webSocketWillSendHandshakeRequestPayload = { + export type requestInterceptedPayload = { + /** + * Each request the page makes will have a unique id, however if any redirects are encountered +while processing that fetch, they will be reported with the same id as the original fetch. +Likewise if HTTP authentication is needed then the same fetch id will be used. + */ + interceptionId: InterceptionId; + request: Request; + /** + * The id of the frame that initiated the request. + */ + frameId: Page.FrameId; + /** + * How the requested resource will be used. + */ + resourceType: ResourceType; + /** + * Whether this is a navigation request, which can abort the navigation completely. + */ + isNavigationRequest: boolean; + /** + * Set if the request is a navigation that will result in a download. +Only present after response is received from the server (i.e. HeadersReceived stage). + */ + isDownload?: boolean; + /** + * Redirect location, only sent if a redirect was intercepted. + */ + redirectUrl?: string; + /** + * Details of the Authorization Challenge encountered. If this is set then +continueInterceptedRequest must contain an authChallengeResponse. + */ + authChallenge?: AuthChallenge; + /** + * Response error if intercepted at response stage or if redirect occurred while intercepting +request. + */ + responseErrorReason?: ErrorReason; + /** + * Response code if intercepted at response stage or if redirect occurred while intercepting +request or auth retry occurred. + */ + responseStatusCode?: number; + /** + * Response headers if intercepted at the response stage or if redirect occurred while +intercepting request or auth retry occurred. + */ + responseHeaders?: Headers; + /** + * If the intercepted request had a corresponding requestWillBeSent event fired for it, then +this requestId will be the same as the requestId present in the requestWillBeSent event. + */ + requestId?: RequestId; + } + /** + * Fired if request ended up loading from cache. + */ + export type requestServedFromCachePayload = { + /** + * Request identifier. + */ + requestId: RequestId; + } + /** + * Fired when page is about to send HTTP request. + */ + export type requestWillBeSentPayload = { /** * Request identifier. */ requestId: RequestId; + /** + * Loader identifier. Empty string if the request is fetched from worker. + */ + loaderId: LoaderId; + /** + * URL of the document this request is loaded for. + */ + documentURL: string; + /** + * Request data. + */ + request: Request; /** * Timestamp. */ timestamp: MonotonicTime; /** - * UTC Timestamp. + * Timestamp. */ wallTime: TimeSinceEpoch; /** - * WebSocket request data. + * Request initiator. */ - request: WebSocketRequest; + initiator: Initiator; + /** + * In the case that redirectResponse is populated, this flag indicates whether +requestWillBeSentExtraInfo and responseReceivedExtraInfo events will be or were emitted +for the request which was just redirected. + */ + redirectHasExtraInfo: boolean; + /** + * Redirect response data. + */ + redirectResponse?: Response; + /** + * Type of this resource. + */ + type?: ResourceType; + /** + * Frame identifier. + */ + frameId?: Page.FrameId; + /** + * Whether the request is initiated by a user gesture. Defaults to false. + */ + hasUserGesture?: boolean; } /** - * Fired upon WebTransport creation. + * Fired when resource loading priority is changed */ - export type webTransportCreatedPayload = { + export type resourceChangedPriorityPayload = { /** - * WebTransport identifier. + * Request identifier. */ - transportId: RequestId; + requestId: RequestId; /** - * WebTransport request URL. + * New priority */ - url: string; + newPriority: ResourcePriority; /** * Timestamp. */ timestamp: MonotonicTime; + } + /** + * Fired when a signed exchange was received over the network + */ + export type signedExchangeReceivedPayload = { /** - * Request initiator. + * Request identifier. */ - initiator?: Initiator; + requestId: RequestId; + /** + * Information about the signed exchange response. + */ + info: SignedExchangeInfo; } /** - * Fired when WebTransport handshake is finished. + * Fired when HTTP response is available. */ - export type webTransportConnectionEstablishedPayload = { + export type responseReceivedPayload = { /** - * WebTransport identifier. + * Request identifier. */ - transportId: RequestId; + requestId: RequestId; + /** + * Loader identifier. Empty string if the request is fetched from worker. + */ + loaderId: LoaderId; /** * Timestamp. */ timestamp: MonotonicTime; + /** + * Resource type. + */ + type: ResourceType; + /** + * Response data. + */ + response: Response; + /** + * Indicates whether requestWillBeSentExtraInfo and responseReceivedExtraInfo events will be +or were emitted for this request. + */ + hasExtraInfo: boolean; + /** + * Frame identifier. + */ + frameId?: Page.FrameId; } /** - * Fired when WebTransport is disposed. + * Fired when WebSocket is closed. */ - export type webTransportClosedPayload = { + export type webSocketClosedPayload = { /** - * WebTransport identifier. + * Request identifier. */ - transportId: RequestId; + requestId: RequestId; /** * Timestamp. */ timestamp: MonotonicTime; } /** - * Fired upon direct_socket.TCPSocket creation. + * Fired upon WebSocket creation. */ - export type directTCPSocketCreatedPayload = { - identifier: RequestId; - remoteAddr: string; + export type webSocketCreatedPayload = { /** - * Unsigned int 16. + * Request identifier. + */ + requestId: RequestId; + /** + * WebSocket request URL. + */ + url: string; + /** + * Request initiator. */ - remotePort: number; - options: DirectTCPSocketOptions; - timestamp: MonotonicTime; initiator?: Initiator; } /** - * Fired when direct_socket.TCPSocket connection is opened. + * Fired when WebSocket message error occurs. */ - export type directTCPSocketOpenedPayload = { - identifier: RequestId; - remoteAddr: string; + export type webSocketFrameErrorPayload = { /** - * Expected to be unsigned integer. + * Request identifier. + */ + requestId: RequestId; + /** + * Timestamp. */ - remotePort: number; timestamp: MonotonicTime; - localAddr?: string; /** - * Expected to be unsigned integer. + * WebSocket error message. */ - localPort?: number; + errorMessage: string; } /** - * Fired when direct_socket.TCPSocket is aborted. + * Fired when WebSocket message is received. + */ + export type webSocketFrameReceivedPayload = { + /** + * Request identifier. + */ + requestId: RequestId; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + /** + * WebSocket response data. + */ + response: WebSocketFrame; + } + /** + * Fired when WebSocket message is sent. + */ + export type webSocketFrameSentPayload = { + /** + * Request identifier. + */ + requestId: RequestId; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + /** + * WebSocket response data. + */ + response: WebSocketFrame; + } + /** + * Fired when WebSocket handshake response becomes available. + */ + export type webSocketHandshakeResponseReceivedPayload = { + /** + * Request identifier. + */ + requestId: RequestId; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + /** + * WebSocket response data. + */ + response: WebSocketResponse; + } + /** + * Fired when WebSocket is about to initiate handshake. + */ + export type webSocketWillSendHandshakeRequestPayload = { + /** + * Request identifier. + */ + requestId: RequestId; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + /** + * UTC Timestamp. + */ + wallTime: TimeSinceEpoch; + /** + * WebSocket request data. + */ + request: WebSocketRequest; + } + /** + * Fired upon WebTransport creation. + */ + export type webTransportCreatedPayload = { + /** + * WebTransport identifier. + */ + transportId: RequestId; + /** + * WebTransport request URL. + */ + url: string; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + /** + * Request initiator. + */ + initiator?: Initiator; + } + /** + * Fired when WebTransport handshake is finished. + */ + export type webTransportConnectionEstablishedPayload = { + /** + * WebTransport identifier. + */ + transportId: RequestId; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + } + /** + * Fired when WebTransport is disposed. + */ + export type webTransportClosedPayload = { + /** + * WebTransport identifier. + */ + transportId: RequestId; + /** + * Timestamp. + */ + timestamp: MonotonicTime; + } + /** + * Fired upon direct_socket.TCPSocket creation. + */ + export type directTCPSocketCreatedPayload = { + identifier: RequestId; + remoteAddr: string; + /** + * Unsigned int 16. + */ + remotePort: number; + options: DirectTCPSocketOptions; + timestamp: MonotonicTime; + initiator?: Initiator; + } + /** + * Fired when direct_socket.TCPSocket connection is opened. + */ + export type directTCPSocketOpenedPayload = { + identifier: RequestId; + remoteAddr: string; + /** + * Expected to be unsigned integer. + */ + remotePort: number; + timestamp: MonotonicTime; + localAddr?: string; + /** + * Expected to be unsigned integer. + */ + localPort?: number; + } + /** + * Fired when direct_socket.TCPSocket is aborted. */ export type directTCPSocketAbortedPayload = { identifier: RequestId; @@ -10688,6 +11832,18 @@ And after 'enableReportingApi' for all existing reports. endpoints: ReportingApiEndpoint[]; } + /** + * Returns enum representing if IP Proxy of requests is available +or reason it is not active. + */ + export type getIPProtectionProxyStatusParameters = { + } + export type getIPProtectionProxyStatusReturnValue = { + /** + * Whether IP proxy is available + */ + status: IpProxyStatus; + } /** * Sets a list of content encodings that will be accepted. Empty list means no encoding is accepted. */ @@ -10894,6 +12050,12 @@ all partition key attributes match the cookie partition key attribute. * Whether DirectSocket chunk send/receive events should be reported. */ reportDirectSocketTraffic?: boolean; + /** + * Enable storing response bodies outside of renderer, so that these survive +a cross-process navigation. Requires maxTotalBufferSize to be set. +Currently defaults to false. + */ + enableDurableMessages?: boolean; } export type enableReturnValue = { } @@ -11879,6 +13041,9 @@ objectId must be specified. } /** * Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport. +Issue: the method does not handle device pixel ratio (DPR) correctly. +The coordinates currently have to be adjusted by the client +if DPR is not 1 (see crbug.com/437807128). */ export type highlightRectParameters = { /** @@ -12124,53 +13289,223 @@ Backend then generates 'inspectNodeRequested' event upon element selection. } /** - * Actions and events related to the inspected page belong to the page domain. + * This domain allows interacting with the browser to control PWAs. */ - export module Page { + export module PWA { /** - * Unique frame identifier. + * The following types are the replica of +https://crsrc.org/c/chrome/browser/web_applications/proto/web_app_os_integration_state.proto;drc=9910d3be894c8f142c977ba1023f30a656bc13fc;l=67 */ - export type FrameId = string; + export interface FileHandlerAccept { + /** + * New name of the mimetype according to +https://www.iana.org/assignments/media-types/media-types.xhtml + */ + mediaType: string; + fileExtensions: string[]; + } + export interface FileHandler { + action: string; + accepts: FileHandlerAccept[]; + displayName: string; + } /** - * Indicates whether a frame has been identified as an ad. + * If user prefers opening the app in browser or an app window. */ - export type AdFrameType = "none"|"child"|"root"; - export type AdFrameExplanation = "ParentIsAd"|"CreatedByAdScript"|"MatchedBlockingRule"; + export type DisplayMode = "standalone"|"browser"; + + /** - * Indicates whether a frame has been identified as an ad and why. + * Returns the following OS state for the given manifest id. */ - export interface AdFrameStatus { - adFrameType: AdFrameType; - explanations?: AdFrameExplanation[]; + export type getOsAppStateParameters = { + /** + * The id from the webapp's manifest file, commonly it's the url of the +site installing the webapp. See +https://web.dev/learn/pwa/web-app-manifest. + */ + manifestId: string; + } + export type getOsAppStateReturnValue = { + badgeCount: number; + fileHandlers: FileHandler[]; } /** - * Identifies the script which caused a script or frame to be labelled as an -ad. + * Installs the given manifest identity, optionally using the given installUrlOrBundleUrl + +IWA-specific install description: +manifestId corresponds to isolated-app:// + web_package::SignedWebBundleId + +File installation mode: +The installUrlOrBundleUrl can be either file:// or http(s):// pointing +to a signed web bundle (.swbn). In this case SignedWebBundleId must correspond to +The .swbn file's signing key. + +Dev proxy installation mode: +installUrlOrBundleUrl must be http(s):// that serves dev mode IWA. +web_package::SignedWebBundleId must be of type dev proxy. + +The advantage of dev proxy mode is that all changes to IWA +automatically will be reflected in the running app without +reinstallation. + +To generate bundle id for proxy mode: +1. Generate 32 random bytes. +2. Add a specific suffix 0x00 at the end. +3. Encode the entire sequence using Base32 without padding. + +If Chrome is not in IWA dev +mode, the installation will fail, regardless of the state of the allowlist. */ - export interface AdScriptId { - /** - * Script Id of the script which caused a script or frame to be labelled as -an ad. - */ - scriptId: Runtime.ScriptId; + export type installParameters = { + manifestId: string; /** - * Id of scriptId's debugger. + * The location of the app or bundle overriding the one derived from the +manifestId. */ - debuggerId: Runtime.UniqueDebuggerId; + installUrlOrBundleUrl?: string; + } + export type installReturnValue = { } /** - * Encapsulates the script ancestry and the root script filterlist rule that -caused the frame to be labelled as an ad. Only created when `ancestryChain` -is not empty. + * Uninstalls the given manifest_id and closes any opened app windows. */ - export interface AdScriptAncestry { - /** - * A chain of `AdScriptId`s representing the ancestry of an ad script that -led to the creation of a frame. The chain is ordered from the script -itself (lower level) up to its root ancestor that was flagged by -filterlist. - */ - ancestryChain: AdScriptId[]; + export type uninstallParameters = { + manifestId: string; + } + export type uninstallReturnValue = { + } + /** + * Launches the installed web app, or an url in the same web app instead of the +default start url if it is provided. Returns a page Target.TargetID which +can be used to attach to via Target.attachToTarget or similar APIs. + */ + export type launchParameters = { + manifestId: string; + url?: string; + } + export type launchReturnValue = { + /** + * ID of the tab target created as a result. + */ + targetId: Target.TargetID; + } + /** + * Opens one or more local files from an installed web app identified by its +manifestId. The web app needs to have file handlers registered to process +the files. The API returns one or more page Target.TargetIDs which can be +used to attach to via Target.attachToTarget or similar APIs. +If some files in the parameters cannot be handled by the web app, they will +be ignored. If none of the files can be handled, this API returns an error. +If no files are provided as the parameter, this API also returns an error. + +According to the definition of the file handlers in the manifest file, one +Target.TargetID may represent a page handling one or more files. The order +of the returned Target.TargetIDs is not guaranteed. + +TODO(crbug.com/339454034): Check the existences of the input files. + */ + export type launchFilesInAppParameters = { + manifestId: string; + files: string[]; + } + export type launchFilesInAppReturnValue = { + /** + * IDs of the tab targets created as the result. + */ + targetIds: Target.TargetID[]; + } + /** + * Opens the current page in its web app identified by the manifest id, needs +to be called on a page target. This function returns immediately without +waiting for the app to finish loading. + */ + export type openCurrentPageInAppParameters = { + manifestId: string; + } + export type openCurrentPageInAppReturnValue = { + } + /** + * Changes user settings of the web app identified by its manifestId. If the +app was not installed, this command returns an error. Unset parameters will +be ignored; unrecognized values will cause an error. + +Unlike the ones defined in the manifest files of the web apps, these +settings are provided by the browser and controlled by the users, they +impact the way the browser handling the web apps. + +See the comment of each parameter. + */ + export type changeAppUserSettingsParameters = { + manifestId: string; + /** + * If user allows the links clicked on by the user in the app's scope, or +extended scope if the manifest has scope extensions and the flags +`DesktopPWAsLinkCapturingWithScopeExtensions` and +`WebAppEnableScopeExtensions` are enabled. + +Note, the API does not support resetting the linkCapturing to the +initial value, uninstalling and installing the web app again will reset +it. + +TODO(crbug.com/339453269): Setting this value on ChromeOS is not +supported yet. + */ + linkCapturing?: boolean; + displayMode?: DisplayMode; + } + export type changeAppUserSettingsReturnValue = { + } + } + + /** + * Actions and events related to the inspected page belong to the page domain. + */ + export module Page { + /** + * Unique frame identifier. + */ + export type FrameId = string; + /** + * Indicates whether a frame has been identified as an ad. + */ + export type AdFrameType = "none"|"child"|"root"; + export type AdFrameExplanation = "ParentIsAd"|"CreatedByAdScript"|"MatchedBlockingRule"; + /** + * Indicates whether a frame has been identified as an ad and why. + */ + export interface AdFrameStatus { + adFrameType: AdFrameType; + explanations?: AdFrameExplanation[]; + } + /** + * Identifies the script which caused a script or frame to be labelled as an +ad. + */ + export interface AdScriptId { + /** + * Script Id of the script which caused a script or frame to be labelled as +an ad. + */ + scriptId: Runtime.ScriptId; + /** + * Id of scriptId's debugger. + */ + debuggerId: Runtime.UniqueDebuggerId; + } + /** + * Encapsulates the script ancestry and the root script filterlist rule that +caused the frame to be labelled as an ad. Only created when `ancestryChain` +is not empty. + */ + export interface AdScriptAncestry { + /** + * A chain of `AdScriptId`s representing the ancestry of an ad script that +led to the creation of a frame. The chain is ordered from the script +itself (lower level) up to its root ancestor that was flagged by +filterlist. + */ + ancestryChain: AdScriptId[]; /** * The filterlist rule that caused the root (last) script in `ancestryChain` to be ad-tagged. Only populated if the rule is @@ -12192,7 +13527,7 @@ available. in services/network/public/cpp/permissions_policy/permissions_policy_features.json5. LINT.IfChange(PermissionsPolicyFeature) */ - export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"aria-notify"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-high-entropy-values"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"device-attributes"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"language-detector"|"language-model"|"local-fonts"|"local-network-access"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"on-device-speech-recognition"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"record-ad-auction-events"|"rewriter"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"summarizer"|"sync-xhr"|"translator"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"writer"|"xr-spatial-tracking"; + export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"aria-notify"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-high-entropy-values"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"device-attributes"|"digital-credentials-create"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"language-detector"|"language-model"|"local-fonts"|"local-network-access"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"on-device-speech-recognition"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"record-ad-auction-events"|"rewriter"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"summarizer"|"sync-xhr"|"translator"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"writer"|"xr-spatial-tracking"; /** * Reason for a permissions policy feature to be disabled. */ @@ -12791,7 +14126,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl /** * List of not restored reasons for back-forward cache. */ - export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"SharedWorkerMessage"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"|"PostMessageByWebViewClient"|"CacheControlNoStoreDeviceBoundSessionTerminated"|"CacheLimitPrunedOnModerateMemoryPressure"|"CacheLimitPrunedOnCriticalMemoryPressure"; + export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"SharedWorkerMessage"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCUsedWithCCNS"|"WebTransportUsedWithCCNS"|"WebSocketUsedWithCCNS"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"|"PostMessageByWebViewClient"|"CacheControlNoStoreDeviceBoundSessionTerminated"|"CacheLimitPrunedOnModerateMemoryPressure"|"CacheLimitPrunedOnCriticalMemoryPressure"; /** * Types of not restored reasons for back-forward cache. */ @@ -14394,108 +15729,292 @@ Note that not all types exposed to the web platform are currently supported. } } - /** - * Security - */ - export module Security { - /** - * An internal certificate ID value. - */ - export type CertificateId = number; - /** - * A description of mixed content (HTTP resources on HTTPS pages), as defined by -https://www.w3.org/TR/mixed-content/#categories - */ - export type MixedContentType = "blockable"|"optionally-blockable"|"none"; + export module Preload { /** - * The security level of a page or resource. + * Unique id */ - export type SecurityState = "unknown"|"neutral"|"insecure"|"secure"|"info"|"insecure-broken"; + export type RuleSetId = string; /** - * Details about the security state of the page certificate. + * Corresponds to SpeculationRuleSet */ - export interface CertificateSecurityState { - /** - * Protocol name (e.g. "TLS 1.2" or "QUIC"). - */ - protocol: string; - /** - * Key Exchange used by the connection, or the empty string if not applicable. - */ - keyExchange: string; - /** - * (EC)DH group used by the connection, if applicable. - */ - keyExchangeGroup?: string; - /** - * Cipher name. - */ - cipher: string; - /** - * TLS MAC. Note that AEAD ciphers do not have separate MACs. - */ - mac?: string; - /** - * Page certificate. - */ - certificate: string[]; - /** - * Certificate subject name. - */ - subjectName: string; - /** - * Name of the issuing CA. - */ - issuer: string; - /** - * Certificate valid from date. - */ - validFrom: Network.TimeSinceEpoch; - /** - * Certificate valid to (expiration) date - */ - validTo: Network.TimeSinceEpoch; - /** - * The highest priority network error code, if the certificate has an error. - */ - certificateNetworkError?: string; - /** - * True if the certificate uses a weak signature algorithm. - */ - certificateHasWeakSignature: boolean; - /** - * True if the certificate has a SHA1 signature in the chain. - */ - certificateHasSha1Signature: boolean; + export interface RuleSet { + id: RuleSetId; /** - * True if modern SSL + * Identifies a document which the rule set is associated with. */ - modernSSL: boolean; + loaderId: Network.LoaderId; /** - * True if the connection is using an obsolete SSL protocol. + * Source text of JSON representing the rule set. If it comes from +``, + 'playwright/index.ts': ``, + 'src/button.tsx': ` + export const Button = () => ; + `, + 'src/button.test.tsx': ` + import { test, expect } from '@playwright/experimental-ct-react'; + import { Button } from './button.tsx'; + test('pass', async ({ mount }) => { + const component = await mount(); + await expect(component).toHaveText('Button'); + }); + `, + 'src/button2.tsx': ` + export const Button2 = () => ; + `, + 'src/button2.test.tsx': ` + import { test, expect } from '@playwright/experimental-ct-react'; + import { Button2 } from './button2.tsx'; + test('pass', async ({ mount }) => { + const component = await mount(); + await expect(component).toHaveText('Button 2'); + }); + `, + }, { workers: 1 }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); + + }); + + await test.step('remove the second test and component and run the tests again', async () => { + + fs.unlinkSync(testInfo.outputPath('src/button2.tsx')); + fs.unlinkSync(testInfo.outputPath('src/button2.test.tsx')); + + const result2 = await runInlineTest({}, { workers: 1 }); + + expect(result2.exitCode).toBe(0); + expect(result2.passed).toBe(1); + }); + +}); + test('should not use global config for preview', async ({ runInlineTest }) => { const result1 = await runInlineTest({ 'playwright.config.ts': playwrightCtConfigText, From 6e81bf0167f62ca8fcf660b3e93db4b495cffe91 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 16 Sep 2025 15:00:57 -0700 Subject: [PATCH 169/329] chore: write .template.spec.ts to tests dir (#37439) --- packages/playwright/src/mcp/test/testTools.ts | 5 +++- packages/playwright/src/runner/testRunner.ts | 7 +++++ tests/mcp/test-tools.spec.ts | 29 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/mcp/test/testTools.ts b/packages/playwright/src/mcp/test/testTools.ts index faa0c8ac10444..d5a832ca0deaf 100644 --- a/packages/playwright/src/mcp/test/testTools.ts +++ b/packages/playwright/src/mcp/test/testTools.ts @@ -139,7 +139,10 @@ export const setupPage = defineTestTool({ let testLocation = params.testLocation; if (!testLocation) { testLocation = '.template.spec.ts'; - const templateFile = path.join(configDir, testLocation); + const config = await testRunner.loadConfig(); + const project = params.project ? config.projects.find(p => p.project.name === params.project) : config.projects[0]; + const testDir = project?.project.testDir || configDir; + const templateFile = path.join(testDir, testLocation); if (!fs.existsSync(templateFile)) { await fs.promises.writeFile(templateFile, ` import { test, expect } from '@playwright/test'; diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index cb24a6d3577ae..b6d1b4fa6dcd5 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -139,6 +139,13 @@ export class TestRunner extends EventEmitter { await registry.install(executables, false); } + async loadConfig() { + const { config, error } = await this._loadConfig(this._configCLIOverrides); + if (config) + return config; + throw new Error('Failed to load config: ' + (error ? error.message : 'Unknown error')); + } + async runGlobalSetup(userReporters: AnyReporter[]): Promise<{ status: FullResultStatus }> { await this.runGlobalTeardown(); diff --git a/tests/mcp/test-tools.spec.ts b/tests/mcp/test-tools.spec.ts index 37de4d325ae5b..4dc8fb4de8928 100644 --- a/tests/mcp/test-tools.spec.ts +++ b/tests/mcp/test-tools.spec.ts @@ -16,6 +16,8 @@ import { test, expect, writeFiles, StartClient } from './fixtures'; +import fs from 'fs'; + test.use({ mcpServerType: 'test-mcp' }); test('test_list', async ({ startClient }) => { @@ -359,6 +361,33 @@ test('test_setup_page (no test location)', async ({ startClient }) => { `); }); +test('test_setup_page without location respects testsDir', async ({ startClient }) => { + await writeFiles({ + 'playwright.config.ts': ` + module.exports = { + testDir: './tests', + projects: [{ name: 'foo' }] + }; + `, + + 'tests/a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('existing', async ({ page }) => { + }); + `, + }); + + const { client } = await startClient(); + expect(await client.callTool({ + name: 'test_setup_page', + arguments: {}, + })).toHaveTextResponse(`### Paused at end of test. ready for interaction + +### Current page snapshot: +`); + expect(fs.existsSync(test.info().outputPath('tests', '.template.spec.ts'))).toBe(true); +}); + async function prepareDebugTest(startClient: StartClient) { await writeFiles({ 'a.test.ts': ` From ddde5ba42b0f52639b614a71e65c783125628cdb Mon Sep 17 00:00:00 2001 From: Matvey Chernyshov <87153191+matthewpango@users.noreply.github.com> Date: Wed, 17 Sep 2025 01:27:33 +0300 Subject: [PATCH 170/329] chore(android): add complete set of Android media keys (#37372) --- packages/playwright-client/types/types.d.ts | 26 +++++++++++++++---- .../server/dispatchers/androidDispatcher.ts | 17 ++++++++++++ packages/playwright-core/types/types.d.ts | 26 +++++++++++++++---- utils/generate_types/overrides.d.ts | 26 +++++++++++++++---- 4 files changed, 80 insertions(+), 15 deletions(-) diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 8534aa43043fa..43653cbe80c13 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -16268,18 +16268,16 @@ export type AndroidKey = 'Home' | 'Back' | 'Call' | 'EndCall' | - '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | - 'Star' | 'Pound' | '*' | '#' | + '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | + 'Star' | '*' | 'Pound' | '#' | 'DialUp' | 'DialDown' | 'DialLeft' | 'DialRight' | 'DialCenter' | 'VolumeUp' | 'VolumeDown' | - 'ChannelUp' | 'ChannelDown' | 'Power' | 'Camera' | 'Clear' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | - 'Comma' | ',' | - 'Period' | '.' | + 'Comma' | ',' | 'Period' | '.' | 'AltLeft' | 'AltRight' | 'ShiftLeft' | 'ShiftRight' | 'Tab' | '\t' | @@ -16307,8 +16305,26 @@ export type AndroidKey = 'Notification' | 'Search' | 'RecentApps' | + 'MediaPlayPause' | + 'MediaStop' | + 'MediaNext' | + 'MediaPrevious' | + 'MediaRewind' | + 'MediaFastForward' | + 'MediaPlay' | + 'MediaPause' | + 'MediaClose' | + 'MediaEject' | + 'MediaRecord' | + 'ChannelUp' | 'ChannelDown' | 'AppSwitch' | 'Assist' | + 'MediaAudioTrack' | + 'MediaTopMenu' | + 'MediaSkipForward' | + 'MediaSkipBackward' | + 'MediaStepForward' | + 'MediaStepBackward' | 'Cut' | 'Copy' | 'Paste'; diff --git a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts index 2ee799f2c7a42..b05e9b9738d12 100644 --- a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts @@ -319,10 +319,27 @@ const keyMap = new Map([ ['Menu', 82], ['Notification', 83], ['Search', 84], + ['MediaPlayPause', 85], + ['MediaStop', 86], + ['MediaNext', 87], + ['MediaPrevious', 88], + ['MediaRewind', 89], + ['MediaFastForward', 90], + ['MediaPlay', 126], + ['MediaPause', 127], + ['MediaClose', 128], + ['MediaEject', 129], + ['MediaRecord', 130], ['ChannelUp', 166], ['ChannelDown', 167], ['AppSwitch', 187], ['Assist', 219], + ['MediaAudioTrack', 222], + ['MediaTopMenu', 226], + ['MediaSkipForward', 272], + ['MediaSkipBackward', 273], + ['MediaStepForward', 274], + ['MediaStepBackward', 275], ['Cut', 277], ['Copy', 278], ['Paste', 279], diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 8534aa43043fa..43653cbe80c13 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -16268,18 +16268,16 @@ export type AndroidKey = 'Home' | 'Back' | 'Call' | 'EndCall' | - '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | - 'Star' | 'Pound' | '*' | '#' | + '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | + 'Star' | '*' | 'Pound' | '#' | 'DialUp' | 'DialDown' | 'DialLeft' | 'DialRight' | 'DialCenter' | 'VolumeUp' | 'VolumeDown' | - 'ChannelUp' | 'ChannelDown' | 'Power' | 'Camera' | 'Clear' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | - 'Comma' | ',' | - 'Period' | '.' | + 'Comma' | ',' | 'Period' | '.' | 'AltLeft' | 'AltRight' | 'ShiftLeft' | 'ShiftRight' | 'Tab' | '\t' | @@ -16307,8 +16305,26 @@ export type AndroidKey = 'Notification' | 'Search' | 'RecentApps' | + 'MediaPlayPause' | + 'MediaStop' | + 'MediaNext' | + 'MediaPrevious' | + 'MediaRewind' | + 'MediaFastForward' | + 'MediaPlay' | + 'MediaPause' | + 'MediaClose' | + 'MediaEject' | + 'MediaRecord' | + 'ChannelUp' | 'ChannelDown' | 'AppSwitch' | 'Assist' | + 'MediaAudioTrack' | + 'MediaTopMenu' | + 'MediaSkipForward' | + 'MediaSkipBackward' | + 'MediaStepForward' | + 'MediaStepBackward' | 'Cut' | 'Copy' | 'Paste'; diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index de8c95376b675..8c7863a3c8edf 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -334,18 +334,16 @@ export type AndroidKey = 'Home' | 'Back' | 'Call' | 'EndCall' | - '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | - 'Star' | 'Pound' | '*' | '#' | + '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | + 'Star' | '*' | 'Pound' | '#' | 'DialUp' | 'DialDown' | 'DialLeft' | 'DialRight' | 'DialCenter' | 'VolumeUp' | 'VolumeDown' | - 'ChannelUp' | 'ChannelDown' | 'Power' | 'Camera' | 'Clear' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | - 'Comma' | ',' | - 'Period' | '.' | + 'Comma' | ',' | 'Period' | '.' | 'AltLeft' | 'AltRight' | 'ShiftLeft' | 'ShiftRight' | 'Tab' | '\t' | @@ -373,8 +371,26 @@ export type AndroidKey = 'Notification' | 'Search' | 'RecentApps' | + 'MediaPlayPause' | + 'MediaStop' | + 'MediaNext' | + 'MediaPrevious' | + 'MediaRewind' | + 'MediaFastForward' | + 'MediaPlay' | + 'MediaPause' | + 'MediaClose' | + 'MediaEject' | + 'MediaRecord' | + 'ChannelUp' | 'ChannelDown' | 'AppSwitch' | 'Assist' | + 'MediaAudioTrack' | + 'MediaTopMenu' | + 'MediaSkipForward' | + 'MediaSkipBackward' | + 'MediaStepForward' | + 'MediaStepBackward' | 'Cut' | 'Copy' | 'Paste'; From 778439d6ceb209b4f50a89db93be4c84ab2b2f9a Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 16 Sep 2025 15:32:17 -0700 Subject: [PATCH 171/329] chore: remove captureGitInfo from examples (#37441) --- examples/todomvc/playwright.config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/todomvc/playwright.config.ts b/examples/todomvc/playwright.config.ts index eb1cf847b9614..e2fbf59bb8d82 100644 --- a/examples/todomvc/playwright.config.ts +++ b/examples/todomvc/playwright.config.ts @@ -12,8 +12,6 @@ export default defineConfig({ /* Maximum time one test can run for. */ timeout: 15_000, - captureGitInfo: { commit: true, diff: true }, - expect: { /** From 29fb9347901dc1d327354ed9645f2b67e48be653 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 16 Sep 2025 17:41:09 -0700 Subject: [PATCH 172/329] fix(mcp): use single output dir (#37436) --- .../src/mcp/browser/browserContextFactory.ts | 15 ++++---- .../src/mcp/browser/browserServerBackend.ts | 13 ++----- packages/playwright/src/mcp/browser/config.ts | 9 ++++- .../playwright/src/mcp/browser/context.ts | 5 ++- .../playwright/src/mcp/browser/sessionLog.ts | 5 ++- .../playwright/src/mcp/extension/cdpRelay.ts | 2 +- .../mcp/extension/extensionContextFactory.ts | 3 +- packages/playwright/src/mcp/sdk/mdb.ts | 18 ++++----- .../playwright/src/mcp/sdk/proxyBackend.ts | 10 ++--- packages/playwright/src/mcp/sdk/server.ts | 30 +++++++++++++-- .../playwright/src/mcp/test/browserBackend.ts | 4 +- .../playwright/src/mcp/test/testBackend.ts | 16 +++----- packages/playwright/src/mcp/vscode/host.ts | 16 ++++---- packages/playwright/src/mcp/vscode/main.ts | 3 +- tests/mcp/fixtures.ts | 8 ++-- tests/mcp/mdb.spec.ts | 10 ++--- tests/mcp/tracing.spec.ts | 38 +++++++++++++++++++ 17 files changed, 131 insertions(+), 74 deletions(-) diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index de52f1052516a..d3dc2d84dd453 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -24,10 +24,12 @@ import { registryDirectory } from 'playwright-core/lib/server/registry/index'; import { startTraceViewerServer } from 'playwright-core/lib/server'; import { findBrowserProcess, getBrowserExecPath } from './processUtils'; import { logUnhandledError, testDebug } from '../log'; -import { outputFile } from './config'; +import { outputFile } from './config'; +import { firstRootPath } from '../sdk/server'; import type { FullConfig } from './config'; import type { LaunchOptions } from '../../../../playwright-core/src/client/types'; +import type { ClientInfo } from '../sdk/server'; export function contextFactory(config: FullConfig): BrowserContextFactory { if (config.browser.remoteEndpoint) @@ -39,8 +41,6 @@ export function contextFactory(config: FullConfig): BrowserContextFactory { return new PersistentContextFactory(config); } -export type ClientInfo = { name?: string, version?: string, rootPath?: string }; - export interface BrowserContextFactory { createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }>; } @@ -105,7 +105,7 @@ class IsolatedContextFactory extends BaseContextFactory { protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise { await injectCdpPort(this.config.browser); const browserType = playwright[this.config.browser.browserName]; - const tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces`); + const tracesDir = await outputFile(this.config, clientInfo, `traces`); if (this.config.saveTrace) await startTraceServer(this.config, tracesDir); return browserType.launch({ @@ -171,8 +171,8 @@ class PersistentContextFactory implements BrowserContextFactory { async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { await injectCdpPort(this.config.browser); testDebug('create browser context (persistent)'); - const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo.rootPath); - const tracesDir = await outputFile(this.config, clientInfo.rootPath, `traces`); + const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo); + const tracesDir = await outputFile(this.config, clientInfo, `traces`); if (this.config.saveTrace) await startTraceServer(this.config, tracesDir); @@ -218,10 +218,11 @@ class PersistentContextFactory implements BrowserContextFactory { testDebug('close browser context complete (persistent)'); } - private async _createUserDataDir(rootPath: string | undefined) { + private async _createUserDataDir(clientInfo: ClientInfo) { const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? registryDirectory; const browserToken = this.config.browser.launchOptions?.channel ?? this.config.browser?.browserName; // Hesitant putting hundreds of files into the user's workspace, so using it for hashing instead. + const rootPath = firstRootPath(clientInfo); const rootPathToken = rootPath ? `-${createHash(rootPath)}` : ''; const result = path.join(dir, `mcp-${browserToken}${rootPathToken}`); await fs.promises.mkdir(result, { recursive: true }); diff --git a/packages/playwright/src/mcp/browser/browserServerBackend.ts b/packages/playwright/src/mcp/browser/browserServerBackend.ts index 0bed7f22faa4d..bd7ab7fbda7be 100644 --- a/packages/playwright/src/mcp/browser/browserServerBackend.ts +++ b/packages/playwright/src/mcp/browser/browserServerBackend.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { fileURLToPath } from 'url'; import { FullConfig } from './config'; import { Context } from './context'; import { logUnhandledError } from '../log'; @@ -41,19 +40,13 @@ export class BrowserServerBackend implements ServerBackend { this._tools = filteredTools(config); } - async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise { - let rootPath: string | undefined; - if (roots.length > 0) { - const firstRootUri = roots[0]?.uri; - const url = firstRootUri ? new URL(firstRootUri) : undefined; - rootPath = url ? fileURLToPath(url) : undefined; - } - this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, rootPath) : undefined; + async initialize(server: mcpServer.Server, clientInfo: mcpServer.ClientInfo): Promise { + this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, clientInfo) : undefined; this._context = new Context({ config: this._config, browserContextFactory: this._browserContextFactory, sessionLog: this._sessionLog, - clientInfo: { ...clientVersion, rootPath }, + clientInfo, }); } diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index 1f9f0aa48e191..4e7f6e9d9679b 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -17,11 +17,15 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; + import { devices } from 'playwright-core'; import { dotenv } from 'playwright-core/lib/utilsBundle'; +import { firstRootPath } from '../sdk/server'; + import type * as playwright from '../../../types/test'; import type { Config, ToolCapability } from '../config'; +import type { ClientInfo } from '../sdk/server'; export type CLIOptions = { allowedOrigins?: string[]; @@ -265,10 +269,11 @@ async function loadConfig(configFile: string | undefined): Promise { } } -export async function outputFile(config: FullConfig, rootPath: string | undefined, name: string): Promise { +export async function outputFile(config: FullConfig, clientInfo: ClientInfo, name: string): Promise { + const rootPath = firstRootPath(clientInfo); const outputDir = config.outputDir ?? (rootPath ? path.join(rootPath, '.playwright-mcp') : undefined) - ?? path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString())); + ?? path.join(process.env.PW_TMPDIR_FOR_TEST ?? os.tmpdir(), 'playwright-mcp-output', String(clientInfo.timestamp)); await fs.promises.mkdir(outputDir, { recursive: true }); const fileName = sanitizeForFilePath(name); diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts index 3b9584802bb81..c353a87ee683b 100644 --- a/packages/playwright/src/mcp/browser/context.ts +++ b/packages/playwright/src/mcp/browser/context.ts @@ -23,10 +23,11 @@ import * as codegen from './codegen'; import type * as playwright from '../../../types/test'; import type { FullConfig } from './config'; -import type { BrowserContextFactory, ClientInfo } from './browserContextFactory'; +import type { BrowserContextFactory } from './browserContextFactory'; import type * as actions from './actions'; import type { SessionLog } from './sessionLog'; import type { Tracing } from '../../../../playwright-core/src/client/tracing'; +import type { ClientInfo } from '../sdk/server'; const testDebug = debug('pw:mcp:test'); @@ -113,7 +114,7 @@ export class Context { } async outputFile(name: string): Promise { - return outputFile(this.config, this._clientInfo.rootPath, name); + return outputFile(this.config, this._clientInfo, name); } private _onPageCreated(page: playwright.Page) { diff --git a/packages/playwright/src/mcp/browser/sessionLog.ts b/packages/playwright/src/mcp/browser/sessionLog.ts index 7e80087454eb5..4b39ea4c76388 100644 --- a/packages/playwright/src/mcp/browser/sessionLog.ts +++ b/packages/playwright/src/mcp/browser/sessionLog.ts @@ -24,6 +24,7 @@ import { outputFile } from './config'; import type { FullConfig } from './config'; import type * as actions from './actions'; import type { Tab, TabSnapshot } from './tab'; +import type * as mcpServer from '../sdk/server'; type LogEntry = { timestamp: number; @@ -51,8 +52,8 @@ export class SessionLog { this._file = path.join(this._folder, 'session.md'); } - static async create(config: FullConfig, rootPath: string | undefined): Promise { - const sessionFolder = await outputFile(config, rootPath, `session-${Date.now()}`); + static async create(config: FullConfig, clientInfo: mcpServer.ClientInfo): Promise { + const sessionFolder = await outputFile(config, clientInfo, `session-${Date.now()}`); await fs.promises.mkdir(sessionFolder, { recursive: true }); // eslint-disable-next-line no-console console.error(`Session: ${sessionFolder}`); diff --git a/packages/playwright/src/mcp/extension/cdpRelay.ts b/packages/playwright/src/mcp/extension/cdpRelay.ts index 37284a0b966e0..b2258d70efe63 100644 --- a/packages/playwright/src/mcp/extension/cdpRelay.ts +++ b/packages/playwright/src/mcp/extension/cdpRelay.ts @@ -34,7 +34,7 @@ import { logUnhandledError } from '../log'; import * as protocol from './protocol'; import type websocket from 'ws'; -import type { ClientInfo } from '../browser/browserContextFactory'; +import type { ClientInfo } from '../sdk/server'; import type { ExtensionCommand, ExtensionEvents } from './protocol'; import type { WebSocket, WebSocketServer } from 'playwright-core/lib/utilsBundle'; diff --git a/packages/playwright/src/mcp/extension/extensionContextFactory.ts b/packages/playwright/src/mcp/extension/extensionContextFactory.ts index ed7a21c9b16c8..c59cff05c9376 100644 --- a/packages/playwright/src/mcp/extension/extensionContextFactory.ts +++ b/packages/playwright/src/mcp/extension/extensionContextFactory.ts @@ -20,7 +20,8 @@ import { debug } from 'playwright-core/lib/utilsBundle'; import { startHttpServer } from '../sdk/http'; import { CDPRelayServer } from './cdpRelay'; -import type { BrowserContextFactory, ClientInfo } from '../browser/browserContextFactory'; +import type { BrowserContextFactory } from '../browser/browserContextFactory'; +import type { ClientInfo } from '../sdk/server'; const debugLogger = debug('pw:mcp:relay'); diff --git a/packages/playwright/src/mcp/sdk/mdb.ts b/packages/playwright/src/mcp/sdk/mdb.ts index b979a90416206..e8b64eb85705f 100644 --- a/packages/playwright/src/mcp/sdk/mdb.ts +++ b/packages/playwright/src/mcp/sdk/mdb.ts @@ -34,15 +34,15 @@ export class MDBBackend implements mcpServer.ServerBackend { private _stack: { client: Client, toolNames: string[], resultPromise: ManualPromise | undefined }[] = []; private _interruptPromise: ManualPromise | undefined; private _topLevelBackend: mcpServer.ServerBackend; - private _roots: mcpServer.Root[] | undefined; + private _clientInfo: mcpServer.ClientInfo | undefined; constructor(topLevelBackend: mcpServer.ServerBackend) { this._topLevelBackend = topLevelBackend; } - async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise { - if (!this._roots) - this._roots = roots; + async initialize(server: mcpServer.Server, clientInfo: mcpServer.ClientInfo): Promise { + if (!this._clientInfo) + this._clientInfo = clientInfo; } async listTools(): Promise { @@ -107,8 +107,8 @@ export class MDBBackend implements mcpServer.ServerBackend { private async _pushClient(transport: Transport, introMessage?: string): Promise { mdbDebug('pushing client to the stack'); - const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' }, { capabilities: { roots: {} } }); - client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots || [] })); + const client = new mcpBundle.Client({ name: 'Pushing client', version: '0.0.0' }, { capabilities: { roots: {} } }); + client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] })); client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); await client.connect(transport); mdbDebug('connected to the new client'); @@ -169,7 +169,7 @@ export async function runOnPauseBackendLoop(backend: mcpServer.ServerBackend, in await mcpHttp.installHttpTransport(httpServer, factory); const url = mcpHttp.httpAddressToString(httpServer.address()); - const client = new mcpBundle.Client({ name: 'Internal client', version: '0.0.0' }); + const client = new mcpBundle.Client({ name: 'On-pause client', version: '0.0.0' }); client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(process.env.PLAYWRIGHT_DEBUGGER_MCP!)); await client.connect(transport); @@ -205,8 +205,8 @@ class ServerBackendWithCloseListener implements mcpServer.ServerBackend { this._backend = backend; } - async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise { - await this._backend.initialize?.(server, clientVersion, roots); + async initialize(server: mcpServer.Server, clientInfo: mcpServer.ClientInfo): Promise { + await this._backend.initialize?.(server, clientInfo); } async listTools(): Promise { diff --git a/packages/playwright/src/mcp/sdk/proxyBackend.ts b/packages/playwright/src/mcp/sdk/proxyBackend.ts index c80b30062f57f..c55e5f97d6768 100644 --- a/packages/playwright/src/mcp/sdk/proxyBackend.ts +++ b/packages/playwright/src/mcp/sdk/proxyBackend.ts @@ -18,7 +18,7 @@ import { debug } from 'playwright-core/lib/utilsBundle'; import * as mcpBundle from './bundle'; -import type { ServerBackend, ClientVersion, Root, Server } from './server'; +import type { ServerBackend, ClientInfo, Server } from './server'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; @@ -36,15 +36,15 @@ export class ProxyBackend implements ServerBackend { private _mcpProviders: MCPProvider[]; private _currentClient: Client | undefined; private _contextSwitchTool: Tool; - private _roots: Root[] = []; + private _clientInfo: ClientInfo | undefined; constructor(mcpProviders: MCPProvider[]) { this._mcpProviders = mcpProviders; this._contextSwitchTool = this._defineContextSwitchTool(); } - async initialize(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise { - this._roots = roots; + async initialize(server: Server, clientInfo: ClientInfo): Promise { + this._clientInfo = clientInfo; } async listTools(): Promise { @@ -124,7 +124,7 @@ export class ProxyBackend implements ServerBackend { listRoots: true, }, }); - client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots })); + client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] })); client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); const transport = await factory.connect(); diff --git a/packages/playwright/src/mcp/sdk/server.ts b/packages/playwright/src/mcp/sdk/server.ts index 8e54afd91c266..81e2f40e83343 100644 --- a/packages/playwright/src/mcp/sdk/server.ts +++ b/packages/playwright/src/mcp/sdk/server.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { fileURLToPath } from 'url'; + import { debug } from 'playwright-core/lib/utilsBundle'; import * as mcpBundle from './bundle'; @@ -28,10 +30,15 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; const serverDebug = debug('pw:mcp:server'); -export type ClientVersion = { name: string, version: string }; +export type ClientInfo = { + name: string; + version: string; + roots: Root[]; + timestamp: number; +}; export interface ServerBackend { - initialize?(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise; + initialize?(server: Server, clientInfo: ClientInfo): Promise; listTools(): Promise; callTool(name: string, args: CallToolRequest['params']['arguments']): Promise; serverClosed?(server: Server): void; @@ -93,8 +100,15 @@ const initializeServer = async (server: Server, backend: ServerBackend, runHeart const { roots } = await server.listRoots(); clientRoots = roots; } - const clientVersion = server.getClientVersion() ?? { name: 'unknown', version: 'unknown' }; - await backend.initialize?.(server, clientVersion, clientRoots); + + const clientInfo: ClientInfo = { + name: server.getClientVersion()?.name ?? 'unknown', + version: server.getClientVersion()?.version ?? 'unknown', + roots: clientRoots, + timestamp: Date.now(), + }; + + await backend.initialize?.(server, clientInfo); if (runHeartbeat) startHeartbeat(server); }; @@ -145,3 +159,11 @@ export async function start(serverBackendFactory: ServerBackendFactory, options: // eslint-disable-next-line no-console console.error(message); } + +export function firstRootPath(clientInfo: ClientInfo): string | undefined { + if (clientInfo.roots.length === 0) + return undefined; + const firstRootUri = clientInfo.roots[0]?.uri; + const url = firstRootUri ? new URL(firstRootUri) : undefined; + return url ? fileURLToPath(url) : undefined; +} diff --git a/packages/playwright/src/mcp/test/browserBackend.ts b/packages/playwright/src/mcp/test/browserBackend.ts index 00329fc0785a5..38d2a7e22b624 100644 --- a/packages/playwright/src/mcp/test/browserBackend.ts +++ b/packages/playwright/src/mcp/test/browserBackend.ts @@ -22,8 +22,8 @@ import { BrowserServerBackend } from '../browser/browserServerBackend'; import type * as playwright from '../../../index'; import type { Page } from '../../../../playwright-core/src/client/page'; -import type { BrowserContextFactory, ClientInfo } from '../browser/browserContextFactory'; - +import type { BrowserContextFactory } from '../browser/browserContextFactory'; +import type { ClientInfo } from '../sdk/server'; export async function runBrowserBackendOnError(page: playwright.Page, message: () => string) { const testInfo = currentTestInfo(); diff --git a/packages/playwright/src/mcp/test/testBackend.ts b/packages/playwright/src/mcp/test/testBackend.ts index 40d716fb34584..eb1255a624b0a 100644 --- a/packages/playwright/src/mcp/test/testBackend.ts +++ b/packages/playwright/src/mcp/test/testBackend.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { fileURLToPath } from 'url'; - import * as mcp from '../sdk/exports'; import { TestContext } from './testContext'; import { listTests, runTests, debugTest, setupPage } from './testTools.js'; @@ -36,20 +34,16 @@ export class TestServerBackend implements mcp.ServerBackend { this._configOption = configOption; } - async initialize(server: mcp.Server, clientVersion: mcp.ClientVersion, roots: mcp.Root[]): Promise { + async initialize(server: mcp.Server, clientInfo: mcp.ClientInfo): Promise { if (this._configOption) { this._context.setConfigLocation(resolveConfigLocation(this._configOption)); return; } - if (roots.length > 0) { - const firstRootUri = roots[0]?.uri; - const url = firstRootUri ? new URL(firstRootUri) : undefined; - const folder = url ? fileURLToPath(url) : undefined; - if (folder) { - this._context.setConfigLocation(resolveConfigLocation(folder)); - return; - } + const rootPath = mcp.firstRootPath(clientInfo); + if (rootPath) { + this._context.setConfigLocation(resolveConfigLocation(rootPath)); + return; } throw new Error('No config option or MCP root path provided'); diff --git a/packages/playwright/src/mcp/vscode/host.ts b/packages/playwright/src/mcp/vscode/host.ts index 15332a32c6226..e3c0a8916d144 100644 --- a/packages/playwright/src/mcp/vscode/host.ts +++ b/packages/playwright/src/mcp/vscode/host.ts @@ -26,8 +26,8 @@ import { contextFactory } from '../browser/browserContextFactory'; import type { z as zod } from 'zod'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; -import type { ClientVersion, ServerBackend } from '../sdk/server'; -import type { Root, Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; +import type { ClientInfo, ServerBackend } from '../sdk/server'; +import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; import type { Browser, BrowserContext, BrowserServer } from 'playwright'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; @@ -47,8 +47,7 @@ class VSCodeProxyBackend implements ServerBackend { private _currentClient: Client | undefined; private _contextSwitchTool: Tool; - private _roots: Root[] = []; - private _clientVersion?: ClientVersion; + private _clientInfo?: ClientInfo; private _context?: BrowserContext; private _browser?: Browser; private _browserServer?: BrowserServer; @@ -57,9 +56,8 @@ class VSCodeProxyBackend implements ServerBackend { this._contextSwitchTool = this._defineContextSwitchTool(); } - async initialize(server: mcpServer.Server, clientVersion: ClientVersion, roots: Root[]): Promise { - this._clientVersion = clientVersion; - this._roots = roots; + async initialize(server: mcpServer.Server, clientInfo: ClientInfo): Promise { + this._clientInfo = clientInfo; const transport = await this._defaultTransportFactory(this); await this._setCurrentClient(transport); } @@ -166,13 +164,13 @@ class VSCodeProxyBackend implements ServerBackend { await this._currentClient?.close(); this._currentClient = undefined; - const client = new mcpBundle.Client(this._clientVersion!); + const client = new mcpBundle.Client({ name: this._clientInfo!.name, version: this._clientInfo!.version }); client.registerCapabilities({ roots: { listRoots: true, }, }); - client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots })); + client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo!.roots })); client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); await client.connect(transport); diff --git a/packages/playwright/src/mcp/vscode/main.ts b/packages/playwright/src/mcp/vscode/main.ts index 5eb46569a3a0b..67ca6a3ac8e61 100644 --- a/packages/playwright/src/mcp/vscode/main.ts +++ b/packages/playwright/src/mcp/vscode/main.ts @@ -17,10 +17,11 @@ import * as mcpBundle from '../sdk/bundle'; import * as mcpServer from '../sdk/server'; import { BrowserServerBackend } from '../browser/browserServerBackend'; -import { BrowserContextFactory, ClientInfo } from '../browser/browserContextFactory'; +import { BrowserContextFactory } from '../browser/browserContextFactory'; import type { FullConfig } from '../browser/config'; import type { BrowserContext } from 'playwright-core'; +import type { ClientInfo } from '../sdk/server'; class VSCodeBrowserContextFactory implements BrowserContextFactory { name = 'vscode'; diff --git a/tests/mcp/fixtures.ts b/tests/mcp/fixtures.ts index 49f6cd6632d80..f08d23b086f89 100644 --- a/tests/mcp/fixtures.ts +++ b/tests/mcp/fixtures.ts @@ -48,6 +48,7 @@ export type StartClient = (options?: { config?: Config, roots?: { name: string, uri: string }[], rootsResponseDelay?: number, + env?: NodeJS.ProcessEnv, }) => Promise<{ client: Client, stderr: () => string }>; @@ -112,7 +113,8 @@ export const test = serverTest.extend { if (process.env.PWMCP_DEBUG) @@ -176,7 +178,7 @@ export const test = serverTest.extend { @@ -187,7 +189,7 @@ async function createTransport(mcpServerType: TestOptions['mcpServerType'], args cwd: test.info().outputPath(), stderr: 'pipe', env: { - ...process.env, + ...env, DEBUG: process.env.DEBUG ? `${process.env.DEBUG},pw:mcp:test` : 'pw:mcp:test', DEBUG_COLORS: '0', DEBUG_HIDE_DATE: '1', diff --git a/tests/mcp/mdb.spec.ts b/tests/mcp/mdb.spec.ts index 4ec782eb8a4ef..43d77661adbb2 100644 --- a/tests/mcp/mdb.spec.ts +++ b/tests/mcp/mdb.spec.ts @@ -139,7 +139,7 @@ async function startMDBAndCLI(): Promise<{ mdbUrl: string }> { } async function createMDBClient(mdbUrl: string, roots: any[] | undefined = undefined): Promise<{ client: Client, close: () => Promise }> { - const client = new Client({ name: 'Internal client', version: '0.0.0' }, roots ? { capabilities: { roots: {} } } : undefined); + const client = new Client({ name: 'Test client', version: '0.0.0' }, roots ? { capabilities: { roots: {} } } : undefined); if (roots) client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots })); const transport = new StreamableHTTPClientTransport(new URL(mdbUrl)); @@ -158,8 +158,8 @@ class CLIBackend { constructor(private readonly mdbUrlBox: { mdbUrl: string | undefined }) {} - async initialize(server, clientVersion, roots) { - this._roots = roots; + async initialize(server, clientInfo) { + this._roots = clientInfo.roots; } async listTools() { @@ -197,8 +197,8 @@ class CLIBackend { class GDBBackend { private _roots: any[] | undefined; - async initialize(server, clientVersion, roots) { - this._roots = roots; + async initialize(server, clientVersion) { + this._roots = clientVersion.roots; } async listTools() { diff --git a/tests/mcp/tracing.spec.ts b/tests/mcp/tracing.spec.ts index 9a1d88a4ca60d..c790aa926c9bd 100644 --- a/tests/mcp/tracing.spec.ts +++ b/tests/mcp/tracing.spec.ts @@ -67,3 +67,41 @@ test('check that trace is saved with browser_start_tracing', async ({ startClien expect.stringMatching(/trace-\d+\.trace/), ]); }); + +test('check that trace is saved with browser_start_tracing (no output dir)', async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath(); + + const { client } = await startClient({ + args: ['--caps=tracing'], + env: { ...process.env, PW_TMPDIR_FOR_TEST: outputDir }, + }); + + expect(await client.callTool({ + name: 'browser_start_tracing', + })).toHaveResponse({ + result: expect.stringContaining(`Tracing started, saving to ${outputDir}`), + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + expect(await client.callTool({ + name: 'browser_stop_tracing', + })).toHaveResponse({ + result: expect.stringMatching(/trace-\d+.trace/) + }); + + const folders = await fs.promises.readdir(path.join(outputDir, 'playwright-mcp-output')); + expect(folders.length).toBe(1); + expect(folders[0]).toMatch(/\d+/); + const files = await fs.promises.readdir(path.join(outputDir, 'playwright-mcp-output', folders[0], 'traces')); + expect(files).toEqual([ + 'resources', + expect.stringMatching(/trace-\d+\.network/), + expect.stringMatching(/trace-\d+\.trace/), + ]); +}); From a008e126c086b0aab3931eb3f50f1b091c801b30 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 16 Sep 2025 17:41:26 -0700 Subject: [PATCH 173/329] chore: remove stray type exports (#37440) --- packages/playwright-core/package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json index ece81c449d6a4..5eec99ac41453 100644 --- a/packages/playwright-core/package.json +++ b/packages/playwright-core/package.json @@ -33,9 +33,7 @@ "./lib/server/registry/index": "./lib/server/registry/index.js", "./lib/utils": "./lib/utils.js", "./lib/utilsBundle": "./lib/utilsBundle.js", - "./lib/zipBundle": "./lib/zipBundle.js", - "./types/protocol": "./types/protocol.d.ts", - "./types/structs": "./types/structs.d.ts" + "./lib/zipBundle": "./lib/zipBundle.js" }, "bin": { "playwright-core": "cli.js" From 25f89ac4d20dad67a9f0643ac56211168f2f169c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 16 Sep 2025 17:41:55 -0700 Subject: [PATCH 174/329] fix(trace): survive sw restart (#37442) --- package-lock.json | 7 ++++ packages/trace-viewer/package.json | 1 + packages/trace-viewer/src/sw/main.ts | 54 ++++++++++++++++++++++++++-- tests/library/trace-viewer.spec.ts | 19 ++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 548715024d74b..28ac6268a687d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4802,6 +4802,12 @@ "node": ">=0.10.0" } }, + "node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8377,6 +8383,7 @@ "packages/trace-viewer": { "version": "0.0.0", "dependencies": { + "idb-keyval": "^6.2.2", "yaml": "^2.6.0" } }, diff --git a/packages/trace-viewer/package.json b/packages/trace-viewer/package.json index 57a3dfd4094f3..699014a40fc7a 100644 --- a/packages/trace-viewer/package.json +++ b/packages/trace-viewer/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "type": "module", "dependencies": { + "idb-keyval": "^6.2.2", "yaml": "^2.6.0" } } diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index cdce3261feac0..0e7ff887cc332 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import * as idbKeyval from 'idb-keyval'; + import { splitProgress } from './progress'; import { unwrapPopoutUrl } from './snapshotRenderer'; import { SnapshotServer } from './snapshotServer'; @@ -33,11 +35,14 @@ self.addEventListener('activate', function(event: any) { }); const scopePath = new URL(self.registration.scope).pathname; - const loadedTraces = new Map(); - const clientIdToTraceUrls = new Map, traceViewerServer: TraceViewerServer }>(); +function simulateServiceWorkerRestart() { + loadedTraces.clear(); + clientIdToTraceUrls.clear(); +} + async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, limit: number | undefined, progress: (done: number, total: number) => undefined): Promise { await gc(); const clientId = client?.id ?? ''; @@ -49,6 +54,7 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, client: clientIdToTraceUrls.set(clientId, data); } data.traceUrls.add(traceUrl); + await saveClientIdParams(); const traceModel = new TraceModel(); try { @@ -101,6 +107,10 @@ async function doFetch(event: FetchEvent): Promise { await gc(); return new Response(null, { status: 200 }); } + if (relativePath === '/restartServiceWorker') { + simulateServiceWorkerRestart(); + return new Response(null, { status: 200 }); + } const traceUrl = url.searchParams.get('trace'); @@ -122,6 +132,16 @@ async function doFetch(event: FetchEvent): Promise { } } + if (!clientIdToTraceUrls.has(event.clientId)) { + // Service worker was restarted upon subresource fetch. + // It was stopped because ping did not keep it alive since the tab itself was throttled. + const params = await loadClientIdParams(event.clientId); + if (params) { + for (const traceUrl of params.traceUrls) + await loadTrace(traceUrl, null, client, params.limit, () => {}); + } + } + if (relativePath.startsWith('/snapshotInfo/')) { const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; if (!snapshotServer) @@ -221,6 +241,36 @@ async function gc() { if (!usedTraces.has(traceUrl)) loadedTraces.delete(traceUrl); } + + await saveClientIdParams(); +} + +// Persist clientIdToTraceUrls to localStorage to avoid losing it when the service worker is restarted. +async function saveClientIdParams() { + const serialized: Record = {}; + for (const [clientId, data] of clientIdToTraceUrls) { + serialized[clientId] = { + limit: data.limit, + traceUrls: [...data.traceUrls] + }; + } + + const newValue = JSON.stringify(serialized); + const oldValue = await idbKeyval.get('clientIdToTraceUrls'); + if (newValue === oldValue) + return; + idbKeyval.set('clientIdToTraceUrls', newValue); +} + +async function loadClientIdParams(clientId: string): Promise<{ limit: number | undefined, traceUrls: string[] } | undefined> { + const serialized = await idbKeyval.get('clientIdToTraceUrls') as string | undefined; + if (!serialized) + return; + const deserialized = JSON.parse(serialized); + return deserialized[clientId]; } // @ts-ignore diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 086b347bbc461..064be8097f8af 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -2021,3 +2021,22 @@ test.describe(() => { await expect(frame.getByRole('button')).toHaveCSS('color', 'rgb(255, 0, 0)'); }); }); + +test('should survive service worker restart', async ({ page, runAndTrace, server }) => { + const traceViewer = await runAndTrace(async () => { + await page.goto(server.EMPTY_PAGE); + await page.setContent('Old world'); + await page.evaluate(() => document.body.textContent = 'New world'); + }); + const snapshot1 = await traceViewer.snapshotFrame('Evaluate'); + await expect(snapshot1.locator('body')).toHaveText('New world'); + + const status = await traceViewer.page.evaluate(async () => { + const response = await fetch('restartServiceWorker'); + return response.status; + }); + expect(status).toBe(200); + + const snapshot2 = await traceViewer.snapshotFrame('Set content'); + await expect(snapshot2.locator('body')).toHaveText('Old world'); +}); From 0b40ef34b44760f06c7209708a0264a34886a5cf Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 09:08:50 +0200 Subject: [PATCH 175/329] feat(chromium-tip-of-tree): roll to r1369 (#37445) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index dbb60b8fc99e9..58d274005ee05 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,15 +15,15 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1368", + "revision": "1369", "installByDefault": false, - "browserVersion": "142.0.7407.0" + "browserVersion": "142.0.7417.0" }, { "name": "chromium-tip-of-tree-headless-shell", - "revision": "1368", + "revision": "1369", "installByDefault": false, - "browserVersion": "142.0.7407.0" + "browserVersion": "142.0.7417.0" }, { "name": "firefox", From ef40dcbd59fcff11dde5eb6146e58ce3cccff1a4 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 17 Sep 2025 12:22:50 +0100 Subject: [PATCH 176/329] test: unflake "should click bottom row w/ infobar in OOPIF" (#37448) --- tests/library/headful.spec.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/library/headful.spec.ts b/tests/library/headful.spec.ts index c09a379b743be..06d69b2df72c8 100644 --- a/tests/library/headful.spec.ts +++ b/tests/library/headful.spec.ts @@ -249,9 +249,7 @@ it('should click in OOPIF', async ({ browserName, launchPersistent, server }) => expect(consoleLog).toContain('ok'); }); -it('should click bottom row w/ infobar in OOPIF', async ({ browserName, launchPersistent, server, isWindows }) => { - it.fixme(browserName === 'chromium' && isWindows, 'Click is offset by the infobar height'); - +it('should click bottom row w/ infobar in OOPIF', async ({ browserName, launchPersistent, server }) => { server.setRoute('/empty.html', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` @@ -270,18 +268,24 @@ it('should click bottom row w/ infobar in OOPIF', async ({ browserName, launchPe html, body { margin: 0; padding: 0; width: 100%; height: 100%; } button { position: absolute; bottom: 0; } - `); + `); }); const { page } = await launchPersistent(); await page.goto(server.EMPTY_PAGE); - // Chrome bug! Investigate what's happening in the oopif router. - const consoleLog: string[] = []; - page.on('console', m => consoleLog.push(m.text())); - while (!consoleLog.includes('ok')) { - await page.waitForTimeout(100); - await page.frames()[1].click('text=Submit'); + if (browserName === 'chromium') { + // CHROME BUG: + // Unfortunately, on some platforms the automation infobar is shown up late or animates in. + // Upon showing, it triggers a resize of WebContentsView and RenderWidgetHostView + // through the native view hierarchy. This in turn resizes the compositor to the visible view size + // instead of the emulated size specified in Emulation.setDeviceMetricsOverride. + // Hit testing for OOPIFs is affected by the new size, and clicks do not reach the iframe. + // + // The workaround is to re-apply the viewport after a delay, in this case after the navigation. + await page.setViewportSize({ width: 800, height: 600 }); } + await page.frames()[1].click('text=Submit'); + expect(await page.frames()[1].evaluate('window._clicked')).toBe(true); }); it('headless and headful should use same default fonts', async ({ page, browserName, browserType }) => { From 21296cd1e6a33d2a26ab84a9021f1087fcc77faa Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 17 Sep 2025 11:55:02 -0700 Subject: [PATCH 177/329] Revert "fix(mcp): wait if profile is still in use (#37327)" (#37455) --- .../src/mcp/browser/browserContextFactory.ts | 60 ++++++------ .../src/mcp/browser/processUtils.ts | 93 ------------------- tests/mcp/launch.spec.ts | 43 --------- 3 files changed, 26 insertions(+), 170 deletions(-) delete mode 100644 packages/playwright/src/mcp/browser/processUtils.ts diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index d3dc2d84dd453..f6b7a55e12f42 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -22,7 +22,6 @@ import path from 'path'; import * as playwright from 'playwright-core'; import { registryDirectory } from 'playwright-core/lib/server/registry/index'; import { startTraceViewerServer } from 'playwright-core/lib/server'; -import { findBrowserProcess, getBrowserExecPath } from './processUtils'; import { logUnhandledError, testDebug } from '../log'; import { outputFile } from './config'; import { firstRootPath } from '../sdk/server'; @@ -181,33 +180,33 @@ class PersistentContextFactory implements BrowserContextFactory { const browserType = playwright[this.config.browser.browserName]; for (let i = 0; i < 5; i++) { - if (!await alreadyRunning(this.config, browserType, userDataDir)) - break; - // User data directory is already in use, wait for the previous browser instance to close. - await new Promise(resolve => setTimeout(resolve, 1000)); - } - const launchOptions: LaunchOptions = { - tracesDir, - ...this.config.browser.launchOptions, - ...this.config.browser.contextOptions, - handleSIGINT: false, - handleSIGTERM: false, - ignoreDefaultArgs: [ - '--disable-extensions', - ], - assistantMode: true, - }; - try { - const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions); - const close = () => this._closeBrowserContext(browserContext, userDataDir); - return { browserContext, close }; - } catch (error: any) { - if (error.message.includes('Executable doesn\'t exist')) - throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); - if (error.message.includes('ProcessSingleton') || error.message.includes('Invalid URL')) - throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); - throw error; + const launchOptions: LaunchOptions = { + tracesDir, + ...this.config.browser.launchOptions, + ...this.config.browser.contextOptions, + handleSIGINT: false, + handleSIGTERM: false, + ignoreDefaultArgs: [ + '--disable-extensions', + ], + assistantMode: true, + }; + try { + const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions); + const close = () => this._closeBrowserContext(browserContext, userDataDir); + return { browserContext, close }; + } catch (error: any) { + if (error.message.includes('Executable doesn\'t exist')) + throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); + if (error.message.includes('ProcessSingleton') || error.message.includes('Invalid URL')) { + // User data directory is already in use, try again. + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + throw error; + } } + throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); } private async _closeBrowserContext(browserContext: playwright.BrowserContext, userDataDir: string) { @@ -230,13 +229,6 @@ class PersistentContextFactory implements BrowserContextFactory { } } -async function alreadyRunning(config: FullConfig, browserType: playwright.BrowserType, userDataDir: string) { - const execPath = config.browser.launchOptions.executablePath ?? getBrowserExecPath(config.browser.launchOptions.channel ?? browserType.name()); - if (!execPath) - return false; - return !!findBrowserProcess(execPath, userDataDir); -} - async function injectCdpPort(browserConfig: FullConfig['browser']) { if (browserConfig.browserName === 'chromium') (browserConfig.launchOptions as any).cdpPort = await findFreePort(); diff --git a/packages/playwright/src/mcp/browser/processUtils.ts b/packages/playwright/src/mcp/browser/processUtils.ts deleted file mode 100644 index 585e728294a07..0000000000000 --- a/packages/playwright/src/mcp/browser/processUtils.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import childProcess from 'child_process'; -import fs from 'fs'; - -import { registry } from 'playwright-core/lib/server/registry/index'; - -export function getBrowserExecPath(channelOrName: string): string | undefined { - return registry.findExecutable(channelOrName)?.executablePath('javascript'); -} - -type CmdlinePredicate = (line: string) => boolean; - -export function findBrowserProcess(execPath: string, arg: string): string | undefined { - const predicate = (line: string) => line.includes(execPath) && line.includes(arg) && !line.includes('--type'); - try { - switch (process.platform) { - case 'darwin': - return findProcessMacos(predicate); - case 'linux': - return findProcessLinux(predicate); - case 'win32': - return findProcessWindows(execPath, arg, predicate); - default: - return undefined; - } - } catch { - return undefined; - } -} - -function findProcessLinux(predicate: CmdlinePredicate): string | undefined { - // /bin/ps is missing in slim docker images, so we read /proc fs directly. - const procDirs = fs.readdirSync('/proc').filter(name => /^\d+$/.test(name)); - for (const pid of procDirs) { - try { - const cmdlineBuffer = fs.readFileSync(`/proc/${pid}/cmdline`); - // Convert 0-separated arguments to space-separated string - const cmdline = cmdlineBuffer.toString().replace(/\0/g, ' ').trim(); - if (predicate(cmdline)) - return `${pid} ${cmdline}`; - } catch { - // Skip processes we can't read (permission denied, process died, etc.) - continue; - } - } - return undefined; -} - -function findProcessMacos(predicate: CmdlinePredicate): string | undefined { - const result = childProcess.spawnSync('/bin/ps', ['-axo', 'pid=,command=']); - if (result.status !== 0 || !result.stdout) - return undefined; - return findMatchingLine(result.stdout.toString(), predicate); -} - -function findProcessWindows(execPath: string, arg: string, predicate: CmdlinePredicate): string | undefined { - const psEscape = (path: string) => `'${path.replaceAll("'", "''")}'`; - const filter = `$_.ExecutablePath -eq ${psEscape(execPath)} -and $_.CommandLine.Contains(${psEscape(arg)}) -and $_.CommandLine -notmatch '--type'`; - const ps = childProcess.spawnSync( - 'powershell.exe', - [ - '-NoProfile', - '-Command', - `Get-CimInstance Win32_Process | Where-Object { ${filter} } | Select-Object -Property ProcessId,CommandLine | ForEach-Object { "$($_.ProcessId) $($_.CommandLine)" }` - ], - { encoding: 'utf8' } - ); - - if (ps.status !== 0 || !ps.stdout) - return undefined; - - return findMatchingLine(ps.stdout.toString(), predicate); -} - -function findMatchingLine(psOutput: string, predicate: CmdlinePredicate): string | undefined { - const lines = psOutput.split('\n').map(l => l.trim()).filter(Boolean); - return lines.find(predicate); -} diff --git a/tests/mcp/launch.spec.ts b/tests/mcp/launch.spec.ts index afea8b00fb629..03a637b99848f 100644 --- a/tests/mcp/launch.spec.ts +++ b/tests/mcp/launch.spec.ts @@ -167,46 +167,3 @@ test('isolated context with storage state', async ({ startClient, server }, test pageState: expect.stringContaining(`Storage: session-value`), }); }); - -test('persistent context already running', async ({ startClient, server, mcpBrowser }, testInfo) => { - const userDataDir = testInfo.outputPath('user-data-dir'); - const { client } = await startClient({ - args: [`--user-data-dir=${userDataDir}`], - }); - await client.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const { client: client2, stderr } = await startClient({ - args: [`--user-data-dir=${userDataDir}`], - }); - const navigationPromise = client2.callTool({ - name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, - }); - - const wait = await Promise.race([ - navigationPromise.then(() => 'done'), - new Promise(resolve => setTimeout(resolve, 1_000)).then(() => 'timeout'), - ]); - expect(wait).toBe('timeout'); - - // Check that the second client is trying to launch the browser. - await expect.poll(() => formatOutput(stderr()), { timeout: 0 }).toEqual([ - 'create context', - 'create browser context (persistent)', - 'lock user data dir' - ]); - - // Close first client's browser. - await client.callTool({ - name: 'browser_close', - arguments: { url: server.HELLO_WORLD }, - }); - - const result = await navigationPromise; - expect(result).toHaveResponse({ - pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), - }); -}); From a6dbce101aba8ae95fe07bbb5649b1fb655f76f8 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 17 Sep 2025 11:59:58 -0700 Subject: [PATCH 178/329] feat(mcp): generate vscode chatmodes (#37456) --- packages/playwright/src/agents/fixer.md | 1 - .../playwright/src/agents/generateAgents.ts | 58 ++++++++++++++++++- packages/playwright/src/program.ts | 5 +- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/playwright/src/agents/fixer.md b/packages/playwright/src/agents/fixer.md index 0afe7efcbc804..4f159ea4b2c8d 100644 --- a/packages/playwright/src/agents/fixer.md +++ b/packages/playwright/src/agents/fixer.md @@ -15,7 +15,6 @@ tools: - playwright/test_debug - playwright/test_list - playwright/test_run - - playwright/test_setup_page mcp-servers: playwright: type: 'local' diff --git a/packages/playwright/src/agents/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts index 3201c2ada605f..30dd5c964494b 100644 --- a/packages/playwright/src/agents/generateAgents.ts +++ b/packages/playwright/src/agents/generateAgents.ts @@ -107,7 +107,7 @@ const claudeToolMap = new Map([ ['ls', ['Glob']], ['grep', ['Grep']], ['read', ['Read']], - ['edit', ['Edit', 'MultiEdit', 'NotebookEdit']], + ['edit', ['Edit', 'MultiEdit']], ['write', ['Write']], ]); @@ -219,6 +219,62 @@ export async function initClaudeCodeRepo() { await writeFile('.mcp.json', JSON.stringify({ mcpServers }, null, 2)); } +const vscodeToolMap = new Map([ + ['ls', ['listDirectory', 'fileSearch']], + ['grep', ['textSearch']], + ['read', ['readFile']], + ['edit', ['editFiles']], + ['write', ['createFile', 'createDirectory']], +]); +function saveAsVSCodeChatmode(agent: Agent): string { + function asVscodeTool(tool: string): string | string[] { + const [first, second] = tool.split('/'); + if (second) + return second; + return vscodeToolMap.get(first) || first; + } + const tools = agent.header.tools.map(asVscodeTool).flat().map(tool => `'${tool}'`).join(', '); + + const lines: string[] = []; + lines.push(`---`); + lines.push(`description: ${agent.header.description}. Examples: ${agent.examples.map(example => `${example}`).join('')}`); + lines.push(`tools: [${tools}]`); + lines.push(`---`); + lines.push(''); + lines.push(agent.instructions); + return lines.join('\n'); +} + +export async function initVSCodeRepo() { + const agents = await loadAgents(); + + await fs.promises.mkdir('.github/chatmodes', { recursive: true }); + for (const agent of agents) + await writeFile(`.github/chatmodes/${agent.header.name}.chatmode.md`, saveAsVSCodeChatmode(agent)); + + await fs.promises.mkdir('.vscode', { recursive: true }); + + const mcpJsonPath = '.vscode/mcp.json'; + let mcpJson: any = { + servers: {}, + inputs: [] + }; + try { + mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8')); + } catch { + } + + if (!mcpJson.servers) + mcpJson.servers = {}; + + mcpJson.servers['playwright-test-mcp'] = { + type: 'stdio', + command: 'npx', + args: ['playwright', 'run-test-mcp-server'], + }; + await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2)); +} + export async function initOpencodeRepo() { const agents = await loadAgents(); diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index bad07fbc5fee7..5bf80e7b29541 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -37,7 +37,7 @@ import { ServerBackendFactory, runMainBackend } from './mcp/sdk/exports'; import { TestServerBackend } from './mcp/test/testBackend'; import { decorateCommand } from './mcp/program'; import { setupExitWatchdog } from './mcp/browser/watchdog'; -import { initClaudeCodeRepo, initOpencodeRepo } from './agents/generateAgents'; +import { initClaudeCodeRepo, initOpencodeRepo, initVSCodeRepo } from './agents/generateAgents'; import type { ConfigCLIOverrides } from './common/ipc'; import type { TraceMode } from '../types/test'; @@ -179,9 +179,12 @@ function addInitAgentsCommand(program: Command) { command.description('Initialize repository agents for the Claude Code'); command.option('--claude', 'Initialize repository agents for the Claude Code'); command.option('--opencode', 'Initialize repository agents for the Opencode'); + command.option('--vscode', 'Initialize repository agents for the VS Code Copilot'); command.action(async opts => { if (opts.opencode) await initOpencodeRepo(); + else if (opts.vscode) + await initVSCodeRepo(); else await initClaudeCodeRepo(); }); From 45b487f3715007a033223531d0e1661aba0fb85e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 17 Sep 2025 12:26:04 -0700 Subject: [PATCH 179/329] feat(html): allow hiding copy prompt button (#37443) --- docs/src/test-reporters-js.md | 1 + packages/html-reporter/src/reportView.tsx | 8 +++--- packages/html-reporter/src/testCaseView.tsx | 7 ++--- packages/html-reporter/src/testFilesView.tsx | 2 +- packages/html-reporter/src/testResultView.tsx | 8 ++++-- packages/html-reporter/src/types.d.ts | 8 +++++- packages/playwright/src/reporters/html.ts | 27 ++++++++++++------- packages/playwright/types/test.d.ts | 11 +++++++- packages/web/src/shared/prompts.ts | 2 +- utils/generate_types/overrides-test.d.ts | 11 +++++++- 10 files changed, 63 insertions(+), 22 deletions(-) diff --git a/docs/src/test-reporters-js.md b/docs/src/test-reporters-js.md index f3393c6a25657..973cc1d64110a 100644 --- a/docs/src/test-reporters-js.md +++ b/docs/src/test-reporters-js.md @@ -252,6 +252,7 @@ HTML report supports the following configuration options and environment variabl | `PLAYWRIGHT_HTML_HOST` | `host` | When report opens in the browser, it will be served bound to this hostname. | `localhost` | `PLAYWRIGHT_HTML_PORT` | `port` | When report opens in the browser, it will be served on this port. | `9323` or any available port when `9323` is not available. | `PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL` | `attachmentsBaseURL` | A separate location where attachments from the `data` subdirectory are uploaded. Only needed when you upload report and `data` separately to different locations. | `data/` +| `PLAYWRIGHT_HTML_NO_COPY_PROMPT` | `noCopyPrompt` | If true, disable rendering of the Copy prompt for errors. Supports `true`, `1`, `false`, and `0`. | `false` | `PLAYWRIGHT_HTML_NO_SNIPPETS` | `noSnippets` | If true, disable rendering code snippets in the action log. If there is a top level error, that report section with code snippet will still render. Supports `true`, `1`, `false`, and `0`. | `false` ### Blob reporter diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index c68e130a5b533..7209ca929d760 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -52,7 +52,7 @@ export const ReportView: React.FC<{ const testId = searchParams.get('testId'); const q = searchParams.get('q')?.toString() || ''; const filterParam = q ? '&q=' + q : ''; - const reportTitle = report?.json()?.title; + const reportTitle = report?.json()?.options.title; const testIdToFileIdMap = React.useMemo(() => { const map = new Map(); @@ -181,10 +181,12 @@ const TestCaseViewLoader: React.FC<{
; } + const { projectNames, metadata, options } = report.json(); return
= ({ projectNames, test, testRunMetadata, run, next, prev }) => { + options?: HTMLReportOptions, +}> = ({ projectNames, test, testRunMetadata, run, next, prev, options }) => { const [selectedResultIndex, setSelectedResultIndex] = React.useState(run); const searchParams = React.useContext(SearchParamsContext); @@ -83,7 +84,7 @@ export const TestCaseView: React.FC<{ {!!visibleAnnotations.length && {visibleAnnotations.map((annotation, index) => )} } - + ; }, })) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} /> diff --git a/packages/html-reporter/src/testFilesView.tsx b/packages/html-reporter/src/testFilesView.tsx index efffa58a955f1..b19e2591b6cee 100644 --- a/packages/html-reporter/src/testFilesView.tsx +++ b/packages/html-reporter/src/testFilesView.tsx @@ -93,7 +93,7 @@ export const TestFilesHeader: React.FC<{ ; return <> - + {!isMetadataInTopLine && metadataToggleButton} {metadataVisible && } {!!report.errors.length && diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 8fa4ab017d4b4..1b5d7c6c87489 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -14,7 +14,7 @@ limitations under the License. */ -import type { TestAttachment, TestCase, TestResult, TestStep } from './types'; +import type { HTMLReportOptions, TestAttachment, TestCase, TestResult, TestStep } from './types'; import * as React from 'react'; import { TreeItem } from './treeItem'; import { msToString } from './utils'; @@ -74,7 +74,8 @@ export const TestResultView: React.FC<{ test: TestCase, result: TestResult, testRunMetadata: MetadataWithCommitInfo | undefined, -}> = ({ test, result, testRunMetadata }) => { + options?: HTMLReportOptions, +}> = ({ test, result, testRunMetadata, options }) => { const { screenshots, videos, traces, otherAttachments, diffs, errors, otherAttachmentAnchors, screenshotAnchors, errorContext } = React.useMemo(() => { const attachments = result.attachments.filter(a => !a.name.startsWith('_')); const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/'))); @@ -91,6 +92,9 @@ export const TestResultView: React.FC<{ }, [result]); const prompt = useAsyncMemo(async () => { + if (options?.noCopyPrompt) + return undefined; + const stdoutAttachment = result.attachments.find(a => a.name === 'stdout'); const stderrAttachment = result.attachments.find(a => a.name === 'stderr'); const stdout = stdoutAttachment?.body && stdoutAttachment.contentType === 'text/plain' ? stdoutAttachment.body : undefined; diff --git a/packages/html-reporter/src/types.d.ts b/packages/html-reporter/src/types.d.ts index 1acf73d780a2f..270a8cb9838c5 100644 --- a/packages/html-reporter/src/types.d.ts +++ b/packages/html-reporter/src/types.d.ts @@ -36,15 +36,21 @@ export type Location = { column: number; }; +export type HTMLReportOptions = { + title?: string; + noCopyPrompt?: boolean; + noSnippets?: boolean; +}; + export type HTMLReport = { metadata: Metadata; - title: string | undefined; files: TestFileSummary[]; stats: Stats; projectNames: string[]; startTime: number; duration: number; errors: string[]; // Top-level errors that are not attributed to any test. + options: HTMLReportOptions; }; export type TestFile = { diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 5e92291f8b0b1..04be226d86426 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -31,7 +31,7 @@ import { resolveReporterOutputPath, stripAnsiEscapes } from '../util'; import type { ReporterV2 } from './reporterV2'; import type { HtmlReporterOptions as HtmlReporterConfigOptions, Metadata, TestAnnotation } from '../../types/test'; import type * as api from '../../types/testReporter'; -import type { HTMLReport, Location, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types'; +import type { HTMLReport, HTMLReportOptions, Location, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types'; import type { ZipFile } from 'playwright-core/lib/zipBundle'; import type { TransformCallback } from 'stream'; @@ -131,7 +131,18 @@ class HtmlReporter implements ReporterV2 { noSnippets = true; noSnippets = noSnippets || this._options.noSnippets; - const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, process.env.PLAYWRIGHT_HTML_TITLE || this._options.title, noSnippets); + let noCopyPrompt: boolean | undefined; + if (process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT === 'false' || process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT === '0') + noCopyPrompt = false; + else if (process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT) + noCopyPrompt = true; + noCopyPrompt = noCopyPrompt || this._options.noCopyPrompt; + + const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, { + title: process.env.PLAYWRIGHT_HTML_TITLE || this._options.title, + noSnippets, + noCopyPrompt, + }); this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors); } @@ -228,17 +239,15 @@ class HtmlBuilder { private _dataZipFile: ZipFile; private _hasTraces = false; private _attachmentsBaseURL: string; - private _title: string | undefined; - private _noSnippets: boolean; + private _options: HTMLReportOptions; - constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string, title: string | undefined, noSnippets: boolean = false) { + constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string, options: HTMLReportOptions) { this._config = config; this._reportFolder = outputDir; - this._noSnippets = noSnippets; + this._options = options; fs.mkdirSync(this._reportFolder, { recursive: true }); this._dataZipFile = new yazl.ZipFile(); this._attachmentsBaseURL = attachmentsBaseURL; - this._title = title; } async build(metadata: Metadata, projectSuites: api.Suite[], result: api.FullResult, topLevelErrors: api.TestError[]): Promise<{ ok: boolean, singleTestId: string | undefined }> { @@ -264,7 +273,7 @@ class HtmlBuilder { } } } - if (!this._noSnippets) + if (!this._options.noSnippets) createSnippets(this._stepsInFile); let ok = true; @@ -296,13 +305,13 @@ class HtmlBuilder { } const htmlReport: HTMLReport = { metadata, - title: this._title, startTime: result.startTime.getTime(), duration: result.duration, files: [...data.values()].map(e => e.testFileSummary), projectNames: projectSuites.map(r => r.project()!.name), stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()) }, errors: topLevelErrors.map(error => formatError(internalScreen, error).message), + options: this._options, }; htmlReport.files.sort((f1, f2) => { const w1 = f1.stats.unexpected * 1000 + f1.stats.flaky; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 71a1ac0cdc4b6..91c168cd734d8 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -22,7 +22,16 @@ export type BlobReporterOptions = { outputDir?: string, fileName?: string }; export type ListReporterOptions = { printSteps?: boolean }; export type JUnitReporterOptions = { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean }; export type JsonReporterOptions = { outputFile?: string }; -export type HtmlReporterOptions = { outputFolder?: string, open?: 'always' | 'never' | 'on-failure', host?: string, port?: number, attachmentsBaseURL?: string, title?: string, noSnippets?: boolean }; +export type HtmlReporterOptions = { + outputFolder?: string; + open?: 'always' | 'never' | 'on-failure'; + host?: string; + port?: number; + attachmentsBaseURL?: string; + title?: string; + noSnippets?: boolean; + noCopyPrompt?: boolean; +}; export type ReporterDescription = Readonly< ['blob'] | ['blob', BlobReporterOptions] | diff --git a/packages/web/src/shared/prompts.ts b/packages/web/src/shared/prompts.ts index 1abe7cfcc0c0d..f6807af40d962 100644 --- a/packages/web/src/shared/prompts.ts +++ b/packages/web/src/shared/prompts.ts @@ -42,7 +42,7 @@ export async function copyPrompt({ buildCodeFrame(error: ErrorInfo): Promise; stdout?: string; stderr?: string; -}) { +}): Promise { const meaningfulSingleLineErrors = new Set(errors.filter(e => e.message && !e.message.includes('\n')).map(e => e.message!)); for (const error of errors) { for (const singleLineError of meaningfulSingleLineErrors.keys()) { diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 03f9babb43c77..665c7ecd32d12 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -21,7 +21,16 @@ export type BlobReporterOptions = { outputDir?: string, fileName?: string }; export type ListReporterOptions = { printSteps?: boolean }; export type JUnitReporterOptions = { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean }; export type JsonReporterOptions = { outputFile?: string }; -export type HtmlReporterOptions = { outputFolder?: string, open?: 'always' | 'never' | 'on-failure', host?: string, port?: number, attachmentsBaseURL?: string, title?: string, noSnippets?: boolean }; +export type HtmlReporterOptions = { + outputFolder?: string; + open?: 'always' | 'never' | 'on-failure'; + host?: string; + port?: number; + attachmentsBaseURL?: string; + title?: string; + noSnippets?: boolean; + noCopyPrompt?: boolean; +}; export type ReporterDescription = Readonly< ['blob'] | ['blob', BlobReporterOptions] | From cb2d76ee8190db5f72bd3ae196a15c90f4e8c230 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 17 Sep 2025 13:33:05 -0700 Subject: [PATCH 180/329] chore(mcp): generate single mcp entry in the config (#37458) --- .../agents/playwright-test-generator.md | 2 +- .../.claude/agents/playwright-test-planner.md | 4 +- examples/todomvc/.mcp.json | 16 +--- examples/todomvc/prompt.claudecode.md | 2 +- .../playwright/src/agents/generateAgents.ts | 76 ++++++++----------- packages/playwright/src/agents/generator.md | 39 +++++----- .../src/agents/{fixer.md => healer.md} | 31 ++++---- packages/playwright/src/agents/planner.md | 43 +++++------ 8 files changed, 87 insertions(+), 126 deletions(-) rename packages/playwright/src/agents/{fixer.md => healer.md} (80%) diff --git a/examples/todomvc/.claude/agents/playwright-test-generator.md b/examples/todomvc/.claude/agents/playwright-test-generator.md index 772dd180f8f11..a22b2d022f0fd 100644 --- a/examples/todomvc/.claude/agents/playwright-test-generator.md +++ b/examples/todomvc/.claude/agents/playwright-test-generator.md @@ -1,7 +1,7 @@ --- name: playwright-test-generator description: Use this agent when you need to create automated browser tests using Playwright. Examples: Context: User wants to test a login flow on their web application. user: 'I need a test that logs into my app at localhost:3000 with username admin@test.com and password 123456, then verifies the dashboard page loads' assistant: 'I'll use the playwright-test-generator agent to create and validate this login test for you' The user needs a specific browser automation test created, which is exactly what the playwright-test-generator agent is designed for. Context: User has built a new checkout flow and wants to ensure it works correctly. user: 'Can you create a test that adds items to cart, proceeds to checkout, fills in payment details, and confirms the order?' assistant: 'I'll use the playwright-test-generator agent to build a comprehensive checkout flow test' This is a complex user journey that needs to be automated and tested, perfect for the playwright-test-generator agent. -tools: Glob, Grep, Read, Write, mcp__playwright-0d2f__browser_click, mcp__playwright-0d2f__browser_drag, mcp__playwright-0d2f__browser_evaluate, mcp__playwright-0d2f__browser_file_upload, mcp__playwright-0d2f__browser_handle_dialog, mcp__playwright-0d2f__browser_hover, mcp__playwright-0d2f__browser_navigate, mcp__playwright-0d2f__browser_press_key, mcp__playwright-0d2f__browser_select_option, mcp__playwright-0d2f__browser_snapshot, mcp__playwright-0d2f__browser_type, mcp__playwright-0d2f__browser_verify_element_visible, mcp__playwright-0d2f__browser_verify_list_visible, mcp__playwright-0d2f__browser_verify_text_visible, mcp__playwright-0d2f__browser_verify_value, mcp__playwright-0d2f__browser_wait_for, mcp__playwright-0d2f__test_setup_page +tools: Glob, Grep, Read, Write, mcp__playwright-test__browser_click, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_verify_element_visible, mcp__playwright-test__browser_verify_list_visible, mcp__playwright-test__browser_verify_text_visible, mcp__playwright-test__browser_verify_value, mcp__playwright-test__browser_wait_for, mcp__playwright-test__test_setup_page model: sonnet color: blue --- diff --git a/examples/todomvc/.claude/agents/playwright-test-planner.md b/examples/todomvc/.claude/agents/playwright-test-planner.md index 1f233162f5aa7..0cd8fdaaff4c3 100644 --- a/examples/todomvc/.claude/agents/playwright-test-planner.md +++ b/examples/todomvc/.claude/agents/playwright-test-planner.md @@ -1,7 +1,7 @@ --- name: playwright-test-planner description: Use this agent when you need to create comprehensive test plan for a web application or website. Examples: Context: User wants to test a new e-commerce checkout flow. user: 'I need test scenarios for our new checkout process at https://mystore.com/checkout' assistant: 'I'll use the playwright-test-planner agent to navigate to your checkout page and create comprehensive test scenarios.' The user needs test planning for a specific web page, so use the playwright-test-planner agent to explore and create test scenarios. Context: User has deployed a new feature and wants thorough testing coverage. user: 'Can you help me test our new user dashboard at https://app.example.com/dashboard?' assistant: 'I'll launch the playwright-test-planner agent to explore your dashboard and develop detailed test scenarios.' This requires web exploration and test scenario creation, perfect for the playwright-test-planner agent. -tools: Glob, Grep, Read, Write, mcp__playwright-396e__browser_click, mcp__playwright-396e__browser_close, mcp__playwright-396e__browser_console_messages, mcp__playwright-396e__browser_drag, mcp__playwright-396e__browser_evaluate, mcp__playwright-396e__browser_file_upload, mcp__playwright-396e__browser_handle_dialog, mcp__playwright-396e__browser_hover, mcp__playwright-396e__browser_navigate, mcp__playwright-396e__browser_navigate_back, mcp__playwright-396e__browser_network_requests, mcp__playwright-396e__browser_press_key, mcp__playwright-396e__browser_select_option, mcp__playwright-396e__browser_snapshot, mcp__playwright-396e__browser_take_screenshot, mcp__playwright-396e__browser_type, mcp__playwright-396e__browser_wait_for, mcp__playwright-396e__test_setup_page +tools: Glob, Grep, Read, Write, mcp__playwright-test__browser_click, mcp__playwright-test__browser_close, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_navigate_back, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_take_screenshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_wait_for, mcp__playwright-test__test_setup_page model: sonnet color: green --- @@ -10,7 +10,7 @@ You are an expert web test planner with extensive experience in quality assuranc When given a target web page or application, you will: -1. **Navigate and Explore**: +1. **Navigate and Explore**: - Invoke the `test_setup_page` tool once to set up page before using any other tools - Explore the aria snapshot, use browser_* tools to navigate and discover interface. - Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality diff --git a/examples/todomvc/.mcp.json b/examples/todomvc/.mcp.json index 3472f022fa72c..d9dfc1253b94a 100644 --- a/examples/todomvc/.mcp.json +++ b/examples/todomvc/.mcp.json @@ -1,20 +1,6 @@ { "mcpServers": { - "playwright-df91": { - "command": "npx", - "args": [ - "playwright", - "run-test-mcp-server" - ] - }, - "playwright-0d2f": { - "command": "npx", - "args": [ - "playwright", - "run-test-mcp-server" - ] - }, - "playwright-396e": { + "playwright-test": { "command": "npx", "args": [ "playwright", diff --git a/examples/todomvc/prompt.claudecode.md b/examples/todomvc/prompt.claudecode.md index 99f51b7c479d4..3be701148c070 100644 --- a/examples/todomvc/prompt.claudecode.md +++ b/examples/todomvc/prompt.claudecode.md @@ -10,4 +10,4 @@ Test basic functionality of todo app. - For each scenario in `specs/test-plan.md`, use `playwright-test-generator` subagent to perform the scenario and generate the test source code into `tests/` folder. -- Use `playwright-test-fixer` subagent to fix the failing tests. +- Use `playwright-test-healer` subagent to fix the failing tests. diff --git a/packages/playwright/src/agents/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts index 30dd5c964494b..211aab6c573dc 100644 --- a/packages/playwright/src/agents/generateAgents.ts +++ b/packages/playwright/src/agents/generateAgents.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; import { yaml } from 'playwright-core/lib/utilsBundle'; @@ -25,15 +24,8 @@ interface AgentHeader { model: string; color: string; tools: string[]; - 'mcp-servers': Record; } -interface McpServerConfig { - type: string; - command: string; - args: string[]; - tools: string[]; -} interface Agent { header: AgentHeader; @@ -111,21 +103,28 @@ const claudeToolMap = new Map([ ['write', ['Write']], ]); +// Common MCP server configurations +const commonMcpServers = { + playwrightTest: { + type: 'local', + command: 'npx', + args: ['playwright', 'run-test-mcp-server'] + } +}; + function saveAsClaudeCode(agent: Agent): string { - function asClaudeTool(hash: string, tool: string): string { + function asClaudeTool(tool: string): string { const [first, second] = tool.split('/'); if (!second) return (claudeToolMap.get(first) || [first]).join(', '); - return `mcp__${first}-${hash}__${second}`; + return `mcp__${first}__${second}`; } - const hash = shortHash(agent.header.name); - const lines: string[] = []; lines.push(`---`); lines.push(`name: ${agent.header.name}`); lines.push(`description: ${agent.header.description}. Examples: ${agent.examples.map(example => `${example}`).join('')}`); - lines.push(`tools: ${agent.header.tools.map(tool => asClaudeTool(hash, tool)).join(', ')}`); + lines.push(`tools: ${agent.header.tools.map(tool => asClaudeTool(tool)).join(', ')}`); lines.push(`model: ${agent.header.model}`); lines.push(`color: ${agent.header.color}`); lines.push(`---`); @@ -143,13 +142,13 @@ const opencodeToolMap = new Map([ ]); function saveAsOpencodeJson(agents: Agent[]): string { - function asOpencodeTool(tools: Record, hash: string, tool: string): void { + function asOpencodeTool(tools: Record, tool: string): void { const [first, second] = tool.split('/'); if (!second) { for (const tool of opencodeToolMap.get(first) || [first]) tools[tool] = true; } else { - tools[`${first}-${hash}*${second}`] = true; + tools[`${first}*${second}`] = true; } } @@ -161,7 +160,6 @@ function saveAsOpencodeJson(agents: Agent[]): string { }; result['agent'] = {}; for (const agent of agents) { - const hash = shortHash(agent.header.name); const tools: Record = {}; result['agent'][agent.header.name] = { description: agent.header.description, @@ -170,21 +168,17 @@ function saveAsOpencodeJson(agents: Agent[]): string { tools, }; for (const tool of agent.header.tools) - asOpencodeTool(tools, hash, tool); - - for (const [name, mcp] of Object.entries(agent.header['mcp-servers'])) { - result['mcp'][name + '-' + hash] = { - type: mcp.type, - command: [mcp.command, ...mcp.args], - enabled: true, - }; - } + asOpencodeTool(tools, tool); } - return JSON.stringify(result, null, 2); -} -function shortHash(str: string): string { - return crypto.createHash('sha256').update(str).digest('hex').slice(0, 4); + const server = commonMcpServers.playwrightTest; + result['mcp']['playwright-test'] = { + type: server.type, + command: [server.command, ...server.args], + enabled: true, + }; + + return JSON.stringify(result, null, 2); } async function loadAgents(): Promise { @@ -205,18 +199,14 @@ export async function initClaudeCodeRepo() { for (const agent of agents) await writeFile(`.claude/agents/${agent.header.name}.md`, saveAsClaudeCode(agent)); - const mcpServers: Record = {}; - for (const agent of agents) { - const hash = shortHash(agent.header.name); - for (const [name, mcp] of Object.entries(agent.header['mcp-servers'])) { - const entry = { - command: mcp.command, - args: mcp.args, - }; - mcpServers[name + '-' + hash] = entry; + await writeFile('.mcp.json', JSON.stringify({ + mcpServers: { + 'playwright-test': { + command: commonMcpServers.playwrightTest.command, + args: commonMcpServers.playwrightTest.args, + } } - } - await writeFile('.mcp.json', JSON.stringify({ mcpServers }, null, 2)); + }, null, 2)); } const vscodeToolMap = new Map([ @@ -267,10 +257,10 @@ export async function initVSCodeRepo() { if (!mcpJson.servers) mcpJson.servers = {}; - mcpJson.servers['playwright-test-mcp'] = { + mcpJson.servers['playwright-test'] = { type: 'stdio', - command: 'npx', - args: ['playwright', 'run-test-mcp-server'], + command: commonMcpServers.playwrightTest.command, + args: commonMcpServers.playwrightTest.args, }; await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2)); } diff --git a/packages/playwright/src/agents/generator.md b/packages/playwright/src/agents/generator.md index c85e3c9ac6a21..f5abc27db5cb3 100644 --- a/packages/playwright/src/agents/generator.md +++ b/packages/playwright/src/agents/generator.md @@ -8,28 +8,23 @@ tools: - grep - read - write - - playwright/browser_click - - playwright/browser_drag - - playwright/browser_evaluate - - playwright/browser_file_upload - - playwright/browser_handle_dialog - - playwright/browser_hover - - playwright/browser_navigate - - playwright/browser_press_key - - playwright/browser_select_option - - playwright/browser_snapshot - - playwright/browser_type - - playwright/browser_verify_element_visible - - playwright/browser_verify_list_visible - - playwright/browser_verify_text_visible - - playwright/browser_verify_value - - playwright/browser_wait_for - - playwright/test_setup_page -mcp-servers: - playwright: - type: 'local' - command: 'npx' - args: ['playwright', 'run-test-mcp-server'] + - playwright-test/browser_click + - playwright-test/browser_drag + - playwright-test/browser_evaluate + - playwright-test/browser_file_upload + - playwright-test/browser_handle_dialog + - playwright-test/browser_hover + - playwright-test/browser_navigate + - playwright-test/browser_press_key + - playwright-test/browser_select_option + - playwright-test/browser_snapshot + - playwright-test/browser_type + - playwright-test/browser_verify_element_visible + - playwright-test/browser_verify_list_visible + - playwright-test/browser_verify_text_visible + - playwright-test/browser_verify_value + - playwright-test/browser_wait_for + - playwright-test/test_setup_page --- You are a Playwright Test Generator, an expert in browser automation and end-to-end testing. Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate application behavior. diff --git a/packages/playwright/src/agents/fixer.md b/packages/playwright/src/agents/healer.md similarity index 80% rename from packages/playwright/src/agents/fixer.md rename to packages/playwright/src/agents/healer.md index 4f159ea4b2c8d..c2c826f68afa1 100644 --- a/packages/playwright/src/agents/fixer.md +++ b/packages/playwright/src/agents/healer.md @@ -1,5 +1,5 @@ --- -name: playwright-test-fixer +name: playwright-test-healer description: Use this agent when you need to debug and fix failing Playwright tests color: red model: sonnet @@ -9,20 +9,15 @@ tools: - read - write - edit - - playwright/browser_evaluate - - playwright/browser_generate_locator - - playwright/browser_snapshot - - playwright/test_debug - - playwright/test_list - - playwright/test_run -mcp-servers: - playwright: - type: 'local' - command: 'npx' - args: ['playwright', 'run-test-mcp-server'] + - playwright-test/browser_evaluate + - playwright-test/browser_generate_locator + - playwright-test/browser_snapshot + - playwright-test/test_debug + - playwright-test/test_list + - playwright-test/test_run --- -You are the Playwright Test Fixer, an expert test automation engineer specializing in debugging and +You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix broken Playwright tests using a methodical approach. @@ -42,7 +37,7 @@ Your workflow: - Updating selectors to match current application state - Fixing assertions and expected values - Improving test reliability and maintainability - - For inherently dynamic data, utilize regular expressions to produce resilient locators + - For inherently dynamic data, utilize regular expressions to produce resilient locators 6. **Verification**: Restart the test after each fix to validate the changes 7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly @@ -61,19 +56,19 @@ Key principles: Context: A developer has a failing Playwright test that needs to be debugged and fixed. user: 'The login test is failing, can you fix it?' - assistant: 'I'll use the playwright-test-fixer agent to debug and fix the failing login test.' + assistant: 'I'll use the playwright-test-healer agent to debug and fix the failing login test.' The user has identified a specific failing test that needs debugging and fixing, which is exactly what the - playwright-test-fixer agent is designed for. + playwright-test-healer agent is designed for. Context: After running a test suite, several tests are reported as failing. user: 'Test user-registration.spec.ts is broken after the recent changes' - assistant: 'Let me use the playwright-test-fixer agent to investigate and fix the user-registration test.' + assistant: 'Let me use the playwright-test-healer agent to investigate and fix the user-registration test.' A specific test file is failing and needs debugging, which requires the systematic approach of the - playwright-test-fixer agent. + playwright-test-healer agent. diff --git a/packages/playwright/src/agents/planner.md b/packages/playwright/src/agents/planner.md index c6b94691f2dfc..98ecb1cc8c224 100644 --- a/packages/playwright/src/agents/planner.md +++ b/packages/playwright/src/agents/planner.md @@ -8,36 +8,31 @@ tools: - grep - read - write - - playwright/browser_click - - playwright/browser_close - - playwright/browser_console_messages - - playwright/browser_drag - - playwright/browser_evaluate - - playwright/browser_file_upload - - playwright/browser_handle_dialog - - playwright/browser_hover - - playwright/browser_navigate - - playwright/browser_navigate_back - - playwright/browser_network_requests - - playwright/browser_press_key - - playwright/browser_select_option - - playwright/browser_snapshot - - playwright/browser_take_screenshot - - playwright/browser_type - - playwright/browser_wait_for - - playwright/test_setup_page -mcp-servers: - playwright: - type: 'local' - command: 'npx' - args: ['playwright', 'run-test-mcp-server'] + - playwright-test/browser_click + - playwright-test/browser_close + - playwright-test/browser_console_messages + - playwright-test/browser_drag + - playwright-test/browser_evaluate + - playwright-test/browser_file_upload + - playwright-test/browser_handle_dialog + - playwright-test/browser_hover + - playwright-test/browser_navigate + - playwright-test/browser_navigate_back + - playwright-test/browser_network_requests + - playwright-test/browser_press_key + - playwright-test/browser_select_option + - playwright-test/browser_snapshot + - playwright-test/browser_take_screenshot + - playwright-test/browser_type + - playwright-test/browser_wait_for + - playwright-test/test_setup_page --- You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test scenario design. Your expertise includes functional testing, usability testing, edge case identification, and comprehensive test coverage planning. When given a target web page or application, you will: -1. **Navigate and Explore**: +1. **Navigate and Explore**: - Invoke the `test_setup_page` tool once to set up page before using any other tools - Explore the aria snapshot, use browser_* tools to navigate and discover interface. - Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality From 5397ae1c4ddf666a7cd9342ace757ec50576d753 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 17 Sep 2025 23:33:16 +0200 Subject: [PATCH 181/329] chore(flakiness-dashboard): use ManagedIdentityCredential instead of DefaultAzureCredential (#37453) --- utils/flakiness-dashboard/processing/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/flakiness-dashboard/processing/utils.js b/utils/flakiness-dashboard/processing/utils.js index a0f166f7e5473..06cd2b09dec67 100644 --- a/utils/flakiness-dashboard/processing/utils.js +++ b/utils/flakiness-dashboard/processing/utils.js @@ -14,9 +14,9 @@ * limitations under the License. */ // @ts-check -const { DefaultAzureCredential } = require('@azure/identity'); +const { ManagedIdentityCredential } = require('@azure/identity'); const { BlobServiceClient } = require('@azure/storage-blob'); -const defaultAzureCredential = new DefaultAzureCredential(); +const defaultAzureCredential = new ManagedIdentityCredential(); const zlib = require('zlib'); const util = require('util'); From 7765583dc806afe83830f4b2bd9927979415d2be Mon Sep 17 00:00:00 2001 From: Loud QA <51411715+emyjamalian@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:36:21 +0200 Subject: [PATCH 182/329] fix(core): prevent concurrent downloads clobbering by using unique temp dirs (#37335) --- .../src/server/registry/browserFetcher.ts | 11 +++-- .../installation/concurrent-download.spec.ts | 40 +++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 tests/installation/concurrent-download.spec.ts diff --git a/packages/playwright-core/src/server/registry/browserFetcher.ts b/packages/playwright-core/src/server/registry/browserFetcher.ts index acd3a52f143fd..64000580f73b3 100644 --- a/packages/playwright-core/src/server/registry/browserFetcher.ts +++ b/packages/playwright-core/src/server/registry/browserFetcher.ts @@ -24,7 +24,7 @@ import { debugLogger } from '../utils/debugLogger'; import { ManualPromise } from '../../utils/isomorphic/manualPromise'; import { getUserAgent } from '../utils/userAgent'; import { progress as ProgressBar, colors } from '../../utilsBundle'; -import { existsAsync } from '../utils/fileUtils'; +import { existsAsync, removeFolders } from '../utils/fileUtils'; import { browserDirectoryToMarkerFilePath } from '.'; @@ -37,7 +37,10 @@ export async function downloadBrowserWithProgressBar(title: string, browserDirec return false; } - const zipPath = path.join(os.tmpdir(), downloadFileName); + // Create a unique temporary directory for this download to prevent concurrent downloads from clobbering each other + const uniqueTempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-download-')); + const zipPath = path.join(uniqueTempDir, downloadFileName); + try { const retryCount = 5; for (let attempt = 1; attempt <= retryCount; ++attempt) { @@ -63,8 +66,8 @@ export async function downloadBrowserWithProgressBar(title: string, browserDirec process.exitCode = 1; throw e; } finally { - if (await existsAsync(zipPath)) - await fs.promises.unlink(zipPath); + // Clean up the temporary directory and its contents + await removeFolders([uniqueTempDir]); } logPolitely(`${title} downloaded to ${browserDirectory}`); return true; diff --git a/tests/installation/concurrent-download.spec.ts b/tests/installation/concurrent-download.spec.ts new file mode 100644 index 0000000000000..e448087be82bf --- /dev/null +++ b/tests/installation/concurrent-download.spec.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test } from './npmTest'; +import fs from 'fs'; +test.use({ isolateBrowsers: true }); + +test('concurrent browser downloads should not clobber each other', async ({ exec }, testInfo) => { + await exec('npm init -y'); + await exec('npm install playwright'); + const numProcesses = 3; + await Promise.all(Array.from({ length: numProcesses }, async (_, index) => { + const browserPath = testInfo.outputPath(`browsers-${index}`); + await exec('npx playwright install chromium', { + env: { + PLAYWRIGHT_BROWSERS_PATH: browserPath, + } + }); + + // Check that each installation has all required binaries + const entries = await fs.promises.readdir(browserPath).catch(() => []); + const installed = new Set(entries.map(entry => entry.split('-')[0].replace(/_/g, '-')).filter(name => !name.startsWith('.'))); + test.expect(installed.has('chromium')).toBeTruthy(); + test.expect(installed.has('chromium-headless-shell')).toBeTruthy(); + test.expect(installed.has('ffmpeg')).toBeTruthy(); + })); +}); From ea235b31ef2435d149a0ad4bd21451facf7f9cc7 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 17 Sep 2025 15:54:11 -0700 Subject: [PATCH 183/329] chore: regenerate agents in examples (#37460) --- ...ywright-test-fixer.md => playwright-test-healer.md} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename examples/todomvc/.claude/agents/{playwright-test-fixer.md => playwright-test-healer.md} (68%) diff --git a/examples/todomvc/.claude/agents/playwright-test-fixer.md b/examples/todomvc/.claude/agents/playwright-test-healer.md similarity index 68% rename from examples/todomvc/.claude/agents/playwright-test-fixer.md rename to examples/todomvc/.claude/agents/playwright-test-healer.md index 5fab3059e823d..d6500a090b08a 100644 --- a/examples/todomvc/.claude/agents/playwright-test-fixer.md +++ b/examples/todomvc/.claude/agents/playwright-test-healer.md @@ -1,12 +1,12 @@ --- -name: playwright-test-fixer -description: Use this agent when you need to debug and fix failing Playwright tests. Examples: Context: A developer has a failing Playwright test that needs to be debugged and fixed. user: 'The login test is failing, can you fix it?' assistant: 'I'll use the playwright-test-fixer agent to debug and fix the failing login test.' The user has identified a specific failing test that needs debugging and fixing, which is exactly what the playwright-test-fixer agent is designed for. Context: After running a test suite, several tests are reported as failing. user: 'Test user-registration.spec.ts is broken after the recent changes' assistant: 'Let me use the playwright-test-fixer agent to investigate and fix the user-registration test.' A specific test file is failing and needs debugging, which requires the systematic approach of the playwright-test-fixer agent. -tools: Glob, Grep, Read, Write, Edit, MultiEdit, NotebookEdit, mcp__playwright-df91__browser_evaluate, mcp__playwright-df91__browser_generate_locator, mcp__playwright-df91__browser_snapshot, mcp__playwright-df91__test_debug, mcp__playwright-df91__test_list, mcp__playwright-df91__test_run, mcp__playwright-df91__test_setup_page +name: playwright-test-healer +description: Use this agent when you need to debug and fix failing Playwright tests. Examples: Context: A developer has a failing Playwright test that needs to be debugged and fixed. user: 'The login test is failing, can you fix it?' assistant: 'I'll use the playwright-test-healer agent to debug and fix the failing login test.' The user has identified a specific failing test that needs debugging and fixing, which is exactly what the playwright-test-healer agent is designed for. Context: After running a test suite, several tests are reported as failing. user: 'Test user-registration.spec.ts is broken after the recent changes' assistant: 'Let me use the playwright-test-healer agent to investigate and fix the user-registration test.' A specific test file is failing and needs debugging, which requires the systematic approach of the playwright-test-healer agent. +tools: Glob, Grep, Read, Write, Edit, MultiEdit, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_generate_locator, mcp__playwright-test__browser_snapshot, mcp__playwright-test__test_debug, mcp__playwright-test__test_list, mcp__playwright-test__test_run model: sonnet color: red --- -You are the Playwright Test Fixer, an expert test automation engineer specializing in debugging and +You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix broken Playwright tests using a methodical approach. @@ -26,7 +26,7 @@ Your workflow: - Updating selectors to match current application state - Fixing assertions and expected values - Improving test reliability and maintainability - - For inherently dynamic data, utilize regular expressions to produce resilient locators + - For inherently dynamic data, utilize regular expressions to produce resilient locators 6. **Verification**: Restart the test after each fix to validate the changes 7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly From cde6c43868be94f1ee31e468c0cd06e200eff5c3 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 17 Sep 2025 17:02:25 -0700 Subject: [PATCH 184/329] chore: add test agents docs (#37461) --- docs/src/test-agents-js.md | 217 ++++++++++++++++++++++++++++++ docs/src/test-configuration-js.md | 2 +- docs/src/test-use-options-js.md | 2 +- utils/doclint/cli.js | 1 + 4 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 docs/src/test-agents-js.md diff --git a/docs/src/test-agents-js.md b/docs/src/test-agents-js.md new file mode 100644 index 0000000000000..15b65370340bc --- /dev/null +++ b/docs/src/test-agents-js.md @@ -0,0 +1,217 @@ +--- +id: test-agents +title: "Agents" +--- + +# Playwright Agents + +## Test Coverage in 1-2-3 + +Playwright’s agentic workflow makes it possible to generate test coverage in three straightforward steps. +These steps can be performed independently, manually, or as chained calls in an agentic loop. + +1. **Plan**: A planning agent explores the app and produces a test plan in `specs/*.md`. + +2. **Generate**: A generating agent transforms the plan into `tests/*.spec.ts` files. It executes actions live to verify selectors and flows, then emits testing code and assertions. + +3. **Heal**: A healing agent executes the test suite and automatically repairs failing tests by applying diffs in place. + +## 1. Plan + +**Input** + +* A clear request (e.g., “Generate a plan for guest checkout.”) +* A live entry point (URL) or a seed Playwright test that sets up the environment +* PRD (optional) + +**Prompt** + +```markdown +Ask `playwright-test-planner` agent to generate a test plan for "Guest Checkout" scenario. +Use `seed.spec.ts` as a seed test for the plan. +``` + +**Output** + +* A Markdown test plan saved to `specs/guest-checkout.md`. The plan is human-readable but precise enough for test generation. + +
+Example: specs/guest-checkout.md + +```markdown +# Feature: Guest Checkout + +## Purpose +Allow a user to purchase without creating an account. + +## Preconditions +- Test seed `tests/seed.spec.ts`. +- Payment sandbox credentials available via env vars. + +## Scenarios + +### SC-1: Add single item to cart and purchase +**Steps** +1. Open home page. +2. Search for "Wireless Mouse". +3. Open product page and add to cart. +4. Proceed to checkout as guest. +5. Fill shipping and payment details. +6. Confirm order. + +**Expected** +- Cart count increments after item is added. +- Checkout page shows item, price, tax, and total. +- Order confirmation number appears; status is "Processing". + +### SC-2: Tax and shipping recalculation on address change +**Steps** +1. Start checkout with a CA address. +2. Change state to NY. + +**Expected** +- Tax and shipping values recalculate. + +## Data +- Product SKU: `WM-123` +- Payment: sandbox card `4111 1111 1111 1111`, valid expiry, CVV `123`. + +## Methodology +*Optional notes about testing methodology* +``` +
+ +## 2. Generate + +The generating agent uses the Markdown plan to produce executable Playwright tests. +It verifies selectors and assertions live against the application. Playwright supports +generation hints and provides a catalog of assertions for efficient structural and +behavioral validation. + +**Input** + +* Markdown plan from `specs/` + +**Prompt** + +```markdown +Ask `playwright-test-generator` to generate tests for the guest checkout plan under `specs/`. +``` + +**Output** + +* A test suite under `tests/` +* Tests may include initial errors that can be healed automatically + +
+Example: tests/guest-checkout.spec.ts + +```ts +import { test, expect } from '@playwright/test'; + +test.describe('Guest Checkout', () => { + test('SC-1: add item and purchase', async ({ page }) => { + await page.goto('/'); + await page.getByRole('searchbox', { name: /search/i }).fill('Wireless Mouse'); + await page.getByRole('button', { name: /search/i }).click(); + + await page.getByRole('link', { name: /wireless mouse/i }).click(); + await page.getByRole('button', { name: /add to cart/i }).click(); + + // Assertion: cart badge increments + await expect(page.getByTestId('cart-badge')).toHaveText('1'); + + await page.getByRole('link', { name: /checkout/i }).click(); + await page.getByRole('button', { name: /continue as guest/i }).click(); + + // Fill checkout form + await page.getByLabel('Email').fill(process.env.CHECKOUT_EMAIL!); + await page.getByLabel('Full name').fill('Alex Guest'); + await page.getByLabel('Address').fill('1 Market St'); + await page.getByLabel('City').fill('San Francisco'); + await page.getByLabel('State').selectOption('CA'); + await page.getByLabel('ZIP').fill('94105'); + + // Payment (sandbox) + const frame = page.frameLocator('[data-testid="card-iframe"]'); + await frame.getByLabel('Card number').fill('4111111111111111'); + await frame.getByLabel('MM / YY').fill('12/30'); + await frame.getByLabel('CVC').fill('123'); + + await page.getByRole('button', { name: /pay/i }).click(); + + // Assertions: confirmation invariants + await expect(page).toHaveURL(/\/orders\/\w+\/confirmation/); + await expect(page.getByRole('heading', { name: /thank you/i })).toBeVisible(); + await expect(page.getByTestId('order-status')).toHaveText(/processing/i); + + // Optional visual check + await expect(page.locator('[data-testid="order-summary"]')).toHaveScreenshot(); + }); +}); +``` +
+ +## 3. Heal + +When a test fails, the healing agent: + +* Replays the failing steps +* Inspects the current UI to locate equivalent elements or flows +* Suggests a patch (e.g., locator update, wait adjustment, data fix) +* Re-runs the test until it passes or until guardrails stop the loop + +**Input** + +* Failing test name + +**Prompt** + +```markdown +Ask `playwright-test-healer` to fix all failing tests for the guest checkout scenario. +``` + +**Output** + +* A passing test, or a skipped test if the functionality is broken + +## Artifacts and Conventions + +Follow a simple, auditable structure: + +```bash +repo/ + .{claude|copilot|vscode|...}/ # agent definitions, tools, guardrails + specs/ # human-readable test plans + checkout-guest.md + account-settings.md + tests/ # generated Playwright tests + seed.spec.ts + checkout-guest.spec.ts + account-settings.spec.ts + playwright.config.ts +``` + +### Agents definitions + +Agent definitions are collections of instructions and MCP tools. They are provided by +Playwright and should be regenerated whenever Playwright is updated. + +Example for Claude Code subagents: + +```bash +npx playwright init-agents --claude +``` + +### Specs in `specs/` + +Specs are structured plans describing scenarios in human-readable terms. They include +steps, expected outcomes, and data. Specs can start from scratch or extend a seed test. + +### Tests in `tests/` + +Generated Playwright tests, aligned one-to-one with specs wherever feasible. + +### Seed tests `seed.spec.ts` + +Seed tests provide a ready-to-use `page` context to bootstrap execution. diff --git a/docs/src/test-configuration-js.md b/docs/src/test-configuration-js.md index e3831c19112d6..52e68c658c390 100644 --- a/docs/src/test-configuration-js.md +++ b/docs/src/test-configuration-js.md @@ -1,6 +1,6 @@ --- id: test-configuration -title: "Test configuration" +title: "Configuration" --- ## Introduction diff --git a/docs/src/test-use-options-js.md b/docs/src/test-use-options-js.md index 0b46393941219..50c9246e8661d 100644 --- a/docs/src/test-use-options-js.md +++ b/docs/src/test-use-options-js.md @@ -1,6 +1,6 @@ --- id: test-use-options -title: "Test use options" +title: "Configuration (use)" --- ## Introduction diff --git a/utils/doclint/cli.js b/utils/doclint/cli.js index a694cd07497ef..958a13e1815ca 100755 --- a/utils/doclint/cli.js +++ b/utils/doclint/cli.js @@ -197,6 +197,7 @@ async function run() { 'java', 'css', 'js', + 'markdown', 'ts', 'python', 'py', From 1125381b946b22bbc7fa1c4a6a75177cd06e47ea Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 18 Sep 2025 13:09:58 +0100 Subject: [PATCH 185/329] feat: page.consoleMessages and page.pageErrors (#37450) --- docs/src/api/class-page.md | 15 +++++ packages/playwright-client/types/types.d.ts | 12 ++++ .../src/client/browserContext.ts | 2 +- .../src/client/consoleMessage.ts | 6 +- .../playwright-core/src/client/electron.ts | 2 +- packages/playwright-core/src/client/page.ts | 13 ++++- packages/playwright-core/src/client/worker.ts | 2 +- .../playwright-core/src/protocol/validator.ts | 17 ++++++ .../src/server/bidi/bidiPage.ts | 2 +- .../src/server/chromium/crPage.ts | 4 +- .../dispatchers/browserContextDispatcher.ts | 12 +--- .../src/server/dispatchers/pageDispatcher.ts | 24 +++++++- .../src/server/firefox/ffPage.ts | 3 +- packages/playwright-core/src/server/page.ts | 57 +++++++++++++------ .../src/server/webkit/wkPage.ts | 3 +- .../src/utils/isomorphic/protocolMetainfo.ts | 2 + packages/playwright-core/types/types.d.ts | 12 ++++ packages/protocol/src/channels.d.ts | 21 +++++++ packages/protocol/src/protocol.yml | 19 +++++++ tests/page/page-event-console.spec.ts | 17 ++++++ tests/page/page-event-pageerror.spec.ts | 18 ++++++ 21 files changed, 219 insertions(+), 44 deletions(-) diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index a4bb867483eff..541aebce3c0bc 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2677,6 +2677,21 @@ Returns whether the element is [visible](../actionability.md#visible). [`param: * since: v1.8 - type: <[Keyboard]> + +## async method: Page.consoleMessages +* since: v1.56 +- returns: <[Array]<[ConsoleMessage]>> + +Returns up to 200 last console messages from this page. See [`event: Page.console`] for more details. + + +## async method: Page.pageErrors +* since: v1.56 +- returns: <[Array]<[Error]>> + +Returns up to 200 last page errors from this page. See [`event: Page.pageError`] for more details. + + ## method: Page.locator * since: v1.14 - returns: <[Locator]> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 43653cbe80c13..867dfd391ceca 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -2284,6 +2284,12 @@ export interface Page { runBeforeUnload?: boolean; }): Promise; + /** + * Returns up to 200 last console messages from this page. See + * [page.on('console')](https://playwright.dev/docs/api/class-page#page-event-console) for more details. + */ + consoleMessages(): Promise>; + /** * Gets the full HTML contents of the page, including the doctype. */ @@ -3598,6 +3604,12 @@ export interface Page { */ opener(): Promise; + /** + * Returns up to 200 last page errors from this page. See + * [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) for more details. + */ + pageErrors(): Promise>; + /** * Pauses script execution. Playwright will stop executing the script and wait for the user to either press the * 'Resume' button in the page overlay or to call `playwright.resume()` in the DevTools console. diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 7b8015b630cb4..7f0473e7b5659 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -114,7 +114,7 @@ export class BrowserContext extends ChannelOwner this.emit(Events.BrowserContext.ServiceWorker, serviceWorker); }); this._channel.on('console', event => { - const consoleMessage = new ConsoleMessage(this._platform, event); + const consoleMessage = new ConsoleMessage(this._platform, event, Page.fromNullable(event.page)); this.emit(Events.BrowserContext.Console, consoleMessage); const page = consoleMessage.page(); if (page) diff --git a/packages/playwright-core/src/client/consoleMessage.ts b/packages/playwright-core/src/client/consoleMessage.ts index 5f8efee8c27ea..e186fdad3d2c1 100644 --- a/packages/playwright-core/src/client/consoleMessage.ts +++ b/packages/playwright-core/src/client/consoleMessage.ts @@ -26,10 +26,10 @@ type ConsoleMessageLocation = channels.BrowserContextConsoleEvent['location']; export class ConsoleMessage implements api.ConsoleMessage { private _page: Page | null; - private _event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent; + private _event: channels.WorkerConsoleEvent; - constructor(platform: Platform, event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent) { - this._page = ('page' in event && event.page) ? Page.from(event.page) : null; + constructor(platform: Platform, event: channels.WorkerConsoleEvent, page: Page | null) { + this._page = page; this._event = event; if (platform.inspectCustom) (this as any)[platform.inspectCustom] = () => this._inspect(); diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index 083e7b6f9f22b..efef83d840cde 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -92,7 +92,7 @@ export class ElectronApplication extends ChannelOwner { this.emit(Events.ElectronApplication.Close); }); - this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(this._platform, event))); + this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(this._platform, event, null))); this._setEventToSubscriptionMapping(new Map([ [Events.ElectronApplication.Console, 'console'], ])); diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 4dbf85ab03c42..d838311f14a78 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -22,7 +22,7 @@ import { evaluationScript } from './clientHelper'; import { Coverage } from './coverage'; import { Download } from './download'; import { ElementHandle, determineScreenshotType } from './elementHandle'; -import { TargetClosedError, isTargetClosedError, serializeError } from './errors'; +import { TargetClosedError, isTargetClosedError, parseError, serializeError } from './errors'; import { Events } from './events'; import { FileChooser } from './fileChooser'; import { Frame, verifyLoadState } from './frame'; @@ -41,6 +41,7 @@ import { trimStringWithEllipsis } from '../utils/isomorphic/stringUtils'; import { urlMatches, urlMatchesEqual } from '../utils/isomorphic/urlMatch'; import { LongStandingScope } from '../utils/isomorphic/manualPromise'; import { isObject, isRegExp, isString } from '../utils/isomorphic/rtti'; +import { ConsoleMessage } from './consoleMessage'; import type { BrowserContext } from './browserContext'; import type { Clock } from './clock'; @@ -669,6 +670,16 @@ export class Page extends ChannelOwner implements api.Page return await this._mainFrame.fill(selector, value, options); } + async consoleMessages(): Promise { + const { messages } = await this._channel.consoleMessages(); + return messages.map(message => new ConsoleMessage(this._platform, message, this)); + } + + async pageErrors(): Promise { + const { errors } = await this._channel.pageErrors(); + return errors.map(error => parseError(error)); + } + locator(selector: string, options?: LocatorOptions): Locator { return this.mainFrame().locator(selector, options); } diff --git a/packages/playwright-core/src/client/worker.ts b/packages/playwright-core/src/client/worker.ts index 06e77290c36dd..fcac272b131c0 100644 --- a/packages/playwright-core/src/client/worker.ts +++ b/packages/playwright-core/src/client/worker.ts @@ -47,7 +47,7 @@ export class Worker extends ChannelOwner implements api. this.emit(Events.Worker.Close, this); }); this._channel.on('console', event => { - this.emit(Events.Worker.Console, new ConsoleMessage(this._page?.context()._platform ?? this._context?._platform!, event)); + this.emit(Events.Worker.Console, new ConsoleMessage(this._page?.context()._platform ?? this._context?._platform!, event, null)); }); this.once(Events.Worker.Close, () => this._closedScope.close(this._page?._closeErrorWithReason() || new TargetClosedError())); this._setEventToSubscriptionMapping(new Map([ diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 437f1d489ac37..f5cb173a42087 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1245,6 +1245,19 @@ scheme.PageCloseParams = tObject({ reason: tOptional(tString), }); scheme.PageCloseResult = tOptional(tObject({})); +scheme.PageConsoleMessagesParams = tOptional(tObject({})); +scheme.PageConsoleMessagesResult = tObject({ + messages: tArray(tObject({ + type: tString, + text: tString, + args: tArray(tChannel(['ElementHandle', 'JSHandle'])), + location: tObject({ + url: tString, + lineNumber: tInt, + columnNumber: tInt, + }), + })), +}); scheme.PageEmulateMediaParams = tObject({ media: tOptional(tEnum(['screen', 'print', 'no-override'])), colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference', 'no-override'])), @@ -1440,6 +1453,10 @@ scheme.PageAccessibilitySnapshotParams = tObject({ scheme.PageAccessibilitySnapshotResult = tObject({ rootAXNode: tOptional(tType('AXNode')), }); +scheme.PagePageErrorsParams = tOptional(tObject({})); +scheme.PagePageErrorsResult = tObject({ + errors: tArray(tType('SerializedError')), +}); scheme.PagePdfParams = tObject({ scale: tOptional(tFloat), displayHeaderFooter: tOptional(tBoolean), diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index 5b508bce38652..f6d4976ec5bed 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -271,7 +271,7 @@ export class BidiPage implements PageDelegate { const location = `${f.url}:${f.lineNumber + 1}:${f.columnNumber + 1}`; return f.functionName ? ` at ${f.functionName} (${location})` : ` at ${location}`; }).join('\n')}`; - this._page.emitOnContextOnceInitialized(BidiBrowserContext.Events.PageError, error, this._page); + this._page.addPageError(error); return; } if (params.type !== 'console') diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 33167497baaef..a93760f060e77 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -783,7 +783,7 @@ class FrameSession { const args = event.args.map(o => createHandle(worker.existingExecutionContext!, o)); this._page.addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace)); }); - session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exception.exceptionDetails), this._page)); + session.on('Runtime.exceptionThrown', exception => this._page.addPageError(exceptionToError(exception.exceptionDetails))); } _onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) { @@ -873,7 +873,7 @@ class FrameSession { } _handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) { - this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exceptionDetails), this._page); + this._page.addPageError(exceptionToError(exceptionDetails)); } async _onTargetCrashed() { diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index cc779595e1992..8208655bc2bfd 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -22,9 +22,7 @@ import { ArtifactDispatcher } from './artifactDispatcher'; import { CDPSessionDispatcher } from './cdpSessionDispatcher'; import { DialogDispatcher } from './dialogDispatcher'; import { Dispatcher } from './dispatcher'; -import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { FrameDispatcher } from './frameDispatcher'; -import { JSHandleDispatcher } from './jsHandleDispatcher'; import { APIRequestContextDispatcher, RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers'; import { BindingCallDispatcher, PageDispatcher, WorkerDispatcher } from './pageDispatcher'; import { CRBrowserContext } from '../chromium/crBrowser'; @@ -126,15 +124,7 @@ export class BrowserContextDispatcher extends Dispatcher { - const elementHandle = a.asElement(); - if (elementHandle) - return ElementHandleDispatcher.from(FrameDispatcher.from(this, elementHandle._frame), elementHandle); - return JSHandleDispatcher.fromJSHandle(pageDispatcher, a); - }), - location: message.location(), + ...pageDispatcher.serializeConsoleMessage(message), }); } }); diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index cd48ef9eda296..f975bad360634 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -16,7 +16,7 @@ import { Page, Worker } from '../page'; import { Dispatcher } from './dispatcher'; -import { parseError } from '../errors'; +import { parseError, serializeError } from '../errors'; import { ArtifactDispatcher } from './artifactDispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { FrameDispatcher } from './frameDispatcher'; @@ -126,6 +126,20 @@ export class PageDispatcher extends Dispatcher { + const elementHandle = a.asElement(); + if (elementHandle) + return ElementHandleDispatcher.from(FrameDispatcher.from(this.parentScope(), elementHandle._frame), elementHandle); + return JSHandleDispatcher.fromJSHandle(this, a); + }), + location: message.location(), + }; + } + async exposeBinding(params: channels.PageExposeBindingParams, progress: Progress): Promise { const binding = await this._page.exposeBinding(progress, params.name, !!params.needsHandle, (source, ...args) => { // When reusing the context, we might have some bindings called late enough, @@ -273,6 +287,14 @@ export class PageDispatcher extends Dispatcher { + return { messages: this._page.consoleMessages().map(message => this.serializeConsoleMessage(message)) }; + } + + async pageErrors(params: channels.PagePageErrorsParams, progress: Progress): Promise { + return { errors: this._page.pageErrors().map(error => serializeError(error)) }; + } + async mouseMove(params: channels.PageMouseMoveParams, progress: Progress): Promise { progress.metadata.point = { x: params.x, y: params.y }; await this._page.mouse.move(progress, params.x, params.y, params); diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index c282649e7726d..16dd3186e7e73 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -27,7 +27,6 @@ import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './ffInput'; import { FFNetworkManager } from './ffNetworkManager'; import { debugLogger } from '../utils/debugLogger'; import { splitErrorMessage } from '../../utils/isomorphic/stackTrace'; -import { BrowserContext } from '../browserContext'; import { TargetClosedError } from '../errors'; import type { Progress } from '../progress'; @@ -224,7 +223,7 @@ export class FFPage implements PageDelegate { const error = new Error(message); error.stack = params.message + '\n' + params.stack.split('\n').filter(Boolean).map(a => a.replace(/([^@]*)@(.*)/, ' at $1 ($2)')).join('\n'); error.name = name; - this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, error, this._page); + this._page.addPageError(error); } _onConsole(payload: Protocol.Runtime.consolePayload) { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 6a7edd191decd..b36b062fe3f42 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -138,7 +138,8 @@ export class Page extends SdkObject { private _closedPromise = new ManualPromise(); private _initialized: Page | Error | undefined; private _initializedPromise = new ManualPromise(); - private _eventsToEmitAfterInitialized: { event: string | symbol, args: any[] }[] = []; + private _consoleMessages: ConsoleMessage[] = []; + private _pageErrors: Error[] = []; private _crashed = false; readonly openScope = new LongStandingScope(); readonly browserContext: BrowserContext; @@ -209,9 +210,10 @@ export class Page extends SdkObject { this._initialized = error || this; this.emitOnContext(contextEvent, this); - for (const { event, args } of this._eventsToEmitAfterInitialized) - this.browserContext.emit(event, ...args); - this._eventsToEmitAfterInitialized = []; + for (const pageError of this._pageErrors) + this.emitOnContext(BrowserContext.Events.PageError, pageError, this); + for (const message of this._consoleMessages) + this.emitOnContext(BrowserContext.Events.Console, message); // It may happen that page initialization finishes after Close event has already been sent, // in that case we fire another Close event to ensure that each reported Page will have @@ -240,19 +242,6 @@ export class Page extends SdkObject { this.browserContext.emit(event, ...args); } - emitOnContextOnceInitialized(event: string | symbol, ...args: any[]) { - if (this.isStorageStatePage) - return; - // Some events, like console messages, may come before page is ready. - // In this case, postpone the event until page is initialized, - // and dispatch it to the client later, either on the live Page, - // or on the "errored" Page. - if (this._initialized) - this.browserContext.emit(event, ...args); - else - this._eventsToEmitAfterInitialized.push({ event, args }); - } - async resetForReuse(progress: Progress) { // Re-navigate once init scripts are gone. await this.mainFrame().gotoImpl(progress, 'about:blank', {}); @@ -374,7 +363,34 @@ export class Page extends SdkObject { args.forEach(arg => arg.dispose()); return; } - this.emitOnContextOnceInitialized(BrowserContext.Events.Console, message); + + this._consoleMessages.push(message); + ensureArrayLimit(this._consoleMessages, 200); // Avoid unbounded memory growth. + + // Console messages may come before the page is ready. In this case, + // we'll dispatch them to the client later, either on the live Page, + // or on the "errored" Page. + if (this._initialized) + this.emitOnContext(BrowserContext.Events.Console, message); + } + + consoleMessages() { + return this._consoleMessages; + } + + addPageError(pageError: Error) { + this._pageErrors.push(pageError); + ensureArrayLimit(this._pageErrors, 200); // Avoid unbounded memory growth. + + // Page errors may come before the page is ready. In this case, + // we'll dispatch them to the client later, either on the live Page, + // or on the "errored" Page. + if (this._initialized) + this.emitOnContext(BrowserContext.Events.PageError, pageError, this); + } + + pageErrors() { + return this._pageErrors; } async reload(progress: Progress, options: types.NavigateOptions): Promise { @@ -1061,3 +1077,8 @@ async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, frame } return result; } + +function ensureArrayLimit(array: any[], limit: number) { + if (array.length > limit) + array.splice(0, limit / 10); +} diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 58ab0cc4dfcb8..db1c4c639d5a6 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -24,7 +24,6 @@ import { eventsHelper } from '../utils/eventsHelper'; import { hostPlatform } from '../utils/hostPlatform'; import { splitErrorMessage } from '../../utils/isomorphic/stackTrace'; import { PNG, jpegjs } from '../../utilsBundle'; -import { BrowserContext } from '../browserContext'; import * as dialog from '../dialog'; import * as dom from '../dom'; import { TargetClosedError } from '../errors'; @@ -540,7 +539,7 @@ export class WKPage implements PageDelegate { error.stack = stack; error.name = name; - this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, error, this._page); + this._page.addPageError(error); return; } diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index cb841371fa7ea..76f231b300c10 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -101,6 +101,7 @@ export const methodMetainfo = new Map; + /** + * Returns up to 200 last console messages from this page. See + * [page.on('console')](https://playwright.dev/docs/api/class-page#page-event-console) for more details. + */ + consoleMessages(): Promise>; + /** * Gets the full HTML contents of the page, including the doctype. */ @@ -3598,6 +3604,12 @@ export interface Page { */ opener(): Promise; + /** + * Returns up to 200 last page errors from this page. See + * [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) for more details. + */ + pageErrors(): Promise>; + /** * Pauses script execution. Playwright will stop executing the script and wait for the user to either press the * 'Resume' button in the page overlay or to call `playwright.resume()` in the DevTools console. diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 73d5efef3a649..fe346de7f1588 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -2091,6 +2091,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { _type_Page: boolean; addInitScript(params: PageAddInitScriptParams, progress?: Progress): Promise; close(params: PageCloseParams, progress?: Progress): Promise; + consoleMessages(params?: PageConsoleMessagesParams, progress?: Progress): Promise; emulateMedia(params: PageEmulateMediaParams, progress?: Progress): Promise; exposeBinding(params: PageExposeBindingParams, progress?: Progress): Promise; goBack(params: PageGoBackParams, progress?: Progress): Promise; @@ -2118,6 +2119,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { mouseWheel(params: PageMouseWheelParams, progress?: Progress): Promise; touchscreenTap(params: PageTouchscreenTapParams, progress?: Progress): Promise; accessibilitySnapshot(params: PageAccessibilitySnapshotParams, progress?: Progress): Promise; + pageErrors(params?: PagePageErrorsParams, progress?: Progress): Promise; pdf(params: PagePdfParams, progress?: Progress): Promise; snapshotForAI(params: PageSnapshotForAIParams, progress?: Progress): Promise; startJSCoverage(params: PageStartJSCoverageParams, progress?: Progress): Promise; @@ -2187,6 +2189,20 @@ export type PageCloseOptions = { reason?: string, }; export type PageCloseResult = void; +export type PageConsoleMessagesParams = {}; +export type PageConsoleMessagesOptions = {}; +export type PageConsoleMessagesResult = { + messages: { + type: string, + text: string, + args: JSHandleChannel[], + location: { + url: string, + lineNumber: number, + columnNumber: number, + }, + }[], +}; export type PageEmulateMediaParams = { media?: 'screen' | 'print' | 'no-override', colorScheme?: 'dark' | 'light' | 'no-preference' | 'no-override', @@ -2503,6 +2519,11 @@ export type PageAccessibilitySnapshotOptions = { export type PageAccessibilitySnapshotResult = { rootAXNode?: AXNode, }; +export type PagePageErrorsParams = {}; +export type PagePageErrorsOptions = {}; +export type PagePageErrorsResult = { + errors: SerializedError[], +}; export type PagePdfParams = { scale?: number, displayHeaderFooter?: boolean, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 8da80c373a12c..ef04a9d3a8233 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1618,6 +1618,17 @@ Page: flags: pausesBeforeAction: true + consoleMessages: + title: Get console messages + group: getter + returns: + messages: + type: array + items: + type: object + properties: + $mixin: ConsoleMessage + emulateMedia: title: Emulate media parameters: @@ -1953,6 +1964,14 @@ Page: returns: rootAXNode: AXNode? + pageErrors: + title: Get page errors + group: getter + returns: + errors: + type: array + items: SerializedError + pdf: title: PDF parameters: diff --git a/tests/page/page-event-console.spec.ts b/tests/page/page-event-console.spec.ts index 10cfe1823f813..2609a6cc272b4 100644 --- a/tests/page/page-event-console.spec.ts +++ b/tests/page/page-event-console.spec.ts @@ -222,3 +222,20 @@ it('do not update console count on unhandled rejections', async ({ page }) => { await expect.poll(() => messages).toEqual(['begin', 'end']); }); + +it('consoleMessages should work', async ({ page }) => { + await page.evaluate(() => { + for (let i = 0; i < 301; i++) + console.log('message' + i); + }); + + const messages = await page.consoleMessages(); + const objects = messages.map(m => ({ text: m.text(), type: m.type(), page: m.page() })); + + const expected = []; + for (let i = 201; i < 301; i++) + expected.push(expect.objectContaining({ text: 'message' + i, type: 'log', page })); + + expect(objects.length, 'should be at least 100 messages').toBeGreaterThanOrEqual(100); + expect(objects.slice(objects.length - expected.length), 'should return last messages').toEqual(expected); +}); diff --git a/tests/page/page-event-pageerror.spec.ts b/tests/page/page-event-pageerror.spec.ts index 312ddf0d7ff94..310f4917f0bb9 100644 --- a/tests/page/page-event-pageerror.spec.ts +++ b/tests/page/page-event-pageerror.spec.ts @@ -150,3 +150,21 @@ it('should emit error from unhandled rejects', async ({ page, browserName }) => ]); expect(error.message).toContain('sad :('); }); + +it('pageErrors should work', async ({ page }) => { + await page.evaluate(async () => { + for (let i = 0; i < 301; i++) + window.builtins.setTimeout(() => { throw new Error('error' + i); }, 0); + await new Promise(f => window.builtins.setTimeout(f, 100)); + }); + + const errors = await page.pageErrors(); + const messages = errors.map(e => e.message); + + const expected = []; + for (let i = 201; i < 301; i++) + expected.push('error' + i); + + expect(messages.length, 'should be at least 100 errors').toBeGreaterThanOrEqual(100); + expect(messages.slice(messages.length - expected.length), 'should return last errors').toEqual(expected); +}); From 4e9ce1deb87a3fd9e1f922e737b78b658aaf7d8d Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 18 Sep 2025 05:23:45 -0700 Subject: [PATCH 186/329] chore(ct): expose fixture types for downstream use (#37457) --- packages/playwright-ct-react/index.d.ts | 6 ++++-- packages/playwright-ct-react17/index.d.ts | 6 ++++-- packages/playwright-ct-svelte/index.d.ts | 6 ++++-- packages/playwright-ct-vue/index.d.ts | 6 ++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/playwright-ct-react/index.d.ts b/packages/playwright-ct-react/index.d.ts index d7000c5c85719..96b26d951bfd8 100644 --- a/packages/playwright-ct-react/index.d.ts +++ b/packages/playwright-ct-react/index.d.ts @@ -27,11 +27,13 @@ export interface MountResult extends Locator { update(component: React.JSX.Element): Promise; } -export const test: TestType<{ +export interface ComponentFixtures { mount( component: React.JSX.Element, options?: MountOptions ): Promise; -}>; +} + +export const test: TestType; export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core'; diff --git a/packages/playwright-ct-react17/index.d.ts b/packages/playwright-ct-react17/index.d.ts index 748212c45ed6f..f7009542be49c 100644 --- a/packages/playwright-ct-react17/index.d.ts +++ b/packages/playwright-ct-react17/index.d.ts @@ -25,11 +25,13 @@ export interface MountResult extends Locator { update(component: JSX.Element): Promise; } -export const test: TestType<{ +export interface ComponentFixtures { mount( component: JSX.Element, options?: MountOptions ): Promise; -}>; +} + +export const test: TestType; export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core'; diff --git a/packages/playwright-ct-svelte/index.d.ts b/packages/playwright-ct-svelte/index.d.ts index 7cf427ba5d603..8f90d4f2cc89c 100644 --- a/packages/playwright-ct-svelte/index.d.ts +++ b/packages/playwright-ct-svelte/index.d.ts @@ -39,11 +39,13 @@ export interface MountResult extends Locator }): Promise; } -export const test: TestType<{ +export interface ComponentFixtures { mount( component: Component, options?: MountOptions ): Promise>; -}>; +} + +export const test: TestType; export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core'; diff --git a/packages/playwright-ct-vue/index.d.ts b/packages/playwright-ct-vue/index.d.ts index 822711f4b6660..efe3f69f05335 100644 --- a/packages/playwright-ct-vue/index.d.ts +++ b/packages/playwright-ct-vue/index.d.ts @@ -52,7 +52,7 @@ export interface MountResultJsx extends Locator { update(component: JSX.Element): Promise; } -export const test: TestType<{ +export interface ComponentFixtures { mount( component: JSX.Element, options?: MountOptionsJsx @@ -61,6 +61,8 @@ export const test: TestType<{ component: Component, options?: MountOptions ): Promise>; -}>; +} + +export const test: TestType; export { defineConfig, PlaywrightTestConfig, expect, devices } from '@playwright/experimental-ct-core'; From 635f2bb0f01cbdc10fd26ff2c7b81468341d1d8c Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 18 Sep 2025 05:24:21 -0700 Subject: [PATCH 187/329] fix(ct): don't modify React core in Node development mode (#37451) --- packages/playwright-ct-core/src/viteUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/playwright-ct-core/src/viteUtils.ts b/packages/playwright-ct-core/src/viteUtils.ts index 58bfececc9e21..2cb5ea4cf62e9 100644 --- a/packages/playwright-ct-core/src/viteUtils.ts +++ b/packages/playwright-ct-core/src/viteUtils.ts @@ -169,7 +169,8 @@ const compiledReactRE = /(const|var)\s+React\s*=/; export function transformIndexFile(id: string, content: string, templateDir: string, registerSource: string, importInfos: Map): TransformResult | null { // Vite React plugin will do this for .jsx files, but not .js files. - if (id.endsWith('.js') && content.includes('React.createElement') && !content.match(importReactRE) && !content.match(compiledReactRE)) { + // `__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED` check is to avoid modifying React itself (such as react.development.js) + if (id.endsWith('.js') && content.includes('React.createElement') && !content.includes('__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED') && !content.match(importReactRE) && !content.match(compiledReactRE)) { const code = `import React from 'react';\n${content}`; return { code, map: { mappings: '' } }; } From b8beeef1acf36633fb9d97612f70fd85bba67a8e Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 18 Sep 2025 05:24:47 -0700 Subject: [PATCH 188/329] fix(html): don't include extra spacing in metadata section (#37409) --- packages/html-reporter/src/metadataView.tsx | 32 +++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/html-reporter/src/metadataView.tsx b/packages/html-reporter/src/metadataView.tsx index 545eb14513a8a..11077b9a04345 100644 --- a/packages/html-reporter/src/metadataView.tsx +++ b/packages/html-reporter/src/metadataView.tsx @@ -65,21 +65,23 @@ const InnerMetadataView: React.FC<{ metadata: Metadata }> = params => { return
{commitInfo.ci && !commitInfo.gitCommit && } {commitInfo.gitCommit && } - {otherEntries.length > 0 && (commitInfo.gitCommit || commitInfo.ci) &&
} -
- {otherEntries.map(([propertyName, value]) => { - const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value); - const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString; - return ( -
- - {propertyName} - : {linkifyText(trimmedValue)} - -
- ); - })} -
+ {otherEntries.length > 0 && <> + {(commitInfo.gitCommit || commitInfo.ci) &&
} +
+ {otherEntries.map(([propertyName, value]) => { + const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value); + const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString; + return ( +
+ + {propertyName} + : {linkifyText(trimmedValue)} + +
+ ); + })} +
+ }
; }; From a16b75c0c4591b05b1620eb931be9c69512a1d10 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 18 Sep 2025 05:43:19 -0700 Subject: [PATCH 189/329] chore(codegen): accessibly indicate recording status on button (#37362) --- packages/injected/src/highlight.css | 5 +++++ packages/injected/src/recorder/clipPaths.ts | 2 +- packages/injected/src/recorder/icons/stop-circle.svg | 1 + packages/injected/src/recorder/recorder.ts | 4 +++- utils/generate_clip_paths.js | 1 + 5 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 packages/injected/src/recorder/icons/stop-circle.svg diff --git a/packages/injected/src/highlight.css b/packages/injected/src/highlight.css index f884d5191e237..c82132b0ed70a 100644 --- a/packages/injected/src/highlight.css +++ b/packages/injected/src/highlight.css @@ -205,6 +205,11 @@ x-pw-tool-item.record > x-div { clip-path: url(#icon-circle-large-filled); } +x-pw-tool-item.record.toggled > x-div { + /* codicon: stop-circle */ + clip-path: url(#icon-stop-circle); +} + x-pw-tool-item.pick-locator > x-div { /* codicon: inspect */ clip-path: url(#icon-inspect); diff --git a/packages/injected/src/recorder/clipPaths.ts b/packages/injected/src/recorder/clipPaths.ts index 1ac490843d292..45b55a7df2933 100644 --- a/packages/injected/src/recorder/clipPaths.ts +++ b/packages/injected/src/recorder/clipPaths.ts @@ -27,5 +27,5 @@ import type { SvgJson } from './recorder'; // eslint-disable-next-line key-spacing, object-curly-spacing, comma-spacing, quotes -const svgJson: SvgJson = {"tagName":"svg","children":[{"tagName":"defs","children":[{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gripper"},"children":[{"tagName":"path","attrs":{"d":"M5 3h2v2H5zm0 4h2v2H5zm0 4h2v2H5zm4-8h2v2H9zm0 4h2v2H9zm0 4h2v2H9z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-circle-large-filled"},"children":[{"tagName":"path","attrs":{"d":"M8 1a6.8 6.8 0 0 1 1.86.253 6.899 6.899 0 0 1 3.083 1.805 6.903 6.903 0 0 1 1.804 3.083C14.916 6.738 15 7.357 15 8s-.084 1.262-.253 1.86a6.9 6.9 0 0 1-.704 1.674 7.157 7.157 0 0 1-2.516 2.509 6.966 6.966 0 0 1-1.668.71A6.984 6.984 0 0 1 8 15a6.984 6.984 0 0 1-1.86-.246 7.098 7.098 0 0 1-1.674-.711 7.3 7.3 0 0 1-1.415-1.094 7.295 7.295 0 0 1-1.094-1.415 7.098 7.098 0 0 1-.71-1.675A6.985 6.985 0 0 1 1 8c0-.643.082-1.262.246-1.86a6.968 6.968 0 0 1 .711-1.667 7.156 7.156 0 0 1 2.509-2.516 6.895 6.895 0 0 1 1.675-.704A6.808 6.808 0 0 1 8 1z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-inspect"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 3l1-1h12l1 1v6h-1V3H2v8h5v1H2l-1-1V3zm14.707 9.707L9 6v9.414l2.707-2.707h4zM10 13V8.414l3.293 3.293h-2L10 13z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-whole-word"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M0 11H1V13H15V11H16V14H15H1H0V11Z"}},{"tagName":"path","attrs":{"d":"M6.84048 11H5.95963V10.1406H5.93814C5.555 10.7995 4.99104 11.1289 4.24625 11.1289C3.69839 11.1289 3.26871 10.9839 2.95718 10.6938C2.64924 10.4038 2.49527 10.0189 2.49527 9.53906C2.49527 8.51139 3.10041 7.91341 4.3107 7.74512L5.95963 7.51416C5.95963 6.57959 5.58186 6.1123 4.82632 6.1123C4.16389 6.1123 3.56591 6.33789 3.03238 6.78906V5.88672C3.57307 5.54297 4.19612 5.37109 4.90152 5.37109C6.19416 5.37109 6.84048 6.05501 6.84048 7.42285V11ZM5.95963 8.21777L4.63297 8.40039C4.22476 8.45768 3.91682 8.55973 3.70914 8.70654C3.50145 8.84977 3.39761 9.10579 3.39761 9.47461C3.39761 9.74316 3.4925 9.96338 3.68228 10.1353C3.87564 10.3035 4.13166 10.3877 4.45035 10.3877C4.8872 10.3877 5.24706 10.2355 5.52994 9.93115C5.8164 9.62321 5.95963 9.2347 5.95963 8.76562V8.21777Z"}},{"tagName":"path","attrs":{"d":"M9.3475 10.2051H9.32601V11H8.44515V2.85742H9.32601V6.4668H9.3475C9.78076 5.73633 10.4146 5.37109 11.2489 5.37109C11.9543 5.37109 12.5057 5.61816 12.9032 6.1123C13.3042 6.60286 13.5047 7.26172 13.5047 8.08887C13.5047 9.00911 13.2809 9.74674 12.8333 10.3018C12.3857 10.8532 11.7734 11.1289 10.9964 11.1289C10.2695 11.1289 9.71989 10.821 9.3475 10.2051ZM9.32601 7.98682V8.75488C9.32601 9.20964 9.47282 9.59635 9.76644 9.91504C10.0636 10.2301 10.4396 10.3877 10.8944 10.3877C11.4279 10.3877 11.8451 10.1836 12.1458 9.77539C12.4502 9.36719 12.6024 8.79964 12.6024 8.07275C12.6024 7.46045 12.4609 6.98063 12.1781 6.6333C11.8952 6.28597 11.512 6.1123 11.0286 6.1123C10.5166 6.1123 10.1048 6.29134 9.7933 6.64941C9.48177 7.00391 9.32601 7.44971 9.32601 7.98682Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-eye"},"children":[{"tagName":"path","attrs":{"d":"M7.99993 6.00316C9.47266 6.00316 10.6666 7.19708 10.6666 8.66981C10.6666 10.1426 9.47266 11.3365 7.99993 11.3365C6.52715 11.3365 5.33324 10.1426 5.33324 8.66981C5.33324 7.19708 6.52715 6.00316 7.99993 6.00316ZM7.99993 7.00315C7.07946 7.00315 6.33324 7.74935 6.33324 8.66981C6.33324 9.59028 7.07946 10.3365 7.99993 10.3365C8.9204 10.3365 9.6666 9.59028 9.6666 8.66981C9.6666 7.74935 8.9204 7.00315 7.99993 7.00315ZM7.99993 3.66675C11.0756 3.66675 13.7307 5.76675 14.4673 8.70968C14.5344 8.97755 14.3716 9.24908 14.1037 9.31615C13.8358 9.38315 13.5643 9.22041 13.4973 8.95248C12.8713 6.45205 10.6141 4.66675 7.99993 4.66675C5.38454 4.66675 3.12664 6.45359 2.50182 8.95555C2.43491 9.22341 2.16348 9.38635 1.89557 9.31948C1.62766 9.25255 1.46471 8.98115 1.53162 8.71321C2.26701 5.76856 4.9229 3.66675 7.99993 3.66675Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-symbol-constant"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M4 6h8v1H4V6zm8 3H4v1h8V9z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-check"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M14.431 3.323l-8.47 10-.79-.036-3.35-4.77.818-.574 2.978 4.24 8.051-9.506.764.646z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-close"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-pass"},"children":[{"tagName":"path","attrs":{"d":"M6.27 10.87h.71l4.56-4.56-.71-.71-4.2 4.21-1.92-1.92L4 8.6l2.27 2.27z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gist"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M10.57 1.14l3.28 3.3.15.36v9.7l-.5.5h-11l-.5-.5v-13l.5-.5h7.72l.35.14zM10 5h3l-3-3v3zM3 2v12h10V6H9.5L9 5.5V2H3zm2.062 7.533l1.817-1.828L6.17 7 4 9.179v.707l2.171 2.174.707-.707-1.816-1.82zM8.8 7.714l.7-.709 2.189 2.175v.709L9.5 12.062l-.705-.709 1.831-1.82L8.8 7.714z"}}]}]}]}; +const svgJson: SvgJson = {"tagName":"svg","children":[{"tagName":"defs","children":[{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gripper"},"children":[{"tagName":"path","attrs":{"d":"M5 3h2v2H5zm0 4h2v2H5zm0 4h2v2H5zm4-8h2v2H9zm0 4h2v2H9zm0 4h2v2H9z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-circle-large-filled"},"children":[{"tagName":"path","attrs":{"d":"M8 1a6.8 6.8 0 0 1 1.86.253 6.899 6.899 0 0 1 3.083 1.805 6.903 6.903 0 0 1 1.804 3.083C14.916 6.738 15 7.357 15 8s-.084 1.262-.253 1.86a6.9 6.9 0 0 1-.704 1.674 7.157 7.157 0 0 1-2.516 2.509 6.966 6.966 0 0 1-1.668.71A6.984 6.984 0 0 1 8 15a6.984 6.984 0 0 1-1.86-.246 7.098 7.098 0 0 1-1.674-.711 7.3 7.3 0 0 1-1.415-1.094 7.295 7.295 0 0 1-1.094-1.415 7.098 7.098 0 0 1-.71-1.675A6.985 6.985 0 0 1 1 8c0-.643.082-1.262.246-1.86a6.968 6.968 0 0 1 .711-1.667 7.156 7.156 0 0 1 2.509-2.516 6.895 6.895 0 0 1 1.675-.704A6.808 6.808 0 0 1 8 1z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-stop-circle"},"children":[{"tagName":"path","attrs":{"d":"M6 6h4v4H6z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-inspect"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 3l1-1h12l1 1v6h-1V3H2v8h5v1H2l-1-1V3zm14.707 9.707L9 6v9.414l2.707-2.707h4zM10 13V8.414l3.293 3.293h-2L10 13z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-whole-word"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M0 11H1V13H15V11H16V14H15H1H0V11Z"}},{"tagName":"path","attrs":{"d":"M6.84048 11H5.95963V10.1406H5.93814C5.555 10.7995 4.99104 11.1289 4.24625 11.1289C3.69839 11.1289 3.26871 10.9839 2.95718 10.6938C2.64924 10.4038 2.49527 10.0189 2.49527 9.53906C2.49527 8.51139 3.10041 7.91341 4.3107 7.74512L5.95963 7.51416C5.95963 6.57959 5.58186 6.1123 4.82632 6.1123C4.16389 6.1123 3.56591 6.33789 3.03238 6.78906V5.88672C3.57307 5.54297 4.19612 5.37109 4.90152 5.37109C6.19416 5.37109 6.84048 6.05501 6.84048 7.42285V11ZM5.95963 8.21777L4.63297 8.40039C4.22476 8.45768 3.91682 8.55973 3.70914 8.70654C3.50145 8.84977 3.39761 9.10579 3.39761 9.47461C3.39761 9.74316 3.4925 9.96338 3.68228 10.1353C3.87564 10.3035 4.13166 10.3877 4.45035 10.3877C4.8872 10.3877 5.24706 10.2355 5.52994 9.93115C5.8164 9.62321 5.95963 9.2347 5.95963 8.76562V8.21777Z"}},{"tagName":"path","attrs":{"d":"M9.3475 10.2051H9.32601V11H8.44515V2.85742H9.32601V6.4668H9.3475C9.78076 5.73633 10.4146 5.37109 11.2489 5.37109C11.9543 5.37109 12.5057 5.61816 12.9032 6.1123C13.3042 6.60286 13.5047 7.26172 13.5047 8.08887C13.5047 9.00911 13.2809 9.74674 12.8333 10.3018C12.3857 10.8532 11.7734 11.1289 10.9964 11.1289C10.2695 11.1289 9.71989 10.821 9.3475 10.2051ZM9.32601 7.98682V8.75488C9.32601 9.20964 9.47282 9.59635 9.76644 9.91504C10.0636 10.2301 10.4396 10.3877 10.8944 10.3877C11.4279 10.3877 11.8451 10.1836 12.1458 9.77539C12.4502 9.36719 12.6024 8.79964 12.6024 8.07275C12.6024 7.46045 12.4609 6.98063 12.1781 6.6333C11.8952 6.28597 11.512 6.1123 11.0286 6.1123C10.5166 6.1123 10.1048 6.29134 9.7933 6.64941C9.48177 7.00391 9.32601 7.44971 9.32601 7.98682Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-eye"},"children":[{"tagName":"path","attrs":{"d":"M7.99993 6.00316C9.47266 6.00316 10.6666 7.19708 10.6666 8.66981C10.6666 10.1426 9.47266 11.3365 7.99993 11.3365C6.52715 11.3365 5.33324 10.1426 5.33324 8.66981C5.33324 7.19708 6.52715 6.00316 7.99993 6.00316ZM7.99993 7.00315C7.07946 7.00315 6.33324 7.74935 6.33324 8.66981C6.33324 9.59028 7.07946 10.3365 7.99993 10.3365C8.9204 10.3365 9.6666 9.59028 9.6666 8.66981C9.6666 7.74935 8.9204 7.00315 7.99993 7.00315ZM7.99993 3.66675C11.0756 3.66675 13.7307 5.76675 14.4673 8.70968C14.5344 8.97755 14.3716 9.24908 14.1037 9.31615C13.8358 9.38315 13.5643 9.22041 13.4973 8.95248C12.8713 6.45205 10.6141 4.66675 7.99993 4.66675C5.38454 4.66675 3.12664 6.45359 2.50182 8.95555C2.43491 9.22341 2.16348 9.38635 1.89557 9.31948C1.62766 9.25255 1.46471 8.98115 1.53162 8.71321C2.26701 5.76856 4.9229 3.66675 7.99993 3.66675Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-symbol-constant"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M4 6h8v1H4V6zm8 3H4v1h8V9z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-check"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M14.431 3.323l-8.47 10-.79-.036-3.35-4.77.818-.574 2.978 4.24 8.051-9.506.764.646z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-close"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-pass"},"children":[{"tagName":"path","attrs":{"d":"M6.27 10.87h.71l4.56-4.56-.71-.71-4.2 4.21-1.92-1.92L4 8.6l2.27 2.27z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gist"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M10.57 1.14l3.28 3.3.15.36v9.7l-.5.5h-11l-.5-.5v-13l.5-.5h7.72l.35.14zM10 5h3l-3-3v3zM3 2v12h10V6H9.5L9 5.5V2H3zm2.062 7.533l1.817-1.828L6.17 7 4 9.179v.707l2.171 2.174.707-.707-1.816-1.82zM8.8 7.714l.7-.709 2.189 2.175v.709L9.5 12.062l-.705-.709 1.831-1.82L8.8 7.714z"}}]}]}]}; export default svgJson; diff --git a/packages/injected/src/recorder/icons/stop-circle.svg b/packages/injected/src/recorder/icons/stop-circle.svg new file mode 100644 index 0000000000000..4f39984fa23d2 --- /dev/null +++ b/packages/injected/src/recorder/icons/stop-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/injected/src/recorder/recorder.ts b/packages/injected/src/recorder/recorder.ts index 5dfff539e179f..08da712f0a1c7 100644 --- a/packages/injected/src/recorder/recorder.ts +++ b/packages/injected/src/recorder/recorder.ts @@ -1274,7 +1274,9 @@ class Overlay { } setUIState(state: UIState) { - this._recordToggle.classList.toggle('toggled', state.mode === 'recording' || state.mode === 'assertingText' || state.mode === 'assertingVisibility' || state.mode === 'assertingValue' || state.mode === 'assertingSnapshot' || state.mode === 'recording-inspecting'); + const isRecording = state.mode === 'recording' || state.mode === 'assertingText' || state.mode === 'assertingVisibility' || state.mode === 'assertingValue' || state.mode === 'assertingSnapshot' || state.mode === 'recording-inspecting'; + this._recordToggle.classList.toggle('toggled', isRecording); + this._recordToggle.title = isRecording ? 'Stop Recording' : 'Start Recording'; this._pickLocatorToggle.classList.toggle('toggled', state.mode === 'inspecting' || state.mode === 'recording-inspecting'); this._assertVisibilityToggle.classList.toggle('toggled', state.mode === 'assertingVisibility'); this._assertVisibilityToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting'); diff --git a/utils/generate_clip_paths.js b/utils/generate_clip_paths.js index 210a8405818f8..96747e7f62bf1 100644 --- a/utils/generate_clip_paths.js +++ b/utils/generate_clip_paths.js @@ -57,6 +57,7 @@ const outFile = path.join(ROOT, 'packages', 'injected', 'src', 'recorder', 'clip const iconNames = [ 'gripper', 'circle-large-filled', + 'stop-circle', 'inspect', 'whole-word', 'eye', From 8c0bea65d48b5c1934d301492906e2fa845a9228 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:15:41 +0200 Subject: [PATCH 190/329] feat(webkit): roll to r2211 (#37473) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 58d274005ee05..6d3ad2780c9a3 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -39,7 +39,7 @@ }, { "name": "webkit", - "revision": "2210", + "revision": "2211", "installByDefault": true, "revisionOverrides": { "debian11-x64": "2105", From 6be87e904d766ce18f5b467a3ad2df32edddbbdc Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 18 Sep 2025 09:07:20 -0700 Subject: [PATCH 191/329] chore(agent): use seed terminology consistently (#37462) --- docs/src/test-agents-js.md | 2 +- .../.claude/agents/playwright-test-planner.md | 43 +- examples/todomvc/prompt.claudecode.md | 6 +- examples/todomvc/specs/basic-operations.md | 466 ++++++++++++++++++ .../tests/{template.spec.ts => seed.spec.ts} | 2 +- packages/playwright/src/agents/planner.md | 45 +- packages/playwright/src/mcp/test/testTools.ts | 16 +- packages/playwright/src/program.ts | 12 +- tests/mcp/test-tools.spec.ts | 2 +- 9 files changed, 567 insertions(+), 27 deletions(-) create mode 100644 examples/todomvc/specs/basic-operations.md rename examples/todomvc/tests/{template.spec.ts => seed.spec.ts} (89%) diff --git a/docs/src/test-agents-js.md b/docs/src/test-agents-js.md index 15b65370340bc..94f27da20ae45 100644 --- a/docs/src/test-agents-js.md +++ b/docs/src/test-agents-js.md @@ -200,7 +200,7 @@ Playwright and should be regenerated whenever Playwright is updated. Example for Claude Code subagents: ```bash -npx playwright init-agents --claude +npx playwright init-agents --loop=claude ``` ### Specs in `specs/` diff --git a/examples/todomvc/.claude/agents/playwright-test-planner.md b/examples/todomvc/.claude/agents/playwright-test-planner.md index 0cd8fdaaff4c3..aac0ac50f7f69 100644 --- a/examples/todomvc/.claude/agents/playwright-test-planner.md +++ b/examples/todomvc/.claude/agents/playwright-test-planner.md @@ -6,13 +6,15 @@ model: sonnet color: green --- -You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test scenario design. Your expertise includes functional testing, usability testing, edge case identification, and comprehensive test coverage planning. +You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage planning. -When given a target web page or application, you will: +You will: 1. **Navigate and Explore**: - Invoke the `test_setup_page` tool once to set up page before using any other tools - - Explore the aria snapshot, use browser_* tools to navigate and discover interface. + - Explore the browser snapshot + - Do not take screenshots unless absolutely necessary + - Use browser_* tools to navigate and discover interface - Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality 2. **Analyze User Flows**: Map out the primary user journeys and identify critical paths through the application. Consider different user types and their typical behaviors. @@ -35,6 +37,41 @@ When given a target web page or application, you will: - Each scenario formatted with numbered steps - Clear expected results for verification + +# TodoMVC Application - Comprehensive Test Plan + +## Application Overview + +The TodoMVC application is a React-based todo list manager that provides core task management functionality. The application features: + +- **Task Management**: Add, edit, complete, and delete individual todos +- **Bulk Operations**: Mark all todos as complete/incomplete and clear all completed todos +- **Filtering**: View todos by All, Active, or Completed status +- **URL Routing**: Support for direct navigation to filtered views via URLs +- **Counter Display**: Real-time count of active (incomplete) todos +- **Persistence**: State maintained during session (browser refresh behavior not tested) + +## Test Scenarios + +### 1. Adding New Todos + +#### 1.1 Add Valid Todo +**Steps:** +1. Use seed test `tests/seed.spec.ts` +2. Click in the "What needs to be done?" input field +3. Type "Buy groceries" +4. Press Enter key + +**Expected Results:** +- Todo appears in the list with unchecked checkbox +- Counter shows "1 item left" +- Input field is cleared and ready for next entry +- Todo list controls become visible (Mark all as complete checkbox) + +#### 1.2 +... + + **Quality Standards**: - Write steps that are specific enough for any tester to follow - Include negative testing scenarios diff --git a/examples/todomvc/prompt.claudecode.md b/examples/todomvc/prompt.claudecode.md index 3be701148c070..0bd66985543a7 100644 --- a/examples/todomvc/prompt.claudecode.md +++ b/examples/todomvc/prompt.claudecode.md @@ -4,10 +4,10 @@ Test basic functionality of todo app. ## Test setup: tests/template.spec.ts:11 ## Steps -- Use `playwright-test-planner` subagent to create a test plan. - Save the test plan as `specs/test-plan.md`. +- Use `playwright-test-planner` subagent to create a test plan for "Basic operations". + Use seed test from `tests/seed.spec.ts` to init page. Save the test plan as `specs/basic-operations.md`. -- For each scenario in `specs/test-plan.md`, use `playwright-test-generator` +- For each scenario in `specs/basic-operations.md`, use `playwright-test-generator` subagent to perform the scenario and generate the test source code into `tests/` folder. - Use `playwright-test-healer` subagent to fix the failing tests. diff --git a/examples/todomvc/specs/basic-operations.md b/examples/todomvc/specs/basic-operations.md new file mode 100644 index 0000000000000..5477258a43d53 --- /dev/null +++ b/examples/todomvc/specs/basic-operations.md @@ -0,0 +1,466 @@ +# TodoMVC Application - Basic Operations Test Plan + +## Application Overview + +The TodoMVC application is a React-based todo list manager that demonstrates standard todo application functionality. The application provides comprehensive task management capabilities with a clean, intuitive interface. Key features include: + +- **Task Management**: Add, edit, complete, and delete individual todos +- **Bulk Operations**: Mark all todos as complete/incomplete and clear all completed todos +- **Filtering System**: View todos by All, Active, or Completed status with URL routing support +- **Real-time Counter**: Display of active (incomplete) todo count +- **Interactive UI**: Hover states, edit-in-place functionality, and responsive design +- **State Persistence**: Maintains state during session navigation + +## Test Scenarios + +### 1. Adding New Todos + +#### 1.1 Add Valid Todo +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Click in the "What needs to be done?" input field +3. Type "Buy groceries" +4. Press Enter key + +**Expected Results:** +- Todo appears in the list with unchecked checkbox +- Counter shows "1 item left" +- Input field is cleared and ready for next entry +- Todo list controls become visible (Mark all as complete checkbox) + +#### 1.2 Add Multiple Todos +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add first todo: "Buy groceries" and press Enter +3. Add second todo: "Walk the dog" and press Enter +4. Add third todo: "Call dentist" and press Enter + +**Expected Results:** +- All three todos appear in the list in the order added +- Counter shows "3 items left" +- Each todo has its own unchecked checkbox +- Input field remains active and cleared after each addition + +#### 1.3 Add Todo with Special Characters +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Type "Buy coffee & donuts (2-3 pieces) @$5.99!" in input field +3. Press Enter + +**Expected Results:** +- Todo appears exactly as typed with all special characters preserved +- Counter shows "1 item left" +- No encoding or display issues with special characters + +#### 1.4 Add Empty Todo (Negative Test) +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Click in input field but don't type anything +3. Press Enter + +**Expected Results:** +- No todo is added to the list +- List remains empty +- No counter appears +- Input field remains focused and empty + +#### 1.5 Add Todo with Only Whitespace (Negative Test) +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Type only spaces " " in input field +3. Press Enter + +**Expected Results:** +- No todo is added to the list +- List remains empty +- Input field is cleared +- No counter appears + +### 2. Marking Todos Complete/Incomplete + +#### 2.1 Mark Single Todo Complete +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todo "Buy groceries" +3. Click the checkbox next to "Buy groceries" + +**Expected Results:** +- Checkbox becomes checked +- Todo text may show strikethrough or completed styling +- Counter shows "0 items left" +- "Clear completed" button appears +- Delete button (×) becomes visible on hover + +#### 2.2 Mark Multiple Todos Complete +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +3. Click checkbox for "Buy groceries" +4. Click checkbox for "Call dentist" + +**Expected Results:** +- Two todos show as completed +- Counter shows "1 item left" (for "Walk the dog") +- "Clear completed" button appears +- Only "Walk the dog" remains unchecked + +#### 2.3 Toggle Todo Back to Incomplete +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todo "Buy groceries" +3. Click checkbox to mark complete +4. Click checkbox again to mark incomplete + +**Expected Results:** +- Checkbox becomes unchecked +- Completed styling is removed +- Counter shows "1 item left" +- "Clear completed" button disappears if no other completed todos exist + +#### 2.4 Mark All Todos Complete +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +3. Click the "Mark all as complete" checkbox + +**Expected Results:** +- All todo checkboxes become checked +- Counter shows "0 items left" +- "Clear completed" button appears +- "Mark all as complete" checkbox shows as checked + +#### 2.5 Toggle All Todos Back to Incomplete +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog" +3. Click "Mark all as complete" checkbox +4. Click "Mark all as complete" checkbox again + +**Expected Results:** +- All todo checkboxes become unchecked +- Counter shows "2 items left" +- "Clear completed" button disappears +- "Mark all as complete" checkbox shows as unchecked + +### 3. Editing Todos + +#### 3.1 Edit Todo Text +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todo "Buy groceries" +3. Double-click on the todo text "Buy groceries" +4. Clear text and type "Buy organic groceries" +5. Press Enter + +**Expected Results:** +- Todo enters edit mode with text selected +- Text changes to "Buy organic groceries" +- Todo exits edit mode +- Counter remains "1 item left" + +#### 3.2 Cancel Edit with Escape +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todo "Buy groceries" +3. Double-click on the todo text +4. Change text to "Buy organic groceries" +5. Press Escape key + +**Expected Results:** +- Todo exits edit mode +- Text reverts to original "Buy groceries" +- No changes are saved +- Todo remains in its original state + +#### 3.3 Edit Todo to Empty Text (Negative Test) +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todo "Buy groceries" +3. Double-click on the todo text +4. Clear all text +5. Press Enter + +**Expected Results:** +- Todo should be deleted/removed from list +- Counter decrements appropriately +- List becomes empty if this was the only todo + +#### 3.4 Edit Multiple Todos +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog" +3. Double-click "Buy groceries", change to "Buy organic groceries", press Enter +4. Double-click "Walk the dog", change to "Walk the cat", press Enter + +**Expected Results:** +- Both todos are updated with new text +- Counter remains "2 items left" +- Both todos maintain their completion state + +### 4. Deleting Todos + +#### 4.1 Delete Single Todo +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todo "Buy groceries" +3. Hover over the todo item +4. Click the delete button (×) + +**Expected Results:** +- Todo is removed from the list +- List becomes empty +- Counter disappears +- Todo controls (filters, mark all) disappear + +#### 4.2 Delete Multiple Todos +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +3. Hover over "Walk the dog" and click delete (×) +4. Hover over "Call dentist" and click delete (×) + +**Expected Results:** +- Only "Buy groceries" remains in the list +- Counter shows "1 item left" +- List controls remain visible + +#### 4.3 Delete Completed Todo +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog" +3. Mark "Buy groceries" as complete +4. Hover over "Buy groceries" and click delete (×) + +**Expected Results:** +- "Buy groceries" is removed from list +- Only "Walk the dog" remains +- Counter shows "1 item left" +- "Clear completed" button disappears + +#### 4.4 Clear All Completed Todos +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +3. Mark "Buy groceries" and "Call dentist" as complete +4. Click "Clear completed" button + +**Expected Results:** +- Both completed todos are removed +- Only "Walk the dog" remains +- Counter shows "1 item left" +- "Clear completed" button disappears + +### 5. Filtering Todos + +#### 5.1 Filter by All +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +3. Mark "Buy groceries" as complete +4. Click "All" filter link + +**Expected Results:** +- All todos are visible (both completed and active) +- URL shows "#/" +- "All" filter appears active/highlighted +- Counter shows "2 items left" + +#### 5.2 Filter by Active +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +3. Mark "Buy groceries" as complete +4. Click "Active" filter link + +**Expected Results:** +- Only incomplete todos are visible ("Walk the dog", "Call dentist") +- Completed todo "Buy groceries" is hidden +- URL shows "#/active" +- "Active" filter appears active/highlighted +- Counter shows "2 items left" + +#### 5.3 Filter by Completed +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +3. Mark "Buy groceries" and "Walk the dog" as complete +4. Click "Completed" filter link + +**Expected Results:** +- Only completed todos are visible ("Buy groceries", "Walk the dog") +- Active todo "Call dentist" is hidden +- URL shows "#/completed" +- "Completed" filter appears active/highlighted +- Counter still shows "1 item left" (maintains global count) + +#### 5.4 Navigate Between Filters +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog" +3. Mark "Buy groceries" as complete +4. Click "Active" filter +5. Click "Completed" filter +6. Click "All" filter + +**Expected Results:** +- Each filter shows appropriate todos +- URL updates correctly for each filter +- Active filter is highlighted appropriately +- Counter remains consistent across filters +- Todos maintain their state when switching views + +### 6. Counter and Status Display + +#### 6.1 Counter with Single Item +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add one todo "Buy groceries" + +**Expected Results:** +- Counter displays "1 item left" (singular form) +- Counter updates immediately when todo is added + +#### 6.2 Counter with Multiple Items +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog" + +**Expected Results:** +- Counter displays "2 items left" (plural form) +- Counter shows correct count + +#### 6.3 Counter Updates with Completion +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +3. Mark "Buy groceries" as complete +4. Mark "Walk the dog" as complete + +**Expected Results:** +- Counter starts at "3 items left" +- After first completion: "2 items left" +- After second completion: "1 item left" +- Counter updates immediately with each change + +#### 6.4 Counter with All Items Complete +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todo "Buy groceries" +3. Mark it as complete + +**Expected Results:** +- Counter shows "0 items left" +- "Clear completed" button is visible +- Filter links remain functional + +### 7. Bulk Operations + +#### 7.1 Mark All Complete When None Completed +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +3. Click "Mark all as complete" checkbox + +**Expected Results:** +- All todos become checked/completed +- Counter shows "0 items left" +- "Clear completed" button appears +- "Mark all as complete" checkbox shows as checked + +#### 7.2 Mark All Incomplete When All Completed +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog" +3. Mark both todos as complete individually +4. Click "Mark all as complete" checkbox + +**Expected Results:** +- All todos become unchecked/incomplete +- Counter shows "2 items left" +- "Clear completed" button disappears +- "Mark all as complete" checkbox shows as unchecked + +#### 7.3 Mark All with Mixed State +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +3. Mark "Buy groceries" as complete +4. Click "Mark all as complete" checkbox + +**Expected Results:** +- All todos become completed (including the already completed one) +- Counter shows "0 items left" +- "Mark all as complete" checkbox shows as checked + +### 8. Edge Cases and Error Handling + +#### 8.1 Very Long Todo Text +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Type a very long todo text (200+ characters) +3. Press Enter + +**Expected Results:** +- Todo is added successfully +- Text wraps appropriately in the display +- Interface remains usable +- Edit functionality works with long text + +#### 8.2 Rapid Sequential Actions +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Quickly add multiple todos by typing and pressing Enter rapidly +3. Quickly toggle completion states +4. Rapidly switch between filters + +**Expected Results:** +- All actions are processed correctly +- Counter updates accurately +- No todos are lost or duplicated +- Interface remains responsive + +#### 8.3 Direct URL Navigation +**Steps:** +1. Navigate directly to `{base_url}#/active` +2. Navigate directly to `{base_url}#/completed` +3. Navigate directly to `{base_url}#/` + +**Expected Results:** +- Page loads correctly for each URL +- Appropriate filter is active +- Interface is fully functional +- No JavaScript errors occur + +#### 8.4 Todo Operations Across Filters +**Steps:** +1. Use seed test `tests/seed.spec.ts` to initialize page +2. Add todos: "Buy groceries", "Walk the dog" +3. Navigate to "Active" filter +4. Mark "Buy groceries" as complete +5. Navigate to "Completed" filter +6. Delete "Buy groceries" + +**Expected Results:** +- Operations work correctly across filter views +- Todo states are maintained when switching filters +- Counter updates appropriately +- UI remains consistent + +## Test Data Considerations + +- **Todo Text Variations**: Test with short text, long text, special characters, Unicode characters, HTML entities +- **Volume Testing**: Test with 1, 2, 10, 50+ todos to ensure performance +- **State Combinations**: Test all combinations of completed/incomplete todos with different filters +- **Boundary Values**: Test edge cases like exactly 0 items, exactly 1 item, maximum reasonable todo count + +## Success Criteria + +All test scenarios should pass without: +- JavaScript console errors +- Visual layout issues +- Incorrect counter displays +- Lost or corrupted todo data +- Non-functional UI elements +- Accessibility violations + +The application should maintain consistent behavior across all supported browsers and provide a smooth, intuitive user experience for basic todo management operations. \ No newline at end of file diff --git a/examples/todomvc/tests/template.spec.ts b/examples/todomvc/tests/seed.spec.ts similarity index 89% rename from examples/todomvc/tests/template.spec.ts rename to examples/todomvc/tests/seed.spec.ts index 84df685985c6d..4f685f0a3e901 100644 --- a/examples/todomvc/tests/template.spec.ts +++ b/examples/todomvc/tests/seed.spec.ts @@ -8,7 +8,7 @@ test.beforeEach(async ({ page }) => { await page.goto('https://demo.playwright.dev/todomvc'); }); -test('template', async ({ page }) => { +test('seed', async ({ page }) => { // This test tells agents how to start recording the test // so that the page was already configured. }); diff --git a/packages/playwright/src/agents/planner.md b/packages/playwright/src/agents/planner.md index 98ecb1cc8c224..ee00dd96026c5 100644 --- a/packages/playwright/src/agents/planner.md +++ b/packages/playwright/src/agents/planner.md @@ -28,13 +28,15 @@ tools: - playwright-test/test_setup_page --- -You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test scenario design. Your expertise includes functional testing, usability testing, edge case identification, and comprehensive test coverage planning. +You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage planning. -When given a target web page or application, you will: +You will: 1. **Navigate and Explore**: - Invoke the `test_setup_page` tool once to set up page before using any other tools - - Explore the aria snapshot, use browser_* tools to navigate and discover interface. + - Explore the browser snapshot + - Do not take screenshots unless absolutely necessary + - Use browser_* tools to navigate and discover interface - Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality 2. **Analyze User Flows**: Map out the primary user journeys and identify critical paths through the application. Consider different user types and their typical behaviors. @@ -51,12 +53,47 @@ When given a target web page or application, you will: - Assumptions about starting state (always assume blank/fresh state) - Success criteria and failure conditions -5. **Create Documentation**: Save your test plan as a markdown file in specs/ folder with: +5. **Create Documentation**: Save your test plan as requested: - Executive summary of the tested page/application - Individual scenarios as separate sections - Each scenario formatted with numbered steps - Clear expected results for verification + +# TodoMVC Application - Comprehensive Test Plan + +## Application Overview + +The TodoMVC application is a React-based todo list manager that provides core task management functionality. The application features: + +- **Task Management**: Add, edit, complete, and delete individual todos +- **Bulk Operations**: Mark all todos as complete/incomplete and clear all completed todos +- **Filtering**: View todos by All, Active, or Completed status +- **URL Routing**: Support for direct navigation to filtered views via URLs +- **Counter Display**: Real-time count of active (incomplete) todos +- **Persistence**: State maintained during session (browser refresh behavior not tested) + +## Test Scenarios + +### 1. Adding New Todos + +#### 1.1 Add Valid Todo +**Steps:** +1. Use seed test `tests/seed.spec.ts` +2. Click in the "What needs to be done?" input field +3. Type "Buy groceries" +4. Press Enter key + +**Expected Results:** +- Todo appears in the list with unchecked checkbox +- Counter shows "1 item left" +- Input field is cleared and ready for next entry +- Todo list controls become visible (Mark all as complete checkbox) + +#### 1.2 +... + + **Quality Standards**: - Write steps that are specific enough for any tester to follow - Include negative testing scenarios diff --git a/packages/playwright/src/mcp/test/testTools.ts b/packages/playwright/src/mcp/test/testTools.ts index d5a832ca0deaf..894e64fe0f3e9 100644 --- a/packages/playwright/src/mcp/test/testTools.ts +++ b/packages/playwright/src/mcp/test/testTools.ts @@ -125,7 +125,7 @@ export const setupPage = defineTestTool({ description: 'Setup the page for test', inputSchema: z.object({ project: z.string().optional().describe('Project to use for setup. For example: "chromium", if no project is provided uses the first project in the config.'), - testLocation: z.string().optional().describe('Location of the test to use for setup. For example: "test/e2e/file.spec.ts:20". Sets up blank page if no location is provided.'), + testLocation: z.string().optional().describe('Location of the seed test to use for setup. For example: "test/seed/default.spec.ts:20".'), }), type: 'readOnly', }, @@ -138,16 +138,16 @@ export const setupPage = defineTestTool({ let testLocation = params.testLocation; if (!testLocation) { - testLocation = '.template.spec.ts'; + testLocation = 'default.seed.spec.ts'; const config = await testRunner.loadConfig(); const project = params.project ? config.projects.find(p => p.project.name === params.project) : config.projects[0]; const testDir = project?.project.testDir || configDir; - const templateFile = path.join(testDir, testLocation); - if (!fs.existsSync(templateFile)) { - await fs.promises.writeFile(templateFile, ` - import { test, expect } from '@playwright/test'; - test('template', async ({ page }) => {}); - `); + const seedFile = path.join(testDir, testLocation); + if (!fs.existsSync(seedFile)) { + await fs.promises.writeFile(seedFile, `import { test, expect } from '@playwright/test'; + +test('seed', async ({ page }) => {}); +`); } } diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 5bf80e7b29541..9c8cd1c28f6ac 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -177,15 +177,15 @@ function addTestMCPServerCommand(program: Command) { function addInitAgentsCommand(program: Command) { const command = program.command('init-agents', { hidden: true }); command.description('Initialize repository agents for the Claude Code'); - command.option('--claude', 'Initialize repository agents for the Claude Code'); - command.option('--opencode', 'Initialize repository agents for the Opencode'); - command.option('--vscode', 'Initialize repository agents for the VS Code Copilot'); + const option = command.createOption('--loop ', 'Agentic loop provider'); + option.choices(['claude', 'opencode', 'vscode']); + command.addOption(option); command.action(async opts => { - if (opts.opencode) + if (opts.loop === 'opencode') await initOpencodeRepo(); - else if (opts.vscode) + else if (opts.loop === 'vscode') await initVSCodeRepo(); - else + else if (opts.loop === 'claude') await initClaudeCodeRepo(); }); } diff --git a/tests/mcp/test-tools.spec.ts b/tests/mcp/test-tools.spec.ts index 4dc8fb4de8928..8336833a94f57 100644 --- a/tests/mcp/test-tools.spec.ts +++ b/tests/mcp/test-tools.spec.ts @@ -385,7 +385,7 @@ test('test_setup_page without location respects testsDir', async ({ startClient ### Current page snapshot: `); - expect(fs.existsSync(test.info().outputPath('tests', '.template.spec.ts'))).toBe(true); + expect(fs.existsSync(test.info().outputPath('tests', 'default.seed.spec.ts'))).toBe(true); }); async function prepareDebugTest(startClient: StartClient) { From 2a1ccaf587e9ba949c2fe9d84410e0360a44513f Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 18 Sep 2025 11:15:11 -0700 Subject: [PATCH 192/329] chore(build): trim old transformed source files when generating new ones (#37363) --- .../src/transform/compilationCache.ts | 31 +++++- tests/playwright-test/cache.spec.ts | 103 ++++++++++++++++++ tests/playwright-test/clear-cache.spec.ts | 48 -------- .../playwright-test-fixtures.ts | 4 +- 4 files changed, 131 insertions(+), 55 deletions(-) create mode 100644 tests/playwright-test/cache.spec.ts delete mode 100644 tests/playwright-test/clear-cache.spec.ts diff --git a/packages/playwright/src/transform/compilationCache.ts b/packages/playwright/src/transform/compilationCache.ts index 87fb7f5654b95..d132f5f50dbfc 100644 --- a/packages/playwright/src/transform/compilationCache.ts +++ b/packages/playwright/src/transform/compilationCache.ts @@ -18,6 +18,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; +import { calculateSha1 } from 'playwright-core/lib/utils'; import { isWorkerProcess } from '../common/globals'; import { sourceMapSupport } from '../utilsBundle'; @@ -104,7 +105,7 @@ type CompilationCacheLookupResult = { addToCache?: (code: string, map: any | undefined | null, data: Map) => { serializedCache?: any }; }; -export function getFromCompilationCache(filename: string, hash: string, moduleUrl?: string): CompilationCacheLookupResult { +export function getFromCompilationCache(filename: string, contentHash: string, moduleUrl?: string): CompilationCacheLookupResult { // First check the memory cache by filename, this cache will always work in the worker, // because we just compiled this file in the loader. const cache = memoryCache.get(filename); @@ -117,7 +118,10 @@ export function getFromCompilationCache(filename: string, hash: string, moduleUr } // Then do the disk cache, this cache works between the Playwright Test runs. - const cachePath = calculateCachePath(filename, hash); + const filePathHash = calculateFilePathHash(filename); + const hashPrefix = filePathHash + '_' + contentHash.substring(0, 7); + const cacheFolderName = filePathHash.substring(0, 2); + const cachePath = calculateCachePath(filename, cacheFolderName, hashPrefix); const codePath = cachePath + '.js'; const sourceMapPath = cachePath + '.map'; const dataPath = cachePath + '.data'; @@ -132,6 +136,8 @@ export function getFromCompilationCache(filename: string, hash: string, moduleUr addToCache: (code: string, map: any | undefined | null, data: Map) => { if (isWorkerProcess()) return {}; + // Trim cache. This won't help with deleted files, but it will remove storing multiple copies of the same file + clearOldCacheEntries(cacheFolderName, filePathHash); fs.mkdirSync(path.dirname(cachePath), { recursive: true }); if (map) fs.writeFileSync(sourceMapPath, JSON.stringify(map), 'utf8'); @@ -168,9 +174,24 @@ export function addToCompilationCache(payload: SerializedCompilationCache) { } } -function calculateCachePath(filePath: string, hash: string): string { - const fileName = path.basename(filePath, path.extname(filePath)).replace(/\W/g, '') + '_' + hash; - return path.join(cacheDir, hash[0] + hash[1], fileName); +function calculateFilePathHash(filePath: string): string { + // Larger file path hash allows for fewer collisions compared to content, as we only check file path collision for deleting files + return calculateSha1(filePath).substring(0, 10); +} + +function calculateCachePath(filePath: string, cacheFolderName: string, hashPrefix: string): string { + const fileName = hashPrefix + '_' + path.basename(filePath, path.extname(filePath)).replace(/\W/g, ''); + return path.join(cacheDir, cacheFolderName, fileName); +} + +function clearOldCacheEntries(cacheFolderName: string, filePathHash: string) { + const cachePath = path.join(cacheDir, cacheFolderName); + try { + const cachedRelevantFiles = fs.readdirSync(cachePath).filter(file => file.startsWith(filePathHash)); + for (const file of cachedRelevantFiles) + fs.rmSync(path.join(cachePath, file), { force: true }); + } catch { + } } // Since ESM and CJS collect dependencies differently, diff --git a/tests/playwright-test/cache.spec.ts b/tests/playwright-test/cache.spec.ts new file mode 100644 index 0000000000000..132cf8ca6ee68 --- /dev/null +++ b/tests/playwright-test/cache.spec.ts @@ -0,0 +1,103 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +import { test, expect } from './playwright-test-fixtures'; + +test('should clear cache with type:module', async ({ runCLICommand }) => { + const result = await runCLICommand({ + 'playwright.config.ts': ` + import { defineConfig } from '@playwright/test'; + export default defineConfig({}); + `, + 'package.json': ` + { "type": "module" } + `, + 'a.spec.ts': ` + import { test } from '@playwright/test'; + test('example', () => {}); + `, + }, 'clear-cache'); + expect(result.exitCode).toBe(0); +}); + +test('should clear cache for ct', async ({ runCLICommand }) => { + const result = await runCLICommand({ + 'playwright.config.ts': ` + import { defineConfig } from '@playwright/test'; + export default defineConfig({}); + `, + 'a.spec.ts': ` + import { test } from '@playwright/test'; + test('example', () => {}); + `, + }, 'clear-cache', []); + expect(result.exitCode).toBe(0); +}); + +test('should automatically clean cached versions of a changed file', async ({ runInlineTest, writeFiles }) => { + const cacheDir = test.info().outputPath('playwright-test-cache'); + await runInlineTest({ + 'a.spec.ts': ` + import { test } from '@playwright/test'; + test('example', () => {}); + `, + }, undefined, { + PWTEST_CACHE_DIR: cacheDir + }); + + const cacheDirectories = await fs.promises.readdir(cacheDir); + expect(cacheDirectories).toHaveLength(1); + const testCacheDirectory = path.join(cacheDir, cacheDirectories[0]); + + const matchRegex = (extension: string) => new RegExp('^([0-9a-f]{10})_([0-9a-f]{7})_aspec\\.' + extension + '$', 'i'); + + let cachedFiles = await fs.promises.readdir(testCacheDirectory); + cachedFiles.sort(); + expect(cachedFiles).toHaveLength(2); + expect(cachedFiles[0]).toMatch(matchRegex('js')); + expect(cachedFiles[1]).toMatch(matchRegex('map')); + + const initialMatches = cachedFiles[0].match(matchRegex('js')); + expect(initialMatches).toHaveLength(3); + const firstPathHash = initialMatches[1]; + const firstTestHash = initialMatches[2]; + + await runInlineTest({ + 'a.spec.ts': ` + import { test } from '@playwright/test'; + test('modified test', () => {}); + `, + }, undefined, { + PWTEST_CACHE_DIR: cacheDir + }); + + cachedFiles = await fs.promises.readdir(testCacheDirectory); + cachedFiles.sort(); + expect(cachedFiles).toHaveLength(2); + expect(cachedFiles[0]).toMatch(matchRegex('js')); + expect(cachedFiles[1]).toMatch(matchRegex('map')); + + const finalMatches = cachedFiles[0].match(matchRegex('js')); + expect(finalMatches).toHaveLength(3); + const finalPathHash = finalMatches[1]; + const finalTestHash = finalMatches[2]; + + expect(finalPathHash).toBe(firstPathHash); + expect(finalTestHash).not.toBe(firstTestHash); +}); diff --git a/tests/playwright-test/clear-cache.spec.ts b/tests/playwright-test/clear-cache.spec.ts deleted file mode 100644 index 128896c2e3ecc..0000000000000 --- a/tests/playwright-test/clear-cache.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './playwright-test-fixtures'; - -test('should clear cache with type:module', async ({ runCLICommand }) => { - const result = await runCLICommand({ - 'playwright.config.ts': ` - import { defineConfig } from '@playwright/test'; - export default defineConfig({}); - `, - 'package.json': ` - { "type": "module" } - `, - 'a.spec.ts': ` - import { test } from '@playwright/test'; - test('example', () => {}); - `, - }, 'clear-cache'); - expect(result.exitCode).toBe(0); -}); - -test('should clear cache for ct', async ({ runCLICommand }) => { - const result = await runCLICommand({ - 'playwright.config.ts': ` - import { defineConfig } from '@playwright/test'; - export default defineConfig({}); - `, - 'a.spec.ts': ` - import { test } from '@playwright/test'; - test('example', () => {}); - `, - }, 'clear-cache', []); - expect(result.exitCode).toBe(0); -}); diff --git a/tests/playwright-test/playwright-test-fixtures.ts b/tests/playwright-test/playwright-test-fixtures.ts index c06019a3b2d69..641b0f18c2f13 100644 --- a/tests/playwright-test/playwright-test-fixtures.ts +++ b/tests/playwright-test/playwright-test-fixtures.ts @@ -281,7 +281,7 @@ export const test = base const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-')); await use(async (files: Files, params: Params = {}, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => { const baseDir = await writeFiles(testInfo, files, true); - return await runPlaywrightTest(childProcess, baseDir, params, { ...env, PWTEST_CACHE_DIR: cacheDir }, options, files, mergeReports, useIntermediateMergeReport); + return await runPlaywrightTest(childProcess, baseDir, params, { PWTEST_CACHE_DIR: cacheDir, ...env }, options, files, mergeReports, useIntermediateMergeReport); }); await removeFolders([cacheDir]); }, @@ -313,7 +313,7 @@ export const test = base let testProcess: TestChildProcess | undefined; await use(async (files: Files, params: Params = {}, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => { const baseDir = await writeFiles(testInfo, files, true); - testProcess = startPlaywrightTest(childProcess, baseDir, params, { ...env, PWTEST_CACHE_DIR: cacheDir }, options); + testProcess = startPlaywrightTest(childProcess, baseDir, params, { PWTEST_CACHE_DIR: cacheDir, ...env }, options); return testProcess; }); await testProcess?.kill(); From 8a6a4522e83639a6a8327a5d13876c8869e48717 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 18 Sep 2025 22:46:52 +0100 Subject: [PATCH 193/329] fix(test-mcp): handle dependent projects in tools (#37459) --- packages/playwright/src/mcp/test/testTools.ts | 7 +- packages/playwright/src/runner/dispatcher.ts | 2 +- .../playwright/src/runner/failureTracker.ts | 10 +- packages/playwright/src/runner/loadUtils.ts | 7 +- .../playwright/src/runner/projectUtils.ts | 7 +- packages/playwright/src/runner/tasks.ts | 11 +- packages/playwright/src/runner/testRunner.ts | 6 +- packages/playwright/src/runner/testServer.ts | 5 +- tests/mcp/test-tools.spec.ts | 116 +++++++++++++++++- 9 files changed, 154 insertions(+), 17 deletions(-) diff --git a/packages/playwright/src/mcp/test/testTools.ts b/packages/playwright/src/mcp/test/testTools.ts index 894e64fe0f3e9..b5ed411b0c401 100644 --- a/packages/playwright/src/mcp/test/testTools.ts +++ b/packages/playwright/src/mcp/test/testTools.ts @@ -23,6 +23,7 @@ import { z } from '../sdk/bundle'; import { terminalScreen } from '../../reporters/base'; import ListReporter from '../../reporters/list'; import ListModeReporter from '../../reporters/listModeReporter'; +import { findTopLevelProjects } from '../../runner/projectUtils'; import { defineTestTool } from './testTool'; import { StringWriteStream } from './streams'; @@ -68,6 +69,7 @@ export const runTests = defineTestTool({ const result = await testRunner.runTests(reporter, { locations: params.locations, projects: params.projects, + disableConfigReporters: true, }); const text = stream.content(); @@ -106,6 +108,7 @@ export const debugTest = defineTestTool({ timeout: 0, workers: 1, pauseOnError: true, + disableConfigReporters: true, }); const text = stream.content(); @@ -140,10 +143,11 @@ export const setupPage = defineTestTool({ if (!testLocation) { testLocation = 'default.seed.spec.ts'; const config = await testRunner.loadConfig(); - const project = params.project ? config.projects.find(p => p.project.name === params.project) : config.projects[0]; + const project = params.project ? config.projects.find(p => p.project.name === params.project) : findTopLevelProjects(config)[0]; const testDir = project?.project.testDir || configDir; const seedFile = path.join(testDir, testLocation); if (!fs.existsSync(seedFile)) { + await fs.promises.mkdir(path.dirname(seedFile), { recursive: true }); await fs.promises.writeFile(seedFile, `import { test, expect } from '@playwright/test'; test('seed', async ({ page }) => {}); @@ -158,6 +162,7 @@ test('seed', async ({ page }) => {}); timeout: 0, workers: 1, pauseAtEnd: true, + disableConfigReporters: true, }); const text = stream.content(); diff --git a/packages/playwright/src/runner/dispatcher.ts b/packages/playwright/src/runner/dispatcher.ts index 7d64c2f3a57bc..d9db512cd98a9 100644 --- a/packages/playwright/src/runner/dispatcher.ts +++ b/packages/playwright/src/runner/dispatcher.ts @@ -224,7 +224,7 @@ export class Dispatcher { extraEnv: this._extraEnvByProjectId.get(testGroup.projectId) || {}, outputDir, pauseOnError: this._failureTracker.pauseOnError(), - pauseAtEnd: this._failureTracker.pauseAtEnd(), + pauseAtEnd: this._failureTracker.pauseAtEnd(projectConfig), }); const handleOutput = (params: TestOutputPayload) => { const chunk = chunkFromParams(params); diff --git a/packages/playwright/src/runner/failureTracker.ts b/packages/playwright/src/runner/failureTracker.ts index c2d709ec6165e..ef276fa509bd0 100644 --- a/packages/playwright/src/runner/failureTracker.ts +++ b/packages/playwright/src/runner/failureTracker.ts @@ -15,13 +15,14 @@ */ import type { TestResult } from '../../types/testReporter'; -import type { FullConfigInternal } from '../common/config'; +import type { FullConfigInternal, FullProjectInternal } from '../common/config'; import type { Suite, TestCase } from '../common/test'; export class FailureTracker { private _failureCount = 0; private _hasWorkerErrors = false; private _rootSuite: Suite | undefined; + private _topLevelProjects: FullProjectInternal[] = []; private _pauseOnError: boolean; private _pauseAtEnd: boolean; @@ -30,8 +31,9 @@ export class FailureTracker { this._pauseAtEnd = options?.pauseAtEnd ?? false; } - onRootSuite(rootSuite: Suite) { + onRootSuite(rootSuite: Suite, topLevelProjects: FullProjectInternal[]) { this._rootSuite = rootSuite; + this._topLevelProjects = topLevelProjects; } onTestEnd(test: TestCase, result: TestResult) { @@ -48,8 +50,8 @@ export class FailureTracker { return this._pauseOnError; } - pauseAtEnd(): boolean { - return this._pauseAtEnd; + pauseAtEnd(inProject: FullProjectInternal): boolean { + return this._pauseAtEnd && this._topLevelProjects.includes(inProject); } hasReachedMaxFailures() { diff --git a/packages/playwright/src/runner/loadUtils.ts b/packages/playwright/src/runner/loadUtils.ts index 05c5bba7182ee..065cd0cc4a3be 100644 --- a/packages/playwright/src/runner/loadUtils.ts +++ b/packages/playwright/src/runner/loadUtils.ts @@ -120,7 +120,7 @@ export async function loadFileSuites(testRun: TestRun, mode: 'out-of-process' | } } -export async function createRootSuite(testRun: TestRun, errors: TestError[], shouldFilterOnly: boolean): Promise { +export async function createRootSuite(testRun: TestRun, errors: TestError[], shouldFilterOnly: boolean): Promise<{ rootSuite: Suite, topLevelProjects: FullProjectInternal[] }> { const config = testRun.config; // Create root suite, where each child will be a project suite with cloned file suites inside it. const rootSuite = new Suite('', 'root'); @@ -200,6 +200,7 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho if (config.postShardTestFilters.length) filterTestsRemoveEmptySuites(rootSuite, test => config.postShardTestFilters.every(filter => filter(test))); + const topLevelProjects = []; // Now prepend dependency projects without filtration. { // Filtering 'only' and sharding might have reduced the number of top-level projects. @@ -210,10 +211,12 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho for (const [project, level] of projectClosure.entries()) { if (level === 'dependency') rootSuite._prependSuite(buildProjectSuite(project, projectSuites.get(project)!)); + else + topLevelProjects.push(project); } } - return rootSuite; + return { rootSuite, topLevelProjects }; } function createProjectSuite(project: FullProjectInternal, fileSuites: Suite[]): Suite { diff --git a/packages/playwright/src/runner/projectUtils.ts b/packages/playwright/src/runner/projectUtils.ts index b52503c50c495..3ad639a9955ce 100644 --- a/packages/playwright/src/runner/projectUtils.ts +++ b/packages/playwright/src/runner/projectUtils.ts @@ -23,7 +23,7 @@ import { minimatch } from 'playwright-core/lib/utilsBundle'; import { createFileMatcher } from '../util'; -import type { FullProjectInternal } from '../common/config'; +import type { FullConfigInternal, FullProjectInternal } from '../common/config'; const readFileAsync = promisify(fs.readFile); @@ -115,6 +115,11 @@ export function buildProjectsClosure(projects: FullProjectInternal[], hasTests?: return result; } +export function findTopLevelProjects(config: FullConfigInternal): FullProjectInternal[] { + const closure = buildProjectsClosure(config.projects); + return [...closure].filter(entry => entry[1] === 'top-level').map(entry => entry[0]); +} + export function buildDependentProjects(forProjects: FullProjectInternal[], projects: FullProjectInternal[]): Set { const reverseDeps = new Map(projects.map(p => ([p, []]))); for (const project of projects) { diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index c383d71360c25..7d6245ad877a6 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -63,6 +63,7 @@ export class TestRun { readonly phases: Phase[] = []; projectFiles: Map = new Map(); projectSuites: Map = new Map(); + topLevelProjects: FullProjectInternal[] = []; constructor(config: FullConfigInternal, reporter: InternalReporter, options?: { pauseOnError?: boolean, pauseAtEnd?: boolean }) { this.config = config; @@ -232,8 +233,9 @@ export function createListFilesTask(): Task { return { title: 'load tests', setup: async (testRun, errors) => { - testRun.rootSuite = await createRootSuite(testRun, errors, false); - testRun.failureTracker.onRootSuite(testRun.rootSuite); + const { rootSuite, topLevelProjects } = await createRootSuite(testRun, errors, false); + testRun.rootSuite = rootSuite; + testRun.failureTracker.onRootSuite(rootSuite, topLevelProjects); await collectProjectsAndTestFiles(testRun, false); for (const [project, files] of testRun.projectFiles) { const projectSuite = new Suite(project.project.name, 'project'); @@ -269,8 +271,9 @@ export function createLoadTask(mode: 'out-of-process' | 'in-process', options: { testRun.config.preOnlyTestFilters.push(test => changedFiles.has(test.location.file)); } - testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly); - testRun.failureTracker.onRootSuite(testRun.rootSuite); + const { rootSuite, topLevelProjects } = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly); + testRun.rootSuite = rootSuite; + testRun.failureTracker.onRootSuite(rootSuite, topLevelProjects); // Fail when no tests. if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard && !testRun.config.cliOnlyChanged) { if (testRun.config.cliArgs.length) { diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index b6d1b4fa6dcd5..e5967c199be95 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -73,6 +73,8 @@ export type RunTestsParams = { connectWsEndpoint?: string; pauseOnError?: boolean; pauseAtEnd?: boolean; + doNotRunDepsOutsideProjectFilter?: boolean; + disableConfigReporters?: boolean; }; type FullResultStatus = reporterTypes.FullResult['status']; @@ -337,12 +339,12 @@ export class TestRunner extends EventEmitter { config.preOnlyTestFilters.push(test => testIdSet.has(test.id)); } - const configReporters = await createReporters(config, 'test', true); + const configReporters = params.disableConfigReporters ? [] : await createReporters(config, 'test', true); const reporter = new InternalReporter([...configReporters, userReporter]); const stop = new ManualPromise(); const tasks = [ createApplyRebaselinesTask(), - createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }), + createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: params.doNotRunDepsOutsideProjectFilter }), ...createRunTestsTasks(config), ]; const testRun = new TestRun(config, reporter, { pauseOnError: params.pauseOnError, pauseAtEnd: params.pauseAtEnd }); diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 26ac3a0e5245d..08fc4415e7700 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -200,7 +200,10 @@ export class TestServerDispatcher implements TestServerInterface { async runTests(params: Parameters[0]): ReturnType { const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e)); - const { status } = await this._testRunner.runTests(wireReporter, params); + const { status } = await this._testRunner.runTests(wireReporter, { + ...params, + doNotRunDepsOutsideProjectFilter: true, + }); return { status }; } diff --git a/tests/mcp/test-tools.spec.ts b/tests/mcp/test-tools.spec.ts index 8336833a94f57..9c64551ae23e6 100644 --- a/tests/mcp/test-tools.spec.ts +++ b/tests/mcp/test-tools.spec.ts @@ -17,6 +17,7 @@ import { test, expect, writeFiles, StartClient } from './fixtures'; import fs from 'fs'; +import path from 'path'; test.use({ mcpServerType: 'test-mcp' }); @@ -137,6 +138,51 @@ Running 2 tests using 1 worker `); }); +test('test_run should include dependencies', async ({ startClient }) => { + await writeFiles({ + 'playwright.config.ts': ` + module.exports = { + projects: [ + { name: 'setup', testMatch: /.*setup\\.ts/ }, + { name: 'chromium', dependencies: ['setup'] }, + ], + }; + `, + 'auth.setup.ts': ` + import { test as setup, expect } from '@playwright/test'; + setup('auth', async ({}) => { + expect(1 + 1).toBe(2); + }); + `, + 'example.test.ts': ` + import { test, expect } from '@playwright/test'; + test('example1', async ({}) => { + expect(1 + 1).toBe(2); + }); + test('example2', async ({}) => { + expect(1 + 1).toBe(2); + }); + ` + }); + + const { client } = await startClient(); + expect(await client.callTool({ + name: 'test_run', + arguments: { + locations: ['example.test.ts'], + projects: ['chromium'], + }, + })).toHaveTextResponse(` +Running 3 tests using 1 worker + + ok 1 [id=] [project=setup] › auth.setup.ts:3:12 › auth (XXms) + ok 2 [id=] [project=chromium] › example.test.ts:3:11 › example1 (XXms) + ok 3 [id=] [project=chromium] › example.test.ts:6:11 › example2 (XXms) + + 3 passed (XXms) +`); +}); + test('test_debug (passed)', async ({ startClient }) => { await writeFiles({ 'a.test.ts': ` @@ -217,7 +263,7 @@ test('test_debug / browser_snapshot', async ({ startClient }) => { }); }); -test('test_debug_test (pause/snapshot/resume)', async ({ startClient }) => { +test('test_debug (pause/snapshot/resume)', async ({ startClient }) => { const { client, id } = await prepareDebugTest(startClient); expect(await client.callTool({ @@ -350,6 +396,49 @@ test('test_setup_page', async ({ startClient }) => { }); }); +test('test_setup_page with dependencies', async ({ startClient }) => { + const baseDir = await writeFiles({ + 'playwright.config.ts': ` + module.exports = { + projects: [ + { name: 'setup', testMatch: /.*setup\\.ts/ }, + { name: 'chromium', dependencies: ['setup'] }, + { name: 'ignored', dependencies: ['chromium'] }, + ], + }; + `, + 'auth.setup.ts': ` + import { test as setup, expect } from '@playwright/test'; + setup('auth', async ({ page }, testInfo) => { + require('fs').writeFileSync(testInfo.outputPath('auth.txt'), 'done'); + }); + `, + 'template.test.ts': ` + import { test, expect } from '@playwright/test'; + test('template', async ({ page }, testInfo) => { + require('fs').writeFileSync(testInfo.outputPath('template.txt'), 'done'); + }); + `, + }); + + const { client } = await startClient(); + expect(await client.callTool({ + name: 'test_setup_page', + arguments: { + testLocation: 'template.test.ts:3', + project: 'chromium', + }, + })).toHaveTextResponse(`### Paused at end of test. ready for interaction + +### Current page snapshot: +`); + + // Should pause at the target test, not in a dependency or any other stray project. + expect(fs.existsSync(path.join(baseDir, 'test-results', 'auth.setup.ts-auth-setup', 'auth.txt'))).toBe(true); + expect(fs.existsSync(path.join(baseDir, 'test-results', 'template-template-chromium', 'template.txt'))).toBe(true); + expect(fs.existsSync(path.join(baseDir, 'test-results', 'template-template-ignored', 'template.txt'))).toBe(false); +}); + test('test_setup_page (no test location)', async ({ startClient }) => { const { client } = await startClient(); expect(await client.callTool({ @@ -361,6 +450,31 @@ test('test_setup_page (no test location)', async ({ startClient }) => { `); }); +test('test_setup_page chooses top-level project', async ({ startClient }) => { + const baseDir = await writeFiles({ + 'playwright.config.ts': ` + module.exports = { + projects: [ + { name: 'one', testDir: './one' }, + { name: 'two', testDir: './two', dependencies: ['one'] }, + ], + }; + `, + }); + + const { client } = await startClient(); + expect(await client.callTool({ + name: 'test_setup_page', + arguments: {}, + })).toHaveTextResponse(`### Paused at end of test. ready for interaction + +### Current page snapshot: +`); + + expect(fs.existsSync(path.join(baseDir, 'one', 'default.seed.spec.ts'))).toBe(false); + expect(fs.existsSync(path.join(baseDir, 'two', 'default.seed.spec.ts'))).toBe(true); +}); + test('test_setup_page without location respects testsDir', async ({ startClient }) => { await writeFiles({ 'playwright.config.ts': ` From 0574514a008cb9323858e260c67d31046582c85b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 18 Sep 2025 15:06:33 -0700 Subject: [PATCH 194/329] feat(mcp): support shared browser context (#37463) --- .../src/mcp/browser/browserContextFactory.ts | 50 +++++++++++++ packages/playwright/src/mcp/browser/config.ts | 2 + .../playwright/src/mcp/browser/watchdog.ts | 2 + packages/playwright/src/mcp/config.d.ts | 5 ++ packages/playwright/src/mcp/program.ts | 1 + tests/mcp/http.spec.ts | 74 ++++++++++++++++++- tests/mcp/sse.spec.ts | 72 +++++++++++++++++- 7 files changed, 202 insertions(+), 4 deletions(-) diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index f6b7a55e12f42..97cf9f12201af 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -31,6 +31,8 @@ import type { LaunchOptions } from '../../../../playwright-core/src/client/types import type { ClientInfo } from '../sdk/server'; export function contextFactory(config: FullConfig): BrowserContextFactory { + if (config.sharedBrowserContext) + return SharedContextFactory.create(config); if (config.browser.remoteEndpoint) return new RemoteContextFactory(config); if (config.browser.cdpEndpoint) @@ -259,3 +261,51 @@ async function startTraceServer(config: FullConfig, tracesDir: string): Promise< function createHash(data: string): string { return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); } + +export class SharedContextFactory implements BrowserContextFactory { + private _contextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> | undefined; + private _baseFactory: BrowserContextFactory; + private static _instance: SharedContextFactory | undefined; + + static create(config: FullConfig) { + if (SharedContextFactory._instance) + throw new Error('SharedContextFactory already exists'); + const baseConfig = { ...config, sharedBrowserContext: false }; + const baseFactory = contextFactory(baseConfig); + SharedContextFactory._instance = new SharedContextFactory(baseFactory); + return SharedContextFactory._instance; + } + + private constructor(baseFactory: BrowserContextFactory) { + this._baseFactory = baseFactory; + } + + async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { + if (!this._contextPromise) { + testDebug('create shared browser context'); + this._contextPromise = this._baseFactory.createContext(clientInfo, abortSignal, toolName); + } + + const { browserContext } = await this._contextPromise; + testDebug(`shared context client connected`); + return { + browserContext, + close: async () => { + testDebug(`shared context client disconnected`); + }, + }; + } + + static async dispose() { + await SharedContextFactory._instance?._dispose(); + } + + private async _dispose() { + const contextPromise = this._contextPromise; + this._contextPromise = undefined; + if (!contextPromise) + return; + const { close } = await contextPromise; + await close(); + } +} diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index 4e7f6e9d9679b..b41d07f585452 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -52,6 +52,7 @@ export type CLIOptions = { saveSession?: boolean; saveTrace?: boolean; secrets?: Record; + sharedBrowserContext?: boolean; storageState?: string; timeoutAction?: number; timeoutNavigation?: number; @@ -212,6 +213,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { saveSession: cliOptions.saveSession, saveTrace: cliOptions.saveTrace, secrets: cliOptions.secrets, + sharedBrowserContext: cliOptions.sharedBrowserContext, outputDir: cliOptions.outputDir, imageResponses: cliOptions.imageResponses, timeouts: { diff --git a/packages/playwright/src/mcp/browser/watchdog.ts b/packages/playwright/src/mcp/browser/watchdog.ts index c342e039f505b..a437accab7ce6 100644 --- a/packages/playwright/src/mcp/browser/watchdog.ts +++ b/packages/playwright/src/mcp/browser/watchdog.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { SharedContextFactory } from './browserContextFactory'; import { Context } from './context'; export function setupExitWatchdog() { @@ -25,6 +26,7 @@ export function setupExitWatchdog() { // eslint-disable-next-line no-restricted-properties setTimeout(() => process.exit(0), 15000); await Context.disposeAll(); + await SharedContextFactory.dispose(); // eslint-disable-next-line no-restricted-properties process.exit(0); }; diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index 845f0ede6cb47..aed8c4745b8ec 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -100,6 +100,11 @@ export type Config = { */ saveTrace?: boolean; + /** + * Reuse the same browser context between all connected HTTP clients. + */ + sharedBrowserContext?: boolean; + /** * Secrets are used to prevent LLM from getting sensitive data while * automating scenarios such as authentication. diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index acc349ba00756..852f4b5a75851 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -53,6 +53,7 @@ export function decorateCommand(command: Command, version: string) { .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.') .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.') .option('--secrets ', 'path to a file containing secrets in the dotenv format', dotenvFileLoader) + .option('--shared-browser-context', 'reuse the same browser context between all connected HTTP clients.') .option('--storage-state ', 'path to the storage state file for isolated sessions.') .option('--timeout-action ', 'specify action timeout in milliseconds, defaults to 5000ms', numberParser) .option('--timeout-navigation ', 'specify navigation timeout in milliseconds, defaults to 60000ms', numberParser) diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index 4a498072b12d5..458b2e6c6e0ef 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -24,7 +24,7 @@ import { test as baseTest, expect, mcpServerPath } from './fixtures'; import type { Config } from '../../packages/playwright/src/mcp/config'; import { ListRootsRequestSchema } from 'packages/playwright/lib/mcp/sdk/bundle'; -const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ +const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string, kill: () => void }> }>({ serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { let cp: ChildProcess | undefined; const userDataDir = testInfo.outputPath('user-data-dir'); @@ -55,7 +55,10 @@ const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noP resolve(match[1]); })); - return { url: new URL(url), stderr: () => stderr }; + return { url: new URL(url), stderr: () => stderr, kill: () => { + cp?.kill('SIGTERM'); + cp = undefined; + } }; }); cp?.kill('SIGTERM'); }, @@ -245,6 +248,73 @@ test('http transport browser lifecycle (persistent, multiclient)', async ({ serv await client2.close(); }); +test('http transport shared context', async ({ serverEndpoint, server }) => { + const { url, stderr, kill } = await serverEndpoint({ args: ['--shared-browser-context'] }); + + // Create first client and navigate + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test1', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + // Create second client - should reuse the same browser context + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test2', version: '1.0.0' }); + await client2.connect(transport2); + + // Get tabs from second client - should see the tab created by first client + const tabsResult = await client2.callTool({ + name: 'browser_tabs', + arguments: { action: 'list' }, + }); + + // Should have at least one tab (the one created by client1) + expect(tabsResult.content[0]?.text).toContain('tabs'); + + await transport1.terminateSession(); + await client1.close(); + + // Second client should still work since context is shared + await client2.callTool({ + name: 'browser_snapshot', + arguments: {}, + }); + + await transport2.terminateSession(); + await client2.close(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create http session/)).length).toBe(2); + expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2); + + // Should have only one context creation since it's shared + expect(lines.filter(line => line.match(/create shared browser context/)).length).toBe(1); + + // Should see client connect/disconnect messages + expect(lines.filter(line => line.match(/shared context client connected/)).length).toBe(2); + expect(lines.filter(line => line.match(/shared context client disconnected/)).length).toBe(2); + expect(lines.filter(line => line.match(/create context/)).length).toBe(2); + expect(lines.filter(line => line.match(/close context/)).length).toBe(2); + + // Context should only close when the server shuts down. + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(0); + }).toPass(); + + kill(); + + if (process.platform !== 'win32') { + await expect(async () => { + const lines = stderr().split('\n'); + // Context should only close when the server shuts down. + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); + }).toPass(); + } +}); + test('http transport (default)', async ({ serverEndpoint }) => { const { url } = await serverEndpoint(); const transport = new StreamableHTTPClientTransport(url); diff --git a/tests/mcp/sse.spec.ts b/tests/mcp/sse.spec.ts index d67a24a98b2ac..c214e44157724 100644 --- a/tests/mcp/sse.spec.ts +++ b/tests/mcp/sse.spec.ts @@ -23,7 +23,7 @@ import { test as baseTest, expect, mcpServerPath } from './fixtures'; import type { Config } from '../../packages/playwright/src/mcp/config'; -const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ +const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string, kill: () => void }> }>({ serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { let cp: ChildProcess | undefined; const userDataDir = testInfo.outputPath('user-data-dir'); @@ -54,7 +54,10 @@ const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noP resolve(match[1]); })); - return { url: new URL(url), stderr: () => stderr }; + return { url: new URL(url), stderr: () => stderr, kill: () => { + cp?.kill('SIGTERM'); + cp = undefined; + } }; }); cp?.kill('SIGTERM'); }, @@ -229,3 +232,68 @@ test('sse transport browser lifecycle (persistent, multiclient)', async ({ serve await client1.close(); await client2.close(); }); + +test('sse transport shared context', async ({ serverEndpoint, server }) => { + const { url, stderr, kill } = await serverEndpoint({ args: ['--shared-browser-context'] }); + + // Create first client and navigate + const transport1 = new SSEClientTransport(new URL('/sse', url)); + const client1 = new Client({ name: 'test1', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + // Create second client - should reuse the same browser context + const transport2 = new SSEClientTransport(new URL('/sse', url)); + const client2 = new Client({ name: 'test2', version: '1.0.0' }); + await client2.connect(transport2); + + // Get tabs from second client - should see the tab created by first client + const tabsResult = await client2.callTool({ + name: 'browser_tabs', + arguments: { action: 'list' }, + }); + + // Should have at least one tab (the one created by client1) + expect(tabsResult.content[0]?.text).toContain('tabs'); + + await client1.close(); + + // Second client should still work since context is shared + await client2.callTool({ + name: 'browser_snapshot', + arguments: {}, + }); + + await client2.close(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create SSE session/)).length).toBe(2); + expect(lines.filter(line => line.match(/delete SSE session/)).length).toBe(2); + + // Should have only one context creation since it's shared + expect(lines.filter(line => line.match(/create shared browser context/)).length).toBe(1); + + // Should see client connect/disconnect messages + expect(lines.filter(line => line.match(/shared context client connected/)).length).toBe(2); + expect(lines.filter(line => line.match(/shared context client disconnected/)).length).toBe(2); + expect(lines.filter(line => line.match(/create context/)).length).toBe(2); + expect(lines.filter(line => line.match(/close context/)).length).toBe(2); + + // Context should only close when the server shuts down. + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(0); + }).toPass(); + + kill(); + + if (process.platform !== 'win32') { + await expect(async () => { + const lines = stderr().split('\n'); + // Context should only close when the server shuts down. + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); + }).toPass(); + } +}); From 757c9cfa459b3f8ff2177f4becac5992a66a308e Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Fri, 19 Sep 2025 02:06:22 +0200 Subject: [PATCH 195/329] test(bidi): enable more tests for BiDi (#37468) --- tests/bidi/playwright.config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/bidi/playwright.config.ts b/tests/bidi/playwright.config.ts index d8c7e651056b6..e4dc010728d1b 100644 --- a/tests/bidi/playwright.config.ts +++ b/tests/bidi/playwright.config.ts @@ -92,10 +92,7 @@ for (const [key, channels] of Object.entries(browserToChannels)) { console.error(`Using executable at ${executablePath}`); for (const channel of channels) { const testIgnore: RegExp[] = [ - /library\/debug-controller/, /library\/inspector/, - /library\/trace-viewer.spec.ts/, - /library\/tracing.spec.ts/, /page\/page-leaks.spec.ts/, ]; if (browserName.toLowerCase().includes('firefox')) From cb3d18da7bbfef19cbe5d994ec5bdf25aa308840 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Fri, 19 Sep 2025 02:09:19 +0200 Subject: [PATCH 196/329] test(bidi): unskip or add expectations to some tests that work with BiDi (#37475) --- tests/library/browsercontext-network-event.spec.ts | 2 +- tests/library/favicon.spec.ts | 2 +- tests/library/har.spec.ts | 2 +- tests/page/page-event-console.spec.ts | 4 +++- tests/page/page-route.spec.ts | 4 ++-- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/library/browsercontext-network-event.spec.ts b/tests/library/browsercontext-network-event.spec.ts index 45d42c41bfb16..e1a60c8a4f3d0 100644 --- a/tests/library/browsercontext-network-event.spec.ts +++ b/tests/library/browsercontext-network-event.spec.ts @@ -105,7 +105,7 @@ it('should fire events in proper order', async ({ context, server }) => { }); it('should not fire events for favicon or favicon redirects', async ({ context, page, server, browserName, channel, headless }) => { - it.skip(headless && browserName !== 'firefox', 'headless browsers, except firefox, do not request favicons'); + it.skip(headless && browserName !== 'firefox' && browserName as any !== '_bidiFirefox', 'headless browsers, except firefox, do not request favicons'); it.skip(!headless && browserName === 'webkit' && !channel, 'headed webkit does not have a favicon feature'); const favicon = `/no-cache/favicon.ico`; const hashedFaviconUrl = `/favicon-hashed.ico`; diff --git a/tests/library/favicon.spec.ts b/tests/library/favicon.spec.ts index f3e318a60f61e..d267fdb1865ca 100644 --- a/tests/library/favicon.spec.ts +++ b/tests/library/favicon.spec.ts @@ -18,7 +18,7 @@ import { contextTest as it } from '../config/browserTest'; it('should load svg favicon with prefer-color-scheme', async ({ page, server, browserName, channel, headless, asset }) => { - it.skip(headless && browserName !== 'firefox', 'headless browsers, except firefox, do not request favicons'); + it.skip(headless && browserName !== 'firefox' && browserName as any !== '_bidiFirefox', 'headless browsers, except firefox, do not request favicons'); it.skip(!headless && browserName === 'webkit' && !channel, 'headed webkit does not have a favicon feature'); // Browsers aggressively cache favicons, so force bust with the diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index 6d420e87cc148..d0d97b0854588 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -699,7 +699,7 @@ it('should contain http2 for http2 requests', async ({ contextFactory }, testInf }); it('should filter favicon and favicon redirects', async ({ server, browserName, channel, headless, asset, contextFactory }, testInfo) => { - it.skip(headless && browserName !== 'firefox', 'headless browsers, except firefox, do not request favicons'); + it.skip(headless && browserName !== 'firefox' && browserName as any !== '_bidiFirefox', 'headless browsers, except firefox, do not request favicons'); it.skip(!headless && browserName === 'webkit' && !channel, 'headed webkit does not have a favicon feature'); const { page, getLog } = await pageWithHar(contextFactory, testInfo); diff --git a/tests/page/page-event-console.spec.ts b/tests/page/page-event-console.spec.ts index 2609a6cc272b4..081f743937e91 100644 --- a/tests/page/page-event-console.spec.ts +++ b/tests/page/page-event-console.spec.ts @@ -199,7 +199,9 @@ it('should use object previews for errors', async ({ page, browserName }) => { await page.evaluate(() => console.log(new Error('Exception'))); if (browserName === 'chromium') expect(text).toContain('.evaluate'); - if (browserName === 'webkit') + if (browserName as any === '_bidiChromium') + expect(text).toEqual('error'); + if (browserName === 'webkit' || browserName as any === '_bidiFirefox') expect(text).toEqual('Error: Exception'); if (browserName === 'firefox') expect(text).toEqual('Error'); diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts index 592d4957cf412..00b9754bd9de7 100644 --- a/tests/page/page-route.spec.ts +++ b/tests/page/page-route.spec.ts @@ -634,11 +634,11 @@ it('should support cors with GET', async ({ page, server, browserName }) => { const response = await fetch('https://example.com/cars?reject', { mode: 'cors' }); return response.json(); }).catch(e => e); - if (browserName === 'chromium') + if (browserName === 'chromium' || browserName === '_bidiChromium') expect(error.message).toContain('Failed'); if (browserName === 'webkit') expect(error.message).toContain('TypeError'); - if (browserName === 'firefox') + if (browserName === 'firefox' || browserName === '_bidiFirefox') expect(error.message).toContain('NetworkError'); } }); From 0d85d44ab0695a9ff6827b914a2e6d2fb8aea509 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Fri, 19 Sep 2025 02:10:25 +0200 Subject: [PATCH 197/329] chore(bidi): don't accept page in firefox launch arguments (#37469) --- packages/playwright-core/src/server/bidi/bidiFirefox.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/playwright-core/src/server/bidi/bidiFirefox.ts b/packages/playwright-core/src/server/bidi/bidiFirefox.ts index 8a11c0ba50f9a..40543a0d0f5b6 100644 --- a/packages/playwright-core/src/server/bidi/bidiFirefox.ts +++ b/packages/playwright-core/src/server/bidi/bidiFirefox.ts @@ -97,6 +97,8 @@ export class BidiFirefox extends BrowserType { const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile')); if (userDataDirArg) throw this._createUserDataDirArgMisuseError('--profile'); + if (args.find(arg => !arg.startsWith('-'))) + throw new Error('Arguments can not specify page to be opened'); const firefoxArguments = ['--remote-debugging-port=0']; if (headless) firefoxArguments.push('--headless'); From 1408667001ef1264a661a2b6a0230e2332091dff Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Fri, 19 Sep 2025 02:12:14 +0200 Subject: [PATCH 198/329] chore(bidi): remove stringified exceptionDetails from error messages (#37470) --- .../src/server/bidi/bidiExecutionContext.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts b/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts index 33a2890bfb853..c7e0a745ebafe 100644 --- a/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts +++ b/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts @@ -57,7 +57,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate { if (response.type === 'success') return BidiDeserializer.deserialize(response.result); if (response.type === 'exception') - throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails)); + throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text); throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response)); } @@ -76,7 +76,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate { throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result)); } if (response.type === 'exception') - throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails)); + throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text); throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response)); } @@ -95,7 +95,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate { userActivation: true, }); if (response.type === 'exception') - throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails)); + throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text); if (response.type === 'success') { if (returnByValue) return parseEvaluationResultValue(BidiDeserializer.deserialize(response.result)); @@ -180,7 +180,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate { userActivation: true, }); if (response.type === 'exception') - throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails)); + throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text); if (response.type === 'success') return response.result; throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response)); From 83cd5af5b5d0b8faeb1ea14e15c6ca8b1c59e0e5 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 18 Sep 2025 17:27:20 -0700 Subject: [PATCH 199/329] chore(mcp): test graceful http server shutdown on windows (#37484) --- packages/playwright/src/mcp/sdk/http.ts | 7 +++++++ tests/mcp/http.spec.ts | 25 +++++++++++-------------- tests/mcp/sse.spec.ts | 23 +++++++++-------------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/packages/playwright/src/mcp/sdk/http.ts b/packages/playwright/src/mcp/sdk/http.ts index d9359880a3686..4d941d0b0dfa0 100644 --- a/packages/playwright/src/mcp/sdk/http.ts +++ b/packages/playwright/src/mcp/sdk/http.ts @@ -64,6 +64,13 @@ export async function installHttpTransport(httpServer: http.Server, serverBacken const streamableSessions = new Map(); httpServer.on('request', async (req, res) => { const url = new URL(`http://localhost${req.url}`); + if (url.pathname === '/killkillkill' && req.method === 'GET') { + res.statusCode = 200; + res.end('Killing process'); + // Simulate Ctrl+C in a way that works on Windows too. + process.emit('SIGINT'); + return; + } if (url.pathname.startsWith('/sse')) await handleSSE(serverBackendFactory, req, res, url, sseSessions); else diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index 458b2e6c6e0ef..af43cc4bd339b 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -24,7 +24,7 @@ import { test as baseTest, expect, mcpServerPath } from './fixtures'; import type { Config } from '../../packages/playwright/src/mcp/config'; import { ListRootsRequestSchema } from 'packages/playwright/lib/mcp/sdk/bundle'; -const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string, kill: () => void }> }>({ +const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { let cp: ChildProcess | undefined; const userDataDir = testInfo.outputPath('user-data-dir'); @@ -55,10 +55,7 @@ const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noP resolve(match[1]); })); - return { url: new URL(url), stderr: () => stderr, kill: () => { - cp?.kill('SIGTERM'); - cp = undefined; - } }; + return { url: new URL(url), stderr: () => stderr }; }); cp?.kill('SIGTERM'); }, @@ -249,7 +246,7 @@ test('http transport browser lifecycle (persistent, multiclient)', async ({ serv }); test('http transport shared context', async ({ serverEndpoint, server }) => { - const { url, stderr, kill } = await serverEndpoint({ args: ['--shared-browser-context'] }); + const { url, stderr } = await serverEndpoint({ args: ['--shared-browser-context'] }); // Create first client and navigate const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); @@ -304,15 +301,15 @@ test('http transport shared context', async ({ serverEndpoint, server }) => { expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(0); }).toPass(); - kill(); + // Simulate Ctrl+C in a way that works on Windows too. + await fetch(new URL('/killkillkill', url).href).catch(() => {}); + + await expect(async () => { + const lines = stderr().split('\n'); + // Context should only close when the server shuts down. + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); + }).toPass(); - if (process.platform !== 'win32') { - await expect(async () => { - const lines = stderr().split('\n'); - // Context should only close when the server shuts down. - expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); - }).toPass(); - } }); test('http transport (default)', async ({ serverEndpoint }) => { diff --git a/tests/mcp/sse.spec.ts b/tests/mcp/sse.spec.ts index c214e44157724..c1c8c9cdf54a5 100644 --- a/tests/mcp/sse.spec.ts +++ b/tests/mcp/sse.spec.ts @@ -23,7 +23,7 @@ import { test as baseTest, expect, mcpServerPath } from './fixtures'; import type { Config } from '../../packages/playwright/src/mcp/config'; -const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string, kill: () => void }> }>({ +const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { let cp: ChildProcess | undefined; const userDataDir = testInfo.outputPath('user-data-dir'); @@ -54,10 +54,7 @@ const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noP resolve(match[1]); })); - return { url: new URL(url), stderr: () => stderr, kill: () => { - cp?.kill('SIGTERM'); - cp = undefined; - } }; + return { url: new URL(url), stderr: () => stderr }; }); cp?.kill('SIGTERM'); }, @@ -234,7 +231,7 @@ test('sse transport browser lifecycle (persistent, multiclient)', async ({ serve }); test('sse transport shared context', async ({ serverEndpoint, server }) => { - const { url, stderr, kill } = await serverEndpoint({ args: ['--shared-browser-context'] }); + const { url, stderr } = await serverEndpoint({ args: ['--shared-browser-context'] }); // Create first client and navigate const transport1 = new SSEClientTransport(new URL('/sse', url)); @@ -287,13 +284,11 @@ test('sse transport shared context', async ({ serverEndpoint, server }) => { expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(0); }).toPass(); - kill(); + await fetch(new URL('/killkillkill', url).href).catch(() => {}); - if (process.platform !== 'win32') { - await expect(async () => { - const lines = stderr().split('\n'); - // Context should only close when the server shuts down. - expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); - }).toPass(); - } + await expect(async () => { + const lines = stderr().split('\n'); + // Context should only close when the server shuts down. + expect(lines.filter(line => line.match(/close browser context complete \(persistent\)/)).length).toBe(1); + }).toPass(); }); From 447c88c0db825ec7773f5424fdd247ddbf3e1e22 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 18 Sep 2025 18:39:08 -0700 Subject: [PATCH 200/329] chore: update generator prompt (#37483) --- .../agents/playwright-test-generator.md | 83 ++++-- .../.claude/agents/playwright-test-healer.md | 4 +- .../.claude/agents/playwright-test-planner.md | 37 ++- examples/todomvc/prompt.claudecode.md | 1 + examples/todomvc/specs/basic-operations.md | 254 +++++++++--------- .../tests/create/add-empty-todo.spec.ts | 29 ++ .../tests/create/add-multiple-todos.spec.ts | 47 ++++ .../add-todo-with-only-whitespace.spec.ts | 31 +++ .../add-todo-with-special-characters.spec.ts | 30 +++ .../tests/create/add-valid-todo.spec.ts | 35 +++ .../edit/cancel-edit-with-escape.spec.ts | 46 ++++ .../tests/edit/edit-multiple-todos.spec.ts | 61 +++++ .../todomvc/tests/edit/edit-todo-text.spec.ts | 42 +++ .../edit/edit-todo-to-empty-text.spec.ts | 44 +++ examples/todomvc/tests/fixtures.ts | 12 + examples/todomvc/tests/integration.spec.ts | 6 +- examples/todomvc/tests/seed.spec.ts | 8 +- .../toggle/mark-all-todos-complete.spec.ts | 39 +++ .../mark-multiple-todos-complete.spec.ts | 48 ++++ .../toggle/mark-single-todo-complete.spec.ts | 39 +++ .../toggle-all-todos-incomplete.spec.ts | 37 +++ .../toggle/toggle-todo-incomplete.spec.ts | 46 ++++ packages/playwright/src/agents/generator.md | 93 +++++-- packages/playwright/src/agents/healer.md | 4 +- packages/playwright/src/agents/planner.md | 43 ++- 25 files changed, 902 insertions(+), 217 deletions(-) create mode 100644 examples/todomvc/tests/create/add-empty-todo.spec.ts create mode 100644 examples/todomvc/tests/create/add-multiple-todos.spec.ts create mode 100644 examples/todomvc/tests/create/add-todo-with-only-whitespace.spec.ts create mode 100644 examples/todomvc/tests/create/add-todo-with-special-characters.spec.ts create mode 100644 examples/todomvc/tests/create/add-valid-todo.spec.ts create mode 100644 examples/todomvc/tests/edit/cancel-edit-with-escape.spec.ts create mode 100644 examples/todomvc/tests/edit/edit-multiple-todos.spec.ts create mode 100644 examples/todomvc/tests/edit/edit-todo-text.spec.ts create mode 100644 examples/todomvc/tests/edit/edit-todo-to-empty-text.spec.ts create mode 100644 examples/todomvc/tests/fixtures.ts create mode 100644 examples/todomvc/tests/toggle/mark-all-todos-complete.spec.ts create mode 100644 examples/todomvc/tests/toggle/mark-multiple-todos-complete.spec.ts create mode 100644 examples/todomvc/tests/toggle/mark-single-todo-complete.spec.ts create mode 100644 examples/todomvc/tests/toggle/toggle-all-todos-incomplete.spec.ts create mode 100644 examples/todomvc/tests/toggle/toggle-todo-incomplete.spec.ts diff --git a/examples/todomvc/.claude/agents/playwright-test-generator.md b/examples/todomvc/.claude/agents/playwright-test-generator.md index a22b2d022f0fd..7e94347e73e9d 100644 --- a/examples/todomvc/.claude/agents/playwright-test-generator.md +++ b/examples/todomvc/.claude/agents/playwright-test-generator.md @@ -6,40 +6,75 @@ model: sonnet color: blue --- -You are a Playwright Test Generator, an expert in browser automation and end-to-end testing. Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate application behavior. +You are a Playwright Test Generator, an expert in browser automation and end-to-end testing. +Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate +application behavior. Your process is methodical and thorough: -1. **Scenario Analysis**: Carefully analyze the test scenario provided, identifying all user actions, expected outcomes, and validation points. Break down complex flows into discrete, testable steps. +1. **Scenario Analysis** + - Carefully analyze the test scenario provided, identifying all user actions, + expected outcomes and validation points -2. **Interactive Execution**: - - For each test, start with the `test_setup_page` tool to set up page for the scenario +2. **Interactive Execution** + - For each scenario, start with the `test_setup_page` tool to set up page for the scenario - Use Playwright tools to manually execute each step of the scenario in real-time - Verify that each action works as expected - Identify the correct locators and interaction patterns - Observe actual application behavior and responses - - Catch potential timing issues or dynamic content - Validate that assertions will work correctly -3. **Test Code Generation**: After successfully completing the manual execution, generate clean, maintainable @playwright/test source code that: - - Uses descriptive test names that clearly indicate what is being tested - - Implements proper page object patterns when beneficial - - Includes appropriate waits and assertions - - Handles dynamic content and loading states - - Uses reliable locators (preferring data-testid, role-based, or text-based selectors over fragile CSS selectors) - - Includes proper setup and teardown - - Is self-contained and can run independently - - Use explicit waits rather than arbitrary timeouts - - Never wait for networkidle or use other discouraged or deprecated apis +3. **Test Code Generation** -4. **Quality Assurance**: Ensure each generated test: - - Has clear, descriptive assertions that validate the expected behavior - - Includes proper error handling and meaningful failure messages - - Uses Playwright best practices (page.waitForLoadState, expect.toBeVisible, etc.) - - Is deterministic and not prone to flaky behavior - - Follows consistent naming conventions and code structure + After successfully completing the manual execution, generate clean, maintainable + @playwright/test source code that follows following convention: + + - One file per scenario, one test in a file + - File name must be fs-friendly scenario name + - Test must be placed in a describe matching the top-level test plan item + - Test title must match the scenario name + - Includes a comment with the step text before each step execution + + + For following plan: + + ```markdown file=specs/plan.md + ### 1. Adding New Todos + **Seed:** `tests/seed.spec.ts` + + #### 1.1 Add Valid Todo + **Steps:** + 1. Click in the "What needs to be done?" input field -5. **Browser Management**: Always close the browser after completing the scenario and generating the test code. + #### 1.2 Add Multiple Todos + ... + ``` -Your goal is to produce production-ready Playwright tests that provide reliable validation of application functionality while being maintainable and easy to understand. -Process all scenarios sequentially, do not run in parallel. Save tests in the tests/ folder. \ No newline at end of file + Following file is generated: + + ```ts file=add-valid-todo.spec.ts + // spec: specs/plan.md + // seed: tests/seed.spec.ts + + test.describe('Adding New Todos', () => { + test('Add Valid Todo', async { page } => { + // 1. Click in the "What needs to be done?" input field + await page.click(...); + + ... + }); + }); + ``` + + +4. **Best practices**: + - Each test has clear, descriptive assertions that validate the expected behavior + - Includes proper error handling and meaningful failure messages + - Uses Playwright best practices (page.waitForLoadState, expect.toBeVisible, etc.) + - Do not improvise, do not add directives that were not asked for + - Uses reliable locators (preferring data-testid, role-based, or text-based selectors over fragile CSS selectors) + - Uses local variables for locators that are used multiple times + - Uses explicit waits rather than arbitrary timeouts + - Never waits for networkidle or use other discouraged or deprecated apis + - Is self-contained and can run independently + - Is deterministic and not prone to flaky behavior \ No newline at end of file diff --git a/examples/todomvc/.claude/agents/playwright-test-healer.md b/examples/todomvc/.claude/agents/playwright-test-healer.md index d6500a090b08a..952b083e8ec60 100644 --- a/examples/todomvc/.claude/agents/playwright-test-healer.md +++ b/examples/todomvc/.claude/agents/playwright-test-healer.md @@ -38,6 +38,8 @@ Key principles: - If multiple errors exist, fix them one at a time and retest - Provide clear explanations of what was broken and how you fixed it - You will continue this process until the test runs successfully without any failures or errors. -- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme() so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead of the expected behavior. +- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme() + so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead + of the expected behavior. - Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test. - Never wait for networkidle or use other discouraged or deprecated apis \ No newline at end of file diff --git a/examples/todomvc/.claude/agents/playwright-test-planner.md b/examples/todomvc/.claude/agents/playwright-test-planner.md index aac0ac50f7f69..834421306fac7 100644 --- a/examples/todomvc/.claude/agents/playwright-test-planner.md +++ b/examples/todomvc/.claude/agents/playwright-test-planner.md @@ -6,32 +6,42 @@ model: sonnet color: green --- -You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage planning. +You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test +scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage +planning. You will: -1. **Navigate and Explore**: +1. **Navigate and Explore** - Invoke the `test_setup_page` tool once to set up page before using any other tools - Explore the browser snapshot - Do not take screenshots unless absolutely necessary - Use browser_* tools to navigate and discover interface - Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality -2. **Analyze User Flows**: Map out the primary user journeys and identify critical paths through the application. Consider different user types and their typical behaviors. +2. **Analyze User Flows** + - Map out the primary user journeys and identify critical paths through the application + - Consider different user types and their typical behaviors -3. **Design Comprehensive Scenarios**: Create detailed test scenarios that cover: +3. **Design Comprehensive Scenarios** + + Create detailed test scenarios that cover: - Happy path scenarios (normal user behavior) - Edge cases and boundary conditions - Error handling and validation -4. **Structure Test Plans**: Each scenario must include: +4. **Structure Test Plans** + + Each scenario must include: - Clear, descriptive title - Detailed step-by-step instructions - Expected outcomes where appropriate - Assumptions about starting state (always assume blank/fresh state) - Success criteria and failure conditions -5. **Create Documentation**: Save your test plan as a markdown file in specs/ folder with: +5. **Create Documentation** + + Save your test plan as requested: - Executive summary of the tested page/application - Individual scenarios as separate sections - Each scenario formatted with numbered steps @@ -42,7 +52,8 @@ You will: ## Application Overview -The TodoMVC application is a React-based todo list manager that provides core task management functionality. The application features: +The TodoMVC application is a React-based todo list manager that provides core task management functionality. The +application features: - **Task Management**: Add, edit, complete, and delete individual todos - **Bulk Operations**: Mark all todos as complete/incomplete and clear all completed todos @@ -55,12 +66,13 @@ The TodoMVC application is a React-based todo list manager that provides core ta ### 1. Adding New Todos +**Seed:** `tests/seed.spec.ts` + #### 1.1 Add Valid Todo **Steps:** -1. Use seed test `tests/seed.spec.ts` -2. Click in the "What needs to be done?" input field -3. Type "Buy groceries" -4. Press Enter key +1. Click in the "What needs to be done?" input field +2. Type "Buy groceries" +3. Press Enter key **Expected Results:** - Todo appears in the list with unchecked checkbox @@ -77,4 +89,5 @@ The TodoMVC application is a React-based todo list manager that provides core ta - Include negative testing scenarios - Ensure scenarios are independent and can be run in any order -**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and professional formatting suitable for sharing with development and QA teams. \ No newline at end of file +**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and +professional formatting suitable for sharing with development and QA teams. \ No newline at end of file diff --git a/examples/todomvc/prompt.claudecode.md b/examples/todomvc/prompt.claudecode.md index 0bd66985543a7..21ee6db859ab1 100644 --- a/examples/todomvc/prompt.claudecode.md +++ b/examples/todomvc/prompt.claudecode.md @@ -9,5 +9,6 @@ Test basic functionality of todo app. - For each scenario in `specs/basic-operations.md`, use `playwright-test-generator` subagent to perform the scenario and generate the test source code into `tests/` folder. + Process all scenarios sequentially, do not run in parallel. - Use `playwright-test-healer` subagent to fix the failing tests. diff --git a/examples/todomvc/specs/basic-operations.md b/examples/todomvc/specs/basic-operations.md index 5477258a43d53..0adc624bdd6fe 100644 --- a/examples/todomvc/specs/basic-operations.md +++ b/examples/todomvc/specs/basic-operations.md @@ -15,12 +15,14 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta ### 1. Adding New Todos +**Seed:** `tests/seed.spec.ts` + #### 1.1 Add Valid Todo + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Click in the "What needs to be done?" input field -3. Type "Buy groceries" -4. Press Enter key +1. Click in the "What needs to be done?" input field +2. Type "Buy groceries" +3. Press Enter key **Expected Results:** - Todo appears in the list with unchecked checkbox @@ -29,11 +31,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Todo list controls become visible (Mark all as complete checkbox) #### 1.2 Add Multiple Todos + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add first todo: "Buy groceries" and press Enter -3. Add second todo: "Walk the dog" and press Enter -4. Add third todo: "Call dentist" and press Enter +1. Add first todo: "Buy groceries" and press Enter +2. Add second todo: "Walk the dog" and press Enter +3. Add third todo: "Call dentist" and press Enter **Expected Results:** - All three todos appear in the list in the order added @@ -42,10 +44,10 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Input field remains active and cleared after each addition #### 1.3 Add Todo with Special Characters + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Type "Buy coffee & donuts (2-3 pieces) @$5.99!" in input field -3. Press Enter +1. Type "Buy coffee & donuts (2-3 pieces) @$5.99!" in input field +2. Press Enter **Expected Results:** - Todo appears exactly as typed with all special characters preserved @@ -53,10 +55,10 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - No encoding or display issues with special characters #### 1.4 Add Empty Todo (Negative Test) + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Click in input field but don't type anything -3. Press Enter +1. Click in input field but don't type anything +2. Press Enter **Expected Results:** - No todo is added to the list @@ -65,10 +67,10 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Input field remains focused and empty #### 1.5 Add Todo with Only Whitespace (Negative Test) + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Type only spaces " " in input field -3. Press Enter +1. Type only spaces " " in input field +2. Press Enter **Expected Results:** - No todo is added to the list @@ -79,10 +81,10 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta ### 2. Marking Todos Complete/Incomplete #### 2.1 Mark Single Todo Complete + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todo "Buy groceries" -3. Click the checkbox next to "Buy groceries" +1. Add todo "Buy groceries" +2. Click the checkbox next to "Buy groceries" **Expected Results:** - Checkbox becomes checked @@ -92,11 +94,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Delete button (×) becomes visible on hover #### 2.2 Mark Multiple Todos Complete + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -3. Click checkbox for "Buy groceries" -4. Click checkbox for "Call dentist" +1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +2. Click checkbox for "Buy groceries" +3. Click checkbox for "Call dentist" **Expected Results:** - Two todos show as completed @@ -105,11 +107,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Only "Walk the dog" remains unchecked #### 2.3 Toggle Todo Back to Incomplete + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todo "Buy groceries" -3. Click checkbox to mark complete -4. Click checkbox again to mark incomplete +1. Add todo "Buy groceries" +2. Click checkbox to mark complete +3. Click checkbox again to mark incomplete **Expected Results:** - Checkbox becomes unchecked @@ -118,10 +120,10 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - "Clear completed" button disappears if no other completed todos exist #### 2.4 Mark All Todos Complete + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -3. Click the "Mark all as complete" checkbox +1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +2. Click the "Mark all as complete" checkbox **Expected Results:** - All todo checkboxes become checked @@ -130,11 +132,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - "Mark all as complete" checkbox shows as checked #### 2.5 Toggle All Todos Back to Incomplete + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog" -3. Click "Mark all as complete" checkbox -4. Click "Mark all as complete" checkbox again +1. Add todos: "Buy groceries", "Walk the dog" +2. Click "Mark all as complete" checkbox +3. Click "Mark all as complete" checkbox again **Expected Results:** - All todo checkboxes become unchecked @@ -145,12 +147,12 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta ### 3. Editing Todos #### 3.1 Edit Todo Text + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todo "Buy groceries" -3. Double-click on the todo text "Buy groceries" -4. Clear text and type "Buy organic groceries" -5. Press Enter +1. Add todo "Buy groceries" +2. Double-click on the todo text "Buy groceries" +3. Clear text and type "Buy organic groceries" +4. Press Enter **Expected Results:** - Todo enters edit mode with text selected @@ -159,12 +161,12 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Counter remains "1 item left" #### 3.2 Cancel Edit with Escape + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todo "Buy groceries" -3. Double-click on the todo text -4. Change text to "Buy organic groceries" -5. Press Escape key +1. Add todo "Buy groceries" +2. Double-click on the todo text +3. Change text to "Buy organic groceries" +4. Press Escape key **Expected Results:** - Todo exits edit mode @@ -173,12 +175,12 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Todo remains in its original state #### 3.3 Edit Todo to Empty Text (Negative Test) + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todo "Buy groceries" -3. Double-click on the todo text -4. Clear all text -5. Press Enter +1. Add todo "Buy groceries" +2. Double-click on the todo text +3. Clear all text +4. Press Enter **Expected Results:** - Todo should be deleted/removed from list @@ -186,11 +188,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - List becomes empty if this was the only todo #### 3.4 Edit Multiple Todos + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog" -3. Double-click "Buy groceries", change to "Buy organic groceries", press Enter -4. Double-click "Walk the dog", change to "Walk the cat", press Enter +1. Add todos: "Buy groceries", "Walk the dog" +2. Double-click "Buy groceries", change to "Buy organic groceries", press Enter +3. Double-click "Walk the dog", change to "Walk the cat", press Enter **Expected Results:** - Both todos are updated with new text @@ -200,11 +202,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta ### 4. Deleting Todos #### 4.1 Delete Single Todo + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todo "Buy groceries" -3. Hover over the todo item -4. Click the delete button (×) +1. Add todo "Buy groceries" +2. Hover over the todo item +3. Click the delete button (×) **Expected Results:** - Todo is removed from the list @@ -213,11 +215,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Todo controls (filters, mark all) disappear #### 4.2 Delete Multiple Todos + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -3. Hover over "Walk the dog" and click delete (×) -4. Hover over "Call dentist" and click delete (×) +1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +2. Hover over "Walk the dog" and click delete (×) +3. Hover over "Call dentist" and click delete (×) **Expected Results:** - Only "Buy groceries" remains in the list @@ -225,11 +227,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - List controls remain visible #### 4.3 Delete Completed Todo + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog" -3. Mark "Buy groceries" as complete -4. Hover over "Buy groceries" and click delete (×) +1. Add todos: "Buy groceries", "Walk the dog" +2. Mark "Buy groceries" as complete +3. Hover over "Buy groceries" and click delete (×) **Expected Results:** - "Buy groceries" is removed from list @@ -238,11 +240,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - "Clear completed" button disappears #### 4.4 Clear All Completed Todos + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -3. Mark "Buy groceries" and "Call dentist" as complete -4. Click "Clear completed" button +1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +2. Mark "Buy groceries" and "Call dentist" as complete +3. Click "Clear completed" button **Expected Results:** - Both completed todos are removed @@ -253,11 +255,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta ### 5. Filtering Todos #### 5.1 Filter by All + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -3. Mark "Buy groceries" as complete -4. Click "All" filter link +1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +2. Mark "Buy groceries" as complete +3. Click "All" filter link **Expected Results:** - All todos are visible (both completed and active) @@ -266,11 +268,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Counter shows "2 items left" #### 5.2 Filter by Active + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -3. Mark "Buy groceries" as complete -4. Click "Active" filter link +1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +2. Mark "Buy groceries" as complete +3. Click "Active" filter link **Expected Results:** - Only incomplete todos are visible ("Walk the dog", "Call dentist") @@ -280,11 +282,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Counter shows "2 items left" #### 5.3 Filter by Completed + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -3. Mark "Buy groceries" and "Walk the dog" as complete -4. Click "Completed" filter link +1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +2. Mark "Buy groceries" and "Walk the dog" as complete +3. Click "Completed" filter link **Expected Results:** - Only completed todos are visible ("Buy groceries", "Walk the dog") @@ -294,13 +296,13 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Counter still shows "1 item left" (maintains global count) #### 5.4 Navigate Between Filters + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog" -3. Mark "Buy groceries" as complete -4. Click "Active" filter -5. Click "Completed" filter -6. Click "All" filter +1. Add todos: "Buy groceries", "Walk the dog" +2. Mark "Buy groceries" as complete +3. Click "Active" filter +4. Click "Completed" filter +5. Click "All" filter **Expected Results:** - Each filter shows appropriate todos @@ -312,29 +314,29 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta ### 6. Counter and Status Display #### 6.1 Counter with Single Item + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add one todo "Buy groceries" +1. Add one todo "Buy groceries" **Expected Results:** - Counter displays "1 item left" (singular form) - Counter updates immediately when todo is added #### 6.2 Counter with Multiple Items + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog" +1. Add todos: "Buy groceries", "Walk the dog" **Expected Results:** - Counter displays "2 items left" (plural form) - Counter shows correct count #### 6.3 Counter Updates with Completion + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -3. Mark "Buy groceries" as complete -4. Mark "Walk the dog" as complete +1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +2. Mark "Buy groceries" as complete +3. Mark "Walk the dog" as complete **Expected Results:** - Counter starts at "3 items left" @@ -343,10 +345,10 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Counter updates immediately with each change #### 6.4 Counter with All Items Complete + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todo "Buy groceries" -3. Mark it as complete +1. Add todo "Buy groceries" +2. Mark it as complete **Expected Results:** - Counter shows "0 items left" @@ -356,10 +358,10 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta ### 7. Bulk Operations #### 7.1 Mark All Complete When None Completed + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -3. Click "Mark all as complete" checkbox +1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +2. Click "Mark all as complete" checkbox **Expected Results:** - All todos become checked/completed @@ -368,11 +370,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - "Mark all as complete" checkbox shows as checked #### 7.2 Mark All Incomplete When All Completed + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog" -3. Mark both todos as complete individually -4. Click "Mark all as complete" checkbox +1. Add todos: "Buy groceries", "Walk the dog" +2. Mark both todos as complete individually +3. Click "Mark all as complete" checkbox **Expected Results:** - All todos become unchecked/incomplete @@ -381,11 +383,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - "Mark all as complete" checkbox shows as unchecked #### 7.3 Mark All with Mixed State + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -3. Mark "Buy groceries" as complete -4. Click "Mark all as complete" checkbox +1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" +2. Mark "Buy groceries" as complete +3. Click "Mark all as complete" checkbox **Expected Results:** - All todos become completed (including the already completed one) @@ -395,10 +397,10 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta ### 8. Edge Cases and Error Handling #### 8.1 Very Long Todo Text + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Type a very long todo text (200+ characters) -3. Press Enter +1. Type a very long todo text (200+ characters) +2. Press Enter **Expected Results:** - Todo is added successfully @@ -407,11 +409,11 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - Edit functionality works with long text #### 8.2 Rapid Sequential Actions + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Quickly add multiple todos by typing and pressing Enter rapidly -3. Quickly toggle completion states -4. Rapidly switch between filters +1. Quickly add multiple todos by typing and pressing Enter rapidly +2. Quickly toggle completion states +3. Rapidly switch between filters **Expected Results:** - All actions are processed correctly @@ -422,8 +424,8 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta #### 8.3 Direct URL Navigation **Steps:** 1. Navigate directly to `{base_url}#/active` -2. Navigate directly to `{base_url}#/completed` -3. Navigate directly to `{base_url}#/` +1. Navigate directly to `{base_url}#/completed` +2. Navigate directly to `{base_url}#/` **Expected Results:** - Page loads correctly for each URL @@ -432,13 +434,13 @@ The TodoMVC application is a React-based todo list manager that demonstrates sta - No JavaScript errors occur #### 8.4 Todo Operations Across Filters + **Steps:** -1. Use seed test `tests/seed.spec.ts` to initialize page -2. Add todos: "Buy groceries", "Walk the dog" -3. Navigate to "Active" filter -4. Mark "Buy groceries" as complete -5. Navigate to "Completed" filter -6. Delete "Buy groceries" +1. Add todos: "Buy groceries", "Walk the dog" +2. Navigate to "Active" filter +3. Mark "Buy groceries" as complete +4. Navigate to "Completed" filter +5. Delete "Buy groceries" **Expected Results:** - Operations work correctly across filter views @@ -463,4 +465,4 @@ All test scenarios should pass without: - Non-functional UI elements - Accessibility violations -The application should maintain consistent behavior across all supported browsers and provide a smooth, intuitive user experience for basic todo management operations. \ No newline at end of file +The application should maintain consistent behavior across all supported browsers and provide a smooth, intuitive user experience for basic todo management operations. diff --git a/examples/todomvc/tests/create/add-empty-todo.spec.ts b/examples/todomvc/tests/create/add-empty-todo.spec.ts new file mode 100644 index 0000000000000..1ed79f985bcca --- /dev/null +++ b/examples/todomvc/tests/create/add-empty-todo.spec.ts @@ -0,0 +1,29 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Adding New Todos', () => { + test('Add Empty Todo (Negative Test)', async ({ page }) => { + // 1. Click in input field but don't type anything + const todoInput = page.getByRole('textbox', { name: 'What needs to be done?' }); + await todoInput.click(); + + // 2. Press Enter + await todoInput.press('Enter'); + + // Expected Results: + // - No todo is added to the list + await expect(page.locator('.todo-list li')).toHaveCount(0); + + // - List remains empty + await expect(page.locator('.todo-list')).not.toBeVisible(); + + // - No counter appears + await expect(page.getByText(/\d+ items? left/)).not.toBeVisible(); + + // - Input field remains focused and empty + await expect(todoInput).toBeFocused(); + await expect(todoInput).toHaveValue(''); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/create/add-multiple-todos.spec.ts b/examples/todomvc/tests/create/add-multiple-todos.spec.ts new file mode 100644 index 0000000000000..a698ca5546761 --- /dev/null +++ b/examples/todomvc/tests/create/add-multiple-todos.spec.ts @@ -0,0 +1,47 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Adding New Todos', () => { + test('Add Multiple Todos', async ({ page }) => { + const todoInput = page.getByRole('textbox', { name: 'What needs to be done?' }); + + // 1. Add first todo: "Buy groceries" and press Enter + await todoInput.fill('Buy groceries'); + await todoInput.press('Enter'); + + // 2. Add second todo: "Walk the dog" and press Enter + await todoInput.fill('Walk the dog'); + await todoInput.press('Enter'); + + // 3. Add third todo: "Call dentist" and press Enter + await todoInput.fill('Call dentist'); + await todoInput.press('Enter'); + + // Expected Results: + // - All three todos appear in the list in the order added + const todoItems = page.getByTestId('todo-item'); + await expect(todoItems).toHaveCount(3); + + await expect(todoItems.nth(0)).toContainText('Buy groceries'); + await expect(todoItems.nth(1)).toContainText('Walk the dog'); + await expect(todoItems.nth(2)).toContainText('Call dentist'); + + // - Counter shows "3 items left" + await expect(page.getByText('3 items left')).toBeVisible(); + + // - Each todo has its own unchecked checkbox + const todoCheckboxes = page.getByRole('checkbox', { name: 'Toggle Todo' }); + await expect(todoCheckboxes).toHaveCount(3); + + for (let i = 0; i < 3; i++) { + await expect(todoCheckboxes.nth(i)).toBeVisible(); + await expect(todoCheckboxes.nth(i)).not.toBeChecked(); + } + + // - Input field remains active and cleared after each addition + await expect(todoInput).toHaveValue(''); + await expect(todoInput).toBeFocused(); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/create/add-todo-with-only-whitespace.spec.ts b/examples/todomvc/tests/create/add-todo-with-only-whitespace.spec.ts new file mode 100644 index 0000000000000..6a7b70d89d802 --- /dev/null +++ b/examples/todomvc/tests/create/add-todo-with-only-whitespace.spec.ts @@ -0,0 +1,31 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Adding New Todos', () => { + test('Add Todo with Only Whitespace (Negative Test)', async ({ page }) => { + // 1. Type only spaces " " in input field + const todoInput = page.getByRole('textbox', { name: 'What needs to be done?' }); + await todoInput.fill(' '); + + // 2. Press Enter + await todoInput.press('Enter'); + + // Expected Results: + // - No todo is added to the list + await expect(page.locator('.todo-list li')).toHaveCount(0); + + // - List remains empty + await expect(page.locator('.todo-list')).not.toBeVisible(); + + // - Input field retains the whitespace (actual behavior differs from spec) + await expect(todoInput).toHaveValue(' '); + + // - No counter appears + await expect(page.getByText(/\d+ items? left/)).not.toBeVisible(); + + // - Input field remains focused + await expect(todoInput).toBeFocused(); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/create/add-todo-with-special-characters.spec.ts b/examples/todomvc/tests/create/add-todo-with-special-characters.spec.ts new file mode 100644 index 0000000000000..3b6bcaf427552 --- /dev/null +++ b/examples/todomvc/tests/create/add-todo-with-special-characters.spec.ts @@ -0,0 +1,30 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Adding New Todos', () => { + test('Add Todo with Special Characters', async ({ page }) => { + // 1. Type "Buy coffee & donuts (2-3 pieces) @$5.99!" in input field + const todoInput = page.getByRole('textbox', { name: 'What needs to be done?' }); + await todoInput.fill('Buy coffee & donuts (2-3 pieces) @$5.99!'); + + // 2. Press Enter + await todoInput.press('Enter'); + + // Expected Results: + // - Todo appears exactly as typed with all special characters preserved + await expect(page.getByText('Buy coffee & donuts (2-3 pieces) @$5.99!')).toBeVisible(); + + // - Counter shows "1 item left" + await expect(page.getByText('1 item left')).toBeVisible(); + + // - No encoding or display issues with special characters + const todoCheckbox = page.getByRole('checkbox', { name: 'Toggle Todo' }); + await expect(todoCheckbox).toBeVisible(); + await expect(todoCheckbox).not.toBeChecked(); + + // Verify input field is cleared + await expect(todoInput).toHaveValue(''); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/create/add-valid-todo.spec.ts b/examples/todomvc/tests/create/add-valid-todo.spec.ts new file mode 100644 index 0000000000000..0353dba82e384 --- /dev/null +++ b/examples/todomvc/tests/create/add-valid-todo.spec.ts @@ -0,0 +1,35 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Adding New Todos', () => { + test('Add Valid Todo', async ({ page }) => { + // 1. Click in the "What needs to be done?" input field + const todoInput = page.getByRole('textbox', { name: 'What needs to be done?' }); + await todoInput.click(); + + // 2. Type "Buy groceries" + await todoInput.fill('Buy groceries'); + + // 3. Press Enter key + await todoInput.press('Enter'); + + // Expected Results: + // - Todo appears in the list with unchecked checkbox + await expect(page.getByText('Buy groceries')).toBeVisible(); + const todoCheckbox = page.getByRole('checkbox', { name: 'Toggle Todo' }); + await expect(todoCheckbox).toBeVisible(); + await expect(todoCheckbox).not.toBeChecked(); + + // - Counter shows "1 item left" + await expect(page.getByText('1 item left')).toBeVisible(); + + // - Input field is cleared and ready for next entry + await expect(todoInput).toHaveValue(''); + await expect(todoInput).toBeFocused(); + + // - Todo list controls become visible (Mark all as complete checkbox) + await expect(page.getByRole('checkbox', { name: '❯Mark all as complete' })).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/edit/cancel-edit-with-escape.spec.ts b/examples/todomvc/tests/edit/cancel-edit-with-escape.spec.ts new file mode 100644 index 0000000000000..9fc5ad164b3f4 --- /dev/null +++ b/examples/todomvc/tests/edit/cancel-edit-with-escape.spec.ts @@ -0,0 +1,46 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Editing Todos', () => { + test('Cancel Edit with Escape', async ({ page }) => { + // 1. Add todo "Buy groceries" + await page.getByRole('textbox', { name: 'What needs to be done?' }).click(); + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy groceries'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); + + // Verify todo was added + await expect(page.getByTestId('todo-title')).toHaveText('Buy groceries'); + await expect(page.locator('.todo-count')).toHaveText('1 item left'); + + // 2. Double-click on the todo text + await page.getByTestId('todo-title').dblclick(); + + // Verify todo enters edit mode with text selected + const editInput = page.getByRole('textbox', { name: 'Edit' }); + await expect(editInput).toBeVisible(); + await expect(editInput).toHaveValue('Buy groceries'); + await expect(editInput).toBeFocused(); + + // 3. Change text to "Buy organic groceries" + await editInput.fill('Buy organic groceries'); + + // Verify text was changed in edit input + await expect(editInput).toHaveValue('Buy organic groceries'); + + // 4. Press Escape key + await page.keyboard.press('Escape'); + + // Expected results: + // - Todo exits edit mode + await expect(editInput).not.toBeVisible(); + + // - Text reverts to original "Buy groceries" + await expect(page.getByTestId('todo-title')).toHaveText('Buy groceries'); + + // - No changes are saved (verified by text reversion above) + // - Todo remains in its original state + await expect(page.locator('.todo-count')).toHaveText('1 item left'); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/edit/edit-multiple-todos.spec.ts b/examples/todomvc/tests/edit/edit-multiple-todos.spec.ts new file mode 100644 index 0000000000000..643921ed7d730 --- /dev/null +++ b/examples/todomvc/tests/edit/edit-multiple-todos.spec.ts @@ -0,0 +1,61 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Editing Todos', () => { + test('Edit Multiple Todos', async ({ page }) => { + // 1. Add todos: "Buy groceries", "Walk the dog" + await page.getByRole('textbox', { name: 'What needs to be done?' }).click(); + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy groceries'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); + + await page.getByRole('textbox', { name: 'What needs to be done?' }).click(); + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Walk the dog'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); + + // Verify both todos were added + await expect(page.getByText('Buy groceries')).toBeVisible(); + await expect(page.getByText('Walk the dog')).toBeVisible(); + await expect(page.locator('.todo-count')).toHaveText('2 items left'); + + // 2. Double-click "Buy groceries", change to "Buy organic groceries", press Enter + await page.getByText('Buy groceries').dblclick(); + + const editInput = page.getByRole('textbox', { name: 'Edit' }); + await expect(editInput).toBeVisible(); + await expect(editInput).toHaveValue('Buy groceries'); + + await editInput.fill('Buy organic groceries'); + await page.keyboard.press('Enter'); + + // 3. Double-click "Walk the dog", change to "Walk the cat", press Enter + await page.getByText('Walk the dog').dblclick(); + + const editInput2 = page.getByRole('textbox', { name: 'Edit' }); + await expect(editInput2).toBeVisible(); + await expect(editInput2).toHaveValue('Walk the dog'); + + await editInput2.fill('Walk the cat'); + await page.keyboard.press('Enter'); + + // Expected results: + // - Both todos are updated with new text + await expect(page.getByText('Buy organic groceries')).toBeVisible(); + await expect(page.getByText('Walk the cat')).toBeVisible(); + + // - Counter remains "2 items left" + await expect(page.locator('.todo-count')).toHaveText('2 items left'); + + // - Both todos maintain their completion state (uncompleted) + const todo1Checkbox = page.getByText('Buy organic groceries').locator('..').getByRole('checkbox', { name: 'Toggle Todo' }); + const todo2Checkbox = page.getByText('Walk the cat').locator('..').getByRole('checkbox', { name: 'Toggle Todo' }); + + await expect(todo1Checkbox).not.toBeChecked(); + await expect(todo2Checkbox).not.toBeChecked(); + + // Verify edit inputs are no longer visible + await expect(editInput).not.toBeVisible(); + await expect(editInput2).not.toBeVisible(); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/edit/edit-todo-text.spec.ts b/examples/todomvc/tests/edit/edit-todo-text.spec.ts new file mode 100644 index 0000000000000..f0629ba745a2a --- /dev/null +++ b/examples/todomvc/tests/edit/edit-todo-text.spec.ts @@ -0,0 +1,42 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Editing Todos', () => { + test('Edit Todo Text', async ({ page }) => { + // 1. Add todo "Buy groceries" + await page.getByRole('textbox', { name: 'What needs to be done?' }).click(); + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy groceries'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); + + // Verify todo was added + await expect(page.getByTestId('todo-title')).toHaveText('Buy groceries'); + await expect(page.locator('.todo-count')).toHaveText('1 item left'); + + // 2. Double-click on the todo text "Buy groceries" + await page.getByTestId('todo-title').dblclick(); + + // Verify todo enters edit mode with text selected + const editInput = page.getByRole('textbox', { name: 'Edit' }); + await expect(editInput).toBeVisible(); + await expect(editInput).toHaveValue('Buy groceries'); + await expect(editInput).toBeFocused(); + + // 3. Clear text and type "Buy organic groceries" + await editInput.fill('Buy organic groceries'); + + // 4. Press Enter + await page.keyboard.press('Enter'); + + // Expected results: + // - Text changes to "Buy organic groceries" + await expect(page.getByTestId('todo-title')).toHaveText('Buy organic groceries'); + + // - Todo exits edit mode + await expect(editInput).not.toBeVisible(); + + // - Counter remains "1 item left" + await expect(page.locator('.todo-count')).toHaveText('1 item left'); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/edit/edit-todo-to-empty-text.spec.ts b/examples/todomvc/tests/edit/edit-todo-to-empty-text.spec.ts new file mode 100644 index 0000000000000..961e3e39216ac --- /dev/null +++ b/examples/todomvc/tests/edit/edit-todo-to-empty-text.spec.ts @@ -0,0 +1,44 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Editing Todos', () => { + test('Edit Todo to Empty Text (Negative Test)', async ({ page }) => { + // 1. Add todo "Buy groceries" + await page.getByRole('textbox', { name: 'What needs to be done?' }).click(); + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy groceries'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); + + // Verify todo was added + await expect(page.getByTestId('todo-title')).toHaveText('Buy groceries'); + await expect(page.locator('.todo-count')).toHaveText('1 item left'); + + // 2. Double-click on the todo text + await page.getByTestId('todo-title').dblclick(); + + // Verify todo enters edit mode + const editInput = page.getByRole('textbox', { name: 'Edit' }); + await expect(editInput).toBeVisible(); + await expect(editInput).toHaveValue('Buy groceries'); + + // 3. Clear all text + await editInput.fill(''); + + // 4. Press Enter + await page.keyboard.press('Enter'); + + // Expected results: + // - Todo should be deleted/removed from list + await expect(page.getByTestId('todo-title')).not.toBeVisible(); + + // - Counter decrements appropriately (disappears when no todos) + await expect(page.locator('.todo-count')).not.toBeVisible(); + + // - List becomes empty if this was the only todo + await expect(page.locator('.todo-list')).not.toBeVisible(); + + // - Only the main input should remain visible + await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/fixtures.ts b/examples/todomvc/tests/fixtures.ts new file mode 100644 index 0000000000000..c58c0055c58a0 --- /dev/null +++ b/examples/todomvc/tests/fixtures.ts @@ -0,0 +1,12 @@ +/* eslint-disable notice/notice */ + +import { test as baseTest } from '@playwright/test'; + +export { expect } from '@playwright/test'; + +export const test = baseTest.extend({ + page: async ({ page }, use) => { + await page.goto('https://demo.playwright.dev/todomvc'); + await use(page); + }, +}); diff --git a/examples/todomvc/tests/integration.spec.ts b/examples/todomvc/tests/integration.spec.ts index 007896bb2cd5e..f41b0a2d581b5 100644 --- a/examples/todomvc/tests/integration.spec.ts +++ b/examples/todomvc/tests/integration.spec.ts @@ -1,14 +1,10 @@ /* eslint-disable notice/notice */ -import { test, expect } from '@playwright/test'; +import { test, expect } from './fixtures'; import type { Page } from '@playwright/test'; test.describe.configure({ mode: 'parallel' }); -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - const TODO_ITEMS = [ 'buy some cheese', 'feed the cat', diff --git a/examples/todomvc/tests/seed.spec.ts b/examples/todomvc/tests/seed.spec.ts index 4f685f0a3e901..43fd45c83a8a7 100644 --- a/examples/todomvc/tests/seed.spec.ts +++ b/examples/todomvc/tests/seed.spec.ts @@ -1,12 +1,6 @@ /* eslint-disable notice/notice */ -import { test, expect } from '@playwright/test'; - -test.describe.configure({ mode: 'parallel' }); - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); +import { test, expect } from './fixtures'; test('seed', async ({ page }) => { // This test tells agents how to start recording the test diff --git a/examples/todomvc/tests/toggle/mark-all-todos-complete.spec.ts b/examples/todomvc/tests/toggle/mark-all-todos-complete.spec.ts new file mode 100644 index 0000000000000..3bd89cb4df8c8 --- /dev/null +++ b/examples/todomvc/tests/toggle/mark-all-todos-complete.spec.ts @@ -0,0 +1,39 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Marking Todos Complete/Incomplete', () => { + test('Mark All Todos Complete', async ({ page }) => { + // 1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" + const newTodoInput = page.getByRole('textbox', { name: 'What needs to be done?' }); + + await newTodoInput.fill('Buy groceries'); + await newTodoInput.press('Enter'); + + await newTodoInput.fill('Walk the dog'); + await newTodoInput.press('Enter'); + + await newTodoInput.fill('Call dentist'); + await newTodoInput.press('Enter'); + + // 2. Click the "Mark all as complete" checkbox + await page.getByRole('checkbox', { name: '❯Mark all as complete' }).click(); + + // Expected results to verify: + // - All todo checkboxes become checked + const todoCheckboxes = page.getByRole('checkbox', { name: 'Toggle Todo' }); + await expect(todoCheckboxes.nth(0)).toBeChecked(); + await expect(todoCheckboxes.nth(1)).toBeChecked(); + await expect(todoCheckboxes.nth(2)).toBeChecked(); + + // - Counter shows "0 items left" + await expect(page.getByText('0 items left')).toBeVisible(); + + // - "Clear completed" button appears + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + + // - "Mark all as complete" checkbox shows as checked + await expect(page.getByRole('checkbox', { name: '❯Mark all as complete' })).toBeChecked(); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/toggle/mark-multiple-todos-complete.spec.ts b/examples/todomvc/tests/toggle/mark-multiple-todos-complete.spec.ts new file mode 100644 index 0000000000000..4a6ed1a2daf84 --- /dev/null +++ b/examples/todomvc/tests/toggle/mark-multiple-todos-complete.spec.ts @@ -0,0 +1,48 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Marking Todos Complete/Incomplete', () => { + test('Mark Multiple Todos Complete', async ({ page }) => { + const todoInput = page.getByRole('textbox', { name: 'What needs to be done?' }); + + // 1. Add todos: "Buy groceries", "Walk the dog", "Call dentist" + await todoInput.click(); + await todoInput.fill('Buy groceries'); + await todoInput.press('Enter'); + + await todoInput.fill('Walk the dog'); + await todoInput.press('Enter'); + + await todoInput.fill('Call dentist'); + await todoInput.press('Enter'); + + // Verify all todos are added + await expect(page.getByText('3 items left')).toBeVisible(); + + // 2. Click checkbox for "Buy groceries" + await page.getByRole('listitem').filter({ hasText: 'Buy groceries' }).getByLabel('Toggle Todo').click(); + + // 3. Click checkbox for "Call dentist" + await page.getByRole('listitem').filter({ hasText: 'Call dentist' }).getByLabel('Toggle Todo').click(); + + // Expected Results: + // - Two todos show as completed + const buyGroceriesCheckbox = page.getByRole('listitem').filter({ hasText: 'Buy groceries' }).getByLabel('Toggle Todo'); + const callDentistCheckbox = page.getByRole('listitem').filter({ hasText: 'Call dentist' }).getByLabel('Toggle Todo'); + const walkDogCheckbox = page.getByRole('listitem').filter({ hasText: 'Walk the dog' }).getByLabel('Toggle Todo'); + + await expect(buyGroceriesCheckbox).toBeChecked(); + await expect(callDentistCheckbox).toBeChecked(); + + // - Counter shows "1 item left" (for "Walk the dog") + await expect(page.getByText('1 item left')).toBeVisible(); + + // - "Clear completed" button appears + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + + // - Only "Walk the dog" remains unchecked + await expect(walkDogCheckbox).not.toBeChecked(); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/toggle/mark-single-todo-complete.spec.ts b/examples/todomvc/tests/toggle/mark-single-todo-complete.spec.ts new file mode 100644 index 0000000000000..b76a25f412207 --- /dev/null +++ b/examples/todomvc/tests/toggle/mark-single-todo-complete.spec.ts @@ -0,0 +1,39 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Marking Todos Complete/Incomplete', () => { + test('Mark Single Todo Complete', async ({ page }) => { + // 1. Add todo "Buy groceries" + const todoInput = page.getByRole('textbox', { name: 'What needs to be done?' }); + await todoInput.click(); + await todoInput.fill('Buy groceries'); + await todoInput.press('Enter'); + + // Verify todo was added successfully + await expect(page.getByText('Buy groceries')).toBeVisible(); + await expect(page.getByText('1 item left')).toBeVisible(); + + // 2. Click the checkbox next to "Buy groceries" + const todoCheckbox = page.getByRole('checkbox', { name: 'Toggle Todo' }); + await todoCheckbox.click(); + + // Expected Results: + // - Checkbox becomes checked + await expect(todoCheckbox).toBeChecked(); + + // - Todo text may show strikethrough or completed styling (verified by checking the todo is still visible) + await expect(page.getByText('Buy groceries')).toBeVisible(); + + // - Counter shows "0 items left" + await expect(page.getByText('0 items left')).toBeVisible(); + + // - "Clear completed" button appears + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + + // - Delete button (×) becomes visible on hover + await page.getByText('Buy groceries').hover(); + await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/toggle/toggle-all-todos-incomplete.spec.ts b/examples/todomvc/tests/toggle/toggle-all-todos-incomplete.spec.ts new file mode 100644 index 0000000000000..11ec10d2276a7 --- /dev/null +++ b/examples/todomvc/tests/toggle/toggle-all-todos-incomplete.spec.ts @@ -0,0 +1,37 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Marking Todos Complete/Incomplete', () => { + test('Toggle All Todos Back to Incomplete', async ({ page }) => { + // 1. Add todos: "Buy groceries", "Walk the dog" + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy groceries'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); + + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Walk the dog'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); + + // 2. Click "Mark all as complete" checkbox + await page.getByRole('checkbox', { name: '❯Mark all as complete' }).click(); + + // 3. Click "Mark all as complete" checkbox again + await page.getByRole('checkbox', { name: '❯Mark all as complete' }).click(); + + // Verify: All todo checkboxes become unchecked + const buyGroceriesCheckbox = page.getByRole('listitem').filter({ hasText: 'Buy groceries' }).getByRole('checkbox', { name: 'Toggle Todo' }); + const walkDogCheckbox = page.getByRole('listitem').filter({ hasText: 'Walk the dog' }).getByRole('checkbox', { name: 'Toggle Todo' }); + + await expect(buyGroceriesCheckbox).not.toBeChecked(); + await expect(walkDogCheckbox).not.toBeChecked(); + + // Verify: Counter shows "2 items left" + await expect(page.locator('text=2 items left')).toBeVisible(); + + // Verify: "Clear completed" button disappears + await expect(page.getByRole('button', { name: 'Clear completed' })).not.toBeVisible(); + + // Verify: "Mark all as complete" checkbox shows as unchecked + await expect(page.getByRole('checkbox', { name: '❯Mark all as complete' })).not.toBeChecked(); + }); +}); \ No newline at end of file diff --git a/examples/todomvc/tests/toggle/toggle-todo-incomplete.spec.ts b/examples/todomvc/tests/toggle/toggle-todo-incomplete.spec.ts new file mode 100644 index 0000000000000..4f775fed381cf --- /dev/null +++ b/examples/todomvc/tests/toggle/toggle-todo-incomplete.spec.ts @@ -0,0 +1,46 @@ +// spec: specs/basic-operations.md +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Marking Todos Complete/Incomplete', () => { + test('Toggle Todo Back to Incomplete', async ({ page }) => { + // 1. Add todo "Buy groceries" + const todoInput = page.getByRole('textbox', { name: 'What needs to be done?' }); + await todoInput.click(); + await todoInput.fill('Buy groceries'); + await todoInput.press('Enter'); + + // Verify todo was added + const todoItem = page.getByText('Buy groceries'); + await expect(todoItem).toBeVisible(); + await expect(page.getByText('1 item left')).toBeVisible(); + + // 2. Click checkbox to mark complete + const todoCheckbox = page.getByRole('checkbox', { name: 'Toggle Todo' }); + await todoCheckbox.click(); + + // Verify todo is marked complete + await expect(todoCheckbox).toBeChecked(); + await expect(page.getByText('0 items left')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + + // 3. Click checkbox again to mark incomplete + await todoCheckbox.click(); + + // Expected results to verify: + // - Checkbox becomes unchecked + await expect(todoCheckbox).not.toBeChecked(); + + // - Completed styling is removed (checkbox is unchecked indicates this) + // - Counter shows "1 item left" + await expect(page.getByText('1 item left')).toBeVisible(); + + // - "Clear completed" button disappears if no other completed todos exist + await expect(page.getByRole('button', { name: 'Clear completed' })).not.toBeVisible(); + + // Additional verification that the todo is still present and functional + await expect(todoItem).toBeVisible(); + await expect(page.getByRole('checkbox', { name: '❯Mark all as complete' })).not.toBeChecked(); + }); +}); \ No newline at end of file diff --git a/packages/playwright/src/agents/generator.md b/packages/playwright/src/agents/generator.md index f5abc27db5cb3..3479c03a322e1 100644 --- a/packages/playwright/src/agents/generator.md +++ b/packages/playwright/src/agents/generator.md @@ -27,57 +27,96 @@ tools: - playwright-test/test_setup_page --- -You are a Playwright Test Generator, an expert in browser automation and end-to-end testing. Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate application behavior. +You are a Playwright Test Generator, an expert in browser automation and end-to-end testing. +Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate +application behavior. Your process is methodical and thorough: -1. **Scenario Analysis**: Carefully analyze the test scenario provided, identifying all user actions, expected outcomes, and validation points. Break down complex flows into discrete, testable steps. +1. **Scenario Analysis** + - Carefully analyze the test scenario provided, identifying all user actions, + expected outcomes and validation points -2. **Interactive Execution**: - - For each test, start with the `test_setup_page` tool to set up page for the scenario +2. **Interactive Execution** + - For each scenario, start with the `test_setup_page` tool to set up page for the scenario - Use Playwright tools to manually execute each step of the scenario in real-time - Verify that each action works as expected - Identify the correct locators and interaction patterns - Observe actual application behavior and responses - - Catch potential timing issues or dynamic content - Validate that assertions will work correctly -3. **Test Code Generation**: After successfully completing the manual execution, generate clean, maintainable @playwright/test source code that: - - Uses descriptive test names that clearly indicate what is being tested - - Implements proper page object patterns when beneficial - - Includes appropriate waits and assertions - - Handles dynamic content and loading states - - Uses reliable locators (preferring data-testid, role-based, or text-based selectors over fragile CSS selectors) - - Includes proper setup and teardown - - Is self-contained and can run independently - - Use explicit waits rather than arbitrary timeouts - - Never wait for networkidle or use other discouraged or deprecated apis +3. **Test Code Generation** + + After successfully completing the manual execution, generate clean, maintainable + @playwright/test source code that follows following convention: + + - One file per scenario, one test in a file + - File name must be fs-friendly scenario name + - Test must be placed in a describe matching the top-level test plan item + - Test title must match the scenario name + - Includes a comment with the step text before each step execution + + + For following plan: + + ```markdown file=specs/plan.md + ### 1. Adding New Todos + **Seed:** `tests/seed.spec.ts` + + #### 1.1 Add Valid Todo + **Steps:** + 1. Click in the "What needs to be done?" input field + + #### 1.2 Add Multiple Todos + ... + ``` -4. **Quality Assurance**: Ensure each generated test: - - Has clear, descriptive assertions that validate the expected behavior + Following file is generated: + + ```ts file=add-valid-todo.spec.ts + // spec: specs/plan.md + // seed: tests/seed.spec.ts + + test.describe('Adding New Todos', () => { + test('Add Valid Todo', async { page } => { + // 1. Click in the "What needs to be done?" input field + await page.click(...); + + ... + }); + }); + ``` + + +4. **Best practices**: + - Each test has clear, descriptive assertions that validate the expected behavior - Includes proper error handling and meaningful failure messages - Uses Playwright best practices (page.waitForLoadState, expect.toBeVisible, etc.) + - Do not improvise, do not add directives that were not asked for + - Uses reliable locators (preferring data-testid, role-based, or text-based selectors over fragile CSS selectors) + - Uses local variables for locators that are used multiple times + - Uses explicit waits rather than arbitrary timeouts + - Never waits for networkidle or use other discouraged or deprecated apis + - Is self-contained and can run independently - Is deterministic and not prone to flaky behavior - - Follows consistent naming conventions and code structure - -5. **Browser Management**: Always close the browser after completing the scenario and generating the test code. - -Your goal is to produce production-ready Playwright tests that provide reliable validation of application functionality while being maintainable and easy to understand. -Process all scenarios sequentially, do not run in parallel. Save tests in the tests/ folder. Context: User wants to test a login flow on their web application. - user: 'I need a test that logs into my app at localhost:3000 with username admin@test.com and password 123456, then verifies the dashboard page loads' + user: 'I need a test that logs into my app at localhost:3000 with username admin@test.com and password 123456, then + verifies the dashboard page loads' assistant: 'I'll use the playwright-test-generator agent to create and validate this login test for you' - The user needs a specific browser automation test created, which is exactly what the playwright-test-generator agent is designed for. + The user needs a specific browser automation test created, which is exactly what the playwright-test-generator agent + is designed for. Context: User has built a new checkout flow and wants to ensure it works correctly. - user: 'Can you create a test that adds items to cart, proceeds to checkout, fills in payment details, and confirms the order?' + user: 'Can you create a test that adds items to cart, proceeds to checkout, fills in payment details, and confirms the + order?' assistant: 'I'll use the playwright-test-generator agent to build a comprehensive checkout flow test' - This is a complex user journey that needs to be automated and tested, perfect for the playwright-test-generator agent. + This is a complex user journey that needs to be automated and tested, perfect for the playwright-test-generator + agent. diff --git a/packages/playwright/src/agents/healer.md b/packages/playwright/src/agents/healer.md index c2c826f68afa1..3b25a39b7b9be 100644 --- a/packages/playwright/src/agents/healer.md +++ b/packages/playwright/src/agents/healer.md @@ -49,7 +49,9 @@ Key principles: - If multiple errors exist, fix them one at a time and retest - Provide clear explanations of what was broken and how you fixed it - You will continue this process until the test runs successfully without any failures or errors. -- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme() so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead of the expected behavior. +- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme() + so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead + of the expected behavior. - Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test. - Never wait for networkidle or use other discouraged or deprecated apis diff --git a/packages/playwright/src/agents/planner.md b/packages/playwright/src/agents/planner.md index ee00dd96026c5..9c2a0ce8b3026 100644 --- a/packages/playwright/src/agents/planner.md +++ b/packages/playwright/src/agents/planner.md @@ -28,32 +28,42 @@ tools: - playwright-test/test_setup_page --- -You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage planning. +You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test +scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage +planning. You will: -1. **Navigate and Explore**: +1. **Navigate and Explore** - Invoke the `test_setup_page` tool once to set up page before using any other tools - Explore the browser snapshot - Do not take screenshots unless absolutely necessary - Use browser_* tools to navigate and discover interface - Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality -2. **Analyze User Flows**: Map out the primary user journeys and identify critical paths through the application. Consider different user types and their typical behaviors. +2. **Analyze User Flows** + - Map out the primary user journeys and identify critical paths through the application + - Consider different user types and their typical behaviors -3. **Design Comprehensive Scenarios**: Create detailed test scenarios that cover: +3. **Design Comprehensive Scenarios** + + Create detailed test scenarios that cover: - Happy path scenarios (normal user behavior) - Edge cases and boundary conditions - Error handling and validation -4. **Structure Test Plans**: Each scenario must include: +4. **Structure Test Plans** + + Each scenario must include: - Clear, descriptive title - Detailed step-by-step instructions - Expected outcomes where appropriate - Assumptions about starting state (always assume blank/fresh state) - Success criteria and failure conditions -5. **Create Documentation**: Save your test plan as requested: +5. **Create Documentation** + + Save your test plan as requested: - Executive summary of the tested page/application - Individual scenarios as separate sections - Each scenario formatted with numbered steps @@ -64,7 +74,8 @@ You will: ## Application Overview -The TodoMVC application is a React-based todo list manager that provides core task management functionality. The application features: +The TodoMVC application is a React-based todo list manager that provides core task management functionality. The +application features: - **Task Management**: Add, edit, complete, and delete individual todos - **Bulk Operations**: Mark all todos as complete/incomplete and clear all completed todos @@ -77,12 +88,13 @@ The TodoMVC application is a React-based todo list manager that provides core ta ### 1. Adding New Todos +**Seed:** `tests/seed.spec.ts` + #### 1.1 Add Valid Todo **Steps:** -1. Use seed test `tests/seed.spec.ts` -2. Click in the "What needs to be done?" input field -3. Type "Buy groceries" -4. Press Enter key +1. Click in the "What needs to be done?" input field +2. Type "Buy groceries" +3. Press Enter key **Expected Results:** - Todo appears in the list with unchecked checkbox @@ -99,12 +111,14 @@ The TodoMVC application is a React-based todo list manager that provides core ta - Include negative testing scenarios - Ensure scenarios are independent and can be run in any order -**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and professional formatting suitable for sharing with development and QA teams. +**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and +professional formatting suitable for sharing with development and QA teams. Context: User wants to test a new e-commerce checkout flow. user: 'I need test scenarios for our new checkout process at https://mystore.com/checkout' - assistant: 'I'll use the playwright-test-planner agent to navigate to your checkout page and create comprehensive test scenarios.' + assistant: 'I'll use the playwright-test-planner agent to navigate to your checkout page and create comprehensive test + scenarios.' The user needs test planning for a specific web page, so use the playwright-test-planner agent to explore and create test scenarios. @@ -113,7 +127,8 @@ The TodoMVC application is a React-based todo list manager that provides core ta Context: User has deployed a new feature and wants thorough testing coverage. user: 'Can you help me test our new user dashboard at https://app.example.com/dashboard?' - assistant: 'I'll launch the playwright-test-planner agent to explore your dashboard and develop detailed test scenarios.' + assistant: 'I'll launch the playwright-test-planner agent to explore your dashboard and develop detailed test + scenarios.' This requires web exploration and test scenario creation, perfect for the playwright-test-planner agent. From 8a91226126ef050d7068dbb9de87d32a518c7a75 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 18 Sep 2025 18:39:28 -0700 Subject: [PATCH 201/329] feat(html): allow hiding files in html report (#37482) --- docs/src/test-reporters-js.md | 1 + packages/html-reporter/src/types.d.ts | 1 + packages/playwright/src/reporters/html.ts | 68 +++++++++++++++------ packages/playwright/types/test.d.ts | 1 + tests/playwright-test/reporter-html.spec.ts | 41 +++++++++++++ utils/generate_types/overrides-test.d.ts | 1 + 6 files changed, 93 insertions(+), 20 deletions(-) diff --git a/docs/src/test-reporters-js.md b/docs/src/test-reporters-js.md index 973cc1d64110a..ed50f672aff43 100644 --- a/docs/src/test-reporters-js.md +++ b/docs/src/test-reporters-js.md @@ -253,6 +253,7 @@ HTML report supports the following configuration options and environment variabl | `PLAYWRIGHT_HTML_PORT` | `port` | When report opens in the browser, it will be served on this port. | `9323` or any available port when `9323` is not available. | `PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL` | `attachmentsBaseURL` | A separate location where attachments from the `data` subdirectory are uploaded. Only needed when you upload report and `data` separately to different locations. | `data/` | `PLAYWRIGHT_HTML_NO_COPY_PROMPT` | `noCopyPrompt` | If true, disable rendering of the Copy prompt for errors. Supports `true`, `1`, `false`, and `0`. | `false` +| `PLAYWRIGHT_HTML_NO_FILES` | `noFiles` | If true, omit file-based grouping and render describe suites on the top. Supports `true`, `1`, `false`, and `0`. | `false` | `PLAYWRIGHT_HTML_NO_SNIPPETS` | `noSnippets` | If true, disable rendering code snippets in the action log. If there is a top level error, that report section with code snippet will still render. Supports `true`, `1`, `false`, and `0`. | `false` ### Blob reporter diff --git a/packages/html-reporter/src/types.d.ts b/packages/html-reporter/src/types.d.ts index 270a8cb9838c5..e6aebdc31b303 100644 --- a/packages/html-reporter/src/types.d.ts +++ b/packages/html-reporter/src/types.d.ts @@ -40,6 +40,7 @@ export type HTMLReportOptions = { title?: string; noCopyPrompt?: boolean; noSnippets?: boolean; + noFiles?: boolean; }; export type HTMLReport = { diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 04be226d86426..4040ffc18799c 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -138,10 +138,18 @@ class HtmlReporter implements ReporterV2 { noCopyPrompt = true; noCopyPrompt = noCopyPrompt || this._options.noCopyPrompt; + let noFiles: boolean | undefined; + if (process.env.PLAYWRIGHT_HTML_NO_FILES === 'false' || process.env.PLAYWRIGHT_HTML_NO_FILES === '0') + noFiles = false; + else if (process.env.PLAYWRIGHT_HTML_NO_FILES) + noFiles = true; + noFiles = noFiles || this._options.noFiles; + const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, { title: process.env.PLAYWRIGHT_HTML_TITLE || this._options.title, noSnippets, noCopyPrompt, + noFiles, }); this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors); } @@ -232,6 +240,8 @@ export function startHtmlReportServer(folder: string): HttpServer { return server; } +type DataMap = Map; + class HtmlBuilder { private _config: api.FullConfig; private _reportFolder: string; @@ -251,25 +261,23 @@ class HtmlBuilder { } async build(metadata: Metadata, projectSuites: api.Suite[], result: api.FullResult, topLevelErrors: api.TestError[]): Promise<{ ok: boolean, singleTestId: string | undefined }> { - const data = new Map(); + const data: DataMap = new Map(); for (const projectSuite of projectSuites) { + const projectName = projectSuite.project()!.name; for (const fileSuite of projectSuite.suites) { - const fileName = this._relativeLocation(fileSuite.location)!.file; - const fileId = calculateSha1(toPosixPath(fileName)).slice(0, 20); - let fileEntry = data.get(fileId); - if (!fileEntry) { - fileEntry = { - testFile: { fileId, fileName, tests: [] }, - testFileSummary: { fileId, fileName, tests: [], stats: emptyStats() }, - }; - data.set(fileId, fileEntry); - } - const { testFile, testFileSummary } = fileEntry; - const testEntries: TestEntry[] = []; - this._processSuite(fileSuite, projectSuite.project()!.name, [], testEntries); - for (const test of testEntries) { - testFile.tests.push(test.testCase); - testFileSummary.tests.push(test.testCaseSummary); + if (this._options.noFiles) { + for (const describeSuite of fileSuite.suites) { + const groupName = describeSuite.title; + this._createEntryForSuite(data, projectName, describeSuite, groupName, true); + } + const hasTestsOutsideGroups = fileSuite.tests.length > 0; + if (hasTestsOutsideGroups) { + const fileName = ''; + this._createEntryForSuite(data, projectName, fileSuite, fileName, false); + } + } else { + const fileName = this._relativeLocation(fileSuite.location)!.file; + this._createEntryForSuite(data, projectName, fileSuite, fileName, true); } } } @@ -396,13 +404,33 @@ class HtmlBuilder { this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName); } - private _processSuite(suite: api.Suite, projectName: string, path: string[], outTests: TestEntry[]) { + private _createEntryForSuite(data: DataMap, projectName: string, suite: api.Suite, fileName: string, deep: boolean) { + const fileId = calculateSha1(fileName).slice(0, 20); + let fileEntry = data.get(fileId); + if (!fileEntry) { + fileEntry = { + testFile: { fileId, fileName, tests: [] }, + testFileSummary: { fileId, fileName, tests: [], stats: emptyStats() }, + }; + data.set(fileId, fileEntry); + } + + const { testFile, testFileSummary } = fileEntry; + const testEntries: TestEntry[] = []; + this._processSuite(suite, projectName, [], deep, testEntries); + for (const test of testEntries) { + testFile.tests.push(test.testCase); + testFileSummary.tests.push(test.testCaseSummary); + } + } + + private _processSuite(suite: api.Suite, projectName: string, path: string[], deep: boolean, outTests: TestEntry[]) { const newPath = [...path, suite.title]; suite.entries().forEach(e => { if (e.type === 'test') outTests.push(this._createTestEntry(e, projectName, newPath)); - else - this._processSuite(e, projectName, newPath, outTests); + else if (deep) + this._processSuite(e, projectName, newPath, deep, outTests); }); } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 91c168cd734d8..aaf187f497ac0 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -31,6 +31,7 @@ export type HtmlReporterOptions = { title?: string; noSnippets?: boolean; noCopyPrompt?: boolean; + noFiles?: boolean; }; export type ReporterDescription = Readonly< diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 6ee8959a00bf9..ded289bd040f6 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -3204,6 +3204,47 @@ for (const useIntermediateMergeReport of [true, false] as const) { }); } +test('should support noFiles option', async ({ runInlineTest, showReport, page }) => { + await runInlineTest({ + 'playwright.config.ts': ` + import { defineConfig } from '@playwright/test'; + export default defineConfig({ + name: 'project-name', + reporter: [['html', { noFiles: true }]] + }); + module.exports = { name: 'project-name', reporter: [['html', { noFiles: true }]] }; + `, + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test.describe('describe', () => { + test('test 1', async ({}) => {}); + }); + test('test 2', async ({}) => {}); + `, + 'b.test.js': ` + import { test, expect } from '@playwright/test'; + test.describe('describe', () => { + test('test 3', async ({}) => {}); + }); + `, + }, {}, { PLAYWRIGHT_HTML_OPEN: 'never' }); + + await showReport(); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button "describe" [expanded] + - region: + - link "test 1" + - link "a.test.js:4" + - link "test 3" + - link "b.test.js:4" + - button "" [expanded] + - region: + - link "test 2" + - link "a.test.js:6" + `); +}); + function readAllFromStream(stream: NodeJS.ReadableStream): Promise { return new Promise(resolve => { const chunks: Buffer[] = []; diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 665c7ecd32d12..7255ea295cd58 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -30,6 +30,7 @@ export type HtmlReporterOptions = { title?: string; noSnippets?: boolean; noCopyPrompt?: boolean; + noFiles?: boolean; }; export type ReporterDescription = Readonly< From 269b3af4b41b977a641b9e04626ac8ee71ea5cf9 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 06:24:19 +0200 Subject: [PATCH 202/329] feat(webkit): roll to r2212 (#37485) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 6d3ad2780c9a3..b79ed0653893b 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -39,7 +39,7 @@ }, { "name": "webkit", - "revision": "2211", + "revision": "2212", "installByDefault": true, "revisionOverrides": { "debian11-x64": "2105", From a62740581e5123933f8c62e18ce2756acc249534 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 19 Sep 2025 10:49:53 +0200 Subject: [PATCH 203/329] chore: update React to 19 (#37467) --- package-lock.json | 63 ++++++++----------- package.json | 8 +-- packages/html-reporter/src/chip.tsx | 4 +- packages/html-reporter/src/statusIcon.tsx | 2 +- packages/html-reporter/src/tabbedPane.tsx | 2 +- packages/html-reporter/src/testFileView.tsx | 4 +- packages/html-reporter/src/treeItem.tsx | 4 +- packages/trace-viewer/src/ui/consoleTab.tsx | 14 ++--- packages/trace-viewer/src/ui/filmStrip.tsx | 2 +- packages/trace-viewer/src/ui/workbench.tsx | 2 +- .../trace-viewer/src/ui/workbenchLoader.tsx | 2 +- packages/web/src/components/dialog.tsx | 2 +- packages/web/src/components/expandable.tsx | 2 +- .../playwright.ct-build.spec.ts | 2 +- 14 files changed, 52 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 28ac6268a687d..0396d7fc631de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,8 @@ "@types/codemirror": "^5.60.7", "@types/formidable": "^2.0.4", "@types/node": "18.19.76", - "@types/react": "^18.0.12", - "@types/react-dom": "^18.0.5", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", "@types/ws": "^8.5.3", "@types/xml2js": "^0.4.9", "@typescript-eslint/eslint-plugin": "^8.41.0", @@ -53,8 +53,8 @@ "license-checker": "^25.0.1", "mime": "^3.0.0", "node-stream-zip": "^1.15.0", - "react": "^18.1.0", - "react-dom": "^18.1.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", "ssim.js": "^3.5.0", "typescript": "^5.9.2", "vite": "^6.3.6", @@ -1810,29 +1810,24 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true - }, "node_modules/@types/react": { - "version": "18.3.24", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", - "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "version": "19.1.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", + "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", "dev": true, + "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "dev": true, + "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.0.0" } }, "node_modules/@types/responselike": { @@ -6439,28 +6434,26 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "dev": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "dev": true, + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.1.1" } }, "node_modules/react-is": { @@ -6865,13 +6858,11 @@ "dev": true }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "dev": true, - "dependencies": { - "loose-envify": "^1.1.0" - } + "license": "MIT" }, "node_modules/semver": { "version": "7.7.2", diff --git a/package.json b/package.json index ec56915d80149..2973982979f40 100644 --- a/package.json +++ b/package.json @@ -65,8 +65,8 @@ "@types/codemirror": "^5.60.7", "@types/formidable": "^2.0.4", "@types/node": "18.19.76", - "@types/react": "^18.0.12", - "@types/react-dom": "^18.0.5", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", "@types/ws": "^8.5.3", "@types/xml2js": "^0.4.9", "@typescript-eslint/eslint-plugin": "^8.41.0", @@ -94,8 +94,8 @@ "license-checker": "^25.0.1", "mime": "^3.0.0", "node-stream-zip": "^1.15.0", - "react": "^18.1.0", - "react-dom": "^18.1.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", "ssim.js": "^3.5.0", "typescript": "^5.9.2", "vite": "^6.3.6", diff --git a/packages/html-reporter/src/chip.tsx b/packages/html-reporter/src/chip.tsx index cdd07777a67f7..456c8157ed6b0 100644 --- a/packages/html-reporter/src/chip.tsx +++ b/packages/html-reporter/src/chip.tsx @@ -23,7 +23,7 @@ import { clsx } from '@web/uiUtils'; import { type AnchorID, useAnchor } from './links'; export const Chip: React.FC<{ - header: JSX.Element | string, + header: React.JSX.Element | string, expanded?: boolean, noInsets?: boolean, setExpanded?: (expanded: boolean) => void, @@ -48,7 +48,7 @@ export const Chip: React.FC<{ }; export const AutoChip: React.FC<{ - header: JSX.Element | string, + header: React.JSX.Element | string, initialExpanded?: boolean, noInsets?: boolean, children?: any, diff --git a/packages/html-reporter/src/statusIcon.tsx b/packages/html-reporter/src/statusIcon.tsx index 39837955aecb7..778321276fecc 100644 --- a/packages/html-reporter/src/statusIcon.tsx +++ b/packages/html-reporter/src/statusIcon.tsx @@ -18,7 +18,7 @@ import * as icons from './icons'; import './colors.css'; import './common.css'; -export function statusIcon(status: 'failed' | 'timedOut' | 'skipped' | 'passed' | 'expected' | 'unexpected' | 'flaky' | 'interrupted'): JSX.Element { +export function statusIcon(status: 'failed' | 'timedOut' | 'skipped' | 'passed' | 'expected' | 'unexpected' | 'flaky' | 'interrupted'): React.JSX.Element { switch (status) { case 'failed': case 'unexpected': diff --git a/packages/html-reporter/src/tabbedPane.tsx b/packages/html-reporter/src/tabbedPane.tsx index 25d1f72169db8..a1e533348b873 100644 --- a/packages/html-reporter/src/tabbedPane.tsx +++ b/packages/html-reporter/src/tabbedPane.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; export interface TabbedPaneTab { id: string; - title: string | JSX.Element; + title: string | React.JSX.Element; count?: number; render: () => React.ReactElement; } diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx index fa88fbb65be58..34e349b1e9486 100644 --- a/packages/html-reporter/src/testFileView.tsx +++ b/packages/html-reporter/src/testFileView.tsx @@ -71,7 +71,7 @@ export const TestFileView: React.FC; }; -function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined { +function imageDiffBadge(test: TestCaseSummary): React.JSX.Element | undefined { for (const result of test.results) { for (const attachment of result.attachments) { if (attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/)) @@ -80,7 +80,7 @@ function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined { } } -function videoBadge(test: TestCaseSummary): JSX.Element | undefined { +function videoBadge(test: TestCaseSummary): React.JSX.Element | undefined { const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video')); return resultWithVideo ? {video()} : undefined; } diff --git a/packages/html-reporter/src/treeItem.tsx b/packages/html-reporter/src/treeItem.tsx index 546510afc3fad..389e558650071 100644 --- a/packages/html-reporter/src/treeItem.tsx +++ b/packages/html-reporter/src/treeItem.tsx @@ -20,8 +20,8 @@ import * as icons from './icons'; import { clsx } from '@web/uiUtils'; export const TreeItem: React.FunctionComponent<{ - title: JSX.Element, - loadChildren?: () => JSX.Element[], + title: React.JSX.Element, + loadChildren?: () => React.JSX.Element[], onClick?: () => void, expandByDefault?: boolean, depth: number, diff --git a/packages/trace-viewer/src/ui/consoleTab.tsx b/packages/trace-viewer/src/ui/consoleTab.tsx index d8e9ec51e0811..28550cb15be9d 100644 --- a/packages/trace-viewer/src/ui/consoleTab.tsx +++ b/packages/trace-viewer/src/ui/consoleTab.tsx @@ -26,7 +26,7 @@ import { PlaceholderPanel } from './placeholderPanel'; export type ConsoleEntry = { browserMessage?: { - body: JSX.Element[]; + body: React.JSX.Element[]; bodyString: string; location: string; }, @@ -150,9 +150,9 @@ export const ConsoleTab: React.FunctionComponent<{ const errorSuffix = entry.isError ? 'status-error' : entry.isWarning ? 'status-warning' : 'status-none'; const statusElement = entry.browserMessage || entry.browserError ? : ; let locationText: string | undefined; - let messageBody: JSX.Element[] | string | undefined; + let messageBody: React.JSX.Element[] | string | undefined; let messageInnerHTML: string | undefined; - let messageStack: JSX.Element[] | string | undefined; + let messageStack: React.JSX.Element[] | string | undefined; const { browserMessage, browserError, nodeMessage } = entry; if (browserMessage) { @@ -187,7 +187,7 @@ export const ConsoleTab: React.FunctionComponent<{
; }; -function format(args: { preview: string, value: any }[]): JSX.Element[] { +function format(args: { preview: string, value: any }[]): React.JSX.Element[] { if (args.length === 1) return formatAnsi(args[0].preview); @@ -198,8 +198,8 @@ function format(args: { preview: string, value: any }[]): JSX.Element[] { const regex = /%([%sdifoOc])/g; let match; - const formatted: JSX.Element[] = []; - let tokens: JSX.Element[] = []; + const formatted: React.JSX.Element[] = []; + let tokens: React.JSX.Element[] = []; formatted.push({tokens}); let formatIndex = 0; while ((match = regex.exec(messageFormat)) !== null) { @@ -236,7 +236,7 @@ function format(args: { preview: string, value: any }[]): JSX.Element[] { return formatted; } -function formatAnsi(text: string): JSX.Element[] { +function formatAnsi(text: string): React.JSX.Element[] { // eslint-disable-next-line react/jsx-key return []; } diff --git a/packages/trace-viewer/src/ui/filmStrip.tsx b/packages/trace-viewer/src/ui/filmStrip.tsx index dd8a0cb0ac15d..19ed90505de9f 100644 --- a/packages/trace-viewer/src/ui/filmStrip.tsx +++ b/packages/trace-viewer/src/ui/filmStrip.tsx @@ -106,7 +106,7 @@ const FilmStripLane: React.FunctionComponent<{ const frameCount = (effectiveWidth / (frameSize.width + 2 * frameMargin)) | 0; const frameDuration = (endTime - startTime) / frameCount; - const frames: JSX.Element[] = []; + const frames: React.JSX.Element[] = []; for (let i = 0; startTime && frameDuration && i < frameCount; ++i) { const time = startTime + frameDuration * i; const index = upperBound(screencastFrames, time, timeComparator) - 1; diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 2581b672cb0d5..0989e87debe99 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -325,7 +325,7 @@ export const Workbench: React.FunctionComponent<{ component: }; - return
+ return
{!hideTimeline && { event.preventDefault(); setDragOver(true); }}> -
+
Playwright logo
diff --git a/packages/web/src/components/dialog.tsx b/packages/web/src/components/dialog.tsx index 4e2972e4f6a57..df40bde3fbb05 100644 --- a/packages/web/src/components/dialog.tsx +++ b/packages/web/src/components/dialog.tsx @@ -25,7 +25,7 @@ export interface DialogProps { width?: number; verticalOffset?: number; requestClose?: () => void; - anchor?: React.RefObject; + anchor?: React.RefObject; dataTestId?: string; } diff --git a/packages/web/src/components/expandable.tsx b/packages/web/src/components/expandable.tsx index a837bd0a0c0e7..c0ac3ed841b1f 100644 --- a/packages/web/src/components/expandable.tsx +++ b/packages/web/src/components/expandable.tsx @@ -19,7 +19,7 @@ import './expandable.css'; import { clsx } from '../uiUtils'; export const Expandable: React.FunctionComponent void, expanded: boolean, expandOnTitleClick?: boolean, diff --git a/tests/playwright-test/playwright.ct-build.spec.ts b/tests/playwright-test/playwright.ct-build.spec.ts index 58451f7015293..ecf9d6d1fa12d 100644 --- a/tests/playwright-test/playwright.ct-build.spec.ts +++ b/tests/playwright-test/playwright.ct-build.spec.ts @@ -41,7 +41,7 @@ test('should work with the empty component list', async ({ runInlineTest }, test expect(metainfo.version).toEqual(require('playwright-core/package.json').version); expect(metainfo.viteVersion).toEqual(require('vite/package.json').version); expect(Object.entries(metainfo.deps)).toHaveLength(0); - expect(Object.entries(metainfo.sources)).toHaveLength(9); + expect(Object.entries(metainfo.sources)).toHaveLength(10); }); test('should extract component list', async ({ runInlineTest }, testInfo) => { From 307421d5b9ad35ab4875cb629c4983abce398ba7 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 19 Sep 2025 12:28:36 +0200 Subject: [PATCH 204/329] test: rebase WebKit WSL headed tests (#37488) --- tests/library/browsercontext-network-event.spec.ts | 4 ++-- tests/library/favicon.spec.ts | 4 ++-- tests/library/har.spec.ts | 4 ++-- tests/library/video.spec.ts | 4 ++-- tests/page/page-screenshot.spec.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/library/browsercontext-network-event.spec.ts b/tests/library/browsercontext-network-event.spec.ts index e1a60c8a4f3d0..6eaf9e624b04a 100644 --- a/tests/library/browsercontext-network-event.spec.ts +++ b/tests/library/browsercontext-network-event.spec.ts @@ -104,9 +104,9 @@ it('should fire events in proper order', async ({ context, server }) => { ]); }); -it('should not fire events for favicon or favicon redirects', async ({ context, page, server, browserName, channel, headless }) => { +it('should not fire events for favicon or favicon redirects', async ({ context, page, server, browserName, headless }) => { it.skip(headless && browserName !== 'firefox' && browserName as any !== '_bidiFirefox', 'headless browsers, except firefox, do not request favicons'); - it.skip(!headless && browserName === 'webkit' && !channel, 'headed webkit does not have a favicon feature'); + it.skip(!headless && browserName === 'webkit', 'headed webkit does not have a favicon feature'); const favicon = `/no-cache/favicon.ico`; const hashedFaviconUrl = `/favicon-hashed.ico`; const imagePath = `/fakeimage.png`; diff --git a/tests/library/favicon.spec.ts b/tests/library/favicon.spec.ts index d267fdb1865ca..e8c5d56c76292 100644 --- a/tests/library/favicon.spec.ts +++ b/tests/library/favicon.spec.ts @@ -17,9 +17,9 @@ import { contextTest as it } from '../config/browserTest'; -it('should load svg favicon with prefer-color-scheme', async ({ page, server, browserName, channel, headless, asset }) => { +it('should load svg favicon with prefer-color-scheme', async ({ page, server, browserName, headless, asset }) => { it.skip(headless && browserName !== 'firefox' && browserName as any !== '_bidiFirefox', 'headless browsers, except firefox, do not request favicons'); - it.skip(!headless && browserName === 'webkit' && !channel, 'headed webkit does not have a favicon feature'); + it.skip(!headless && browserName === 'webkit', 'headed webkit does not have a favicon feature'); // Browsers aggressively cache favicons, so force bust with the // `d` parameter to make iterating on this test more predictable and isolated. diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index d0d97b0854588..d85ba463c82bf 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -698,9 +698,9 @@ it('should contain http2 for http2 requests', async ({ contextFactory }, testInf server.close(); }); -it('should filter favicon and favicon redirects', async ({ server, browserName, channel, headless, asset, contextFactory }, testInfo) => { +it('should filter favicon and favicon redirects', async ({ server, browserName, headless, asset, contextFactory }, testInfo) => { it.skip(headless && browserName !== 'firefox' && browserName as any !== '_bidiFirefox', 'headless browsers, except firefox, do not request favicons'); - it.skip(!headless && browserName === 'webkit' && !channel, 'headed webkit does not have a favicon feature'); + it.skip(!headless && browserName === 'webkit', 'headed webkit does not have a favicon feature'); const { page, getLog } = await pageWithHar(contextFactory, testInfo); diff --git a/tests/library/video.spec.ts b/tests/library/video.spec.ts index 91ce942a67834..fc2c89a40c87d 100644 --- a/tests/library/video.spec.ts +++ b/tests/library/video.spec.ts @@ -755,11 +755,11 @@ it.describe('screencast', () => { expectAll(pixels, isAlmostRed); }); - it('should capture full viewport on hidpi', async ({ browserType, browserName, headless, isWindows, isLinux, isHeadlessShell }, testInfo) => { + it('should capture full viewport on hidpi', async ({ browserType, browserName, headless, isWindows, isLinux, isHeadlessShell, channel }, testInfo) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22411' }); it.fixme(browserName === 'chromium' && !isHeadlessShell, 'The square is not on the video'); it.fixme(browserName === 'firefox' && isWindows, 'https://github.com/microsoft/playwright/issues/14405'); - it.fixme(browserName === 'webkit' && isLinux && !headless, 'https://github.com/microsoft/playwright/issues/22617'); + it.fixme(browserName === 'webkit' && !headless && (isLinux || (isWindows && channel === 'webkit-wsl')), 'https://github.com/microsoft/playwright/issues/22617'); const size = { width: 600, height: 400 }; const browser = await browserType.launch(); diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index 39dc5e56dc6de..1be6ce3e04dca 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -280,9 +280,9 @@ it.describe('page screenshot', () => { expect(screenshot).toMatchSnapshot('screenshot-clip-odd-size.png'); }); - it('should work for canvas', async ({ page, server, isElectron, isMac, isLinux, macVersion, browserName, isHeadlessShell, headless, channel }) => { + it('should work for canvas', async ({ page, server, isElectron, isMac, isLinux, isWindows, macVersion, browserName, isHeadlessShell, headless, channel }) => { it.fixme(isElectron && isMac, 'Fails on the bots'); - it.fixme(browserName === 'webkit' && isLinux && !headless, 'WebKit has slightly different corners on gtk4.'); + it.fixme(browserName === 'webkit' && !headless && (isLinux || (isWindows && channel === 'webkit-wsl')), 'WebKit has slightly different corners on gtk4.'); await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/screenshots/canvas.html'); const screenshot = await page.screenshot(); From c7b7f3fdf17104fe2dc02dbce5e3025fe90940ad Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 19 Sep 2025 12:28:51 +0200 Subject: [PATCH 205/329] test: rebase stress tests after `method: Page.consoleMessages` was added (#37474) --- tests/stress/heap.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/stress/heap.spec.ts b/tests/stress/heap.spec.ts index 66bedfe9ff265..d07edddab788d 100644 --- a/tests/stress/heap.spec.ts +++ b/tests/stress/heap.spec.ts @@ -66,7 +66,6 @@ test('should not leak dispatchers after closing page', async ({ context, server expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/page').Page)).toBe(COUNT); expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').RequestDispatcher)).toBe(COUNT); expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').ResponseDispatcher)).toBe(COUNT); - expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/console').ConsoleMessage)).toBe(0); for (const page of pages) await page.close(); @@ -75,7 +74,6 @@ test('should not leak dispatchers after closing page', async ({ context, server expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/page').Page)).toBe(0); expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').RequestDispatcher)).toBe(0); expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').ResponseDispatcher)).toBe(0); - expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/console').ConsoleMessage)).toBe(0); expect(await queryObjectCount(require('../../packages/playwright-core/lib/client/page').Page)).toBeLessThan(COUNT); expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/page').Page)).toBe(0); From afb59a0ec0223c80c93acfd69b952607da83ea21 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 19 Sep 2025 07:36:16 -0700 Subject: [PATCH 206/329] fix(mcp): tolerate malformed roots (#37492) --- packages/playwright/src/mcp/sdk/server.ts | 5 ++++- tests/mcp/roots.spec.ts | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/mcp/sdk/server.ts b/packages/playwright/src/mcp/sdk/server.ts index 81e2f40e83343..b95c5759253d0 100644 --- a/packages/playwright/src/mcp/sdk/server.ts +++ b/packages/playwright/src/mcp/sdk/server.ts @@ -97,7 +97,10 @@ const initializeServer = async (server: Server, backend: ServerBackend, runHeart const capabilities = server.getClientCapabilities(); let clientRoots: Root[] = []; if (capabilities?.roots) { - const { roots } = await server.listRoots(); + const { roots } = await server.listRoots().catch(e => { + serverDebug(e); + return { roots: [] }; + }); clientRoots = roots; } diff --git a/tests/mcp/roots.spec.ts b/tests/mcp/roots.spec.ts index 904a3265be221..3a24b33e77863 100644 --- a/tests/mcp/roots.spec.ts +++ b/tests/mcp/roots.spec.ts @@ -78,6 +78,28 @@ test('should list all tools when listRoots is slow', async ({ startClient }) => expect(tools.tools.length).toBeGreaterThan(10); }); +test('should tolerate malformed roots', async ({ startClient, server }, testInfo) => { + const { client } = await startClient({ + clientName: 'Visual Studio Code', + roots: [ + { + name: 'test', + uri: 'bogus://' + p.replace(/\\/g, '/'), + } + ], + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + const [file] = await fs.promises.readdir(testInfo.outputPath('ms-playwright')); + expect(file).toMatch(/mcp-.*/); +}); + function createHash(data: string): string { return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); } From 58687bbf74813d4850201e23054322f34a1d1105 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 19 Sep 2025 08:39:05 -0700 Subject: [PATCH 207/329] chore(bidi): create as Bidi session before browser.close (#36482) --- .../src/server/bidi/bidiConnection.ts | 5 +++-- .../src/server/bidi/bidiFirefox.ts | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/server/bidi/bidiConnection.ts b/packages/playwright-core/src/server/bidi/bidiConnection.ts index cd7357b2560a5..5dfca87212a9f 100644 --- a/packages/playwright-core/src/server/bidi/bidiConnection.ts +++ b/packages/playwright-core/src/server/bidi/bidiConnection.ts @@ -28,7 +28,8 @@ import type * as bidi from './third_party/bidiProtocol'; // BidiPlaywright uses this special id to issue Browser.close command which we // should ignore. -export const kBrowserCloseMessageId = 0; +export const kBrowserCloseMessageId = Number.MAX_SAFE_INTEGER - 1; +export const kShutdownSessionNewMessageId = kBrowserCloseMessageId - 1; export class BidiConnection { private readonly _transport: ConnectionTransport; @@ -235,7 +236,7 @@ export class BidiSession extends EventEmitter { dispatchMessage(message: any) { const object = message as bidi.Message; - if (object.id === kBrowserCloseMessageId) + if (object.id === kBrowserCloseMessageId || object.id === kShutdownSessionNewMessageId) return; if (object.id && this._callbacks.has(object.id)) { const callback = this._callbacks.get(object.id)!; diff --git a/packages/playwright-core/src/server/bidi/bidiFirefox.ts b/packages/playwright-core/src/server/bidi/bidiFirefox.ts index 40543a0d0f5b6..9201a5ffc56af 100644 --- a/packages/playwright-core/src/server/bidi/bidiFirefox.ts +++ b/packages/playwright-core/src/server/bidi/bidiFirefox.ts @@ -20,7 +20,7 @@ import path from 'path'; import { wrapInASCIIBox } from '../utils/ascii'; import { BrowserType, kNoXServerRunningError } from '../browserType'; import { BidiBrowser } from './bidiBrowser'; -import { kBrowserCloseMessageId } from './bidiConnection'; +import { kBrowserCloseMessageId, kShutdownSessionNewMessageId } from './bidiConnection'; import { createProfile } from './third_party/firefoxPrefs'; import { ManualPromise } from '../../utils/isomorphic/manualPromise'; @@ -76,8 +76,23 @@ export class BidiFirefox extends BrowserType { return env; } - override attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void { + override attemptToGracefullyCloseBrowser(transport: ConnectionTransport) { + this._attemptToGracefullyCloseBrowser(transport).catch(() => {}); + } + + private async _attemptToGracefullyCloseBrowser(transport: ConnectionTransport): Promise { // Note that it's fine to reuse the transport, since our connection ignores kBrowserCloseMessageId. + if (!transport.onmessage) { + // browser.close does not work without an active session. If there is no connection + // created with the transport, make sure to create a new session first. + transport.send({ method: 'session.new', params: { capabilities: {} }, id: kShutdownSessionNewMessageId }); + await new Promise(resolve => { + transport.onmessage = message => { + if (message.id === kShutdownSessionNewMessageId) + resolve(true); + }; + }); + } transport.send({ method: 'browser.close', params: {}, id: kBrowserCloseMessageId }); } From 9b0903231828dc84806b4a0c7dcf688c07873e0f Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Fri, 19 Sep 2025 17:52:04 +0200 Subject: [PATCH 208/329] test(bidi): adapt the invalid timezone test for BiDi (#37472) --- .../browsercontext-timezone-id.spec.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/library/browsercontext-timezone-id.spec.ts b/tests/library/browsercontext-timezone-id.spec.ts index 11643b4882580..91b274fc6c00d 100644 --- a/tests/library/browsercontext-timezone-id.spec.ts +++ b/tests/library/browsercontext-timezone-id.spec.ts @@ -45,13 +45,21 @@ it('should work @smoke', async ({ browser, browserName }) => { } }); -it('should throw for invalid timezone IDs when creating pages', async ({ browser }) => { +it('should throw for invalid timezone IDs when creating pages', async ({ browser, browserName }) => { for (const timezoneId of ['Foo/Bar', 'Baz/Qux']) { - let error = null; - const context = await browser.newContext({ timezoneId }); - await context.newPage().catch(e => error = e); - expect(error.message).toContain(`Invalid timezone ID: ${timezoneId}`); - await context.close(); + if (browserName as any === '_bidiChromium' || browserName as any === '_bidiFirefox') { + const error = await browser.newContext({ timezoneId }).catch(e => e); + if (browserName as any === '_bidiChromium') + expect(error.message).toContain(`Invalid timezone "${timezoneId}"`); + else if (browserName as any === '_bidiFirefox') + expect(error.message).toContain(`Expected "timezone" to be a valid timezone ID (e.g., "Europe/Berlin") or a valid timezone offset (e.g., "+01:00"), got ${timezoneId}`); + } else { + let error = null; + const context = await browser.newContext({ timezoneId }); + await context.newPage().catch(e => error = e); + expect(error.message).toContain(`Invalid timezone ID: ${timezoneId}`); + await context.close(); + } } }); From e992c11070848d8524a33b10b65678cb374ae88d Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 19 Sep 2025 16:42:55 -0700 Subject: [PATCH 209/329] docs(agent): rephase agent interaction in the docs (#37501) --- docs/src/test-agents-js.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/test-agents-js.md b/docs/src/test-agents-js.md index 94f27da20ae45..517bd89118054 100644 --- a/docs/src/test-agents-js.md +++ b/docs/src/test-agents-js.md @@ -27,8 +27,8 @@ These steps can be performed independently, manually, or as chained calls in an **Prompt** ```markdown -Ask `playwright-test-planner` agent to generate a test plan for "Guest Checkout" scenario. -Use `seed.spec.ts` as a seed test for the plan. + Generate a test plan for "Guest Checkout" scenario. + Use `seed.spec.ts` as a seed test for the plan. ``` **Output** @@ -95,7 +95,7 @@ behavioral validation. **Prompt** ```markdown -Ask `playwright-test-generator` to generate tests for the guest checkout plan under `specs/`. + Generate tests for the guest checkout plan under `specs/`. ``` **Output** @@ -168,7 +168,7 @@ When a test fails, the healing agent: **Prompt** ```markdown -Ask `playwright-test-healer` to fix all failing tests for the guest checkout scenario. + Fix all failing tests for the guest checkout scenario. ``` **Output** From cd8f3f1c1246f877f331c35010db5e1d51a07ac6 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 19 Sep 2025 17:22:01 -0700 Subject: [PATCH 210/329] chore(mcp): do not mark test run result as error (#37506) --- packages/playwright/src/mcp/test/testTools.ts | 5 ++--- tests/mcp/test-tools.spec.ts | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/playwright/src/mcp/test/testTools.ts b/packages/playwright/src/mcp/test/testTools.ts index b5ed411b0c401..ae37d68453075 100644 --- a/packages/playwright/src/mcp/test/testTools.ts +++ b/packages/playwright/src/mcp/test/testTools.ts @@ -66,7 +66,7 @@ export const runTests = defineTestTool({ const configDir = context.configLocation.configDir; const reporter = new ListReporter({ configDir, screen, includeTestId: true }); const testRunner = await context.createTestRunner(); - const result = await testRunner.runTests(reporter, { + await testRunner.runTests(reporter, { locations: params.locations, projects: params.projects, disableConfigReporters: true, @@ -76,8 +76,7 @@ export const runTests = defineTestTool({ return { content: [ { type: 'text', text }, - ], - isError: result.status !== 'passed', + ] }; }, }); diff --git a/tests/mcp/test-tools.spec.ts b/tests/mcp/test-tools.spec.ts index 9c64551ae23e6..b13432d81f2fa 100644 --- a/tests/mcp/test-tools.spec.ts +++ b/tests/mcp/test-tools.spec.ts @@ -96,6 +96,26 @@ test('test_run', async ({ startClient }) => { expect(text).not.toContain(`../../test-results`); }); +test('test_run for a failed tests is not an error', async ({ startClient }) => { + await writeFiles({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('fails', () => { expect(1).toBe(2); }); + `, + }); + + const { client } = await startClient(); + const response = await client.callTool({ + name: 'test_run', + }); + + const text = response.content[0].text; + // The tool run has succeeded, even though the test has failed. + expect(response.isError).toBeFalsy(); + + expect(text).toContain(`1 failed`); +}); + test('test_run filters', async ({ startClient }) => { await writeFiles({ 'playwright.config.ts': ` From ec8edbc371acb2057100266d788fbac10b6a51af Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 19 Sep 2025 19:32:02 -0700 Subject: [PATCH 211/329] feat(ui): allow inlining file suites (#37505) --- .../playwright/src/isomorphic/testTree.ts | 44 ++++++++++++--- .../src/ui/defaultSettingsView.tsx | 7 +++ packages/trace-viewer/src/ui/uiModeView.tsx | 11 ++-- .../playwright-test/ui-mode-test-tree.spec.ts | 54 +++++++++++++++++++ 4 files changed, 103 insertions(+), 13 deletions(-) diff --git a/packages/playwright/src/isomorphic/testTree.ts b/packages/playwright/src/isomorphic/testTree.ts index 6f38a99588389..713b0d6d5d1d3 100644 --- a/packages/playwright/src/isomorphic/testTree.ts +++ b/packages/playwright/src/isomorphic/testTree.ts @@ -60,7 +60,7 @@ export class TestTree { private _treeItemByTestId = new Map(); readonly pathSeparator: string; - constructor(rootFolder: string, rootSuite: reporterTypes.Suite | undefined, loadErrors: reporterTypes.TestError[], projectFilters: Map | undefined, pathSeparator: string) { + constructor(rootFolder: string, rootSuite: reporterTypes.Suite | undefined, loadErrors: reporterTypes.TestError[], projectFilters: Map | undefined, pathSeparator: string, hideFiles: boolean) { const filterProjects = projectFilters && [...projectFilters.values()].some(Boolean); this.pathSeparator = pathSeparator; this.rootItem = { @@ -77,11 +77,11 @@ export class TestTree { }; this._treeItemById.set(rootFolder, this.rootItem); - const visitSuite = (project: reporterTypes.FullProject, parentSuite: reporterTypes.Suite, parentGroup: GroupItem) => { - for (const suite of parentSuite.suites) { + const visitSuite = (project: reporterTypes.FullProject, parentSuite: reporterTypes.Suite, parentGroup: GroupItem, mode: 'tests' | 'suites' | 'all') => { + for (const suite of mode === 'tests' ? [] : parentSuite.suites) { if (!suite.title) { // Flatten anonymous describes. - visitSuite(project, suite, parentGroup); + visitSuite(project, suite, parentGroup, 'all'); continue; } @@ -101,10 +101,10 @@ export class TestTree { }; this._addChild(parentGroup, group); } - visitSuite(project, suite, group); + visitSuite(project, suite, group, 'all'); } - for (const test of parentSuite.tests) { + for (const test of mode === 'suites' ? [] : parentSuite.tests) { const title = test.title; let testCaseItem = parentGroup.children.find(t => t.kind !== 'group' && t.title === title) as TestCaseItem; if (!testCaseItem) { @@ -163,8 +163,16 @@ export class TestTree { if (filterProjects && !projectFilters.get(projectSuite.title)) continue; for (const fileSuite of projectSuite.suites) { - const fileItem = this._fileItem(fileSuite.location!.file.split(pathSeparator), true); - visitSuite(projectSuite.project()!, fileSuite, fileItem); + if (hideFiles) { + visitSuite(projectSuite.project()!, fileSuite, this.rootItem, 'suites'); + if (fileSuite.tests.length) { + const defaultDescribeItem = this._defaultDescribeItem(); + visitSuite(projectSuite.project()!, fileSuite, defaultDescribeItem, 'tests'); + } + } else { + const fileItem = this._fileItem(fileSuite.location!.file.split(pathSeparator), true); + visitSuite(projectSuite.project()!, fileSuite, fileItem, 'all'); + } } } @@ -238,6 +246,26 @@ export class TestTree { return fileItem; } + private _defaultDescribeItem(): GroupItem { + let defaultDescribeItem = this._treeItemById.get('') as GroupItem; + if (!defaultDescribeItem) { + defaultDescribeItem = { + kind: 'group', + subKind: 'describe', + id: '', + title: '', + location: { file: '', line: 0, column: 0 }, + duration: 0, + parent: this.rootItem, + children: [], + status: 'none', + hasLoadErrors: false, + }; + this._addChild(this.rootItem, defaultDescribeItem); + } + return defaultDescribeItem; + } + sortAndPropagateStatus() { sortAndPropagateStatus(this.rootItem); } diff --git a/packages/trace-viewer/src/ui/defaultSettingsView.tsx b/packages/trace-viewer/src/ui/defaultSettingsView.tsx index 9d555708789cf..6334d3cded2be 100644 --- a/packages/trace-viewer/src/ui/defaultSettingsView.tsx +++ b/packages/trace-viewer/src/ui/defaultSettingsView.tsx @@ -27,6 +27,7 @@ export const DefaultSettingsView: React.FC<{}> = () => { shouldPopulateCanvasFromScreenshot, setShouldPopulateCanvasFromScreenshot, ] = useSetting('shouldPopulateCanvasFromScreenshot', false); + const [hideFiles, setHideFiles] = useSetting('hideFiles', false); const [darkMode, setDarkMode] = useDarkModeSetting(); return ( @@ -38,6 +39,12 @@ export const DefaultSettingsView: React.FC<{}> = () => { set: setDarkMode, name: 'Dark mode' }, + { + type: 'check', + value: hideFiles, + set: setHideFiles, + name: 'Hide files' + }, { type: 'check', value: shouldPopulateCanvasFromScreenshot, diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 78ae6175208ec..b5938bd0454f0 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -102,6 +102,7 @@ export const UIModeView: React.FC<{}> = ({ const [showBrowser, setShowBrowser] = useSetting('show-browser', false); const [updateSnapshots, setUpdateSnapshots] = useSetting('updateSnapshots', 'missing'); + const [hideFiles] = useSetting('hideFiles', false); const inputRef = React.useRef(null); @@ -240,15 +241,15 @@ export const UIModeView: React.FC<{}> = ({ // Test tree is built from the model and filters. const { testTree } = React.useMemo(() => { if (!testModel) - return { testTree: new TestTree('', new TeleSuite('', 'root'), [], projectFilters, queryParams.pathSeparator) }; - const testTree = new TestTree('', testModel.rootSuite, testModel.loadErrors, projectFilters, queryParams.pathSeparator); + return { testTree: new TestTree('', new TeleSuite('', 'root'), [], projectFilters, queryParams.pathSeparator, hideFiles) }; + const testTree = new TestTree('', testModel.rootSuite, testModel.loadErrors, projectFilters, queryParams.pathSeparator, hideFiles); testTree.filterTree(filterText, statusFilters, isRunningTest ? runningState?.testIds : undefined); testTree.sortAndPropagateStatus(); testTree.shortenRoot(); testTree.flattenForSingleProject(); setVisibleTestIds(testTree.testIds()); return { testTree }; - }, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds, runningState, isRunningTest]); + }, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds, runningState, isRunningTest, hideFiles]); const runTests = React.useCallback((mode: 'queue-if-busy' | 'bounce-if-busy', testIds: Set) => { if (!testServerConnection || !testModel) @@ -326,7 +327,7 @@ export const UIModeView: React.FC<{}> = ({ // run affected watched tests const testModel = teleSuiteUpdater.asModel(); - const testTree = new TestTree('', testModel.rootSuite, testModel.loadErrors, projectFilters, queryParams.pathSeparator); + const testTree = new TestTree('', testModel.rootSuite, testModel.loadErrors, projectFilters, queryParams.pathSeparator, hideFiles); const testIds: string[] = []; const set = new Set(params.testFiles); @@ -350,7 +351,7 @@ export const UIModeView: React.FC<{}> = ({ runTests('queue-if-busy', new Set(testIds)); }); return () => disposable.dispose(); - }, [runTests, testServerConnection, watchAll, watchedTreeIds, teleSuiteUpdater, projectFilters]); + }, [runTests, testServerConnection, watchAll, watchedTreeIds, teleSuiteUpdater, projectFilters, hideFiles]); // Shortcuts. React.useEffect(() => { diff --git a/tests/playwright-test/ui-mode-test-tree.spec.ts b/tests/playwright-test/ui-mode-test-tree.spec.ts index 36942507b62cc..993f7ccb796b3 100644 --- a/tests/playwright-test/ui-mode-test-tree.spec.ts +++ b/tests/playwright-test/ui-mode-test-tree.spec.ts @@ -577,3 +577,57 @@ test('should resolve title conflicts', async ({ runUITest }) => { - treeitem "[icon-circle-outline] bar 2" `); }); + +test('should hide file names', async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.test.ts': ` + import { test } from '@playwright/test'; + + test("first", () => {}); + + test.describe("group", () => { + test("second", () => {}); + }); + + test("third", () => {}); + `, + 'b.test.ts': ` + import { test } from '@playwright/test'; + + test("fourth", () => {}); + + test.describe("group", () => { + test("fifth", () => {}); + }); + + test("sixth", () => {}); + ` + }); + + await page.getByText('Settings', { exact: true }).click(); + await page.getByLabel('Hide files').click(); + + await expect.poll(dumpTestTree(page)).toContain(` + ▼ ◯ + ◯ first + ◯ third + ◯ fourth + ◯ sixth + ▼ ◯ group + ◯ second + ◯ fifth + `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] " [expanded]: + - group: + - treeitem "[icon-circle-outline] first" + - treeitem "[icon-circle-outline] third" + - treeitem "[icon-circle-outline] fourth" + - treeitem "[icon-circle-outline] sixth" + - treeitem "[icon-circle-outline] group" [expanded]: + - group: + - treeitem "[icon-circle-outline] second" + - treeitem "[icon-circle-outline] fifth" + `); +}); From 2f820cb20611e1259b069067019233a2d13d049d Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 19 Sep 2025 20:52:41 -0700 Subject: [PATCH 212/329] fix(mcp): lax file path sanitization (#37502) --- .../src/mcp/browser/browserContextFactory.ts | 4 ++-- packages/playwright/src/mcp/browser/config.ts | 20 +++++++++++++++---- .../playwright/src/mcp/browser/context.ts | 4 ++-- .../playwright/src/mcp/browser/sessionLog.ts | 2 +- packages/playwright/src/mcp/browser/tab.ts | 2 +- .../playwright/src/mcp/browser/tools/pdf.ts | 3 ++- .../src/mcp/browser/tools/screenshot.ts | 4 ++-- .../src/mcp/browser/tools/tracing.ts | 2 +- .../playwright/src/mcp/browser/tools/utils.ts | 5 +++++ 9 files changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index 97cf9f12201af..4152d70605251 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -106,7 +106,7 @@ class IsolatedContextFactory extends BaseContextFactory { protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise { await injectCdpPort(this.config.browser); const browserType = playwright[this.config.browser.browserName]; - const tracesDir = await outputFile(this.config, clientInfo, `traces`); + const tracesDir = await outputFile(this.config, clientInfo, `traces`, { origin: 'code' }); if (this.config.saveTrace) await startTraceServer(this.config, tracesDir); return browserType.launch({ @@ -173,7 +173,7 @@ class PersistentContextFactory implements BrowserContextFactory { await injectCdpPort(this.config.browser); testDebug('create browser context (persistent)'); const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo); - const tracesDir = await outputFile(this.config, clientInfo, `traces`); + const tracesDir = await outputFile(this.config, clientInfo, `traces`, { origin: 'code' }); if (this.config.saveTrace) await startTraceServer(this.config, tracesDir); diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index b41d07f585452..0aa7124d99b35 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -271,15 +271,27 @@ async function loadConfig(configFile: string | undefined): Promise { } } -export async function outputFile(config: FullConfig, clientInfo: ClientInfo, name: string): Promise { +export async function outputFile(config: FullConfig, clientInfo: ClientInfo, fileName: string, options: { origin: 'code' | 'llm' | 'web' }): Promise { const rootPath = firstRootPath(clientInfo); const outputDir = config.outputDir ?? (rootPath ? path.join(rootPath, '.playwright-mcp') : undefined) ?? path.join(process.env.PW_TMPDIR_FOR_TEST ?? os.tmpdir(), 'playwright-mcp-output', String(clientInfo.timestamp)); - await fs.promises.mkdir(outputDir, { recursive: true }); - const fileName = sanitizeForFilePath(name); - return path.join(outputDir, fileName); + // Trust code. + if (options.origin === 'code') + return path.resolve(outputDir, fileName); + + // Trust llm to use valid characters in file names. + if (options.origin === 'llm') { + fileName = fileName.split('\\').join('/'); + const resolvedFile = path.resolve(outputDir, fileName); + if (!resolvedFile.startsWith(path.resolve(outputDir) + path.sep)) + throw new Error(`Resolved file path for ${fileName} is outside of the output directory`); + return resolvedFile; + } + + // Do not trust web, at all. + return path.join(outputDir, sanitizeForFilePath(fileName)); } function pickDefined(obj: T | undefined): Partial { diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts index c353a87ee683b..f06c8155dd7db 100644 --- a/packages/playwright/src/mcp/browser/context.ts +++ b/packages/playwright/src/mcp/browser/context.ts @@ -113,8 +113,8 @@ export class Context { return url; } - async outputFile(name: string): Promise { - return outputFile(this.config, this._clientInfo, name); + async outputFile(fileName: string, options: { origin: 'code' | 'llm' | 'web' }): Promise { + return outputFile(this.config, this._clientInfo, fileName, options); } private _onPageCreated(page: playwright.Page) { diff --git a/packages/playwright/src/mcp/browser/sessionLog.ts b/packages/playwright/src/mcp/browser/sessionLog.ts index 4b39ea4c76388..ef236770951cf 100644 --- a/packages/playwright/src/mcp/browser/sessionLog.ts +++ b/packages/playwright/src/mcp/browser/sessionLog.ts @@ -53,7 +53,7 @@ export class SessionLog { } static async create(config: FullConfig, clientInfo: mcpServer.ClientInfo): Promise { - const sessionFolder = await outputFile(config, clientInfo, `session-${Date.now()}`); + const sessionFolder = await outputFile(config, clientInfo, `session-${Date.now()}`, { origin: 'code' }); await fs.promises.mkdir(sessionFolder, { recursive: true }); // eslint-disable-next-line no-console console.error(`Session: ${sessionFolder}`); diff --git a/packages/playwright/src/mcp/browser/tab.ts b/packages/playwright/src/mcp/browser/tab.ts index 8a5dd8eb77e22..1669d517676e2 100644 --- a/packages/playwright/src/mcp/browser/tab.ts +++ b/packages/playwright/src/mcp/browser/tab.ts @@ -117,7 +117,7 @@ export class Tab extends EventEmitter { const entry = { download, finished: false, - outputFile: await this.context.outputFile(download.suggestedFilename()) + outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: 'web' }) }; this._downloads.push(entry); await download.saveAs(entry.outputFile); diff --git a/packages/playwright/src/mcp/browser/tools/pdf.ts b/packages/playwright/src/mcp/browser/tools/pdf.ts index b4c9b14ef46a8..91d142dcefcbc 100644 --- a/packages/playwright/src/mcp/browser/tools/pdf.ts +++ b/packages/playwright/src/mcp/browser/tools/pdf.ts @@ -17,6 +17,7 @@ import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; import * as javascript from '../codegen'; +import { dateAsFileName } from './utils'; const pdfSchema = z.object({ filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'), @@ -34,7 +35,7 @@ const pdf = defineTabTool({ }, handle: async (tab, params, response) => { - const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.pdf`); + const fileName = await tab.context.outputFile(params.filename ?? `page-${dateAsFileName()}.pdf`, { origin: 'llm' }); response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`); response.addResult(`Saved page as ${fileName}`); await tab.page.pdf({ path: fileName }); diff --git a/packages/playwright/src/mcp/browser/tools/screenshot.ts b/packages/playwright/src/mcp/browser/tools/screenshot.ts index 4ee5cf81d91af..c3d0c383be4e1 100644 --- a/packages/playwright/src/mcp/browser/tools/screenshot.ts +++ b/packages/playwright/src/mcp/browser/tools/screenshot.ts @@ -17,7 +17,7 @@ import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; import * as javascript from '../codegen'; -import { generateLocator } from './utils'; +import { generateLocator, dateAsFileName } from './utils'; import type * as playwright from 'playwright-core'; @@ -51,7 +51,7 @@ const screenshot = defineTabTool({ handle: async (tab, params, response) => { const fileType = params.type || 'png'; - const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.${fileType}`); + const fileName = await tab.context.outputFile(params.filename ?? `page-${dateAsFileName()}.${fileType}`, { origin: 'llm' }); const options: playwright.PageScreenshotOptions = { type: fileType, quality: fileType === 'png' ? undefined : 90, diff --git a/packages/playwright/src/mcp/browser/tools/tracing.ts b/packages/playwright/src/mcp/browser/tools/tracing.ts index 828386c40d29c..613a9ae0dc47a 100644 --- a/packages/playwright/src/mcp/browser/tools/tracing.ts +++ b/packages/playwright/src/mcp/browser/tools/tracing.ts @@ -32,7 +32,7 @@ const tracingStart = defineTool({ handle: async (context, params, response) => { const browserContext = await context.ensureBrowserContext(); - const tracesDir = await context.outputFile(`traces`); + const tracesDir = await context.outputFile(`traces`, { origin: 'code' }); const name = 'trace-' + Date.now(); await (browserContext.tracing as Tracing).start({ name, diff --git a/packages/playwright/src/mcp/browser/tools/utils.ts b/packages/playwright/src/mcp/browser/tools/utils.ts index 2aedef1e507fd..8f5b3849247db 100644 --- a/packages/playwright/src/mcp/browser/tools/utils.ts +++ b/packages/playwright/src/mcp/browser/tools/utils.ts @@ -82,3 +82,8 @@ export async function generateLocator(locator: playwright.Locator): Promise(page: playwright.Page, callback: (page: playwright.Page) => Promise): Promise { return await (page as any)._wrapApiCall(() => callback(page), { internal: true }); } + +export function dateAsFileName(): string { + const date = new Date(); + return date.toISOString().replace(/[:.]/g, '-'); +} From 78977d88a446327c2091a368e72d236aec9e1369 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Sat, 20 Sep 2025 14:17:56 +0100 Subject: [PATCH 213/329] feat: page.requests() (#37490) --- docs/src/api/class-page.md | 7 ++++ packages/playwright-client/types/types.d.ts | 6 +++ packages/playwright-core/src/client/page.ts | 9 +++- .../playwright-core/src/protocol/validator.ts | 4 ++ .../src/server/browserContext.ts | 1 + .../dispatchers/browserContextDispatcher.ts | 6 +++ .../server/dispatchers/networkDispatchers.ts | 5 ++- .../src/server/dispatchers/pageDispatcher.ts | 4 ++ packages/playwright-core/src/server/frames.ts | 1 + packages/playwright-core/src/server/page.ts | 16 ++++++- .../src/utils/isomorphic/protocolMetainfo.ts | 1 + packages/playwright-core/types/types.d.ts | 6 +++ packages/protocol/src/channels.d.ts | 6 +++ packages/protocol/src/protocol.yml | 8 ++++ tests/page/page-event-request.spec.ts | 42 +++++++++++++++++++ tests/stress/heap.spec.ts | 12 ++++++ 16 files changed, 128 insertions(+), 6 deletions(-) diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 541aebce3c0bc..2a17c0b1ceb92 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -3137,6 +3137,13 @@ return value resolves to `[]`. * since: v1.9 +## async method: Page.requests +* since: v1.56 +- returns: <[Array]<[Request]>> + +Returns up to 100 last network request from this page. See [`event: Page.request`] for more details. + + ## async method: Page.addLocatorHandler * since: v1.42 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 867dfd391ceca..32c68fbc77e92 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -3926,6 +3926,12 @@ export interface Page { */ requestGC(): Promise; + /** + * Returns up to 100 last network request from this page. See + * [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request) for more details. + */ + requests(): Promise>; + /** * Routing provides the capability to modify network requests that are made by a page. * diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index d838311f14a78..61394f99799d2 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -29,7 +29,7 @@ import { Frame, verifyLoadState } from './frame'; import { HarRouter } from './harRouter'; import { Keyboard, Mouse, Touchscreen } from './input'; import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle'; -import { Response, Route, RouteHandler, WebSocket, WebSocketRoute, WebSocketRouteHandler, validateHeaders } from './network'; +import { Request, Response, Route, RouteHandler, WebSocket, WebSocketRoute, WebSocketRouteHandler, validateHeaders } from './network'; import { Video } from './video'; import { Waiter } from './waiter'; import { Worker } from './worker'; @@ -48,7 +48,7 @@ import type { Clock } from './clock'; import type { APIRequestContext } from './fetch'; import type { WaitForNavigationOptions } from './frame'; import type { FrameLocator, Locator, LocatorOptions } from './locator'; -import type { Request, RouteHandlerCallback, WebSocketRouteHandlerCallback } from './network'; +import type { RouteHandlerCallback, WebSocketRouteHandlerCallback } from './network'; import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, TimeoutOptions, WaitForEventOptions, WaitForFunctionOptions } from './types'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; @@ -804,6 +804,11 @@ export class Page extends ChannelOwner implements api.Page return await this._mainFrame.waitForFunction(pageFunction, arg, options); } + async requests() { + const { requests } = await this._channel.requests(); + return requests.map(request => Request.from(request)); + } + workers(): Worker[] { return [...this._workers]; } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index f5cb173a42087..f68ee39bc4b13 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1481,6 +1481,10 @@ scheme.PagePdfParams = tObject({ scheme.PagePdfResult = tObject({ pdf: tBinary, }); +scheme.PageRequestsParams = tOptional(tObject({})); +scheme.PageRequestsResult = tObject({ + requests: tArray(tChannel(['Request'])), +}); scheme.PageSnapshotForAIParams = tObject({ timeout: tFloat, }); diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 1866e05a21b44..e9b36e61c65ff 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -62,6 +62,7 @@ export abstract class BrowserContext extends SdkObject { RequestAborted: 'requestaborted', RequestFulfilled: 'requestfulfilled', RequestContinued: 'requestcontinued', + RequestCollected: 'requestcollected', BeforeClose: 'beforeclose', VideoStarted: 'videostarted', RecorderEvent: 'recorderevent', diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 8208655bc2bfd..f06787b81260f 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -154,6 +154,7 @@ export class BrowserContextDispatcher extends Dispatcher { + const requestDispatcher = this.connection.existingDispatcher(request); + if (requestDispatcher && !requestDispatcher.reportedThroughEvent) + requestDispatcher._dispose('gc'); + }); this.addObjectListener(BrowserContext.Events.RecorderEvent, ({ event, data, page, code }: { event: 'actionAdded' | 'actionUpdated' | 'signalAdded', data: any, page: Page, code: string }) => { this._dispatchEvent('recorderEvent', { event, data, code, page: PageDispatcher.from(this, page) }); }); diff --git a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts index 1e406f15a0a83..671f2996ed910 100644 --- a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts +++ b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts @@ -32,6 +32,7 @@ import type { Progress } from '@protocol/progress'; export class RequestDispatcher extends Dispatcher implements channels.RequestChannel { _type_Request: boolean; private _browserContextDispatcher: BrowserContextDispatcher; + reportedThroughEvent = false; static from(scope: BrowserContextDispatcher, request: Request): RequestDispatcher { const result = scope.connection.existingDispatcher(request); @@ -48,9 +49,9 @@ export class RequestDispatcher extends Dispatcher(page) : null; - const frameDispatcher = frame ? FrameDispatcher.from(scope, frame) : null; + const frameDispatcher = FrameDispatcher.fromNullable(scope, frame); super(pageDispatcher || frameDispatcher || scope, request, 'Request', { - frame: FrameDispatcher.fromNullable(scope, request.frame()), + frame: frameDispatcher, serviceWorker: WorkerDispatcher.fromNullable(scope, request.serviceWorker()), url: request.url(), resourceType: request.resourceType(), diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index f975bad360634..b1585017b5494 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -339,6 +339,10 @@ export class PageDispatcher extends Dispatcher { + return { requests: this._page.networkRequests().map(request => RequestDispatcher.from(this.parentScope(), request)) }; + } + async snapshotForAI(params: channels.PageSnapshotForAIParams, progress: Progress): Promise { return { snapshot: await this._page.snapshotForAI(progress) }; } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 4fd2e7368bd9a..c705cef56327e 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -302,6 +302,7 @@ export class FrameManager { route?.abort('aborted').catch(() => {}); return; } + this._page.addNetworkRequest(request); this._page.emitOnContext(BrowserContext.Events.Request, request); if (route) new network.Route(request, route).handle([...this._page.requestInterceptors, ...this._page.browserContext.requestInterceptors]); diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index b36b062fe3f42..d17c0ce76241f 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -166,6 +166,7 @@ export class Page extends SdkObject { private _locatorHandlers = new Map }>(); private _lastLocatorHandlerUid = 0; private _locatorHandlerRunningCounter = 0; + private _networkRequests: network.Request[] = []; // Aiming at 25 fps by default - each frame is 40ms, but we give some slack with 35ms. // When throttling for tracing, 200ms between frames, except for 10 frames around the action. @@ -350,6 +351,16 @@ export class Page extends SdkObject { return this._extraHTTPHeaders; } + addNetworkRequest(request: network.Request) { + this._networkRequests.push(request); + for (const collected of ensureArrayLimit(this._networkRequests, 100)) + this.emitOnContext(BrowserContext.Events.RequestCollected, collected); + } + + networkRequests() { + return this._networkRequests; + } + async onBindingCalled(payload: string, context: dom.FrameExecutionContext) { if (this._closedState === 'closed') return; @@ -1078,7 +1089,8 @@ async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, frame return result; } -function ensureArrayLimit(array: any[], limit: number) { +function ensureArrayLimit(array: T[], limit: number): T[] { if (array.length > limit) - array.splice(0, limit / 10); + return array.splice(0, limit / 10); + return []; } diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index 76f231b300c10..5e4ede100e7e3 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -131,6 +131,7 @@ export const methodMetainfo = new Map; + /** + * Returns up to 100 last network request from this page. See + * [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request) for more details. + */ + requests(): Promise>; + /** * Routing provides the capability to modify network requests that are made by a page. * diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index fe346de7f1588..e772163e131c6 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -2121,6 +2121,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { accessibilitySnapshot(params: PageAccessibilitySnapshotParams, progress?: Progress): Promise; pageErrors(params?: PagePageErrorsParams, progress?: Progress): Promise; pdf(params: PagePdfParams, progress?: Progress): Promise; + requests(params?: PageRequestsParams, progress?: Progress): Promise; snapshotForAI(params: PageSnapshotForAIParams, progress?: Progress): Promise; startJSCoverage(params: PageStartJSCoverageParams, progress?: Progress): Promise; stopJSCoverage(params?: PageStopJSCoverageParams, progress?: Progress): Promise; @@ -2569,6 +2570,11 @@ export type PagePdfOptions = { export type PagePdfResult = { pdf: Binary, }; +export type PageRequestsParams = {}; +export type PageRequestsOptions = {}; +export type PageRequestsResult = { + requests: RequestChannel[], +}; export type PageSnapshotForAIParams = { timeout: number, }; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index ef04a9d3a8233..b7658a2013d79 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1998,6 +1998,14 @@ Page: returns: pdf: binary + requests: + title: Get network requests + group: getter + returns: + requests: + type: array + items: Request + snapshotForAI: internal: true parameters: diff --git a/tests/page/page-event-request.spec.ts b/tests/page/page-event-request.spec.ts index 028ddcca54fbe..4bf4df5410c8e 100644 --- a/tests/page/page-event-request.spec.ts +++ b/tests/page/page-event-request.spec.ts @@ -376,3 +376,45 @@ it('should not expose preflight OPTIONS request with network interception', { `POST ${server.CROSS_PROCESS_PREFIX}/cors`, ]); }); + +it('should return last requests', async ({ page, server }) => { + await page.goto(server.PREFIX + '/title.html'); + for (let i = 0; i < 200; ++i) + server.setRoute('/fetch?' + i, (req, res) => res.end('url:' + server.PREFIX + req.url)); + + // #0 is the navigation request, so start with #1. + for (let i = 1; i < 50; ++i) + await page.evaluate(url => fetch(url), server.PREFIX + '/fetch?' + i); + const first50Requests = await page.requests(); + const firstReponse = await first50Requests[1].response(); + expect(await firstReponse.text()).toBe('url:' + server.PREFIX + '/fetch?1'); + + page.on('request', () => {}); + for (let i = 50; i < 100; ++i) + await page.evaluate(url => fetch(url), server.PREFIX + '/fetch?' + i); + const first100Requests = await page.requests(); + + for (let i = 100; i < 200; ++i) + await page.evaluate(url => fetch(url), server.PREFIX + '/fetch?' + i); + const last100Requests = await page.requests(); + + // Last 100 requests are fully functional. + const received = await Promise.all(last100Requests.map(async request => { + const response = await request.response(); + return { text: await response.text(), url: request.url() }; + })); + const expected = []; + for (let i = 100; i < 200; ++i) { + const url = server.PREFIX + '/fetch?' + i; + expected.push({ url, text: 'url:' + url }); + } + expect(received).toEqual(expected); + + // First 50 requests were collected. + const error = await first50Requests[1].response().catch(e => e); + expect(error.message).toContain('request.response: The object has been collected to prevent unbounded heap growth.'); + + // Second 50 requests are functional, because they were reported through the event and not collected. + const reponse50 = await first100Requests[50].response(); + expect(await reponse50.text()).toBe('url:' + server.PREFIX + '/fetch?50'); +}); diff --git a/tests/stress/heap.spec.ts b/tests/stress/heap.spec.ts index d07edddab788d..d3d6f41a1d91f 100644 --- a/tests/stress/heap.spec.ts +++ b/tests/stress/heap.spec.ts @@ -81,6 +81,18 @@ test('should not leak dispatchers after closing page', async ({ context, server expect(await queryObjectCount(require('../../packages/playwright-core/lib/client/network').Response)).toBe(0); }); +test('should not leak requests over 100', async ({ context, server }) => { + const page = await context.newPage(); + await page.goto(server.PREFIX + '/title.html'); + for (let i = 0; i < 100; ++i) + await page.evaluate(url => fetch(url), server.EMPTY_PAGE); + await page.requests(); + for (let i = 0; i < 200; ++i) + await page.evaluate(url => fetch(url), server.EMPTY_PAGE); + await page.requests(); + expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').RequestDispatcher)).toBeLessThanOrEqual(100); +}); + test.describe(() => { test.beforeEach(() => { require('../../packages/playwright-core/lib/server/dispatchers/dispatcher').setMaxDispatchersForTest(100); From fff065816cb1675b4c4ffea8a60ab4302c6727e4 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Sat, 20 Sep 2025 22:21:28 -0700 Subject: [PATCH 214/329] feat(mcp): add --init-script option (#37507) --- packages/playwright/src/mcp/browser/DEPS.list | 1 + .../src/mcp/browser/browserContextFactory.ts | 7 ++ packages/playwright/src/mcp/browser/config.ts | 16 +++++ packages/playwright/src/mcp/config.d.ts | 6 ++ packages/playwright/src/mcp/program.ts | 1 + tests/mcp/init-script.spec.ts | 71 +++++++++++++++++++ 6 files changed, 102 insertions(+) create mode 100644 tests/mcp/init-script.spec.ts diff --git a/packages/playwright/src/mcp/browser/DEPS.list b/packages/playwright/src/mcp/browser/DEPS.list index 936611f47be24..7c9ee17346917 100644 --- a/packages/playwright/src/mcp/browser/DEPS.list +++ b/packages/playwright/src/mcp/browser/DEPS.list @@ -3,3 +3,4 @@ ../sdk/ ../log.ts ../package.ts +../../util.ts diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index 4152d70605251..2710e96857506 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -79,6 +79,7 @@ class BaseContextFactory implements BrowserContextFactory { testDebug(`create browser context (${this._logName})`); const browser = await this._obtainBrowser(clientInfo); const browserContext = await this._doCreateContext(browser); + await addInitScript(browserContext, this.config.browser.initScript); return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) }; } @@ -195,6 +196,7 @@ class PersistentContextFactory implements BrowserContextFactory { }; try { const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions); + await addInitScript(browserContext, this.config.browser.initScript); const close = () => this._closeBrowserContext(browserContext, userDataDir); return { browserContext, close }; } catch (error: any) { @@ -262,6 +264,11 @@ function createHash(data: string): string { return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); } +async function addInitScript(browserContext: playwright.BrowserContext, initScript: string[] | undefined) { + for (const scriptPath of initScript ?? []) + await browserContext.addInitScript({ path: path.resolve(scriptPath) }); +} + export class SharedContextFactory implements BrowserContextFactory { private _contextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> | undefined; private _baseFactory: BrowserContextFactory; diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index 0aa7124d99b35..1e66feffd79fd 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -20,6 +20,7 @@ import path from 'path'; import { devices } from 'playwright-core'; import { dotenv } from 'playwright-core/lib/utilsBundle'; +import { fileExistsAsync } from '../../util'; import { firstRootPath } from '../sdk/server'; @@ -42,6 +43,7 @@ export type CLIOptions = { headless?: boolean; host?: string; ignoreHttpsErrors?: boolean; + initScript?: string[]; isolated?: boolean; imageResponses?: 'allow' | 'omit'; sandbox?: boolean; @@ -114,9 +116,19 @@ export async function resolveCLIConfig(cliOptions: CLIOptions): Promise { + if (config.browser.initScript) { + for (const script of config.browser.initScript) { + if (!await fileExistsAsync(script)) + throw new Error(`Init script file does not exist: ${script}`); + } + } +} + export function configFromCLIOptions(cliOptions: CLIOptions): Config { let browserName: 'chromium' | 'firefox' | 'webkit' | undefined; let channel: string | undefined; @@ -200,6 +212,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { contextOptions, cdpEndpoint: cliOptions.cdpEndpoint, cdpHeaders: cliOptions.cdpHeader, + initScript: cliOptions.initScript, }, server: { port: cliOptions.port, @@ -241,6 +254,9 @@ function configFromEnv(): Config { options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS); options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST); options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS); + const initScript = envToString(process.env.PLAYWRIGHT_MCP_INIT_SCRIPT); + if (initScript) + options.initScript = [initScript]; options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED); if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === 'omit') options.imageResponses = 'omit'; diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index aed8c4745b8ec..66b015ce7f71a 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -68,6 +68,12 @@ export type Config = { * Remote endpoint to connect to an existing Playwright server. */ remoteEndpoint?: string; + + /** + * Paths to JavaScript files to add as initialization scripts. + * The scripts will be evaluated in every page before any of the page's scripts. + */ + initScript?: string[]; }, server?: { diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index 852f4b5a75851..1af6c0e587ac2 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -43,6 +43,7 @@ export function decorateCommand(command: Command, version: string) { .option('--headless', 'run browser in headless mode, headed by default') .option('--host ', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.') .option('--ignore-https-errors', 'ignore https errors') + .option('--init-script ', 'path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page\'s scripts. Can be specified multiple times.') .option('--isolated', 'keep the browser profile in memory, do not save it to disk.') .option('--image-responses ', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".') .option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.') diff --git a/tests/mcp/init-script.spec.ts b/tests/mcp/init-script.spec.ts new file mode 100644 index 0000000000000..b0c001e5d58b1 --- /dev/null +++ b/tests/mcp/init-script.spec.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; +import fs from 'fs'; + + +for (const context of ['isolated', 'persistent']) { + test(`--init-script option loads and executes script (${context})`, async ({ startClient, server }, testInfo) => { + // Create a temporary init script + const initScriptPath = testInfo.outputPath('init-script1.js'); + const initScriptContent1 = `window.testInitScriptExecuted = true;`; + await fs.promises.writeFile(initScriptPath, initScriptContent1); + + const initScriptPath2 = testInfo.outputPath('init-script2.js'); + const initScriptContent2 = `console.log('Init script executed successfully');`; + await fs.promises.writeFile(initScriptPath2, initScriptContent2); + + // Start the client with the init script option + const { client: client } = await startClient({ + args: [`--init-script=${initScriptPath}`, `--init-script=${initScriptPath2}`, ...(context === 'isolated' ? ['--isolated'] : [])] + }); + + // Navigate to a page and verify the init script was executed + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + await client.callTool({ + name: 'browser_evaluate', + arguments: { function: '() => console.log("Custom log")' } + }); + + // Check that the init script variables are available + expect(await client.callTool({ + name: 'browser_evaluate', + arguments: { function: '() => window.testInitScriptExecuted' } + })).toHaveResponse({ + result: 'true', + }); + + expect(await client.callTool({ + name: 'browser_console_messages', + })).toHaveResponse({ + result: expect.stringMatching(/Init script executed successfully.*Custom log/ms), + }); + }); +} + +test('--init-script option with non-existent file throws error', async ({ startClient }, testInfo) => { + const nonExistentPath = testInfo.outputPath('non-existent-script.js'); + + // Attempting to start with a non-existent init script should fail + await expect(startClient({ + args: [`--init-script=${nonExistentPath}`] + })).rejects.toThrow(); +}); From 4825c0c5827c87d8b02a460788a5cb0141dade73 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:26:39 +0200 Subject: [PATCH 215/329] test: roll stable-test-runner to 1.56.0-alpha-2025-09-22 (#37517) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- .../stable-test-runner/package-lock.json | 46 +++++++++---------- .../stable-test-runner/package.json | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index 167de56f12c25..493d9e3de3e7e 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,16 +5,16 @@ "packages": { "": { "dependencies": { - "@playwright/test": "^1.56.0-alpha-2025-09-15" + "@playwright/test": "^1.56.0-alpha-2025-09-22" } }, "node_modules/@playwright/test": { - "version": "1.56.0-alpha-2025-09-15", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-09-15.tgz", - "integrity": "sha512-LXudQbZtsvE49LBKO3L8HC8iNm9yPAZm3/MgVSCpJJ4caNO0Xtfo6GEfBE4vu58fjAQ3k/W59a2qXY935cXmYA==", + "version": "1.56.0-alpha-2025-09-22", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-09-22.tgz", + "integrity": "sha512-RkEGMsxw0xSZHEmorADxzqbtjSzcX4huo4cqtPXyLGm2fLkfmsS3+Xkwp1OeMsY6BFG76dciSqQugEeCXsfDkg==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.0-alpha-2025-09-15" + "playwright": "1.56.0-alpha-2025-09-22" }, "bin": { "playwright": "cli.js" @@ -38,12 +38,12 @@ } }, "node_modules/playwright": { - "version": "1.56.0-alpha-2025-09-15", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-09-15.tgz", - "integrity": "sha512-o2bXPS909kvWmvBz6KfakCzcsYN304B7Upq5W9CY77LEZzL0qhgpveUAJAUDzJfPw4kHhXrrXiGf7L5i8EBZqw==", + "version": "1.56.0-alpha-2025-09-22", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-09-22.tgz", + "integrity": "sha512-Lo6VnO+ZSWf0E7sf1sU+KKqJeVS9AfeQWGjf6cvsszfw7H1SP6FeVdVXyuf+X/EFR8yeJw3i5SE4C1MFOgDElg==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-alpha-2025-09-15" + "playwright-core": "1.56.0-alpha-2025-09-22" }, "bin": { "playwright": "cli.js" @@ -56,9 +56,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.0-alpha-2025-09-15", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-09-15.tgz", - "integrity": "sha512-2/Q5yAym14bVmwv5PvX/dX1SFFlFHbE88aH3SMJblfV8OQbbYHEhcHni/S40oFX9EVpU6JA1xOk/6N+Fzsdwpg==", + "version": "1.56.0-alpha-2025-09-22", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-09-22.tgz", + "integrity": "sha512-bzigMysSsUWa6rrhrRYUb7SMbPbEQWGF0QSSRMhGYx6spD+r5ybeJyTQrwBiyz+qp3BZPofNCfkIiogMWu8upA==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -70,11 +70,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.56.0-alpha-2025-09-15", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-09-15.tgz", - "integrity": "sha512-LXudQbZtsvE49LBKO3L8HC8iNm9yPAZm3/MgVSCpJJ4caNO0Xtfo6GEfBE4vu58fjAQ3k/W59a2qXY935cXmYA==", + "version": "1.56.0-alpha-2025-09-22", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-09-22.tgz", + "integrity": "sha512-RkEGMsxw0xSZHEmorADxzqbtjSzcX4huo4cqtPXyLGm2fLkfmsS3+Xkwp1OeMsY6BFG76dciSqQugEeCXsfDkg==", "requires": { - "playwright": "1.56.0-alpha-2025-09-15" + "playwright": "1.56.0-alpha-2025-09-22" } }, "fsevents": { @@ -84,18 +84,18 @@ "optional": true }, "playwright": { - "version": "1.56.0-alpha-2025-09-15", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-09-15.tgz", - "integrity": "sha512-o2bXPS909kvWmvBz6KfakCzcsYN304B7Upq5W9CY77LEZzL0qhgpveUAJAUDzJfPw4kHhXrrXiGf7L5i8EBZqw==", + "version": "1.56.0-alpha-2025-09-22", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-09-22.tgz", + "integrity": "sha512-Lo6VnO+ZSWf0E7sf1sU+KKqJeVS9AfeQWGjf6cvsszfw7H1SP6FeVdVXyuf+X/EFR8yeJw3i5SE4C1MFOgDElg==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.56.0-alpha-2025-09-15" + "playwright-core": "1.56.0-alpha-2025-09-22" } }, "playwright-core": { - "version": "1.56.0-alpha-2025-09-15", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-09-15.tgz", - "integrity": "sha512-2/Q5yAym14bVmwv5PvX/dX1SFFlFHbE88aH3SMJblfV8OQbbYHEhcHni/S40oFX9EVpU6JA1xOk/6N+Fzsdwpg==" + "version": "1.56.0-alpha-2025-09-22", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-09-22.tgz", + "integrity": "sha512-bzigMysSsUWa6rrhrRYUb7SMbPbEQWGF0QSSRMhGYx6spD+r5ybeJyTQrwBiyz+qp3BZPofNCfkIiogMWu8upA==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index 1969f5274a558..ca9b4973dec9f 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "^1.56.0-alpha-2025-09-15" + "@playwright/test": "^1.56.0-alpha-2025-09-22" } } From 5c8e064766ff74f607b58b342e5aadc59f1d03ae Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:38:15 +0200 Subject: [PATCH 216/329] feat(webkit): roll to r2213 (#37496) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index b79ed0653893b..0df6e394fba7b 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -39,7 +39,7 @@ }, { "name": "webkit", - "revision": "2212", + "revision": "2213", "installByDefault": true, "revisionOverrides": { "debian11-x64": "2105", From ed734c83149166860c6bbf19d97491fa95c19a54 Mon Sep 17 00:00:00 2001 From: alexkimru <36776954+alexkimru@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:38:31 +0300 Subject: [PATCH 217/329] docs: fix typo in NUnit section of BrowserContext customization guide (#37504) --- docs/src/test-runners-csharp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/test-runners-csharp.md b/docs/src/test-runners-csharp.md index f193d73619780..194c56a2911da 100644 --- a/docs/src/test-runners-csharp.md +++ b/docs/src/test-runners-csharp.md @@ -124,7 +124,7 @@ xUnit v3 uses the [`conservative` parallelism algorithm](https://xunit.net/docs/ }> -To customize context options, you can override the `ContextOptions` method of your test class derived from `Microsoft.Playwright.MSTest.PageTest` or `Microsoft.Playwright.MSTest.ContextTest`. See the following example: +To customize context options, you can override the `ContextOptions` method of your test class derived from `Microsoft.Playwright.NUnit.PageTest` or `Microsoft.Playwright.NUnit.ContextTest`. See the following example: ```csharp using Microsoft.Playwright.NUnit; From 7c4d2e7b4d21ef7f41d1ae7f986cfa1e755176d5 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 22 Sep 2025 12:46:43 +0100 Subject: [PATCH 218/329] feat(codegen): pick locator in right-click menu (#37516) --- packages/injected/src/recorder/recorder.ts | 24 +++++++++++-------- tests/library/inspector/cli-codegen-3.spec.ts | 1 + 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/injected/src/recorder/recorder.ts b/packages/injected/src/recorder/recorder.ts index 08da712f0a1c7..60162b760a917 100644 --- a/packages/injected/src/recorder/recorder.ts +++ b/packages/injected/src/recorder/recorder.ts @@ -514,10 +514,10 @@ class RecordActionTool implements RecorderTool { private _showActionListDialog(model: HighlightModelWithSelector, event: MouseEvent) { consumeEvent(event); const actionPosition = positionForEvent(event); - const actions: { title: string, action: actions.PerformOnRecordAction }[] = [ + const actions: { title: string, cb: () => void }[] = [ { title: 'Click', - action: { + cb: () => this._performAction({ name: 'click', selector: model.selector, position: actionPosition, @@ -525,11 +525,11 @@ class RecordActionTool implements RecorderTool { button: 'left', modifiers: 0, clickCount: 0, - } + }), }, { title: 'Right click', - action: { + cb: () => this._performAction({ name: 'click', selector: model.selector, position: actionPosition, @@ -537,11 +537,11 @@ class RecordActionTool implements RecorderTool { button: 'right', modifiers: 0, clickCount: 0, - } + }), }, { title: 'Double click', - action: { + cb: () => this._performAction({ name: 'click', selector: model.selector, position: actionPosition, @@ -549,16 +549,20 @@ class RecordActionTool implements RecorderTool { button: 'left', modifiers: 0, clickCount: 2, - } + }), }, { title: 'Hover', - action: { + cb: () => this._performAction({ name: 'hover', selector: model.selector, position: actionPosition, signals: [], - } + }), + }, + { + title: 'Pick locator', + cb: () => this._recorder.elementPicked(model.selector, model), }, ]; @@ -572,7 +576,7 @@ class RecordActionTool implements RecorderTool { actionElement.setAttribute('aria-label', action.title); actionElement.addEventListener('click', () => { this._dialog.close(); - this._performAction(action.action); + action.cb(); }); listElement.appendChild(actionElement); } diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 6b911bbedb984..ec380a9bba533 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -729,6 +729,7 @@ await page.GetByRole(AriaRole.Textbox, new() { Name = \"Coun\\\"try\" }).ClickAs - listitem "Right click" - listitem "Double click" - listitem "Hover" + - listitem "Pick locator" `); await recorder.page.getByRole('listitem', { name: 'Hover' }).click(); }; From 59b849f9e877e5151ad04857cd90155e66763ded Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 22 Sep 2025 12:47:39 +0100 Subject: [PATCH 219/329] chore: remove backgroundPages (#37518) --- docs/src/api/class-browsercontext.md | 40 ++---------------- packages/playwright-client/types/types.d.ts | 41 +++---------------- .../src/client/browserContext.ts | 8 +--- packages/playwright-core/src/client/events.ts | 2 +- packages/playwright-core/src/client/page.ts | 1 - .../playwright-core/src/protocol/validator.ts | 3 -- .../src/server/chromium/crBrowser.ts | 37 +---------------- .../src/server/chromium/crPage.ts | 10 ++--- .../dispatchers/browserContextDispatcher.ts | 3 -- packages/playwright-core/src/server/page.ts | 8 ++-- packages/playwright-core/types/types.d.ts | 41 +++---------------- packages/protocol/src/channels.d.ts | 5 --- packages/protocol/src/protocol.yml | 4 -- 13 files changed, 26 insertions(+), 177 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index c0dbcedc7a60a..97c9303225d13 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -63,39 +63,10 @@ await context.CloseAsync(); ## event: BrowserContext.backgroundPage * since: v1.11 +* deprecated: Background pages have been removed from Chromium together with Manifest V2 extensions. - argument: <[Page]> -:::note -Only works with Chromium browser's persistent context. -::: - -Emitted when new background page is created in the context. - -```java -context.onBackgroundPage(backgroundPage -> { - System.out.println(backgroundPage.url()); -}); -``` - -```js -const backgroundPage = await context.waitForEvent('backgroundpage'); -``` - -```python async -background_page = await context.wait_for_event("backgroundpage") -``` - -```python sync -background_page = context.wait_for_event("backgroundpage") -``` - -```csharp -context.BackgroundPage += (_, backgroundPage) => -{ - Console.WriteLine(backgroundPage.Url); -}; - -``` +This event is not emitted. ## property: BrowserContext.clock * since: v1.45 @@ -456,13 +427,10 @@ Script to be evaluated in all pages in the browser context. Optional. ## method: BrowserContext.backgroundPages * since: v1.11 +* deprecated: Background pages have been removed from Chromium together with Manifest V2 extensions. - returns: <[Array]<[Page]>> -:::note -Background pages are only supported on Chromium-based browsers. -::: - -All existing background pages in the context. +Returns an empty list. ## method: BrowserContext.browser * since: v1.8 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 32c68fbc77e92..86cd33384e8d9 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -8199,14 +8199,7 @@ export interface BrowserContext { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; /** - * **NOTE** Only works with Chromium browser's persistent context. - * - * Emitted when new background page is created in the context. - * - * ```js - * const backgroundPage = await context.waitForEvent('backgroundpage'); - * ``` - * + * This event is not emitted. */ on(event: 'backgroundpage', listener: (page: Page) => any): this; @@ -8398,14 +8391,7 @@ export interface BrowserContext { once(event: 'weberror', listener: (webError: WebError) => any): this; /** - * **NOTE** Only works with Chromium browser's persistent context. - * - * Emitted when new background page is created in the context. - * - * ```js - * const backgroundPage = await context.waitForEvent('backgroundpage'); - * ``` - * + * This event is not emitted. */ addListener(event: 'backgroundpage', listener: (page: Page) => any): this; @@ -8652,14 +8638,7 @@ export interface BrowserContext { off(event: 'weberror', listener: (webError: WebError) => any): this; /** - * **NOTE** Only works with Chromium browser's persistent context. - * - * Emitted when new background page is created in the context. - * - * ```js - * const backgroundPage = await context.waitForEvent('backgroundpage'); - * ``` - * + * This event is not emitted. */ prependListener(event: 'backgroundpage', listener: (page: Page) => any): this; @@ -8858,9 +8837,8 @@ export interface BrowserContext { }>): Promise; /** - * **NOTE** Background pages are only supported on Chromium-based browsers. - * - * All existing background pages in the context. + * Returns an empty list. + * @deprecated Background pages have been removed from Chromium together with Manifest V2 extensions. */ backgroundPages(): Array; @@ -9377,14 +9355,7 @@ export interface BrowserContext { }): Promise; /** - * **NOTE** Only works with Chromium browser's persistent context. - * - * Emitted when new background page is created in the context. - * - * ```js - * const backgroundPage = await context.waitForEvent('backgroundpage'); - * ``` - * + * This event is not emitted. */ waitForEvent(event: 'backgroundpage', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 7f0473e7b5659..9f97260f97759 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -72,7 +72,6 @@ export class BrowserContext extends ChannelOwner readonly tracing: Tracing; readonly clock: Clock; - readonly _backgroundPages = new Set(); readonly _serviceWorkers = new Set(); private _harRecorders = new Map(); _closingStatus: 'none' | 'closing' | 'closed' = 'none'; @@ -102,11 +101,6 @@ export class BrowserContext extends ChannelOwner this._channel.on('page', ({ page }) => this._onPage(Page.from(page))); this._channel.on('route', ({ route }) => this._onRoute(network.Route.from(route))); this._channel.on('webSocketRoute', ({ webSocketRoute }) => this._onWebSocketRoute(network.WebSocketRoute.from(webSocketRoute))); - this._channel.on('backgroundPage', ({ page }) => { - const backgroundPage = Page.from(page); - this._backgroundPages.add(backgroundPage); - this.emit(Events.BrowserContext.BackgroundPage, backgroundPage); - }); this._channel.on('serviceWorker', ({ worker }) => { const serviceWorker = Worker.from(worker); serviceWorker._context = this; @@ -456,7 +450,7 @@ export class BrowserContext extends ChannelOwner } backgroundPages(): Page[] { - return [...this._backgroundPages]; + return []; } serviceWorkers(): Worker[] { diff --git a/packages/playwright-core/src/client/events.ts b/packages/playwright-core/src/client/events.ts index 06a536086c523..cede73640cf3b 100644 --- a/packages/playwright-core/src/client/events.ts +++ b/packages/playwright-core/src/client/events.ts @@ -42,7 +42,7 @@ export const Events = { // Can't use just 'error' due to node.js special treatment of error events. // @see https://nodejs.org/api/events.html#events_error_events WebError: 'weberror', - BackgroundPage: 'backgroundpage', + BackgroundPage: 'backgroundpage', // Deprecated in v1.56, never emitted anymore. ServiceWorker: 'serviceworker', Request: 'request', Response: 'response', diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 61394f99799d2..739231637fbff 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -238,7 +238,6 @@ export class Page extends ChannelOwner implements api.Page _onClose() { this._closed = true; this._browserContext._pages.delete(this); - this._browserContext._backgroundPages.delete(this); this._disposeHarRouters(); this.emit(Events.Page.Close, this); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index f68ee39bc4b13..ef91d80dbed84 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -963,9 +963,6 @@ scheme.BrowserContextWebSocketRouteEvent = tObject({ scheme.BrowserContextVideoEvent = tObject({ artifact: tChannel(['Artifact']), }); -scheme.BrowserContextBackgroundPageEvent = tObject({ - page: tChannel(['Page']), -}); scheme.BrowserContextServiceWorkerEvent = tObject({ worker: tChannel(['Worker']), }); diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index decbf3a86167f..acee565940843 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -46,7 +46,6 @@ export class CRBrowser extends Browser { private _clientRootSessionPromise: Promise | null = null; readonly _contexts = new Map(); _crPages = new Map(); - _backgroundPages = new Map(); _serviceWorkers = new Map(); _devtools?: CRDevTools; private _version = ''; @@ -176,18 +175,11 @@ export class CRBrowser extends Browser { } assert(!this._crPages.has(targetInfo.targetId), 'Duplicate target ' + targetInfo.targetId); - assert(!this._backgroundPages.has(targetInfo.targetId), 'Duplicate target ' + targetInfo.targetId); assert(!this._serviceWorkers.has(targetInfo.targetId), 'Duplicate target ' + targetInfo.targetId); - if (targetInfo.type === 'background_page') { - const backgroundPage = new CRPage(session, targetInfo.targetId, context, null, { hasUIWindow: false, isBackgroundPage: true }); - this._backgroundPages.set(targetInfo.targetId, backgroundPage); - return; - } - if (targetInfo.type === 'page' || treatOtherAsPage) { const opener = targetInfo.openerId ? this._crPages.get(targetInfo.openerId) || null : null; - const crPage = new CRPage(session, targetInfo.targetId, context, opener, { hasUIWindow: targetInfo.type === 'page', isBackgroundPage: false }); + const crPage = new CRPage(session, targetInfo.targetId, context, opener, { hasUIWindow: targetInfo.type === 'page' }); this._crPages.set(targetInfo.targetId, crPage); return; } @@ -215,12 +207,6 @@ export class CRBrowser extends Browser { crPage.didClose(); return; } - const backgroundPage = this._backgroundPages.get(targetId); - if (backgroundPage) { - this._backgroundPages.delete(targetId); - backgroundPage.didClose(); - return; - } const serviceWorker = this._serviceWorkers.get(targetId); if (serviceWorker) { this._serviceWorkers.delete(targetId); @@ -233,9 +219,6 @@ export class CRBrowser extends Browser { for (const crPage of this._crPages.values()) crPage.didClose(); this._crPages.clear(); - for (const backgroundPage of this._backgroundPages.values()) - backgroundPage.didClose(); - this._backgroundPages.clear(); for (const serviceWorker of this._serviceWorkers.values()) serviceWorker.didClose(); this._serviceWorkers.clear(); @@ -337,7 +320,6 @@ export class CRBrowser extends Browser { export class CRBrowserContext extends BrowserContext { static CREvents = { - BackgroundPage: 'backgroundpage', ServiceWorker: 'serviceworker', }; @@ -566,14 +548,6 @@ export class CRBrowserContext extends BrowserContext { } onClosePersistent() { - // When persistent context is closed, we do not necessary get Target.detachedFromTarget - // for all the background pages. - for (const [targetId, backgroundPage] of this._browser._backgroundPages.entries()) { - if (backgroundPage._browserContext === this && backgroundPage._page.initializedOrUndefined()) { - backgroundPage.didClose(); - this._browser._backgroundPages.delete(targetId); - } - } } override async clearCache(): Promise { @@ -591,15 +565,6 @@ export class CRBrowserContext extends BrowserContext { }); } - backgroundPages(): Page[] { - const result: Page[] = []; - for (const backgroundPage of this._browser._backgroundPages.values()) { - if (backgroundPage._browserContext === this && backgroundPage._page.initializedOrUndefined()) - result.push(backgroundPage._page); - } - return result; - } - serviceWorkers(): Worker[] { return Array.from(this._browser._serviceWorkers.values()).filter(serviceWorker => serviceWorker.browserContext === this); } diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index a93760f060e77..df181ccec9945 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -39,7 +39,6 @@ import { CRPDF } from './crPdf'; import { exceptionToError, releaseObject, toConsoleMessageLocation } from './crProtocolHelper'; import { platformToFontFamilies } from './defaultFontFamilies'; import { VideoRecorder } from './videoRecorder'; -import { BrowserContext } from '../browserContext'; import { TargetClosedError } from '../errors'; import { isSessionClosedError } from '../protocolError'; @@ -68,7 +67,6 @@ export class CRPage implements PageDelegate { private readonly _pdf: CRPDF; private readonly _coverage: CRCoverage; readonly _browserContext: CRBrowserContext; - private _isBackgroundPage: boolean; // Holds window features for the next popup being opened via window.open, // until the popup target arrives. This could be racy if two oopifs @@ -82,10 +80,9 @@ export class CRPage implements PageDelegate { return crPage._mainFrameSession; } - constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null, bits: { hasUIWindow: boolean, isBackgroundPage: boolean }) { + constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null, bits: { hasUIWindow: boolean }) { this._targetId = targetId; this._opener = opener; - this._isBackgroundPage = bits.isBackgroundPage; const dragManager = new DragManager(this); this.rawKeyboard = new RawKeyboardImpl(client, browserContext._browser._platform() === 'mac', dragManager); this.rawMouse = new RawMouseImpl(this, client, dragManager); @@ -113,10 +110,9 @@ export class CRPage implements PageDelegate { this._page.setEmulatedSizeFromWindowOpen({ viewport: viewportSize, screen: viewportSize }); } - const createdEvent = this._isBackgroundPage ? CRBrowserContext.CREvents.BackgroundPage : BrowserContext.Events.Page; this._mainFrameSession._initialize(bits.hasUIWindow).then( - () => this._page.reportAsNew(this._opener?._page, undefined, createdEvent), - error => this._page.reportAsNew(this._opener?._page, error, createdEvent)); + () => this._page.reportAsNew(this._opener?._page, undefined), + error => this._page.reportAsNew(this._opener?._page, error)); } private async _forAllFrameSessions(cb: (frame: FrameSession) => Promise) { diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index f06787b81260f..98c9438143ce0 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -137,9 +137,6 @@ export class BrowserContextDispatcher extends Dispatcher this._dispatchEvent('backgroundPage', { page: PageDispatcher.from(this, page) })); for (const serviceWorker of (context as CRBrowserContext).serviceWorkers()) this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this, serviceWorker) }); this.addObjectListener(CRBrowserContext.CREvents.ServiceWorker, serviceWorker => this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this, serviceWorker) })); diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index d17c0ce76241f..e4a54f80fa4fa 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -191,16 +191,16 @@ export class Page extends SdkObject { this.isStorageStatePage = browserContext.isCreatingStorageStatePage(); } - async reportAsNew(opener: Page | undefined, error: Error | undefined = undefined, contextEvent: string = BrowserContext.Events.Page) { + async reportAsNew(opener: Page | undefined, error?: Error) { if (opener) { const openerPageOrError = await opener.waitForInitializedOrError(); if (openerPageOrError instanceof Page && !openerPageOrError.isClosed()) this._opener = openerPageOrError; } - this._markInitialized(error, contextEvent); + this._markInitialized(error); } - private _markInitialized(error: Error | undefined = undefined, contextEvent: string = BrowserContext.Events.Page) { + private _markInitialized(error: Error | undefined = undefined) { if (error) { // Initialization error could have happened because of // context/browser closure. Just ignore the page. @@ -209,7 +209,7 @@ export class Page extends SdkObject { this.frameManager.createDummyMainFrameIfNeeded(); } this._initialized = error || this; - this.emitOnContext(contextEvent, this); + this.emitOnContext(BrowserContext.Events.Page, this); for (const pageError of this._pageErrors) this.emitOnContext(BrowserContext.Events.PageError, pageError, this); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 32c68fbc77e92..86cd33384e8d9 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8199,14 +8199,7 @@ export interface BrowserContext { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; /** - * **NOTE** Only works with Chromium browser's persistent context. - * - * Emitted when new background page is created in the context. - * - * ```js - * const backgroundPage = await context.waitForEvent('backgroundpage'); - * ``` - * + * This event is not emitted. */ on(event: 'backgroundpage', listener: (page: Page) => any): this; @@ -8398,14 +8391,7 @@ export interface BrowserContext { once(event: 'weberror', listener: (webError: WebError) => any): this; /** - * **NOTE** Only works with Chromium browser's persistent context. - * - * Emitted when new background page is created in the context. - * - * ```js - * const backgroundPage = await context.waitForEvent('backgroundpage'); - * ``` - * + * This event is not emitted. */ addListener(event: 'backgroundpage', listener: (page: Page) => any): this; @@ -8652,14 +8638,7 @@ export interface BrowserContext { off(event: 'weberror', listener: (webError: WebError) => any): this; /** - * **NOTE** Only works with Chromium browser's persistent context. - * - * Emitted when new background page is created in the context. - * - * ```js - * const backgroundPage = await context.waitForEvent('backgroundpage'); - * ``` - * + * This event is not emitted. */ prependListener(event: 'backgroundpage', listener: (page: Page) => any): this; @@ -8858,9 +8837,8 @@ export interface BrowserContext { }>): Promise; /** - * **NOTE** Background pages are only supported on Chromium-based browsers. - * - * All existing background pages in the context. + * Returns an empty list. + * @deprecated Background pages have been removed from Chromium together with Manifest V2 extensions. */ backgroundPages(): Array; @@ -9377,14 +9355,7 @@ export interface BrowserContext { }): Promise; /** - * **NOTE** Only works with Chromium browser's persistent context. - * - * Emitted when new background page is created in the context. - * - * ```js - * const backgroundPage = await context.waitForEvent('backgroundpage'); - * ``` - * + * This event is not emitted. */ waitForEvent(event: 'backgroundpage', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index e772163e131c6..12f7ad4a84a90 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1620,7 +1620,6 @@ export interface BrowserContextEventTarget { on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this; on(event: 'webSocketRoute', callback: (params: BrowserContextWebSocketRouteEvent) => void): this; on(event: 'video', callback: (params: BrowserContextVideoEvent) => void): this; - on(event: 'backgroundPage', callback: (params: BrowserContextBackgroundPageEvent) => void): this; on(event: 'serviceWorker', callback: (params: BrowserContextServiceWorkerEvent) => void): this; on(event: 'request', callback: (params: BrowserContextRequestEvent) => void): this; on(event: 'requestFailed', callback: (params: BrowserContextRequestFailedEvent) => void): this; @@ -1698,9 +1697,6 @@ export type BrowserContextWebSocketRouteEvent = { export type BrowserContextVideoEvent = { artifact: ArtifactChannel, }; -export type BrowserContextBackgroundPageEvent = { - page: PageChannel, -}; export type BrowserContextServiceWorkerEvent = { worker: WorkerChannel, }; @@ -2052,7 +2048,6 @@ export interface BrowserContextEvents { 'route': BrowserContextRouteEvent; 'webSocketRoute': BrowserContextWebSocketRouteEvent; 'video': BrowserContextVideoEvent; - 'backgroundPage': BrowserContextBackgroundPageEvent; 'serviceWorker': BrowserContextServiceWorkerEvent; 'request': BrowserContextRequestEvent; 'requestFailed': BrowserContextRequestFailedEvent; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index b7658a2013d79..84ee9b8a4ab97 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1543,10 +1543,6 @@ BrowserContext: parameters: artifact: Artifact - backgroundPage: - parameters: - page: Page - serviceWorker: parameters: worker: Worker From 809c8be976c2e7aca45c62841c3bfcc405ab859d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 22 Sep 2025 15:07:48 +0100 Subject: [PATCH 220/329] test: do not load playwright.dev in a test (#37520) --- tests/playwright-test/playwright.trace.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts index ee46887e19103..82420aa520a64 100644 --- a/tests/playwright-test/playwright.trace.spec.ts +++ b/tests/playwright-test/playwright.trace.spec.ts @@ -785,7 +785,7 @@ test('should use custom expect message in trace', async ({ runInlineTest }, test ]); }); -test('should not throw when merging traces multiple times', async ({ runInlineTest }, testInfo) => { +test('should not throw when merging traces multiple times', async ({ runInlineTest, server }, testInfo) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/27286' }); const result = await runInlineTest({ @@ -813,7 +813,7 @@ test('should not throw when merging traces multiple times', async ({ runInlineTe }); test.beforeAll(async ({ page }) => { - await page.goto('https://playwright.dev'); + await page.goto(${JSON.stringify(server.PREFIX + '/frames/nested-frames.html')}); }); test.afterAll(async ({ context }) => { @@ -821,7 +821,7 @@ test('should not throw when merging traces multiple times', async ({ runInlineTe }); test('foo', async ({ page }) => { - await expect(page.locator('h1')).toContainText('Playwright'); + await expect(page.frameLocator('iframe').nth(1).locator('div')).toHaveText("Hi, I'm frame"); }); `, }, { workers: 1 }); From c92b9a1e3ee4c19764ad6c63d58365e142c0dd68 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 22 Sep 2025 16:21:19 +0100 Subject: [PATCH 221/329] feat(mcp): respond with snapshot diff when beneficial (#37500) --- .../src/utils/isomorphic/ariaSnapshot.ts | 113 ++++++- .../playwright/src/mcp/browser/response.ts | 27 +- packages/playwright/src/mcp/browser/tab.ts | 40 ++- .../src/mcp/browser/tools/snapshot.ts | 2 +- .../library/unit/parse-aria-snapshot.spec.ts | 309 ++++++++++++++++++ tests/mcp/click.spec.ts | 16 +- tests/mcp/dialogs.spec.ts | 11 +- tests/mcp/session-log.spec.ts | 1 - tests/mcp/snapshot-diff.spec.ts | 163 +++++++++ tests/mcp/tabs.spec.ts | 12 +- tests/mcp/wait.spec.ts | 20 +- 11 files changed, 668 insertions(+), 46 deletions(-) create mode 100644 tests/library/unit/parse-aria-snapshot.spec.ts create mode 100644 tests/mcp/snapshot-diff.spec.ts diff --git a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts index 21c5f9a35956d..86050018f760a 100644 --- a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts +++ b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts @@ -43,9 +43,13 @@ export type AriaTextValue = { normalized: string; }; +// Character offsets in the parsed source. +type SourceRange = { from: number, to: number }; + export type AriaTemplateTextNode = { kind: 'text'; text: AriaTextValue; + sourceRange?: SourceRange; }; export type AriaTemplateRoleNode = AriaProps & { @@ -55,6 +59,8 @@ export type AriaTemplateRoleNode = AriaProps & { children?: AriaTemplateNode[]; props?: Record; containerMode?: 'contain' | 'equal' | 'deep-equal'; + sourceRange?: SourceRange; + subtreeSourceRange?: SourceRange; // only present when different from sourceRange }; export type AriaTemplateNode = AriaTemplateRoleNode | AriaTemplateTextNode; @@ -70,7 +76,7 @@ type YamlLibrary = { }; type ParsedYamlPosition = { line: number; col: number; }; -type ParsingOptions = yamlTypes.ParseOptions; +type ParsingOptions = yamlTypes.ParseOptions & { laxProps?: boolean }; export type ParsedYamlError = { message: string; @@ -98,6 +104,13 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars return [lineCounter.linePos(range[0]), lineCounter.linePos(range[1])]; }; + type WithYamlRange = { range?: yamlTypes.Range | null }; + const computeRange = (firstToken: WithYamlRange, lastToken: WithYamlRange): SourceRange | undefined => { + if (!firstToken.range) + return; + return { from: firstToken.range[0], to: lastToken.range ? lastToken.range[2] : firstToken.range[2] }; + }; + const addError = (error: yamlTypes.YAMLError) => { errors.push({ message: error.message, @@ -111,6 +124,7 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars if (itemIsString) { const childNode = KeyParser.parse(item, parseOptions, errors); if (childNode) { + childNode.sourceRange = computeRange(item, item); container.children = container.children || []; container.children.push(childNode); } @@ -156,7 +170,8 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars } container.children.push({ kind: 'text', - text: textValue(value.value) + text: textValue(value.value), + sourceRange: computeRange(key, value), }); continue; } @@ -211,8 +226,11 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars ...childNode, children: [{ kind: 'text', - text: textValue(String(value.value)) - }] + text: textValue(String(value.value)), + sourceRange: computeRange(value, value), + }], + sourceRange: computeRange(key, key), + subtreeSourceRange: computeRange(key, value), }); continue; } @@ -222,6 +240,8 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars const valueIsSequence = value instanceof yaml.YAMLSeq; if (valueIsSequence) { container.children.push(childNode); + childNode.sourceRange = computeRange(key, key); + childNode.subtreeSourceRange = computeRange(key, value); convertSeq(childNode, value as yamlTypes.YAMLSeq); continue; } @@ -233,7 +253,13 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars } }; - const fragment: AriaTemplateNode = { kind: 'role', role: 'fragment' }; + const emptyRange: WithYamlRange = { range: [0, 0, 0] }; // Fragment has no "self" source, only subtree. + const fragment: AriaTemplateNode = { + kind: 'role', + role: 'fragment', + sourceRange: computeRange(emptyRange, emptyRange), + subtreeSourceRange: computeRange(emptyRange, yamlDoc), + }; yamlDoc.errors.forEach(addError); if (errors.length) @@ -272,13 +298,14 @@ export function textValue(value: string): AriaTextValue { } export class KeyParser { + private _options: ParsingOptions; private _input: string; private _pos: number; private _length: number; static parse(text: yamlTypes.Scalar, options: ParsingOptions, errors: ParsedYamlError[]): AriaTemplateRoleNode | null { try { - return new KeyParser(text.value)._parse(); + return new KeyParser(text.value, options)._parse(); } catch (e) { if (e instanceof ParserError) { const message = options.prettyErrors === false ? e.message : e.message + ':\n\n' + text.value + '\n' + ' '.repeat(e.pos) + '^\n'; @@ -292,7 +319,8 @@ export class KeyParser { } } - constructor(input: string) { + constructor(input: string, options: ParsingOptions) { + this._options = options; this._input = input; this._pos = 0; this._length = input.length; @@ -475,6 +503,11 @@ export class KeyParser { node.selected = value === 'true'; return; } + if (this._options.laxProps) { + node.props = node.props || {}; + node.props[key] = textValue(value); + return; + } this._assert(false, `Unsupported attribute [${key}]`, errorPos); } @@ -492,3 +525,69 @@ export class ParserError extends Error { this.pos = pos; } } + +type AriaSnapshotDiffResult = 'equal' | 'different' | { ref: string, newSource: string }[]; + +export function diffAriaSnapshots(yaml: YamlLibrary, oldSnapshot: string, newSnapshot: string): AriaSnapshotDiffResult { + const diffTree = (oldNode: AriaTemplateNode, newNode: AriaTemplateNode): AriaSnapshotDiffResult => { + if (!oldNode.sourceRange || !newNode.sourceRange) + return 'different'; + + const oldSelfSource = oldSnapshot.slice(oldNode.sourceRange.from, oldNode.sourceRange.to); + const newSelfSource = newSnapshot.slice(newNode.sourceRange.from, newNode.sourceRange.to); + if (oldNode.kind !== 'role' || newNode.kind !== 'role') + return (oldNode.kind === newNode.kind && oldSelfSource === newSelfSource) ? 'equal' : 'different'; + + const newNodeSubtreeSourceRange = newNode.subtreeSourceRange || newNode.sourceRange; + const newSubtreeSource = newSnapshot.slice(newNodeSubtreeSourceRange.from, newNodeSubtreeSourceRange.to); + + const oldChildren = oldNode.children || []; + const newChildren = newNode.children || []; + const childrenDiffs = []; + // When "self" is the same, we can diff children and try to find a small diff there. + let useChildrenDiffs = oldSelfSource === newSelfSource && oldChildren.length === newChildren.length; + let childrenTotalLength = 0; + + if (useChildrenDiffs) { + for (let i = 0; i < oldChildren.length; i++) { + const childDiff = diffTree(oldChildren[i], newChildren[i]); + if (childDiff === 'equal') + continue; + if (childDiff === 'different') { + useChildrenDiffs = false; + break; + } + for (const diff of childDiff) { + childrenTotalLength += diff.newSource.length; + childrenDiffs.push(diff); + } + } + } + + if (childrenDiffs.length > 1 && childrenTotalLength * 2 >= newSubtreeSource.length) { + // Too many children diffs without too much of a benefit. + useChildrenDiffs = false; + } + + if (useChildrenDiffs) { + if (!childrenDiffs.length) + return 'equal'; + return childrenDiffs; + } + + const oldRef = oldNode.props?.ref?.raw; + const newRef = newNode.props?.ref?.raw; + if (!oldRef || oldRef !== newRef) + return 'different'; + + return [{ ref: oldRef, newSource: newSubtreeSource }]; + }; + + try { + const oldParsed = parseAriaSnapshotUnsafe(yaml, oldSnapshot, { laxProps: true }); + const newParsed = parseAriaSnapshotUnsafe(yaml, newSnapshot, { laxProps: true }); + return diffTree(oldParsed, newParsed); + } catch { + return 'different'; + } +} diff --git a/packages/playwright/src/mcp/browser/response.ts b/packages/playwright/src/mcp/browser/response.ts index db910cc2227c8..348f03e355f25 100644 --- a/packages/playwright/src/mcp/browser/response.ts +++ b/packages/playwright/src/mcp/browser/response.ts @@ -25,7 +25,7 @@ export class Response { private _code: string[] = []; private _images: { contentType: string, data: Buffer }[] = []; private _context: Context; - private _includeSnapshot = false; + private _includeSnapshot: 'full' | 'partial' | 'none' = 'none'; private _includeTabs = false; private _tabSnapshot: TabSnapshot | undefined; @@ -72,8 +72,8 @@ export class Response { return this._images; } - setIncludeSnapshot() { - this._includeSnapshot = true; + setIncludeSnapshot(full?: 'full') { + this._includeSnapshot = full ?? 'partial'; } setIncludeTabs() { @@ -83,7 +83,7 @@ export class Response { async finish() { // All the async snapshotting post-action is happening here. // Everything below should race against modal states. - if (this._includeSnapshot && this._context.currentTab()) + if (this._includeSnapshot !== 'none' && this._context.currentTab()) this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot(); for (const tab of this._context.tabs()) await tab.updateTitle(); @@ -113,7 +113,7 @@ ${this._code.join('\n')} } // List browser tabs. - if (this._includeSnapshot || this._includeTabs) + if (this._includeSnapshot !== 'none' || this._includeTabs) response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs)); // Add snapshot if provided. @@ -121,7 +121,7 @@ ${this._code.join('\n')} response.push(...renderModalStates(this._context, this._tabSnapshot.modalStates)); response.push(''); } else if (this._tabSnapshot) { - response.push(renderTabSnapshot(this._tabSnapshot)); + response.push(renderTabSnapshot(this._tabSnapshot, this._includeSnapshot === 'full')); response.push(''); } @@ -153,7 +153,7 @@ ${this._code.join('\n')} } } -function renderTabSnapshot(tabSnapshot: TabSnapshot): string { +function renderTabSnapshot(tabSnapshot: TabSnapshot, fullSnapshot: boolean): string { const lines: string[] = []; if (tabSnapshot.consoleMessages.length) { @@ -177,10 +177,15 @@ function renderTabSnapshot(tabSnapshot: TabSnapshot): string { lines.push(`### Page state`); lines.push(`- Page URL: ${tabSnapshot.url}`); lines.push(`- Page Title: ${tabSnapshot.title}`); - lines.push(`- Page Snapshot:`); - lines.push('```yaml'); - lines.push(tabSnapshot.ariaSnapshot); - lines.push('```'); + if (!fullSnapshot && tabSnapshot.formattedAriaSnapshotDiff) { + lines.push(`- Page Snapshot Diff:`); + lines.push(tabSnapshot.formattedAriaSnapshotDiff); + } else { + lines.push(`- Page Snapshot:`); + lines.push('```yaml'); + lines.push(tabSnapshot.ariaSnapshot); + lines.push('```'); + } return lines.join('\n'); } diff --git a/packages/playwright/src/mcp/browser/tab.ts b/packages/playwright/src/mcp/browser/tab.ts index 1669d517676e2..b0190b6c247e2 100644 --- a/packages/playwright/src/mcp/browser/tab.ts +++ b/packages/playwright/src/mcp/browser/tab.ts @@ -16,7 +16,8 @@ import { EventEmitter } from 'events'; import * as playwright from 'playwright-core'; -import { ManualPromise } from 'playwright-core/lib/utils'; +import { ManualPromise, diffAriaSnapshots } from 'playwright-core/lib/utils'; +import { yaml } from 'playwright-core/lib/utilsBundle'; import { callOnPageNoTrace, waitForCompletion } from './tools/utils'; import { logUnhandledError } from '../log'; @@ -40,6 +41,7 @@ export type TabSnapshot = { url: string; title: string; ariaSnapshot: string; + formattedAriaSnapshotDiff?: string; modalStates: ModalState[]; consoleMessages: ConsoleMessage[]; downloads: { download: playwright.Download, finished: boolean, outputFile: string }[]; @@ -55,6 +57,7 @@ export class Tab extends EventEmitter { private _onPageClose: (tab: Tab) => void; private _modalStates: ModalState[] = []; private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = []; + private _lastAriaSnapshot: string | undefined; constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) { super(); @@ -63,7 +66,14 @@ export class Tab extends EventEmitter { this._onPageClose = onPageClose; page.on('console', event => this._handleConsoleMessage(messageToConsoleMessage(event))); page.on('pageerror', error => this._handleConsoleMessage(pageErrorToConsoleMessage(error))); - page.on('request', request => this._requests.set(request, null)); + page.on('request', request => { + this._requests.set(request, null); + // Note: unfortunately, 'framenavigated' event does not differentiate between + // a same-document (#hash or pushState) navigation and a new document being loaded. + // Relying upon a navigation request heuristic. + if (request.frame() === page.mainFrame() && request.isNavigationRequest()) + this._willNavigateMainFrameToNewDocument(); + }); page.on('response', response => this._requests.set(response.request(), response)); page.on('close', () => this._onClose()); page.on('filechooser', chooser => { @@ -140,6 +150,10 @@ export class Tab extends EventEmitter { this._onPageClose(this); } + private _willNavigateMainFrameToNewDocument() { + this._lastAriaSnapshot = undefined; + } + async updateTitle() { await this._raceAgainstModalStates(async () => { this._lastTitle = await callOnPageNoTrace(this.page, page => page.title()); @@ -160,6 +174,7 @@ export class Tab extends EventEmitter { async navigate(url: string) { this._clearCollectedArtifacts(); + this._willNavigateMainFrameToNewDocument(); const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(logUnhandledError)); try { @@ -203,6 +218,7 @@ export class Tab extends EventEmitter { url: this.page.url(), title: await this.page.title(), ariaSnapshot: snapshot, + formattedAriaSnapshotDiff: this._lastAriaSnapshot ? generateAriaSnapshotDiff(this._lastAriaSnapshot, snapshot) : undefined, modalStates: [], consoleMessages: [], downloads: this._downloads, @@ -212,6 +228,7 @@ export class Tab extends EventEmitter { // Assign console message late so that we did not lose any to modal state. tabSnapshot.consoleMessages = this._recentConsoleMessages; this._recentConsoleMessages = []; + this._lastAriaSnapshot = tabSnapshot.ariaSnapshot; } return tabSnapshot ?? { url: this.page.url(), @@ -312,3 +329,22 @@ export function renderModalStates(context: Context, modalStates: ModalState[]): } const tabSymbol = Symbol('tabSymbol'); + +function generateAriaSnapshotDiff(oldSnapshot: string, newSnapshot: string) { + const diffs = diffAriaSnapshots(yaml, oldSnapshot, newSnapshot); + if (diffs === 'equal') + return ''; + if (diffs === 'different') + return; + if (diffs.length > 3 || diffs.some(diff => diff.newSource.split('\n').length > 100)) { + // Being conservative - up to 3 small diff changes, otherwise include full snapshot. + return; + } + const lines = [`The following refs have changed`]; + for (const diff of diffs) + lines.push('', '```yaml', diff.newSource.trimEnd(), '```'); + const combined = lines.join('\n'); + if (combined.length >= newSnapshot.length) + return; + return combined; +} diff --git a/packages/playwright/src/mcp/browser/tools/snapshot.ts b/packages/playwright/src/mcp/browser/tools/snapshot.ts index bea116ee8fb5e..c7e7cc539f518 100644 --- a/packages/playwright/src/mcp/browser/tools/snapshot.ts +++ b/packages/playwright/src/mcp/browser/tools/snapshot.ts @@ -31,7 +31,7 @@ const snapshot = defineTool({ handle: async (context, params, response) => { await context.ensureTab(); - response.setIncludeSnapshot(); + response.setIncludeSnapshot('full'); }, }); diff --git a/tests/library/unit/parse-aria-snapshot.spec.ts b/tests/library/unit/parse-aria-snapshot.spec.ts new file mode 100644 index 0000000000000..9aac6bee1634e --- /dev/null +++ b/tests/library/unit/parse-aria-snapshot.spec.ts @@ -0,0 +1,309 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from '@playwright/test'; +import { parseAriaSnapshotUnsafe, diffAriaSnapshots } from '../../../packages/playwright-core/lib/utils/isomorphic/ariaSnapshot.js'; +import { yaml } from 'playwright-core/lib/utilsBundle'; + +function addSource(text: string, node: any) { + if (node.sourceRange) + node.source = text.slice(node.sourceRange.from, node.sourceRange.to).trim(); + if (node.subtreeSourceRange) + node.subtreeSource = text.slice(node.subtreeSourceRange.from, node.subtreeSourceRange.to).trim(); + (node.children || []).forEach(child => addSource(text, child)); +} + +function parse(text: string) { + const result = parseAriaSnapshotUnsafe(yaml, text, { laxProps: true }); + addSource(text, result); + return result; +} + +function diff(oldSnapshot: string, newSnapshot: string) { + const diffs = diffAriaSnapshots(yaml, oldSnapshot, newSnapshot); + if (Array.isArray(diffs)) + diffs.forEach(diff => diff.newSource = diff.newSource.trimEnd()); + return diffs; +} + +test('parse aria snapshot returns source ranges', () => { + const listSnapshot = `list "list name" [ref=e1]: + - listitem: item 1 + - listitem "item 2": + - button "click me" [ref=e2] + - text: hello + - listitem: + - generic: text1 + - generic [ref=e3] [active] [cursor=pointer]: text2`; + + const snapshot = ` +- ${listSnapshot} +- button "another button" [ref=e4] +`; + + expect(parse(snapshot)).toEqual(expect.objectContaining({ + role: 'fragment', + source: '', + subtreeSource: snapshot.trim(), + children: [ + expect.objectContaining({ + role: 'list', + name: 'list name', + props: { ref: expect.objectContaining({ raw: 'e1' }) }, + source: 'list "list name" [ref=e1]', + subtreeSource: listSnapshot, + children: [ + expect.objectContaining({ + role: 'listitem', + source: 'listitem', + subtreeSource: 'listitem: item 1', + children: [ + expect.objectContaining({ + kind: 'text', + text: expect.objectContaining({ raw: 'item 1' }), + source: 'item 1', + }), + ], + }), + expect.objectContaining({ + role: 'listitem', + name: 'item 2', + source: 'listitem "item 2"', + subtreeSource: 'listitem "item 2":\n - button "click me" [ref=e2]\n - text: hello', + children: [ + expect.objectContaining({ + role: 'button', + name: 'click me', + props: { ref: expect.objectContaining({ raw: 'e2' }) }, + source: 'button "click me" [ref=e2]', + }), + expect.objectContaining({ + kind: 'text', + text: expect.objectContaining({ raw: 'hello' }), + source: 'text: hello', + }), + ], + }), + expect.objectContaining({ + role: 'listitem', + source: 'listitem', + subtreeSource: 'listitem:\n - generic: text1\n - generic [ref=e3] [active] [cursor=pointer]: text2', + children: [ + expect.objectContaining({ + role: 'generic', + source: 'generic', + subtreeSource: 'generic: text1', + children: [ + expect.objectContaining({ + kind: 'text', + text: expect.objectContaining({ raw: 'text1' }), + source: 'text1', + }), + ], + }), + expect.objectContaining({ + role: 'generic', + props: { ref: expect.objectContaining({ raw: 'e3' }), cursor: expect.objectContaining({ raw: 'pointer' }) }, + active: true, + source: 'generic [ref=e3] [active] [cursor=pointer]', + subtreeSource: 'generic [ref=e3] [active] [cursor=pointer]: text2', + children: [ + expect.objectContaining({ + kind: 'text', + text: expect.objectContaining({ raw: 'text2' }), + source: 'text2', + }), + ], + }), + ], + }), + ], + }), + expect.objectContaining({ + role: 'button', + name: 'another button', + props: { ref: expect.objectContaining({ raw: 'e4' }) }, + source: 'button "another button" [ref=e4]', + }), + ], + })); +}); + +test('diff: equal', () => { + expect(diff(`- button "name"`, `- button "name"`)).toBe('equal'); +}); + +test('diff: different', () => { + expect(diff(`- button "name"`, `- button "new name"`)).toBe('different'); +}); + +test('diff: one ref', () => { + expect(diff(` +- button "button 1" [ref=e1] +`, ` +- button "button 2" [ref=e1] +`)).toEqual([ + { + ref: 'e1', + newSource: 'button "button 2" [ref=e1]', + } + ]); +}); + +test('diff: no ref upgrades to parent', () => { + expect(diff(` +- listitem [ref=e6]: + - generic: some text +`, ` +- listitem [ref=e6]: + - generic: some text has changed +`)).toEqual([ + { + ref: 'e6', + newSource: `listitem [ref=e6]: + - generic: some text has changed`, + } + ]); +}); + +test('diff: two refs in a large parent', () => { + expect(diff(` +- listitem [ref=e6]: some text +- listitem +- listitem +- listitem +- listitem +- listitem +- listitem +- listitem +- listitem [ref=e7]: other text +`, ` +- listitem [ref=e6]: some new text +- listitem +- listitem +- listitem +- listitem +- listitem +- listitem +- listitem +- listitem [ref=e7]: other new text +`)).toEqual([ + { + ref: 'e6', + newSource: `listitem [ref=e6]: some new text`, + }, + { + ref: 'e7', + newSource: `listitem [ref=e7]: other new text`, + }, + ]); +}); + +test('diff: mixed', () => { + const snapshot1 = ` + - button "button 1" [ref=e1] [active] + - button "padding" + - button "button 2" [ref=e2] + - button "padding" + - button "padding" + - list "list name" [ref=e3]: + - listitem [ref=e4]: item 1 + - listitem "item 2" [ref=e5] + - button "padding" + - button "padding" + - button "padding" + - button "padding" + - listitem [ref=e6]: + - generic: some text + - button "padding" + - button "padding" + - button "padding" + - button "padding" + - generic: + - listitem [ref=e10]: more text + - listitem "item name" [ref=e7] + - listitem [ref=e8]: + - listitem "deep item" [ref=e9] + `; + const snapshot2 = ` + - button "button 1" [ref=e1] + - button "padding" + - button "button 2" [ref=e2] [active] + - button "padding" + - button "padding" + - list "list name" [ref=e3]: + - listitem [ref=e4]: item 1 + - listitem "item 2" [ref=e5]: + - button "new button 1" [ref=e100] + - button "padding" + - button "padding" + - button "padding" + - button "padding" + - listitem [ref=e6]: + - generic: some text has changed + - button "padding" + - button "padding" + - button "padding" + - button "padding" + - generic: + - listitem [ref=e10]: more text has changed + - listitem "new item name" [ref=e7] + - listitem [ref=e8]: + - listitem "new ref" [ref=e101] + `; + expect(diff(snapshot1, snapshot2)).toEqual([ + { ref: 'e1', newSource: 'button "button 1" [ref=e1]' }, + { ref: 'e2', newSource: 'button "button 2" [ref=e2] [active]' }, + { ref: 'e5', newSource: `listitem "item 2" [ref=e5]: + - button "new button 1" [ref=e100]` }, + { ref: 'e6', newSource: `listitem [ref=e6]: + - generic: some text has changed` }, + { ref: 'e10', newSource: 'listitem [ref=e10]: more text has changed' }, + { ref: 'e7', newSource: `listitem "new item name" [ref=e7]` }, + { ref: 'e8', newSource: `listitem [ref=e8]: + - listitem "new ref" [ref=e101]` }, + ]); +}); + +test('diff: two refs in a small parent are combined', () => { + expect(diff(` +- button "name" +- list [ref=e1]: + - listitem [ref=e6]: some text + - listitem + - listitem + - listitem + - listitem [ref=e7]: other text +`, ` +- button "name" +- list [ref=e1]: + - listitem [ref=e6]: some new text + - listitem + - listitem + - listitem + - listitem [ref=e7]: other new text +`)).toEqual([ + { + ref: 'e1', + newSource: `list [ref=e1]: + - listitem [ref=e6]: some new text + - listitem + - listitem + - listitem + - listitem [ref=e7]: other new text`, + }, + ]); +}); diff --git a/tests/mcp/click.spec.ts b/tests/mcp/click.spec.ts index 6f6a06dbd017c..169d57dca4b8e 100644 --- a/tests/mcp/click.spec.ts +++ b/tests/mcp/click.spec.ts @@ -16,10 +16,16 @@ import { test, expect } from './fixtures'; -test('browser_click', async ({ client, server, mcpBrowser }) => { +test('browser_click', async ({ client, server }) => { server.setContent('/', ` Title + `, 'text/html'); await client.callTool({ @@ -35,7 +41,7 @@ test('browser_click', async ({ client, server, mcpBrowser }) => { }, })).toHaveResponse({ code: `await page.getByRole('button', { name: 'Submit' }).click();`, - pageState: expect.stringContaining(`- button "Submit" ${mcpBrowser !== 'webkit' || process.platform === 'linux' ? '[active] ' : ''}[ref=e2]`), + pageState: expect.stringContaining(`- button "Submit" [active] [ref=e2]`), }); }); @@ -125,7 +131,7 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => { }, })).toHaveResponse({ code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Control'] });`, - pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:true metaKey:false shiftKey:false altKey:false`), + pageState: expect.stringContaining(`generic [ref=e3]: ctrlKey:true metaKey:false shiftKey:false altKey:false`), }); } @@ -138,7 +144,7 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => { }, })).toHaveResponse({ code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Shift'] });`, - pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:false`), + pageState: expect.stringContaining(`generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:false`), }); expect(await client.callTool({ @@ -150,6 +156,6 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => { }, })).toHaveResponse({ code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Shift', 'Alt'] });`, - pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:true`), + pageState: expect.stringContaining(`generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:true`), }); }); diff --git a/tests/mcp/dialogs.spec.ts b/tests/mcp/dialogs.spec.ts index 4c2bf98eb5025..fa2be5cc738e4 100644 --- a/tests/mcp/dialogs.spec.ts +++ b/tests/mcp/dialogs.spec.ts @@ -17,7 +17,7 @@ import { test, expect } from './fixtures'; test('alert dialog', async ({ client, server }) => { - server.setContent('/', ``, 'text/html'); + server.setContent('/', `Title`, 'text/html'); expect(await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX }, @@ -54,7 +54,7 @@ test('alert dialog', async ({ client, server }) => { }, })).toHaveResponse({ modalState: undefined, - pageState: expect.stringContaining(`- button "Button"`), + pageState: expect.stringContaining(`Page Title: Title`), }); }); @@ -218,7 +218,7 @@ test('prompt dialog', async ({ client, server }) => { }); test('alert dialog w/ race', async ({ client, server }) => { - server.setContent('/', ``, 'text/html'); + server.setContent('/', `Title`, 'text/html'); expect(await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX }, @@ -247,9 +247,6 @@ test('alert dialog w/ race', async ({ client, server }) => { expect(result).toHaveResponse({ modalState: undefined, pageState: expect.stringContaining(`- Page URL: ${server.PREFIX}/ -- Page Title: -- Page Snapshot: -\`\`\`yaml -- button "Button"`), +- Page Title: Title`), }); }); diff --git a/tests/mcp/session-log.spec.ts b/tests/mcp/session-log.spec.ts index 6f9a98ee66cb6..7e072aa55bad3 100644 --- a/tests/mcp/session-log.spec.ts +++ b/tests/mcp/session-log.spec.ts @@ -42,7 +42,6 @@ test('session log should record tool calls', async ({ startClient, server }, tes }, })).toHaveResponse({ code: `await page.getByRole('button', { name: 'Submit' }).click();`, - pageState: expect.stringContaining(`- button "Submit"`), }); const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0]; diff --git a/tests/mcp/snapshot-diff.spec.ts b/tests/mcp/snapshot-diff.spec.ts new file mode 100644 index 0000000000000..f5ca5346c0536 --- /dev/null +++ b/tests/mcp/snapshot-diff.spec.ts @@ -0,0 +1,163 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('should return aria snapshot diff', async ({ client, server }) => { + server.setContent('/', ` + + +
    + + `, 'text/html'); + + const listitems = new Array(100).fill(0).map((_, i) => `\n - listitem [ref=e${5 + i}]: Filler ${i}`).join(''); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + })).toHaveResponse({ + pageState: expect.stringContaining(` + - button "Button 1" [ref=e2] + - button "Button 2" [active] [ref=e3] + - list [ref=e4]:${listitems}`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button 2', + ref: 'e3', + }, + })).toHaveResponse({ + pageState: expect.stringContaining(` +- Page Snapshot Diff: +`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button 1', + ref: 'e2', + }, + })).toHaveResponse({ + pageState: expect.stringContaining(` +- Page Snapshot Diff: +The following refs have changed + +\`\`\`yaml +button "Button 1" [active] [ref=e2] +\`\`\` + +\`\`\`yaml +button "Button 2" [ref=e3] +\`\`\` + +\`\`\`yaml +listitem [ref=e5]: + - text: Filler 0 + - button "new button" [ref=e105] +\`\`\``), + }); + + expect(await client.callTool({ + name: 'browser_snapshot', + })).toHaveResponse({ + pageState: expect.stringContaining(` +- Page Snapshot:`), + }); +}); + +test('should reset aria snapshot diff upon navigation', async ({ client, server }) => { + server.setContent('/before', ` + + +
      + + `, 'text/html'); + + server.setContent('/after', ` + + +
        + + `, 'text/html'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX + '/before', + }, + })).toHaveResponse({ + pageState: expect.stringContaining(` + - button "Button 1" [ref=e2] + - button "Button 2" [active] [ref=e3]`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button 1', + ref: 'e2', + }, + })).toHaveResponse({ + pageState: expect.stringContaining(` +- Page Snapshot:`), + }); +}); diff --git a/tests/mcp/tabs.spec.ts b/tests/mcp/tabs.spec.ts index a0b40d824b783..bc59f2d7a5710 100644 --- a/tests/mcp/tabs.spec.ts +++ b/tests/mcp/tabs.spec.ts @@ -98,10 +98,8 @@ test('select tab', async ({ client }) => { - 2: [Tab two] (data:text/html,Tab twoBody two)`, pageState: expect.stringContaining(`- Page URL: data:text/html,Tab oneBody one - Page Title: Tab one -- Page Snapshot: -\`\`\`yaml -- generic [active] [ref=e1]: Body one -\`\`\``), +- Page Snapshot Diff: +`), }); expect(await client.callTool({ @@ -133,10 +131,8 @@ test('close tab', async ({ client }) => { - 1: (current) [Tab one] (data:text/html,Tab oneBody one)`, pageState: expect.stringContaining(`- Page URL: data:text/html,Tab oneBody one - Page Title: Tab one -- Page Snapshot: -\`\`\`yaml -- generic [active] [ref=e1]: Body one -\`\`\``), +- Page Snapshot Diff: +`), }); }); diff --git a/tests/mcp/wait.spec.ts b/tests/mcp/wait.spec.ts index 0388faf259b57..e844eac4ea89d 100644 --- a/tests/mcp/wait.spec.ts +++ b/tests/mcp/wait.spec.ts @@ -31,9 +31,11 @@ test('browser_wait_for(text)', async ({ client, server }) => { `, 'text/html'); - await client.callTool({ + expect(await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [ref=e3]: Text to disappear`), }); await client.callTool({ @@ -44,10 +46,14 @@ test('browser_wait_for(text)', async ({ client, server }) => { }, }); - expect(await client.callTool({ + await client.callTool({ name: 'browser_wait_for', arguments: { text: 'Text to appear' }, code: `await page.getByText("Text to appear").first().waitFor({ state: 'visible' });`, + }); + + expect(await client.callTool({ + name: 'browser_snapshot', })).toHaveResponse({ pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`), }); @@ -68,9 +74,11 @@ test('browser_wait_for(textGone)', async ({ client, server }) => { `, 'text/html'); - await client.callTool({ + expect(await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [ref=e3]: Text to disappear`), }); await client.callTool({ @@ -81,10 +89,14 @@ test('browser_wait_for(textGone)', async ({ client, server }) => { }, }); - expect(await client.callTool({ + await client.callTool({ name: 'browser_wait_for', arguments: { textGone: 'Text to disappear' }, code: `await page.getByText("Text to disappear").first().waitFor({ state: 'hidden' });`, + }); + + expect(await client.callTool({ + name: 'browser_snapshot', })).toHaveResponse({ pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`), }); From f95e5848fef34c9249bbb7ac63c2d9e73d95cdbc Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 22 Sep 2025 11:23:29 -0700 Subject: [PATCH 222/329] docs(agents): apply a polish pass to agent docs (#37525) --- docs/src/test-agents-js.md | 46 ++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/docs/src/test-agents-js.md b/docs/src/test-agents-js.md index 517bd89118054..3b7c7d46705e0 100644 --- a/docs/src/test-agents-js.md +++ b/docs/src/test-agents-js.md @@ -12,19 +12,41 @@ These steps can be performed independently, manually, or as chained calls in an 1. **Plan**: A planning agent explores the app and produces a test plan in `specs/*.md`. -2. **Generate**: A generating agent transforms the plan into `tests/*.spec.ts` files. It executes actions live to verify selectors and flows, then emits testing code and assertions. +2. **Generate**: A generating agent transforms the plan into `tests/*.spec.ts` files. It executes actions against your site to verify selectors and flows, then emits testing code and assertions. 3. **Heal**: A healing agent executes the test suite and automatically repairs failing tests by applying diffs in place. +### Getting Started + +In order to use Playwright Agents, you must add their definitions to your project using +the `init-agents` command. These definitions should be regenerated whenever Playwright +is updated. + +You need to run this command for each agentic loop you will be using: + +```bash +# Generate agent files for each agentic loop +# Visual Studio Code +npx playwright init-agents --loop=vscode +# Claude Code +npx playwright init-agents --loop=claude +# opencode +npx playwright init-agents --loop=opencode +``` + +Once the agents have been generated, you can use your AI tool of choice to command these agents to build Playwright Tests. Playwright splits this into three steps with one agent per step: + ## 1. Plan +The planning agent explores your app environment and produces a test plan for one or many scenarios and user flows. + **Input** -* A clear request (e.g., “Generate a plan for guest checkout.”) -* A live entry point (URL) or a seed Playwright test that sets up the environment -* PRD (optional) +* A clear request to the planning agent (e.g., “Generate a plan for guest checkout.”) +* A live app entry point (URL) or a seed Playwright test that sets up the environment necessary to talk to your app +* A Product Requirement Document (PRD) (optional) -**Prompt** +**Example Prompt** ```markdown Generate a test plan for "Guest Checkout" scenario. @@ -33,7 +55,7 @@ These steps can be performed independently, manually, or as chained calls in an **Output** -* A Markdown test plan saved to `specs/guest-checkout.md`. The plan is human-readable but precise enough for test generation. +* A Markdown test plan saved to `specs/[scenario name].md`. The plan is human-readable but precise enough for test generation.
        Example: specs/guest-checkout.md @@ -92,7 +114,7 @@ behavioral validation. * Markdown plan from `specs/` -**Prompt** +**Example Prompt** ```markdown Generate tests for the guest checkout plan under `specs/`. @@ -101,7 +123,7 @@ behavioral validation. **Output** * A test suite under `tests/` -* Tests may include initial errors that can be healed automatically +* Generated tests may include initial errors that can be healed automatically by the healer agent
        Example: tests/guest-checkout.spec.ts @@ -165,7 +187,7 @@ When a test fails, the healing agent: * Failing test name -**Prompt** +**Example Prompt** ```markdown Fix all failing tests for the guest checkout scenario. @@ -173,11 +195,11 @@ When a test fails, the healing agent: **Output** -* A passing test, or a skipped test if the functionality is broken +* A passing test, or a skipped test if the healer was unable to ensure correct functionality ## Artifacts and Conventions -Follow a simple, auditable structure: +The static agent definitions and generated files follow a simple, auditable structure: ```bash repo/ @@ -192,7 +214,7 @@ repo/ playwright.config.ts ``` -### Agents definitions +### Agent Definitions Agent definitions are collections of instructions and MCP tools. They are provided by Playwright and should be regenerated whenever Playwright is updated. From 499d084a5cc6556ef0629227a40d13fcac339841 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 22 Sep 2025 14:03:18 -0700 Subject: [PATCH 223/329] fix(session): an extra check for session log (#37508) --- packages/playwright/src/mcp/browser/sessionLog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright/src/mcp/browser/sessionLog.ts b/packages/playwright/src/mcp/browser/sessionLog.ts index ef236770951cf..6a1a7a22539c2 100644 --- a/packages/playwright/src/mcp/browser/sessionLog.ts +++ b/packages/playwright/src/mcp/browser/sessionLog.ts @@ -79,7 +79,7 @@ export class SessionLog { code = code.trim(); if (isUpdate) { const lastEntry = this._pendingEntries[this._pendingEntries.length - 1]; - if (lastEntry.userAction?.name === action.name) { + if (lastEntry?.userAction?.name === action.name) { lastEntry.userAction = action; lastEntry.code = code; return; From 9c860fb7445e806db8b49737efaece411f86f787 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Mon, 22 Sep 2025 23:17:27 +0200 Subject: [PATCH 224/329] test(bidi): hide scrollbars in screenshot tests in Firefox (#37499) --- tests/assets/grid.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/assets/grid.html b/tests/assets/grid.html index 571d5c797efee..8345f429bd5f1 100644 --- a/tests/assets/grid.html +++ b/tests/assets/grid.html @@ -50,6 +50,10 @@ display: none; } +html { + scrollbar-width: none; +} + @keyframes move { from { left: -500px; background-color: cyan; } to { left: 0; background-color: rgb(255, 210, 204); } From 5da9b6f74f0349a3c0d5a9f07e9ce30c2b79033c Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 22 Sep 2025 23:24:43 +0200 Subject: [PATCH 225/329] docs(python): add `connect_options` for remote browser connections in Pytest plugin (#37523) --- docs/src/test-runners-python.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/src/test-runners-python.md b/docs/src/test-runners-python.md index 1590f863c847a..4c1d4d43207dd 100644 --- a/docs/src/test-runners-python.md +++ b/docs/src/test-runners-python.md @@ -70,6 +70,7 @@ def test_my_app_is_working(fixture_name): - `browser_type_launch_args`: Override launch arguments for [`method: BrowserType.launch`]. It should return a Dict. - `browser_context_args`: Override the options for [`method: Browser.newContext`]. It should return a Dict. +- `connect_options`: Connect to an existing browser via WebSocket endpoint. It should return a Dict with [`method: BrowserType.connect`] options. Its also possible to override the context options ([`method: Browser.newContext`]) for a single test by using the `browser_context_args` marker: @@ -219,6 +220,18 @@ def browser_context_args(browser_context_args, playwright): Or via the CLI `--device="iPhone 11 Pro"` +### Connect to remote browsers + +```py title="conftest.py" +import pytest + +@pytest.fixture(scope="session") +def connect_options(): + return { + "wsEndpoint": "ws://localhost:8080/ws" + } +``` + ### Using with `unittest.TestCase` See the following example for using it with `unittest.TestCase`. This has a limitation, From 796cf89ed4a26bd207f79cec62ec950e4b6f9f75 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 22 Sep 2025 23:07:01 +0100 Subject: [PATCH 226/329] chore(mcp): debug_test returns pre-existing console/network (#37519) --- packages/playwright/src/mcp/browser/tab.ts | 18 +++++++++++++ tests/mcp/test-tools.spec.ts | 30 ++++++++++++++++++---- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/playwright/src/mcp/browser/tab.ts b/packages/playwright/src/mcp/browser/tab.ts index b0190b6c247e2..44a855b444755 100644 --- a/packages/playwright/src/mcp/browser/tab.ts +++ b/packages/playwright/src/mcp/browser/tab.ts @@ -91,12 +91,30 @@ export class Tab extends EventEmitter { page.setDefaultNavigationTimeout(this.context.config.timeouts.navigation); page.setDefaultTimeout(this.context.config.timeouts.action); (page as any)[tabSymbol] = this; + void this._initialize(); } static forPage(page: playwright.Page): Tab | undefined { return (page as any)[tabSymbol]; } + private async _initialize() { + const messages = await this.page.consoleMessages().catch(() => []); + for (const message of messages) + this._handleConsoleMessage(messageToConsoleMessage(message)); + const errors = await this.page.pageErrors().catch(() => []); + for (const error of errors) + this._handleConsoleMessage(pageErrorToConsoleMessage(error)); + const requests = await this.page.requests().catch(() => []); + for (const request of requests) { + this._requests.set(request, null); + void request.response().catch(() => null).then(response => { + if (response) + this._requests.set(request, response); + }); + } + } + modalStates(): ModalState[] { return this._modalStates; } diff --git a/tests/mcp/test-tools.spec.ts b/tests/mcp/test-tools.spec.ts index b13432d81f2fa..76151f87aaea6 100644 --- a/tests/mcp/test-tools.spec.ts +++ b/tests/mcp/test-tools.spec.ts @@ -268,18 +268,38 @@ Try recovering from the error prior to continuing`); })).toHaveTextResponse(expect.stringContaining(`1) [id=] a.test.ts:3:11 › fail`)); }); -test('test_debug / browser_snapshot', async ({ startClient }) => { - const { client, id } = await prepareDebugTest(startClient); +test('test_debug (browser_snapshot/network/console)', async ({ startClient, server }) => { + const { client, id } = await prepareDebugTest(startClient, ` + import { test, expect } from '@playwright/test'; + test('fail', async ({ page }) => { + await page.goto(${JSON.stringify(server.HELLO_WORLD)}); + await page.evaluate(() => { + console.log('hello from console'); + setTimeout(() => { throw new Error('error from page'); }, 0); + }); + await expect(page.getByRole('button', { name: 'Missing' })).toBeVisible({ timeout: 1000 }); + }); + `); await client.callTool({ name: 'test_debug', arguments: { test: { id, title: 'fail' }, }, }); + await expect.poll(() => client.callTool({ + name: 'browser_network_requests', + })).toHaveResponse({ + result: expect.stringContaining(`[GET] ${server.HELLO_WORLD} => [200] OK`), + }); + expect(await client.callTool({ + name: 'browser_console_messages', + })).toHaveResponse({ + result: expect.stringMatching(/\[LOG\] hello from console.*\nError: error from page/), + }); expect(await client.callTool({ name: 'browser_snapshot', })).toHaveResponse({ - pageState: expect.stringContaining(`- button \"Submit\" [ref=e2]`), + pageState: expect.stringContaining(`generic [active] [ref=e1]: Hello, world!`), }); }); @@ -522,9 +542,9 @@ test('test_setup_page without location respects testsDir', async ({ startClient expect(fs.existsSync(test.info().outputPath('tests', 'default.seed.spec.ts'))).toBe(true); }); -async function prepareDebugTest(startClient: StartClient) { +async function prepareDebugTest(startClient: StartClient, testFile?: string) { await writeFiles({ - 'a.test.ts': ` + 'a.test.ts': testFile || ` import { test, expect } from '@playwright/test'; test('fail', async ({ page }) => { await page.setContent(''); From da333232e715419990479357916bfc992d81cbf3 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 22 Sep 2025 16:35:11 -0700 Subject: [PATCH 227/329] fix(mcp): snapshot for ai fixes (#37527) --- packages/injected/src/ariaSnapshot.ts | 2 +- tests/page/page-aria-snapshot-ai.spec.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 2dc9e19641a9f..a1e1afd4256af 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -302,7 +302,7 @@ function normalizeGenericRoles(node: AriaNode) { } // Only remove generic that encloses one element, logical grouping still makes sense, even if it is not ref-able. - const removeSelf = node.role === 'generic' && result.length <= 1 && result.every(c => typeof c !== 'string' && !!c.ref); + const removeSelf = node.role === 'generic' && !node.name && result.length <= 1 && result.every(c => typeof c !== 'string' && !!c.ref); if (removeSelf) return result; node.children = result; diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 816ac4a688456..982cc120cb6cf 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -439,3 +439,12 @@ it('should collapse inline generic nodes', async ({ page }) => { - generic [ref=e12]: 1,200 `); }); + +it('should not remove generic nodes with title', async ({ page }) => { + await page.setContent(`
        Element content
        `); + + const snapshot = await snapshotForAI(page); + expect(snapshot).toContainYaml(` + - generic "Element title" [ref=e2] + `); +}); From 72c62d840247d9defd87c6beb0344d456794b570 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 22 Sep 2025 17:04:06 -0700 Subject: [PATCH 228/329] chore: do not use -k option (#37532) --- packages/playwright-core/bin/reinstall_chrome_beta_mac.sh | 2 +- packages/playwright-core/bin/reinstall_chrome_stable_mac.sh | 2 +- packages/playwright-core/bin/reinstall_msedge_beta_mac.sh | 2 +- packages/playwright-core/bin/reinstall_msedge_dev_mac.sh | 2 +- packages/playwright-core/bin/reinstall_msedge_stable_mac.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/playwright-core/bin/reinstall_chrome_beta_mac.sh b/packages/playwright-core/bin/reinstall_chrome_beta_mac.sh index c563c81de95ff..617e3b5ee5bb7 100755 --- a/packages/playwright-core/bin/reinstall_chrome_beta_mac.sh +++ b/packages/playwright-core/bin/reinstall_chrome_beta_mac.sh @@ -4,7 +4,7 @@ set -x rm -rf "/Applications/Google Chrome Beta.app" cd /tmp -curl --retry 3 -o ./googlechromebeta.dmg -k https://dl.google.com/chrome/mac/universal/beta/googlechromebeta.dmg +curl --retry 3 -o ./googlechromebeta.dmg https://dl.google.com/chrome/mac/universal/beta/googlechromebeta.dmg hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechromebeta.dmg ./googlechromebeta.dmg cp -pR "/Volumes/googlechromebeta.dmg/Google Chrome Beta.app" /Applications hdiutil detach /Volumes/googlechromebeta.dmg diff --git a/packages/playwright-core/bin/reinstall_chrome_stable_mac.sh b/packages/playwright-core/bin/reinstall_chrome_stable_mac.sh index 035fa863f161e..6aa650a55a23f 100755 --- a/packages/playwright-core/bin/reinstall_chrome_stable_mac.sh +++ b/packages/playwright-core/bin/reinstall_chrome_stable_mac.sh @@ -4,7 +4,7 @@ set -x rm -rf "/Applications/Google Chrome.app" cd /tmp -curl --retry 3 -o ./googlechrome.dmg -k https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg +curl --retry 3 -o ./googlechrome.dmg https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechrome.dmg ./googlechrome.dmg cp -pR "/Volumes/googlechrome.dmg/Google Chrome.app" /Applications hdiutil detach /Volumes/googlechrome.dmg diff --git a/packages/playwright-core/bin/reinstall_msedge_beta_mac.sh b/packages/playwright-core/bin/reinstall_msedge_beta_mac.sh index c03bb022643f2..72ec3e4e5732d 100755 --- a/packages/playwright-core/bin/reinstall_msedge_beta_mac.sh +++ b/packages/playwright-core/bin/reinstall_msedge_beta_mac.sh @@ -3,7 +3,7 @@ set -e set -x cd /tmp -curl --retry 3 -o ./msedge_beta.pkg -k "$1" +curl --retry 3 -o ./msedge_beta.pkg "$1" # Note: there's no way to uninstall previously installed MSEdge. # However, running PKG again seems to update installation. sudo installer -pkg /tmp/msedge_beta.pkg -target / diff --git a/packages/playwright-core/bin/reinstall_msedge_dev_mac.sh b/packages/playwright-core/bin/reinstall_msedge_dev_mac.sh index 9b664dadab4c6..3376e8696cde7 100755 --- a/packages/playwright-core/bin/reinstall_msedge_dev_mac.sh +++ b/packages/playwright-core/bin/reinstall_msedge_dev_mac.sh @@ -3,7 +3,7 @@ set -e set -x cd /tmp -curl --retry 3 -o ./msedge_dev.pkg -k "$1" +curl --retry 3 -o ./msedge_dev.pkg "$1" # Note: there's no way to uninstall previously installed MSEdge. # However, running PKG again seems to update installation. sudo installer -pkg /tmp/msedge_dev.pkg -target / diff --git a/packages/playwright-core/bin/reinstall_msedge_stable_mac.sh b/packages/playwright-core/bin/reinstall_msedge_stable_mac.sh index 7a72703c3e5e4..afcd2f531b5e4 100755 --- a/packages/playwright-core/bin/reinstall_msedge_stable_mac.sh +++ b/packages/playwright-core/bin/reinstall_msedge_stable_mac.sh @@ -3,7 +3,7 @@ set -e set -x cd /tmp -curl --retry 3 -o ./msedge_stable.pkg -k "$1" +curl --retry 3 -o ./msedge_stable.pkg "$1" # Note: there's no way to uninstall previously installed MSEdge. # However, running PKG again seems to update installation. sudo installer -pkg /tmp/msedge_stable.pkg -target / From c813e9cf595cd4b83f6590afda26b460993231cb Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 23 Sep 2025 11:14:48 -0700 Subject: [PATCH 229/329] feat(mcp): allow saving videos for sessions (#37531) --- .../src/mcp/browser/browserContextFactory.ts | 32 +++++--- packages/playwright/src/mcp/browser/config.ts | 74 ++++++++++++++----- .../playwright/src/mcp/browser/context.ts | 24 +++++- .../playwright/src/mcp/browser/tools/pdf.ts | 2 +- .../src/mcp/browser/tools/screenshot.ts | 2 +- .../playwright/src/mcp/browser/tools/utils.ts | 4 +- packages/playwright/src/mcp/config.d.ts | 8 ++ packages/playwright/src/mcp/program.ts | 5 +- tests/mcp/fixtures.ts | 2 +- tests/mcp/video.spec.ts | 48 ++++++++++++ 10 files changed, 160 insertions(+), 41 deletions(-) create mode 100644 tests/mcp/video.spec.ts diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index 2710e96857506..7e75c7d56e4dd 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -27,7 +27,7 @@ import { outputFile } from './config'; import { firstRootPath } from '../sdk/server'; import type { FullConfig } from './config'; -import type { LaunchOptions } from '../../../../playwright-core/src/client/types'; +import type { LaunchOptions, BrowserContextOptions } from '../../../../playwright-core/src/client/types'; import type { ClientInfo } from '../sdk/server'; export function contextFactory(config: FullConfig): BrowserContextFactory { @@ -42,8 +42,13 @@ export function contextFactory(config: FullConfig): BrowserContextFactory { return new PersistentContextFactory(config); } +export type BrowserContextFactoryResult = { + browserContext: playwright.BrowserContext; + close: (afterClose: () => Promise) => Promise; +}; + export interface BrowserContextFactory { - createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }>; + createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise; } class BaseContextFactory implements BrowserContextFactory { @@ -75,23 +80,27 @@ class BaseContextFactory implements BrowserContextFactory { throw new Error('Not implemented'); } - async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { + async createContext(clientInfo: ClientInfo): Promise { testDebug(`create browser context (${this._logName})`); const browser = await this._obtainBrowser(clientInfo); const browserContext = await this._doCreateContext(browser); await addInitScript(browserContext, this.config.browser.initScript); - return { browserContext, close: () => this._closeBrowserContext(browserContext, browser) }; + return { + browserContext, + close: (afterClose: () => Promise) => this._closeBrowserContext(browserContext, browser, afterClose) + }; } protected async _doCreateContext(browser: playwright.Browser): Promise { throw new Error('Not implemented'); } - private async _closeBrowserContext(browserContext: playwright.BrowserContext, browser: playwright.Browser) { + private async _closeBrowserContext(browserContext: playwright.BrowserContext, browser: playwright.Browser, afterClose: () => Promise) { testDebug(`close browser context (${this._logName})`); if (browser.contexts().length === 1) this._browserPromise = undefined; await browserContext.close().catch(logUnhandledError); + await afterClose(); if (browser.contexts().length === 0) { testDebug(`close browser (${this._logName})`); await browser.close().catch(logUnhandledError); @@ -170,7 +179,7 @@ class PersistentContextFactory implements BrowserContextFactory { this.config = config; } - async createContext(clientInfo: ClientInfo): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { + async createContext(clientInfo: ClientInfo): Promise { await injectCdpPort(this.config.browser); testDebug('create browser context (persistent)'); const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo); @@ -183,7 +192,7 @@ class PersistentContextFactory implements BrowserContextFactory { const browserType = playwright[this.config.browser.browserName]; for (let i = 0; i < 5; i++) { - const launchOptions: LaunchOptions = { + const launchOptions: LaunchOptions & BrowserContextOptions = { tracesDir, ...this.config.browser.launchOptions, ...this.config.browser.contextOptions, @@ -197,7 +206,7 @@ class PersistentContextFactory implements BrowserContextFactory { try { const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions); await addInitScript(browserContext, this.config.browser.initScript); - const close = () => this._closeBrowserContext(browserContext, userDataDir); + const close = (afterClose: () => Promise) => this._closeBrowserContext(browserContext, userDataDir, afterClose); return { browserContext, close }; } catch (error: any) { if (error.message.includes('Executable doesn\'t exist')) @@ -213,10 +222,11 @@ class PersistentContextFactory implements BrowserContextFactory { throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`); } - private async _closeBrowserContext(browserContext: playwright.BrowserContext, userDataDir: string) { + private async _closeBrowserContext(browserContext: playwright.BrowserContext, userDataDir: string, afterClose: () => Promise) { testDebug('close browser context (persistent)'); testDebug('release user data dir', userDataDir); await browserContext.close().catch(() => {}); + await afterClose(); this._userDataDirs.delete(userDataDir); testDebug('close browser context complete (persistent)'); } @@ -270,7 +280,7 @@ async function addInitScript(browserContext: playwright.BrowserContext, initScri } export class SharedContextFactory implements BrowserContextFactory { - private _contextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> | undefined; + private _contextPromise: Promise | undefined; private _baseFactory: BrowserContextFactory; private static _instance: SharedContextFactory | undefined; @@ -313,6 +323,6 @@ export class SharedContextFactory implements BrowserContextFactory { if (!contextPromise) return; const { close } = await contextPromise; - await close(); + await close(async () => {}); } } diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index 1e66feffd79fd..c4a67c986f9b3 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -28,6 +28,8 @@ import type * as playwright from '../../../types/test'; import type { Config, ToolCapability } from '../config'; import type { ClientInfo } from '../sdk/server'; +type ViewportSize = { width: number; height: number }; + export type CLIOptions = { allowedOrigins?: string[]; blockedOrigins?: string[]; @@ -53,6 +55,7 @@ export type CLIOptions = { proxyServer?: string; saveSession?: boolean; saveTrace?: boolean; + saveVideo?: ViewportSize; secrets?: Record; sharedBrowserContext?: boolean; storageState?: string; @@ -60,7 +63,7 @@ export type CLIOptions = { timeoutNavigation?: number; userAgent?: string; userDataDir?: string; - viewportSize?: string; + viewportSize?: ViewportSize; }; export const defaultConfig: FullConfig = { @@ -127,6 +130,8 @@ async function validateConfig(config: FullConfig): Promise { throw new Error(`Init script file does not exist: ${script}`); } } + if (config.sharedBrowserContext && config.saveVideo) + throw new Error('saveVideo is not supported when sharedBrowserContext is true'); } export function configFromCLIOptions(cliOptions: CLIOptions): Config { @@ -183,16 +188,8 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { if (cliOptions.userAgent) contextOptions.userAgent = cliOptions.userAgent; - if (cliOptions.viewportSize) { - try { - const [width, height] = cliOptions.viewportSize.split(',').map(n => +n); - if (isNaN(width) || isNaN(height)) - throw new Error('bad values'); - contextOptions.viewport = { width, height }; - } catch (e) { - throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"'); - } - } + if (cliOptions.viewportSize) + contextOptions.viewport = cliOptions.viewportSize; if (cliOptions.ignoreHttpsErrors) contextOptions.ignoreHTTPSErrors = true; @@ -203,6 +200,14 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { if (cliOptions.grantPermissions) contextOptions.permissions = cliOptions.grantPermissions; + if (cliOptions.saveVideo) { + contextOptions.recordVideo = { + // Videos are moved to output directory on saveAs. + dir: tmpDir(), + size: cliOptions.saveVideo, + }; + } + const result: Config = { browser: { browserName, @@ -225,6 +230,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { }, saveSession: cliOptions.saveSession, saveTrace: cliOptions.saveTrace, + saveVideo: cliOptions.saveVideo, secrets: cliOptions.secrets, sharedBrowserContext: cliOptions.sharedBrowserContext, outputDir: cliOptions.outputDir, @@ -266,13 +272,14 @@ function configFromEnv(): Config { options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS); options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER); options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE); + options.saveVideo = resolutionParser('--save-video', process.env.PLAYWRIGHT_MCP_SAVE_VIDEO); options.secrets = dotenvFileLoader(process.env.PLAYWRIGHT_MCP_SECRETS_FILE); options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE); options.timeoutAction = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION); options.timeoutNavigation = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION); options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT); options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR); - options.viewportSize = envToString(process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE); + options.viewportSize = resolutionParser('--viewport-size', process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE); return configFromCLIOptions(options); } @@ -287,27 +294,35 @@ async function loadConfig(configFile: string | undefined): Promise { } } -export async function outputFile(config: FullConfig, clientInfo: ClientInfo, fileName: string, options: { origin: 'code' | 'llm' | 'web' }): Promise { +function tmpDir(): string { + return path.join(process.env.PW_TMPDIR_FOR_TEST ?? os.tmpdir(), 'playwright-mcp-output'); +} + +export function outputDir(config: FullConfig, clientInfo: ClientInfo): string { const rootPath = firstRootPath(clientInfo); - const outputDir = config.outputDir + return config.outputDir ?? (rootPath ? path.join(rootPath, '.playwright-mcp') : undefined) - ?? path.join(process.env.PW_TMPDIR_FOR_TEST ?? os.tmpdir(), 'playwright-mcp-output', String(clientInfo.timestamp)); + ?? path.join(tmpDir(), String(clientInfo.timestamp)); +} + +export async function outputFile(config: FullConfig, clientInfo: ClientInfo, fileName: string, options: { origin: 'code' | 'llm' | 'web' }): Promise { + const dir = outputDir(config, clientInfo); // Trust code. if (options.origin === 'code') - return path.resolve(outputDir, fileName); + return path.resolve(dir, fileName); // Trust llm to use valid characters in file names. if (options.origin === 'llm') { fileName = fileName.split('\\').join('/'); - const resolvedFile = path.resolve(outputDir, fileName); - if (!resolvedFile.startsWith(path.resolve(outputDir) + path.sep)) + const resolvedFile = path.resolve(dir, fileName); + if (!resolvedFile.startsWith(path.resolve(dir) + path.sep)) throw new Error(`Resolved file path for ${fileName} is outside of the output directory`); return resolvedFile; } // Do not trust web, at all. - return path.join(outputDir, sanitizeForFilePath(fileName)); + return path.join(dir, sanitizeForFilePath(fileName)); } function pickDefined(obj: T | undefined): Partial { @@ -379,6 +394,27 @@ export function numberParser(value: string | undefined): number | undefined { return +value; } +export function resolutionParser(name: string, value: string | undefined): ViewportSize | undefined { + if (!value) + return undefined; + if (value.includes('x')) { + const [width, height] = value.split('x').map(v => +v); + if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) + throw new Error(`Invalid resolution format: use ${name}="800x600"`); + return { width, height }; + } + + // Legacy format + if (value.includes(',')) { + const [width, height] = value.split(',').map(v => +v); + if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) + throw new Error(`Invalid resolution format: use ${name}="800x600"`); + return { width, height }; + } + + throw new Error(`Invalid resolution format: use ${name}="800x600"`); +} + export function headerParser(arg: string | undefined, previous?: Record): Record { if (!arg) return previous || {}; diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts index f06c8155dd7db..3c905a68d106e 100644 --- a/packages/playwright/src/mcp/browser/context.ts +++ b/packages/playwright/src/mcp/browser/context.ts @@ -14,16 +14,19 @@ * limitations under the License. */ +import fs from 'fs'; + import { debug } from 'playwright-core/lib/utilsBundle'; import { logUnhandledError } from '../log'; import { Tab } from './tab'; import { outputFile } from './config'; import * as codegen from './codegen'; +import { dateAsFileName } from './tools/utils'; import type * as playwright from '../../../types/test'; import type { FullConfig } from './config'; -import type { BrowserContextFactory } from './browserContextFactory'; +import type { BrowserContextFactory, BrowserContextFactoryResult } from './browserContextFactory'; import type * as actions from './actions'; import type { SessionLog } from './sessionLog'; import type { Tracing } from '../../../../playwright-core/src/client/tracing'; @@ -42,7 +45,7 @@ export class Context { readonly config: FullConfig; readonly sessionLog: SessionLog | undefined; readonly options: ContextOptions; - private _browserContextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> | undefined; + private _browserContextPromise: Promise | undefined; private _browserContextFactory: BrowserContextFactory; private _tabs: Tab[] = []; private _currentTab: Tab | undefined; @@ -163,7 +166,20 @@ export class Context { await promise.then(async ({ browserContext, close }) => { if (this.config.saveTrace) await browserContext.tracing.stop(); - await close(); + const videos = browserContext.pages().map(page => page.video()).filter(video => !!video); + await close(async () => { + for (const video of videos) { + const name = await this.outputFile(dateAsFileName('webm'), { origin: 'code' }); + const path = await video.path(); + // video.saveAs() does not work for persistent contexts. + try { + if (fs.existsSync(path)) + await fs.promises.rename(path, name); + } catch (e) { + logUnhandledError(e); + } + } + }); }); } @@ -202,7 +218,7 @@ export class Context { return this._browserContextPromise; } - private async _setupBrowserContext(): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { + private async _setupBrowserContext(): Promise { if (this._closeBrowserContextPromise) throw new Error('Another browser context is being closed.'); // TODO: move to the browser context factory to make it based on isolation mode. diff --git a/packages/playwright/src/mcp/browser/tools/pdf.ts b/packages/playwright/src/mcp/browser/tools/pdf.ts index 91d142dcefcbc..a58035e7a2c3d 100644 --- a/packages/playwright/src/mcp/browser/tools/pdf.ts +++ b/packages/playwright/src/mcp/browser/tools/pdf.ts @@ -35,7 +35,7 @@ const pdf = defineTabTool({ }, handle: async (tab, params, response) => { - const fileName = await tab.context.outputFile(params.filename ?? `page-${dateAsFileName()}.pdf`, { origin: 'llm' }); + const fileName = await tab.context.outputFile(params.filename ?? dateAsFileName('pdf'), { origin: 'llm' }); response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`); response.addResult(`Saved page as ${fileName}`); await tab.page.pdf({ path: fileName }); diff --git a/packages/playwright/src/mcp/browser/tools/screenshot.ts b/packages/playwright/src/mcp/browser/tools/screenshot.ts index c3d0c383be4e1..a3df61000beb5 100644 --- a/packages/playwright/src/mcp/browser/tools/screenshot.ts +++ b/packages/playwright/src/mcp/browser/tools/screenshot.ts @@ -51,7 +51,7 @@ const screenshot = defineTabTool({ handle: async (tab, params, response) => { const fileType = params.type || 'png'; - const fileName = await tab.context.outputFile(params.filename ?? `page-${dateAsFileName()}.${fileType}`, { origin: 'llm' }); + const fileName = await tab.context.outputFile(params.filename ?? dateAsFileName(fileType), { origin: 'llm' }); const options: playwright.PageScreenshotOptions = { type: fileType, quality: fileType === 'png' ? undefined : 90, diff --git a/packages/playwright/src/mcp/browser/tools/utils.ts b/packages/playwright/src/mcp/browser/tools/utils.ts index 8f5b3849247db..6adae9428e8e4 100644 --- a/packages/playwright/src/mcp/browser/tools/utils.ts +++ b/packages/playwright/src/mcp/browser/tools/utils.ts @@ -83,7 +83,7 @@ export async function callOnPageNoTrace(page: playwright.Page, callback: (pag return await (page as any)._wrapApiCall(() => callback(page), { internal: true }); } -export function dateAsFileName(): string { +export function dateAsFileName(extension: string): string { const date = new Date(); - return date.toISOString().replace(/[:.]/g, '-'); + return `page-${date.toISOString().replace(/[:.]/g, '-')}.${extension}`; } diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index 66b015ce7f71a..12ec2323bd85e 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -106,6 +106,14 @@ export type Config = { */ saveTrace?: boolean; + /** + * If specified, saves the Playwright video of the session into the output directory. + */ + saveVideo?: { + width: number; + height: number; + }; + /** * Reuse the same browser context between all connected HTTP clients. */ diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index 1af6c0e587ac2..55cf76370c21f 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -16,7 +16,7 @@ import { ProgramOption } from 'playwright-core/lib/utilsBundle'; import * as mcpServer from './sdk/server'; -import { commaSeparatedList, dotenvFileLoader, headerParser, numberParser, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; +import { commaSeparatedList, dotenvFileLoader, headerParser, numberParser, resolutionParser, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; import { setupExitWatchdog } from './browser/watchdog'; import { contextFactory } from './browser/browserContextFactory'; import { ProxyBackend } from './sdk/proxyBackend'; @@ -53,6 +53,7 @@ export function decorateCommand(command: Command, version: string) { .option('--proxy-server ', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"') .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.') .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.') + .option('--save-video ', 'Whether to save the video of the session into the output directory. For example "--save-video=800x600"', resolutionParser.bind(null, '--save-video')) .option('--secrets ', 'path to a file containing secrets in the dotenv format', dotenvFileLoader) .option('--shared-browser-context', 'reuse the same browser context between all connected HTTP clients.') .option('--storage-state ', 'path to the storage state file for isolated sessions.') @@ -60,7 +61,7 @@ export function decorateCommand(command: Command, version: string) { .option('--timeout-navigation ', 'specify navigation timeout in milliseconds, defaults to 60000ms', numberParser) .option('--user-agent ', 'specify user agent string') .option('--user-data-dir ', 'path to the user data directory. If not specified, a temporary directory will be created.') - .option('--viewport-size ', 'specify browser viewport size in pixels, for example "1280, 720"') + .option('--viewport-size ', 'specify browser viewport size in pixels, for example "1280x720"', resolutionParser.bind(null, '--viewport-size')) .addOption(new ProgramOption('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp()) .addOption(new ProgramOption('--vscode', 'VS Code tools.').hideHelp()) .addOption(new ProgramOption('--vision', 'Legacy option, use --caps=vision instead').hideHelp()) diff --git a/tests/mcp/fixtures.ts b/tests/mcp/fixtures.ts index f08d23b086f89..26f239f6be633 100644 --- a/tests/mcp/fixtures.ts +++ b/tests/mcp/fixtures.ts @@ -117,7 +117,7 @@ export const test = serverTest.extend { - if (process.env.PWMCP_DEBUG) + if (process.env.PWDEBUGIMPL) process.stderr.write(data); stderrBuffer += data.toString(); }); diff --git a/tests/mcp/video.spec.ts b/tests/mcp/video.spec.ts new file mode 100644 index 0000000000000..13c5f1898fbab --- /dev/null +++ b/tests/mcp/video.spec.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import { test, expect } from './fixtures'; + +for (const mode of ['isolated', 'persistent']) { + test(`should work with --save-video (${mode})`, async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + + const { client } = await startClient({ + args: [ + '--save-video=800x600', + ...(mode === 'isolated' ? ['--isolated'] : []), + '--output-dir', outputDir, + ], + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + expect(await client.callTool({ + name: 'browser_close', + })).toHaveResponse({ + code: expect.stringContaining(`page.close()`), + }); + + const [file] = await fs.promises.readdir(outputDir); + expect(file).toMatch(/page-.*.webm/); + }); +} From 6bbb2b42cc893677802361a48ef411d61cf66d21 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 23 Sep 2025 20:25:35 +0100 Subject: [PATCH 230/329] Revert "feat(mcp): respond with snapshot diff when beneficial (#37500)" (#37537) --- .../src/utils/isomorphic/ariaSnapshot.ts | 113 +------ .../playwright/src/mcp/browser/response.ts | 27 +- packages/playwright/src/mcp/browser/tab.ts | 40 +-- .../src/mcp/browser/tools/snapshot.ts | 2 +- .../library/unit/parse-aria-snapshot.spec.ts | 309 ------------------ tests/mcp/click.spec.ts | 16 +- tests/mcp/dialogs.spec.ts | 11 +- tests/mcp/session-log.spec.ts | 1 + tests/mcp/snapshot-diff.spec.ts | 163 --------- tests/mcp/tabs.spec.ts | 12 +- tests/mcp/wait.spec.ts | 20 +- 11 files changed, 46 insertions(+), 668 deletions(-) delete mode 100644 tests/library/unit/parse-aria-snapshot.spec.ts delete mode 100644 tests/mcp/snapshot-diff.spec.ts diff --git a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts index 86050018f760a..21c5f9a35956d 100644 --- a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts +++ b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts @@ -43,13 +43,9 @@ export type AriaTextValue = { normalized: string; }; -// Character offsets in the parsed source. -type SourceRange = { from: number, to: number }; - export type AriaTemplateTextNode = { kind: 'text'; text: AriaTextValue; - sourceRange?: SourceRange; }; export type AriaTemplateRoleNode = AriaProps & { @@ -59,8 +55,6 @@ export type AriaTemplateRoleNode = AriaProps & { children?: AriaTemplateNode[]; props?: Record; containerMode?: 'contain' | 'equal' | 'deep-equal'; - sourceRange?: SourceRange; - subtreeSourceRange?: SourceRange; // only present when different from sourceRange }; export type AriaTemplateNode = AriaTemplateRoleNode | AriaTemplateTextNode; @@ -76,7 +70,7 @@ type YamlLibrary = { }; type ParsedYamlPosition = { line: number; col: number; }; -type ParsingOptions = yamlTypes.ParseOptions & { laxProps?: boolean }; +type ParsingOptions = yamlTypes.ParseOptions; export type ParsedYamlError = { message: string; @@ -104,13 +98,6 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars return [lineCounter.linePos(range[0]), lineCounter.linePos(range[1])]; }; - type WithYamlRange = { range?: yamlTypes.Range | null }; - const computeRange = (firstToken: WithYamlRange, lastToken: WithYamlRange): SourceRange | undefined => { - if (!firstToken.range) - return; - return { from: firstToken.range[0], to: lastToken.range ? lastToken.range[2] : firstToken.range[2] }; - }; - const addError = (error: yamlTypes.YAMLError) => { errors.push({ message: error.message, @@ -124,7 +111,6 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars if (itemIsString) { const childNode = KeyParser.parse(item, parseOptions, errors); if (childNode) { - childNode.sourceRange = computeRange(item, item); container.children = container.children || []; container.children.push(childNode); } @@ -170,8 +156,7 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars } container.children.push({ kind: 'text', - text: textValue(value.value), - sourceRange: computeRange(key, value), + text: textValue(value.value) }); continue; } @@ -226,11 +211,8 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars ...childNode, children: [{ kind: 'text', - text: textValue(String(value.value)), - sourceRange: computeRange(value, value), - }], - sourceRange: computeRange(key, key), - subtreeSourceRange: computeRange(key, value), + text: textValue(String(value.value)) + }] }); continue; } @@ -240,8 +222,6 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars const valueIsSequence = value instanceof yaml.YAMLSeq; if (valueIsSequence) { container.children.push(childNode); - childNode.sourceRange = computeRange(key, key); - childNode.subtreeSourceRange = computeRange(key, value); convertSeq(childNode, value as yamlTypes.YAMLSeq); continue; } @@ -253,13 +233,7 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: Pars } }; - const emptyRange: WithYamlRange = { range: [0, 0, 0] }; // Fragment has no "self" source, only subtree. - const fragment: AriaTemplateNode = { - kind: 'role', - role: 'fragment', - sourceRange: computeRange(emptyRange, emptyRange), - subtreeSourceRange: computeRange(emptyRange, yamlDoc), - }; + const fragment: AriaTemplateNode = { kind: 'role', role: 'fragment' }; yamlDoc.errors.forEach(addError); if (errors.length) @@ -298,14 +272,13 @@ export function textValue(value: string): AriaTextValue { } export class KeyParser { - private _options: ParsingOptions; private _input: string; private _pos: number; private _length: number; static parse(text: yamlTypes.Scalar, options: ParsingOptions, errors: ParsedYamlError[]): AriaTemplateRoleNode | null { try { - return new KeyParser(text.value, options)._parse(); + return new KeyParser(text.value)._parse(); } catch (e) { if (e instanceof ParserError) { const message = options.prettyErrors === false ? e.message : e.message + ':\n\n' + text.value + '\n' + ' '.repeat(e.pos) + '^\n'; @@ -319,8 +292,7 @@ export class KeyParser { } } - constructor(input: string, options: ParsingOptions) { - this._options = options; + constructor(input: string) { this._input = input; this._pos = 0; this._length = input.length; @@ -503,11 +475,6 @@ export class KeyParser { node.selected = value === 'true'; return; } - if (this._options.laxProps) { - node.props = node.props || {}; - node.props[key] = textValue(value); - return; - } this._assert(false, `Unsupported attribute [${key}]`, errorPos); } @@ -525,69 +492,3 @@ export class ParserError extends Error { this.pos = pos; } } - -type AriaSnapshotDiffResult = 'equal' | 'different' | { ref: string, newSource: string }[]; - -export function diffAriaSnapshots(yaml: YamlLibrary, oldSnapshot: string, newSnapshot: string): AriaSnapshotDiffResult { - const diffTree = (oldNode: AriaTemplateNode, newNode: AriaTemplateNode): AriaSnapshotDiffResult => { - if (!oldNode.sourceRange || !newNode.sourceRange) - return 'different'; - - const oldSelfSource = oldSnapshot.slice(oldNode.sourceRange.from, oldNode.sourceRange.to); - const newSelfSource = newSnapshot.slice(newNode.sourceRange.from, newNode.sourceRange.to); - if (oldNode.kind !== 'role' || newNode.kind !== 'role') - return (oldNode.kind === newNode.kind && oldSelfSource === newSelfSource) ? 'equal' : 'different'; - - const newNodeSubtreeSourceRange = newNode.subtreeSourceRange || newNode.sourceRange; - const newSubtreeSource = newSnapshot.slice(newNodeSubtreeSourceRange.from, newNodeSubtreeSourceRange.to); - - const oldChildren = oldNode.children || []; - const newChildren = newNode.children || []; - const childrenDiffs = []; - // When "self" is the same, we can diff children and try to find a small diff there. - let useChildrenDiffs = oldSelfSource === newSelfSource && oldChildren.length === newChildren.length; - let childrenTotalLength = 0; - - if (useChildrenDiffs) { - for (let i = 0; i < oldChildren.length; i++) { - const childDiff = diffTree(oldChildren[i], newChildren[i]); - if (childDiff === 'equal') - continue; - if (childDiff === 'different') { - useChildrenDiffs = false; - break; - } - for (const diff of childDiff) { - childrenTotalLength += diff.newSource.length; - childrenDiffs.push(diff); - } - } - } - - if (childrenDiffs.length > 1 && childrenTotalLength * 2 >= newSubtreeSource.length) { - // Too many children diffs without too much of a benefit. - useChildrenDiffs = false; - } - - if (useChildrenDiffs) { - if (!childrenDiffs.length) - return 'equal'; - return childrenDiffs; - } - - const oldRef = oldNode.props?.ref?.raw; - const newRef = newNode.props?.ref?.raw; - if (!oldRef || oldRef !== newRef) - return 'different'; - - return [{ ref: oldRef, newSource: newSubtreeSource }]; - }; - - try { - const oldParsed = parseAriaSnapshotUnsafe(yaml, oldSnapshot, { laxProps: true }); - const newParsed = parseAriaSnapshotUnsafe(yaml, newSnapshot, { laxProps: true }); - return diffTree(oldParsed, newParsed); - } catch { - return 'different'; - } -} diff --git a/packages/playwright/src/mcp/browser/response.ts b/packages/playwright/src/mcp/browser/response.ts index 348f03e355f25..db910cc2227c8 100644 --- a/packages/playwright/src/mcp/browser/response.ts +++ b/packages/playwright/src/mcp/browser/response.ts @@ -25,7 +25,7 @@ export class Response { private _code: string[] = []; private _images: { contentType: string, data: Buffer }[] = []; private _context: Context; - private _includeSnapshot: 'full' | 'partial' | 'none' = 'none'; + private _includeSnapshot = false; private _includeTabs = false; private _tabSnapshot: TabSnapshot | undefined; @@ -72,8 +72,8 @@ export class Response { return this._images; } - setIncludeSnapshot(full?: 'full') { - this._includeSnapshot = full ?? 'partial'; + setIncludeSnapshot() { + this._includeSnapshot = true; } setIncludeTabs() { @@ -83,7 +83,7 @@ export class Response { async finish() { // All the async snapshotting post-action is happening here. // Everything below should race against modal states. - if (this._includeSnapshot !== 'none' && this._context.currentTab()) + if (this._includeSnapshot && this._context.currentTab()) this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot(); for (const tab of this._context.tabs()) await tab.updateTitle(); @@ -113,7 +113,7 @@ ${this._code.join('\n')} } // List browser tabs. - if (this._includeSnapshot !== 'none' || this._includeTabs) + if (this._includeSnapshot || this._includeTabs) response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs)); // Add snapshot if provided. @@ -121,7 +121,7 @@ ${this._code.join('\n')} response.push(...renderModalStates(this._context, this._tabSnapshot.modalStates)); response.push(''); } else if (this._tabSnapshot) { - response.push(renderTabSnapshot(this._tabSnapshot, this._includeSnapshot === 'full')); + response.push(renderTabSnapshot(this._tabSnapshot)); response.push(''); } @@ -153,7 +153,7 @@ ${this._code.join('\n')} } } -function renderTabSnapshot(tabSnapshot: TabSnapshot, fullSnapshot: boolean): string { +function renderTabSnapshot(tabSnapshot: TabSnapshot): string { const lines: string[] = []; if (tabSnapshot.consoleMessages.length) { @@ -177,15 +177,10 @@ function renderTabSnapshot(tabSnapshot: TabSnapshot, fullSnapshot: boolean): str lines.push(`### Page state`); lines.push(`- Page URL: ${tabSnapshot.url}`); lines.push(`- Page Title: ${tabSnapshot.title}`); - if (!fullSnapshot && tabSnapshot.formattedAriaSnapshotDiff) { - lines.push(`- Page Snapshot Diff:`); - lines.push(tabSnapshot.formattedAriaSnapshotDiff); - } else { - lines.push(`- Page Snapshot:`); - lines.push('```yaml'); - lines.push(tabSnapshot.ariaSnapshot); - lines.push('```'); - } + lines.push(`- Page Snapshot:`); + lines.push('```yaml'); + lines.push(tabSnapshot.ariaSnapshot); + lines.push('```'); return lines.join('\n'); } diff --git a/packages/playwright/src/mcp/browser/tab.ts b/packages/playwright/src/mcp/browser/tab.ts index 44a855b444755..45456d86c61bf 100644 --- a/packages/playwright/src/mcp/browser/tab.ts +++ b/packages/playwright/src/mcp/browser/tab.ts @@ -16,8 +16,7 @@ import { EventEmitter } from 'events'; import * as playwright from 'playwright-core'; -import { ManualPromise, diffAriaSnapshots } from 'playwright-core/lib/utils'; -import { yaml } from 'playwright-core/lib/utilsBundle'; +import { ManualPromise } from 'playwright-core/lib/utils'; import { callOnPageNoTrace, waitForCompletion } from './tools/utils'; import { logUnhandledError } from '../log'; @@ -41,7 +40,6 @@ export type TabSnapshot = { url: string; title: string; ariaSnapshot: string; - formattedAriaSnapshotDiff?: string; modalStates: ModalState[]; consoleMessages: ConsoleMessage[]; downloads: { download: playwright.Download, finished: boolean, outputFile: string }[]; @@ -57,7 +55,6 @@ export class Tab extends EventEmitter { private _onPageClose: (tab: Tab) => void; private _modalStates: ModalState[] = []; private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = []; - private _lastAriaSnapshot: string | undefined; constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) { super(); @@ -66,14 +63,7 @@ export class Tab extends EventEmitter { this._onPageClose = onPageClose; page.on('console', event => this._handleConsoleMessage(messageToConsoleMessage(event))); page.on('pageerror', error => this._handleConsoleMessage(pageErrorToConsoleMessage(error))); - page.on('request', request => { - this._requests.set(request, null); - // Note: unfortunately, 'framenavigated' event does not differentiate between - // a same-document (#hash or pushState) navigation and a new document being loaded. - // Relying upon a navigation request heuristic. - if (request.frame() === page.mainFrame() && request.isNavigationRequest()) - this._willNavigateMainFrameToNewDocument(); - }); + page.on('request', request => this._requests.set(request, null)); page.on('response', response => this._requests.set(response.request(), response)); page.on('close', () => this._onClose()); page.on('filechooser', chooser => { @@ -168,10 +158,6 @@ export class Tab extends EventEmitter { this._onPageClose(this); } - private _willNavigateMainFrameToNewDocument() { - this._lastAriaSnapshot = undefined; - } - async updateTitle() { await this._raceAgainstModalStates(async () => { this._lastTitle = await callOnPageNoTrace(this.page, page => page.title()); @@ -192,7 +178,6 @@ export class Tab extends EventEmitter { async navigate(url: string) { this._clearCollectedArtifacts(); - this._willNavigateMainFrameToNewDocument(); const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(logUnhandledError)); try { @@ -236,7 +221,6 @@ export class Tab extends EventEmitter { url: this.page.url(), title: await this.page.title(), ariaSnapshot: snapshot, - formattedAriaSnapshotDiff: this._lastAriaSnapshot ? generateAriaSnapshotDiff(this._lastAriaSnapshot, snapshot) : undefined, modalStates: [], consoleMessages: [], downloads: this._downloads, @@ -246,7 +230,6 @@ export class Tab extends EventEmitter { // Assign console message late so that we did not lose any to modal state. tabSnapshot.consoleMessages = this._recentConsoleMessages; this._recentConsoleMessages = []; - this._lastAriaSnapshot = tabSnapshot.ariaSnapshot; } return tabSnapshot ?? { url: this.page.url(), @@ -347,22 +330,3 @@ export function renderModalStates(context: Context, modalStates: ModalState[]): } const tabSymbol = Symbol('tabSymbol'); - -function generateAriaSnapshotDiff(oldSnapshot: string, newSnapshot: string) { - const diffs = diffAriaSnapshots(yaml, oldSnapshot, newSnapshot); - if (diffs === 'equal') - return ''; - if (diffs === 'different') - return; - if (diffs.length > 3 || diffs.some(diff => diff.newSource.split('\n').length > 100)) { - // Being conservative - up to 3 small diff changes, otherwise include full snapshot. - return; - } - const lines = [`The following refs have changed`]; - for (const diff of diffs) - lines.push('', '```yaml', diff.newSource.trimEnd(), '```'); - const combined = lines.join('\n'); - if (combined.length >= newSnapshot.length) - return; - return combined; -} diff --git a/packages/playwright/src/mcp/browser/tools/snapshot.ts b/packages/playwright/src/mcp/browser/tools/snapshot.ts index c7e7cc539f518..bea116ee8fb5e 100644 --- a/packages/playwright/src/mcp/browser/tools/snapshot.ts +++ b/packages/playwright/src/mcp/browser/tools/snapshot.ts @@ -31,7 +31,7 @@ const snapshot = defineTool({ handle: async (context, params, response) => { await context.ensureTab(); - response.setIncludeSnapshot('full'); + response.setIncludeSnapshot(); }, }); diff --git a/tests/library/unit/parse-aria-snapshot.spec.ts b/tests/library/unit/parse-aria-snapshot.spec.ts deleted file mode 100644 index 9aac6bee1634e..0000000000000 --- a/tests/library/unit/parse-aria-snapshot.spec.ts +++ /dev/null @@ -1,309 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from '@playwright/test'; -import { parseAriaSnapshotUnsafe, diffAriaSnapshots } from '../../../packages/playwright-core/lib/utils/isomorphic/ariaSnapshot.js'; -import { yaml } from 'playwright-core/lib/utilsBundle'; - -function addSource(text: string, node: any) { - if (node.sourceRange) - node.source = text.slice(node.sourceRange.from, node.sourceRange.to).trim(); - if (node.subtreeSourceRange) - node.subtreeSource = text.slice(node.subtreeSourceRange.from, node.subtreeSourceRange.to).trim(); - (node.children || []).forEach(child => addSource(text, child)); -} - -function parse(text: string) { - const result = parseAriaSnapshotUnsafe(yaml, text, { laxProps: true }); - addSource(text, result); - return result; -} - -function diff(oldSnapshot: string, newSnapshot: string) { - const diffs = diffAriaSnapshots(yaml, oldSnapshot, newSnapshot); - if (Array.isArray(diffs)) - diffs.forEach(diff => diff.newSource = diff.newSource.trimEnd()); - return diffs; -} - -test('parse aria snapshot returns source ranges', () => { - const listSnapshot = `list "list name" [ref=e1]: - - listitem: item 1 - - listitem "item 2": - - button "click me" [ref=e2] - - text: hello - - listitem: - - generic: text1 - - generic [ref=e3] [active] [cursor=pointer]: text2`; - - const snapshot = ` -- ${listSnapshot} -- button "another button" [ref=e4] -`; - - expect(parse(snapshot)).toEqual(expect.objectContaining({ - role: 'fragment', - source: '', - subtreeSource: snapshot.trim(), - children: [ - expect.objectContaining({ - role: 'list', - name: 'list name', - props: { ref: expect.objectContaining({ raw: 'e1' }) }, - source: 'list "list name" [ref=e1]', - subtreeSource: listSnapshot, - children: [ - expect.objectContaining({ - role: 'listitem', - source: 'listitem', - subtreeSource: 'listitem: item 1', - children: [ - expect.objectContaining({ - kind: 'text', - text: expect.objectContaining({ raw: 'item 1' }), - source: 'item 1', - }), - ], - }), - expect.objectContaining({ - role: 'listitem', - name: 'item 2', - source: 'listitem "item 2"', - subtreeSource: 'listitem "item 2":\n - button "click me" [ref=e2]\n - text: hello', - children: [ - expect.objectContaining({ - role: 'button', - name: 'click me', - props: { ref: expect.objectContaining({ raw: 'e2' }) }, - source: 'button "click me" [ref=e2]', - }), - expect.objectContaining({ - kind: 'text', - text: expect.objectContaining({ raw: 'hello' }), - source: 'text: hello', - }), - ], - }), - expect.objectContaining({ - role: 'listitem', - source: 'listitem', - subtreeSource: 'listitem:\n - generic: text1\n - generic [ref=e3] [active] [cursor=pointer]: text2', - children: [ - expect.objectContaining({ - role: 'generic', - source: 'generic', - subtreeSource: 'generic: text1', - children: [ - expect.objectContaining({ - kind: 'text', - text: expect.objectContaining({ raw: 'text1' }), - source: 'text1', - }), - ], - }), - expect.objectContaining({ - role: 'generic', - props: { ref: expect.objectContaining({ raw: 'e3' }), cursor: expect.objectContaining({ raw: 'pointer' }) }, - active: true, - source: 'generic [ref=e3] [active] [cursor=pointer]', - subtreeSource: 'generic [ref=e3] [active] [cursor=pointer]: text2', - children: [ - expect.objectContaining({ - kind: 'text', - text: expect.objectContaining({ raw: 'text2' }), - source: 'text2', - }), - ], - }), - ], - }), - ], - }), - expect.objectContaining({ - role: 'button', - name: 'another button', - props: { ref: expect.objectContaining({ raw: 'e4' }) }, - source: 'button "another button" [ref=e4]', - }), - ], - })); -}); - -test('diff: equal', () => { - expect(diff(`- button "name"`, `- button "name"`)).toBe('equal'); -}); - -test('diff: different', () => { - expect(diff(`- button "name"`, `- button "new name"`)).toBe('different'); -}); - -test('diff: one ref', () => { - expect(diff(` -- button "button 1" [ref=e1] -`, ` -- button "button 2" [ref=e1] -`)).toEqual([ - { - ref: 'e1', - newSource: 'button "button 2" [ref=e1]', - } - ]); -}); - -test('diff: no ref upgrades to parent', () => { - expect(diff(` -- listitem [ref=e6]: - - generic: some text -`, ` -- listitem [ref=e6]: - - generic: some text has changed -`)).toEqual([ - { - ref: 'e6', - newSource: `listitem [ref=e6]: - - generic: some text has changed`, - } - ]); -}); - -test('diff: two refs in a large parent', () => { - expect(diff(` -- listitem [ref=e6]: some text -- listitem -- listitem -- listitem -- listitem -- listitem -- listitem -- listitem -- listitem [ref=e7]: other text -`, ` -- listitem [ref=e6]: some new text -- listitem -- listitem -- listitem -- listitem -- listitem -- listitem -- listitem -- listitem [ref=e7]: other new text -`)).toEqual([ - { - ref: 'e6', - newSource: `listitem [ref=e6]: some new text`, - }, - { - ref: 'e7', - newSource: `listitem [ref=e7]: other new text`, - }, - ]); -}); - -test('diff: mixed', () => { - const snapshot1 = ` - - button "button 1" [ref=e1] [active] - - button "padding" - - button "button 2" [ref=e2] - - button "padding" - - button "padding" - - list "list name" [ref=e3]: - - listitem [ref=e4]: item 1 - - listitem "item 2" [ref=e5] - - button "padding" - - button "padding" - - button "padding" - - button "padding" - - listitem [ref=e6]: - - generic: some text - - button "padding" - - button "padding" - - button "padding" - - button "padding" - - generic: - - listitem [ref=e10]: more text - - listitem "item name" [ref=e7] - - listitem [ref=e8]: - - listitem "deep item" [ref=e9] - `; - const snapshot2 = ` - - button "button 1" [ref=e1] - - button "padding" - - button "button 2" [ref=e2] [active] - - button "padding" - - button "padding" - - list "list name" [ref=e3]: - - listitem [ref=e4]: item 1 - - listitem "item 2" [ref=e5]: - - button "new button 1" [ref=e100] - - button "padding" - - button "padding" - - button "padding" - - button "padding" - - listitem [ref=e6]: - - generic: some text has changed - - button "padding" - - button "padding" - - button "padding" - - button "padding" - - generic: - - listitem [ref=e10]: more text has changed - - listitem "new item name" [ref=e7] - - listitem [ref=e8]: - - listitem "new ref" [ref=e101] - `; - expect(diff(snapshot1, snapshot2)).toEqual([ - { ref: 'e1', newSource: 'button "button 1" [ref=e1]' }, - { ref: 'e2', newSource: 'button "button 2" [ref=e2] [active]' }, - { ref: 'e5', newSource: `listitem "item 2" [ref=e5]: - - button "new button 1" [ref=e100]` }, - { ref: 'e6', newSource: `listitem [ref=e6]: - - generic: some text has changed` }, - { ref: 'e10', newSource: 'listitem [ref=e10]: more text has changed' }, - { ref: 'e7', newSource: `listitem "new item name" [ref=e7]` }, - { ref: 'e8', newSource: `listitem [ref=e8]: - - listitem "new ref" [ref=e101]` }, - ]); -}); - -test('diff: two refs in a small parent are combined', () => { - expect(diff(` -- button "name" -- list [ref=e1]: - - listitem [ref=e6]: some text - - listitem - - listitem - - listitem - - listitem [ref=e7]: other text -`, ` -- button "name" -- list [ref=e1]: - - listitem [ref=e6]: some new text - - listitem - - listitem - - listitem - - listitem [ref=e7]: other new text -`)).toEqual([ - { - ref: 'e1', - newSource: `list [ref=e1]: - - listitem [ref=e6]: some new text - - listitem - - listitem - - listitem - - listitem [ref=e7]: other new text`, - }, - ]); -}); diff --git a/tests/mcp/click.spec.ts b/tests/mcp/click.spec.ts index 169d57dca4b8e..6f6a06dbd017c 100644 --- a/tests/mcp/click.spec.ts +++ b/tests/mcp/click.spec.ts @@ -16,16 +16,10 @@ import { test, expect } from './fixtures'; -test('browser_click', async ({ client, server }) => { +test('browser_click', async ({ client, server, mcpBrowser }) => { server.setContent('/', ` Title - `, 'text/html'); await client.callTool({ @@ -41,7 +35,7 @@ test('browser_click', async ({ client, server }) => { }, })).toHaveResponse({ code: `await page.getByRole('button', { name: 'Submit' }).click();`, - pageState: expect.stringContaining(`- button "Submit" [active] [ref=e2]`), + pageState: expect.stringContaining(`- button "Submit" ${mcpBrowser !== 'webkit' || process.platform === 'linux' ? '[active] ' : ''}[ref=e2]`), }); }); @@ -131,7 +125,7 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => { }, })).toHaveResponse({ code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Control'] });`, - pageState: expect.stringContaining(`generic [ref=e3]: ctrlKey:true metaKey:false shiftKey:false altKey:false`), + pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:true metaKey:false shiftKey:false altKey:false`), }); } @@ -144,7 +138,7 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => { }, })).toHaveResponse({ code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Shift'] });`, - pageState: expect.stringContaining(`generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:false`), + pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:false`), }); expect(await client.callTool({ @@ -156,6 +150,6 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => { }, })).toHaveResponse({ code: `await page.getByRole('button', { name: 'Submit' }).click({ modifiers: ['Shift', 'Alt'] });`, - pageState: expect.stringContaining(`generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:true`), + pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:true`), }); }); diff --git a/tests/mcp/dialogs.spec.ts b/tests/mcp/dialogs.spec.ts index fa2be5cc738e4..4c2bf98eb5025 100644 --- a/tests/mcp/dialogs.spec.ts +++ b/tests/mcp/dialogs.spec.ts @@ -17,7 +17,7 @@ import { test, expect } from './fixtures'; test('alert dialog', async ({ client, server }) => { - server.setContent('/', `Title`, 'text/html'); + server.setContent('/', ``, 'text/html'); expect(await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX }, @@ -54,7 +54,7 @@ test('alert dialog', async ({ client, server }) => { }, })).toHaveResponse({ modalState: undefined, - pageState: expect.stringContaining(`Page Title: Title`), + pageState: expect.stringContaining(`- button "Button"`), }); }); @@ -218,7 +218,7 @@ test('prompt dialog', async ({ client, server }) => { }); test('alert dialog w/ race', async ({ client, server }) => { - server.setContent('/', `Title`, 'text/html'); + server.setContent('/', ``, 'text/html'); expect(await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX }, @@ -247,6 +247,9 @@ test('alert dialog w/ race', async ({ client, server }) => { expect(result).toHaveResponse({ modalState: undefined, pageState: expect.stringContaining(`- Page URL: ${server.PREFIX}/ -- Page Title: Title`), +- Page Title: +- Page Snapshot: +\`\`\`yaml +- button "Button"`), }); }); diff --git a/tests/mcp/session-log.spec.ts b/tests/mcp/session-log.spec.ts index 7e072aa55bad3..6f9a98ee66cb6 100644 --- a/tests/mcp/session-log.spec.ts +++ b/tests/mcp/session-log.spec.ts @@ -42,6 +42,7 @@ test('session log should record tool calls', async ({ startClient, server }, tes }, })).toHaveResponse({ code: `await page.getByRole('button', { name: 'Submit' }).click();`, + pageState: expect.stringContaining(`- button "Submit"`), }); const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0]; diff --git a/tests/mcp/snapshot-diff.spec.ts b/tests/mcp/snapshot-diff.spec.ts deleted file mode 100644 index f5ca5346c0536..0000000000000 --- a/tests/mcp/snapshot-diff.spec.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './fixtures'; - -test('should return aria snapshot diff', async ({ client, server }) => { - server.setContent('/', ` - - -
          - - `, 'text/html'); - - const listitems = new Array(100).fill(0).map((_, i) => `\n - listitem [ref=e${5 + i}]: Filler ${i}`).join(''); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX, - }, - })).toHaveResponse({ - pageState: expect.stringContaining(` - - button "Button 1" [ref=e2] - - button "Button 2" [active] [ref=e3] - - list [ref=e4]:${listitems}`), - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button 2', - ref: 'e3', - }, - })).toHaveResponse({ - pageState: expect.stringContaining(` -- Page Snapshot Diff: -`), - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button 1', - ref: 'e2', - }, - })).toHaveResponse({ - pageState: expect.stringContaining(` -- Page Snapshot Diff: -The following refs have changed - -\`\`\`yaml -button "Button 1" [active] [ref=e2] -\`\`\` - -\`\`\`yaml -button "Button 2" [ref=e3] -\`\`\` - -\`\`\`yaml -listitem [ref=e5]: - - text: Filler 0 - - button "new button" [ref=e105] -\`\`\``), - }); - - expect(await client.callTool({ - name: 'browser_snapshot', - })).toHaveResponse({ - pageState: expect.stringContaining(` -- Page Snapshot:`), - }); -}); - -test('should reset aria snapshot diff upon navigation', async ({ client, server }) => { - server.setContent('/before', ` - - -
            - - `, 'text/html'); - - server.setContent('/after', ` - - -
              - - `, 'text/html'); - - expect(await client.callTool({ - name: 'browser_navigate', - arguments: { - url: server.PREFIX + '/before', - }, - })).toHaveResponse({ - pageState: expect.stringContaining(` - - button "Button 1" [ref=e2] - - button "Button 2" [active] [ref=e3]`), - }); - - expect(await client.callTool({ - name: 'browser_click', - arguments: { - element: 'Button 1', - ref: 'e2', - }, - })).toHaveResponse({ - pageState: expect.stringContaining(` -- Page Snapshot:`), - }); -}); diff --git a/tests/mcp/tabs.spec.ts b/tests/mcp/tabs.spec.ts index bc59f2d7a5710..a0b40d824b783 100644 --- a/tests/mcp/tabs.spec.ts +++ b/tests/mcp/tabs.spec.ts @@ -98,8 +98,10 @@ test('select tab', async ({ client }) => { - 2: [Tab two] (data:text/html,Tab twoBody two)`, pageState: expect.stringContaining(`- Page URL: data:text/html,Tab oneBody one - Page Title: Tab one -- Page Snapshot Diff: -`), +- Page Snapshot: +\`\`\`yaml +- generic [active] [ref=e1]: Body one +\`\`\``), }); expect(await client.callTool({ @@ -131,8 +133,10 @@ test('close tab', async ({ client }) => { - 1: (current) [Tab one] (data:text/html,Tab oneBody one)`, pageState: expect.stringContaining(`- Page URL: data:text/html,Tab oneBody one - Page Title: Tab one -- Page Snapshot Diff: -`), +- Page Snapshot: +\`\`\`yaml +- generic [active] [ref=e1]: Body one +\`\`\``), }); }); diff --git a/tests/mcp/wait.spec.ts b/tests/mcp/wait.spec.ts index e844eac4ea89d..0388faf259b57 100644 --- a/tests/mcp/wait.spec.ts +++ b/tests/mcp/wait.spec.ts @@ -31,11 +31,9 @@ test('browser_wait_for(text)', async ({ client, server }) => { `, 'text/html'); - expect(await client.callTool({ + await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- generic [ref=e3]: Text to disappear`), }); await client.callTool({ @@ -46,14 +44,10 @@ test('browser_wait_for(text)', async ({ client, server }) => { }, }); - await client.callTool({ + expect(await client.callTool({ name: 'browser_wait_for', arguments: { text: 'Text to appear' }, code: `await page.getByText("Text to appear").first().waitFor({ state: 'visible' });`, - }); - - expect(await client.callTool({ - name: 'browser_snapshot', })).toHaveResponse({ pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`), }); @@ -74,11 +68,9 @@ test('browser_wait_for(textGone)', async ({ client, server }) => { `, 'text/html'); - expect(await client.callTool({ + await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX }, - })).toHaveResponse({ - pageState: expect.stringContaining(`- generic [ref=e3]: Text to disappear`), }); await client.callTool({ @@ -89,14 +81,10 @@ test('browser_wait_for(textGone)', async ({ client, server }) => { }, }); - await client.callTool({ + expect(await client.callTool({ name: 'browser_wait_for', arguments: { textGone: 'Text to disappear' }, code: `await page.getByText("Text to disappear").first().waitFor({ state: 'hidden' });`, - }); - - expect(await client.callTool({ - name: 'browser_snapshot', })).toHaveResponse({ pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`), }); From bbc71098fe8d0adca173316fcf31d2201fe11c9c Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 23 Sep 2025 20:26:26 +0100 Subject: [PATCH 231/329] chore: reduce plumbing for pauseAtEnd/pauseOnError (#37515) --- .../src/client/channelOwner.ts | 6 +- .../src/client/clientInstrumentation.ts | 5 - packages/playwright/src/index.ts | 62 +----------- .../playwright/src/matchers/toBeTruthy.ts | 3 - packages/playwright/src/matchers/toEqual.ts | 3 - .../playwright/src/matchers/toMatchText.ts | 4 - .../playwright/src/mcp/test/browserBackend.ts | 58 +++++------ packages/playwright/src/worker/testInfo.ts | 2 +- packages/playwright/src/worker/workerMain.ts | 5 +- tests/mcp/test-tools.spec.ts | 99 +++++++++++++++++-- 10 files changed, 127 insertions(+), 120 deletions(-) diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index 26bc4bab7a9a7..7bd097b3ff486 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -20,7 +20,7 @@ import { methodMetainfo } from '../utils/isomorphic/protocolMetainfo'; import { captureLibraryStackTrace } from './clientStackTrace'; import { stringifyStackFrames } from '../utils/isomorphic/stackTrace'; -import type { ClientInstrumentation, RecoverFromApiErrorHandler } from './clientInstrumentation'; +import type { ClientInstrumentation } from './clientInstrumentation'; import type { Connection } from './connection'; import type { Logger } from './types'; import type { ValidatorContext } from '../protocol/validator'; @@ -199,11 +199,7 @@ export abstract class ChannelOwner Promise; - export interface ClientInstrumentation { addListener(listener: ClientInstrumentationListener): void; removeListener(listener: ClientInstrumentationListener): void; removeAllListeners(): void; onApiCallBegin(apiCall: ApiCallData, channel: { type: string, method: string, params?: Record }): void; - onApiCallRecovery(apiCall: ApiCallData, error: Error, channelOwner: ChannelOwner, recoveryHandlers: RecoverFromApiErrorHandler[]): void; onApiCallEnd(apiCall: ApiCallData): void; onWillPause(options: { keepTestTimeout: boolean }): void; @@ -48,7 +44,6 @@ export interface ClientInstrumentation { export interface ClientInstrumentationListener { onApiCallBegin?(apiCall: ApiCallData, channel: { type: string, method: string, params?: Record }): void; - onApiCallRecovery?(apiCall: ApiCallData, error: Error, channelOwner: ChannelOwner, recoveryHandlers: RecoverFromApiErrorHandler[]): void; onApiCallEnd?(apiCall: ApiCallData): void; onWillPause?(options: { keepTestTimeout: boolean }): void; diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 40fa04ae7c6a8..6c412c0d98bbd 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -22,11 +22,9 @@ import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringif import { currentTestInfo } from './common/globals'; import { rootTestType } from './common/testType'; -import { runBrowserBackendOnError, runBrowserBackendAtEnd } from './mcp/test/browserBackend'; -import { codeFrameColumns } from './transform/babelBundle'; -import { stripAnsiEscapes } from './util'; +import { runBrowserBackendAtEnd } from './mcp/test/browserBackend'; -import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode, Location } from '../types/test'; +import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; import type { ContextReuseMode } from './common/config'; import type { TestInfoImpl, TestStepInternal } from './worker/testInfo'; import type { ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation'; @@ -36,7 +34,6 @@ import type { BrowserContext as BrowserContextImpl } from '../../playwright-core import type { APIRequestContext as APIRequestContextImpl } from '../../playwright-core/src/client/fetch'; import type { ChannelOwner } from '../../playwright-core/src/client/channelOwner'; import type { Page as PageImpl } from '../../playwright-core/src/client/page'; -import type { Frame as FrameImpl } from '../../playwright-core/src/client/frame'; import type { BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing } from 'playwright-core'; export { expect } from './matchers/expect'; @@ -291,23 +288,6 @@ const playwrightFixtures: Fixtures = ({ if (data.apiName === 'tracing.group') tracingGroupSteps.push(step); }, - onApiCallRecovery: (data, error, channelOwner, recoveryHandlers) => { - const testInfo = currentTestInfo(); - if (!testInfo || !testInfo._pauseOnError()) - return; - - const step = data.userData; - if (!step) - return; - const page = channelToPage(channelOwner); - if (!page) - return; - recoveryHandlers.push(async () => { - await runBrowserBackendOnError(page, () => { - return stripAnsiEscapes(createErrorCodeframe(error.message, step.location)); - }); - }); - }, onApiCallEnd: data => { // "tracing.group" step will end later, when "tracing.groupEnd" finishes. @@ -437,13 +417,14 @@ const playwrightFixtures: Fixtures = ({ attachConnectedHeaderIfNeeded(testInfo, browserImpl); if (!_reuseContext) { const { context, close } = await _contextFactory(); + (testInfo as TestInfoImpl)._onDidFinishTestFunctions.unshift(() => runBrowserBackendAtEnd(context, testInfo.errors[0]?.message)); await use(context); - await runBrowserBackendAtEnd(context); await close(); return; } const context = await browserImpl._wrapApiCall(() => browserImpl._newContextForReuse(), { internal: true }); + (testInfo as TestInfoImpl)._onDidFinishTestFunctions.unshift(() => runBrowserBackendAtEnd(context, testInfo.errors[0]?.message)); await use(context); const closeReason = testInfo.status === 'timedOut' ? 'Test timeout of ' + testInfo.timeout + 'ms exceeded.' : 'Test ended.'; await browserImpl._wrapApiCall(() => browserImpl._disconnectFromReusedContext(closeReason), { internal: true }); @@ -666,7 +647,7 @@ class ArtifactsRecorder { async willStartTest(testInfo: TestInfoImpl) { this._testInfo = testInfo; - testInfo._onDidFinishTestFunction = () => this.didFinishTestFunction(); + testInfo._onDidFinishTestFunctions.push(() => this.didFinishTestFunction()); this._screenshotRecorder.fixOrdinal(); @@ -804,36 +785,3 @@ export const test = _baseTest.extend(playwrightFix export { defineConfig } from './common/configLoader'; export { mergeTests } from './common/testType'; export { mergeExpects } from './matchers/expect'; - -function channelToPage(channelOwner: ChannelOwner): Page | undefined { - if (channelOwner._type === 'Page') - return channelOwner as PageImpl; - if (channelOwner._type === 'Frame') - return (channelOwner as FrameImpl).page(); - return undefined; -} - -function createErrorCodeframe(message: string, location: Location) { - let source: string; - try { - source = fs.readFileSync(location.file, 'utf-8') + '\n//'; - } catch (e) { - return ''; - } - - return codeFrameColumns( - source, - { - start: { - line: location.line, - column: location.column, - }, - }, - { - highlightCode: true, - linesAbove: 5, - linesBelow: 5, - message: message.split('\n')[0] || undefined, - } - ); -} diff --git a/packages/playwright/src/matchers/toBeTruthy.ts b/packages/playwright/src/matchers/toBeTruthy.ts index 89940e7a3ed2d..0802277421a73 100644 --- a/packages/playwright/src/matchers/toBeTruthy.ts +++ b/packages/playwright/src/matchers/toBeTruthy.ts @@ -16,7 +16,6 @@ import { expectTypes } from '../util'; import { formatMatcherMessage } from './matcherHint'; -import { runBrowserBackendOnError } from '../mcp/test/browserBackend'; import type { MatcherResult } from './matcherHint'; import type { ExpectMatcherState } from '../../types/test'; @@ -70,8 +69,6 @@ export async function toBeTruthy( }); }; - await runBrowserBackendOnError(locator.page(), message); - return { message, pass, diff --git a/packages/playwright/src/matchers/toEqual.ts b/packages/playwright/src/matchers/toEqual.ts index 082eaa3b4d014..e22f8a9c657d3 100644 --- a/packages/playwright/src/matchers/toEqual.ts +++ b/packages/playwright/src/matchers/toEqual.ts @@ -18,7 +18,6 @@ import { isRegExp } from 'playwright-core/lib/utils'; import { expectTypes } from '../util'; import { formatMatcherMessage } from './matcherHint'; -import { runBrowserBackendOnError } from '../mcp/test/browserBackend'; import type { MatcherResult } from './matcherHint'; import type { ExpectMatcherState } from '../../types/test'; @@ -99,8 +98,6 @@ export async function toEqual( }); }; - await runBrowserBackendOnError(locator.page(), message); - // Passing the actual and expected objects so that a custom reporter // could access them, for example in order to display a custom visual diff, // or create a different error message diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index 8c5cfd06dc1b6..508702bcd01d1 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -22,7 +22,6 @@ import { } from './expect'; import { formatMatcherMessage } from './matcherHint'; import { EXPECTED_COLOR } from '../common/expectBundle'; -import { runBrowserBackendOnError } from '../mcp/test/browserBackend'; import type { MatcherResult } from './matcherHint'; import type { ExpectMatcherState } from '../../types/test'; @@ -103,9 +102,6 @@ export async function toMatchText( }); }; - if (locator) - await runBrowserBackendOnError(locator.page(), message); - return { name: matcherName, expected, diff --git a/packages/playwright/src/mcp/test/browserBackend.ts b/packages/playwright/src/mcp/test/browserBackend.ts index 38d2a7e22b624..3653e7c4e7b56 100644 --- a/packages/playwright/src/mcp/test/browserBackend.ts +++ b/packages/playwright/src/mcp/test/browserBackend.ts @@ -25,50 +25,46 @@ import type { Page } from '../../../../playwright-core/src/client/page'; import type { BrowserContextFactory } from '../browser/browserContextFactory'; import type { ClientInfo } from '../sdk/server'; -export async function runBrowserBackendOnError(page: playwright.Page, message: () => string) { +export async function runBrowserBackendAtEnd(context: playwright.BrowserContext, errorMessage?: string) { const testInfo = currentTestInfo(); - if (!testInfo || !testInfo._pauseOnError()) + if (!testInfo) return; - const config: FullConfig = { - ...defaultConfig, - capabilities: ['testing'], - }; - - const snapshot = await (page as Page)._snapshotForAI(); - const introMessage = `### Paused on error: -${stripAnsiEscapes(message())} - -### Current page snapshot: -${snapshot} - -### Task -Try recovering from the error prior to continuing`; - - await mcp.runOnPauseBackendLoop(new BrowserServerBackend(config, identityFactory(page.context())), introMessage); -} - -export async function runBrowserBackendAtEnd(context: playwright.BrowserContext) { - const testInfo = currentTestInfo(); - if (!testInfo || !testInfo._pauseAtEnd()) + const shouldPause = errorMessage ? testInfo?._pauseOnError() : testInfo?._pauseAtEnd(); + if (!shouldPause) return; - const page = context.pages()[0]; - if (!page) - return; + const lines: string[] = []; + if (errorMessage) + lines.push(`### Paused on error:`, stripAnsiEscapes(errorMessage)); + else + lines.push(`### Paused at end of test. ready for interaction`); - const snapshot = await (page as Page)._snapshotForAI(); - const introMessage = `### Paused at end of test. ready for interaction + for (let i = 0; i < context.pages().length; i++) { + const page = context.pages()[i]; + const stateSuffix = context.pages().length > 1 ? (i + 1) + ' of ' + (context.pages().length) : 'state'; + lines.push( + '', + `### Page ${stateSuffix}`, + `- Page URL: ${page.url()}`, + `- Page Title: ${await page.title()}`.trim(), + `- Page Snapshot:`, + '```yaml', + await (page as Page)._snapshotForAI(), + '```', + ); + } -### Current page snapshot: -${snapshot}`; + lines.push(''); + if (errorMessage) + lines.push(`### Task`, `Try recovering from the error prior to continuing`); const config: FullConfig = { ...defaultConfig, capabilities: ['testing'], }; - await mcp.runOnPauseBackendLoop(new BrowserServerBackend(config, identityFactory(context)), introMessage); + await mcp.runOnPauseBackendLoop(new BrowserServerBackend(config, identityFactory(context)), lines.join('\n')); } function identityFactory(browserContext: playwright.BrowserContext): BrowserContextFactory { diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index aba9a28b9f6d3..e4add9bbae440 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -83,7 +83,7 @@ export class TestInfoImpl implements TestInfo { readonly _configInternal: FullConfigInternal; private readonly _steps: TestStepInternal[] = []; private readonly _stepMap = new Map(); - _onDidFinishTestFunction: (() => Promise) | undefined; + _onDidFinishTestFunctions: (() => Promise)[] = []; _hasNonRetriableError = false; _hasUnhandledError = false; _allowSkips = false; diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index 29f421dcc9be8..154c341a4b1be 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -396,7 +396,10 @@ export class WorkerMain extends ProcessRunner { try { // Run "immediately upon test function finish" callback. - await testInfo._runWithTimeout({ type: 'test', slot: afterHooksSlot }, async () => testInfo._onDidFinishTestFunction?.()); + await testInfo._runWithTimeout({ type: 'test', slot: afterHooksSlot }, async () => { + for (const fn of testInfo._onDidFinishTestFunctions) + await fn(); + }); } catch (error) { firstAfterHooksError = firstAfterHooksError ?? error; } diff --git a/tests/mcp/test-tools.spec.ts b/tests/mcp/test-tools.spec.ts index 76151f87aaea6..9da107e4f49a8 100644 --- a/tests/mcp/test-tools.spec.ts +++ b/tests/mcp/test-tools.spec.ts @@ -242,7 +242,7 @@ test('test_debug (pause/resume)', async ({ startClient }) => { test: { id, title: 'fail' }, }, })).toHaveTextResponse(`### Paused on error: -expect(locator).toBeVisible() failed +Error: expect(locator).toBeVisible() failed Locator: getByRole('button', { name: 'Missing' }) Expected: visible @@ -254,8 +254,13 @@ Call log: - waiting for getByRole('button', { name: 'Missing' }) -### Current page snapshot: +### Page state +- Page URL: about:blank +- Page Title: +- Page Snapshot: +\`\`\`yaml - button "Submit" [ref=e2] +\`\`\` ### Task Try recovering from the error prior to continuing`); @@ -303,6 +308,45 @@ test('test_debug (browser_snapshot/network/console)', async ({ startClient, serv }); }); +test('test_debug (multiple pages and custom errors)', async ({ startClient, server }) => { + const { client, id } = await prepareDebugTest(startClient, ` + import { test, expect } from '@playwright/test'; + test('fail', async ({ page }) => { + const page2 = await page.context().newPage(); + await page.goto(${JSON.stringify(server.PREFIX + '/frames/frame.html')}); + await page2.goto(${JSON.stringify(server.PREFIX + '/wrappedlink.html')}); + throw new Error('non-api error'); + }); + `); + expect(await client.callTool({ + name: 'test_debug', + arguments: { + test: { id, title: 'fail' }, + }, + })).toHaveTextResponse(`### Paused on error: +Error: non-api error + +### Page 1 of 2 +- Page URL: ${server.PREFIX + '/frames/frame.html'} +- Page Title: +- Page Snapshot: +\`\`\`yaml +- generic [ref=e2]: Hi, I'm frame +\`\`\` + +### Page 2 of 2 +- Page URL: ${server.PREFIX + '/wrappedlink.html'} +- Page Title: +- Page Snapshot: +\`\`\`yaml +- link "123321" [ref=e3] [cursor=pointer]: + - /url: "#clicked" +\`\`\` + +### Task +Try recovering from the error prior to continuing`); +}); + test('test_debug (pause/snapshot/resume)', async ({ startClient }) => { const { client, id } = await prepareDebugTest(startClient); @@ -312,7 +356,7 @@ test('test_debug (pause/snapshot/resume)', async ({ startClient }) => { test: { id, title: 'fail' }, }, })).toHaveTextResponse(`### Paused on error: -expect(locator).toBeVisible() failed +Error: expect(locator).toBeVisible() failed Locator: getByRole('button', { name: 'Missing' }) Expected: visible @@ -324,8 +368,13 @@ Call log: - waiting for getByRole('button', { name: 'Missing' }) -### Current page snapshot: +### Page state +- Page URL: about:blank +- Page Title: +- Page Snapshot: +\`\`\`yaml - button "Submit" [ref=e2] +\`\`\` ### Task Try recovering from the error prior to continuing`); @@ -421,8 +470,14 @@ test('test_setup_page', async ({ startClient }) => { }, })).toHaveTextResponse(`### Paused at end of test. ready for interaction -### Current page snapshot: -- button "Submit" [ref=e2]`); +### Page state +- Page URL: about:blank +- Page Title: +- Page Snapshot: +\`\`\`yaml +- button "Submit" [ref=e2] +\`\`\` +`); expect(await client.callTool({ name: 'browser_click', @@ -470,7 +525,13 @@ test('test_setup_page with dependencies', async ({ startClient }) => { }, })).toHaveTextResponse(`### Paused at end of test. ready for interaction -### Current page snapshot: +### Page state +- Page URL: about:blank +- Page Title: +- Page Snapshot: +\`\`\`yaml + +\`\`\` `); // Should pause at the target test, not in a dependency or any other stray project. @@ -486,7 +547,13 @@ test('test_setup_page (no test location)', async ({ startClient }) => { arguments: {}, })).toHaveTextResponse(`### Paused at end of test. ready for interaction -### Current page snapshot: +### Page state +- Page URL: about:blank +- Page Title: +- Page Snapshot: +\`\`\`yaml + +\`\`\` `); }); @@ -508,7 +575,13 @@ test('test_setup_page chooses top-level project', async ({ startClient }) => { arguments: {}, })).toHaveTextResponse(`### Paused at end of test. ready for interaction -### Current page snapshot: +### Page state +- Page URL: about:blank +- Page Title: +- Page Snapshot: +\`\`\`yaml + +\`\`\` `); expect(fs.existsSync(path.join(baseDir, 'one', 'default.seed.spec.ts'))).toBe(false); @@ -537,7 +610,13 @@ test('test_setup_page without location respects testsDir', async ({ startClient arguments: {}, })).toHaveTextResponse(`### Paused at end of test. ready for interaction -### Current page snapshot: +### Page state +- Page URL: about:blank +- Page Title: +- Page Snapshot: +\`\`\`yaml + +\`\`\` `); expect(fs.existsSync(test.info().outputPath('tests', 'default.seed.spec.ts'))).toBe(true); }); From d54093fa1f90f65aaab20aac135fbdba7afb222f Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 23 Sep 2025 13:35:23 -0700 Subject: [PATCH 232/329] Revert "chore: update xterm dependency (#37412)" (#37539) --- package-lock.json | 36 ++++++++++---------- packages/web/package.json | 4 +-- packages/web/src/components/xtermModule.tsx | 6 ++-- packages/web/src/components/xtermWrapper.tsx | 4 +-- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0396d7fc631de..416a2d7dfed2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2264,21 +2264,6 @@ "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", "peer": true }, - "node_modules/@xterm/addon-fit": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", - "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.0.0" - } - }, - "node_modules/@xterm/xterm": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" - }, "node_modules/@zip.js/zip.js": { "version": "2.7.73", "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.73.tgz", @@ -8047,6 +8032,21 @@ "node": ">=4.0" } }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead." + }, + "node_modules/xterm-addon-fit": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz", + "integrity": "sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", + "peerDependencies": { + "xterm": "^5.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -8381,9 +8381,9 @@ "packages/web": { "version": "0.0.0", "dependencies": { - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", - "codemirror": "5.65.18" + "codemirror": "5.65.18", + "xterm": "^5.1.0", + "xterm-addon-fit": "^0.7.0" } } } diff --git a/packages/web/package.json b/packages/web/package.json index 0c45c30c7bad5..0465fdf4c4a16 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -5,7 +5,7 @@ "scripts": {}, "dependencies": { "codemirror": "5.65.18", - "@xterm/xterm": "^5.5.0", - "@xterm/addon-fit": "^0.10.0" + "xterm": "^5.1.0", + "xterm-addon-fit": "^0.7.0" } } diff --git a/packages/web/src/components/xtermModule.tsx b/packages/web/src/components/xtermModule.tsx index d04f36ad33a7b..00474787a655b 100644 --- a/packages/web/src/components/xtermModule.tsx +++ b/packages/web/src/components/xtermModule.tsx @@ -14,10 +14,10 @@ limitations under the License. */ -import '@xterm/xterm/css/xterm.css'; +import 'xterm/css/xterm.css'; -import { Terminal } from '@xterm/xterm'; -import { FitAddon } from '@xterm/addon-fit'; +import { Terminal } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; export type XtermModule = { Terminal: typeof Terminal; diff --git a/packages/web/src/components/xtermWrapper.tsx b/packages/web/src/components/xtermWrapper.tsx index dc2c3c5b5cbe4..9293c558d0326 100644 --- a/packages/web/src/components/xtermWrapper.tsx +++ b/packages/web/src/components/xtermWrapper.tsx @@ -16,8 +16,8 @@ import * as React from 'react'; import './xtermWrapper.css'; -import type { ITheme, Terminal } from '@xterm/xterm'; -import type { FitAddon } from '@xterm/addon-fit'; +import type { ITheme, Terminal } from 'xterm'; +import type { FitAddon } from 'xterm-addon-fit'; import type { XtermModule } from './xtermModule'; import { currentTheme, addThemeListener, removeThemeListener } from '../theme'; import { useMeasure } from '../uiUtils'; From 89b74e9399ebbe28044f8393cee590e439b2ad24 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 23 Sep 2025 21:56:53 +0100 Subject: [PATCH 233/329] chore: a few fixes for vscode agents (#37535) --- .../playwright/src/agents/generateAgents.ts | 23 ++++++++++++++--- packages/playwright/src/mcp/browser/tools.ts | 7 ++++++ tests/mcp/vscode.spec.ts | 25 +++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/playwright/src/agents/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts index 211aab6c573dc..eac59189a696b 100644 --- a/packages/playwright/src/agents/generateAgents.ts +++ b/packages/playwright/src/agents/generateAgents.ts @@ -216,22 +216,38 @@ const vscodeToolMap = new Map([ ['edit', ['editFiles']], ['write', ['createFile', 'createDirectory']], ]); +const vscodeToolsOrder = ['createFile', 'createDirectory', 'editFiles', 'fileSearch', 'textSearch', 'listDirectory', 'readFile']; +const vscodeToolPrefix = 'test_'; // FIXME: this is ugly, fix VSCode! function saveAsVSCodeChatmode(agent: Agent): string { function asVscodeTool(tool: string): string | string[] { const [first, second] = tool.split('/'); if (second) - return second; + return second.startsWith('browser_') ? vscodeToolPrefix + second : second; return vscodeToolMap.get(first) || first; } - const tools = agent.header.tools.map(asVscodeTool).flat().map(tool => `'${tool}'`).join(', '); + const tools = agent.header.tools.map(asVscodeTool).flat().sort((a, b) => { + // VSCode insisits on the specific tools order when editing agent config. + const indexA = vscodeToolsOrder.indexOf(a); + const indexB = vscodeToolsOrder.indexOf(b); + if (indexA === -1 && indexB === -1) + return a.localeCompare(b); + if (indexA === -1) + return 1; + if (indexB === -1) + return -1; + return indexA - indexB; + }).map(tool => `'${tool}'`).join(', '); const lines: string[] = []; lines.push(`---`); - lines.push(`description: ${agent.header.description}. Examples: ${agent.examples.map(example => `${example}`).join('')}`); + lines.push(`description: ${agent.header.description}.`); lines.push(`tools: [${tools}]`); lines.push(`---`); lines.push(''); lines.push(agent.instructions); + for (const example of agent.examples) + lines.push(`${example}`); + return lines.join('\n'); } @@ -261,6 +277,7 @@ export async function initVSCodeRepo() { type: 'stdio', command: commonMcpServers.playwrightTest.command, args: commonMcpServers.playwrightTest.args, + env: { 'PLAYWRIGHT_MCP_TOOL_PREFIX': vscodeToolPrefix }, }; await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2)); } diff --git a/packages/playwright/src/mcp/browser/tools.ts b/packages/playwright/src/mcp/browser/tools.ts index 3ebbe7591e58f..e5927c2888f81 100644 --- a/packages/playwright/src/mcp/browser/tools.ts +++ b/packages/playwright/src/mcp/browser/tools.ts @@ -57,6 +57,13 @@ export const browserTools: Tool[] = [ ...verify, ]; +// FIXME: this is ugly, fix VSCode! +const customPrefix = process.env.PLAYWRIGHT_MCP_TOOL_PREFIX; +if (customPrefix) { + for (const tool of browserTools) + tool.schema.name = customPrefix + tool.schema.name; +} + export function filteredTools(config: FullConfig) { return browserTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability)); } diff --git a/tests/mcp/vscode.spec.ts b/tests/mcp/vscode.spec.ts index fc0b8187e8ea3..f563d39ed002a 100644 --- a/tests/mcp/vscode.spec.ts +++ b/tests/mcp/vscode.spec.ts @@ -118,3 +118,28 @@ test('browser_connect(debugController) works', async ({ startClient }) => { await expect.poll(() => messages).toContainEqual(expect.objectContaining({ method: 'stateChanged' })); }); + +test('tool prefix', async ({ startClient, server }) => { + const { client } = await startClient({ env: { 'PLAYWRIGHT_MCP_TOOL_PREFIX': 'test_' } }); + expect(await client.callTool({ + name: 'test_browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: `await page.goto('${server.HELLO_WORLD}');`, + pageState: `- Page URL: ${server.HELLO_WORLD} +- Page Title: Title +- Page Snapshot: +\`\`\`yaml +- generic [active] [ref=e1]: Hello, world! +\`\`\``, + }); +}); + +test.describe(() => { + test.use({ mcpServerType: 'test-mcp' }); + test('tool prefix does not affect test tools', async ({ startClient }) => { + const { client } = await startClient({ env: { 'PLAYWRIGHT_MCP_TOOL_PREFIX': 'test_' } }); + const { tools } = await client.listTools(); + expect(tools.map(t => t.name)).toContain('test_setup_page'); + }); +}); From 1313fbd4793e2281614346eb26371fe16708247e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 23 Sep 2025 14:19:06 -0700 Subject: [PATCH 234/329] chore(mcp): introduce allowed-hosts (#37541) --- packages/playwright/src/mcp/browser/config.ts | 3 ++ packages/playwright/src/mcp/config.d.ts | 6 +++ packages/playwright/src/mcp/program.ts | 4 +- packages/playwright/src/mcp/sdk/http.ts | 19 ++++++++- packages/playwright/src/mcp/sdk/server.ts | 4 +- tests/mcp/http.spec.ts | 40 +++++++++++++++++++ 6 files changed, 72 insertions(+), 4 deletions(-) diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index c4a67c986f9b3..4946709d532c6 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -31,6 +31,7 @@ import type { ClientInfo } from '../sdk/server'; type ViewportSize = { width: number; height: number }; export type CLIOptions = { + allowedHosts?: string[]; allowedOrigins?: string[]; blockedOrigins?: string[]; blockServiceWorkers?: boolean; @@ -222,6 +223,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { server: { port: cliOptions.port, host: cliOptions.host, + allowedHosts: cliOptions.allowedHosts, }, capabilities: cliOptions.caps as ToolCapability[], network: { @@ -246,6 +248,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { function configFromEnv(): Config { const options: CLIOptions = {}; + options.allowedHosts = commaSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_HOSTNAMES); options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS); options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS); options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS); diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index 12ec2323bd85e..dd373b9883f03 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -86,6 +86,12 @@ export type Config = { * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces. */ host?: string; + + /** + * The hosts this server is allowed to serve from. Defaults to the host server is bound to. + * This is not for CORS, but rather for the DNS rebinding protection. + */ + allowedHosts?: string[]; }, /** diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index 55cf76370c21f..baad3ab5b4b6c 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -28,7 +28,9 @@ import type { Command } from 'playwright-core/lib/utilsBundle'; import type { MCPProvider } from './sdk/proxyBackend'; export function decorateCommand(command: Command, version: string) { - command.option('--allowed-origins ', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList) + command + .option('--allowed-hosts ', 'comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to.', commaSeparatedList) + .option('--allowed-origins ', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList) .option('--blocked-origins ', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList) .option('--block-service-workers', 'block service workers') .option('--browser ', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.') diff --git a/packages/playwright/src/mcp/sdk/http.ts b/packages/playwright/src/mcp/sdk/http.ts index 4d941d0b0dfa0..59fb59a8b1bb5 100644 --- a/packages/playwright/src/mcp/sdk/http.ts +++ b/packages/playwright/src/mcp/sdk/http.ts @@ -59,10 +59,27 @@ export function httpAddressToString(address: string | net.AddressInfo | null): s return `http://${resolvedHost}:${resolvedPort}`; } -export async function installHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory) { +export async function installHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory, allowedHosts?: string[]) { + const url = httpAddressToString(httpServer.address()); + const host = new URL(url).host; + allowedHosts = (allowedHosts || [host]).map(h => h.toLowerCase()); + const sseSessions = new Map(); const streamableSessions = new Map(); httpServer.on('request', async (req, res) => { + const host = req.headers.host?.toLowerCase(); + if (!host) { + res.statusCode = 400; + return res.end('Missing host'); + } + + // Prevent DNS evil.com -> localhost rebind. + if (!allowedHosts.includes(host)) { + // Access from the browser is forbidden. + res.statusCode = 403; + return res.end('Access is only allowed at ' + allowedHosts.join(', ')); + } + const url = new URL(`http://localhost${req.url}`); if (url.pathname === '/killkillkill' && req.method === 'GET') { res.statusCode = 200; diff --git a/packages/playwright/src/mcp/sdk/server.ts b/packages/playwright/src/mcp/sdk/server.ts index b95c5759253d0..6c1b1acfc76c0 100644 --- a/packages/playwright/src/mcp/sdk/server.ts +++ b/packages/playwright/src/mcp/sdk/server.ts @@ -139,15 +139,15 @@ function addServerListener(server: Server, event: 'close' | 'initialized', liste }; } -export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number }) { +export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number, allowedHosts?: string[] }) { if (options.port === undefined) { await connect(serverBackendFactory, new mcpBundle.StdioServerTransport(), false); return; } const httpServer = await startHttpServer(options); - await installHttpTransport(httpServer, serverBackendFactory); const url = httpAddressToString(httpServer.address()); + await installHttpTransport(httpServer, serverBackendFactory, options.allowedHosts); const mcpConfig: any = { mcpServers: { } }; mcpConfig.mcpServers[serverBackendFactory.nameInConfig] = { diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index af43cc4bd339b..01ef8ac4708b1 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -15,6 +15,7 @@ */ import fs from 'fs'; +import net from 'net'; import { ChildProcess, spawn } from 'child_process'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; @@ -348,3 +349,42 @@ test('client should receive list roots request', async ({ serverEndpoint, server }); expect(await rootsListedPromise).toBe('success'); }); + +test('should not allow rebinding to localhost', async ({ serverEndpoint }) => { + const { url } = await serverEndpoint(); + const response = await fetch(url.href.replace('localhost', '127.0.0.1')); + expect(response.status).toBe(403); + expect(await response.text()).toContain('Access is only allowed at localhost'); +}); + +test('should respect allowed hosts (negative)', async ({ serverEndpoint }) => { + const { url } = await serverEndpoint({ args: ['--allowed-hosts=example.com'] }); + const response = await fetch(url.href); + expect(response.status).toBe(403); + expect(await response.text()).toContain('Access is only allowed at example.com'); +}); + +test('should respect allowed hosts (positive)', async ({ serverEndpoint }) => { + const port = await findFreePort(); + await serverEndpoint({ + args: [ + '--host=127.0.0.1', + '--port=' + port, + '--allowed-hosts=localhost:' + port, + ] + }); + const response = await fetch('http://localhost:' + port); + // 400 is expected for the mcp fetch. + expect(response.status).toBe(400); +}); + +async function findFreePort(): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + server.close(() => resolve(port)); + }); + server.on('error', reject); + }); +} From 1e774e13e4dc251c4345860e9bc5e055603134ac Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Tue, 23 Sep 2025 15:44:25 -0700 Subject: [PATCH 235/329] fix(ui-mode): restore console character spacing (#37540) --- packages/web/src/components/xtermWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/components/xtermWrapper.tsx b/packages/web/src/components/xtermWrapper.tsx index 9293c558d0326..f6b5f5be9ec20 100644 --- a/packages/web/src/components/xtermWrapper.tsx +++ b/packages/web/src/components/xtermWrapper.tsx @@ -64,7 +64,7 @@ export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({ convertEol: true, fontSize: 13, scrollback: 10000, - fontFamily: 'var(--vscode-editor-font-family)', + fontFamily: 'monospace', theme: terminalTheme, }); From 8f586c2116c78e83f83cc128dd2833e63d6c7bf0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 23 Sep 2025 16:13:00 -0700 Subject: [PATCH 236/329] chore(mcp): log request/response and fs operations (#37543) --- .../src/mcp/browser/browserContextFactory.ts | 14 +++++++---- .../src/mcp/browser/browserServerBackend.ts | 2 ++ packages/playwright/src/mcp/browser/config.ts | 11 ++++++--- .../playwright/src/mcp/browser/context.ts | 12 ++++++---- .../playwright/src/mcp/browser/response.ts | 23 +++++++++++++++---- .../playwright/src/mcp/browser/sessionLog.ts | 2 +- packages/playwright/src/mcp/browser/tab.ts | 2 +- .../playwright/src/mcp/browser/tools/pdf.ts | 2 +- .../src/mcp/browser/tools/screenshot.ts | 2 +- .../src/mcp/browser/tools/tracing.ts | 2 +- packages/playwright/src/mcp/log.ts | 4 ++-- 11 files changed, 52 insertions(+), 24 deletions(-) diff --git a/packages/playwright/src/mcp/browser/browserContextFactory.ts b/packages/playwright/src/mcp/browser/browserContextFactory.ts index 7e75c7d56e4dd..5018b5bf9cc3f 100644 --- a/packages/playwright/src/mcp/browser/browserContextFactory.ts +++ b/packages/playwright/src/mcp/browser/browserContextFactory.ts @@ -116,8 +116,8 @@ class IsolatedContextFactory extends BaseContextFactory { protected override async _doObtainBrowser(clientInfo: ClientInfo): Promise { await injectCdpPort(this.config.browser); const browserType = playwright[this.config.browser.browserName]; - const tracesDir = await outputFile(this.config, clientInfo, `traces`, { origin: 'code' }); - if (this.config.saveTrace) + const tracesDir = await computeTracesDir(this.config, clientInfo); + if (tracesDir && this.config.saveTrace) await startTraceServer(this.config, tracesDir); return browserType.launch({ tracesDir, @@ -183,8 +183,8 @@ class PersistentContextFactory implements BrowserContextFactory { await injectCdpPort(this.config.browser); testDebug('create browser context (persistent)'); const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo); - const tracesDir = await outputFile(this.config, clientInfo, `traces`, { origin: 'code' }); - if (this.config.saveTrace) + const tracesDir = await computeTracesDir(this.config, clientInfo); + if (tracesDir && this.config.saveTrace) await startTraceServer(this.config, tracesDir); this._userDataDirs.add(userDataDir); @@ -326,3 +326,9 @@ export class SharedContextFactory implements BrowserContextFactory { await close(async () => {}); } } + +async function computeTracesDir(config: FullConfig, clientInfo: ClientInfo): Promise { + if (!config.saveTrace && !config.capabilities?.includes('tracing')) + return; + return await outputFile(config, clientInfo, `traces`, { origin: 'code', reason: 'Collecting trace' }); +} diff --git a/packages/playwright/src/mcp/browser/browserServerBackend.ts b/packages/playwright/src/mcp/browser/browserServerBackend.ts index bd7ab7fbda7be..e9ec5c0945211 100644 --- a/packages/playwright/src/mcp/browser/browserServerBackend.ts +++ b/packages/playwright/src/mcp/browser/browserServerBackend.ts @@ -61,6 +61,7 @@ export class BrowserServerBackend implements ServerBackend { const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {}); const context = this._context!; const response = new Response(context, name, parsedArguments); + response.logBegin(); context.setRunningTool(name); try { await tool.handle(context, parsedArguments, response); @@ -71,6 +72,7 @@ export class BrowserServerBackend implements ServerBackend { } finally { context.setRunningTool(undefined); } + response.logEnd(); return response.serialize(); } diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index 4946709d532c6..5ffe2f8d3cc11 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -19,9 +19,8 @@ import os from 'os'; import path from 'path'; import { devices } from 'playwright-core'; -import { dotenv } from 'playwright-core/lib/utilsBundle'; +import { dotenv, debug } from 'playwright-core/lib/utilsBundle'; import { fileExistsAsync } from '../../util'; - import { firstRootPath } from '../sdk/server'; import type * as playwright from '../../../types/test'; @@ -308,7 +307,13 @@ export function outputDir(config: FullConfig, clientInfo: ClientInfo): string { ?? path.join(tmpDir(), String(clientInfo.timestamp)); } -export async function outputFile(config: FullConfig, clientInfo: ClientInfo, fileName: string, options: { origin: 'code' | 'llm' | 'web' }): Promise { +export async function outputFile(config: FullConfig, clientInfo: ClientInfo, fileName: string, options: { origin: 'code' | 'llm' | 'web', reason: string }): Promise { + const file = await resolveFile(config, clientInfo, fileName, options); + debug('pw:mcp:file')(options.reason, file); + return file; +} + +async function resolveFile(config: FullConfig, clientInfo: ClientInfo, fileName: string, options: { origin: 'code' | 'llm' | 'web' }): Promise { const dir = outputDir(config, clientInfo); // Trust code. diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts index 3c905a68d106e..80eadfda1c280 100644 --- a/packages/playwright/src/mcp/browser/context.ts +++ b/packages/playwright/src/mcp/browser/context.ts @@ -15,6 +15,7 @@ */ import fs from 'fs'; +import path from 'path'; import { debug } from 'playwright-core/lib/utilsBundle'; @@ -116,7 +117,7 @@ export class Context { return url; } - async outputFile(fileName: string, options: { origin: 'code' | 'llm' | 'web' }): Promise { + async outputFile(fileName: string, options: { origin: 'code' | 'llm' | 'web', reason: string }): Promise { return outputFile(this.config, this._clientInfo, fileName, options); } @@ -169,12 +170,13 @@ export class Context { const videos = browserContext.pages().map(page => page.video()).filter(video => !!video); await close(async () => { for (const video of videos) { - const name = await this.outputFile(dateAsFileName('webm'), { origin: 'code' }); - const path = await video.path(); + const name = await this.outputFile(dateAsFileName('webm'), { origin: 'code', reason: 'Saving video' }); + await fs.promises.mkdir(path.dirname(name), { recursive: true }); + const p = await video.path(); // video.saveAs() does not work for persistent contexts. try { - if (fs.existsSync(path)) - await fs.promises.rename(path, name); + if (fs.existsSync(p)) + await fs.promises.rename(p, name); } catch (e) { logUnhandledError(e); } diff --git a/packages/playwright/src/mcp/browser/response.ts b/packages/playwright/src/mcp/browser/response.ts index db910cc2227c8..3308dd827d616 100644 --- a/packages/playwright/src/mcp/browser/response.ts +++ b/packages/playwright/src/mcp/browser/response.ts @@ -14,12 +14,15 @@ * limitations under the License. */ +import { debug } from 'playwright-core/lib/utilsBundle'; import { renderModalStates } from './tab'; import type { Tab, TabSnapshot } from './tab'; import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; import type { Context } from './context'; +export const requestDebug = debug('pw:mcp:request'); + export class Response { private _result: string[] = []; private _code: string[] = []; @@ -93,7 +96,17 @@ export class Response { return this._tabSnapshot; } - serialize(): { content: (TextContent | ImageContent)[], isError?: boolean } { + logBegin() { + if (requestDebug.enabled) + requestDebug(this.toolName, this.toolArgs); + } + + logEnd() { + if (requestDebug.enabled) + requestDebug(this.serialize({ omitSnapshot: true, omitBlobs: true })); + } + + serialize(options: { omitSnapshot?: boolean, omitBlobs?: boolean } = {}): { content: (TextContent | ImageContent)[], isError?: boolean } { const response: string[] = []; // Start with command result. @@ -121,7 +134,7 @@ ${this._code.join('\n')} response.push(...renderModalStates(this._context, this._tabSnapshot.modalStates)); response.push(''); } else if (this._tabSnapshot) { - response.push(renderTabSnapshot(this._tabSnapshot)); + response.push(renderTabSnapshot(this._tabSnapshot, options)); response.push(''); } @@ -133,7 +146,7 @@ ${this._code.join('\n')} // Image attachments. if (this._context.config.imageResponses !== 'omit') { for (const image of this._images) - content.push({ type: 'image', data: image.data.toString('base64'), mimeType: image.contentType }); + content.push({ type: 'image', data: options.omitBlobs ? '' : image.data.toString('base64'), mimeType: image.contentType }); } this._redactSecrets(content); @@ -153,7 +166,7 @@ ${this._code.join('\n')} } } -function renderTabSnapshot(tabSnapshot: TabSnapshot): string { +function renderTabSnapshot(tabSnapshot: TabSnapshot, options: { omitSnapshot?: boolean } = {}): string { const lines: string[] = []; if (tabSnapshot.consoleMessages.length) { @@ -179,7 +192,7 @@ function renderTabSnapshot(tabSnapshot: TabSnapshot): string { lines.push(`- Page Title: ${tabSnapshot.title}`); lines.push(`- Page Snapshot:`); lines.push('```yaml'); - lines.push(tabSnapshot.ariaSnapshot); + lines.push(options.omitSnapshot ? '' : tabSnapshot.ariaSnapshot); lines.push('```'); return lines.join('\n'); diff --git a/packages/playwright/src/mcp/browser/sessionLog.ts b/packages/playwright/src/mcp/browser/sessionLog.ts index 6a1a7a22539c2..e0558b866c435 100644 --- a/packages/playwright/src/mcp/browser/sessionLog.ts +++ b/packages/playwright/src/mcp/browser/sessionLog.ts @@ -53,7 +53,7 @@ export class SessionLog { } static async create(config: FullConfig, clientInfo: mcpServer.ClientInfo): Promise { - const sessionFolder = await outputFile(config, clientInfo, `session-${Date.now()}`, { origin: 'code' }); + const sessionFolder = await outputFile(config, clientInfo, `session-${Date.now()}`, { origin: 'code', reason: 'Saving session' }); await fs.promises.mkdir(sessionFolder, { recursive: true }); // eslint-disable-next-line no-console console.error(`Session: ${sessionFolder}`); diff --git a/packages/playwright/src/mcp/browser/tab.ts b/packages/playwright/src/mcp/browser/tab.ts index 45456d86c61bf..9babbbc4c5565 100644 --- a/packages/playwright/src/mcp/browser/tab.ts +++ b/packages/playwright/src/mcp/browser/tab.ts @@ -135,7 +135,7 @@ export class Tab extends EventEmitter { const entry = { download, finished: false, - outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: 'web' }) + outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: 'web', reason: 'Saving download' }) }; this._downloads.push(entry); await download.saveAs(entry.outputFile); diff --git a/packages/playwright/src/mcp/browser/tools/pdf.ts b/packages/playwright/src/mcp/browser/tools/pdf.ts index a58035e7a2c3d..13127f031ba90 100644 --- a/packages/playwright/src/mcp/browser/tools/pdf.ts +++ b/packages/playwright/src/mcp/browser/tools/pdf.ts @@ -35,7 +35,7 @@ const pdf = defineTabTool({ }, handle: async (tab, params, response) => { - const fileName = await tab.context.outputFile(params.filename ?? dateAsFileName('pdf'), { origin: 'llm' }); + const fileName = await tab.context.outputFile(params.filename ?? dateAsFileName('pdf'), { origin: 'llm', reason: 'Saving PDF' }); response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`); response.addResult(`Saved page as ${fileName}`); await tab.page.pdf({ path: fileName }); diff --git a/packages/playwright/src/mcp/browser/tools/screenshot.ts b/packages/playwright/src/mcp/browser/tools/screenshot.ts index a3df61000beb5..d074fe507ca8d 100644 --- a/packages/playwright/src/mcp/browser/tools/screenshot.ts +++ b/packages/playwright/src/mcp/browser/tools/screenshot.ts @@ -51,7 +51,7 @@ const screenshot = defineTabTool({ handle: async (tab, params, response) => { const fileType = params.type || 'png'; - const fileName = await tab.context.outputFile(params.filename ?? dateAsFileName(fileType), { origin: 'llm' }); + const fileName = await tab.context.outputFile(params.filename ?? dateAsFileName(fileType), { origin: 'llm', reason: 'Saving screenshot' }); const options: playwright.PageScreenshotOptions = { type: fileType, quality: fileType === 'png' ? undefined : 90, diff --git a/packages/playwright/src/mcp/browser/tools/tracing.ts b/packages/playwright/src/mcp/browser/tools/tracing.ts index 613a9ae0dc47a..ed1a41773ab6b 100644 --- a/packages/playwright/src/mcp/browser/tools/tracing.ts +++ b/packages/playwright/src/mcp/browser/tools/tracing.ts @@ -32,7 +32,7 @@ const tracingStart = defineTool({ handle: async (context, params, response) => { const browserContext = await context.ensureBrowserContext(); - const tracesDir = await context.outputFile(`traces`, { origin: 'code' }); + const tracesDir = await context.outputFile(`traces`, { origin: 'code', reason: 'Collecting trace' }); const name = 'trace-' + Date.now(); await (browserContext.tracing as Tracing).start({ name, diff --git a/packages/playwright/src/mcp/log.ts b/packages/playwright/src/mcp/log.ts index 072ec9ad47447..cd71ef25318a9 100644 --- a/packages/playwright/src/mcp/log.ts +++ b/packages/playwright/src/mcp/log.ts @@ -16,10 +16,10 @@ import { debug } from 'playwright-core/lib/utilsBundle'; -const errorsDebug = debug('pw:mcp:errors'); +const errorDebug = debug('pw:mcp:error'); export function logUnhandledError(error: unknown) { - errorsDebug(error); + errorDebug(error); } export const testDebug = debug('pw:mcp:test'); From eb9ebbe1ec9e9505ce9d9173c2d169742fd9fa34 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 23 Sep 2025 17:39:34 -0700 Subject: [PATCH 237/329] chore(mcp): test prefixes, fix bugs (#37547) --- packages/playwright/src/mcp/browser/tab.ts | 6 +- .../src/mcp/browser/tools/dialogs.ts | 2 +- .../playwright/src/mcp/browser/tools/files.ts | 2 +- .../playwright/src/mcp/browser/tools/tool.ts | 4 +- tests/mcp/fixtures.ts | 19 +++ tests/mcp/prefix.spec.ts | 122 ++++++++++++++++++ tests/mcp/test-tools.spec.ts | 21 +-- tests/mcp/vscode.spec.ts | 25 ---- 8 files changed, 150 insertions(+), 51 deletions(-) create mode 100644 tests/mcp/prefix.spec.ts diff --git a/packages/playwright/src/mcp/browser/tab.ts b/packages/playwright/src/mcp/browser/tab.ts index 9babbbc4c5565..7c58d1b9a77b2 100644 --- a/packages/playwright/src/mcp/browser/tab.ts +++ b/packages/playwright/src/mcp/browser/tab.ts @@ -21,6 +21,8 @@ import { ManualPromise } from 'playwright-core/lib/utils'; import { callOnPageNoTrace, waitForCompletion } from './tools/utils'; import { logUnhandledError } from '../log'; import { ModalState } from './tools/tool'; +import { handleDialog } from './tools/dialogs'; +import { uploadFile } from './tools/files'; import type { Context } from './context'; @@ -71,7 +73,7 @@ export class Tab extends EventEmitter { type: 'fileChooser', description: 'File chooser', fileChooser: chooser, - clearedBy: 'browser_file_upload', + clearedBy: uploadFile.schema.name, }); }); page.on('dialog', dialog => this._dialogShown(dialog)); @@ -127,7 +129,7 @@ export class Tab extends EventEmitter { type: 'dialog', description: `"${dialog.type()}" dialog with message "${dialog.message()}"`, dialog, - clearedBy: 'browser_handle_dialog', + clearedBy: handleDialog.schema.name }); } diff --git a/packages/playwright/src/mcp/browser/tools/dialogs.ts b/packages/playwright/src/mcp/browser/tools/dialogs.ts index f6954b7225629..d606433f1a8da 100644 --- a/packages/playwright/src/mcp/browser/tools/dialogs.ts +++ b/packages/playwright/src/mcp/browser/tools/dialogs.ts @@ -17,7 +17,7 @@ import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; -const handleDialog = defineTabTool({ +export const handleDialog = defineTabTool({ capability: 'core', schema: { diff --git a/packages/playwright/src/mcp/browser/tools/files.ts b/packages/playwright/src/mcp/browser/tools/files.ts index 79514a0b996fb..fcd77ec4c4146 100644 --- a/packages/playwright/src/mcp/browser/tools/files.ts +++ b/packages/playwright/src/mcp/browser/tools/files.ts @@ -17,7 +17,7 @@ import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; -const uploadFile = defineTabTool({ +export const uploadFile = defineTabTool({ capability: 'core', schema: { diff --git a/packages/playwright/src/mcp/browser/tools/tool.ts b/packages/playwright/src/mcp/browser/tools/tool.ts index c3ef466c5d998..131c4f01157e8 100644 --- a/packages/playwright/src/mcp/browser/tools/tool.ts +++ b/packages/playwright/src/mcp/browser/tools/tool.ts @@ -26,14 +26,14 @@ export type FileUploadModalState = { type: 'fileChooser'; description: string; fileChooser: playwright.FileChooser; - clearedBy: 'browser_file_upload'; + clearedBy: string; }; export type DialogModalState = { type: 'dialog'; description: string; dialog: playwright.Dialog; - clearedBy: 'browser_handle_dialog'; + clearedBy: string; }; export type ModalState = FileUploadModalState | DialogModalState; diff --git a/tests/mcp/fixtures.ts b/tests/mcp/fixtures.ts index 26f239f6be633..c17a0030cc403 100644 --- a/tests/mcp/fixtures.ts +++ b/tests/mcp/fixtures.ts @@ -331,3 +331,22 @@ export async function writeFiles(files: Files, options?: { update?: boolean }) { return baseDir; } + +export async function prepareDebugTest(startClient: StartClient, testFile?: string, clientArgs?: Parameters[0]) { + await writeFiles({ + 'a.test.ts': testFile || ` + import { test, expect } from '@playwright/test'; + test('fail', async ({ page }) => { + await page.setContent(''); + await expect(page.getByRole('button', { name: 'Missing' })).toBeVisible({ timeout: 1000 }); + }); + ` + }); + + const { client } = await startClient(clientArgs); + const listResult = await client.callTool({ + name: 'test_list', + }); + const [, id] = listResult.content[0].text.match(/\[id=([^\]]+)\]/); + return { client, id }; +} diff --git a/tests/mcp/prefix.spec.ts b/tests/mcp/prefix.spec.ts new file mode 100644 index 0000000000000..6a096eef0dc22 --- /dev/null +++ b/tests/mcp/prefix.spec.ts @@ -0,0 +1,122 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect, prepareDebugTest } from './fixtures'; + +test('tool prefix', async ({ startClient, server }) => { + const { client } = await startClient({ env: { 'PLAYWRIGHT_MCP_TOOL_PREFIX': 'test_' } }); + expect(await client.callTool({ + name: 'test_browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: `await page.goto('${server.HELLO_WORLD}');`, + pageState: `- Page URL: ${server.HELLO_WORLD} +- Page Title: Title +- Page Snapshot: +\`\`\`yaml +- generic [active] [ref=e1]: Hello, world! +\`\`\``, + }); +}); + +test('alert dialog', async ({ server, startClient }) => { + const { client } = await startClient({ env: { 'PLAYWRIGHT_MCP_TOOL_PREFIX': 'test_' } }); + server.setContent('/', ``, 'text/html'); + expect(await client.callTool({ + name: 'test_browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- button "Button" [ref=e2]`), + }); + + expect(await client.callTool({ + name: 'test_browser_click', + arguments: { + element: 'Button', + ref: 'e2', + }, + })).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Button' }).click();`, + modalState: `- ["alert" dialog with message "Alert"]: can be handled by the "test_browser_handle_dialog" tool`, + }); +}); + +test('browser_file_upload', async ({ server, startClient }) => { + const { client } = await startClient({ env: { 'PLAYWRIGHT_MCP_TOOL_PREFIX': 'test_' } }); + server.setContent('/', ` + + + `, 'text/html'); + + expect(await client.callTool({ + name: 'test_browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + pageState: expect.stringContaining(`- generic [active] [ref=e1]: + - button "Choose File" [ref=e2] + - button "Button" [ref=e3]`), + }); + + expect(await client.callTool({ + name: 'test_browser_file_upload', + arguments: { paths: [] }, + })).toHaveResponse({ + isError: true, + result: expect.stringContaining(`The tool "test_browser_file_upload" can only be used when there is related modal state present.`), + modalState: expect.stringContaining(`- There is no modal state present`), + }); +}); + +test.describe(() => { + test.use({ mcpServerType: 'test-mcp' }); + + test('test_debug (browser_snapshot/network/console)', async ({ startClient, server }) => { + const { client, id } = await prepareDebugTest(startClient, ` + import { test, expect } from '@playwright/test'; + test('fail', async ({ page }) => { + await page.goto(${JSON.stringify(server.HELLO_WORLD)}); + await page.evaluate(() => { + console.log('hello from console'); + setTimeout(() => { throw new Error('error from page'); }, 0); + }); + await expect(page.getByRole('button', { name: 'Missing' })).toBeVisible({ timeout: 1000 }); + }); + `, { + env: { 'PLAYWRIGHT_MCP_TOOL_PREFIX': 'test_' }, + }); + await client.callTool({ + name: 'test_debug', + arguments: { + test: { id, title: 'fail' }, + }, + }); + await expect.poll(() => client.callTool({ + name: 'test_browser_network_requests', + })).toHaveResponse({ + result: expect.stringContaining(`[GET] ${server.HELLO_WORLD} => [200] OK`), + }); + expect(await client.callTool({ + name: 'test_browser_console_messages', + })).toHaveResponse({ + result: expect.stringMatching(/\[LOG\] hello from console.*\nError: error from page/), + }); + expect(await client.callTool({ + name: 'test_browser_snapshot', + })).toHaveResponse({ + pageState: expect.stringContaining(`generic [active] [ref=e1]: Hello, world!`), + }); + }); +}); diff --git a/tests/mcp/test-tools.spec.ts b/tests/mcp/test-tools.spec.ts index 9da107e4f49a8..e7e9193fa256b 100644 --- a/tests/mcp/test-tools.spec.ts +++ b/tests/mcp/test-tools.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { test, expect, writeFiles, StartClient } from './fixtures'; +import { test, expect, writeFiles, prepareDebugTest } from './fixtures'; import fs from 'fs'; import path from 'path'; @@ -620,22 +620,3 @@ test('test_setup_page without location respects testsDir', async ({ startClient `); expect(fs.existsSync(test.info().outputPath('tests', 'default.seed.spec.ts'))).toBe(true); }); - -async function prepareDebugTest(startClient: StartClient, testFile?: string) { - await writeFiles({ - 'a.test.ts': testFile || ` - import { test, expect } from '@playwright/test'; - test('fail', async ({ page }) => { - await page.setContent(''); - await expect(page.getByRole('button', { name: 'Missing' })).toBeVisible({ timeout: 1000 }); - }); - ` - }); - - const { client } = await startClient(); - const listResult = await client.callTool({ - name: 'test_list', - }); - const [, id] = listResult.content[0].text.match(/\[id=([^\]]+)\]/); - return { client, id }; -} diff --git a/tests/mcp/vscode.spec.ts b/tests/mcp/vscode.spec.ts index f563d39ed002a..fc0b8187e8ea3 100644 --- a/tests/mcp/vscode.spec.ts +++ b/tests/mcp/vscode.spec.ts @@ -118,28 +118,3 @@ test('browser_connect(debugController) works', async ({ startClient }) => { await expect.poll(() => messages).toContainEqual(expect.objectContaining({ method: 'stateChanged' })); }); - -test('tool prefix', async ({ startClient, server }) => { - const { client } = await startClient({ env: { 'PLAYWRIGHT_MCP_TOOL_PREFIX': 'test_' } }); - expect(await client.callTool({ - name: 'test_browser_navigate', - arguments: { url: server.HELLO_WORLD }, - })).toHaveResponse({ - code: `await page.goto('${server.HELLO_WORLD}');`, - pageState: `- Page URL: ${server.HELLO_WORLD} -- Page Title: Title -- Page Snapshot: -\`\`\`yaml -- generic [active] [ref=e1]: Hello, world! -\`\`\``, - }); -}); - -test.describe(() => { - test.use({ mcpServerType: 'test-mcp' }); - test('tool prefix does not affect test tools', async ({ startClient }) => { - const { client } = await startClient({ env: { 'PLAYWRIGHT_MCP_TOOL_PREFIX': 'test_' } }); - const { tools } = await client.listTools(); - expect(tools.map(t => t.name)).toContain('test_setup_page'); - }); -}); From ced088c1f8997c84825c9de7c58a33b2a30a8a48 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 24 Sep 2025 07:44:38 +0100 Subject: [PATCH 238/329] chore: address some of the 1.56 api review feedback (#37542) --- docs/src/api/class-page.md | 10 +++++++--- packages/playwright-client/types/types.d.ts | 15 ++++++++++++--- .../src/server/chromium/chromiumSwitches.ts | 2 +- packages/playwright-core/types/types.d.ts | 15 ++++++++++++--- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 2a17c0b1ceb92..82965993c37e2 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2682,14 +2682,14 @@ Returns whether the element is [visible](../actionability.md#visible). [`param: * since: v1.56 - returns: <[Array]<[ConsoleMessage]>> -Returns up to 200 last console messages from this page. See [`event: Page.console`] for more details. +Returns up to (currently) 200 last console messages from this page. See [`event: Page.console`] for more details. ## async method: Page.pageErrors * since: v1.56 - returns: <[Array]<[Error]>> -Returns up to 200 last page errors from this page. See [`event: Page.pageError`] for more details. +Returns up to (currently) 200 last page errors from this page. See [`event: Page.pageError`] for more details. ## method: Page.locator @@ -3141,7 +3141,11 @@ return value resolves to `[]`. * since: v1.56 - returns: <[Array]<[Request]>> -Returns up to 100 last network request from this page. See [`event: Page.request`] for more details. +Returns up to (currently) 100 last network request from this page. See [`event: Page.request`] for more details. + +Returned requests should be accessed immediately, otherwise they might be collected to prevent unbounded memory growth as new requests come in. Once collected, retrieving most information about the request is impossible. + +Note that requests reported through the [`event: Page.request`] request are not collected, so there is a trade off between efficient memory usage with [`method: Page.requests`] and the amount of available information reported through [`event: Page.request`]. ## async method: Page.addLocatorHandler diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 86cd33384e8d9..a7aa4d7d6ff82 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -2285,7 +2285,7 @@ export interface Page { }): Promise; /** - * Returns up to 200 last console messages from this page. See + * Returns up to (currently) 200 last console messages from this page. See * [page.on('console')](https://playwright.dev/docs/api/class-page#page-event-console) for more details. */ consoleMessages(): Promise>; @@ -3605,7 +3605,7 @@ export interface Page { opener(): Promise; /** - * Returns up to 200 last page errors from this page. See + * Returns up to (currently) 200 last page errors from this page. See * [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) for more details. */ pageErrors(): Promise>; @@ -3927,8 +3927,17 @@ export interface Page { requestGC(): Promise; /** - * Returns up to 100 last network request from this page. See + * Returns up to (currently) 100 last network request from this page. See * [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request) for more details. + * + * Returned requests should be accessed immediately, otherwise they might be collected to prevent unbounded memory + * growth as new requests come in. Once collected, retrieving most information about the request is impossible. + * + * Note that requests reported through the + * [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request) request are not collected, so + * there is a trade off between efficient memory usage with + * [page.requests()](https://playwright.dev/docs/api/class-page#page-requests) and the amount of available information + * reported through [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request). */ requests(): Promise>; diff --git a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts index 3a4550a2713a9..5f2526c655a66 100644 --- a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts +++ b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts @@ -58,7 +58,7 @@ export const chromiumSwitches = (assistantMode?: boolean, channel?: string) => [ '--disable-dev-shm-usage', '--disable-extensions', '--disable-features=' + disabledFeatures(assistantMode).join(','), - '--enable-features=CDPScreenshotNewSurface', + process.env.PLAYWRIGHT_LEGACY_SCREENSHOT ? '' : '--enable-features=CDPScreenshotNewSurface', '--allow-pre-commit-input', '--disable-hang-monitor', '--disable-ipc-flooding-protection', diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 86cd33384e8d9..a7aa4d7d6ff82 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2285,7 +2285,7 @@ export interface Page { }): Promise; /** - * Returns up to 200 last console messages from this page. See + * Returns up to (currently) 200 last console messages from this page. See * [page.on('console')](https://playwright.dev/docs/api/class-page#page-event-console) for more details. */ consoleMessages(): Promise>; @@ -3605,7 +3605,7 @@ export interface Page { opener(): Promise; /** - * Returns up to 200 last page errors from this page. See + * Returns up to (currently) 200 last page errors from this page. See * [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error) for more details. */ pageErrors(): Promise>; @@ -3927,8 +3927,17 @@ export interface Page { requestGC(): Promise; /** - * Returns up to 100 last network request from this page. See + * Returns up to (currently) 100 last network request from this page. See * [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request) for more details. + * + * Returned requests should be accessed immediately, otherwise they might be collected to prevent unbounded memory + * growth as new requests come in. Once collected, retrieving most information about the request is impossible. + * + * Note that requests reported through the + * [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request) request are not collected, so + * there is a trade off between efficient memory usage with + * [page.requests()](https://playwright.dev/docs/api/class-page#page-requests) and the amount of available information + * reported through [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request). */ requests(): Promise>; From cedd8ab17955de3a1f1fa1972b6cb02c551c8b2a Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 24 Sep 2025 11:05:13 +0100 Subject: [PATCH 239/329] Revert "feat: --last-run-file (#37209)" (#37550) --- docs/src/test-cli-js.md | 1 - packages/playwright/src/common/config.ts | 1 - packages/playwright/src/program.ts | 2 - packages/playwright/src/runner/lastRun.ts | 28 ++++------- packages/playwright/src/runner/testRunner.ts | 3 +- tests/playwright-test/runner.spec.ts | 51 ++------------------ 6 files changed, 14 insertions(+), 72 deletions(-) diff --git a/docs/src/test-cli-js.md b/docs/src/test-cli-js.md index a47f1a60da1e4..0688e328d767a 100644 --- a/docs/src/test-cli-js.md +++ b/docs/src/test-cli-js.md @@ -91,7 +91,6 @@ npx playwright test --ui | `--ignore-snapshots` | Ignore screenshot and snapshot expectations. | | `-j ` or `--workers ` | Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%). | | `--last-failed` | Only re-run the failures. | -| `--last-run-file` | Path to the last-run file (default: "test-results/.last-run.json"). This json file is used for the `--last-failed` option. You can also apply a custom filter by providing an array of [`property: TestCase.id`] in the `filterTests` property. | | `--list` | Collect all the tests and report them, but do not run. | | `--max-failures ` or `-x` | Stop after the first `N` failures. Passing `-x` stops after the first failure. | | `--no-deps` | Do not run project dependencies. | diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index e76c49a6a9da2..d6ef36ba35d5e 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -57,7 +57,6 @@ export class FullConfigInternal { cliListOnly = false; cliPassWithNoTests?: boolean; cliLastFailed?: boolean; - cliLastRunFile?: string; preOnlyTestFilters: TestCaseFilter[] = []; postShardTestFilters: TestCaseFilter[] = []; defineConfigWasUsed = false; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 9c8cd1c28f6ac..3f7561a4a3399 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -203,7 +203,6 @@ async function runTests(args: string[], opts: { [key: string]: any }) { config.cliProjectFilter = opts.project || undefined; config.cliPassWithNoTests = !!opts.passWithNoTests; config.cliLastFailed = !!opts.lastFailed; - config.cliLastRunFile = opts.lastRunFile ? path.resolve(process.cwd(), opts.lastRunFile) : undefined; // Evaluate project filters against config before starting execution. This enables a consistent error message across run modes filterProjects(config.projects, config.cliProjectFilter); @@ -389,7 +388,6 @@ const testOptions: [string, { description: string, choices?: string[], preset?: ['--headed', { description: `Run tests in headed browsers (default: headless)` }], ['--ignore-snapshots', { description: `Ignore screenshot and snapshot expectations` }], ['--last-failed', { description: `Only re-run the failures` }], - ['--last-run-file ', { description: `Path to the last-run file (default: "test-results/.last-run.json")` }], ['--list', { description: `Collect all the tests and report them, but do not run` }], ['--max-failures ', { description: `Stop after the first N failures` }], ['--no-deps', { description: `Do not run project dependencies` }], diff --git a/packages/playwright/src/runner/lastRun.ts b/packages/playwright/src/runner/lastRun.ts index d6721fd54e7d9..93ffe9205f925 100644 --- a/packages/playwright/src/runner/lastRun.ts +++ b/packages/playwright/src/runner/lastRun.ts @@ -24,9 +24,8 @@ import type { FullConfigInternal } from '../common/config'; import type { ReporterV2 } from '../reporters/reporterV2'; type LastRunInfo = { - status?: FullResult['status']; - failedTests?: string[]; - filterTests?: string[]; + status: FullResult['status']; + failedTests: string[]; }; export class LastRunReporter implements ReporterV2 { @@ -36,28 +35,19 @@ export class LastRunReporter implements ReporterV2 { constructor(config: FullConfigInternal) { this._config = config; - if (config.cliLastRunFile) { - this._lastRunFile = config.cliLastRunFile; - } else { - const [project] = filterProjects(config.projects, config.cliProjectFilter); - if (project) - this._lastRunFile = path.join(project.project.outputDir, '.last-run.json'); - } + const [project] = filterProjects(config.projects, config.cliProjectFilter); + if (project) + this._lastRunFile = path.join(project.project.outputDir, '.last-run.json'); } - async applyFilter() { + async filterLastFailed() { if (!this._lastRunFile) return; try { const lastRunInfo = JSON.parse(await fs.promises.readFile(this._lastRunFile, 'utf8')) as LastRunInfo; - if (lastRunInfo.filterTests) { - const filterTestIds = new Set(lastRunInfo.filterTests); - this._config.preOnlyTestFilters.push(test => filterTestIds.has(test.id)); - } - if (this._config.cliLastFailed) { - const failedTestIds = new Set(lastRunInfo.failedTests ?? []); - this._config.postShardTestFilters.push(test => failedTestIds.has(test.id)); - } + const failedTestIds = new Set(lastRunInfo.failedTests); + // Explicitly apply --last-failed filter after sharding. + this._config.postShardTestFilters.push(test => failedTestIds.has(test.id)); } catch { } } diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index e5967c199be95..305247156cee6 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -454,7 +454,8 @@ export async function runAllTestsWithConfig(config: FullConfigInternal): Promise const reporters = await createReporters(config, listOnly ? 'list' : 'test', false); const lastRun = new LastRunReporter(config); - await lastRun.applyFilter(); + if (config.cliLastFailed) + await lastRun.filterLastFailed(); const reporter = new InternalReporter([...reporters, lastRun]); const tasks = listOnly ? [ diff --git a/tests/playwright-test/runner.spec.ts b/tests/playwright-test/runner.spec.ts index 3a2b9d7bac29a..89bd084db49a7 100644 --- a/tests/playwright-test/runner.spec.ts +++ b/tests/playwright-test/runner.spec.ts @@ -857,7 +857,7 @@ test('should run last failed tests', async ({ runInlineTest }) => { expect(result2.failed).toBe(1); }); -test('should run last failed tests in a shard with a custom path', async ({ runInlineTest }) => { +test('should run last failed tests in a shard', async ({ runInlineTest }) => { const workspace = { 'a.spec.js': ` import { test, expect } from '@playwright/test'; @@ -874,62 +874,17 @@ test('should run last failed tests in a shard with a custom path', async ({ runI }); `, }; - const result1 = await runInlineTest(workspace, { 'shard': '2/2', 'last-run-file': 'custom.json' }); + const result1 = await runInlineTest(workspace, { shard: '2/2' }); expect(result1.exitCode).toBe(1); expect(result1.passed).toBe(1); expect(result1.failed).toBe(1); expect(result1.output).toContain('b.spec.js:3:11 › pass-b'); expect(result1.output).toContain('b.spec.js:4:11 › fail-b'); - expect(fs.existsSync(test.info().outputPath('custom.json'))).toBe(true); - const result2 = await runInlineTest(workspace, { 'shard': '2/2', 'last-failed': true, 'last-run-file': 'custom.json' }); + const result2 = await runInlineTest(workspace, { shard: '2/2' }, {}, { additionalArgs: ['--last-failed'] }); expect(result2.exitCode).toBe(1); expect(result2.passed).toBe(0); expect(result2.failed).toBe(1); expect(result2.output).not.toContain('b.spec.js:3:11 › pass-b'); expect(result2.output).toContain('b.spec.js:4:11 › fail-b'); }); - -test('should apply filterTests from last-run.json', async ({ runInlineTest }) => { - const workspace = { - 'reporter.ts': ` - import fs from 'fs'; - import path from 'path'; - export default class MyReporter { - onBegin(config, suite) { - const filterTests = suite.allTests().filter(t => t.title !== 'test2').map(t => t.id); - const json = JSON.stringify({ filterTests }); - fs.writeFileSync(path.join(__dirname, 'filter.json'), json); - } - } - `, - 'a.spec.js': ` - import { test, expect } from '@playwright/test'; - test('test1', async () => { - console.log('\\n%%test1'); - }); - test('test2', async () => { - console.log('\\n%%test2'); - }); - test('test3', async () => { - console.log('\\n%%test3'); - expect(1).toBe(2); - }); - ` - }; - const result1 = await runInlineTest(workspace, { reporter: './reporter.ts', list: true }); - expect(result1.exitCode).toBe(0); - expect(fs.existsSync(test.info().outputPath('filter.json'))).toBe(true); - - const result2 = await runInlineTest(workspace, { 'last-run-file': 'filter.json' }); - expect(result2.exitCode).toBe(1); - expect(result2.passed).toBe(1); - expect(result2.failed).toBe(1); - expect(result2.outputLines).toEqual(['test1', 'test3']); - - const result3 = await runInlineTest(workspace, { 'last-run-file': 'filter.json', 'last-failed': true }); - expect(result3.exitCode).toBe(1); - expect(result3.passed).toBe(0); - expect(result3.failed).toBe(1); - expect(result3.outputLines).toEqual(['test3']); -}); From d630fd3c8eeb06e58638ae37ec27c561037b47a6 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 24 Sep 2025 05:18:27 -0700 Subject: [PATCH 240/329] Revert "Revert "chore: update xterm dependency (#37412)" (#3:w7539)" (#37546) --- package-lock.json | 36 ++++++++++---------- packages/web/package.json | 4 +-- packages/web/src/components/xtermModule.tsx | 6 ++-- packages/web/src/components/xtermWrapper.tsx | 4 +-- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 416a2d7dfed2f..0396d7fc631de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2264,6 +2264,21 @@ "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", "peer": true }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, "node_modules/@zip.js/zip.js": { "version": "2.7.73", "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.73.tgz", @@ -8032,21 +8047,6 @@ "node": ">=4.0" } }, - "node_modules/xterm": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", - "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", - "deprecated": "This package is now deprecated. Move to @xterm/xterm instead." - }, - "node_modules/xterm-addon-fit": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz", - "integrity": "sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==", - "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", - "peerDependencies": { - "xterm": "^5.0.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -8381,9 +8381,9 @@ "packages/web": { "version": "0.0.0", "dependencies": { - "codemirror": "5.65.18", - "xterm": "^5.1.0", - "xterm-addon-fit": "^0.7.0" + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", + "codemirror": "5.65.18" } } } diff --git a/packages/web/package.json b/packages/web/package.json index 0465fdf4c4a16..0c45c30c7bad5 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -5,7 +5,7 @@ "scripts": {}, "dependencies": { "codemirror": "5.65.18", - "xterm": "^5.1.0", - "xterm-addon-fit": "^0.7.0" + "@xterm/xterm": "^5.5.0", + "@xterm/addon-fit": "^0.10.0" } } diff --git a/packages/web/src/components/xtermModule.tsx b/packages/web/src/components/xtermModule.tsx index 00474787a655b..d04f36ad33a7b 100644 --- a/packages/web/src/components/xtermModule.tsx +++ b/packages/web/src/components/xtermModule.tsx @@ -14,10 +14,10 @@ limitations under the License. */ -import 'xterm/css/xterm.css'; +import '@xterm/xterm/css/xterm.css'; -import { Terminal } from 'xterm'; -import { FitAddon } from 'xterm-addon-fit'; +import { Terminal } from '@xterm/xterm'; +import { FitAddon } from '@xterm/addon-fit'; export type XtermModule = { Terminal: typeof Terminal; diff --git a/packages/web/src/components/xtermWrapper.tsx b/packages/web/src/components/xtermWrapper.tsx index f6b5f5be9ec20..c6e06434a34d1 100644 --- a/packages/web/src/components/xtermWrapper.tsx +++ b/packages/web/src/components/xtermWrapper.tsx @@ -16,8 +16,8 @@ import * as React from 'react'; import './xtermWrapper.css'; -import type { ITheme, Terminal } from 'xterm'; -import type { FitAddon } from 'xterm-addon-fit'; +import type { ITheme, Terminal } from '@xterm/xterm'; +import type { FitAddon } from '@xterm/addon-fit'; import type { XtermModule } from './xtermModule'; import { currentTheme, addThemeListener, removeThemeListener } from '../theme'; import { useMeasure } from '../uiUtils'; From d9613621aee6127222ce73e0db50431ec322cd78 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 24 Sep 2025 10:39:04 -0700 Subject: [PATCH 241/329] chore(ui-mode): switch to only showing Single worker button (#37538) --- packages/trace-viewer/src/ui/uiModeView.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index b5938bd0454f0..1ef78ff0ee8f0 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -100,7 +100,7 @@ export const UIModeView: React.FC<{}> = ({ const [revealSource, setRevealSource] = React.useState(false); const onRevealSource = React.useCallback(() => setRevealSource(true), [setRevealSource]); - const [showBrowser, setShowBrowser] = useSetting('show-browser', false); + const [singleWorker, setSingleWorker] = useSetting('single-worker', false); const [updateSnapshots, setUpdateSnapshots] = useSetting('updateSnapshots', 'missing'); const [hideFiles] = useSetting('hideFiles', false); @@ -289,8 +289,7 @@ export const UIModeView: React.FC<{}> = ({ projects: [...projectFilters].filter(([_, v]) => v).map(([p]) => p), updateSnapshots, reporters: queryParams.reporters, - headed: showBrowser, - workers: showBrowser ? 1 : undefined, + workers: singleWorker ? 1 : undefined, trace: 'on', }); // Clear pending tests in case of interrupt. @@ -301,7 +300,7 @@ export const UIModeView: React.FC<{}> = ({ setTestModel({ ...testModel }); setRunningState(oldState => oldState ? ({ ...oldState, completed: true }) : undefined); }); - }, [projectFilters, isRunningTest, testModel, testServerConnection, updateSnapshots, showBrowser]); + }, [projectFilters, isRunningTest, testModel, testServerConnection, updateSnapshots, singleWorker]); React.useEffect(() => { if (!testServerConnection || !teleSuiteUpdater) @@ -505,7 +504,7 @@ export const UIModeView: React.FC<{}> = ({
              Testing Options
              {testingOptionsVisible && Date: Wed, 24 Sep 2025 11:11:58 -0700 Subject: [PATCH 242/329] chore(mcp): integrate with flakiness dashboard (#37555) --- .github/workflows/tests_mcp.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests_mcp.yml b/.github/workflows/tests_mcp.yml index 3be6a59554561..46d4b5497cb8e 100644 --- a/.github/workflows/tests_mcp.yml +++ b/.github/workflows/tests_mcp.yml @@ -33,14 +33,16 @@ jobs: matrix: os: [ubuntu-latest, macos-15, windows-latest] runs-on: ${{ matrix.os }} + permissions: + id-token: write # This is required for OIDC login (azure/login) to succeed + contents: read # This is required for actions/checkout to succeed steps: - uses: actions/checkout@v5 - - name: Use Node.js 20 - uses: actions/setup-node@v5 + - uses: ./.github/actions/run-test with: - node-version: '20' - cache: 'npm' - - run: npm ci - - run: npm run build - - run: npx playwright install --with-deps - - run: npm run test-mcp + node-version: "20" + command: npm run test-mcp + bot-name: "${{ matrix.os }}" + flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} + flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} + flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} From 2b2554ba9b3553fdc02650faabb90e6797142542 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 24 Sep 2025 11:54:41 -0700 Subject: [PATCH 243/329] devops: drop support for -rc puplishing (#37556) --- .github/workflows/publish_release_npm.yml | 5 ---- utils/publish_all_packages.sh | 30 +++++++---------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/.github/workflows/publish_release_npm.yml b/.github/workflows/publish_release_npm.yml index 8aa10c5710d67..479bdbab2c942 100644 --- a/.github/workflows/publish_release_npm.yml +++ b/.github/workflows/publish_release_npm.yml @@ -23,11 +23,6 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build - - run: utils/publish_all_packages.sh --release-candidate - if: ${{ github.event.release.prerelease }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: utils/publish_all_packages.sh --release - if: ${{ !github.event.release.prerelease }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/utils/publish_all_packages.sh b/utils/publish_all_packages.sh index 23e4d3eca19f6..c2d19becffbc5 100755 --- a/utils/publish_all_packages.sh +++ b/utils/publish_all_packages.sh @@ -15,19 +15,18 @@ trap "cleanup; cd $(pwd -P)" EXIT cd "$(dirname $0)" if [[ $1 == "--help" ]]; then - echo "usage: $(basename $0) [--release|--release-candidate|--alpha|--beta]" + echo "usage: $(basename $0) [--release|--alpha|--beta]" echo echo "Publishes all packages." echo echo "--release publish @latest version of all packages" - echo "--release-candidate publish @rc version of all packages" echo "--alpha publish @next version of all packages" echo "--beta publish @beta version of all packages" exit 1 fi if [[ $# < 1 ]]; then - echo "Please specify either --release, --beta or --alpha or --release-candidate" + echo "Please specify either --release, --beta or --alpha" exit 1 fi @@ -54,33 +53,22 @@ if [[ "$1" == "--release" ]]; then fi # Ensure package version does not contain dash. if [[ "${VERSION}" == *-* ]]; then - echo "ERROR: cannot publish pre-release version with --release flag" + echo "ERROR: cannot publish pre-release version ${VERSION} with --release flag" exit 1 fi NPM_PUBLISH_TAG="latest" -elif [[ "$1" == "--release-candidate" ]]; then - if [[ -n $(git status -s) ]]; then - echo "ERROR: git status is dirty; some uncommitted changes or untracked files" - exit 1 - fi - # Ensure package version is properly formatted. - if [[ "${VERSION}" != *-rc* ]]; then - echo "ERROR: release candidate version must have a dash" - exit 1 - fi - NPM_PUBLISH_TAG="rc" elif [[ "$1" == "--alpha" ]]; then - # Ensure package version contains alpha and does not contain rc - if [[ "${VERSION}" != *-alpha* || "${VERSION}" == *-rc* ]]; then - echo "ERROR: cannot publish release version with --alpha flag" + # Ensure package version contains alpha. + if [[ "${VERSION}" != *-alpha* ]]; then + echo "ERROR: cannot publish release version ${VERSION} with --alpha flag" exit 1 fi NPM_PUBLISH_TAG="next" elif [[ "$1" == "--beta" ]]; then - # Ensure package version contains dash. - if [[ "${VERSION}" != *-beta* || "${VERSION}" == *-rc* ]]; then - echo "ERROR: cannot publish release version with --beta flag" + # Ensure package version contains beta. + if [[ "${VERSION}" != *-beta* ]]; then + echo "ERROR: cannot publish release version ${VERSION} with --beta flag" exit 1 fi From c798707ddcf6a4d36b4f322ef18f1258e95a60a0 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 24 Sep 2025 12:06:32 -0700 Subject: [PATCH 244/329] chore(mcp): add missing auth environment for flakiness dashboard (#37557) --- .github/workflows/tests_mcp.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests_mcp.yml b/.github/workflows/tests_mcp.yml index 46d4b5497cb8e..60d66e24497e1 100644 --- a/.github/workflows/tests_mcp.yml +++ b/.github/workflows/tests_mcp.yml @@ -28,6 +28,8 @@ env: jobs: test_mcp: name: ${{ matrix.os }} + # Used for authentication of flakiness upload + environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} strategy: fail-fast: false matrix: From bd83bb2fb6d9f2e891d81a7808a4e60e4c2a7bc1 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 24 Sep 2025 12:12:14 -0700 Subject: [PATCH 245/329] devops: merge npm publish workflows into single file (#37559) --- ...publish_canary.yml => publish_release.yml} | 34 ++++++++++-------- .github/workflows/publish_release_driver.yml | 36 ------------------- .github/workflows/publish_release_npm.yml | 28 --------------- .../workflows/publish_release_traceviewer.yml | 26 -------------- 4 files changed, 20 insertions(+), 104 deletions(-) rename .github/workflows/{publish_canary.yml => publish_release.yml} (76%) delete mode 100644 .github/workflows/publish_release_driver.yml delete mode 100644 .github/workflows/publish_release_npm.yml delete mode 100644 .github/workflows/publish_release_traceviewer.yml diff --git a/.github/workflows/publish_canary.yml b/.github/workflows/publish_release.yml similarity index 76% rename from .github/workflows/publish_canary.yml rename to .github/workflows/publish_release.yml index 42453f8a37b04..3fbfe457e2d14 100644 --- a/.github/workflows/publish_canary.yml +++ b/.github/workflows/publish_release.yml @@ -7,17 +7,19 @@ on: push: branches: - release-* + release: + types: [published] env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 jobs: - publish-canary: - name: "publish canary NPM" + publish-npm: + name: "publish NPM" runs-on: ubuntu-24.04 if: github.repository == 'microsoft/playwright' permissions: - id-token: write # This is required for OIDC login (azure/login) to succeed + id-token: write # This is required for OIDC login (azure/login, NPM publish) to succeed contents: read # This is required for actions/checkout to succeed environment: allow-publish-driver-to-cdn # This is required for OIDC login (azure/login) steps: @@ -28,27 +30,26 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build + - name: "@next: publish with commit timestamp (triggered manually)" if: contains(github.ref, 'main') && github.event_name == 'workflow_dispatch' run: | node utils/build/update_canary_version.js --alpha --commit-timestamp utils/publish_all_packages.sh --alpha - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: "@next: publish with today's date (triggered automatically)" - if: contains(github.ref, 'main') && github.event_name != 'workflow_dispatch' + if: contains(github.ref, 'main') && github.event.schedule run: | node utils/build/update_canary_version.js --alpha --today-date utils/publish_all_packages.sh --alpha - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: "@beta: publish with commit timestamp (triggered automatically)" - if: contains(github.ref, 'release') && github.event_name != 'workflow_dispatch' + if: contains(github.ref, 'release') && github.event_name == 'push' run: | node utils/build/update_canary_version.js --beta --commit-timestamp utils/publish_all_packages.sh --beta - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: "publish release to NPM" + if: contains(github.ref, 'release') && github.event_name == 'release' + run: utils/publish_all_packages.sh --release + - name: Azure Login uses: azure/login@v2 with: @@ -57,7 +58,7 @@ jobs: subscription-id: ${{ secrets.AZURE_PW_CDN_SUBSCRIPTION_ID }} - name: build & publish driver env: - AZ_UPLOAD_FOLDER: driver/next + AZ_UPLOAD_FOLDER: ${{ github.event_name == 'release' && 'driver' || 'driver/next' }} run: | utils/build/build-playwright-driver.sh utils/build/upload-playwright-driver.sh @@ -82,8 +83,13 @@ jobs: if: contains(github.ref, 'main') env: GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }} - - name: Deploy BETA + - name: Deploy Beta run: bash utils/build/deploy-trace-viewer.sh --beta - if: contains(github.ref, 'release') + if: contains(github.ref, 'release') && github.event_name == 'push' + env: + GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }} + - name: Deploy Stable + run: bash utils/build/deploy-trace-viewer.sh --stable + if: contains(github.ref, 'release') && github.event_name == 'release' env: GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/publish_release_driver.yml b/.github/workflows/publish_release_driver.yml deleted file mode 100644 index 0f50ec1e50229..0000000000000 --- a/.github/workflows/publish_release_driver.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: "publish release - driver" - -on: - release: - types: [published] - -env: - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - -jobs: - publish-driver-release: - name: "publish playwright driver to CDN" - runs-on: ubuntu-24.04 - if: github.repository == 'microsoft/playwright' - permissions: - id-token: write # This is required for OIDC login (azure/login) to succeed - contents: read # This is required for actions/checkout to succeed - environment: allow-publish-driver-to-cdn # This is required for OIDC login (azure/login) - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 - with: - node-version: 18 - registry-url: 'https://registry.npmjs.org' - - run: npm ci - - run: npm run build - - run: utils/build/build-playwright-driver.sh - - name: Azure Login - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_PW_CDN_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_PW_CDN_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_PW_CDN_SUBSCRIPTION_ID }} - - run: utils/build/upload-playwright-driver.sh - env: - AZ_UPLOAD_FOLDER: driver diff --git a/.github/workflows/publish_release_npm.yml b/.github/workflows/publish_release_npm.yml deleted file mode 100644 index 479bdbab2c942..0000000000000 --- a/.github/workflows/publish_release_npm.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: "publish release - NPM" - -on: - release: - types: [published] - -env: - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - -jobs: - publish-npm-release: - name: "publish to NPM" - runs-on: ubuntu-24.04 - if: github.repository == 'microsoft/playwright' - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 - with: - node-version: 18 - registry-url: 'https://registry.npmjs.org' - - run: npm ci - - run: npm run build - - run: utils/publish_all_packages.sh --release - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/publish_release_traceviewer.yml b/.github/workflows/publish_release_traceviewer.yml deleted file mode 100644 index c7cc5a6580762..0000000000000 --- a/.github/workflows/publish_release_traceviewer.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: "publish release - TraceViewer" - -on: - release: - types: [published] - -jobs: - publish-trace-viewer: - name: "publish Trace Viewer to trace.playwright.dev" - runs-on: ubuntu-24.04 - if: github.repository == 'microsoft/playwright' - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 - with: - node-version: 18 - - uses: actions/create-github-app-token@v2 - id: app-token - with: - app-id: ${{ vars.PLAYWRIGHT_APP_ID }} - private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }} - repositories: trace.playwright.dev - - name: Deploy Stable - run: bash utils/build/deploy-trace-viewer.sh --stable - env: - GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }} From d981aa957dd65b2677895329a6985f831efd8361 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 24 Sep 2025 12:34:15 -0700 Subject: [PATCH 246/329] chore: render agents nicely in vscode (#37548) --- docs/src/test-agents-js.md | 2 +- packages/playwright/src/agents/generateAgents.ts | 13 +++++++------ packages/playwright/src/agents/generator.md | 10 +++++----- packages/playwright/src/agents/healer.md | 8 ++++---- packages/playwright/src/agents/planner.md | 10 +++++----- packages/playwright/src/program.ts | 4 ++-- 6 files changed, 24 insertions(+), 23 deletions(-) diff --git a/docs/src/test-agents-js.md b/docs/src/test-agents-js.md index 3b7c7d46705e0..2130fa3ed6cee 100644 --- a/docs/src/test-agents-js.md +++ b/docs/src/test-agents-js.md @@ -27,7 +27,7 @@ You need to run this command for each agentic loop you will be using: ```bash # Generate agent files for each agentic loop # Visual Studio Code -npx playwright init-agents --loop=vscode +npx playwright init-agents --loop=code # Claude Code npx playwright init-agents --loop=claude # opencode diff --git a/packages/playwright/src/agents/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts index eac59189a696b..63ab8663f0f2c 100644 --- a/packages/playwright/src/agents/generateAgents.ts +++ b/packages/playwright/src/agents/generateAgents.ts @@ -122,7 +122,7 @@ function saveAsClaudeCode(agent: Agent): string { const lines: string[] = []; lines.push(`---`); - lines.push(`name: ${agent.header.name}`); + lines.push(`name: playwright-test-${agent.header.name}`); lines.push(`description: ${agent.header.description}. Examples: ${agent.examples.map(example => `${example}`).join('')}`); lines.push(`tools: ${agent.header.tools.map(tool => asClaudeTool(tool)).join(', ')}`); lines.push(`model: ${agent.header.model}`); @@ -161,10 +161,10 @@ function saveAsOpencodeJson(agents: Agent[]): string { result['agent'] = {}; for (const agent of agents) { const tools: Record = {}; - result['agent'][agent.header.name] = { + result['agent']['playwright-test-' + agent.header.name] = { description: agent.header.description, mode: 'subagent', - prompt: `{file:.opencode/prompts/${agent.header.name}.md}`, + prompt: `{file:.opencode/prompts/playwright-test-${agent.header.name}.md}`, tools, }; for (const tool of agent.header.tools) @@ -197,7 +197,7 @@ export async function initClaudeCodeRepo() { await fs.promises.mkdir('.claude/agents', { recursive: true }); for (const agent of agents) - await writeFile(`.claude/agents/${agent.header.name}.md`, saveAsClaudeCode(agent)); + await writeFile(`.claude/agents/playwright-test-${agent.header.name}.md`, saveAsClaudeCode(agent)); await writeFile('.mcp.json', JSON.stringify({ mcpServers: { @@ -218,6 +218,7 @@ const vscodeToolMap = new Map([ ]); const vscodeToolsOrder = ['createFile', 'createDirectory', 'editFiles', 'fileSearch', 'textSearch', 'listDirectory', 'readFile']; const vscodeToolPrefix = 'test_'; // FIXME: this is ugly, fix VSCode! + function saveAsVSCodeChatmode(agent: Agent): string { function asVscodeTool(tool: string): string | string[] { const [first, second] = tool.split('/'); @@ -256,7 +257,7 @@ export async function initVSCodeRepo() { await fs.promises.mkdir('.github/chatmodes', { recursive: true }); for (const agent of agents) - await writeFile(`.github/chatmodes/${agent.header.name}.chatmode.md`, saveAsVSCodeChatmode(agent)); + await writeFile(`.github/chatmodes/${agent.header.name === 'planner' ? ' ' : ''}🎭 ${agent.header.name}.chatmode.md`, saveAsVSCodeChatmode(agent)); await fs.promises.mkdir('.vscode', { recursive: true }); @@ -290,7 +291,7 @@ export async function initOpencodeRepo() { const prompt = [agent.instructions]; prompt.push(''); prompt.push(...agent.examples.map(example => `${example}`)); - await writeFile(`.opencode/prompts/${agent.header.name}.md`, prompt.join('\n')); + await writeFile(`.opencode/prompts/playwright-test-${agent.header.name}.md`, prompt.join('\n')); } await writeFile('opencode.json', saveAsOpencodeJson(agents)); } diff --git a/packages/playwright/src/agents/generator.md b/packages/playwright/src/agents/generator.md index 3479c03a322e1..d448d42d51f83 100644 --- a/packages/playwright/src/agents/generator.md +++ b/packages/playwright/src/agents/generator.md @@ -1,5 +1,5 @@ --- -name: playwright-test-generator +name: generator description: Use this agent when you need to create automated browser tests using Playwright model: sonnet color: blue @@ -104,9 +104,9 @@ Your process is methodical and thorough: Context: User wants to test a login flow on their web application. user: 'I need a test that logs into my app at localhost:3000 with username admin@test.com and password 123456, then verifies the dashboard page loads' - assistant: 'I'll use the playwright-test-generator agent to create and validate this login test for you' + assistant: 'I'll use the generator agent to create and validate this login test for you' - The user needs a specific browser automation test created, which is exactly what the playwright-test-generator agent + The user needs a specific browser automation test created, which is exactly what the generator agent is designed for. @@ -114,9 +114,9 @@ Your process is methodical and thorough: Context: User has built a new checkout flow and wants to ensure it works correctly. user: 'Can you create a test that adds items to cart, proceeds to checkout, fills in payment details, and confirms the order?' - assistant: 'I'll use the playwright-test-generator agent to build a comprehensive checkout flow test' + assistant: 'I'll use the generator agent to build a comprehensive checkout flow test' - This is a complex user journey that needs to be automated and tested, perfect for the playwright-test-generator + This is a complex user journey that needs to be automated and tested, perfect for the generator agent. diff --git a/packages/playwright/src/agents/healer.md b/packages/playwright/src/agents/healer.md index 3b25a39b7b9be..0f7e7289588a4 100644 --- a/packages/playwright/src/agents/healer.md +++ b/packages/playwright/src/agents/healer.md @@ -1,5 +1,5 @@ --- -name: playwright-test-healer +name: healer description: Use this agent when you need to debug and fix failing Playwright tests color: red model: sonnet @@ -58,17 +58,17 @@ Key principles: Context: A developer has a failing Playwright test that needs to be debugged and fixed. user: 'The login test is failing, can you fix it?' - assistant: 'I'll use the playwright-test-healer agent to debug and fix the failing login test.' + assistant: 'I'll use the healer agent to debug and fix the failing login test.' The user has identified a specific failing test that needs debugging and fixing, which is exactly what the - playwright-test-healer agent is designed for. + healer agent is designed for. Context: After running a test suite, several tests are reported as failing. user: 'Test user-registration.spec.ts is broken after the recent changes' - assistant: 'Let me use the playwright-test-healer agent to investigate and fix the user-registration test.' + assistant: 'Let me use the healer agent to investigate and fix the user-registration test.' A specific test file is failing and needs debugging, which requires the systematic approach of the playwright-test-healer agent. diff --git a/packages/playwright/src/agents/planner.md b/packages/playwright/src/agents/planner.md index 9c2a0ce8b3026..46a269bd6eff8 100644 --- a/packages/playwright/src/agents/planner.md +++ b/packages/playwright/src/agents/planner.md @@ -1,5 +1,5 @@ --- -name: playwright-test-planner +name: planner description: Use this agent when you need to create comprehensive test plan for a web application or website model: sonnet color: green @@ -117,19 +117,19 @@ professional formatting suitable for sharing with development and QA teams. Context: User wants to test a new e-commerce checkout flow. user: 'I need test scenarios for our new checkout process at https://mystore.com/checkout' - assistant: 'I'll use the playwright-test-planner agent to navigate to your checkout page and create comprehensive test + assistant: 'I'll use the planner agent to navigate to your checkout page and create comprehensive test scenarios.' - The user needs test planning for a specific web page, so use the playwright-test-planner agent to explore and create + The user needs test planning for a specific web page, so use the planner agent to explore and create test scenarios. Context: User has deployed a new feature and wants thorough testing coverage. user: 'Can you help me test our new user dashboard at https://app.example.com/dashboard?' - assistant: 'I'll launch the playwright-test-planner agent to explore your dashboard and develop detailed test + assistant: 'I'll launch the planner agent to explore your dashboard and develop detailed test scenarios.' - This requires web exploration and test scenario creation, perfect for the playwright-test-planner agent. + This requires web exploration and test scenario creation, perfect for the planner agent. diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 3f7561a4a3399..81f285fd4bcce 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -178,12 +178,12 @@ function addInitAgentsCommand(program: Command) { const command = program.command('init-agents', { hidden: true }); command.description('Initialize repository agents for the Claude Code'); const option = command.createOption('--loop ', 'Agentic loop provider'); - option.choices(['claude', 'opencode', 'vscode']); + option.choices(['code', 'claude', 'opencode']); command.addOption(option); command.action(async opts => { if (opts.loop === 'opencode') await initOpencodeRepo(); - else if (opts.loop === 'vscode') + else if (opts.loop === 'code') await initVSCodeRepo(); else if (opts.loop === 'claude') await initClaudeCodeRepo(); From e136c38bff9d5eefddaf45aabfc8a49f4987c998 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 24 Sep 2025 12:35:24 -0700 Subject: [PATCH 247/329] chore: follow-up to workflows merge (#37560) --- .github/workflows/publish_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 3fbfe457e2d14..297f5c156b857 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -1,4 +1,4 @@ -name: "publish canary" +name: "publish release - npm, driver, trace viewer" on: workflow_dispatch: @@ -14,7 +14,7 @@ env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 jobs: - publish-npm: + publish-npm-and-driver: name: "publish NPM" runs-on: ubuntu-24.04 if: github.repository == 'microsoft/playwright' From 39867eb049f0717f42380b99ec61509bef13e732 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 24 Sep 2025 12:47:32 -0700 Subject: [PATCH 248/329] devops: npm whoami fails with OIDC workflow (#37562) --- utils/publish_all_packages.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/utils/publish_all_packages.sh b/utils/publish_all_packages.sh index c2d19becffbc5..ab0c0d2383e26 100755 --- a/utils/publish_all_packages.sh +++ b/utils/publish_all_packages.sh @@ -35,11 +35,6 @@ if ! command -v npm >/dev/null; then exit 1 fi -if ! npm whoami >/dev/null 2>&1; then - echo "ERROR: NPM is not logged in." - exit 1 -fi - cd .. NPM_PUBLISH_TAG="next" From b14bf3ba8e67a2b0b24d104ec389fddaadac8923 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 24 Sep 2025 20:55:01 +0100 Subject: [PATCH 249/329] chore: bump installation tests timeout to 45 (#37558) --- .github/workflows/tests_primary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests_primary.yml b/.github/workflows/tests_primary.yml index 68af6addae671..9037cf31ae921 100644 --- a/.github/workflows/tests_primary.yml +++ b/.github/workflows/tests_primary.yml @@ -220,7 +220,7 @@ jobs: - macos-latest - windows-latest runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 45 permissions: id-token: write # This is required for OIDC login (azure/login) to succeed contents: read # This is required for actions/checkout to succeed From d5b4c6535b61d8b1a936b436debb19679735de85 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 24 Sep 2025 20:55:33 +0100 Subject: [PATCH 250/329] feat(test runner): --test-list and --test-list-invert (#37552) --- docs/src/test-cli-js.md | 25 ++++ packages/playwright/src/common/config.ts | 2 + packages/playwright/src/program.ts | 4 + packages/playwright/src/runner/loadUtils.ts | 35 +++++- packages/playwright/src/runner/tasks.ts | 17 ++- packages/playwright/src/util.ts | 17 ++- tests/playwright-test/test-list.spec.ts | 132 ++++++++++++++++++++ utils/doclint/cli.js | 2 +- 8 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 tests/playwright-test/test-list.spec.ts diff --git a/docs/src/test-cli-js.md b/docs/src/test-cli-js.md index 0688e328d767a..d45ee1d6a3409 100644 --- a/docs/src/test-cli-js.md +++ b/docs/src/test-cli-js.md @@ -103,6 +103,8 @@ npx playwright test --ui | `--reporter ` | Reporter to use, comma-separated, can be "dot", "line", "list", or others (default: "list"). You can also pass a path to a custom reporter file. | | `--retries ` | Maximum retry count for flaky tests, zero for no retries (default: no retries). | | `--shard ` | Shard tests and execute only the selected shard, specified in the form "current/all", 1-based, e.g., "3/5". | +| `--test-list ` | Path to a file containing a list of tests to run. See [test list](#test-list) for details. | +| `--test-list-invert ` | Path to a file containing a list of tests to skip. See [test list](#test-list) for details. | | `--timeout ` | Specify test timeout threshold in milliseconds, zero for unlimited (default: 30 seconds). | | `--trace ` | Force tracing mode, can be `on`, `off`, `on-first-retry`, `on-all-retries`, `retain-on-failure`, `retain-on-first-failure`. | | `--tsconfig ` | Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately). | @@ -113,6 +115,29 @@ npx playwright test --ui | `--update-source-method [mode]` | Update snapshots with actual results. Possible values are "patch" (default), "3way" and "overwrite". "Patch" creates a unified diff file that can be used to update the source code later. "3way" generates merge conflict markers in source code. "Overwrite" overwrites the source code with the new snapshot values.| | `-x` | Stop after the first failure. | +#### Test list + +Options `--test-list` and `--test-list-invert` accept a path to a test list file. This file should list tests in the format similar to the output produced in `--list` mode. + +```txt +# This is a test list file. +# It can include comments and empty lines. + +# Fully qualified test with a project: +[chromium] › path/to/example.spec.ts:3:9 › suite › nested suite › example test + +# This test is included for all projects: +path/to/example.spec.ts:3:9 › example test + +# Use "›" or ">" as a separator: +[firefox] > example.spec.ts > suite > nested suite > example test + +# Line/column numbers are completely ignored, you can omit them. +# Three entries below refer to the same test: +example.spec.ts › example test +example.spec.ts:15 › example test +example.spec.ts:42:42 › example test +``` ### Show Report diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index d6ef36ba35d5e..50eb2ac8a7c6c 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -57,6 +57,8 @@ export class FullConfigInternal { cliListOnly = false; cliPassWithNoTests?: boolean; cliLastFailed?: boolean; + cliTestList?: string; + cliTestListInvert?: string; preOnlyTestFilters: TestCaseFilter[] = []; postShardTestFilters: TestCaseFilter[] = []; defineConfigWasUsed = false; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 81f285fd4bcce..0ed4fd76d44cd 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -203,6 +203,8 @@ async function runTests(args: string[], opts: { [key: string]: any }) { config.cliProjectFilter = opts.project || undefined; config.cliPassWithNoTests = !!opts.passWithNoTests; config.cliLastFailed = !!opts.lastFailed; + config.cliTestList = opts.testList ? path.resolve(process.cwd(), opts.testList) : undefined; + config.cliTestListInvert = opts.testListInvert ? path.resolve(process.cwd(), opts.testListInvert) : undefined; // Evaluate project filters against config before starting execution. This enables a consistent error message across run modes filterProjects(config.projects, config.cliProjectFilter); @@ -400,6 +402,8 @@ const testOptions: [string, { description: string, choices?: string[], preset?: ['--reporter ', { description: `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")` }], ['--retries ', { description: `Maximum retry count for flaky tests, zero for no retries (default: no retries)` }], ['--shard ', { description: `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"` }], + ['--test-list ', { description: `Path to a file containing a list of tests to run. See https://playwright.dev/docs/test-cli for more details.` }], + ['--test-list-invert ', { description: `Path to a file containing a list of tests to skip. See https://playwright.dev/docs/test-cli for more details.` }], ['--timeout ', { description: `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${defaultTimeout})` }], ['--trace ', { description: `Force tracing mode`, choices: kTraceModes as string[] }], ['--tsconfig ', { description: `Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately)` }], diff --git a/packages/playwright/src/runner/loadUtils.ts b/packages/playwright/src/runner/loadUtils.ts index 065cd0cc4a3be..e61a2cca86165 100644 --- a/packages/playwright/src/runner/loadUtils.ts +++ b/packages/playwright/src/runner/loadUtils.ts @@ -15,9 +15,11 @@ */ import path from 'path'; +import fs from 'fs'; +import { toPosixPath } from 'playwright-core/lib/utils'; import { InProcessLoaderHost, OutOfProcessLoaderHost } from './loaderHost'; -import { createFileFiltersFromArguments, createFileMatcherFromArguments, createTitleMatcher, errorWithFile, forceRegExp } from '../util'; +import { createFileFiltersFromArguments, createFileMatcherFromArguments, createTitleMatcher, errorWithFile, forceRegExp, parseLocationArg } from '../util'; import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils'; import { createTestGroups, filterForShard } from './testGroups'; import { applyRepeatEachIndex, bindFileSuiteToProject, filterByFocusedLine, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; @@ -346,3 +348,34 @@ function sourceMapSources(file: string, cache: Map): string[] return sources; } } + +export async function loadTestList(config: FullConfigInternal, filePath: string): Promise { + try { + const content = await fs.promises.readFile(filePath, 'utf-8'); + const lines = content.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('#')); + const descriptions = lines.map(line => { + const delimiter = line.includes('›') ? '›' : '>'; + const tokens = line.split(delimiter).map(token => token.trim()); + let project: string | undefined; + if (tokens[0].startsWith('[')) { + if (!tokens[0].endsWith(']')) + throw new Error(`Malformed test description: ${line}`); + project = tokens[0].substring(1, tokens[0].length - 1); + tokens.shift(); + } + return { project, file: toPosixPath(parseLocationArg(tokens[0]).file), titlePath: tokens.slice(1) }; + }); + return (test: TestCase) => descriptions.some(d => { + // Note: there is no root yet at the time of filtering. + const [projectName, , ...titles] = test.titlePath(); + if (d.project !== undefined && d.project !== projectName) + return false; + const relativeFile = toPosixPath(path.relative(config.config.rootDir, test.location.file)); + if (relativeFile !== d.file) + return false; + return d.titlePath.length === titles.length && d.titlePath.every((_, index) => titles[index] === d.titlePath[index]); + }); + } catch (e) { + throw errorWithFile(filePath, 'Cannot read test list file: ' + e.message); + } +} diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 7d6245ad877a6..96cadb02d923b 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -23,7 +23,7 @@ import { debug } from 'playwright-core/lib/utilsBundle'; import { Dispatcher } from './dispatcher'; import { FailureTracker } from './failureTracker'; -import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils'; +import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook, loadTestList } from './loadUtils'; import { buildDependentProjects, buildTeardownToSetupsMap, filterProjects } from './projectUtils'; import { applySuggestedRebaselines, clearSuggestedRebaselines } from './rebase'; import { TaskRunner } from './taskRunner'; @@ -271,11 +271,24 @@ export function createLoadTask(mode: 'out-of-process' | 'in-process', options: { testRun.config.preOnlyTestFilters.push(test => changedFiles.has(test.location.file)); } + if (testRun.config.cliTestList) { + const testListFilter = await loadTestList(testRun.config, testRun.config.cliTestList); + testRun.config.preOnlyTestFilters.push(testListFilter); + } + + if (testRun.config.cliTestListInvert) { + const testListInvertFilter = await loadTestList(testRun.config, testRun.config.cliTestListInvert); + testRun.config.preOnlyTestFilters.push(test => !testListInvertFilter(test)); + } + const { rootSuite, topLevelProjects } = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly); testRun.rootSuite = rootSuite; testRun.failureTracker.onRootSuite(rootSuite, topLevelProjects); // Fail when no tests. - if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard && !testRun.config.cliOnlyChanged) { + if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length + && !testRun.config.cliPassWithNoTests + && !testRun.config.config.shard && !testRun.config.cliOnlyChanged + && !testRun.config.cliTestList && !testRun.config.cliTestListInvert) { if (testRun.config.cliArgs.length) { throw new Error([ `No tests found.`, diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index 65040aea914fe..6ab72eb1dbf89 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -85,14 +85,19 @@ export type TestFileFilter = { export type TestCaseFilter = (test: TestCase) => boolean; +export function parseLocationArg(arg: string): { file: string, line: number | null, column: number | null } { + const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg); + return { + file: match ? match[1] : arg, + line: match ? parseInt(match[2], 10) : null, + column: match?.[3] ? parseInt(match[3], 10) : null, + }; +} + export function createFileFiltersFromArguments(args: string[]): TestFileFilter[] { return args.map(arg => { - const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg); - return { - re: forceRegExp(match ? match[1] : arg), - line: match ? parseInt(match[2], 10) : null, - column: match?.[3] ? parseInt(match[3], 10) : null, - }; + const parsed = parseLocationArg(arg); + return { re: forceRegExp(parsed.file), line: parsed.line, column: parsed.column }; }); } diff --git a/tests/playwright-test/test-list.spec.ts b/tests/playwright-test/test-list.spec.ts new file mode 100644 index 0000000000000..c307d461533dc --- /dev/null +++ b/tests/playwright-test/test-list.spec.ts @@ -0,0 +1,132 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; +import fs from 'fs'; +import { test, expect } from './playwright-test-fixtures'; + +const varietyWorkspace = { + 'playwright.config.ts': ` + module.exports = { projects: [{ name: 'p1' }, { name: 'p2' }], testDir: 'tests', }; + `, + 'dir/test.list': ` + # this is a multiline comment + + # with an empty line in between + [p1] > dir1/a.test.ts > test1 + [p2] › dir2${path.sep}b.spec.ts › suite › test2 + + # more comments + dir3/c.spec.ts > test2 + `, + 'empty.list': ` + # nothing to see here + `, + 'ignore.list': ` + [p1] > dir1/a.test.ts > test1 + `, + 'tests/dir1/a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('test1', async () => { console.log('\\n%%a-test1-' + test.info().project.name); }); + test('test2', async () => { console.log('\\n%%a-test2-' + test.info().project.name); }); + `, + 'tests/dir2/b.spec.ts': ` + import { test, expect } from '@playwright/test'; + test.describe('suite', () => { + test('test1', async () => { console.log('\\n%%b-test1-' + test.info().project.name); }); + test.describe(() => { + test('test2', async () => { console.log('\\n%%b-test2-' + test.info().project.name); }); + }); + }); + `, + 'tests/dir3/c.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('test1', async () => { console.log('\\n%%c-test1-' + test.info().project.name); }); + test('test2', async () => { console.log('\\n%%c-test2-' + test.info().project.name); }); + `, +}; + +test('--test-list should work', async ({ runInlineTest }) => { + const result = await runInlineTest(varietyWorkspace, { 'workers': 1, 'test-list': 'dir/test.list' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(4); + expect(result.outputLines).toEqual([ + 'a-test1-p1', + 'c-test2-p1', + 'b-test2-p2', + 'c-test2-p2', + ]); +}); + +test('--test-list-invert should work', async ({ runInlineTest }) => { + const result = await runInlineTest(varietyWorkspace, { 'workers': 1, 'test-list-invert': 'dir/test.list' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(8); + expect(result.outputLines).toEqual([ + 'a-test2-p1', + 'b-test1-p1', + 'b-test2-p1', + 'c-test1-p1', + 'a-test1-p2', + 'a-test2-p2', + 'b-test1-p2', + 'c-test1-p2', + ]); +}); + +test('--test-list applies before --test-list-invert', async ({ runInlineTest }) => { + const result = await runInlineTest(varietyWorkspace, { 'workers': 1, 'test-list': 'dir/test.list', 'test-list-invert': 'ignore.list' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + expect(result.outputLines).toEqual([ + 'c-test2-p1', + 'b-test2-p2', + 'c-test2-p2', + ]); +}); + +test('empty --test-list should work', async ({ runInlineTest }) => { + const result = await runInlineTest(varietyWorkspace, { 'workers': 1, 'test-list': 'empty.list' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(0); + expect(result.outputLines).toEqual([ + ]); +}); + +test('--list output should work for --test-list', async ({ runInlineTest }) => { + const listResult = await runInlineTest(varietyWorkspace, { 'list': true }); + expect(listResult.exitCode).toBe(0); + const lines = listResult.output.split('\n').filter(line => !line.includes('Listing tests') && !line.includes('Total:')); + await fs.promises.writeFile(test.info().outputPath('generated.list'), lines.join('\n'), 'utf-8'); + + const result = await runInlineTest(varietyWorkspace, { 'workers': 1, 'test-list': 'generated.list' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(12); + expect(result.outputLines).toEqual([ + 'a-test1-p1', + 'a-test2-p1', + 'b-test1-p1', + 'b-test2-p1', + 'c-test1-p1', + 'c-test2-p1', + 'a-test1-p2', + 'a-test2-p2', + 'b-test1-p2', + 'b-test2-p2', + 'c-test1-p2', + 'c-test2-p2', + ]); +}); diff --git a/utils/doclint/cli.js b/utils/doclint/cli.js index 958a13e1815ca..33f2846ee0722 100755 --- a/utils/doclint/cli.js +++ b/utils/doclint/cli.js @@ -218,7 +218,7 @@ async function run() { 'Dockerfile', ]); if (!allowedCodeLangs.has(node.codeLang.split(' ')[0])) - throw new Error(`${path.relative(PROJECT_DIR, filePath)} contains code block with invalid code block language ${node.codeLang}`); + throw new Error(`${path.relative(PROJECT_DIR, filePath)} contains code block with invalid code block language "${node.codeLang}"`); } if (node.type.startsWith('h')) { const hash = mdSectionHash(node.text || ''); From 81ecc95ce6bac39efa32efb2cc925bd5075e51c6 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 24 Sep 2025 20:56:29 +0100 Subject: [PATCH 251/329] chore(trace viewer): update actions list filter (#37554) --- packages/trace-viewer/src/ui/metadataView.tsx | 2 +- packages/trace-viewer/src/ui/modelUtil.ts | 13 ++++++++---- packages/trace-viewer/src/ui/settingsView.css | 13 ++++++++++++ packages/trace-viewer/src/ui/settingsView.tsx | 5 +++-- packages/trace-viewer/src/ui/workbench.css | 15 ++++++++++++++ packages/trace-viewer/src/ui/workbench.tsx | 13 +++++++++--- packages/web/src/components/dialog.tsx | 2 +- tests/library/trace-viewer.spec.ts | 20 +++++++++++++++++-- 8 files changed, 70 insertions(+), 13 deletions(-) diff --git a/packages/trace-viewer/src/ui/metadataView.tsx b/packages/trace-viewer/src/ui/metadataView.tsx index 9d2e1c207cda4..ac511a21e3690 100644 --- a/packages/trace-viewer/src/ui/metadataView.tsx +++ b/packages/trace-viewer/src/ui/metadataView.tsx @@ -27,7 +27,7 @@ export const MetadataView: React.FunctionComponent<{ const wallTime = model.wallTime !== undefined ? new Date(model.wallTime).toLocaleString(undefined, { timeZoneName: 'short' }) : undefined; - return
              + return
              Time
              {!!wallTime &&
              start time:{wallTime}
              }
              duration:{msToString(model.endTime - model.startTime)}
              diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index cd5ed0a168945..5f8c3da6e93ed 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -84,6 +84,7 @@ export class MultiTraceModel { readonly testIdAttributeName: string | undefined; readonly sources: Map; resources: ResourceSnapshot[]; + readonly actionCounters: Map; constructor(contexts: ContextEntry[]) { @@ -116,6 +117,13 @@ export class MultiTraceModel { this.resources.sort((a1, a2) => a1._monotonicTime! - a2._monotonicTime!); this.errorDescriptors = this.hasStepData ? this._errorDescriptorsFromTestRunner() : this._errorDescriptorsFromActions(); this.sources = collectSources(this.actions, this.errorDescriptors); + + this.actionCounters = new Map(); + for (const action of this.actions) { + action.group = action.group ?? getActionGroup({ type: action.class, method: action.method }); + if (action.group) + this.actionCounters.set(action.group, 1 + (this.actionCounters.get(action.group) || 0)); + } } failedAction() { @@ -125,10 +133,7 @@ export class MultiTraceModel { filteredActions(actionsFilter: ActionGroup[]) { const filter = new Set(actionsFilter); - return this.actions.filter(action => { - const group = action.group ?? getActionGroup({ type: action.class, method: action.method }); - return !group || filter.has(group); - }); + return this.actions.filter(action => !action.group || filter.has(action.group)); } private _errorDescriptorsFromActions(): ErrorDescription[] { diff --git a/packages/trace-viewer/src/ui/settingsView.css b/packages/trace-viewer/src/ui/settingsView.css index 68d8a6a7511d8..62bcc2a2a49dc 100644 --- a/packages/trace-viewer/src/ui/settingsView.css +++ b/packages/trace-viewer/src/ui/settingsView.css @@ -50,3 +50,16 @@ .settings-view .setting-select:not(:first-child) { margin-top: 4px; } + +.settings-view .setting-counter { + padding: 0 4px; + background: var(--vscode-menu-separatorBackground); + border-radius: 9px; + height: 18px; + margin-left: 4px; + line-height: 18px; + min-width: 18px; + display: inline-flex; + align-items: center; + justify-content: center; +} diff --git a/packages/trace-viewer/src/ui/settingsView.tsx b/packages/trace-viewer/src/ui/settingsView.tsx index cc50343817d09..12bb8f1788524 100644 --- a/packages/trace-viewer/src/ui/settingsView.tsx +++ b/packages/trace-viewer/src/ui/settingsView.tsx @@ -20,6 +20,7 @@ import './settingsView.css'; export type Setting = { name: string; title?: string; + count?: number; } & ({ type: 'check', value: boolean; @@ -60,13 +61,13 @@ const renderSetting = (setting: Setting, labelId: string) => { checked={setting.value} onChange={() => setting.set(!setting.value)} /> - + ); case 'select': return ( <> - + ' }); @@ -1969,9 +1969,25 @@ test('should show all actions', async ({ runAndTrace, page }) => { /Navigate to/, /Expect "toBeChecked"/, ]); + await expect(traceViewer.page.getByText('3 hidden', { exact: true })).toBeVisible(); + + await traceViewer.page.getByRole('button', { name: 'Filter actions' }).click(); + await expect(traceViewer.page.getByTestId('actions-filter-dialog')).toMatchAriaSnapshot(` + - dialog: + - checkbox "Getters 1" + - checkbox "Network routes 2" + - checkbox "Configuration" + `); - await traceViewer.showAllActions(); + await traceViewer.page.locator('.setting').getByText('Getters').click(); + await expect(traceViewer.actionTitles).toHaveText([ + /Navigate to/, + /Get attribute "checked"/, + /Expect "toBeChecked"/, + ]); + await expect(traceViewer.page.getByText('2 hidden', { exact: true })).toBeVisible(); + await traceViewer.page.locator('.setting').getByText('Network routes').click(); await expect(traceViewer.actionTitles).toHaveText([ /Route requests/, /Navigate to/, From 805f96ff2d970772c534b58c9f5d360dff89d3d9 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 24 Sep 2025 13:49:28 -0700 Subject: [PATCH 252/329] devops: update npm to latest for OIDC (#37563) --- .github/workflows/publish_release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 297f5c156b857..6944d7388b6a3 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -15,7 +15,7 @@ env: jobs: publish-npm-and-driver: - name: "publish NPM" + name: "publish NPM and driver" runs-on: ubuntu-24.04 if: github.repository == 'microsoft/playwright' permissions: @@ -28,6 +28,9 @@ jobs: with: node-version: 18 registry-url: 'https://registry.npmjs.org' + # Ensure npm 11.5.1 or later is installed (for OIDC npm publishing) + - name: Update npm + run: npm install -g npm@latest - run: npm ci - run: npm run build From 23dc415b363e43c374a60b9680d6c26310069ae9 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 24 Sep 2025 14:03:42 -0700 Subject: [PATCH 253/329] devops: use node 20 instead of 18 on CI (#37564) --- .github/workflows/create_test_report.yml | 2 +- .github/workflows/infra.yml | 4 ++-- .github/workflows/publish_release.yml | 4 ++-- .github/workflows/publish_release_docker.yml | 2 +- .github/workflows/roll_browser_into_playwright.yml | 2 +- .github/workflows/roll_driver_nodejs.yml | 2 +- .github/workflows/tests_primary.yml | 4 ++-- .github/workflows/tests_secondary.yml | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/create_test_report.yml b/.github/workflows/create_test_report.yml index 9fbb88b2ae82d..5afe85e0c72d9 100644 --- a/.github/workflows/create_test_report.yml +++ b/.github/workflows/create_test_report.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 18 + node-version: 20 - run: npm ci env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 diff --git a/.github/workflows/infra.yml b/.github/workflows/infra.yml index bd9d2262de67f..fa6f958a57515 100644 --- a/.github/workflows/infra.yml +++ b/.github/workflows/infra.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 18 + node-version: 20 - run: npm ci - run: npm run build - run: npx playwright install --with-deps @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 18 + node-version: 20 - uses: actions/setup-python@v6 with: python-version: '3.11' diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 6944d7388b6a3..27dbe70a5e47c 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 18 + node-version: 20 registry-url: 'https://registry.npmjs.org' # Ensure npm 11.5.1 or later is installed (for OIDC npm publishing) - name: Update npm @@ -74,7 +74,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 18 + node-version: 20 - uses: actions/create-github-app-token@v2 id: app-token with: diff --git a/.github/workflows/publish_release_docker.yml b/.github/workflows/publish_release_docker.yml index 061e70e0b938b..f038bd5aad300 100644 --- a/.github/workflows/publish_release_docker.yml +++ b/.github/workflows/publish_release_docker.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 18 + node-version: 20 registry-url: 'https://registry.npmjs.org' - name: Set up Docker QEMU for arm64 docker builds uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/roll_browser_into_playwright.yml b/.github/workflows/roll_browser_into_playwright.yml index ecf8b27f94abf..154f52feaf302 100644 --- a/.github/workflows/roll_browser_into_playwright.yml +++ b/.github/workflows/roll_browser_into_playwright.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 18 + node-version: 20 - run: npm ci - run: npm run build - name: Install dependencies diff --git a/.github/workflows/roll_driver_nodejs.yml b/.github/workflows/roll_driver_nodejs.yml index 4da2cf01143e0..baafef164a91a 100644 --- a/.github/workflows/roll_driver_nodejs.yml +++ b/.github/workflows/roll_driver_nodejs.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 18 + node-version: 20 - run: node utils/build/update-playwright-driver-version.mjs - name: Prepare branch id: prepare-branch diff --git a/.github/workflows/tests_primary.yml b/.github/workflows/tests_primary.yml index 9037cf31ae921..07c5b40867929 100644 --- a/.github/workflows/tests_primary.yml +++ b/.github/workflows/tests_primary.yml @@ -146,7 +146,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 18 + node-version: 20 - run: npm ci - run: npm run build @@ -182,7 +182,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 18 + node-version: 20 - run: npm ci env: DEBUG: pw:install diff --git a/.github/workflows/tests_secondary.yml b/.github/workflows/tests_secondary.yml index 30cb4cb856bc1..cf7f678370f9f 100644 --- a/.github/workflows/tests_secondary.yml +++ b/.github/workflows/tests_secondary.yml @@ -299,7 +299,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: - node-version: 18 + node-version: 20 - run: npm ci - run: npm run build - run: utils/build/build-playwright-driver.sh From 4218714a2b490fee7e5fd38398ab3ebee934bb98 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 24 Sep 2025 14:51:01 -0700 Subject: [PATCH 254/329] devops: provenance data is generated by default for trusted publishers (#37567) --- utils/publish_all_packages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/publish_all_packages.sh b/utils/publish_all_packages.sh index ab0c0d2383e26..20d86d8856747 100755 --- a/utils/publish_all_packages.sh +++ b/utils/publish_all_packages.sh @@ -77,7 +77,7 @@ echo "==================== Publishing version ${VERSION} ================" node ./utils/workspace.js --ensure-consistent node ./utils/workspace.js --list-public-package-paths | while read package do - npm publish --access=public ${package} --tag="${NPM_PUBLISH_TAG}" --provenance + npm publish --access=public ${package} --tag="${NPM_PUBLISH_TAG}" done echo "Done." From aa0f249dc9e06201912d498a08ee0a96d428a942 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 24 Sep 2025 14:52:25 -0700 Subject: [PATCH 255/329] chore: seed code agents for examples/todomvc (#37561) --- .../ \360\237\216\255 planner.chatmode.md" | 92 +++++++++++++++++++ .../\360\237\216\255 generator.chatmode.md" | 79 ++++++++++++++++ .../\360\237\216\255 healer.chatmode.md" | 44 +++++++++ 3 files changed, 215 insertions(+) create mode 100644 "examples/todomvc/.github/chatmodes/ \360\237\216\255 planner.chatmode.md" create mode 100644 "examples/todomvc/.github/chatmodes/\360\237\216\255 generator.chatmode.md" create mode 100644 "examples/todomvc/.github/chatmodes/\360\237\216\255 healer.chatmode.md" diff --git "a/examples/todomvc/.github/chatmodes/ \360\237\216\255 planner.chatmode.md" "b/examples/todomvc/.github/chatmodes/ \360\237\216\255 planner.chatmode.md" new file mode 100644 index 0000000000000..c3dc932df954c --- /dev/null +++ "b/examples/todomvc/.github/chatmodes/ \360\237\216\255 planner.chatmode.md" @@ -0,0 +1,92 @@ +--- +description: Use this agent when you need to create comprehensive test plan for a web application or website. +tools: ['createFile', 'createDirectory', 'fileSearch', 'textSearch', 'listDirectory', 'readFile', 'test_browser_click', 'test_browser_close', 'test_browser_console_messages', 'test_browser_drag', 'test_browser_evaluate', 'test_browser_file_upload', 'test_browser_handle_dialog', 'test_browser_hover', 'test_browser_navigate', 'test_browser_navigate_back', 'test_browser_network_requests', 'test_browser_press_key', 'test_browser_select_option', 'test_browser_snapshot', 'test_browser_take_screenshot', 'test_browser_type', 'test_browser_wait_for', 'test_setup_page'] +--- + +You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test +scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage +planning. + +You will: + +1. **Navigate and Explore** + - Invoke the `test_setup_page` tool once to set up page before using any other tools + - Explore the browser snapshot + - Do not take screenshots unless absolutely necessary + - Use browser_* tools to navigate and discover interface + - Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality + +2. **Analyze User Flows** + - Map out the primary user journeys and identify critical paths through the application + - Consider different user types and their typical behaviors + +3. **Design Comprehensive Scenarios** + + Create detailed test scenarios that cover: + - Happy path scenarios (normal user behavior) + - Edge cases and boundary conditions + - Error handling and validation + +4. **Structure Test Plans** + + Each scenario must include: + - Clear, descriptive title + - Detailed step-by-step instructions + - Expected outcomes where appropriate + - Assumptions about starting state (always assume blank/fresh state) + - Success criteria and failure conditions + +5. **Create Documentation** + + Save your test plan as requested: + - Executive summary of the tested page/application + - Individual scenarios as separate sections + - Each scenario formatted with numbered steps + - Clear expected results for verification + + +# TodoMVC Application - Comprehensive Test Plan + +## Application Overview + +The TodoMVC application is a React-based todo list manager that provides core task management functionality. The +application features: + +- **Task Management**: Add, edit, complete, and delete individual todos +- **Bulk Operations**: Mark all todos as complete/incomplete and clear all completed todos +- **Filtering**: View todos by All, Active, or Completed status +- **URL Routing**: Support for direct navigation to filtered views via URLs +- **Counter Display**: Real-time count of active (incomplete) todos +- **Persistence**: State maintained during session (browser refresh behavior not tested) + +## Test Scenarios + +### 1. Adding New Todos + +**Seed:** `tests/seed.spec.ts` + +#### 1.1 Add Valid Todo +**Steps:** +1. Click in the "What needs to be done?" input field +2. Type "Buy groceries" +3. Press Enter key + +**Expected Results:** +- Todo appears in the list with unchecked checkbox +- Counter shows "1 item left" +- Input field is cleared and ready for next entry +- Todo list controls become visible (Mark all as complete checkbox) + +#### 1.2 +... + + +**Quality Standards**: +- Write steps that are specific enough for any tester to follow +- Include negative testing scenarios +- Ensure scenarios are independent and can be run in any order + +**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and +professional formatting suitable for sharing with development and QA teams. +Context: User wants to test a new e-commerce checkout flow. user: 'I need test scenarios for our new checkout process at https://mystore.com/checkout' assistant: 'I'll use the planner agent to navigate to your checkout page and create comprehensive test scenarios.' The user needs test planning for a specific web page, so use the planner agent to explore and create test scenarios. +Context: User has deployed a new feature and wants thorough testing coverage. user: 'Can you help me test our new user dashboard at https://app.example.com/dashboard?' assistant: 'I'll launch the planner agent to explore your dashboard and develop detailed test scenarios.' This requires web exploration and test scenario creation, perfect for the planner agent. \ No newline at end of file diff --git "a/examples/todomvc/.github/chatmodes/\360\237\216\255 generator.chatmode.md" "b/examples/todomvc/.github/chatmodes/\360\237\216\255 generator.chatmode.md" new file mode 100644 index 0000000000000..79d660e831260 --- /dev/null +++ "b/examples/todomvc/.github/chatmodes/\360\237\216\255 generator.chatmode.md" @@ -0,0 +1,79 @@ +--- +description: Use this agent when you need to create automated browser tests using Playwright. +tools: ['createFile', 'createDirectory', 'fileSearch', 'textSearch', 'listDirectory', 'readFile', 'test_browser_click', 'test_browser_drag', 'test_browser_evaluate', 'test_browser_file_upload', 'test_browser_handle_dialog', 'test_browser_hover', 'test_browser_navigate', 'test_browser_press_key', 'test_browser_select_option', 'test_browser_snapshot', 'test_browser_type', 'test_browser_verify_element_visible', 'test_browser_verify_list_visible', 'test_browser_verify_text_visible', 'test_browser_verify_value', 'test_browser_wait_for', 'test_setup_page'] +--- + +You are a Playwright Test Generator, an expert in browser automation and end-to-end testing. +Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate +application behavior. + +Your process is methodical and thorough: + +1. **Scenario Analysis** + - Carefully analyze the test scenario provided, identifying all user actions, + expected outcomes and validation points + +2. **Interactive Execution** + - For each scenario, start with the `test_setup_page` tool to set up page for the scenario + - Use Playwright tools to manually execute each step of the scenario in real-time + - Verify that each action works as expected + - Identify the correct locators and interaction patterns + - Observe actual application behavior and responses + - Validate that assertions will work correctly + +3. **Test Code Generation** + + After successfully completing the manual execution, generate clean, maintainable + @playwright/test source code that follows following convention: + + - One file per scenario, one test in a file + - File name must be fs-friendly scenario name + - Test must be placed in a describe matching the top-level test plan item + - Test title must match the scenario name + - Includes a comment with the step text before each step execution + + + For following plan: + + ```markdown file=specs/plan.md + ### 1. Adding New Todos + **Seed:** `tests/seed.spec.ts` + + #### 1.1 Add Valid Todo + **Steps:** + 1. Click in the "What needs to be done?" input field + + #### 1.2 Add Multiple Todos + ... + ``` + + Following file is generated: + + ```ts file=add-valid-todo.spec.ts + // spec: specs/plan.md + // seed: tests/seed.spec.ts + + test.describe('Adding New Todos', () => { + test('Add Valid Todo', async { page } => { + // 1. Click in the "What needs to be done?" input field + await page.click(...); + + ... + }); + }); + ``` + + +4. **Best practices**: + - Each test has clear, descriptive assertions that validate the expected behavior + - Includes proper error handling and meaningful failure messages + - Uses Playwright best practices (page.waitForLoadState, expect.toBeVisible, etc.) + - Do not improvise, do not add directives that were not asked for + - Uses reliable locators (preferring data-testid, role-based, or text-based selectors over fragile CSS selectors) + - Uses local variables for locators that are used multiple times + - Uses explicit waits rather than arbitrary timeouts + - Never waits for networkidle or use other discouraged or deprecated apis + - Is self-contained and can run independently + - Is deterministic and not prone to flaky behavior +Context: User wants to test a login flow on their web application. user: 'I need a test that logs into my app at localhost:3000 with username admin@test.com and password 123456, then verifies the dashboard page loads' assistant: 'I'll use the generator agent to create and validate this login test for you' The user needs a specific browser automation test created, which is exactly what the generator agent is designed for. +Context: User has built a new checkout flow and wants to ensure it works correctly. user: 'Can you create a test that adds items to cart, proceeds to checkout, fills in payment details, and confirms the order?' assistant: 'I'll use the generator agent to build a comprehensive checkout flow test' This is a complex user journey that needs to be automated and tested, perfect for the generator agent. \ No newline at end of file diff --git "a/examples/todomvc/.github/chatmodes/\360\237\216\255 healer.chatmode.md" "b/examples/todomvc/.github/chatmodes/\360\237\216\255 healer.chatmode.md" new file mode 100644 index 0000000000000..b2de43deb9458 --- /dev/null +++ "b/examples/todomvc/.github/chatmodes/\360\237\216\255 healer.chatmode.md" @@ -0,0 +1,44 @@ +--- +description: Use this agent when you need to debug and fix failing Playwright tests. +tools: ['createFile', 'createDirectory', 'editFiles', 'fileSearch', 'textSearch', 'listDirectory', 'readFile', 'test_browser_evaluate', 'test_browser_generate_locator', 'test_browser_snapshot', 'test_debug', 'test_list', 'test_run'] +--- + +You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and +resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix +broken Playwright tests using a methodical approach. + +Your workflow: +1. **Initial Execution**: Run all tests using playwright_test_run_test tool to identify failing tests +2. **Debug failed tests**: For each failing test run playwright_test_debug_test. +3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to: + - Examine the error details + - Capture page snapshot to understand the context + - Analyze selectors, timing issues, or assertion failures +4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining: + - Element selectors that may have changed + - Timing and synchronization issues + - Data dependencies or test environment problems + - Application changes that broke test assumptions +5. **Code Remediation**: Edit the test code to address identified issues, focusing on: + - Updating selectors to match current application state + - Fixing assertions and expected values + - Improving test reliability and maintainability + - For inherently dynamic data, utilize regular expressions to produce resilient locators +6. **Verification**: Restart the test after each fix to validate the changes +7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly + +Key principles: +- Be systematic and thorough in your debugging approach +- Document your findings and reasoning for each fix +- Prefer robust, maintainable solutions over quick hacks +- Use Playwright best practices for reliable test automation +- If multiple errors exist, fix them one at a time and retest +- Provide clear explanations of what was broken and how you fixed it +- You will continue this process until the test runs successfully without any failures or errors. +- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme() + so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead + of the expected behavior. +- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test. +- Never wait for networkidle or use other discouraged or deprecated apis +Context: A developer has a failing Playwright test that needs to be debugged and fixed. user: 'The login test is failing, can you fix it?' assistant: 'I'll use the healer agent to debug and fix the failing login test.' The user has identified a specific failing test that needs debugging and fixing, which is exactly what the healer agent is designed for. +Context: After running a test suite, several tests are reported as failing. user: 'Test user-registration.spec.ts is broken after the recent changes' assistant: 'Let me use the healer agent to investigate and fix the user-registration test.' A specific test file is failing and needs debugging, which requires the systematic approach of the playwright-test-healer agent. \ No newline at end of file From f3eadc590ea73c1c43315cd0aece9289d0dd08cf Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 24 Sep 2025 15:20:00 -0700 Subject: [PATCH 256/329] fix: fix the mcp flakiness upload (#37569) --- .github/workflows/tests_mcp.yml | 2 +- tests/mcp/playwright.config.ts | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests_mcp.yml b/.github/workflows/tests_mcp.yml index 60d66e24497e1..1beebe5249d9c 100644 --- a/.github/workflows/tests_mcp.yml +++ b/.github/workflows/tests_mcp.yml @@ -44,7 +44,7 @@ jobs: with: node-version: "20" command: npm run test-mcp - bot-name: "${{ matrix.os }}" + bot-name: "mcp-${{ matrix.os }}" flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} diff --git a/tests/mcp/playwright.config.ts b/tests/mcp/playwright.config.ts index 7edcbb1687481..48208f2500dcb 100644 --- a/tests/mcp/playwright.config.ts +++ b/tests/mcp/playwright.config.ts @@ -13,10 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import * as path from 'path'; import { defineConfig } from '@playwright/test'; import type { TestOptions } from './fixtures'; +import type { ReporterDescription } from '@playwright/test'; + +const outputDir = path.join(__dirname, '..', '..', 'test-results'); + +const reporters = () => { + const result: ReporterDescription[] = process.env.CI ? [ + ['dot'], + ['json', { outputFile: path.join(outputDir, 'report.json') }], + ['blob', { outputDir: path.join(__dirname, '..', '..', 'blob-report'), fileName: `${process.env.PWTEST_BOT_NAME}.zip` }], + ] : [ + ['list'] + ]; + return result; +}; export default defineConfig({ testDir: './', @@ -24,7 +39,7 @@ export default defineConfig({ fullyParallel: true, forbidOnly: !!process.env.CI, workers: process.env.CI ? 2 : undefined, - reporter: 'list', + reporter: reporters(), projects: [ { name: 'chrome' }, { name: 'chromium', use: { mcpBrowser: 'chromium' } }, From a2e470b92d362c7f3180f015c5c90dc2de08737b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 25 Sep 2025 12:57:03 +0100 Subject: [PATCH 257/329] chore: deprecate layout selectors in the docs (#37577) --- docs/src/other-locators.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/src/other-locators.md b/docs/src/other-locators.md index c7f6e234d1e7f..0fda4d2590aba 100644 --- a/docs/src/other-locators.md +++ b/docs/src/other-locators.md @@ -236,8 +236,10 @@ may be useful for specifying a list of extra conditions on an element. ### CSS: matching elements based on layout -:::note -Matching based on layout may produce unexpected results. For example, a different element could be matched when layout changes by one pixel. +:::warning +Layout selectors are deprecated and may be removed in the future. Matching based on layout may produce unexpected results. For example, a different element could be matched when layout changes by one pixel. + +We recommend prioritizing [user-visible locators](./locators.md#quick-guide) instead. ::: Sometimes, it is hard to come up with a good selector to the target element when it lacks distinctive features. In this case, using Playwright layout CSS pseudo-classes could help. These can be combined with regular CSS to pinpoint one of the multiple choices. From a1be1e9e2b6c309775a0268386b9c3e0f5322acd Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:37:56 +0200 Subject: [PATCH 258/329] chore(driver): roll driver to recent Node.js LTS version (#37574) Co-authored-by: microsoft-playwright-automation[bot] <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> --- utils/build/build-playwright-driver.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/build/build-playwright-driver.sh b/utils/build/build-playwright-driver.sh index 6a09a57e57cff..aadf190edeea3 100755 --- a/utils/build/build-playwright-driver.sh +++ b/utils/build/build-playwright-driver.sh @@ -4,7 +4,7 @@ set -x trap "cd $(pwd -P)" EXIT SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" -NODE_VERSION="22.19.0" # autogenerated via ./update-playwright-driver-version.mjs +NODE_VERSION="22.20.0" # autogenerated via ./update-playwright-driver-version.mjs cd "$(dirname "$0")" PACKAGE_VERSION=$(node -p "require('../../package.json').version") From 6d41f6162851b3a1f2189b711df43fd939d4d27d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 25 Sep 2025 17:33:54 +0100 Subject: [PATCH 259/329] fix(test_setup_page): respond with error when failed to load the seed (#37576) --- packages/playwright/src/mcp/test/testTools.ts | 12 ++++++++- packages/playwright/src/runner/testRunner.ts | 3 ++- tests/mcp/test-tools.spec.ts | 27 ++++++++++++++++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/playwright/src/mcp/test/testTools.ts b/packages/playwright/src/mcp/test/testTools.ts index ae37d68453075..42e42b28212d7 100644 --- a/packages/playwright/src/mcp/test/testTools.ts +++ b/packages/playwright/src/mcp/test/testTools.ts @@ -127,7 +127,7 @@ export const setupPage = defineTestTool({ description: 'Setup the page for test', inputSchema: z.object({ project: z.string().optional().describe('Project to use for setup. For example: "chromium", if no project is provided uses the first project in the config.'), - testLocation: z.string().optional().describe('Location of the seed test to use for setup. For example: "test/seed/default.spec.ts:20".'), + testLocation: z.string().optional().describe('Location of the seed test to use for setup. For example: "tests/seed.spec.ts" or "tests/seed.spec.ts:20".'), }), type: 'readOnly', }, @@ -162,8 +162,18 @@ test('seed', async ({ page }) => {}); workers: 1, pauseAtEnd: true, disableConfigReporters: true, + failOnLoadErrors: true, }); + // Ideally, we should check that page was indeed created and browser mcp has kicked in. + // However, that is handled in the upper layer, so hard to check here. + if (result.status === 'passed' && !reporter.suite?.allTests().length) { + return { + content: [{ type: 'text', text: 'Error: seed test not found.' }], + isError: true, + }; + } + const text = stream.content(); return { content: [{ type: 'text', text }], diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index 305247156cee6..59b4ce7400de1 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -75,6 +75,7 @@ export type RunTestsParams = { pauseAtEnd?: boolean; doNotRunDepsOutsideProjectFilter?: boolean; disableConfigReporters?: boolean; + failOnLoadErrors?: boolean; }; type FullResultStatus = reporterTypes.FullResult['status']; @@ -344,7 +345,7 @@ export class TestRunner extends EventEmitter { const stop = new ManualPromise(); const tasks = [ createApplyRebaselinesTask(), - createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: params.doNotRunDepsOutsideProjectFilter }), + createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: !!params.failOnLoadErrors, doNotRunDepsOutsideProjectFilter: params.doNotRunDepsOutsideProjectFilter }), ...createRunTestsTasks(config), ]; const testRun = new TestRun(config, reporter, { pauseOnError: params.pauseOnError, pauseAtEnd: params.pauseAtEnd }); diff --git a/tests/mcp/test-tools.spec.ts b/tests/mcp/test-tools.spec.ts index e7e9193fa256b..1ec6ac2ecbae7 100644 --- a/tests/mcp/test-tools.spec.ts +++ b/tests/mcp/test-tools.spec.ts @@ -520,7 +520,7 @@ test('test_setup_page with dependencies', async ({ startClient }) => { expect(await client.callTool({ name: 'test_setup_page', arguments: { - testLocation: 'template.test.ts:3', + testLocation: 'template.test.ts', project: 'chromium', }, })).toHaveTextResponse(`### Paused at end of test. ready for interaction @@ -540,6 +540,31 @@ test('test_setup_page with dependencies', async ({ startClient }) => { expect(fs.existsSync(path.join(baseDir, 'test-results', 'template-template-ignored', 'template.txt'))).toBe(false); }); +test('test_setup_page (loading error)', async ({ startClient }) => { + await writeFiles({ + 'template.test.ts': ` + throw new Error('loading error'); + `, + }); + const { client } = await startClient(); + expect(await client.callTool({ + name: 'test_setup_page', + arguments: { + testLocation: 'template.test.ts', + }, + })).toHaveTextResponse(expect.stringContaining('Error: loading error')); +}); + +test('test_setup_page (wrong test location)', async ({ startClient }) => { + const { client } = await startClient(); + expect(await client.callTool({ + name: 'test_setup_page', + arguments: { + testLocation: 'a.test.ts:6', + }, + })).toHaveTextResponse(`Error: seed test not found.`); +}); + test('test_setup_page (no test location)', async ({ startClient }) => { const { client } = await startClient(); expect(await client.callTool({ From c3a933e9adfe2e17e01ed71f6e086cc814f0d315 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 25 Sep 2025 11:53:09 -0700 Subject: [PATCH 260/329] chore: various seed file resolutions (#37584) --- packages/playwright/src/mcp/test/testTools.ts | 62 +++++++------ tests/mcp/fixtures.ts | 3 +- tests/mcp/test-tools.spec.ts | 86 ++++++++++++++++--- 3 files changed, 113 insertions(+), 38 deletions(-) diff --git a/packages/playwright/src/mcp/test/testTools.ts b/packages/playwright/src/mcp/test/testTools.ts index 42e42b28212d7..be61b38bbffa4 100644 --- a/packages/playwright/src/mcp/test/testTools.ts +++ b/packages/playwright/src/mcp/test/testTools.ts @@ -27,6 +27,7 @@ import { findTopLevelProjects } from '../../runner/projectUtils'; import { defineTestTool } from './testTool'; import { StringWriteStream } from './streams'; +import { fileExistsAsync } from '../../util'; export const listTests = defineTestTool({ schema: { @@ -124,10 +125,10 @@ export const setupPage = defineTestTool({ schema: { name: 'test_setup_page', title: 'Setup page', - description: 'Setup the page for test', + description: 'Setup the page for test.', inputSchema: z.object({ project: z.string().optional().describe('Project to use for setup. For example: "chromium", if no project is provided uses the first project in the config.'), - testLocation: z.string().optional().describe('Location of the seed test to use for setup. For example: "tests/seed.spec.ts" or "tests/seed.spec.ts:20".'), + seedFile: z.string().optional().describe('A seed file contains a single test that is used to setup the page for testing, for example: "tests/seed.spec.ts". If no seed file is provided, a default seed file is created.'), }), type: 'readOnly', }, @@ -137,26 +138,40 @@ export const setupPage = defineTestTool({ const configDir = context.configLocation.configDir; const reporter = new ListReporter({ configDir, screen }); const testRunner = await context.createTestRunner(); - - let testLocation = params.testLocation; - if (!testLocation) { - testLocation = 'default.seed.spec.ts'; - const config = await testRunner.loadConfig(); - const project = params.project ? config.projects.find(p => p.project.name === params.project) : findTopLevelProjects(config)[0]; - const testDir = project?.project.testDir || configDir; - const seedFile = path.join(testDir, testLocation); - if (!fs.existsSync(seedFile)) { - await fs.promises.mkdir(path.dirname(seedFile), { recursive: true }); - await fs.promises.writeFile(seedFile, `import { test, expect } from '@playwright/test'; - -test('seed', async ({ page }) => {}); + const config = await testRunner.loadConfig(); + const project = params.project ? config.projects.find(p => p.project.name === params.project) : findTopLevelProjects(config)[0]; + const testDir = project?.project.testDir || configDir; + + let seedFile: string | undefined; + if (!params.seedFile) { + seedFile = path.resolve(testDir, 'seed.spec.ts'); + await fs.promises.mkdir(path.dirname(seedFile), { recursive: true }); + await fs.promises.writeFile(seedFile, `import { test, expect } from '@playwright/test'; + +test.describe('Test group', () => { + test('seed', async ({ page }) => { + // generate code here. + }); +}); `); + } else { + const candidateFiles: string[] = []; + candidateFiles.push(path.resolve(testDir, params.seedFile)); + candidateFiles.push(path.resolve(configDir, params.seedFile)); + candidateFiles.push(path.resolve(process.cwd(), params.seedFile)); + for (const candidateFile of candidateFiles) { + if (await fileExistsAsync(candidateFile)) { + seedFile = candidateFile; + break; + } } + if (!seedFile) + throw new Error('seed test not found.'); } const result = await testRunner.runTests(reporter, { headed: !context.options?.headless, - locations: [testLocation], + locations: [seedFile], projects: params.project ? [params.project] : undefined, timeout: 0, workers: 1, @@ -167,17 +182,14 @@ test('seed', async ({ page }) => {}); // Ideally, we should check that page was indeed created and browser mcp has kicked in. // However, that is handled in the upper layer, so hard to check here. - if (result.status === 'passed' && !reporter.suite?.allTests().length) { - return { - content: [{ type: 'text', text: 'Error: seed test not found.' }], - isError: true, - }; - } + if (result.status === 'passed' && !reporter.suite?.allTests().length) + throw new Error('seed test not found.'); + + if (result.status !== 'passed') + throw new Error(stream.content()); - const text = stream.content(); return { - content: [{ type: 'text', text }], - isError: result.status !== 'passed', + content: [{ type: 'text', text: 'Done.' }], }; }, }); diff --git a/tests/mcp/fixtures.ts b/tests/mcp/fixtures.ts index c17a0030cc403..a5efa2fa337fa 100644 --- a/tests/mcp/fixtures.ts +++ b/tests/mcp/fixtures.ts @@ -87,7 +87,8 @@ export const test = serverTest.extend arg.startsWith('--config'))) + args.push('--config', test.info().outputPath()); } else { if (process.env.CI && process.platform === 'linux') args.push('--no-sandbox'); diff --git a/tests/mcp/test-tools.spec.ts b/tests/mcp/test-tools.spec.ts index 1ec6ac2ecbae7..44a6b2fa8fc70 100644 --- a/tests/mcp/test-tools.spec.ts +++ b/tests/mcp/test-tools.spec.ts @@ -466,7 +466,7 @@ test('test_setup_page', async ({ startClient }) => { expect(await client.callTool({ name: 'test_setup_page', arguments: { - testLocation: 'a.test.ts:6', + seedFile: 'a.test.ts', }, })).toHaveTextResponse(`### Paused at end of test. ready for interaction @@ -491,6 +491,67 @@ test('test_setup_page', async ({ startClient }) => { }); }); +test('test_setup_page seed resolution', async ({ startClient }) => { + await writeFiles({ + 'playwright.config.ts': ` + module.exports = { + testDir: './tests', + }; + `, + 'tests/seed.test.ts': ` + import { test, expect } from '@playwright/test'; + test('template', async ({ page }) => { + await page.setContent(''); + }); + `, + }); + + const { client } = await startClient(); + + // Relative to test dir. + expect(await client.callTool({ + name: 'test_setup_page', + arguments: { + seedFile: 'seed.test.ts', + }, + })).toHaveTextResponse(expect.stringContaining(`### Paused at end of test.`)); + + // Relative to config dir. + expect(await client.callTool({ + name: 'test_setup_page', + arguments: { + seedFile: 'tests/seed.test.ts', + }, + })).toHaveTextResponse(expect.stringContaining(`### Paused at end of test.`)); +}); + +test('test_setup_page seed resolution - cwd', async ({ startClient }) => { + await writeFiles({ + 'configs/playwright.config.ts': ` + module.exports = { + testDir: '../tests', + }; + `, + 'tests/seed.test.ts': ` + import { test, expect } from '@playwright/test'; + test('template', async ({ page }) => { + await page.setContent(''); + }); + `, + }); + + const { client } = await startClient({ + args: ['--config=configs/playwright.config.ts'], + }); + + expect(await client.callTool({ + name: 'test_setup_page', + arguments: { + seedFile: 'tests/seed.test.ts', + }, + })).toHaveTextResponse(expect.stringContaining(`### Paused at end of test.`)); +}); + test('test_setup_page with dependencies', async ({ startClient }) => { const baseDir = await writeFiles({ 'playwright.config.ts': ` @@ -508,7 +569,7 @@ test('test_setup_page with dependencies', async ({ startClient }) => { require('fs').writeFileSync(testInfo.outputPath('auth.txt'), 'done'); }); `, - 'template.test.ts': ` + 'seed.test.ts': ` import { test, expect } from '@playwright/test'; test('template', async ({ page }, testInfo) => { require('fs').writeFileSync(testInfo.outputPath('template.txt'), 'done'); @@ -520,7 +581,7 @@ test('test_setup_page with dependencies', async ({ startClient }) => { expect(await client.callTool({ name: 'test_setup_page', arguments: { - testLocation: 'template.test.ts', + seedFile: 'seed.test.ts', project: 'chromium', }, })).toHaveTextResponse(`### Paused at end of test. ready for interaction @@ -536,13 +597,13 @@ test('test_setup_page with dependencies', async ({ startClient }) => { // Should pause at the target test, not in a dependency or any other stray project. expect(fs.existsSync(path.join(baseDir, 'test-results', 'auth.setup.ts-auth-setup', 'auth.txt'))).toBe(true); - expect(fs.existsSync(path.join(baseDir, 'test-results', 'template-template-chromium', 'template.txt'))).toBe(true); - expect(fs.existsSync(path.join(baseDir, 'test-results', 'template-template-ignored', 'template.txt'))).toBe(false); + expect(fs.existsSync(path.join(baseDir, 'test-results', 'seed-template-chromium', 'template.txt'))).toBe(true); + expect(fs.existsSync(path.join(baseDir, 'test-results', 'seed-template-ignored', 'template.txt'))).toBe(false); }); test('test_setup_page (loading error)', async ({ startClient }) => { await writeFiles({ - 'template.test.ts': ` + 'seed.test.ts': ` throw new Error('loading error'); `, }); @@ -550,7 +611,7 @@ test('test_setup_page (loading error)', async ({ startClient }) => { expect(await client.callTool({ name: 'test_setup_page', arguments: { - testLocation: 'template.test.ts', + seedFile: 'seed.test.ts', }, })).toHaveTextResponse(expect.stringContaining('Error: loading error')); }); @@ -560,9 +621,10 @@ test('test_setup_page (wrong test location)', async ({ startClient }) => { expect(await client.callTool({ name: 'test_setup_page', arguments: { - testLocation: 'a.test.ts:6', + seedFile: 'a.test.ts', }, - })).toHaveTextResponse(`Error: seed test not found.`); + })).toHaveTextResponse(`### Result +Error: seed test not found.`); }); test('test_setup_page (no test location)', async ({ startClient }) => { @@ -609,8 +671,8 @@ test('test_setup_page chooses top-level project', async ({ startClient }) => { \`\`\` `); - expect(fs.existsSync(path.join(baseDir, 'one', 'default.seed.spec.ts'))).toBe(false); - expect(fs.existsSync(path.join(baseDir, 'two', 'default.seed.spec.ts'))).toBe(true); + expect(fs.existsSync(path.join(baseDir, 'one', 'seed.spec.ts'))).toBe(false); + expect(fs.existsSync(path.join(baseDir, 'two', 'seed.spec.ts'))).toBe(true); }); test('test_setup_page without location respects testsDir', async ({ startClient }) => { @@ -643,5 +705,5 @@ test('test_setup_page without location respects testsDir', async ({ startClient \`\`\` `); - expect(fs.existsSync(test.info().outputPath('tests', 'default.seed.spec.ts'))).toBe(true); + expect(fs.existsSync(test.info().outputPath('tests', 'seed.spec.ts'))).toBe(true); }); From 97f027670ec2d48edb4b09657906a12c8b5792ce Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 25 Sep 2025 22:31:58 +0200 Subject: [PATCH 261/329] Revert "feat(chromium): add ConsoleMessage support for service workers (#37368)" (#37582) --- docs/src/api/class-worker.md | 10 --- packages/playwright-client/types/types.d.ts | 39 ---------- .../src/client/consoleMessage.ts | 4 +- packages/playwright-core/src/client/events.ts | 1 - packages/playwright-core/src/client/worker.ts | 7 -- .../playwright-core/src/protocol/validator.ts | 15 ---- .../src/server/chromium/crServiceWorker.ts | 11 +-- .../src/server/dispatchers/pageDispatcher.ts | 20 +---- packages/playwright-core/src/server/page.ts | 1 - .../src/utils/isomorphic/protocolMetainfo.ts | 1 - packages/playwright-core/types/types.d.ts | 39 ---------- packages/protocol/src/channels.d.ts | 21 ------ packages/protocol/src/protocol.yml | 13 ---- tests/library/chromium/chromium.spec.ts | 73 ------------------- 14 files changed, 4 insertions(+), 251 deletions(-) diff --git a/docs/src/api/class-worker.md b/docs/src/api/class-worker.md index 3298697febff8..fe69a8424889b 100644 --- a/docs/src/api/class-worker.md +++ b/docs/src/api/class-worker.md @@ -58,16 +58,6 @@ foreach(var pageWorker in page.Workers) Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is terminated. -## event: Worker.console -* since: v1.56 -- argument: <[ConsoleMessage]> - -:::note -Console events from Web Workers are dispatched on the page object. Note that console events are only supported on Chromium-based browsers and within Service Workers. -::: - -Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. - ## async method: Worker.evaluate * since: v1.8 - returns: <[Serializable]> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index a7aa4d7d6ff82..d4e4f52c74f85 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -10324,72 +10324,33 @@ export interface Worker { */ on(event: 'close', listener: (worker: Worker) => any): this; - /** - * **NOTE** Console events from Web Workers are dispatched on the page object. Note that console events are only - * supported on Chromium-based browsers and within Service Workers. - * - * Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. - */ - on(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - /** * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. */ once(event: 'close', listener: (worker: Worker) => any): this; - /** - * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. - */ - once(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - /** * Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is * terminated. */ addListener(event: 'close', listener: (worker: Worker) => any): this; - /** - * **NOTE** Console events from Web Workers are dispatched on the page object. Note that console events are only - * supported on Chromium-based browsers and within Service Workers. - * - * Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. - */ - addListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - /** * Removes an event listener added by `on` or `addListener`. */ removeListener(event: 'close', listener: (worker: Worker) => any): this; - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - /** * Removes an event listener added by `on` or `addListener`. */ off(event: 'close', listener: (worker: Worker) => any): this; - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - /** * Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is * terminated. */ prependListener(event: 'close', listener: (worker: Worker) => any): this; - /** - * **NOTE** Console events from Web Workers are dispatched on the page object. Note that console events are only - * supported on Chromium-based browsers and within Service Workers. - * - * Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. - */ - prependListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - url(): string; } diff --git a/packages/playwright-core/src/client/consoleMessage.ts b/packages/playwright-core/src/client/consoleMessage.ts index e186fdad3d2c1..b7b5a9720a04b 100644 --- a/packages/playwright-core/src/client/consoleMessage.ts +++ b/packages/playwright-core/src/client/consoleMessage.ts @@ -26,9 +26,9 @@ type ConsoleMessageLocation = channels.BrowserContextConsoleEvent['location']; export class ConsoleMessage implements api.ConsoleMessage { private _page: Page | null; - private _event: channels.WorkerConsoleEvent; + private _event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent; - constructor(platform: Platform, event: channels.WorkerConsoleEvent, page: Page | null) { + constructor(platform: Platform, event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent, page: Page | null) { this._page = page; this._event = event; if (platform.inspectCustom) diff --git a/packages/playwright-core/src/client/events.ts b/packages/playwright-core/src/client/events.ts index cede73640cf3b..41544b8c35b71 100644 --- a/packages/playwright-core/src/client/events.ts +++ b/packages/playwright-core/src/client/events.ts @@ -87,7 +87,6 @@ export const Events = { Worker: { Close: 'close', - Console: 'console', }, ElectronApplication: { diff --git a/packages/playwright-core/src/client/worker.ts b/packages/playwright-core/src/client/worker.ts index fcac272b131c0..5a5a5fe246c80 100644 --- a/packages/playwright-core/src/client/worker.ts +++ b/packages/playwright-core/src/client/worker.ts @@ -15,7 +15,6 @@ */ import { ChannelOwner } from './channelOwner'; -import { ConsoleMessage } from './consoleMessage'; import { TargetClosedError } from './errors'; import { Events } from './events'; import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle'; @@ -46,13 +45,7 @@ export class Worker extends ChannelOwner implements api. this._context._serviceWorkers.delete(this); this.emit(Events.Worker.Close, this); }); - this._channel.on('console', event => { - this.emit(Events.Worker.Console, new ConsoleMessage(this._page?.context()._platform ?? this._context?._platform!, event, null)); - }); this.once(Events.Worker.Close, () => this._closedScope.close(this._page?._closeErrorWithReason() || new TargetClosedError())); - this._setEventToSubscriptionMapping(new Map([ - [Events.Worker.Console, 'console'], - ])); } url(): string { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index ef91d80dbed84..339a2d1dc7915 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1947,16 +1947,6 @@ scheme.WorkerInitializer = tObject({ url: tString, }); scheme.WorkerCloseEvent = tOptional(tObject({})); -scheme.WorkerConsoleEvent = tObject({ - type: tString, - text: tString, - args: tArray(tChannel(['ElementHandle', 'JSHandle'])), - location: tObject({ - url: tString, - lineNumber: tInt, - columnNumber: tInt, - }), -}); scheme.WorkerEvaluateExpressionParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), @@ -1973,11 +1963,6 @@ scheme.WorkerEvaluateExpressionHandleParams = tObject({ scheme.WorkerEvaluateExpressionHandleResult = tObject({ handle: tChannel(['ElementHandle', 'JSHandle']), }); -scheme.WorkerUpdateSubscriptionParams = tObject({ - event: tEnum(['console']), - enabled: tBoolean, -}); -scheme.WorkerUpdateSubscriptionResult = tOptional(tObject({})); scheme.JSHandleInitializer = tObject({ preview: tString, }); diff --git a/packages/playwright-core/src/server/chromium/crServiceWorker.ts b/packages/playwright-core/src/server/chromium/crServiceWorker.ts index df6e3e1d5d13d..23099867c588e 100644 --- a/packages/playwright-core/src/server/chromium/crServiceWorker.ts +++ b/packages/playwright-core/src/server/chromium/crServiceWorker.ts @@ -14,12 +14,10 @@ * limitations under the License. */ import { Worker } from '../page'; -import { createHandle, CRExecutionContext } from './crExecutionContext'; +import { CRExecutionContext } from './crExecutionContext'; import { CRNetworkManager } from './crNetworkManager'; import { BrowserContext } from '../browserContext'; -import { ConsoleMessage } from '../console'; import * as network from '../network'; -import { toConsoleMessageLocation } from './crProtocolHelper'; import type { CRBrowserContext } from './crBrowser'; import type { CRSession } from './crConnection'; @@ -53,13 +51,6 @@ export class CRServiceWorker extends Worker { // Resume service worker after restart. session._sendMayFail('Runtime.runIfWaitingForDebugger', {}); }); - session.on('Runtime.consoleAPICalled', event => { - if (!this.existingExecutionContext) - return; - const args = event.args.map(o => createHandle(this.existingExecutionContext!, o)); - const message = new ConsoleMessage(null, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace)); - this.emit(Worker.Events.Console, message); - }); } override didClose() { diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index b1585017b5494..3043192996194 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -31,7 +31,6 @@ import { urlMatches } from '../../utils/isomorphic/urlMatch'; import type { Artifact } from '../artifact'; import type { BrowserContext } from '../browserContext'; import type { CRCoverage } from '../chromium/crCoverage'; -import type { ConsoleMessage } from '../console'; import type { Download } from '../download'; import type { FileChooser } from '../fileChooser'; import type { JSHandle } from '../javascript'; @@ -41,6 +40,7 @@ import type { RouteHandler } from '../network'; import type { InitScript, PageBinding } from '../page'; import type * as channels from '@protocol/channels'; import type { Progress } from '@protocol/progress'; +import type { ConsoleMessage } from '../console'; export class PageDispatcher extends Dispatcher implements channels.PageChannel { _type_EventTarget = true; @@ -414,7 +414,6 @@ export class PageDispatcher extends Dispatcher implements channels.WorkerChannel { _type_Worker = true; - private readonly _subscriptions = new Set(); static fromNullable(scope: PageDispatcher | BrowserContextDispatcher, worker: Worker | null): WorkerDispatcher | undefined { if (!worker) @@ -428,23 +427,6 @@ export class WorkerDispatcher extends Dispatcher this._dispatchEvent('close')); - this.addObjectListener(Worker.Events.Console, (message: ConsoleMessage) => { - if (!this._subscriptions.has('console')) - return; - this._dispatchEvent('console', { - type: message.type(), - text: message.text(), - args: message.args().map(a => JSHandleDispatcher.fromJSHandle(this, a)), - location: message.location(), - }); - }); - } - - async updateSubscription(params: channels.WorkerUpdateSubscriptionParams, progress: Progress): Promise { - if (params.enabled) - this._subscriptions.add(params.event); - else - this._subscriptions.delete(params.event); } async evaluateExpression(params: channels.WorkerEvaluateExpressionParams, progress: Progress): Promise { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index e4a54f80fa4fa..fda0e14dcc3ae 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -868,7 +868,6 @@ export class Page extends SdkObject { export class Worker extends SdkObject { static Events = { Close: 'close', - Console: 'console', }; readonly url: string; diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index 5e4ede100e7e3..b66cf30a5a773 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -188,7 +188,6 @@ export const methodMetainfo = new Map any): this; - /** - * **NOTE** Console events from Web Workers are dispatched on the page object. Note that console events are only - * supported on Chromium-based browsers and within Service Workers. - * - * Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. - */ - on(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - /** * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. */ once(event: 'close', listener: (worker: Worker) => any): this; - /** - * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. - */ - once(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - /** * Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is * terminated. */ addListener(event: 'close', listener: (worker: Worker) => any): this; - /** - * **NOTE** Console events from Web Workers are dispatched on the page object. Note that console events are only - * supported on Chromium-based browsers and within Service Workers. - * - * Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. - */ - addListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - /** * Removes an event listener added by `on` or `addListener`. */ removeListener(event: 'close', listener: (worker: Worker) => any): this; - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - /** * Removes an event listener added by `on` or `addListener`. */ off(event: 'close', listener: (worker: Worker) => any): this; - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - /** * Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is * terminated. */ prependListener(event: 'close', listener: (worker: Worker) => any): this; - /** - * **NOTE** Console events from Web Workers are dispatched on the page object. Note that console events are only - * supported on Chromium-based browsers and within Service Workers. - * - * Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. - */ - prependListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; - url(): string; } diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 12f7ad4a84a90..0bc51cbf7fb00 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -3333,25 +3333,13 @@ export type WorkerInitializer = { }; export interface WorkerEventTarget { on(event: 'close', callback: (params: WorkerCloseEvent) => void): this; - on(event: 'console', callback: (params: WorkerConsoleEvent) => void): this; } export interface WorkerChannel extends WorkerEventTarget, Channel { _type_Worker: boolean; evaluateExpression(params: WorkerEvaluateExpressionParams, progress?: Progress): Promise; evaluateExpressionHandle(params: WorkerEvaluateExpressionHandleParams, progress?: Progress): Promise; - updateSubscription(params: WorkerUpdateSubscriptionParams, progress?: Progress): Promise; } export type WorkerCloseEvent = {}; -export type WorkerConsoleEvent = { - type: string, - text: string, - args: JSHandleChannel[], - location: { - url: string, - lineNumber: number, - columnNumber: number, - }, -}; export type WorkerEvaluateExpressionParams = { expression: string, isFunction?: boolean, @@ -3374,18 +3362,9 @@ export type WorkerEvaluateExpressionHandleOptions = { export type WorkerEvaluateExpressionHandleResult = { handle: JSHandleChannel, }; -export type WorkerUpdateSubscriptionParams = { - event: 'console', - enabled: boolean, -}; -export type WorkerUpdateSubscriptionOptions = { - -}; -export type WorkerUpdateSubscriptionResult = void; export interface WorkerEvents { 'close': WorkerCloseEvent; - 'console': WorkerConsoleEvent; } // ----------- JSHandle ----------- diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 84ee9b8a4ab97..7e2173979b69c 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2879,23 +2879,10 @@ Worker: returns: handle: JSHandle - updateSubscription: - internal: true - parameters: - event: - type: enum - literals: - - console - enabled: boolean - events: close: - console: - parameters: - $mixin: ConsoleMessage - JSHandle: type: interface diff --git a/tests/library/chromium/chromium.spec.ts b/tests/library/chromium/chromium.spec.ts index 29a2b079871d1..f254ad3cf044e 100644 --- a/tests/library/chromium/chromium.spec.ts +++ b/tests/library/chromium/chromium.spec.ts @@ -17,7 +17,6 @@ import { contextTest as test, expect } from '../../config/browserTest'; import { playwrightTest } from '../../config/browserTest'; -import { ConsoleMessage } from 'playwright-core'; test('should create a worker from a service worker', async ({ page, server }) => { const [worker] = await Promise.all([ @@ -129,78 +128,6 @@ test('should not create a worker from a shared worker', async ({ page, server }) expect(serviceWorkerCreated).not.toBeTruthy(); }); -test('should emit console messages from service worker', async ({ page, server }) => { - const [worker] = await Promise.all([ - page.context().waitForEvent('serviceworker'), - page.goto(server.PREFIX + '/serviceworkers/empty/sw.html') - ]); - - const [consoleMessage] = await Promise.all([ - new Promise(resolve => worker.once('console', resolve)), - worker.evaluate(() => console.log('hello from service worker', { - i: 'am', - am: 1, - complex: { - yup: true, - } - })) - ]); - - expect(consoleMessage.text()).toContain('hello from service worker'); - expect(consoleMessage.type()).toBe('log'); - const args = consoleMessage.args(); - expect(args).toHaveLength(2); - expect(await args[0].jsonValue()).toBe('hello from service worker'); - expect(await args[1].jsonValue()).toEqual({ - i: 'am', - am: 1, - complex: { - yup: true, - } - }); -}); - -test('should emit different console types from service worker', async ({ page, server }) => { - const [worker] = await Promise.all([ - page.context().waitForEvent('serviceworker'), - page.goto(server.PREFIX + '/serviceworkers/empty/sw.html') - ]); - - const messages: ConsoleMessage[] = []; - worker.on('console', m => messages.push(m)); - - await worker.evaluate(() => { - console.log('log message'); - console.warn('warn message'); - console.error('error message'); - }); - - expect(messages).toHaveLength(3); - expect(messages[0].type()).toBe('log'); - expect(messages[0].text()).toBe('log message'); - expect(messages[1].type()).toBe('warning'); - expect(messages[1].text()).toBe('warn message'); - expect(messages[2].type()).toBe('error'); - expect(messages[2].text()).toBe('error message'); -}); - -test('should capture console.log from ServiceWorker start', async ({ context, page, server }) => { - server.setRoute('/serviceworkers/empty/sw.js', (req, res) => { - res.writeHead(200, 'OK', { 'Content-Type': 'text/javascript' }); - res.write(`console.log('Hello from the first line of sw.js');`); - res.end(); - }); - - const [worker] = await Promise.all([ - context.waitForEvent('serviceworker'), - page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), - ]); - - const consoleMessage = await new Promise(resolve => worker.once('console', resolve)); - expect(consoleMessage.text()).toBe('Hello from the first line of sw.js'); - expect(consoleMessage.type()).toBe('log'); -}); - test('Page.route should work with intervention headers', async ({ server, page }) => { server.setRoute('/intervention', (req, res) => res.end(`