From 1eea72fec981afe2fa7d48c4f5bde3bbf29bb33a Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 2 Oct 2025 10:49:20 -0700 Subject: [PATCH 001/250] chore: mark 1.57.0-next (#37686) --- package-lock.json | 62 +++++++++---------- 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/package.json | 4 +- packages/playwright-webkit/package.json | 4 +- packages/playwright/package.json | 4 +- 17 files changed, 61 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0396d7fc6..d539f5cb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "playwright-internal", - "version": "1.56.0-next", + "version": "1.57.0-next", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "playwright-internal", - "version": "1.56.0-next", + "version": "1.57.0-next", "license": "Apache-2.0", "workspaces": [ "packages/*" @@ -8148,10 +8148,10 @@ "version": "0.0.0" }, "packages/playwright": { - "version": "1.56.0-next", + "version": "1.57.0-next", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" }, "bin": { "playwright": "cli.js" @@ -8165,11 +8165,11 @@ }, "packages/playwright-browser-chromium": { "name": "@playwright/browser-chromium", - "version": "1.56.0-next", + "version": "1.57.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" }, "engines": { "node": ">=18" @@ -8177,11 +8177,11 @@ }, "packages/playwright-browser-firefox": { "name": "@playwright/browser-firefox", - "version": "1.56.0-next", + "version": "1.57.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" }, "engines": { "node": ">=18" @@ -8189,22 +8189,22 @@ }, "packages/playwright-browser-webkit": { "name": "@playwright/browser-webkit", - "version": "1.56.0-next", + "version": "1.57.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" }, "engines": { "node": ">=18" } }, "packages/playwright-chromium": { - "version": "1.56.0-next", + "version": "1.57.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" }, "bin": { "playwright": "cli.js" @@ -8218,14 +8218,14 @@ "version": "0.0.0", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" }, "engines": { "node": ">=18" } }, "packages/playwright-core": { - "version": "1.56.0-next", + "version": "1.57.0-next", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -8236,11 +8236,11 @@ }, "packages/playwright-ct-core": { "name": "@playwright/experimental-ct-core", - "version": "1.56.0-next", + "version": "1.57.0-next", "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.0-next", - "playwright-core": "1.56.0-next", + "playwright": "1.57.0-next", + "playwright-core": "1.57.0-next", "vite": "^6.3.6" }, "engines": { @@ -8249,10 +8249,10 @@ }, "packages/playwright-ct-react": { "name": "@playwright/experimental-ct-react", - "version": "1.56.0-next", + "version": "1.57.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.56.0-next", + "@playwright/experimental-ct-core": "1.57.0-next", "@vitejs/plugin-react": "^4.2.1" }, "bin": { @@ -8264,10 +8264,10 @@ }, "packages/playwright-ct-react17": { "name": "@playwright/experimental-ct-react17", - "version": "1.56.0-next", + "version": "1.57.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.56.0-next", + "@playwright/experimental-ct-core": "1.57.0-next", "@vitejs/plugin-react": "^4.2.1" }, "bin": { @@ -8279,10 +8279,10 @@ }, "packages/playwright-ct-svelte": { "name": "@playwright/experimental-ct-svelte", - "version": "1.56.0-next", + "version": "1.57.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.56.0-next", + "@playwright/experimental-ct-core": "1.57.0-next", "@sveltejs/vite-plugin-svelte": "^5.1.0" }, "bin": { @@ -8297,10 +8297,10 @@ }, "packages/playwright-ct-vue": { "name": "@playwright/experimental-ct-vue", - "version": "1.56.0-next", + "version": "1.57.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.56.0-next", + "@playwright/experimental-ct-core": "1.57.0-next", "@vitejs/plugin-vue": "^5.2.0" }, "bin": { @@ -8311,11 +8311,11 @@ } }, "packages/playwright-firefox": { - "version": "1.56.0-next", + "version": "1.57.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" }, "bin": { "playwright": "cli.js" @@ -8326,10 +8326,10 @@ }, "packages/playwright-test": { "name": "@playwright/test", - "version": "1.56.0-next", + "version": "1.57.0-next", "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.0-next" + "playwright": "1.57.0-next" }, "bin": { "playwright": "cli.js" @@ -8339,11 +8339,11 @@ } }, "packages/playwright-webkit": { - "version": "1.56.0-next", + "version": "1.57.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" }, "bin": { "playwright": "cli.js" diff --git a/package.json b/package.json index 297398297..d495a0af7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "playwright-internal", "private": true, - "version": "1.56.0-next", + "version": "1.57.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 e768e2496..5223d8f54 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.56.0-next", + "version": "1.57.0-next", "description": "Playwright package that automatically installs Chromium", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" } } diff --git a/packages/playwright-browser-firefox/package.json b/packages/playwright-browser-firefox/package.json index 39702a88f..a5d47c473 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.56.0-next", + "version": "1.57.0-next", "description": "Playwright package that automatically installs Firefox", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" } } diff --git a/packages/playwright-browser-webkit/package.json b/packages/playwright-browser-webkit/package.json index 7e7a32e66..3b0dd5d08 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.56.0-next", + "version": "1.57.0-next", "description": "Playwright package that automatically installs WebKit", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" } } diff --git a/packages/playwright-chromium/package.json b/packages/playwright-chromium/package.json index 61f3fde05..404ece9e1 100644 --- a/packages/playwright-chromium/package.json +++ b/packages/playwright-chromium/package.json @@ -1,6 +1,6 @@ { "name": "playwright-chromium", - "version": "1.56.0-next", + "version": "1.57.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.56.0-next" + "playwright-core": "1.57.0-next" } } diff --git a/packages/playwright-client/package.json b/packages/playwright-client/package.json index 0058240e1..bc76d8a5d 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.56.0-next" + "playwright-core": "1.57.0-next" } } diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json index 5eec99ac4..f4d18a020 100644 --- a/packages/playwright-core/package.json +++ b/packages/playwright-core/package.json @@ -1,6 +1,6 @@ { "name": "playwright-core", - "version": "1.56.0-next", + "version": "1.57.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 97f6ea292..c06491dd8 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.56.0-next", + "version": "1.57.0-next", "description": "Playwright Component Testing Helpers", "repository": { "type": "git", @@ -26,8 +26,8 @@ } }, "dependencies": { - "playwright-core": "1.56.0-next", + "playwright-core": "1.57.0-next", "vite": "^6.3.6", - "playwright": "1.56.0-next" + "playwright": "1.57.0-next" } } diff --git a/packages/playwright-ct-react/package.json b/packages/playwright-ct-react/package.json index 27098cfba..6887fb87d 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.56.0-next", + "version": "1.57.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.56.0-next", + "@playwright/experimental-ct-core": "1.57.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 3754c8583..01129d68e 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.56.0-next", + "version": "1.57.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.56.0-next", + "@playwright/experimental-ct-core": "1.57.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 adfca41c1..4ec07ebcc 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.56.0-next", + "version": "1.57.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.56.0-next", + "@playwright/experimental-ct-core": "1.57.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 6beff5097..9c8847635 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.56.0-next", + "version": "1.57.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.56.0-next", + "@playwright/experimental-ct-core": "1.57.0-next", "@vitejs/plugin-vue": "^5.2.0" }, "bin": { diff --git a/packages/playwright-firefox/package.json b/packages/playwright-firefox/package.json index 8226a820d..e707c26ae 100644 --- a/packages/playwright-firefox/package.json +++ b/packages/playwright-firefox/package.json @@ -1,6 +1,6 @@ { "name": "playwright-firefox", - "version": "1.56.0-next", + "version": "1.57.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.56.0-next" + "playwright-core": "1.57.0-next" } } diff --git a/packages/playwright-test/package.json b/packages/playwright-test/package.json index cbcef86b2..4bfa3ddce 100644 --- a/packages/playwright-test/package.json +++ b/packages/playwright-test/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/test", - "version": "1.56.0-next", + "version": "1.57.0-next", "description": "A high-level API to automate web browsers", "repository": { "type": "git", @@ -30,6 +30,6 @@ }, "scripts": {}, "dependencies": { - "playwright": "1.56.0-next" + "playwright": "1.57.0-next" } } diff --git a/packages/playwright-webkit/package.json b/packages/playwright-webkit/package.json index 8a95b554f..b51f02e29 100644 --- a/packages/playwright-webkit/package.json +++ b/packages/playwright-webkit/package.json @@ -1,6 +1,6 @@ { "name": "playwright-webkit", - "version": "1.56.0-next", + "version": "1.57.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.56.0-next" + "playwright-core": "1.57.0-next" } } diff --git a/packages/playwright/package.json b/packages/playwright/package.json index 0e47ef02e..9584ab2b8 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -1,6 +1,6 @@ { "name": "playwright", - "version": "1.56.0-next", + "version": "1.57.0-next", "description": "A high-level API to automate web browsers", "repository": { "type": "git", @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-next" + "playwright-core": "1.57.0-next" }, "optionalDependencies": { "fsevents": "2.3.2" From 288bd8705491ec5fb0258c6ea413eaa554438132 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 2 Oct 2025 10:52:27 -0700 Subject: [PATCH 002/250] chore(mcp): unguessable mdb url (#37677) --- packages/playwright/src/mcp/sdk/http.ts | 13 +++++++++++-- packages/playwright/src/mcp/sdk/mdb.ts | 6 ++---- packages/playwright/src/mcp/sdk/server.ts | 5 ++--- tests/mcp/mdb.spec.ts | 20 ++++++++++++++++++++ 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/playwright/src/mcp/sdk/http.ts b/packages/playwright/src/mcp/sdk/http.ts index f39608740..41138b086 100644 --- a/packages/playwright/src/mcp/sdk/http.ts +++ b/packages/playwright/src/mcp/sdk/http.ts @@ -59,11 +59,12 @@ export function httpAddressToString(address: string | net.AddressInfo | null): s return `http://${resolvedHost}:${resolvedPort}`; } -export async function installHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory, allowedHosts?: string[]) { +export async function installHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory, unguessableUrl: boolean, allowedHosts?: string[]) { const url = httpAddressToString(httpServer.address()); const host = new URL(url).host; allowedHosts = (allowedHosts || [host]).map(h => h.toLowerCase()); const allowAnyHost = allowedHosts.includes('*'); + const pathPrefix = unguessableUrl ? `/${crypto.randomUUID()}` : ''; const sseSessions = new Map(); const streamableSessions = new Map(); @@ -83,7 +84,13 @@ export async function installHttpTransport(httpServer: http.Server, serverBacken } } - const url = new URL(`http://localhost${req.url}`); + if (!req.url?.startsWith(pathPrefix)) { + res.statusCode = 404; + return res.end('Not found'); + } + + const path = req.url?.slice(pathPrefix.length); + const url = new URL(`http://localhost${path}`); if (url.pathname === '/killkillkill' && req.method === 'GET') { res.statusCode = 200; res.end('Killing process'); @@ -96,6 +103,8 @@ export async function installHttpTransport(httpServer: http.Server, serverBacken else await handleStreamable(serverBackendFactory, req, res, streamableSessions); }); + + return `${url}${pathPrefix}`; } async function handleSSE(serverBackendFactory: ServerBackendFactory, req: http.IncomingMessage, res: http.ServerResponse, url: URL, sessions: Map) { diff --git a/packages/playwright/src/mcp/sdk/mdb.ts b/packages/playwright/src/mcp/sdk/mdb.ts index 88aa42358..d8840cfd6 100644 --- a/packages/playwright/src/mcp/sdk/mdb.ts +++ b/packages/playwright/src/mcp/sdk/mdb.ts @@ -167,8 +167,7 @@ export async function runOnPauseBackendLoop(backend: mcpServer.ServerBackend, in }; const httpServer = await mcpHttp.startHttpServer({ port: 0 }); - await mcpHttp.installHttpTransport(httpServer, factory); - const url = mcpHttp.httpAddressToString(httpServer.address()); + const url = await mcpHttp.installHttpTransport(httpServer, factory, true); const client = new mcpBundle.Client({ name: 'Pushing client', version: '0.0.0' }); client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); @@ -193,8 +192,7 @@ export async function runOnPauseBackendLoop(backend: mcpServer.ServerBackend, in 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()); + return await mcpHttp.installHttpTransport(httpServer, backendFactory, true); } diff --git a/packages/playwright/src/mcp/sdk/server.ts b/packages/playwright/src/mcp/sdk/server.ts index 3239cfa20..5359ce9d2 100644 --- a/packages/playwright/src/mcp/sdk/server.ts +++ b/packages/playwright/src/mcp/sdk/server.ts @@ -19,7 +19,7 @@ import { fileURLToPath } from 'url'; import { debug } from 'playwright-core/lib/utilsBundle'; import * as mcpBundle from './bundle'; -import { httpAddressToString, installHttpTransport, startHttpServer } from './http'; +import { installHttpTransport, startHttpServer } from './http'; import { InProcessTransport } from './inProcessTransport'; import type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js'; @@ -166,8 +166,7 @@ export async function start(serverBackendFactory: ServerBackendFactory, options: } const httpServer = await startHttpServer(options); - const url = httpAddressToString(httpServer.address()); - await installHttpTransport(httpServer, serverBackendFactory, options.allowedHosts); + const url = await installHttpTransport(httpServer, serverBackendFactory, false, options.allowedHosts); const mcpConfig: any = { mcpServers: { } }; mcpConfig.mcpServers[serverBackendFactory.nameInConfig] = { diff --git a/tests/mcp/mdb.spec.ts b/tests/mcp/mdb.spec.ts index 317266cb5..668c1933f 100644 --- a/tests/mcp/mdb.spec.ts +++ b/tests/mcp/mdb.spec.ts @@ -156,6 +156,26 @@ test('reset on pause tools', async () => { await mdbClient.close(); }); +test('mdb has unguessable url', async () => { + let firstPathname: string | undefined; + let secondPathname: string | undefined; + { + const { mdbUrl } = await startMDBAndCLI(); + firstPathname = new URL(mdbUrl).pathname; + const mdbClient = await createMDBClient(mdbUrl); + await mdbClient.close(); + } + { + const { mdbUrl } = await startMDBAndCLI(); + secondPathname = new URL(mdbUrl).pathname; + const mdbClient = await createMDBClient(mdbUrl); + await mdbClient.close(); + } + expect(firstPathname.length).toBe(37); + expect(secondPathname.length).toBe(37); + expect(firstPathname).not.toBe(secondPathname); +}); + async function startMDBAndCLI(): Promise<{ mdbUrl: string, log: string[] }> { const mdbUrlBox = { mdbUrl: undefined as string | undefined }; const log: string[] = []; From d2a6174fb6023308f972ad8475c4b32653b0fbbd Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 2 Oct 2025 11:25:47 -0700 Subject: [PATCH 003/250] chore: update browser_patches to 7eee05d75 (#37688) --- browser_patches/firefox/UPSTREAM_CONFIG.sh | 2 +- .../firefox/juggler/ChannelEventSink.sys.mjs | 99 + .../firefox/juggler/NetworkObserver.js | 2 +- .../firefox/juggler/TargetRegistry.js | 5 +- browser_patches/firefox/juggler/jar.mn | 1 + .../firefox/patches/bootstrap.diff | 252 +- .../firefox/preferences/playwright.cfg | 1 + browser_patches/webkit/UPSTREAM_CONFIG.sh | 2 +- .../embedder/Playwright/mac/AppDelegate.m | 10 +- browser_patches/webkit/patches/bootstrap.diff | 2302 +++++++---------- 10 files changed, 1157 insertions(+), 1519 deletions(-) create mode 100644 browser_patches/firefox/juggler/ChannelEventSink.sys.mjs diff --git a/browser_patches/firefox/UPSTREAM_CONFIG.sh b/browser_patches/firefox/UPSTREAM_CONFIG.sh index 9d6ff4f56..9ab8a1199 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="361373160356d92cb5cd4d67783a3806c776ee78" +BASE_REVISION="c1ee0105d25a4c2b2887e916470bdf41a9fd47ef" diff --git a/browser_patches/firefox/juggler/ChannelEventSink.sys.mjs b/browser_patches/firefox/juggler/ChannelEventSink.sys.mjs new file mode 100644 index 000000000..ab03e9f0a --- /dev/null +++ b/browser_patches/firefox/juggler/ChannelEventSink.sys.mjs @@ -0,0 +1,99 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ComponentUtils } from "resource://gre/modules/ComponentUtils.sys.mjs"; + +const Cm = Components.manager; + +/** + * This is a nsIChannelEventSink implementation that monitors channel redirects. + * This has been forked from: + * https://searchfox.org/mozilla-central/source/devtools/server/actors/network-monitor/channel-event-sink.js + * The rest of this module is also more or less forking: + * https://searchfox.org/mozilla-central/source/devtools/server/actors/network-monitor/network-observer.js + * TODO(try to re-unify /remote/ with /devtools code) + */ +const SINK_CLASS_DESCRIPTION = "NetworkMonitor Channel Event Sink"; +const SINK_CLASS_ID = Components.ID("{c2b4c83e-607a-405a-beab-0ef5dbfb7617}"); +const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1"; +const SINK_CATEGORY_NAME = "net-channel-event-sinks"; + +function ChannelEventSink() { + this.wrappedJSObject = this; + this.collectors = new Set(); +} + +ChannelEventSink.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink"]), + + registerCollector(collector) { + this.collectors.add(collector); + }, + + unregisterCollector(collector) { + this.collectors.delete(collector); + + if (this.collectors.size == 0) { + ChannelEventSinkFactory.unregister(); + } + }, + + asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { + for (const collector of this.collectors) { + try { + collector._onChannelRedirect(oldChannel, newChannel, flags); + } catch (ex) { + console.error( + "StackTraceCollector.onChannelRedirect threw an exception", + ex + ); + } + } + callback.onRedirectVerifyCallback(Cr.NS_OK); + }, +}; + +export const ChannelEventSinkFactory = + ComponentUtils.generateSingletonFactory(ChannelEventSink); + +ChannelEventSinkFactory.register = function () { + const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + if (registrar.isCIDRegistered(SINK_CLASS_ID)) { + return; + } + + registrar.registerFactory( + SINK_CLASS_ID, + SINK_CLASS_DESCRIPTION, + SINK_CONTRACT_ID, + ChannelEventSinkFactory + ); + + Services.catMan.addCategoryEntry( + SINK_CATEGORY_NAME, + SINK_CONTRACT_ID, + SINK_CONTRACT_ID, + false, + true + ); +}; + +ChannelEventSinkFactory.unregister = function () { + const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(SINK_CLASS_ID, ChannelEventSinkFactory); + + Services.catMan.deleteCategoryEntry( + SINK_CATEGORY_NAME, + SINK_CONTRACT_ID, + false + ); +}; + +ChannelEventSinkFactory.getService = function () { + // Make sure the ChannelEventSink service is registered before accessing it + ChannelEventSinkFactory.register(); + + return Cc[SINK_CONTRACT_ID].getService(Ci.nsIChannelEventSink) + .wrappedJSObject; +}; diff --git a/browser_patches/firefox/juggler/NetworkObserver.js b/browser_patches/firefox/juggler/NetworkObserver.js index c063e0484..2719cc102 100644 --- a/browser_patches/firefox/juggler/NetworkObserver.js +++ b/browser_patches/firefox/juggler/NetworkObserver.js @@ -6,7 +6,7 @@ const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js'); const {NetUtil} = ChromeUtils.importESModule('resource://gre/modules/NetUtil.sys.mjs'); -const { ChannelEventSinkFactory } = ChromeUtils.importESModule("chrome://remote/content/cdp/observers/ChannelEventSink.sys.mjs"); +const { ChannelEventSinkFactory } = ChromeUtils.importESModule("chrome://juggler/content/ChannelEventSink.sys.mjs"); const Cc = Components.classes; diff --git a/browser_patches/firefox/juggler/TargetRegistry.js b/browser_patches/firefox/juggler/TargetRegistry.js index 5cddac6f6..14962a91e 100644 --- a/browser_patches/firefox/juggler/TargetRegistry.js +++ b/browser_patches/firefox/juggler/TargetRegistry.js @@ -13,7 +13,7 @@ const Cr = Components.results; const helper = new Helper(); const IDENTITY_NAME = 'JUGGLER '; -const HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100; +const HUNDRED_YEARS = 1000 * 60 * 60 * 24 * 365 * 100; const ALL_PERMISSIONS = [ 'geo', @@ -1124,7 +1124,8 @@ class BrowserContext { setCookies(cookies) { const protocolToSameSite = { - [undefined]: Ci.nsICookie.SAMESITE_NONE, + [undefined]: Ci.nsICookie.SAMESITE_UNSET, + 'None': Ci.nsICookie.SAMESITE_UNSET, 'Lax': Ci.nsICookie.SAMESITE_LAX, 'Strict': Ci.nsICookie.SAMESITE_STRICT, }; diff --git a/browser_patches/firefox/juggler/jar.mn b/browser_patches/firefox/juggler/jar.mn index 7f3ecf5cd..b21b33bc4 100644 --- a/browser_patches/firefox/juggler/jar.mn +++ b/browser_patches/firefox/juggler/jar.mn @@ -9,6 +9,7 @@ juggler.jar: content/Helper.js (Helper.js) content/NetworkObserver.js (NetworkObserver.js) + content/ChannelEventSink.sys.mjs (ChannelEventSink.sys.mjs) content/TargetRegistry.js (TargetRegistry.js) content/SimpleChannel.js (SimpleChannel.js) content/JugglerFrameParent.jsm (JugglerFrameParent.jsm) diff --git a/browser_patches/firefox/patches/bootstrap.diff b/browser_patches/firefox/patches/bootstrap.diff index 523cda6f4..c422ad36e 100644 --- a/browser_patches/firefox/patches/bootstrap.diff +++ b/browser_patches/firefox/patches/bootstrap.diff @@ -106,19 +106,19 @@ index a315ebcb8b60875b4eddbb5d654764603e0e7bec..fa67db1ff5a331dda4eaff4457418c30 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 6ebf2f62c47757f16841d04c164d2d86447fcbd0..cae3f3ac6f6ced0b59fe850643bbb90d619750d0 100644 +index 2b4d35e060dc592bb70a1f2b7ceee68616c8202b..323abfaee37445942f7678037dd5b53792dd5ed4 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in -@@ -192,6 +192,9 @@ +@@ -183,6 +183,9 @@ @RESPATH@/chrome/remote.manifest #endif +@RESPATH@/chrome/juggler@JAREXT@ +@RESPATH@/chrome/juggler.manifest + - ; [Extensions] - @RESPATH@/components/extensions-toolkit.manifest - + ; Modules + @RESPATH@/browser/modules/* + @RESPATH@/modules/* 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,7 +167,7 @@ index d49c6fbf1bf83b832795fa674f6b41f223eef812..7ea3540947ff5f61b15f27fbf4b95564 const transportProvider = { setListener(upgradeListener) { diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp -index 1a33afa481826b1a53c507d5ea596bcb629d8ac4..8f4f13ff1325b104af80e319cf092be9fd2baab0 100644 +index a5273a33caeb963017a41320b08783c3520eb376..08f73f9a316af5d70d4b1658f5d7b6fedd12c6e5 100644 --- a/docshell/base/BrowsingContext.cpp +++ b/docshell/base/BrowsingContext.cpp @@ -109,8 +109,15 @@ struct ParamTraits @@ -222,7 +222,7 @@ index 1a33afa481826b1a53c507d5ea596bcb629d8ac4..8f4f13ff1325b104af80e319cf092be9 nsString&& aOldValue) { MOZ_ASSERT(IsTop()); diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h -index 0bd3efa16dcae42ff02f42bddce378a7f0c7b621..c8cc1bce7d9901aced6032d0e2e2285cf9eb394b 100644 +index 0c133479abbb8b9a1218ebcb95a285f859d4d39d..7fcc205d2c1a44d864339617a22bf4812f5e2659 100644 --- a/docshell/base/BrowsingContext.h +++ b/docshell/base/BrowsingContext.h @@ -205,10 +205,10 @@ struct EmbedderColorSchemes { @@ -248,7 +248,7 @@ index 0bd3efa16dcae42ff02f42bddce378a7f0c7b621..c8cc1bce7d9901aced6032d0e2e2285c /* The number of entries added to the session history because of this \ * browsing context. */ \ FIELD(HistoryEntryCount, uint32_t) \ -@@ -967,6 +970,14 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { +@@ -971,6 +974,14 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { return GetForcedColorsOverride(); } @@ -263,7 +263,7 @@ index 0bd3efa16dcae42ff02f42bddce378a7f0c7b621..c8cc1bce7d9901aced6032d0e2e2285c bool IsInBFCache() const; bool AllowJavascript() const { return GetAllowJavascript(); } -@@ -1131,6 +1142,11 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { +@@ -1137,6 +1148,11 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { return IsTop(); } @@ -275,7 +275,7 @@ index 0bd3efa16dcae42ff02f42bddce378a7f0c7b621..c8cc1bce7d9901aced6032d0e2e2285c bool CanSet(FieldIndex, dom::ForcedColorsOverride, ContentParent*) { return IsTop(); -@@ -1149,10 +1165,22 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { +@@ -1155,10 +1171,22 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { void DidSet(FieldIndex, dom::ForcedColorsOverride aOldValue); @@ -299,10 +299,10 @@ index 0bd3efa16dcae42ff02f42bddce378a7f0c7b621..c8cc1bce7d9901aced6032d0e2e2285c bool CanSet(FieldIndex, bool, ContentParent*) { diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp -index 1c5c421d6ec153ab61d3aae29200f8abb02b1a33..68751ec0190f308c20aae6b6c58687dcf0fa225d 100644 +index 6feae3858ff35301b93632645df402c0da759688..b0f6a331628a5619d68078e2a114102e5cfbcb65 100644 --- a/docshell/base/CanonicalBrowsingContext.cpp +++ b/docshell/base/CanonicalBrowsingContext.cpp -@@ -325,6 +325,8 @@ void CanonicalBrowsingContext::ReplacedBy( +@@ -267,6 +267,8 @@ void CanonicalBrowsingContext::ReplacedBy( txn.SetForceOffline(GetForceOffline()); txn.SetTopInnerSizeForRFP(GetTopInnerSizeForRFP()); txn.SetIPAddressSpace(GetIPAddressSpace()); @@ -311,7 +311,7 @@ index 1c5c421d6ec153ab61d3aae29200f8abb02b1a33..68751ec0190f308c20aae6b6c58687dc // Propagate some settings on BrowsingContext replacement so they're not lost // on bfcached navigations. These are important for GeckoView (see bug -@@ -1635,6 +1637,12 @@ void CanonicalBrowsingContext::LoadURI(nsIURI* aURI, +@@ -1578,6 +1580,12 @@ void CanonicalBrowsingContext::LoadURI(nsIURI* aURI, return; } @@ -325,7 +325,7 @@ index 1c5c421d6ec153ab61d3aae29200f8abb02b1a33..68751ec0190f308c20aae6b6c58687dc } diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp -index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4472cce2d 100644 +index bd505e670bffd4db1f880cdaa90c084915c596bc..df7df15ec45185417a91226fbe15901610af1f2a 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -16,6 +16,12 @@ @@ -357,7 +357,7 @@ index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4 #include "mozilla/net/DocumentChannel.h" #include "mozilla/net/DocumentChannelChild.h" #include "mozilla/net/ParentChannelWrapper.h" -@@ -118,6 +126,7 @@ +@@ -119,6 +127,7 @@ #include "nsIDocumentViewer.h" #include "mozilla/dom/Document.h" #include "nsHTMLDocument.h" @@ -365,7 +365,7 @@ index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4 #include "nsIDocumentLoaderFactory.h" #include "nsIDOMWindow.h" #include "nsIEditingSession.h" -@@ -212,6 +221,7 @@ +@@ -214,6 +223,7 @@ #include "nsGlobalWindowInner.h" #include "nsGlobalWindowOuter.h" #include "nsJSEnvironment.h" @@ -373,7 +373,7 @@ index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4 #include "nsNetCID.h" #include "nsNetUtil.h" #include "nsObjectLoadingContent.h" -@@ -353,6 +363,14 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext, +@@ -355,6 +365,14 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext, mAllowDNSPrefetch(true), mAllowWindowControl(true), mCSSErrorReportingEnabled(false), @@ -388,7 +388,7 @@ index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4 mAllowAuth(mItemType == typeContent), mAllowKeywordFixup(false), mDisableMetaRefreshWhenInactive(false), -@@ -3025,6 +3043,232 @@ nsDocShell::GetMessageManager(ContentFrameMessageManager** aMessageManager) { +@@ -3034,6 +3052,232 @@ nsDocShell::GetMessageManager(ContentFrameMessageManager** aMessageManager) { return NS_OK; } @@ -621,7 +621,7 @@ index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4 NS_IMETHODIMP nsDocShell::GetIsNavigating(bool* aOut) { *aOut = mIsNavigating; -@@ -4790,7 +5034,7 @@ nsDocShell::GetVisibility(bool* aVisibility) { +@@ -4813,7 +5057,7 @@ nsDocShell::GetVisibility(bool* aVisibility) { } void nsDocShell::ActivenessMaybeChanged() { @@ -630,7 +630,7 @@ index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4 if (RefPtr presShell = GetPresShell()) { presShell->ActivenessMaybeChanged(); } -@@ -6723,6 +6967,10 @@ bool nsDocShell::CanSavePresentation(uint32_t aLoadType, +@@ -6877,6 +7121,10 @@ bool nsDocShell::CanSavePresentation(uint32_t aLoadType, return false; // no entry to save into } @@ -641,7 +641,7 @@ index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4 MOZ_ASSERT(!mozilla::SessionHistoryInParent(), "mOSHE cannot be non-null with SHIP"); nsCOMPtr viewer = mOSHE->GetDocumentViewer(); -@@ -8464,6 +8712,12 @@ nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) { +@@ -8633,6 +8881,12 @@ nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) { true, // aForceNoOpener getter_AddRefs(newBC)); MOZ_ASSERT(!newBC); @@ -654,7 +654,7 @@ index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4 return rv; } -@@ -9754,6 +10008,16 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState, +@@ -9938,6 +10192,16 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState, nsINetworkPredictor::PREDICT_LOAD, attrs, nullptr); nsCOMPtr req; @@ -671,7 +671,7 @@ index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4 rv = DoURILoad(aLoadState, aCacheKey, getter_AddRefs(req)); if (NS_SUCCEEDED(rv)) { -@@ -12970,6 +13234,9 @@ class OnLinkClickEvent : public Runnable { +@@ -13165,6 +13429,9 @@ class OnLinkClickEvent : public Runnable { mHandler->OnLinkClickSync(mContent, mLoadState, mNoOpenerImplied, mTriggeringPrincipal); } @@ -681,7 +681,7 @@ index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4 return NS_OK; } -@@ -13083,6 +13350,8 @@ nsresult nsDocShell::OnLinkClick( +@@ -13280,6 +13547,8 @@ nsresult nsDocShell::OnLinkClick( nsCOMPtr ev = new OnLinkClickEvent( this, aContent, loadState, noOpenerImplied, aTriggeringPrincipal); @@ -691,7 +691,7 @@ index 5bcbbfbcb522adb9988ba421a4f590ee2105e1d3..cb9bc09a7901334b6ba73834971498f4 } diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h -index 32d1673ea09a2c9f6bb14f6dcd8a9c1c8f9831c4..a301ecd30fcad4d79d370ab316fa0d8e5e509795 100644 +index db0bd18f9828b903e240f91f8febd2d371b1cefb..104aa94dbb8ca3b2fa5caebde0df22db4f9e02a4 100644 --- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -16,6 +16,7 @@ @@ -702,7 +702,7 @@ index 32d1673ea09a2c9f6bb14f6dcd8a9c1c8f9831c4..a301ecd30fcad4d79d370ab316fa0d8e #include "mozilla/dom/WindowProxyHolder.h" #include "nsCOMPtr.h" #include "nsCharsetSource.h" -@@ -78,6 +79,7 @@ class nsCommandManager; +@@ -79,6 +80,7 @@ class nsCommandManager; class nsDocShellEditorData; class nsDOMNavigationTiming; class nsDSURIContentListener; @@ -710,7 +710,7 @@ index 32d1673ea09a2c9f6bb14f6dcd8a9c1c8f9831c4..a301ecd30fcad4d79d370ab316fa0d8e class nsGlobalWindowOuter; class FramingChecker; -@@ -408,6 +410,15 @@ class nsDocShell final : public nsDocLoader, +@@ -409,6 +411,15 @@ class nsDocShell final : public nsDocLoader, void SetWillChangeProcess() { mWillChangeProcess = true; } bool WillChangeProcess() { return mWillChangeProcess; } @@ -726,7 +726,7 @@ index 32d1673ea09a2c9f6bb14f6dcd8a9c1c8f9831c4..a301ecd30fcad4d79d370ab316fa0d8e // Create a content viewer within this nsDocShell for the given // `WindowGlobalChild` actor. nsresult CreateDocumentViewerForActor( -@@ -1013,6 +1024,8 @@ class nsDocShell final : public nsDocLoader, +@@ -1015,6 +1026,8 @@ class nsDocShell final : public nsDocLoader, bool CSSErrorReportingEnabled() const { return mCSSErrorReportingEnabled; } @@ -735,7 +735,7 @@ index 32d1673ea09a2c9f6bb14f6dcd8a9c1c8f9831c4..a301ecd30fcad4d79d370ab316fa0d8e // 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 -@@ -1301,6 +1314,17 @@ class nsDocShell final : public nsDocLoader, +@@ -1336,6 +1349,17 @@ class nsDocShell final : public nsDocLoader, bool mAllowDNSPrefetch : 1; bool mAllowWindowControl : 1; bool mCSSErrorReportingEnabled : 1; @@ -812,10 +812,10 @@ index 159daa1dd936e84f1bf3ce413643382e4d37f592..607224a5270995abccf439992ec577f1 * 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 a16bef739fcde0f14ba7e53e0acfa3aa2ee1dd3a..c4747478156c973307b7866f84c65520e4bff9d1 100644 +index 36f9681a989bf4007fa837e7c9ef332ee14e55bd..252347c568b99186c006b2d7aa9a35e0c5c2f3db 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp -@@ -3818,6 +3818,9 @@ void Document::SendToConsole(nsCOMArray& aMessages) { +@@ -3795,6 +3795,9 @@ void Document::SendToConsole(nsCOMArray& aMessages) { } void Document::ApplySettingsFromCSP(bool aSpeculative) { @@ -825,7 +825,7 @@ index a16bef739fcde0f14ba7e53e0acfa3aa2ee1dd3a..c4747478156c973307b7866f84c65520 nsresult rv = NS_OK; if (!aSpeculative) { // 1) apply settings from regular CSP -@@ -3875,6 +3878,11 @@ nsresult Document::InitCSP(nsIChannel* aChannel) { +@@ -3852,6 +3855,11 @@ nsresult Document::InitCSP(nsIChannel* aChannel) { MOZ_ASSERT(!mScriptGlobalObject, "CSP must be initialized before mScriptGlobalObject is set!"); @@ -837,7 +837,7 @@ index a16bef739fcde0f14ba7e53e0acfa3aa2ee1dd3a..c4747478156c973307b7866f84c65520 // If this is a data document - no need to set CSP. if (mLoadedAsData) { return NS_OK; -@@ -4699,6 +4707,10 @@ bool Document::HasFocus(ErrorResult& rv) const { +@@ -4717,6 +4725,10 @@ bool Document::HasFocus(ErrorResult& rv) const { return false; } @@ -848,7 +848,7 @@ index a16bef739fcde0f14ba7e53e0acfa3aa2ee1dd3a..c4747478156c973307b7866f84c65520 if (!fm->IsInActiveWindow(bc)) { return false; } -@@ -20107,6 +20119,35 @@ ColorScheme Document::PreferredColorScheme(IgnoreRFP aIgnoreRFP) const { +@@ -20142,6 +20154,35 @@ ColorScheme Document::PreferredColorScheme(IgnoreRFP aIgnoreRFP) const { return PreferenceSheet::PrefsFor(*this).mColorScheme; } @@ -885,10 +885,10 @@ index a16bef739fcde0f14ba7e53e0acfa3aa2ee1dd3a..c4747478156c973307b7866f84c65520 if (!sLoadingForegroundTopLevelContentDocument) { return false; diff --git a/dom/base/Document.h b/dom/base/Document.h -index f9f8089ad484330a9d863fac7559b94aa34d80fd..75b6f3b03a6fe11511cc1e9ebdc6aa16936bbbcb 100644 +index 41df6459683a9ab0f0bd20f4139dada564cc18e8..7caea88ca7934f120091c116e2551b4378559e32 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h -@@ -4224,6 +4224,8 @@ class Document : public nsINode, +@@ -4235,6 +4235,8 @@ class Document : public nsINode, // color-scheme meta tag. ColorScheme DefaultColorScheme() const; @@ -898,7 +898,7 @@ index f9f8089ad484330a9d863fac7559b94aa34d80fd..75b6f3b03a6fe11511cc1e9ebdc6aa16 static bool AutomaticStorageAccessPermissionCanBeGranted( diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp -index fac275953573368e91e99bc8a72a885fb1c75521..7c1bcfdba325f8310239fc69921aaa0f14255f33 100644 +index 3f60404936ba7559d810154bb9eac7aa0a831e27..f9be3dc1b384b4d4f975be83efaac10178e9b355 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp @@ -347,14 +347,18 @@ void Navigator::GetAppName(nsAString& aAppName) const { @@ -937,7 +937,7 @@ index fac275953573368e91e99bc8a72a885fb1c75521..7c1bcfdba325f8310239fc69921aaa0f // 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 -@@ -2309,7 +2319,8 @@ bool Navigator::Webdriver() { +@@ -2303,7 +2313,8 @@ bool Navigator::Webdriver() { } #endif @@ -961,10 +961,10 @@ index 893947475fbb8688becb1c1495385e4048d4927d..dfb50acf689223fdab7ef6f42afbbd53 dom::MediaCapabilities* MediaCapabilities(); dom::MediaSession* MediaSession(); diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp -index 787d4c4e22715a72197e5d06831bd6d284129c2c..75fc73ce2863f994ce703b0f822acb924bee4f3d 100644 +index 67f8e934ce4be73461bf6ddb85ce6cffc55db149..fd7496044e81d82067774f6a83329e8af8c38ff3 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp -@@ -9432,11 +9432,13 @@ nsresult nsContentUtils::SendMouseEvent( +@@ -9454,11 +9454,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 787d4c4e22715a72197e5d06831bd6d284129c2c..75fc73ce2863f994ce703b0f822acb92 if (aType.EqualsLiteral("mousedown")) { msg = eMouseDown; } else if (aType.EqualsLiteral("mouseup")) { -@@ -9462,6 +9464,12 @@ nsresult nsContentUtils::SendMouseEvent( +@@ -9484,6 +9486,12 @@ nsresult nsContentUtils::SendMouseEvent( msg = eMouseHitTest; } else if (aType.EqualsLiteral("MozMouseExploreByTouch")) { msg = eMouseExploreByTouch; @@ -992,7 +992,7 @@ index 787d4c4e22715a72197e5d06831bd6d284129c2c..75fc73ce2863f994ce703b0f822acb92 } else { return NS_ERROR_FAILURE; } -@@ -9472,7 +9480,14 @@ nsresult nsContentUtils::SendMouseEvent( +@@ -9494,7 +9502,14 @@ nsresult nsContentUtils::SendMouseEvent( Maybe pointerEvent; Maybe mouseEvent; @@ -1008,7 +1008,7 @@ index 787d4c4e22715a72197e5d06831bd6d284129c2c..75fc73ce2863f994ce703b0f822acb92 MOZ_ASSERT(!aIsWidgetEventSynthesized, "The event shouldn't be dispatched as a synthesized event"); if (MOZ_UNLIKELY(aIsWidgetEventSynthesized)) { -@@ -9491,8 +9506,11 @@ nsresult nsContentUtils::SendMouseEvent( +@@ -9513,8 +9528,11 @@ nsresult nsContentUtils::SendMouseEvent( contextMenuKey ? WidgetMouseEvent::eContextMenuKey : WidgetMouseEvent::eNormal); } @@ -1020,7 +1020,7 @@ index 787d4c4e22715a72197e5d06831bd6d284129c2c..75fc73ce2863f994ce703b0f822acb92 mouseOrPointerEvent.pointerId = aIdentifier; mouseOrPointerEvent.mModifiers = GetWidgetModifiers(aModifiers); mouseOrPointerEvent.mButton = aButton; -@@ -9505,6 +9523,8 @@ nsresult nsContentUtils::SendMouseEvent( +@@ -9527,6 +9545,8 @@ nsresult nsContentUtils::SendMouseEvent( mouseOrPointerEvent.mClickCount = aClickCount; mouseOrPointerEvent.mFlags.mIsSynthesizedForTests = aIsDOMEventSynthesized; mouseOrPointerEvent.mExitFrom = exitFrom; @@ -1030,10 +1030,10 @@ index 787d4c4e22715a72197e5d06831bd6d284129c2c..75fc73ce2863f994ce703b0f822acb92 nsPresContext* presContext = aPresShell->GetPresContext(); if (!presContext) return NS_ERROR_FAILURE; diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h -index 7d89626774660fb9e0f564808270e3059e4d7b3c..c7f748e6f33cbcd72b0c97d437b2abbcbe4242be 100644 +index 84890a39e694af61974bb4c1d11b679a2d962340..90d9e672aeb47a878b3dd9c8dec4de6e8bf84846 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h -@@ -3078,8 +3078,9 @@ class nsContentUtils { +@@ -3079,8 +3079,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,7 +1046,7 @@ index 7d89626774660fb9e0f564808270e3059e4d7b3c..c7f748e6f33cbcd72b0c97d437b2abbc static void FirePageShowEventForFrameLoaderSwap( nsIDocShellTreeItem* aItem, diff --git a/dom/base/nsDOMWindowUtils.cpp b/dom/base/nsDOMWindowUtils.cpp -index e85140d5afebf57cf56bf16ef0c43c425c8d50c7..3062737c3319e35cdc4786c06750a1ac2a99565f 100644 +index 5624bf11d000de505c51d4edb544f33f83c57c64..2468488e11d6dbeaf1528d233331076e9b693e0d 100644 --- a/dom/base/nsDOMWindowUtils.cpp +++ b/dom/base/nsDOMWindowUtils.cpp @@ -707,6 +707,26 @@ nsDOMWindowUtils::GetPresShellId(uint32_t* aPresShellId) { @@ -1113,7 +1113,7 @@ index e85140d5afebf57cf56bf16ef0c43c425c8d50c7..3062737c3319e35cdc4786c06750a1ac NS_IMETHODIMP diff --git a/dom/base/nsDOMWindowUtils.h b/dom/base/nsDOMWindowUtils.h -index a8a48d28fc4ef612f8234bc2490a41672f1704f5..f702b0c9a0783ec547a41bbefd68e18a27a239fe 100644 +index c9975041127870048a339232eb2701bff47289cb..a53dbf0c6439e359ad75275b002bb6ee39767d26 100644 --- a/dom/base/nsDOMWindowUtils.h +++ b/dom/base/nsDOMWindowUtils.h @@ -94,7 +94,7 @@ class nsDOMWindowUtils final : public nsIDOMWindowUtils, @@ -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 be89a1c984982ea005e9bf7f440f5c8a2e8bab55..f8154882b9af02c5e9d7181e7a15b78824be8df9 100644 +index d4f4148f660d128e8df9a7517369ae744b44d26d..bcc3f91e45e45394e826459d4594a0ba564e85b9 100644 --- a/dom/base/nsFocusManager.cpp +++ b/dom/base/nsFocusManager.cpp -@@ -1719,6 +1719,10 @@ Maybe nsFocusManager::SetFocusInner(Element* aNewContent, +@@ -1717,6 +1717,10 @@ Maybe nsFocusManager::SetFocusInner(Element* aNewContent, (GetActiveBrowsingContext() == newRootBrowsingContext); } @@ -1140,7 +1140,7 @@ index be89a1c984982ea005e9bf7f440f5c8a2e8bab55..f8154882b9af02c5e9d7181e7a15b788 // Exit fullscreen if a website focuses another window if (StaticPrefs::full_screen_api_exit_on_windowRaise() && !isElementInActiveWindow && (aFlags & FLAG_RAISE)) { -@@ -2305,6 +2309,7 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, +@@ -2303,6 +2307,7 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, bool aIsLeavingDocument, bool aAdjustWidget, bool aRemainActive, Element* aElementToFocus, uint64_t aActionId) { @@ -1148,7 +1148,7 @@ index be89a1c984982ea005e9bf7f440f5c8a2e8bab55..f8154882b9af02c5e9d7181e7a15b788 LOGFOCUS(("<>", aActionId)); // hold a reference to the focused content, which may be null -@@ -2351,6 +2356,11 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, +@@ -2346,6 +2351,11 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, return true; } @@ -1160,7 +1160,7 @@ index be89a1c984982ea005e9bf7f440f5c8a2e8bab55..f8154882b9af02c5e9d7181e7a15b788 // Keep a ref to presShell since dispatching the DOM event may cause // the document to be destroyed. RefPtr presShell = docShell->GetPresShell(); -@@ -3061,7 +3071,9 @@ void nsFocusManager::RaiseWindow(nsPIDOMWindowOuter* aWindow, +@@ -3046,7 +3056,9 @@ void nsFocusManager::RaiseWindow(nsPIDOMWindowOuter* aWindow, } } @@ -1172,7 +1172,7 @@ index be89a1c984982ea005e9bf7f440f5c8a2e8bab55..f8154882b9af02c5e9d7181e7a15b788 // 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 42ee50b53a666c05c6540a1ddcf3745694aaed2d..b15150f7d2e293c2e338f8cd3ada927c052b30b2 100644 +index b5cf9524c49361d297121731a8b2bfb77951fa6c..7bc2db9d1e1e795956a959f5aec4a341acf03e61 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -2511,10 +2511,16 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument, @@ -1217,10 +1217,10 @@ index 42ee50b53a666c05c6540a1ddcf3745694aaed2d..b15150f7d2e293c2e338f8cd3ada927c void nsGlobalWindowOuter::SetDocShell(nsDocShell* aDocShell) { diff --git a/dom/base/nsGlobalWindowOuter.h b/dom/base/nsGlobalWindowOuter.h -index a846e57a8786e77e055d17474c5d910a6844cd5f..02815da6a94e98d452e8b4781a998fc0d5ed1124 100644 +index 0aa48c94263f2ba99069780ccdcaf16735356318..48ad1b131b5fa307ac08503630f20bb9601ab019 100644 --- a/dom/base/nsGlobalWindowOuter.h +++ b/dom/base/nsGlobalWindowOuter.h -@@ -320,6 +320,7 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget, +@@ -318,6 +318,7 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget, // Outer windows only. void DispatchDOMWindowCreated(); @@ -1229,10 +1229,10 @@ index a846e57a8786e77e055d17474c5d910a6844cd5f..02815da6a94e98d452e8b4781a998fc0 // Outer windows only. virtual void EnsureSizeAndPositionUpToDate() override; diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp -index 7d5e58f27b2ae07f8e8ac23c6d12bf90d2fdd8b7..2510572d9bfe8ea2909c48d9c3e86aa02c69e28e 100644 +index af4e9644a89c00352e363560e90debada247cfcf..0756cc712d1e7b5b7ce592d95576ceb08c91702d 100644 --- a/dom/base/nsINode.cpp +++ b/dom/base/nsINode.cpp -@@ -1498,6 +1498,61 @@ void nsINode::GetBoxQuadsFromWindowOrigin(const BoxQuadOptions& aOptions, +@@ -1499,6 +1499,61 @@ void nsINode::GetBoxQuadsFromWindowOrigin(const BoxQuadOptions& aOptions, mozilla::GetBoxQuadsFromWindowOrigin(this, aOptions, aResult, aRv); } @@ -1310,10 +1310,10 @@ index 6a982b3f278bf810dd582b3d5ebc33967323047e..4e991ef317c572c4a79c053d468f14df DOMQuad& aQuad, const TextOrElementOrDocument& aFrom, const ConvertCoordinateOptions& aOptions, CallerType aCallerType, diff --git a/dom/base/nsJSUtils.cpp b/dom/base/nsJSUtils.cpp -index bf7eb34da03c0958de688deecb53b407d430f645..a2ec3b1b7e86f72bee38d890c0834abfe4be8637 100644 +index aed59277e27a5a90f4244dd4513f9b4feb5858c3..26750fa09bead1ffabb22482468dea5999899f18 100644 --- a/dom/base/nsJSUtils.cpp +++ b/dom/base/nsJSUtils.cpp -@@ -149,6 +149,11 @@ bool nsJSUtils::GetEnvironmentChainForElement(JSContext* aCx, Element* aElement, +@@ -151,6 +151,11 @@ bool nsJSUtils::GetEnvironmentChainForElement(JSContext* aCx, Element* aElement, return true; } @@ -1382,10 +1382,10 @@ index 7923fcfb3e70aabddf343ab3ec2f25313bbd227e..cf02ce032d11d85a13bcf91e93e98882 * 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 2a29279a6d74770a2ec5cee80891bff9eadf1d13..bce59065615a33cf020aae4b20750ce8b1be66ce 100644 +index 52cf33bfc863d5ef1d3ac889ddfba3023600d60a..ccb521f367b8ccc8c40d8bad78aa621ab1b074c0 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp -@@ -702,6 +702,12 @@ already_AddRefed FetchRequest(nsIGlobalObject* aGlobal, +@@ -704,6 +704,12 @@ already_AddRefed FetchRequest(nsIGlobalObject* aGlobal, ipcArgs.hasCSPEventListener() = false; ipcArgs.isWorkerRequest() = false; @@ -1399,11 +1399,11 @@ index 2a29279a6d74770a2ec5cee80891bff9eadf1d13..bce59065615a33cf020aae4b20750ce8 mozilla::glean::networking::fetch_keepalive_request_count.Get("main"_ns) diff --git a/dom/fetch/FetchService.cpp b/dom/fetch/FetchService.cpp -index fcdddf2f772af305c68cc169ba891386a1dba982..d8b19d35719fff3f9c55c199718f4dc1d15cdfe9 100644 +index 1734429e8eeb155b742211ca58afcbeb73a8bf7c..15635a2e5c266f8085093a0d17288abe8ed54d9e 100644 --- a/dom/fetch/FetchService.cpp +++ b/dom/fetch/FetchService.cpp @@ -268,6 +268,14 @@ RefPtr FetchService::FetchInstance::Fetch() { - false // IsTrackingFetch + net::ClassificationFlags({0, 0}) // TrackingFlags ); + /* --> Playwright: associate keep-alive fetch with the window */ @@ -1418,10 +1418,10 @@ index fcdddf2f772af305c68cc169ba891386a1dba982..d8b19d35719fff3f9c55c199718f4dc1 auto& args = mArgs.as(); mFetchDriver->SetWorkerScript(args.mWorkerScript); diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp -index 9382c73dc527f3fb676e8ea3457a30eee2d9415f..f1785121db1de071ce5aa1a0ad38d3d603c486f7 100644 +index 4d54d0925fce6d2f4f1657e6b37c670c916759a9..d70b048d70435cbdf2f0dab96ced33f685ffcb1d 100644 --- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp -@@ -63,6 +63,7 @@ +@@ -64,6 +64,7 @@ #include "mozilla/dom/Document.h" #include "mozilla/dom/HTMLDataListElement.h" #include "mozilla/dom/HTMLOptionElement.h" @@ -1429,7 +1429,7 @@ index 9382c73dc527f3fb676e8ea3457a30eee2d9415f..f1785121db1de071ce5aa1a0ad38d3d6 #include "nsIFrame.h" #include "nsRangeFrame.h" #include "nsError.h" -@@ -816,6 +817,13 @@ nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) { +@@ -817,6 +818,13 @@ nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) { return NS_ERROR_FAILURE; } @@ -1444,10 +1444,10 @@ index 9382c73dc527f3fb676e8ea3457a30eee2d9415f..f1785121db1de071ce5aa1a0ad38d3d6 return NS_OK; } diff --git a/dom/interfaces/base/nsIDOMWindowUtils.idl b/dom/interfaces/base/nsIDOMWindowUtils.idl -index 4cdfcaafa779ed402d02411f69c02ab0eb5b4e09..e69c837f7ec340a11e8ae9485cd5b714a3ea9a88 100644 +index a1d930645ed1698e809ebce00d1601a1cdd99114..3a4a3ac8001bae356f42d2e08c2e5ddc8943c90b 100644 --- a/dom/interfaces/base/nsIDOMWindowUtils.idl +++ b/dom/interfaces/base/nsIDOMWindowUtils.idl -@@ -374,6 +374,26 @@ interface nsIDOMWindowUtils : nsISupports { +@@ -381,6 +381,26 @@ interface nsIDOMWindowUtils : nsISupports { [optional] in long aButtons, [optional] in unsigned long aIdentifier); @@ -1475,10 +1475,10 @@ index 4cdfcaafa779ed402d02411f69c02ab0eb5b4e09..e69c837f7ec340a11e8ae9485cd5b714 * touchstart, touchend, touchmove, and touchcancel * diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp -index 310e22a130612c055f7642d813b2bc06538c8797..8528cd75252c6cf0bebe42ffbf83c0220551ba28 100644 +index 8895fa24ac3468bcd8d8c9e3c9b1cd2ec2e93c24..4cd6bdcee5e3c9dee934b352e826f88eb2301914 100644 --- a/dom/ipc/BrowserChild.cpp +++ b/dom/ipc/BrowserChild.cpp -@@ -1760,6 +1760,21 @@ void BrowserChild::HandleRealMouseButtonEvent(const WidgetMouseEvent& aEvent, +@@ -1739,6 +1739,21 @@ void BrowserChild::HandleRealMouseButtonEvent(const WidgetMouseEvent& aEvent, if (postLayerization) { postLayerization->Register(); } @@ -1748,7 +1748,7 @@ index d5964b27e9c99af76fe3f4076322592a9370c2d0..da217e2c1fbd9ca02e50ff74dde02818 return aGlobalOrNull; diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp -index 40844c900fc64415e39f4d2dead4c490d5ec301b..f63d0fd6a6df7ea932ca4d4bb70dfbd7562b2fa0 100644 +index 1b88690eb703c548e8d17416a6ff4ca64266406d..5d52a4a9afc97fa133ebab0c4dd9fdccfa4c21e8 100644 --- a/dom/security/nsCSPUtils.cpp +++ b/dom/security/nsCSPUtils.cpp @@ -24,6 +24,7 @@ @@ -2174,10 +2174,10 @@ index 4bfd336ddcbee8004ac538ca7b7d8216d04a61c3..cd22351c4aeacea8afc9828972222aca // No boxes to return return; diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp -index 1922382f924e74dde81501540d38699dcf3d9e57..c05b9ed071a9a4c99e2afb28eef0dba376777976 100644 +index ced65c024dd6e09ee3ad997b6cc616828e0a4d31..4d4b0f06059001400679a00b8ace6b0d11a86c94 100644 --- a/layout/base/PresShell.cpp +++ b/layout/base/PresShell.cpp -@@ -11881,7 +11881,9 @@ bool PresShell::ComputeActiveness() const { +@@ -12043,7 +12043,9 @@ bool PresShell::ComputeActiveness() const { if (!browserChild->IsVisible()) { MOZ_LOG(gLog, LogLevel::Debug, (" > BrowserChild %p is not visible", browserChild)); @@ -2189,7 +2189,7 @@ index 1922382f924e74dde81501540d38699dcf3d9e57..c05b9ed071a9a4c99e2afb28eef0dba3 // 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 bc5b7ab177070178d1a3c49b563df0f04860e7d3..393f694c766bec5e5443d91922a8bc3524ade676 100644 +index ba5183bf1021f9a1113a7de013d60ab8d25fede1..3eb118e808d221395bf01d6d9c0702a005ac4308 100644 --- a/layout/base/nsLayoutUtils.cpp +++ b/layout/base/nsLayoutUtils.cpp @@ -708,6 +708,10 @@ bool nsLayoutUtils::AllowZoomingForDocument( @@ -2203,7 +2203,7 @@ index bc5b7ab177070178d1a3c49b563df0f04860e7d3..393f694c766bec5e5443d91922a8bc35 // True if we allow zooming for all documents on this platform, or if we are // in RDM. BrowsingContext* bc = aDocument->GetBrowsingContext(); -@@ -9773,6 +9777,9 @@ void nsLayoutUtils::ComputeSystemFont(nsFont* aSystemFont, +@@ -9785,6 +9789,9 @@ void nsLayoutUtils::ComputeSystemFont(nsFont* aSystemFont, /* static */ bool nsLayoutUtils::ShouldHandleMetaViewport(const Document* aDocument) { @@ -2214,10 +2214,10 @@ index bc5b7ab177070178d1a3c49b563df0f04860e7d3..393f694c766bec5e5443d91922a8bc35 return StaticPrefs::dom_meta_viewport_enabled() || (bc && bc->InRDMPane()); } diff --git a/layout/style/GeckoBindings.h b/layout/style/GeckoBindings.h -index 4cdceba041ddc5af98982d27b6e622a0dd5d0e34..50f644a91923e529ff06a164256f5704e5c4d8ae 100644 +index a0560c5719310a7d6dbac232bacc1c50e8235c20..1046c095e8672aafcb3b0f053483e81393bca409 100644 --- a/layout/style/GeckoBindings.h +++ b/layout/style/GeckoBindings.h -@@ -594,6 +594,7 @@ float Gecko_MediaFeatures_GetResolution(const mozilla::dom::Document*); +@@ -597,6 +597,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*); @@ -2264,10 +2264,10 @@ index d843c5952e273c64703af6e9f2528798e3ada307..50dcfd6839777dd0ed9f806dfac3e1a4 return StylePrefersContrast::NoPreference; } diff --git a/netwerk/base/LoadInfo.cpp b/netwerk/base/LoadInfo.cpp -index c9d6cace8de545779ae9c4630973c5a920cd52c6..032a42b732179a951181f390199c2520fecd742b 100644 +index 5de4a03bc0ad25b68de169268b68ae2583cd8085..c5ba93ac46cbe6c566c005e059a685f257719244 100644 --- a/netwerk/base/LoadInfo.cpp +++ b/netwerk/base/LoadInfo.cpp -@@ -792,7 +792,8 @@ LoadInfo::LoadInfo(const LoadInfo& rhs) +@@ -798,7 +798,8 @@ LoadInfo::LoadInfo(const LoadInfo& rhs) rhs.mHasInjectedCookieForCookieBannerHandling), mSchemelessInput(rhs.mSchemelessInput), mHttpsUpgradeTelemetry(rhs.mHttpsUpgradeTelemetry), @@ -2277,7 +2277,7 @@ index c9d6cace8de545779ae9c4630973c5a920cd52c6..032a42b732179a951181f390199c2520 } LoadInfo::LoadInfo( -@@ -2705,4 +2706,16 @@ void LoadInfo::UpdateParentAddressSpaceInfo() { +@@ -2758,4 +2759,16 @@ void LoadInfo::UpdateParentAddressSpaceInfo() { } } @@ -2295,10 +2295,10 @@ index c9d6cace8de545779ae9c4630973c5a920cd52c6..032a42b732179a951181f390199c2520 + } // namespace mozilla::net diff --git a/netwerk/base/LoadInfo.h b/netwerk/base/LoadInfo.h -index 7946dacf862aa13da58c65aba3c72deef575dbba..eb307b58b5fda79fa44e5df94f1bcb4dfaa0a8b6 100644 +index 4abce849f65b77800817e16cd2156247bd37f4cf..ac2aff894c4b31409437593d4d50bcae17a5ba0e 100644 --- a/netwerk/base/LoadInfo.h +++ b/netwerk/base/LoadInfo.h -@@ -448,6 +448,8 @@ class LoadInfo final : public nsILoadInfo { +@@ -455,6 +455,8 @@ class LoadInfo final : public nsILoadInfo { bool mIsNewWindowTarget = false; bool mSkipHTTPSUpgrade = false; @@ -2308,10 +2308,10 @@ index 7946dacf862aa13da58c65aba3c72deef575dbba..eb307b58b5fda79fa44e5df94f1bcb4d // 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 dd8cc849fb3dd56d6e979cc3cbff96727f702d90..dd2d543977cd9543494c52e862552831d955fdb2 100644 +index 9d3f613eecce65428fe8c29cca579e6f41fe8bcf..540e2608c37e58c6e1d124023ce185ddd71bd23d 100644 --- a/netwerk/base/TRRLoadInfo.cpp +++ b/netwerk/base/TRRLoadInfo.cpp -@@ -973,5 +973,15 @@ TRRLoadInfo::GetFetchDestination(nsACString& aDestination) { +@@ -1009,5 +1009,15 @@ TRRLoadInfo::GetFetchDestination(nsACString& aDestination) { return NS_ERROR_NOT_IMPLEMENTED; } @@ -2328,10 +2328,10 @@ index dd8cc849fb3dd56d6e979cc3cbff96727f702d90..dd2d543977cd9543494c52e862552831 } // namespace net } // namespace mozilla diff --git a/netwerk/base/nsILoadInfo.idl b/netwerk/base/nsILoadInfo.idl -index 1cfda408c33c16c75a74ea839ae3bde6142ac92b..b3951bc2df4d448e6c3b7e51bef2c0050e97a35d 100644 +index cd73003e5eb54c638225dc75f9e90f01ccb34192..197c0715593865a3a8b95bfc470f6364122c8f21 100644 --- a/netwerk/base/nsILoadInfo.idl +++ b/netwerk/base/nsILoadInfo.idl -@@ -1650,4 +1650,6 @@ interface nsILoadInfo : nsISupports +@@ -1683,4 +1683,6 @@ interface nsILoadInfo : nsISupports return static_cast(userNavigationInvolvement); } %} @@ -2360,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 c486793f79e2c8c248e25f7963ba4e2c08f553d2..f1e625c59ec79c1104fe9594dbf86d39f3293438 100644 +index 15b89ad43640cd7749c1b424762eb805210b4bd1..0044bba11ad2939e66a0e12ecbed598cdce1776c 100644 --- a/netwerk/ipc/DocumentLoadListener.cpp +++ b/netwerk/ipc/DocumentLoadListener.cpp -@@ -178,6 +178,7 @@ static auto CreateDocumentLoadInfo(CanonicalBrowsingContext* aBrowsingContext, +@@ -185,6 +185,7 @@ static auto CreateDocumentLoadInfo(CanonicalBrowsingContext* aBrowsingContext, loadInfo->SetTextDirectiveUserActivation( aLoadState->GetTextDirectiveUserActivation()); loadInfo->SetIsMetaRefresh(aLoadState->IsMetaRefresh()); @@ -2372,7 +2372,7 @@ index c486793f79e2c8c248e25f7963ba4e2c08f553d2..f1e625c59ec79c1104fe9594dbf86d39 return loadInfo.forget(); } diff --git a/netwerk/protocol/http/InterceptedHttpChannel.cpp b/netwerk/protocol/http/InterceptedHttpChannel.cpp -index fbf4bdf1e24d1102df113984be6c8dc3a7d0d810..787bf014d3bf0b8537f99bf5eb4074e100c78c18 100644 +index fbf4bdf1e24d1102df113984be6c8dc3a7d0d810..cf1978009d74221ae804b998e223904fa943a91f 100644 --- a/netwerk/protocol/http/InterceptedHttpChannel.cpp +++ b/netwerk/protocol/http/InterceptedHttpChannel.cpp @@ -728,10 +728,33 @@ NS_IMPL_ISUPPORTS(ResetInterceptionHeaderVisitor, nsIHttpHeaderVisitor) @@ -2445,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 af4c6482ad41ad67f41b93183c94d6f341fb4989..0a18827e53760b4132381fd0aff286ee69c460e7 100644 +index e266de8ccadf11f7e17b282299da0a0e8a88aa83..1abacf109d60f273c2ffdfcb6397a4a0cb82182a 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -688,11 +688,9 @@ nsresult nsHttpChannel::OnBeforeConnect() { @@ -2513,7 +2513,7 @@ index af4c6482ad41ad67f41b93183c94d6f341fb4989..0a18827e53760b4132381fd0aff286ee return; } -@@ -4198,9 +4190,6 @@ nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { +@@ -4221,9 +4213,6 @@ nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { uint32_t cacheEntryOpenFlags; bool offline = gIOService->IsOffline(); @@ -2523,7 +2523,7 @@ index af4c6482ad41ad67f41b93183c94d6f341fb4989..0a18827e53760b4132381fd0aff286ee bool maybeRCWN = false; nsAutoCString cacheControlRequestHeader; -@@ -4211,7 +4200,7 @@ nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { +@@ -4234,7 +4223,7 @@ nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { return NS_OK; } @@ -2532,7 +2532,7 @@ index af4c6482ad41ad67f41b93183c94d6f341fb4989..0a18827e53760b4132381fd0aff286ee if (offline || (mLoadFlags & INHIBIT_CACHING) || forceOffline) { if (BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass()) && !offline && !forceOffline) { -@@ -7315,6 +7304,20 @@ void nsHttpChannel::MaybeStartDNSPrefetch() { +@@ -7338,6 +7327,20 @@ void nsHttpChannel::MaybeStartDNSPrefetch() { } } @@ -2669,10 +2669,10 @@ index 3d8837cdb38fb5b25abfb9e3e369ad6fe688706e..867ce2efaa10eaea87ffeeb2669cc5a7 constructor() { this._policies = null; diff --git a/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp b/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp -index 7dc1cee243017e1647521b021424781532552d8c..47908d73a7a39a7f95ae8ea28362b4f29505508f 100644 +index ee7d7be5f76cb530407675a26bbf7bda5045cf44..f52ac58bfc1a68be92d354db7c523209cd5d47c7 100644 --- a/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp +++ b/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp -@@ -587,7 +587,7 @@ void PopulateLanguages() { +@@ -588,7 +588,7 @@ void PopulateLanguages() { // sufficient to only collect this information as the other properties are // just reformats of Navigator::GetAcceptLanguages. nsTArray languages; @@ -2695,10 +2695,10 @@ index 316a86d5fcadba99918254ba132f363fb462cc3f..9dcd1b208ffd2d42db6c8ff52476a227 if (windowEnumerator) { bool more; diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp -index 89f7233835b4d03e7a140ca2c75ed8db097d482d..ed7c92d30b3cd77c102467eb0f12c9f482a08fde 100644 +index c0d60b927542653ab9beee730ec2c07f4b3d911c..85c416315f567a5edf8cfc89262dd7a48250dfd6 100644 --- a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp +++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp -@@ -174,8 +174,8 @@ nsBrowserStatusFilter::OnStateChange(nsIWebProgress* aWebProgress, +@@ -176,8 +176,8 @@ nsBrowserStatusFilter::OnStateChange(nsIWebProgress* aWebProgress, } NS_IMETHODIMP @@ -2710,7 +2710,7 @@ index 89f7233835b4d03e7a140ca2c75ed8db097d482d..ed7c92d30b3cd77c102467eb0f12c9f4 int32_t aMaxSelfProgress, int32_t aCurTotalProgress, diff --git a/toolkit/components/windowwatcher/nsWindowWatcher.cpp b/toolkit/components/windowwatcher/nsWindowWatcher.cpp -index 811fb16410e8cf900ad873797269e5fe715579a5..821f5b0c2af8e1dc8754cd023571d1d0ff09eeb6 100644 +index 314efebc432db04b484650dfcb781dfd18fe3788..9f4db0f0f5d4fd5fecc744dd37fa6d9b6665e86f 100644 --- a/toolkit/components/windowwatcher/nsWindowWatcher.cpp +++ b/toolkit/components/windowwatcher/nsWindowWatcher.cpp @@ -1880,7 +1880,11 @@ uint32_t nsWindowWatcher::CalculateChromeFlagsForContent( @@ -2727,10 +2727,10 @@ index 811fb16410e8cf900ad873797269e5fe715579a5..821f5b0c2af8e1dc8754cd023571d1d0 /** diff --git a/toolkit/mozapps/update/UpdateService.sys.mjs b/toolkit/mozapps/update/UpdateService.sys.mjs -index 26d633f4f4fe9125df557ee7367809c8591d7211..6c558e8297114ac3f94ae3836c25e9edd51cc4d4 100644 +index 5d5a3f2c46138b578b583ad4a3506e1d227607ad..6d414635b80a74edb0b5ecfc6a98dd0fee763952 100644 --- a/toolkit/mozapps/update/UpdateService.sys.mjs +++ b/toolkit/mozapps/update/UpdateService.sys.mjs -@@ -3873,6 +3873,8 @@ export class UpdateService { +@@ -3874,6 +3874,8 @@ export class UpdateService { } get disabledForTesting() { @@ -2740,10 +2740,10 @@ index 26d633f4f4fe9125df557ee7367809c8591d7211..6c558e8297114ac3f94ae3836c25e9ed } diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild -index 304c39a11bd88e0b8cf681a3c3bc5dc11ed929ec..71e57ce2164e5436fac68d696b632d120a85d6f2 100644 +index e8399d277ec0b64e5ea29aa7262b118936a02779..983ed73ad1d6d734af55a9066a8e8a6cf3ae3f45 100644 --- a/toolkit/toolkit.mozbuild +++ b/toolkit/toolkit.mozbuild -@@ -151,6 +151,7 @@ if CONFIG["ENABLE_WEBDRIVER"]: +@@ -155,6 +155,7 @@ if CONFIG["ENABLE_WEBDRIVER"]: "/remote", "/testing/firefox-ui", "/testing/marionette", @@ -2787,7 +2787,7 @@ 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 444116840b68443c31d8df66699d47a582ce4622..9ae13a8462301a8a3024bffb10d9a20f9accff1d 100644 +index 9ef5370d1524ddecb965785a73a3cfea39867324..e0245cca94e496bcffba40c9660a52b518803d44 100644 --- a/uriloader/base/nsDocLoader.cpp +++ b/uriloader/base/nsDocLoader.cpp @@ -861,6 +861,12 @@ void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout, @@ -2804,7 +2804,7 @@ index 444116840b68443c31d8df66699d47a582ce4622..9ae13a8462301a8a3024bffb10d9a20f // 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 7cd8c24d871465bc96fe7249eb9a41c999057eb5..02b3ce1fbaa8056219a139a3bc3f107343f9bb89 100644 +index 5bd60b585b53cc77e75c0b68c763957f90b7e164..940b3b62b09ca70ae5e69c3e94eda11ad35b9dfe 100644 --- a/uriloader/exthandler/nsExternalHelperAppService.cpp +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -112,6 +112,7 @@ @@ -2828,7 +2828,7 @@ index 7cd8c24d871465bc96fe7249eb9a41c999057eb5..02b3ce1fbaa8056219a139a3bc3f1073 nsresult nsExternalHelperAppService::GetFileTokenForPath( const char16_t* aPlatformAppPath, nsIFile** aFile) { nsDependentString platformAppPath(aPlatformAppPath); -@@ -1486,7 +1493,12 @@ nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) { +@@ -1500,7 +1507,12 @@ nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) { // Strip off the ".part" from mTempLeafName mTempLeafName.Truncate(mTempLeafName.Length() - std::size(".part") + 1); @@ -2841,7 +2841,7 @@ index 7cd8c24d871465bc96fe7249eb9a41c999057eb5..02b3ce1fbaa8056219a139a3bc3f1073 mSaver = do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); -@@ -1671,7 +1683,36 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { +@@ -1684,7 +1696,36 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { return NS_OK; } @@ -2879,7 +2879,7 @@ index 7cd8c24d871465bc96fe7249eb9a41c999057eb5..02b3ce1fbaa8056219a139a3bc3f1073 if (NS_FAILED(rv)) { nsresult transferError = rv; -@@ -1732,6 +1773,9 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { +@@ -1745,6 +1786,9 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { bool alwaysAsk = true; mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk); @@ -2889,7 +2889,7 @@ index 7cd8c24d871465bc96fe7249eb9a41c999057eb5..02b3ce1fbaa8056219a139a3bc3f1073 if (alwaysAsk) { // But we *don't* ask if this mimeInfo didn't come from // our user configuration datastore and the user has said -@@ -2248,6 +2292,16 @@ nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver, +@@ -2261,6 +2305,16 @@ nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver, NotifyTransfer(aStatus); } @@ -2906,7 +2906,7 @@ index 7cd8c24d871465bc96fe7249eb9a41c999057eb5..02b3ce1fbaa8056219a139a3bc3f1073 return NS_OK; } -@@ -2731,6 +2785,15 @@ NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) { +@@ -2744,6 +2798,15 @@ NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) { } } @@ -2923,10 +2923,10 @@ index 7cd8c24d871465bc96fe7249eb9a41c999057eb5..02b3ce1fbaa8056219a139a3bc3f1073 // OnStartRequest) mDialog = nullptr; diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h -index 68f240c9f9280c498900561e6cdb3bc4c4d45a38..395f54e8c47d56b0892c75a513e5828de7e4fceb 100644 +index 26dfc6f04298420317c8b87b2ecc3f2f47f59598..c458ab5efb238c62499ee13452f2d750e0569e38 100644 --- a/uriloader/exthandler/nsExternalHelperAppService.h +++ b/uriloader/exthandler/nsExternalHelperAppService.h -@@ -254,6 +254,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService, +@@ -263,6 +263,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService, mozilla::dom::BrowsingContext* aContentContext, bool aForceSave, nsIInterfaceRequestor* aWindowContext, nsIStreamListener** aStreamListener); @@ -2935,7 +2935,7 @@ index 68f240c9f9280c498900561e6cdb3bc4c4d45a38..395f54e8c47d56b0892c75a513e5828d }; /** -@@ -451,6 +453,9 @@ class nsExternalAppHandler final : public nsIStreamListener, +@@ -460,6 +462,9 @@ class nsExternalAppHandler final : public nsIStreamListener, * Upon successful return, both mTempFile and mSaver will be valid. */ nsresult SetUpTempFile(nsIChannel* aChannel); @@ -2946,7 +2946,7 @@ index 68f240c9f9280c498900561e6cdb3bc4c4d45a38..395f54e8c47d56b0892c75a513e5828d * When we download a helper app, we are going to retarget all load * notifications into our own docloader and load group instead of diff --git a/uriloader/exthandler/nsIExternalHelperAppService.idl b/uriloader/exthandler/nsIExternalHelperAppService.idl -index 53ea934dd4876e4b491b724385c8fbf7d00ee6cd..0b7b88c853b21ce778d8e87fea0a2bfe839ad412 100644 +index e6490ebb086eb4dca9db560c14e073a9e7722e29..2792373dcb30ebc787a9bdca306ac12e72aa19ec 100644 --- a/uriloader/exthandler/nsIExternalHelperAppService.idl +++ b/uriloader/exthandler/nsIExternalHelperAppService.idl @@ -6,8 +6,11 @@ @@ -3017,10 +3017,10 @@ index 7098db301770ecb5b9a506d7caec89d5cf63384b..aff8d7562f8a3e595a077ce8e5910087 } #endif diff --git a/widget/MouseEvents.h b/widget/MouseEvents.h -index 8004d6fe2130246252e57198f2ea731f84d1968a..7a0774b8320741108dc209c3b7069f524b1360ca 100644 +index 9f685cf741d94a48d552ca11f10759ea4c69969d..594a5c6549e5c0837f91de5b6ef76d6a35e45a5d 100644 --- a/widget/MouseEvents.h +++ b/widget/MouseEvents.h -@@ -375,6 +375,9 @@ class WidgetMouseEvent : public WidgetMouseEventBase, +@@ -379,6 +379,9 @@ class WidgetMouseEvent : public WidgetMouseEventBase, // Otherwise, this must be 0. uint32_t mClickCount = 0; @@ -3030,7 +3030,7 @@ index 8004d6fe2130246252e57198f2ea731f84d1968a..7a0774b8320741108dc209c3b7069f52 // Whether the event should ignore scroll frame bounds during dispatch. bool mIgnoreRootScrollFrame = false; -@@ -398,6 +401,7 @@ class WidgetMouseEvent : public WidgetMouseEventBase, +@@ -402,6 +405,7 @@ class WidgetMouseEvent : public WidgetMouseEventBase, mContextMenuTrigger = aEvent.mContextMenuTrigger; mExitFrom = aEvent.mExitFrom; mClickCount = aEvent.mClickCount; @@ -3102,7 +3102,7 @@ index 24b70173c2e8bb9be9fd6255984a70efe3b14099..75ac367a1c4bb44d4b68b5f4ecc6adf5 } if (aEvent.IsMeta()) { diff --git a/widget/gtk/nsFilePicker.cpp b/widget/gtk/nsFilePicker.cpp -index 02b7b185caf4a2352522c0ed4185d89b514c1738..912a270b6d87e1154d844bde2ffe6a3c2b8f3061 100644 +index f09b0e4994304413cbd75f14e6650cf3f9df9025..41c184e74df2b9c59315a236506946bcb5d69216 100644 --- a/widget/gtk/nsFilePicker.cpp +++ b/widget/gtk/nsFilePicker.cpp @@ -21,6 +21,7 @@ @@ -3260,7 +3260,7 @@ index facd2bc65afab8ec1aa322faa20a67464964dfb9..3c5751ad1b7f042bc7cd9a63895cebcd } // namespace widget diff --git a/widget/headless/HeadlessWidget.cpp b/widget/headless/HeadlessWidget.cpp -index daa2d455374fd9f75a5c6ac9f7b91696d88b065c..f45184137b52db0a5774bf3365b15f784532fbdf 100644 +index 290de12f9d0ff858474564fa8ce54814a04f6088..c144a891b84c3c57f394d6d551e6c89dcd6d9829 100644 --- a/widget/headless/HeadlessWidget.cpp +++ b/widget/headless/HeadlessWidget.cpp @@ -111,6 +111,8 @@ void HeadlessWidget::Destroy() { @@ -3288,12 +3288,12 @@ index daa2d455374fd9f75a5c6ac9f7b91696d88b065c..f45184137b52db0a5774bf3365b15f78 } // namespace widget } // namespace mozilla diff --git a/widget/headless/HeadlessWidget.h b/widget/headless/HeadlessWidget.h -index 39833c28e40c61e354119cde429b8389056bafac..a638fb7520b857219ce58fcbf9ca0ed939528924 100644 +index 60598d097d9dae829ebdc15dede78bd915e3107d..3b5ac5e5590dcb36440e28569f71592db3c1218c 100644 --- a/widget/headless/HeadlessWidget.h +++ b/widget/headless/HeadlessWidget.h -@@ -132,6 +132,9 @@ class HeadlessWidget final : public nsBaseWidget { - int32_t aModifierFlags, - nsIObserver* aObserver) override; +@@ -131,6 +131,9 @@ class HeadlessWidget final : public nsBaseWidget { + double aDeltaX, double aDeltaY, int32_t aModifierFlags, + nsISynthesizedEventCallback* aCallback) override; + using SnapshotListener = std::function&&)>; + void SetSnapshotListener(SnapshotListener&& listener); @@ -3302,10 +3302,10 @@ index 39833c28e40c61e354119cde429b8389056bafac..a638fb7520b857219ce58fcbf9ca0ed9 ~HeadlessWidget(); bool mEnabled; diff --git a/widget/nsGUIEventIPC.h b/widget/nsGUIEventIPC.h -index f7262978239665cbe20470da0790d4d177d4c501..70d11aca3d5b509cf5b37d626299a23fede73ba3 100644 +index 2f4c33f151870bb9233899b186a5eccfaa6f5da6..6acf66e4938e3f9727916824a08d6294f8993452 100644 --- a/widget/nsGUIEventIPC.h +++ b/widget/nsGUIEventIPC.h -@@ -244,6 +244,7 @@ struct ParamTraits { +@@ -251,6 +251,7 @@ struct ParamTraits { aParam.mExitFrom.value())); } WriteParam(aWriter, aParam.mClickCount); @@ -3313,7 +3313,7 @@ index f7262978239665cbe20470da0790d4d177d4c501..70d11aca3d5b509cf5b37d626299a23f } static bool Read(MessageReader* aReader, paramType* aResult) { -@@ -268,6 +269,7 @@ struct ParamTraits { +@@ -275,6 +276,7 @@ struct ParamTraits { aResult->mExitFrom = Some(static_cast(exitFrom)); } rv = rv && ReadParam(aReader, &aResult->mClickCount); diff --git a/browser_patches/firefox/preferences/playwright.cfg b/browser_patches/firefox/preferences/playwright.cfg index 5dc8986c1..85deeb381 100644 --- a/browser_patches/firefox/preferences/playwright.cfg +++ b/browser_patches/firefox/preferences/playwright.cfg @@ -36,6 +36,7 @@ pref("fission.bfcacheInParent", false); // When it is enabled, we have to retain "thirdPartyCookie^" permissions // in the storageState. pref("network.cookie.cookieBehavior", 4); +pref("network.cookie.CHIPS.enabled", false); // Increase max number of child web processes so that new pages // get a new process by default and we have a process isolation diff --git a/browser_patches/webkit/UPSTREAM_CONFIG.sh b/browser_patches/webkit/UPSTREAM_CONFIG.sh index d6e587bc4..ecc80310e 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="db897909f7c31d9b38793374c66427b6a4cb3dd3" +BASE_REVISION="cf7cbd32027884e36fa99df79bcd322925f65e48" diff --git a/browser_patches/webkit/embedder/Playwright/mac/AppDelegate.m b/browser_patches/webkit/embedder/Playwright/mac/AppDelegate.m index 0d100edad..19582bb02 100644 --- a/browser_patches/webkit/embedder/Playwright/mac/AppDelegate.m +++ b/browser_patches/webkit/embedder/Playwright/mac/AppDelegate.m @@ -327,15 +327,19 @@ - (WKWebView *)createHeadfulPage:(WKWebViewConfiguration *)configuration withURL - (WKWebView *)createHeadlessPage:(WKWebViewConfiguration *)configuration withURL:(NSString*)urlString { NSRect rect = NSMakeRect(0, 0, 1280, 720); - NSScreen *firstScreen = [[NSScreen screens] objectAtIndex:0]; - NSRect windowRect = NSOffsetRect(rect, -10000, [firstScreen frame].size.height - rect.size.height + 10000); + + // https://github.com/microsoft/playwright/issues/36711 + // https://codereview.chromium.org/1380083005 + NSScreen *firstScreen = [[NSScreen screens] firstObject]; + + NSRect windowRect = firstScreen ? NSOffsetRect(rect, -10000, [firstScreen frame].size.height - rect.size.height + 10000) : rect; NSWindow* window = [[NSWindow alloc] initWithContentRect:windowRect styleMask:NSWindowStyleMaskBorderless backing:(NSBackingStoreType)_NSBackingStoreUnbuffered defer:YES]; WKWebView* webView = [[WKWebView alloc] initWithFrame:[window.contentView bounds] configuration:configuration]; - webView._windowOcclusionDetectionEnabled = NO; if (!webView) return nil; + webView._windowOcclusionDetectionEnabled = NO; webView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; [window.contentView addSubview:webView]; [window setIsVisible:YES]; diff --git a/browser_patches/webkit/patches/bootstrap.diff b/browser_patches/webkit/patches/bootstrap.diff index f713c6fa0..19902f90d 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 1b7690be3268f54cc649a97206fc38fb27453e08..d14a541e585d2b43d6738c9bb5eb26642522b46b 100644 +index 7338af549e9fb377ea1505849b3a2bd3968d8fd7..873872e23a9c3002830600ba3318bb2249b43a00 100644 --- a/Source/JavaScriptCore/CMakeLists.txt +++ b/Source/JavaScriptCore/CMakeLists.txt -@@ -1418,21 +1418,26 @@ set(JavaScriptCore_INSPECTOR_DOMAINS +@@ -1530,21 +1530,26 @@ set(JavaScriptCore_INSPECTOR_DOMAINS ${JAVASCRIPTCORE_DIR}/inspector/protocol/CSS.json ${JAVASCRIPTCORE_DIR}/inspector/protocol/Canvas.json ${JAVASCRIPTCORE_DIR}/inspector/protocol/Console.json @@ -60,10 +60,10 @@ index 53809cbc20c8fb942304a25899a2e5ecccc79ee3..63d93e08bcf8089e4e315e48d5d20937 $(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 cb42b0e750a8b6a64ebc58f1bfe65c50e7b4c99c..ed548bf0ac70da0273a20178a2786e232907ce68 100644 +index f0058c1cea010b2ea0f128fb4670f0fcde93924b..717df1beaa22a0e7c8df0a50f3c0585ada0385d7 100644 --- a/Source/JavaScriptCore/DerivedSources.make +++ b/Source/JavaScriptCore/DerivedSources.make -@@ -303,21 +303,26 @@ INSPECTOR_DOMAINS := \ +@@ -299,21 +299,26 @@ INSPECTOR_DOMAINS := \ $(JavaScriptCore)/inspector/protocol/CSS.json \ $(JavaScriptCore)/inspector/protocol/Canvas.json \ $(JavaScriptCore)/inspector/protocol/Console.json \ @@ -169,7 +169,7 @@ index 6f8cae4d995d3ae020cd09aa0ba0c9ccc1e78409..fc3d0d1e0803ea38a70fee82ea946247 // 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 c988668bd732434ec4eaa485c7bbc0a93e2e64cf..75da5ed8a0575d8fe444f3f251c1af99fed78c97 100644 +index 0f7a5ff3cd0ad31ed9d816fb281a368e1c4ddcd2..8ce48015677d4fb6691a0ce0ac797f5bb370b622 100644 --- a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h +++ b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.h @@ -95,8 +95,11 @@ public: @@ -220,10 +220,10 @@ index 0cc2127c9c12c2d82dea9550bad73f4ffb99ba24..8ca65cc042d435cbc0e05dcc5c5dfc95 } diff --git a/Source/JavaScriptCore/inspector/InspectorTarget.h b/Source/JavaScriptCore/inspector/InspectorTarget.h -index b555c2e5a071d0a6a016061cc60755449557556d..d019346f0932296d15212c76a4a9b56beb565ff4 100644 +index 7c50a9f4c66c1695bd0e300e800c4e882eefdbd2..f530a8210c8ebe551cb2437f47e1a0bc8561e135 100644 --- a/Source/JavaScriptCore/inspector/InspectorTarget.h +++ b/Source/JavaScriptCore/inspector/InspectorTarget.h -@@ -66,8 +66,12 @@ public: +@@ -67,8 +67,12 @@ public: virtual void connect(FrontendChannel::ConnectionType) = 0; virtual void disconnect() = 0; virtual void sendMessageToTargetBackend(const String&) = 0; @@ -268,7 +268,7 @@ index 6bbd0729a65b4a901e7da4dc50cc47c669bd9897..452b25d0e8eba3df1d5f6623dc222048 void warnUnimplemented(const String& method); void internalAddMessage(MessageType, MessageLevel, JSC::JSGlobalObject*, Ref&&); diff --git a/Source/JavaScriptCore/inspector/agents/InspectorRuntimeAgent.cpp b/Source/JavaScriptCore/inspector/agents/InspectorRuntimeAgent.cpp -index 0cb6efeef2430faa5dbd812f71d4abfd5f6eb9df..787ec6a5f8413c0a9dc133cb0e51ccdab58d40d0 100644 +index a478fa0ed95015757707de9ceb163b7e05f52656..f83b62acf22c75b65f5c12346d0756ee3bfa06a1 100644 --- a/Source/JavaScriptCore/inspector/agents/InspectorRuntimeAgent.cpp +++ b/Source/JavaScriptCore/inspector/agents/InspectorRuntimeAgent.cpp @@ -194,9 +194,8 @@ void InspectorRuntimeAgent::callFunctionOn(const Protocol::Runtime::RemoteObject @@ -295,7 +295,7 @@ index 0cb6efeef2430faa5dbd812f71d4abfd5f6eb9df..787ec6a5f8413c0a9dc133cb0e51ccda { Protocol::ErrorString errorString; diff --git a/Source/JavaScriptCore/inspector/agents/InspectorRuntimeAgent.h b/Source/JavaScriptCore/inspector/agents/InspectorRuntimeAgent.h -index 816633a6dfc75a1248f6edb44807e5d4f602568c..687fb7dadfad9357e15a27e0869fa145c46fb39a 100644 +index 8240c23298f680abb6c379d02841c566fb287f1c..c9567e12c54c5775af6d52e59663e18b3a2682e4 100644 --- a/Source/JavaScriptCore/inspector/agents/InspectorRuntimeAgent.h +++ b/Source/JavaScriptCore/inspector/agents/InspectorRuntimeAgent.h @@ -64,6 +64,7 @@ public: @@ -307,7 +307,7 @@ index 816633a6dfc75a1248f6edb44807e5d4f602568c..687fb7dadfad9357e15a27e0869fa145 Protocol::ErrorStringOr> 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 db75bf0f9edf28d3e23ab40740dfb86a6fd3b104..045caf300e3f9085e5d0ae7feb424f4bf3cf964f 100644 +index 827cbaabc98f5571ee007e99fcb7fd987a443f17..c98d8ba1f5ab72ae5f23555ea7518a85c1b77bea 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 db75bf0f9edf28d3e23ab40740dfb86a6fd3b104..045caf300e3f9085e5d0ae7feb424f4b 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 d1ebed23d1fbacd95e1543606b2591438826b0d2..8536f8325de2afa21e814c2d2ce4bef8189bda70 100644 +index 6d221e260d0f3bc6ba3914b8bb14b35befd332b5..a74deeaf80ce411fe9e8600532162f74251b1e22 100644 --- a/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h +++ b/Source/JavaScriptCore/inspector/agents/InspectorTargetAgent.h @@ -53,8 +53,11 @@ public: @@ -1197,10 +1197,10 @@ index 3d032713a7f3bb9645bfc7d42455a0494b5376c0..913dda5e90b86cc5f8e4ca6881f6db57 } diff --git a/Source/JavaScriptCore/inspector/protocol/Playwright.json b/Source/JavaScriptCore/inspector/protocol/Playwright.json new file mode 100644 -index 0000000000000000000000000000000000000000..440dd95173e066a886de120fb3dab7597d85feb6 +index 0000000000000000000000000000000000000000..8dc7f2bd04172dd6cabb1d869a295569b4c38ed8 --- /dev/null +++ b/Source/JavaScriptCore/inspector/protocol/Playwright.json -@@ -0,0 +1,315 @@ +@@ -0,0 +1,316 @@ +{ + "domain": "Playwright", + "availability": ["web"], @@ -1306,7 +1306,8 @@ index 0000000000000000000000000000000000000000..440dd95173e066a886de120fb3dab759 + "description": "Creates new ephemeral browser context.", + "parameters": [ + { "name": "proxyServer", "type": "string", "optional": true, "description": "Proxy server, similar to the one passed to --proxy-server" }, -+ { "name": "proxyBypassList", "type": "string", "optional": true, "description": "Proxy bypass list, similar to the one passed to --proxy-bypass-list" } ++ { "name": "proxyBypassList", "type": "string", "optional": true, "description": "Proxy bypass list, similar to the one passed to --proxy-bypass-list" }, ++ { "name": "enableStoragePartitioning", "type": "boolean", "optional": true, "description": "Wether to use storage partitioning. Be default Playwright disables the partitioning." } + ], + "returns": [ + { "name": "browserContextId", "$ref": "ContextID", "description": "Unique identifier of the context." } @@ -1667,7 +1668,7 @@ index 52920cded24a9c6b0ef6fb4e518664955db4f9fa..bbbabc4e7259088b9404e8cc07eecd6f }, { diff --git a/Source/JavaScriptCore/runtime/ConsoleClient.h b/Source/JavaScriptCore/runtime/ConsoleClient.h -index 24891ad836086fd23024fcb4d08ca63f6974c812..29f4b6b1923383fec7a99d28a4e815dc4536d160 100644 +index ce394be47840853ba08066ebbd1d909fad1f602e..bfd82684410312890c1e112aced16be797437893 100644 --- a/Source/JavaScriptCore/runtime/ConsoleClient.h +++ b/Source/JavaScriptCore/runtime/ConsoleClient.h @@ -78,6 +78,7 @@ public: @@ -1679,10 +1680,10 @@ index 24891ad836086fd23024fcb4d08ca63f6974c812..29f4b6b1923383fec7a99d28a4e815dc private: enum ArgumentRequirement { ArgumentRequired, ArgumentNotRequired }; diff --git a/Source/ThirdParty/libwebrtc/CMakeLists.txt b/Source/ThirdParty/libwebrtc/CMakeLists.txt -index a04e69b350a29f6b569a04d094a15872f0745870..6f2aa0dcb9fe06cdbc8184b80b13bf735729ad1f 100644 +index cb79da46457d2aeddd6c663eeef4d791ed70b0c3..54ca17420dc79c6b763b9da04282098902be0c2d 100644 --- a/Source/ThirdParty/libwebrtc/CMakeLists.txt +++ b/Source/ThirdParty/libwebrtc/CMakeLists.txt -@@ -1807,6 +1807,14 @@ list(APPEND webrtc_SOURCES +@@ -1803,6 +1803,14 @@ list(APPEND webrtc_SOURCES Source/third_party/boringssl/src/util/fipstools/acvp/modulewrapper/modulewrapper.cc ) @@ -1697,7 +1698,7 @@ index a04e69b350a29f6b569a04d094a15872f0745870..6f2aa0dcb9fe06cdbc8184b80b13bf73 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 +@@ -2395,6 +2403,14 @@ set(webrtc_INCLUDE_DIRECTORIES PRIVATE Source/webrtc/modules/video_coding/include ) @@ -1747,7 +1748,7 @@ index 30585bd097ec1e57d0c5f87180bf59a71f0192df..95b7a60b3cc66704c51fd6a6ce4e3258 +_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 df544586f202dfdcb53b0c16ecf7a51b60ca3767..81f39ca4766d0368109ae90b94beb3cbe458a06f 100644 +index 30e52a5829c1c56ebb42cbabc97a5e78d29629e2..f45f24ddf74669d492ae882778bc16698123474a 100644 --- a/Source/ThirdParty/libwebrtc/libwebrtc.xcodeproj/project.pbxproj +++ b/Source/ThirdParty/libwebrtc/libwebrtc.xcodeproj/project.pbxproj @@ -56,6 +56,20 @@ @@ -1771,7 +1772,7 @@ index df544586f202dfdcb53b0c16ecf7a51b60ca3767..81f39ca4766d0368109ae90b94beb3cb /* 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, ); }; }; -@@ -5852,6 +5866,13 @@ +@@ -5637,6 +5651,13 @@ remoteGlobalIDString = DDF30D0527C5C003006A526F; remoteInfo = absl; }; @@ -1785,7 +1786,7 @@ index df544586f202dfdcb53b0c16ecf7a51b60ca3767..81f39ca4766d0368109ae90b94beb3cb /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ -@@ -24570,6 +24591,7 @@ +@@ -24140,6 +24161,7 @@ ); dependencies = ( 410B3827292B73E90003E515 /* PBXTargetDependency */, @@ -1793,7 +1794,7 @@ index df544586f202dfdcb53b0c16ecf7a51b60ca3767..81f39ca4766d0368109ae90b94beb3cb DD2E76E827C6B69A00F2A74C /* PBXTargetDependency */, CDEBB4CC24C01AB400ADBD44 /* PBXTargetDependency */, 411ED040212E0811004320BA /* PBXTargetDependency */, -@@ -24663,6 +24685,7 @@ +@@ -24233,6 +24255,7 @@ 4460B8B92B155B6A00392062 /* vp9_qp_parser_fuzzer */, 444A6EF02AEADFC9005FE121 /* vp9_replay_fuzzer */, 44945C512B9BA1C300447FFD /* webm_fuzzer */, @@ -1801,7 +1802,7 @@ index df544586f202dfdcb53b0c16ecf7a51b60ca3767..81f39ca4766d0368109ae90b94beb3cb ); }; /* End PBXProject section */ -@@ -24766,6 +24789,23 @@ +@@ -24336,6 +24359,23 @@ shellPath = /bin/sh; shellScript = "[ -z \"${WK_DERIVED_SDK_HEADERS_DIR}\" -o -d \"${WK_DERIVED_SDK_HEADERS_DIR}\" ] && touch \"${SCRIPT_OUTPUT_FILE_0}\"\n"; }; @@ -1825,7 +1826,7 @@ index df544586f202dfdcb53b0c16ecf7a51b60ca3767..81f39ca4766d0368109ae90b94beb3cb /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ -@@ -27789,6 +27829,11 @@ +@@ -27359,6 +27399,11 @@ target = DDF30D0527C5C003006A526F /* absl */; targetProxy = DD2E76E727C6B69A00F2A74C /* PBXContainerItemProxy */; }; @@ -1837,7 +1838,7 @@ index df544586f202dfdcb53b0c16ecf7a51b60ca3767..81f39ca4766d0368109ae90b94beb3cb /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ -@@ -28556,6 +28601,27 @@ +@@ -28126,6 +28171,27 @@ }; name = Production; }; @@ -1865,7 +1866,7 @@ index df544586f202dfdcb53b0c16ecf7a51b60ca3767..81f39ca4766d0368109ae90b94beb3cb FB39D0711200ED9200088E69 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5D7C59C71208C68B001C873E /* DebugRelease.xcconfig */; -@@ -28938,6 +29004,16 @@ +@@ -28508,6 +28574,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Production; }; @@ -1904,10 +1905,10 @@ index 8675218a87262162d91bf992d00a1eecaf83f289..7dfe83ab3f9dd97bb13721f7034b4963 WEBKIT_ADD_TARGET_CXX_FLAGS(Skia diff --git a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml -index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb81574982ee7fd1 100644 +index 1d15bce935b114c9c4634cfa127ccaf1ada207e2..63ec486759061e928bbd19bb630a6cf13ef04d3c 100644 --- a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml +++ b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml -@@ -562,6 +562,7 @@ ApplePayEnabled: +@@ -540,6 +540,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. @@ -1915,7 +1916,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 AsyncClipboardAPIEnabled: type: bool status: mature -@@ -572,7 +573,7 @@ AsyncClipboardAPIEnabled: +@@ -550,7 +551,7 @@ AsyncClipboardAPIEnabled: default: false WebKit: "PLATFORM(COCOA) || PLATFORM(GTK) || PLATFORM(WPE)" : true @@ -1924,7 +1925,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 WebCore: default: false -@@ -832,13 +833,10 @@ BlobFileAccessEnforcementEnabled: +@@ -811,13 +812,10 @@ BlobFileAccessEnforcementEnabled: sharedPreferenceForWebProcess: true defaultValue: WebKitLegacy: @@ -1938,7 +1939,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 default: false BlockFontServiceInWebContentSandbox: -@@ -2089,6 +2087,7 @@ CrossOriginEmbedderPolicyEnabled: +@@ -2051,6 +2049,7 @@ CrossOriginEmbedderPolicyEnabled: WebCore: default: false @@ -1946,7 +1947,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 CrossOriginOpenerPolicyEnabled: type: bool status: stable -@@ -2162,6 +2161,7 @@ DOMAudioSessionFullEnabled: +@@ -2124,6 +2123,7 @@ DOMAudioSessionFullEnabled: WebCore: default: false @@ -1954,7 +1955,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 DOMPasteAccessRequestsEnabled: type: bool status: internal -@@ -2173,7 +2173,7 @@ DOMPasteAccessRequestsEnabled: +@@ -2135,7 +2135,7 @@ DOMPasteAccessRequestsEnabled: default: false WebKit: "PLATFORM(IOS) || PLATFORM(MAC) || PLATFORM(GTK) || PLATFORM(WPE) || PLATFORM(VISION)": true @@ -1963,7 +1964,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 WebCore: default: false -@@ -2239,10 +2239,10 @@ DataListElementEnabled: +@@ -2201,10 +2201,10 @@ DataListElementEnabled: WebKitLegacy: default: false WebKit: @@ -1976,7 +1977,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 default: false sharedPreferenceForWebProcess: true -@@ -2255,7 +2255,7 @@ DataTransferItemsEnabled: +@@ -2217,7 +2217,7 @@ DataTransferItemsEnabled: WebKitLegacy: default: true WebKit: @@ -1985,7 +1986,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 default: false WebCore: default: false -@@ -2497,7 +2497,7 @@ DirectoryUploadEnabled: +@@ -2460,7 +2460,7 @@ DirectoryUploadEnabled: WebKitLegacy: default: false WebKit: @@ -1994,7 +1995,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 default: false WebCore: default: false -@@ -3022,10 +3022,10 @@ FullScreenEnabled: +@@ -2999,10 +2999,10 @@ FullScreenEnabled: WebKitLegacy: default: false WebKit: @@ -2007,7 +2008,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 default: false sharedPreferenceForWebProcess: true -@@ -3632,7 +3632,7 @@ InputTypeColorEnabled: +@@ -3639,7 +3639,7 @@ InputTypeColorEnabled: WebKitLegacy: default: false WebKit: @@ -2016,7 +2017,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 default: false WebCore: default: false -@@ -3665,7 +3665,7 @@ InputTypeDateEnabled: +@@ -3672,7 +3672,7 @@ InputTypeDateEnabled: "PLATFORM(IOS_FAMILY)": true default: false WebKit: @@ -2025,7 +2026,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 default: false WebCore: default: false -@@ -3681,7 +3681,7 @@ InputTypeDateTimeLocalEnabled: +@@ -3688,7 +3688,7 @@ InputTypeDateTimeLocalEnabled: "PLATFORM(IOS_FAMILY)": true default: false WebKit: @@ -2034,7 +2035,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 default: false WebCore: default: false -@@ -3713,7 +3713,7 @@ InputTypeTimeEnabled: +@@ -3720,7 +3720,7 @@ InputTypeTimeEnabled: "PLATFORM(IOS_FAMILY)": true default: false WebKit: @@ -2043,7 +2044,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 default: false WebCore: default: false -@@ -3774,6 +3774,7 @@ InspectorMaximumResourcesContentSize: +@@ -3781,6 +3781,7 @@ InspectorMaximumResourcesContentSize: "PLATFORM(WPE)": 50 default: 200 @@ -2051,7 +2052,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 InspectorStartsAttached: type: bool status: embedder -@@ -3781,7 +3782,7 @@ InspectorStartsAttached: +@@ -3788,7 +3789,7 @@ InspectorStartsAttached: exposed: [ WebKit ] defaultValue: WebKit: @@ -2060,7 +2061,7 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 InspectorSupportsShowingCertificate: type: bool -@@ -5701,7 +5702,7 @@ PermissionsAPIEnabled: +@@ -5755,7 +5756,7 @@ PermissionsAPIEnabled: WebKitLegacy: default: false WebKit: @@ -2069,27 +2070,16 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 default: false WebCore: default: false -@@ -5780,6 +5781,19 @@ PitchCorrectionAlgorithm: +@@ -5850,7 +5851,7 @@ PointerLockEnabled: + "PLATFORM(IOS_FAMILY)": false + default: true WebCore: - default: MediaPlayerEnums::PitchCorrectionAlgorithm::BestAllAround - -+# Playwright: add preference 'PointerLockEnabled'. -+PointerLockEnabled: -+ type: bool -+ status: embedder -+ condition: ENABLE(POINTER_LOCK) -+ defaultValue: -+ WebKitLegacy: -+ default: true -+ WebKit: -+ default: true -+ WebCore: +- default: false + default: true -+ + PointerLockOptionsEnabled: type: bool - status: stable -@@ -6364,7 +6378,7 @@ ScreenOrientationAPIEnabled: +@@ -6437,7 +6438,7 @@ ScreenOrientationAPIEnabled: WebKitLegacy: default: false WebKit: @@ -2098,42 +2088,8 @@ index fde8f264b4df392fc8c6a06309f9a676f9c04c9c..955a01995909ccda6d486479bb815749 WebCore: default: false sharedPreferenceForWebProcess: true -@@ -7847,6 +7861,7 @@ UseDamagingInformationForCompositing: - WebCore: - default: false - -+# Playwright: force-disable on Windows. - UseGPUProcessForCanvasRenderingEnabled: - type: bool - status: stable -@@ -7859,7 +7874,7 @@ UseGPUProcessForCanvasRenderingEnabled: - defaultValue: - WebKit: - "ENABLE(GPU_PROCESS_BY_DEFAULT)": true -- "USE(GRAPHICS_LAYER_WC)": true -+ "USE(GRAPHICS_LAYER_WC)": false - default: false - - UseGPUProcessForDOMRenderingEnabled: -@@ -7904,6 +7919,7 @@ UseGPUProcessForMediaEnabled: - sharedPreferenceForWebProcess: true - mediaPlaybackRelated: true - -+# Playwright: force-disable on Windows. - UseGPUProcessForWebGLEnabled: - type: bool - status: internal -@@ -7915,7 +7931,7 @@ UseGPUProcessForWebGLEnabled: - default: false - WebKit: - "ENABLE(GPU_PROCESS_BY_DEFAULT) && ENABLE(GPU_PROCESS_WEBGL_BY_DEFAULT)": true -- "USE(GRAPHICS_LAYER_WC)": true -+ "USE(GRAPHICS_LAYER_WC)": false - default: false - 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 c7bb871d6471f8ad9144db5063f39e37db20a396..0c09abedbc001572341f30deabefe588e4938e71 100644 +index 560f68add67cfce8bc5fb7b90b80160cb4019958..9bc9c8d168411c8f1bf0fe55a149a81e57c10b8e 100644 --- a/Source/WTF/wtf/PlatformEnable.h +++ b/Source/WTF/wtf/PlatformEnable.h @@ -381,7 +381,7 @@ @@ -2144,8 +2100,8 @@ index c7bb871d6471f8ad9144db5063f39e37db20a396..0c09abedbc001572341f30deabefe588 +#define ENABLE_ORIENTATION_EVENTS 1 #endif - #if !defined(ENABLE_OVERFLOW_SCROLLING_TOUCH) -@@ -498,7 +498,7 @@ + #if OS(WINDOWS) +@@ -494,7 +494,7 @@ #endif #if !defined(ENABLE_TOUCH_EVENTS) @@ -2155,10 +2111,10 @@ index c7bb871d6471f8ad9144db5063f39e37db20a396..0c09abedbc001572341f30deabefe588 #if !defined(ENABLE_TOUCH_ACTION_REGIONS) diff --git a/Source/WTF/wtf/PlatformEnableCocoa.h b/Source/WTF/wtf/PlatformEnableCocoa.h -index ab4579dd256f98d3984882e2a2f26a527993416e..3a529e1864e88ccf3ce2df47176b0215427eb4bc 100644 +index 02a85989ff83d25a2fd7bfeec0de13c3065ce45f..0badbfca93d010d259dba4d5f98fc902143d64e2 100644 --- a/Source/WTF/wtf/PlatformEnableCocoa.h +++ b/Source/WTF/wtf/PlatformEnableCocoa.h -@@ -795,7 +795,7 @@ +@@ -779,7 +779,7 @@ #endif #if !defined(ENABLE_SEC_ITEM_SHIM) @@ -2168,10 +2124,10 @@ index ab4579dd256f98d3984882e2a2f26a527993416e..3a529e1864e88ccf3ce2df47176b0215 #if !defined(ENABLE_SERVER_PRECONNECT) diff --git a/Source/WTF/wtf/PlatformHave.h b/Source/WTF/wtf/PlatformHave.h -index bd819cb4b1bef3c4758353827ee0de710708848e..40c923882da567889b48730d3ec3b809aa040b1a 100644 +index 1bddb4666d19509a6e9ac202816dfa1ccbfcf192..33db65976aab8d615ded3c2a5aea0c12e818af39 100644 --- a/Source/WTF/wtf/PlatformHave.h +++ b/Source/WTF/wtf/PlatformHave.h -@@ -1059,7 +1059,8 @@ +@@ -1063,7 +1063,8 @@ #endif #if PLATFORM(MAC) @@ -2181,6 +2137,19 @@ index bd819cb4b1bef3c4758353827ee0de710708848e..40c923882da567889b48730d3ec3b809 #endif #if !defined(HAVE_LOCKDOWN_MODE_PDF_ADDITIONS) && \ +diff --git a/Source/WTF/wtf/Variant.h b/Source/WTF/wtf/Variant.h +index eab0cb4483bd7bf10be2608712112305a100c6d1..d7ed1288ca2e94b707254128473f0f4fe056750c 100644 +--- a/Source/WTF/wtf/Variant.h ++++ b/Source/WTF/wtf/Variant.h +@@ -25,7 +25,7 @@ + + #pragma once + +-#if PLATFORM(COCOA) || PLATFORM(GTK) || PLATFORM(WPE) ++#if PLATFORM(COCOA) + + // MPark.Variant + // diff --git a/Source/WTF/wtf/unicode/UTF8Conversion.h b/Source/WTF/wtf/unicode/UTF8Conversion.h index 007b8fe3292f326504013be8198ae020f7aacf35..1c722c473732ffe05fdb61010fa4417e3e399d1f 100644 --- a/Source/WTF/wtf/unicode/UTF8Conversion.h @@ -2198,10 +2167,10 @@ index 007b8fe3292f326504013be8198ae020f7aacf35..1c722c473732ffe05fdb61010fa4417e namespace Unicode { diff --git a/Source/WebCore/DerivedSources.make b/Source/WebCore/DerivedSources.make -index 6d76e72ddae832186e6f3609146ac29b91e77a85..18a0976d7c065968d688fb1f585b0022541bac04 100644 +index 770b335aea2c8d808bb68e2a6950d6bff3b50dc9..b203428e87c725facdaab7dba01d0e1ced72b09d 100644 --- a/Source/WebCore/DerivedSources.make +++ b/Source/WebCore/DerivedSources.make -@@ -1230,6 +1230,10 @@ JS_BINDING_IDLS := \ +@@ -1226,6 +1226,10 @@ JS_BINDING_IDLS := \ $(WebCore)/dom/SubscriberCallback.idl \ $(WebCore)/dom/SubscriptionObserver.idl \ $(WebCore)/dom/SubscriptionObserverCallback.idl \ @@ -2212,7 +2181,7 @@ index 6d76e72ddae832186e6f3609146ac29b91e77a85..18a0976d7c065968d688fb1f585b0022 $(WebCore)/dom/Text.idl \ $(WebCore)/dom/TextDecoder.idl \ $(WebCore)/dom/TextDecoderStream.idl \ -@@ -1830,9 +1834,6 @@ JS_BINDING_IDLS := \ +@@ -1836,9 +1840,6 @@ JS_BINDING_IDLS := \ ADDITIONAL_BINDING_IDLS = \ DocumentTouch.idl \ GestureEvent.idl \ @@ -2223,17 +2192,17 @@ index 6d76e72ddae832186e6f3609146ac29b91e77a85..18a0976d7c065968d688fb1f585b0022 vpath %.in $(WEBKITADDITIONS_HEADER_SEARCH_PATHS) diff --git a/Source/WebCore/Modules/geolocation/Geolocation.cpp b/Source/WebCore/Modules/geolocation/Geolocation.cpp -index 51d4e0b4cbc69d2c5c5f76b7063b54865b6fc2d0..d23693e52b96d579f0d828dd825c5dab8572e7c7 100644 +index 1bcfaa96ad38e82973150e8292b6d9f7fc40279a..55cdec501f3de05dc7088cdc6d45e38cdde75b01 100644 --- a/Source/WebCore/Modules/geolocation/Geolocation.cpp +++ b/Source/WebCore/Modules/geolocation/Geolocation.cpp @@ -374,8 +374,9 @@ bool Geolocation::shouldBlockGeolocationRequests() + bool isSecure = SecurityOrigin::isSecure(document->url()) || document->isSecureContext(); - bool hasMixedContent = !document->foundMixedContent().isEmpty(); bool isLocalOrigin = securityOrigin()->isLocal(); + bool isPotentiallyTrustworthy = securityOrigin()->isPotentiallyTrustworthy(); if (document->canAccessResource(ScriptExecutionContext::ResourceType::Geolocation) != ScriptExecutionContext::HasResourceAccess::No) { -- if (isLocalOrigin || (isSecure && !hasMixedContent)) -+ if (isLocalOrigin || (isSecure && !hasMixedContent) || isPotentiallyTrustworthy) +- if (isLocalOrigin || isSecure) ++ if (isLocalOrigin || isSecure || isPotentiallyTrustworthy) return false; } @@ -2286,7 +2255,7 @@ index 72b2846f2c82818fc9a64fd90b7cba0c0601e15f..22277ab6c3233f040852d9daf9becf7b elseif (USE_SKIA) list(APPEND WebCore_SOURCES diff --git a/Source/WebCore/SourcesCocoa.txt b/Source/WebCore/SourcesCocoa.txt -index f09f2e709ee13060d25d4419c05affd987e6328f..16a6491a03dc6aeaf5bfdb4c5b75c3bd6dcd6944 100644 +index ef1d576a4a7747af1ee45a9a172b5ac2d4efed20..e5d8b229f4e6a76ceb86a86bdd1f01de66b73a29 100644 --- a/Source/WebCore/SourcesCocoa.txt +++ b/Source/WebCore/SourcesCocoa.txt @@ -732,3 +732,9 @@ testing/cocoa/WebViewVisualIdentificationOverlay.mm @@ -2300,10 +2269,10 @@ index f09f2e709ee13060d25d4419c05affd987e6328f..16a6491a03dc6aeaf5bfdb4c5b75c3bd +JSTouchList.cpp +// Playwright end diff --git a/Source/WebCore/SourcesGTK.txt b/Source/WebCore/SourcesGTK.txt -index 393fc726c204fc3037287d67cc6b7aa9a5a9c4c3..7e45bf78bb7f0688cf3e09449728ad32e2be3870 100644 +index 2bf30108f206c8f21cb63ef2aa54744864862c6c..80bf1267be5754fa6363c9d4af399803daf4fb38 100644 --- a/Source/WebCore/SourcesGTK.txt +++ b/Source/WebCore/SourcesGTK.txt -@@ -113,3 +113,10 @@ platform/unix/SharedMemoryUnix.cpp +@@ -115,3 +115,10 @@ platform/unix/SharedMemoryUnix.cpp platform/xdg/MIMETypeRegistryXdg.cpp platform/xr/openxr/PlatformXROpenXR.cpp @@ -2315,36 +2284,23 @@ index 393fc726c204fc3037287d67cc6b7aa9a5a9c4c3..7e45bf78bb7f0688cf3e09449728ad32 +JSSpeechSynthesisEventInit.cpp +// Playwright: end. diff --git a/Source/WebCore/SourcesWPE.txt b/Source/WebCore/SourcesWPE.txt -index f5fe901c57ba84d42c8ee3be47c404069ac6fed8..599a40b75fdb63306f706dbb7ff845b9e29a473a 100644 +index 9a760528751e6af3e2599a9b0b250607d54430c3..4c230e3ea7af8ffeb1013b2e8a17a297d714372a 100644 --- a/Source/WebCore/SourcesWPE.txt +++ b/Source/WebCore/SourcesWPE.txt -@@ -48,6 +48,8 @@ editing/glib/WebContentReaderGLib.cpp - - loader/soup/ResourceLoaderSoup.cpp - -+page/wpe/DragControllerWPE.cpp -+ - page/linux/ResourceUsageOverlayLinux.cpp - page/linux/ResourceUsageThreadLinux.cpp - -@@ -99,3 +101,12 @@ platform/wpe/PlatformScreenWPE.cpp +@@ -104,3 +104,8 @@ platform/wpe/PlatformScreenWPE.cpp platform/xdg/MIMETypeRegistryXdg.cpp platform/xr/openxr/PlatformXROpenXR.cpp + -+// Playwright: begin. -+platform/wpe/DragDataWPE.cpp -+ +JSSpeechSynthesisErrorCode.cpp +JSSpeechSynthesisErrorEvent.cpp +JSSpeechSynthesisErrorEventInit.cpp +JSSpeechSynthesisEventInit.cpp -+// Playwright: end. diff --git a/Source/WebCore/WebCore.xcodeproj/project.pbxproj b/Source/WebCore/WebCore.xcodeproj/project.pbxproj -index 612c2492b160e840f942b1b5bef3faf995f411cf..221ffcfc32818d1d2839aa984eefc43f60d2ba31 100644 +index cc25f5acc2b5560f6a5ca51a7c3a737e9000de8e..97ffb63520aff377162d398908b972a4f71c28af 100644 --- a/Source/WebCore/WebCore.xcodeproj/project.pbxproj +++ b/Source/WebCore/WebCore.xcodeproj/project.pbxproj -@@ -6503,6 +6503,13 @@ +@@ -6596,6 +6596,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, ); }; }; @@ -2358,7 +2314,7 @@ index 612c2492b160e840f942b1b5bef3faf995f411cf..221ffcfc32818d1d2839aa984eefc43f 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, ); }; }; -@@ -21257,6 +21264,14 @@ +@@ -21529,6 +21536,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 = ""; }; @@ -2373,7 +2329,7 @@ index 612c2492b160e840f942b1b5bef3faf995f411cf..221ffcfc32818d1d2839aa984eefc43f 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 = ""; }; -@@ -29046,6 +29061,11 @@ +@@ -29367,6 +29382,11 @@ BC4A5324256055590028C592 /* TextDirectionSubmenuInclusionBehavior.h */, 2D4F96F11A1ECC240098BF88 /* TextIndicator.cpp */, 2D4F96F21A1ECC240098BF88 /* TextIndicator.h */, @@ -2383,9 +2339,9 @@ index 612c2492b160e840f942b1b5bef3faf995f411cf..221ffcfc32818d1d2839aa984eefc43f + F050E16C23AD66630011CE47 /* TouchList.cpp */, + F050E16B23AD66620011CE47 /* TouchList.h */, F48570A42644C76D00C05F71 /* TranslationContextMenuInfo.h */, - F4E1965F21F26E4E00285078 /* UndoItem.cpp */, - 2ECDBAD521D8906300F00ECD /* UndoItem.h */, -@@ -36194,6 +36214,8 @@ + D640B24C2E3058C800EB6C49 /* UADataValues.h */, + D640B24E2E3058C800EB6C49 /* UADataValues.idl */, +@@ -36803,6 +36823,8 @@ 29E4D8DF16B0940F00C84704 /* PlatformSpeechSynthesizer.h */, 1AD8F81A11CAB9E900E93E54 /* PlatformStrategies.cpp */, 1AD8F81911CAB9E900E93E54 /* PlatformStrategies.h */, @@ -2394,7 +2350,7 @@ index 612c2492b160e840f942b1b5bef3faf995f411cf..221ffcfc32818d1d2839aa984eefc43f FE3DC9932D0C063C0021B6FC /* PlatformTZoneImpls.cpp */, 0FD7C21D23CE41E30096D102 /* PlatformWheelEvent.cpp */, 935C476A09AC4D4F00A6AAB4 /* PlatformWheelEvent.h */, -@@ -39050,6 +39072,7 @@ +@@ -39663,6 +39685,7 @@ AD6E71AB1668899D00320C13 /* DocumentSharedObjectPool.h */, 6BDB5DC1227BD3B800919770 /* DocumentStorageAccess.cpp */, 6BDB5DC0227BD3B800919770 /* DocumentStorageAccess.h */, @@ -2402,7 +2358,7 @@ index 612c2492b160e840f942b1b5bef3faf995f411cf..221ffcfc32818d1d2839aa984eefc43f 7CE7FA5B1EF882300060C9D6 /* DocumentTouch.cpp */, 7CE7FA591EF882300060C9D6 /* DocumentTouch.h */, A8185F3209765765005826D9 /* DocumentType.cpp */, -@@ -44014,6 +44037,8 @@ +@@ -44643,6 +44666,8 @@ F4E90A3C2B52038E002DA469 /* PlatformTextAlternatives.h in Headers */, 0F7D07331884C56C00B4AF86 /* PlatformTextTrack.h in Headers */, 074E82BB18A69F0E007EF54C /* PlatformTimeRanges.h in Headers */, @@ -2411,7 +2367,7 @@ index 612c2492b160e840f942b1b5bef3faf995f411cf..221ffcfc32818d1d2839aa984eefc43f CDD08ABD277E542600EA3755 /* PlatformTrackConfiguration.h in Headers */, CD1F9B022700323D00617EB6 /* PlatformVideoColorPrimaries.h in Headers */, CD1F9B01270020B700617EB6 /* PlatformVideoColorSpace.h in Headers */, -@@ -45423,6 +45448,7 @@ +@@ -46131,6 +46156,7 @@ 0F54DD081881D5F5003EEDBB /* Touch.h in Headers */, 71B7EE0D21B5C6870031C1EF /* TouchAction.h in Headers */, 0F54DD091881D5F5003EEDBB /* TouchEvent.h in Headers */, @@ -2419,16 +2375,17 @@ index 612c2492b160e840f942b1b5bef3faf995f411cf..221ffcfc32818d1d2839aa984eefc43f 0F54DD0A1881D5F5003EEDBB /* TouchList.h in Headers */, 070334D71459FFD5008D8D45 /* TrackBase.h in Headers */, BE88E0C21715CE2600658D98 /* TrackListBase.h in Headers */, -@@ -46627,6 +46653,8 @@ +@@ -47340,7 +47366,9 @@ 2D22830323A8470700364B7E /* CursorMac.mm in Sources */, 5CBD59592280E926002B22AA /* CustomHeaderFields.cpp in Sources */, 07E4BDBF2A3A5FAB000D5509 /* DictationCaretAnimator.cpp in Sources */, + F050E17423AD6A800011CE47 /* DocumentTouch.cpp in Sources */, + 0749E9512E275A23009B912B /* EditingHTMLConverter.mm in Sources */, + 329C0C2528BD96EB00F187D2 /* ElementName.cpp in Sources */, 7CE6CBFD187F394900D46BF5 /* FormatConverter.cpp in Sources */, 4667EA3E2968D9DA00BAB1E2 /* GameControllerHapticEffect.mm in Sources */, 46FE73D32968E52000B8064C /* GameControllerHapticEngines.mm in Sources */, -@@ -46718,6 +46746,9 @@ +@@ -47433,6 +47461,9 @@ CE88EE262414467B007F29C2 /* TextAlternativeWithRange.mm in Sources */, BE39137129B267F500FA5D4F /* TextTransformCocoa.cpp in Sources */, 51DF6D800B92A18E00C2DC85 /* ThreadCheck.mm in Sources */, @@ -2439,18 +2396,18 @@ index 612c2492b160e840f942b1b5bef3faf995f411cf..221ffcfc32818d1d2839aa984eefc43f 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 9587b90007ab8ee8ab32f7f86219732f11d457bf..81e09081c42db3c905f1adb2f917b0bca32c24c2 100644 +index f41551ba8e1d31c2868ede9dc67c75488d0f815e..e2434bb801cb4db6fd965419a8569265279a8a0d 100644 --- a/Source/WebCore/accessibility/AccessibilityObject.cpp +++ b/Source/WebCore/accessibility/AccessibilityObject.cpp -@@ -72,6 +72,7 @@ +@@ -78,6 +78,7 @@ #include "HTMLTableSectionElement.h" #include "HTMLTextAreaElement.h" #include "HitTestResult.h" +#include "InspectorInstrumentation.h" #include "LocalFrame.h" #include "LocalizedStrings.h" - #include "MathMLNames.h" -@@ -3998,7 +3999,12 @@ AccessibilityObjectInclusion AccessibilityObject::defaultObjectInclusion() const + #include "Logging.h" +@@ -4062,7 +4063,12 @@ AccessibilityObjectInclusion AccessibilityObject::defaultObjectInclusion() const if (role() == AccessibilityRole::ApplicationDialog) return AccessibilityObjectInclusion::IncludeObject; @@ -2465,7 +2422,7 @@ index 9587b90007ab8ee8ab32f7f86219732f11d457bf..81e09081c42db3c905f1adb2f917b0bc bool AccessibilityObject::isWithinHiddenWebArea() const diff --git a/Source/WebCore/bindings/js/WebCoreBuiltinNames.h b/Source/WebCore/bindings/js/WebCoreBuiltinNames.h -index c4b32aaefc75bdef3b52f2469786f9abe11fb8d4..6d84e95ff7f766a2e26c1414be92af89fa7d73cf 100644 +index 9d6545ad60257c66a3f04cee67590d00ff9a5720..9a4794cc573a6507395cdd2c7a24209c0d9db3ba 100644 --- a/Source/WebCore/bindings/js/WebCoreBuiltinNames.h +++ b/Source/WebCore/bindings/js/WebCoreBuiltinNames.h @@ -190,6 +190,8 @@ namespace WebCore { @@ -2524,7 +2481,7 @@ index c0b9d058536120b4c368ab8094c16f19a5d2acba..37e7e016f5862bab19d750d06bd72b10 { 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 315635014cf133628d72942eb230df4e8cad036c..5424ad948028c192d9fc165cde0102e15ad65b56 100644 +index b22db7093505b1bfc39b15200640db5a1363e0ef..e96117436863ace7d52c894f37027187e33c182b 100644 --- a/Source/WebCore/dom/DataTransfer.h +++ b/Source/WebCore/dom/DataTransfer.h @@ -92,6 +92,9 @@ public: @@ -2561,44 +2518,8 @@ index 9043052540b13d8120fb641de6337af46c3b36ef..a0f89e64b64640d2d4dbc14734868c4d Exposed=Window ] interface DeviceOrientationEvent : Event { readonly attribute unrestricted double? alpha; -diff --git a/Source/WebCore/dom/Document+PointerLock.idl b/Source/WebCore/dom/Document+PointerLock.idl -index 2e9c9fda6a920cd8904432bd1bdbfbf32d01c085..2fd33597e905b0544665ff765f310945305a0c4a 100644 ---- a/Source/WebCore/dom/Document+PointerLock.idl -+++ b/Source/WebCore/dom/Document+PointerLock.idl -@@ -25,6 +25,7 @@ - - // https://w3c.github.io/pointerlock/#extensions-to-the-document-interface - [ -+ EnabledBySetting=PointerLockEnabled, - Conditional=POINTER_LOCK - ] partial interface Document { - attribute EventHandler onpointerlockchange; -diff --git a/Source/WebCore/dom/DocumentOrShadowRoot+PointerLock.idl b/Source/WebCore/dom/DocumentOrShadowRoot+PointerLock.idl -index 9b8dbfc15ce078702321abcd6c0e636df7a60510..2956f7098e87af10ab8f5584b456ce9a6d432a20 100644 ---- a/Source/WebCore/dom/DocumentOrShadowRoot+PointerLock.idl -+++ b/Source/WebCore/dom/DocumentOrShadowRoot+PointerLock.idl -@@ -25,6 +25,7 @@ - - // https://w3c.github.io/pointerlock/#extensions-to-the-documentorshadowroot-mixin - [ -+ EnabledBySetting=PointerLockEnabled, - Conditional=POINTER_LOCK - ] partial interface mixin DocumentOrShadowRoot { - readonly attribute Element? pointerLockElement; -diff --git a/Source/WebCore/dom/Element+PointerLock.idl b/Source/WebCore/dom/Element+PointerLock.idl -index 9b344003de17b96d8b9ca8c7f32143a27543b1ea..2208a3f2b7d930bcd291e65b474d4c3023d2a7e4 100644 ---- a/Source/WebCore/dom/Element+PointerLock.idl -+++ b/Source/WebCore/dom/Element+PointerLock.idl -@@ -24,6 +24,7 @@ - */ - - [ -+ EnabledBySetting=PointerLockEnabled, - Conditional=POINTER_LOCK - ] partial interface Element { - // Returns Promise if PointerLockOptionsEnabled Runtime Flag is set, otherwise returns undefined. diff --git a/Source/WebCore/dom/PointerEvent.cpp b/Source/WebCore/dom/PointerEvent.cpp -index 6b0390aba75731707ce48ea206b87974ffb15857..8c634c0748f0d447476d460ee2800f82d232d7ff 100644 +index b691425261078163e043b062a0a9f1fe5215d2b5..d3a8a99b08099cf16f0b0274d680c32e9ffef54e 100644 --- a/Source/WebCore/dom/PointerEvent.cpp +++ b/Source/WebCore/dom/PointerEvent.cpp @@ -28,10 +28,13 @@ @@ -2615,8 +2536,8 @@ index 6b0390aba75731707ce48ea206b87974ffb15857..8c634c0748f0d447476d460ee2800f82 namespace WebCore { -@@ -293,4 +296,59 @@ void PointerEvent::receivedTarget() - predictedEvent->setTarget(this->target()); +@@ -331,4 +334,59 @@ double PointerEvent::offsetY() + return adjustedCoordinateForType(offsetLocation().y()); } +#if ENABLE(TOUCH_EVENTS) && !PLATFORM(IOS_FAMILY) && !PLATFORM(WPE) @@ -2641,29 +2562,29 @@ index 6b0390aba75731707ce48ea206b87974ffb15857..8c634c0748f0d447476d460ee2800f82 + return nullAtom(); +} + -+Ref PointerEvent::create(const PlatformTouchEvent& event, const Vector>& coalescedEvents, const Vector>& predictedEvents, unsigned touchIndex, bool isPrimary, Ref&& view, const IntPoint& touchDelta) ++Ref PointerEvent::create(const PlatformTouchEvent& event, const Vector>& coalescedEvents, const Vector>& predictedEvents, unsigned touchIndex, bool isPrimary, Ref&& view, const DoublePoint& touchDelta) +{ + const auto& type = pointerEventType(event.touchPoints().at(touchIndex).state()); + return adoptRef(*new PointerEvent(type, event, coalescedEvents, predictedEvents, typeCanBubble(type), typeIsCancelable(type), touchIndex, isPrimary, WTFMove(view), touchDelta)); +} + -+Ref PointerEvent::create(const PlatformTouchEvent& event, const Vector>& coalescedEvents, const Vector>& predictedEvents, CanBubble canBubble, IsCancelable isCancelable, unsigned touchIndex, bool isPrimary, Ref&& view, const IntPoint& touchDelta) ++Ref PointerEvent::create(const PlatformTouchEvent& event, const Vector>& coalescedEvents, const Vector>& predictedEvents, CanBubble canBubble, IsCancelable isCancelable, unsigned touchIndex, bool isPrimary, Ref&& view, const DoublePoint& touchDelta) +{ + const auto& type = pointerEventType(event.touchPoints().at(touchIndex).state()); + return adoptRef(*new PointerEvent(type, event, coalescedEvents, predictedEvents, canBubble, isCancelable, touchIndex, isPrimary, WTFMove(view), touchDelta)); +} + -+Ref PointerEvent::create(const AtomString& type, const PlatformTouchEvent& event, const Vector>& coalescedEvents, const Vector>& predictedEvents, unsigned touchIndex, bool isPrimary, Ref&& view, const IntPoint& touchDelta) ++Ref PointerEvent::create(const AtomString& type, const PlatformTouchEvent& event, const Vector>& coalescedEvents, const Vector>& predictedEvents, unsigned touchIndex, bool isPrimary, Ref&& view, const DoublePoint& touchDelta) +{ + return adoptRef(*new PointerEvent(type, event, coalescedEvents, predictedEvents, typeCanBubble(type), typeIsCancelable(type), touchIndex, isPrimary, WTFMove(view), touchDelta)); +} + -+PointerEvent::PointerEvent(const AtomString& type, const PlatformTouchEvent& event, const Vector>& coalescedEvents, const Vector>& predictedEvents, CanBubble canBubble, IsCancelable isCancelable, unsigned touchIndex, bool isPrimary, Ref&& view, const IntPoint& touchDelta) ++PointerEvent::PointerEvent(const AtomString& type, const PlatformTouchEvent& event, const Vector>& coalescedEvents, const Vector>& predictedEvents, CanBubble canBubble, IsCancelable isCancelable, unsigned touchIndex, bool isPrimary, Ref&& view, const DoublePoint& touchDelta) + : MouseEvent(EventInterfaceType::PointerEvent, type, canBubble, isCancelable, typeIsComposed(type), event.timestamp().approximateMonotonicTime(), WTFMove(view), 0, + event.touchPoints().at(touchIndex).pos(), event.touchPoints().at(touchIndex).pos(), touchDelta.x(), touchDelta.y(), event.modifiers(), buttonForType(type), buttonsForType(type), nullptr, 0, SyntheticClickType::NoTap, { }, { }, IsSimulated::No, IsTrusted::Yes) + , m_pointerId(event.touchPoints().at(touchIndex).id()) -+ , m_width(2 * event.touchPoints().at(touchIndex).radiusX()) -+ , m_height(2 * event.touchPoints().at(touchIndex).radiusY()) ++ , m_width(2 * event.touchPoints().at(touchIndex).radius().width()) ++ , m_height(2 * event.touchPoints().at(touchIndex).radius().height()) + , m_pressure(event.touchPoints().at(touchIndex).force()) + , m_pointerType(touchPointerEventType()) + , m_isPrimary(isPrimary) @@ -2676,7 +2597,7 @@ index 6b0390aba75731707ce48ea206b87974ffb15857..8c634c0748f0d447476d460ee2800f82 + } // namespace WebCore diff --git a/Source/WebCore/dom/PointerEvent.h b/Source/WebCore/dom/PointerEvent.h -index b034595d01bb63f3d72183c427fcc14695339ae2..1886e4bbba04c6177fad1562c891f2aeff0a8247 100644 +index 2a8233fd13710bd351acd77e66626dd38ffb8749..8dcae583fe4dce69a6604dbd152015510eeb5df6 100644 --- a/Source/WebCore/dom/PointerEvent.h +++ b/Source/WebCore/dom/PointerEvent.h @@ -34,6 +34,8 @@ @@ -2694,16 +2615,16 @@ index b034595d01bb63f3d72183c427fcc14695339ae2..1886e4bbba04c6177fad1562c891f2ae -#if ENABLE(TOUCH_EVENTS) && (PLATFORM(IOS_FAMILY) || PLATFORM(WPE)) +#if ENABLE(TOUCH_EVENTS) - static Ref create(const PlatformTouchEvent&, const Vector>& coalescedEvents, const Vector>& predictedEvents, unsigned touchIndex, bool isPrimary, Ref&&, const IntPoint& touchDelta = { }); - static Ref create(const PlatformTouchEvent&, const Vector>& coalescedEvents, const Vector>& predictedEvents, CanBubble, IsCancelable, unsigned touchIndex, bool isPrimary, Ref&& view, const IntPoint& touchDelta = { }); - static Ref create(const AtomString& type, const PlatformTouchEvent&, const Vector>& coalescedEvents, const Vector>& predictedEvents, unsigned touchIndex, bool isPrimary, Ref&&, const IntPoint& touchDelta = { }); -@@ -173,7 +175,7 @@ private: + static Ref create(const PlatformTouchEvent&, const Vector>& coalescedEvents, const Vector>& predictedEvents, unsigned touchIndex, bool isPrimary, Ref&&, const DoublePoint& touchDelta = { }); + static Ref create(const PlatformTouchEvent&, const Vector>& coalescedEvents, const Vector>& predictedEvents, CanBubble, IsCancelable, unsigned touchIndex, bool isPrimary, Ref&& view, const DoublePoint& touchDelta = { }); + static Ref create(const AtomString& type, const PlatformTouchEvent&, const Vector>& coalescedEvents, const Vector>& predictedEvents, unsigned touchIndex, bool isPrimary, Ref&&, const DoublePoint& touchDelta = { }); +@@ -187,7 +189,7 @@ private: PointerEvent(); PointerEvent(const AtomString&, Init&&, IsTrusted); PointerEvent(const AtomString& type, PointerID, const String& pointerType, IsPrimary); -#if ENABLE(TOUCH_EVENTS) && (PLATFORM(IOS_FAMILY) || PLATFORM(WPE)) +#if ENABLE(TOUCH_EVENTS) - PointerEvent(const AtomString& type, const PlatformTouchEvent&, const Vector>& coalescedEvents, const Vector>& predictedEvents, CanBubble canBubble, IsCancelable isCancelable, unsigned touchIndex, bool isPrimary, Ref&&, const IntPoint& touchDelta = { }); + PointerEvent(const AtomString& type, const PlatformTouchEvent&, const Vector>& coalescedEvents, const Vector>& predictedEvents, CanBubble canBubble, IsCancelable isCancelable, unsigned touchIndex, bool isPrimary, Ref&&, const DoublePoint& touchDelta = { }); #endif diff --git a/Source/WebCore/editing/libwpe/EditorLibWPE.cpp b/Source/WebCore/editing/libwpe/EditorLibWPE.cpp @@ -2734,7 +2655,7 @@ index d0a3d5c048647b07772e1581c76c4eb60ecf41b0..bec324636991079264e620c0dfdaf984 #endif // USE(LIBWPE) diff --git a/Source/WebCore/html/FileInputType.cpp b/Source/WebCore/html/FileInputType.cpp -index 64137520fb575aac59fbb4a8fc8509cbdd8cc04e..5bdac5378686cefef1e05f4aa01a49729c3fd398 100644 +index 51176f56b3e691937622f5b4be0ff39f5e85630b..68c84ce1199ca81180db2b087b53bee2ab3798b5 100644 --- a/Source/WebCore/html/FileInputType.cpp +++ b/Source/WebCore/html/FileInputType.cpp @@ -38,6 +38,7 @@ @@ -2758,7 +2679,7 @@ index 64137520fb575aac59fbb4a8fc8509cbdd8cc04e..5bdac5378686cefef1e05f4aa01a4972 return; diff --git a/Source/WebCore/inspector/InspectorController.cpp b/Source/WebCore/inspector/InspectorController.cpp -index af0482d2d06db91a7964b7662dd3379804062fff..49ed24c301cecabeb21ac5f3d06d2777218cd538 100644 +index 297085f75efe06143a806e1242750c9b6ad92dee..361fb796457998d4b6e7e39a448e03811c456a1c 100644 --- a/Source/WebCore/inspector/InspectorController.cpp +++ b/Source/WebCore/inspector/InspectorController.cpp @@ -296,6 +296,8 @@ void InspectorController::disconnectFrontend(FrontendChannel& frontendChannel) @@ -2769,7 +2690,7 @@ index af0482d2d06db91a7964b7662dd3379804062fff..49ed24c301cecabeb21ac5f3d06d2777 + m_pauseOnStart = PauseCondition::DONT_PAUSE; } - m_inspectorClient->frontendCountChanged(m_frontendRouter->frontendCount()); + m_inspectorBackendClient->frontendCountChanged(m_frontendRouter->frontendCount()); @@ -315,6 +317,8 @@ void InspectorController::disconnectAllFrontends() // The frontend should call setInspectorFrontendClient(nullptr) under closeWindow(). ASSERT(!m_inspectorFrontendClient); @@ -2826,7 +2747,7 @@ index af0482d2d06db91a7964b7662dd3379804062fff..49ed24c301cecabeb21ac5f3d06d2777 + } // namespace WebCore diff --git a/Source/WebCore/inspector/InspectorController.h b/Source/WebCore/inspector/InspectorController.h -index d56afdb6e48840802c5a4f28b9a954695f09965d..7c3223c9f57bc45f4109325433ac4f5e745eb986 100644 +index fe10a03fd9d664e8735970b35819b639a60e4e32..8d3d8e0201213aa2c4ea27674388d2d0372c200b 100644 --- a/Source/WebCore/inspector/InspectorController.h +++ b/Source/WebCore/inspector/InspectorController.h @@ -106,6 +106,12 @@ public: @@ -2859,10 +2780,10 @@ index d56afdb6e48840802c5a4f28b9a954695f09965d..7c3223c9f57bc45f4109325433ac4f5e } // namespace WebCore diff --git a/Source/WebCore/inspector/InspectorInstrumentation.cpp b/Source/WebCore/inspector/InspectorInstrumentation.cpp -index 1c660f381ad7e9a9201aeced2906f39135e810f6..6c47abc2f32b39fd5c0bf9048b9c1224959ba08c 100644 +index d23aa990856bf855830e69b3bbab2cb024d7f198..a80a86c732b514c07a92005fbdd1cc03b347ff3f 100644 --- a/Source/WebCore/inspector/InspectorInstrumentation.cpp +++ b/Source/WebCore/inspector/InspectorInstrumentation.cpp -@@ -595,6 +595,12 @@ void InspectorInstrumentation::applyUserAgentOverrideImpl(InstrumentingAgents& i +@@ -596,6 +596,12 @@ void InspectorInstrumentation::applyUserAgentOverrideImpl(InstrumentingAgents& i pageAgent->applyUserAgentOverride(userAgent); } @@ -2875,7 +2796,7 @@ index 1c660f381ad7e9a9201aeced2906f39135e810f6..6c47abc2f32b39fd5c0bf9048b9c1224 void InspectorInstrumentation::applyEmulatedMediaImpl(InstrumentingAgents& instrumentingAgents, AtomString& media) { if (auto* pageAgent = instrumentingAgents.enabledPageAgent()) -@@ -678,6 +684,12 @@ void InspectorInstrumentation::didFailLoadingImpl(InstrumentingAgents& instrumen +@@ -679,6 +685,12 @@ void InspectorInstrumentation::didFailLoadingImpl(InstrumentingAgents& instrumen consoleAgent->didFailLoading(identifier, error); // This should come AFTER resource notification, front-end relies on this. } @@ -2888,7 +2809,7 @@ index 1c660f381ad7e9a9201aeced2906f39135e810f6..6c47abc2f32b39fd5c0bf9048b9c1224 void InspectorInstrumentation::willLoadXHRSynchronouslyImpl(InstrumentingAgents& instrumentingAgents) { if (auto* networkAgent = instrumentingAgents.enabledNetworkAgent()) -@@ -710,20 +722,17 @@ void InspectorInstrumentation::didReceiveScriptResponseImpl(InstrumentingAgents& +@@ -711,20 +723,17 @@ void InspectorInstrumentation::didReceiveScriptResponseImpl(InstrumentingAgents& void InspectorInstrumentation::domContentLoadedEventFiredImpl(InstrumentingAgents& instrumentingAgents, LocalFrame& frame) { @@ -2912,7 +2833,7 @@ index 1c660f381ad7e9a9201aeced2906f39135e810f6..6c47abc2f32b39fd5c0bf9048b9c1224 } void InspectorInstrumentation::frameDetachedFromParentImpl(InstrumentingAgents& instrumentingAgents, LocalFrame& frame) -@@ -803,12 +812,6 @@ void InspectorInstrumentation::frameDocumentUpdatedImpl(InstrumentingAgents& ins +@@ -804,12 +813,6 @@ void InspectorInstrumentation::frameDocumentUpdatedImpl(InstrumentingAgents& ins pageDOMDebuggerAgent->frameDocumentUpdated(frame); } @@ -2925,7 +2846,7 @@ index 1c660f381ad7e9a9201aeced2906f39135e810f6..6c47abc2f32b39fd5c0bf9048b9c1224 void InspectorInstrumentation::frameStartedLoadingImpl(InstrumentingAgents& instrumentingAgents, LocalFrame& frame) { if (frame.isMainFrame()) { -@@ -839,10 +842,10 @@ void InspectorInstrumentation::frameStoppedLoadingImpl(InstrumentingAgents& inst +@@ -840,10 +843,10 @@ void InspectorInstrumentation::frameStoppedLoadingImpl(InstrumentingAgents& inst inspectorPageAgent->frameStoppedLoading(frame); } @@ -2938,7 +2859,7 @@ index 1c660f381ad7e9a9201aeced2906f39135e810f6..6c47abc2f32b39fd5c0bf9048b9c1224 } void InspectorInstrumentation::frameClearedScheduledNavigationImpl(InstrumentingAgents& instrumentingAgents, Frame& frame) -@@ -857,6 +860,12 @@ void InspectorInstrumentation::accessibilitySettingsDidChangeImpl(InstrumentingA +@@ -858,6 +861,12 @@ void InspectorInstrumentation::accessibilitySettingsDidChangeImpl(InstrumentingA inspectorPageAgent->accessibilitySettingsDidChange(); } @@ -2951,7 +2872,7 @@ index 1c660f381ad7e9a9201aeced2906f39135e810f6..6c47abc2f32b39fd5c0bf9048b9c1224 #if ENABLE(DARK_MODE_CSS) void InspectorInstrumentation::defaultAppearanceDidChangeImpl(InstrumentingAgents& instrumentingAgents) { -@@ -909,6 +918,12 @@ void InspectorInstrumentation::interceptResponseImpl(InstrumentingAgents& instru +@@ -910,6 +919,12 @@ void InspectorInstrumentation::interceptResponseImpl(InstrumentingAgents& instru networkAgent->interceptResponse(response, identifier, WTFMove(handler)); } @@ -2964,7 +2885,7 @@ index 1c660f381ad7e9a9201aeced2906f39135e810f6..6c47abc2f32b39fd5c0bf9048b9c1224 // JavaScriptCore InspectorDebuggerAgent should know Console MessageTypes. static bool isConsoleAssertMessage(MessageSource source, MessageType type) { -@@ -1027,6 +1042,12 @@ void InspectorInstrumentation::consoleStopRecordingCanvasImpl(InstrumentingAgent +@@ -1028,6 +1043,12 @@ void InspectorInstrumentation::consoleStopRecordingCanvasImpl(InstrumentingAgent canvasAgent->consoleStopRecordingCanvas(context); } @@ -2977,7 +2898,7 @@ index 1c660f381ad7e9a9201aeced2906f39135e810f6..6c47abc2f32b39fd5c0bf9048b9c1224 void InspectorInstrumentation::didDispatchDOMStorageEventImpl(InstrumentingAgents& instrumentingAgents, const String& key, const String& oldValue, const String& newValue, StorageType storageType, const SecurityOrigin& securityOrigin) { if (auto* domStorageAgent = instrumentingAgents.enabledDOMStorageAgent()) -@@ -1317,6 +1338,36 @@ void InspectorInstrumentation::renderLayerDestroyedImpl(InstrumentingAgents& ins +@@ -1318,6 +1339,36 @@ void InspectorInstrumentation::renderLayerDestroyedImpl(InstrumentingAgents& ins layerTreeAgent->renderLayerDestroyed(renderLayer); } @@ -3014,7 +2935,7 @@ index 1c660f381ad7e9a9201aeced2906f39135e810f6..6c47abc2f32b39fd5c0bf9048b9c1224 InstrumentingAgents& InspectorInstrumentation::instrumentingAgents(WorkerOrWorkletGlobalScope& globalScope) { return globalScope.inspectorController().m_instrumentingAgents; -@@ -1333,6 +1384,13 @@ InstrumentingAgents& InspectorInstrumentation::instrumentingAgents(Page& page) +@@ -1334,6 +1385,13 @@ InstrumentingAgents& InspectorInstrumentation::instrumentingAgents(Page& page) return page.inspectorController().m_instrumentingAgents.get(); } @@ -3029,7 +2950,7 @@ index 1c660f381ad7e9a9201aeced2906f39135e810f6..6c47abc2f32b39fd5c0bf9048b9c1224 { // Using RefPtr makes us hit the m_inRemovedLastRefFunction assert. diff --git a/Source/WebCore/inspector/InspectorInstrumentation.h b/Source/WebCore/inspector/InspectorInstrumentation.h -index ed15dcc0dd1eb8d22dc48d8c6515b2258db574c5..c297c2d0e61b8ca1f881bb7942c463626bf0118f 100644 +index 645aa88d02d3cc85bfce2905bcba61a2a398a3e7..1097e767c9ed1a6b7b8c8bc449d5d54bcc97419f 100644 --- a/Source/WebCore/inspector/InspectorInstrumentation.h +++ b/Source/WebCore/inspector/InspectorInstrumentation.h @@ -31,6 +31,7 @@ @@ -3341,7 +3262,7 @@ index a67a1244fa526ad5759068e97e0d220f59565d6e..0048589109fccb9472fe35a410337771 + } // namespace WebCore diff --git a/Source/WebCore/inspector/InspectorInstrumentationWebKit.h b/Source/WebCore/inspector/InspectorInstrumentationWebKit.h -index c028341e84e59a6b1b16107fd74feb21f70b12ab..d385418ac34e8f315f201801a2c65226c8f6fee2 100644 +index b8a1585c14a753344d5b52b316511d2efb2d2bfe..910b98291328cd90109beeeb66e9ecf22e124a89 100644 --- a/Source/WebCore/inspector/InspectorInstrumentationWebKit.h +++ b/Source/WebCore/inspector/InspectorInstrumentationWebKit.h @@ -33,6 +33,7 @@ @@ -3379,7 +3300,7 @@ index c028341e84e59a6b1b16107fd74feb21f70b12ab..d385418ac34e8f315f201801a2c65226 + } diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp -index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a93705e1a0 100644 +index ae1b0775177048d3464ae2961e1f86f044ab4521..a10bc643f3ddf82696100345037b820a764431b3 100644 --- a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp +++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp @@ -54,6 +54,7 @@ @@ -3405,7 +3326,7 @@ index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a9 #include "HTMLMediaElement.h" #include "HTMLNames.h" #include "HTMLScriptElement.h" -@@ -102,12 +108,14 @@ +@@ -103,12 +109,14 @@ #include "Pasteboard.h" #include "PseudoElement.h" #include "RenderGrid.h" @@ -3420,9 +3341,9 @@ index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a9 #include "StaticNodeList.h" #include "StyleProperties.h" #include "StyleResolver.h" -@@ -149,7 +157,8 @@ using namespace HTMLNames; +@@ -150,7 +158,8 @@ using namespace HTMLNames; static const size_t maxTextSize = 10000; - static const UChar horizontalEllipsisUChar[] = { horizontalEllipsis, 0 }; + static const char16_t horizontalEllipsisUChar[] = { horizontalEllipsis, 0 }; -static std::optional parseColor(RefPtr&& colorObject) +// static @@ -3430,7 +3351,7 @@ index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a9 { if (!colorObject) return std::nullopt; -@@ -168,7 +177,7 @@ static std::optional parseColor(RefPtr&& colorObject) +@@ -169,7 +178,7 @@ static std::optional parseColor(RefPtr&& colorObject) static std::optional parseRequiredConfigColor(const String& fieldName, JSON::Object& configObject) { @@ -3439,7 +3360,7 @@ index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a9 } static Color parseOptionalConfigColor(const String& fieldName, JSON::Object& configObject) -@@ -195,6 +204,20 @@ static bool parseQuad(Ref&& quadArray, FloatQuad* quad) +@@ -196,6 +205,20 @@ static bool parseQuad(Ref&& quadArray, FloatQuad* quad) return true; } @@ -3460,7 +3381,7 @@ index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a9 class RevalidateStyleAttributeTask final : public CanMakeCheckedPtr { WTF_MAKE_TZONE_ALLOCATED(RevalidateStyleAttributeTask); WTF_OVERRIDE_DELETE_FOR_CHECKED_PTR(RevalidateStyleAttributeTask); -@@ -475,6 +498,20 @@ Node* InspectorDOMAgent::assertNode(Inspector::Protocol::ErrorString& errorStrin +@@ -476,6 +499,20 @@ Node* InspectorDOMAgent::assertNode(Inspector::Protocol::ErrorString& errorStrin return node.get(); } @@ -3481,7 +3402,7 @@ index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a9 Document* InspectorDOMAgent::assertDocument(Inspector::Protocol::ErrorString& errorString, Inspector::Protocol::DOM::NodeId nodeId) { RefPtr node = assertNode(errorString, nodeId); -@@ -1593,16 +1630,7 @@ Inspector::Protocol::ErrorStringOr InspectorDOMAgent::highlightNode(std::o +@@ -1594,16 +1631,7 @@ Inspector::Protocol::ErrorStringOr InspectorDOMAgent::highlightNode(std::o Inspector::Protocol::ErrorStringOr InspectorDOMAgent::highlightNode(std::optional&& nodeId, const Inspector::Protocol::Runtime::RemoteObjectId& objectId, Ref&& highlightInspectorObject, RefPtr&& gridOverlayInspectorObject, RefPtr&& flexOverlayInspectorObject, std::optional&& showRulers) { Inspector::Protocol::ErrorString errorString; @@ -3499,7 +3420,7 @@ index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a9 if (!node) return makeUnexpected(errorString); -@@ -1857,15 +1885,159 @@ Inspector::Protocol::ErrorStringOr InspectorDOMAgent::setInspectedNode(Ins +@@ -1858,15 +1886,159 @@ Inspector::Protocol::ErrorStringOr InspectorDOMAgent::setInspectedNode(Ins return { }; } @@ -3662,7 +3583,7 @@ index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a9 if (!object) return makeUnexpected("Missing injected script for given nodeId"_s); -@@ -3131,7 +3303,7 @@ Inspector::Protocol::ErrorStringOr InspectorDO +@@ -3132,7 +3304,7 @@ Inspector::Protocol::ErrorStringOr InspectorDO return makeUnexpected("Missing node for given path"_s); } @@ -3671,7 +3592,7 @@ index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a9 { Document* document = &node->document(); if (auto* templateHost = document->templateDocumentHost()) -@@ -3140,12 +3312,18 @@ RefPtr InspectorDOMAgent::resolveNod +@@ -3141,12 +3313,18 @@ RefPtr InspectorDOMAgent::resolveNod if (!frame) return nullptr; @@ -3693,7 +3614,7 @@ index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a9 } Node* InspectorDOMAgent::scriptValueAsNode(JSC::JSValue value) -@@ -3291,4 +3469,53 @@ Inspector::Protocol::ErrorStringOr> In +@@ -3292,4 +3470,53 @@ Inspector::Protocol::ErrorStringOr> In #endif } @@ -3748,7 +3669,7 @@ index 08bdd6e6cd5295f4be4df37c3e3e830b03916dc7..078ef814e006a45c4b83ff1c2ff341a9 + } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.h b/Source/WebCore/inspector/agents/InspectorDOMAgent.h -index 185ba93a265317bf876cc5fedd5f3d5cf3a8757e..664fc4f47f0540e5b84d96ba4c6a530d44aacf25 100644 +index cd5edd3322a55e72643560ad852af14f7104f35b..25c3d3c73c3b9395323b4c853a70d5502c15ae52 100644 --- a/Source/WebCore/inspector/agents/InspectorDOMAgent.h +++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.h @@ -59,6 +59,7 @@ namespace WebCore { @@ -3821,7 +3742,7 @@ index 185ba93a265317bf876cc5fedd5f3d5cf3a8757e..664fc4f47f0540e5b84d96ba4c6a530d void discardBindings(); diff --git a/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp b/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp -index dfb469de2c5dcaa39b03ef6c188eda34d869401c..4d1fb3b02d63c8b425b50b92c25f334334e8ac86 100644 +index 49a732b0e976efae1ec6f687efb31efe0dca01de..a2f723b067cd4aff83e29e341ee90925dc48f281 100644 --- a/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp +++ b/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp @@ -58,6 +58,7 @@ @@ -3938,7 +3859,7 @@ index dfb469de2c5dcaa39b03ef6c188eda34d869401c..4d1fb3b02d63c8b425b50b92c25f3343 { return startsWithLettersIgnoringASCIICase(mimeType, "text/"_s) diff --git a/Source/WebCore/inspector/agents/InspectorNetworkAgent.h b/Source/WebCore/inspector/agents/InspectorNetworkAgent.h -index 66da07b880259dd1388703cb6c8c7b10d63c32d7..ff00bc16dd84f34056fe0b36896c0049d8fa9a49 100644 +index 7ae1b53be8a9b5f0d0e2787a22f5534500b7ab19..f6f33d05bcdb4084f55631fdcfd3b4d012dfc804 100644 --- a/Source/WebCore/inspector/agents/InspectorNetworkAgent.h +++ b/Source/WebCore/inspector/agents/InspectorNetworkAgent.h @@ -35,6 +35,8 @@ @@ -3975,10 +3896,10 @@ index 66da07b880259dd1388703cb6c8c7b10d63c32d7..ff00bc16dd84f34056fe0b36896c0049 } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp -index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e19a9056d 100644 +index 033d3cb9c8c8d605e97bb78f9b794cf4a8b861ba..8e9d36406513b09928be27bcc6e837bcf4e1a7ce 100644 --- a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp +++ b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp -@@ -32,19 +32,27 @@ +@@ -32,20 +32,28 @@ #include "config.h" #include "InspectorPageAgent.h" @@ -3995,6 +3916,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e #include "DocumentLoader.h" +#include "Editor.h" #include "ElementInlines.h" + #include "EventTargetInlines.h" +#include "FocusController.h" #include "ForcedAccessibilityValue.h" #include "FrameLoadRequest.h" @@ -4005,8 +3927,8 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e +#include "HTMLInputElement.h" #include "HTMLNames.h" #include "ImageBuffer.h" - #include "InspectorClient.h" -@@ -57,24 +65,38 @@ + #include "InspectorBackendClient.h" +@@ -58,24 +66,38 @@ #include "MIMETypeRegistry.h" #include "MemoryCache.h" #include "Page.h" @@ -4045,7 +3967,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e #include #if ENABLE(APPLICATION_MANIFEST) -@@ -96,6 +118,11 @@ using namespace Inspector; +@@ -97,6 +119,11 @@ using namespace Inspector; WTF_MAKE_TZONE_ALLOCATED_IMPL(InspectorPageAgent); @@ -4057,7 +3979,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e static bool decodeBuffer(std::span buffer, const String& textEncodingName, String* result) { if (buffer.data()) { -@@ -348,6 +375,7 @@ InspectorPageAgent::InspectorPageAgent(PageAgentContext& context, InspectorClien +@@ -350,6 +377,7 @@ InspectorPageAgent::InspectorPageAgent(PageAgentContext& context, InspectorBacke , m_frontendDispatcher(makeUniqueRef(context.frontendRouter)) , m_backendDispatcher(Inspector::PageBackendDispatcher::create(context.backendDispatcher, this)) , m_inspectedPage(context.inspectedPage) @@ -4065,7 +3987,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e , m_client(client) , m_overlay(overlay) { -@@ -377,12 +405,20 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::enable() +@@ -379,12 +407,20 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::enable() defaultUserPreferencesDidChange(); @@ -4086,7 +4008,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e setShowPaintRects(false); #if !PLATFORM(IOS_FAMILY) -@@ -435,6 +471,22 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::reload(std::optiona +@@ -437,6 +473,22 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::reload(std::optiona return { }; } @@ -4109,7 +4031,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e Inspector::Protocol::ErrorStringOr InspectorPageAgent::navigate(const String& url) { RefPtr localMainFrame = m_inspectedPage->localMainFrame(); -@@ -461,6 +513,13 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideUserAgent(c +@@ -463,6 +515,13 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideUserAgent(c return { }; } @@ -4123,7 +4045,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Inspector::Protocol::Page::Setting setting, std::optional&& value) { auto& inspectedPageSettings = m_inspectedPage->settings(); -@@ -474,6 +533,12 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins +@@ -476,6 +535,12 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins inspectedPageSettings.setAuthorAndUserStylesEnabledInspectorOverride(value); return { }; @@ -4136,8 +4058,8 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e case Inspector::Protocol::Page::Setting::ICECandidateFilteringEnabled: inspectedPageSettings.setICECandidateFilteringEnabledInspectorOverride(value); return { }; -@@ -500,6 +565,39 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins - m_client->setDeveloperPreferenceOverride(InspectorClient::DeveloperPreference::NeedsSiteSpecificQuirks, value); +@@ -502,6 +567,39 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins + m_client->setDeveloperPreferenceOverride(InspectorBackendClient::DeveloperPreference::NeedsSiteSpecificQuirks, value); return { }; +#if ENABLE(NOTIFICATIONS) @@ -4176,7 +4098,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e case Inspector::Protocol::Page::Setting::ScriptEnabled: inspectedPageSettings.setScriptEnabledInspectorOverride(value); return { }; -@@ -512,6 +610,12 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins +@@ -514,6 +612,12 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins inspectedPageSettings.setShowRepaintCounterInspectorOverride(value); return { }; @@ -4189,7 +4111,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e case Inspector::Protocol::Page::Setting::WebSecurityEnabled: inspectedPageSettings.setWebSecurityEnabledInspectorOverride(value); return { }; -@@ -920,15 +1024,16 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::setShowPaintRects(b +@@ -922,15 +1026,16 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::setShowPaintRects(b return { }; } @@ -4211,7 +4133,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e } void InspectorPageAgent::frameNavigated(LocalFrame& frame) -@@ -936,13 +1041,29 @@ void InspectorPageAgent::frameNavigated(LocalFrame& frame) +@@ -938,13 +1043,29 @@ void InspectorPageAgent::frameNavigated(LocalFrame& frame) m_frontendDispatcher->frameNavigated(buildObjectForFrame(&frame)); } @@ -4244,7 +4166,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e } Frame* InspectorPageAgent::frameForId(const Inspector::Protocol::Network::FrameId& frameId) -@@ -954,20 +1075,21 @@ String InspectorPageAgent::frameId(Frame* frame) +@@ -956,20 +1077,21 @@ String InspectorPageAgent::frameId(Frame* frame) { if (!frame) return emptyString(); @@ -4274,7 +4196,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e } LocalFrame* InspectorPageAgent::assertFrame(Inspector::Protocol::ErrorString& errorString, const Inspector::Protocol::Network::FrameId& frameId) -@@ -978,11 +1100,6 @@ LocalFrame* InspectorPageAgent::assertFrame(Inspector::Protocol::ErrorString& er +@@ -980,11 +1102,6 @@ LocalFrame* InspectorPageAgent::assertFrame(Inspector::Protocol::ErrorString& er return frame; } @@ -4286,7 +4208,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e void InspectorPageAgent::frameStartedLoading(LocalFrame& frame) { m_frontendDispatcher->frameStartedLoading(frameId(&frame)); -@@ -993,9 +1110,9 @@ void InspectorPageAgent::frameStoppedLoading(LocalFrame& frame) +@@ -995,9 +1112,9 @@ void InspectorPageAgent::frameStoppedLoading(LocalFrame& frame) m_frontendDispatcher->frameStoppedLoading(frameId(&frame)); } @@ -4298,7 +4220,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e } void InspectorPageAgent::frameClearedScheduledNavigation(Frame& frame) -@@ -1042,6 +1159,12 @@ void InspectorPageAgent::defaultUserPreferencesDidChange() +@@ -1044,6 +1161,12 @@ void InspectorPageAgent::defaultUserPreferencesDidChange() m_frontendDispatcher->defaultUserPreferencesDidChange(WTFMove(defaultUserPreferences)); } @@ -4311,7 +4233,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e #if ENABLE(DARK_MODE_CSS) void InspectorPageAgent::defaultAppearanceDidChange() { -@@ -1055,6 +1178,9 @@ void InspectorPageAgent::didClearWindowObjectInWorld(LocalFrame& frame, DOMWrapp +@@ -1057,6 +1180,9 @@ void InspectorPageAgent::didClearWindowObjectInWorld(LocalFrame& frame, DOMWrapp return; if (m_bootstrapScript.isEmpty()) @@ -4321,7 +4243,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e return; frame.script().evaluateIgnoringException(ScriptSourceCode(m_bootstrapScript, JSC::SourceTaintedOrigin::Untainted, URL { "web-inspector://bootstrap.js"_str })); -@@ -1102,6 +1228,51 @@ void InspectorPageAgent::didRecalculateStyle() +@@ -1104,6 +1230,51 @@ void InspectorPageAgent::didRecalculateStyle() protectedOverlay()->update(); } @@ -4373,7 +4295,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e Ref InspectorPageAgent::buildObjectForFrame(LocalFrame* frame) { ASSERT_ARG(frame, frame); -@@ -1195,6 +1366,12 @@ void InspectorPageAgent::applyUserAgentOverride(String& userAgent) +@@ -1197,6 +1368,12 @@ void InspectorPageAgent::applyUserAgentOverride(String& userAgent) userAgent = m_userAgentOverride; } @@ -4386,7 +4308,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e void InspectorPageAgent::applyEmulatedMedia(AtomString& media) { if (!m_emulatedMedia.isEmpty()) -@@ -1222,11 +1399,13 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotNode(Insp +@@ -1224,11 +1401,13 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotNode(Insp return snapshot->toDataURL("image/png"_s, std::nullopt, PreserveResolution::Yes); } @@ -4401,7 +4323,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e IntRect rectangle(x, y, width, height); RefPtr localMainFrame = m_inspectedPage->localMainFrame(); -@@ -1240,6 +1419,43 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotRect(int +@@ -1242,6 +1421,43 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotRect(int return snapshot->toDataURL("image/png"_s, std::nullopt, PreserveResolution::Yes); } @@ -4445,7 +4367,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e #if ENABLE(WEB_ARCHIVE) && USE(CF) Inspector::Protocol::ErrorStringOr InspectorPageAgent::archive() { -@@ -1256,7 +1472,6 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::archive() +@@ -1258,7 +1474,6 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::archive() } #endif @@ -4453,7 +4375,7 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e Inspector::Protocol::ErrorStringOr InspectorPageAgent::setScreenSizeOverride(std::optional&& width, std::optional&& height) { if (width.has_value() != height.has_value()) -@@ -1274,6 +1489,496 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::setScreenSizeOverri +@@ -1276,6 +1491,496 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::setScreenSizeOverri localMainFrame->setOverrideScreenSize(FloatSize(width.value_or(0), height.value_or(0))); return { }; } @@ -4952,21 +4874,10 @@ index 7ec43a15440452915980d0eca565f25b8c6880e9..868a6279fd57ddc3f880eeef18d0559e } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.h b/Source/WebCore/inspector/agents/InspectorPageAgent.h -index 169a3023f68227a6ab6538dc6b1271882154deb9..b300be1ff6f278a79601613f76266c06c5ac92a8 100644 +index d8a8b85ff220105e832a6d8075982f4722197afb..dc4a5caea97e3458bd9884e1265a92e72b727bc0 100644 --- a/Source/WebCore/inspector/agents/InspectorPageAgent.h +++ b/Source/WebCore/inspector/agents/InspectorPageAgent.h -@@ -32,8 +32,10 @@ - #pragma once - - #include "CachedResource.h" -+#include "FrameIdentifier.h" - #include "InspectorWebAgentBase.h" - #include "LayoutRect.h" -+#include "ProcessIdentifier.h" - #include - #include - #include -@@ -43,11 +45,16 @@ +@@ -43,11 +43,16 @@ #include #include @@ -4980,10 +4891,10 @@ index 169a3023f68227a6ab6538dc6b1271882154deb9..b300be1ff6f278a79601613f76266c06 class DocumentLoader; class Frame; +class HTMLInputElement; - class InspectorClient; + class InspectorBackendClient; class InspectorOverlay; class LocalFrame; -@@ -80,6 +87,8 @@ public: +@@ -80,6 +85,8 @@ public: OtherResource, }; @@ -4992,7 +4903,7 @@ index 169a3023f68227a6ab6538dc6b1271882154deb9..b300be1ff6f278a79601613f76266c06 static bool sharedBufferContent(RefPtr&&, const String& textEncodingName, bool withBase64Encode, String* result); static Vector cachedResourcesForFrame(LocalFrame*); static void resourceContent(Inspector::Protocol::ErrorString&, LocalFrame*, const URL&, String* result, bool* base64Encoded); -@@ -100,8 +109,11 @@ public: +@@ -100,8 +107,11 @@ public: Inspector::Protocol::ErrorStringOr enable(); Inspector::Protocol::ErrorStringOr disable(); Inspector::Protocol::ErrorStringOr reload(std::optional&& ignoreCache, std::optional&& revalidateAllResources); @@ -5004,7 +4915,7 @@ index 169a3023f68227a6ab6538dc6b1271882154deb9..b300be1ff6f278a79601613f76266c06 Inspector::Protocol::ErrorStringOr overrideSetting(Inspector::Protocol::Page::Setting, std::optional&& value); Inspector::Protocol::ErrorStringOr overrideUserPreference(Inspector::Protocol::Page::UserPreferenceName, std::optional&&); Inspector::Protocol::ErrorStringOr>> getCookies(); -@@ -117,45 +129,65 @@ public: +@@ -117,45 +127,65 @@ public: #endif Inspector::Protocol::ErrorStringOr setShowPaintRects(bool); Inspector::Protocol::ErrorStringOr setEmulatedMedia(const String&); @@ -5077,12 +4988,12 @@ index 169a3023f68227a6ab6538dc6b1271882154deb9..b300be1ff6f278a79601613f76266c06 Ref protectedOverlay() const; -@@ -173,17 +205,22 @@ private: +@@ -173,17 +203,22 @@ private: const Ref m_backendDispatcher; WeakRef m_inspectedPage; + Inspector::InjectedScriptManager& m_injectedScriptManager; - InspectorClient* m_client { nullptr }; + InspectorBackendClient* m_client { nullptr }; WeakRef m_overlay; - WeakHashMap m_frameToIdentifier; @@ -5268,7 +5179,7 @@ index 6544b61b15956b218e97814b0ceb7250809f8ed3..c6657031bcd27573533a34be62e0ea0d } // namespace WebCore diff --git a/Source/WebCore/loader/CookieJar.h b/Source/WebCore/loader/CookieJar.h -index 5dbca10d65d0426d90c743b789e18c3786025817..56b1aa222343649be7c28e104e6ae1dde598e59f 100644 +index ed8bec9c10c9d1bf659efab09f77ad7d8e439594..49bdaf8b08d157ecb7cd412ea847eb8ceedb8adf 100644 --- a/Source/WebCore/loader/CookieJar.h +++ b/Source/WebCore/loader/CookieJar.h @@ -48,6 +48,7 @@ class NetworkStorageSession; @@ -5290,10 +5201,10 @@ index 5dbca10d65d0426d90c743b789e18c3786025817..56b1aa222343649be7c28e104e6ae1dd protected: static SameSiteInfo sameSiteInfo(const Document&, IsForDOMCookieAccess = IsForDOMCookieAccess::No); diff --git a/Source/WebCore/loader/DocumentLoader.cpp b/Source/WebCore/loader/DocumentLoader.cpp -index c63569ec7dbb402d261f16a5b5febdf428cfe749..b5cff81dcf8fbcc5a0a8cb6f05857f4382b090be 100644 +index 861b82e0ae132215c5c319e94a3ca42028557990..4031b67577112eae8cf28099425cc40c42c81f6e 100644 --- a/Source/WebCore/loader/DocumentLoader.cpp +++ b/Source/WebCore/loader/DocumentLoader.cpp -@@ -775,8 +775,10 @@ void DocumentLoader::willSendRequest(ResourceRequest&& newRequest, const Resourc +@@ -776,8 +776,10 @@ void DocumentLoader::willSendRequest(ResourceRequest&& newRequest, const Resourc if (!didReceiveRedirectResponse) return completionHandler(WTFMove(newRequest)); @@ -5304,7 +5215,7 @@ index c63569ec7dbb402d261f16a5b5febdf428cfe749..b5cff81dcf8fbcc5a0a8cb6f05857f43 switch (navigationPolicyDecision) { case NavigationPolicyDecision::IgnoreLoad: case NavigationPolicyDecision::LoadWillContinueInAnotherProcess: -@@ -1592,11 +1594,17 @@ void DocumentLoader::detachFromFrame(LoadWillContinueInAnotherProcess loadWillCo +@@ -1601,11 +1603,17 @@ void DocumentLoader::detachFromFrame(LoadWillContinueInAnotherProcess loadWillCo if (auto navigationID = std::exchange(m_navigationID, { })) frame->loader().client().documentLoaderDetached(*navigationID, loadWillContinueInAnotherProcess); @@ -5325,7 +5236,7 @@ index c63569ec7dbb402d261f16a5b5febdf428cfe749..b5cff81dcf8fbcc5a0a8cb6f05857f43 { m_navigationID = navigationID; diff --git a/Source/WebCore/loader/DocumentLoader.h b/Source/WebCore/loader/DocumentLoader.h -index 061494ea45af99d0516cf16af3f11c25a180e970..eff67613ea81446d7f53ecf2e66d5b428b4c6bab 100644 +index 7f1ae6df69510c2dbdeedc1163b31bd968e6da01..3767fe35412c3c1cb6739dfd178aacae527b86a7 100644 --- a/Source/WebCore/loader/DocumentLoader.h +++ b/Source/WebCore/loader/DocumentLoader.h @@ -220,6 +220,8 @@ public: @@ -5338,10 +5249,10 @@ index 061494ea45af99d0516cf16af3f11c25a180e970..eff67613ea81446d7f53ecf2e66d5b42 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 1263d20c197bc21cb5a4098a07493943fd2830e4..fbcfaa000c4095c9ba9257dfb2229424229ee670 100644 +index 81336952a927d10a54a5282edef51d67024a3d91..6f042943462e1c69ccffd57cf49724d032f1ce65 100644 --- a/Source/WebCore/loader/FrameLoader.cpp +++ b/Source/WebCore/loader/FrameLoader.cpp -@@ -1332,6 +1332,7 @@ void FrameLoader::loadInSameDocument(URL url, RefPtr stat +@@ -1334,6 +1334,7 @@ void FrameLoader::loadInSameDocument(URL url, RefPtr stat } m_client->dispatchDidNavigateWithinPage(); @@ -5349,7 +5260,7 @@ index 1263d20c197bc21cb5a4098a07493943fd2830e4..fbcfaa000c4095c9ba9257dfb2229424 document->statePopped(stateObject ? stateObject.releaseNonNull() : SerializedScriptValue::nullValue()); m_client->dispatchDidPopStateWithinPage(); -@@ -1874,6 +1875,7 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t +@@ -1876,6 +1877,7 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t const String& httpMethod = loader->request().httpMethod(); if (shouldPerformFragmentNavigation(isFormSubmission, httpMethod, policyChecker().loadType(), newURL)) { @@ -5357,7 +5268,7 @@ index 1263d20c197bc21cb5a4098a07493943fd2830e4..fbcfaa000c4095c9ba9257dfb2229424 RefPtr oldDocumentLoader = m_documentLoader; NavigationAction action { frame->protectedDocument().releaseNonNull(), loader->request(), InitiatedByMainFrame::Unknown, loader->isRequestFromClientOrUserInput(), policyChecker().loadType(), isFormSubmission }; -@@ -1912,7 +1914,9 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t +@@ -1914,7 +1916,9 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t auto policyDecisionMode = loader->triggeringAction().isFromNavigationAPI() ? PolicyDecisionMode::Synchronous : PolicyDecisionMode::Asynchronous; RELEASE_ASSERT(!isBackForwardLoadType(policyChecker().loadType()) || history().provisionalItem()); @@ -5367,7 +5278,7 @@ index 1263d20c197bc21cb5a4098a07493943fd2830e4..fbcfaa000c4095c9ba9257dfb2229424 continueLoadAfterNavigationPolicy(request, RefPtr { weakFormState.get() }.get(), navigationPolicyDecision, allowNavigationToInvalidURL); completionHandler(); }, policyDecisionMode); -@@ -3238,10 +3242,15 @@ String FrameLoader::userAgent(const URL& url) const +@@ -3250,10 +3254,15 @@ String FrameLoader::userAgent(const URL& url) const String FrameLoader::navigatorPlatform() const { @@ -5385,7 +5296,7 @@ index 1263d20c197bc21cb5a4098a07493943fd2830e4..fbcfaa000c4095c9ba9257dfb2229424 } void FrameLoader::dispatchOnloadEvents() -@@ -3702,6 +3711,8 @@ void FrameLoader::receivedMainResourceError(const ResourceError& error, LoadWill +@@ -3715,6 +3724,8 @@ void FrameLoader::receivedMainResourceError(const ResourceError& error, LoadWill checkCompleted(); if (frame->page()) checkLoadComplete(loadWillContinueInAnotherProcess); @@ -5394,7 +5305,7 @@ index 1263d20c197bc21cb5a4098a07493943fd2830e4..fbcfaa000c4095c9ba9257dfb2229424 } void FrameLoader::continueFragmentScrollAfterNavigationPolicy(const ResourceRequest& request, const SecurityOrigin* requesterOrigin, bool shouldContinue, NavigationHistoryBehavior historyHandling) -@@ -4591,9 +4602,6 @@ String FrameLoader::referrer() const +@@ -4672,9 +4683,6 @@ String FrameLoader::referrer() const void FrameLoader::dispatchDidClearWindowObjectsInAllWorlds() { @@ -5404,7 +5315,7 @@ index 1263d20c197bc21cb5a4098a07493943fd2830e4..fbcfaa000c4095c9ba9257dfb2229424 Vector> worlds; ScriptController::getAllWorlds(worlds); for (auto& world : worlds) -@@ -4603,13 +4611,12 @@ void FrameLoader::dispatchDidClearWindowObjectsInAllWorlds() +@@ -4684,13 +4692,12 @@ void FrameLoader::dispatchDidClearWindowObjectsInAllWorlds() void FrameLoader::dispatchDidClearWindowObjectInWorld(DOMWrapperWorld& world) { Ref frame = m_frame.get(); @@ -5425,7 +5336,7 @@ index 1263d20c197bc21cb5a4098a07493943fd2830e4..fbcfaa000c4095c9ba9257dfb2229424 InspectorInstrumentation::didClearWindowObjectInWorld(frame, world); } diff --git a/Source/WebCore/loader/LoaderStrategy.h b/Source/WebCore/loader/LoaderStrategy.h -index a33d00c3ce3a861f8746e58b01ea51e5777cb2ea..9cf20e3c47c7a0bb9db72b9385431cc3df48df86 100644 +index 31d9bf99b45e972aab694f3f5d6f48ab73f8a85d..9ff42c5fc8ca162128fa853e0c36b87cd29301c2 100644 --- a/Source/WebCore/loader/LoaderStrategy.h +++ b/Source/WebCore/loader/LoaderStrategy.h @@ -86,6 +86,7 @@ public: @@ -5437,10 +5348,10 @@ index a33d00c3ce3a861f8746e58b01ea51e5777cb2ea..9cf20e3c47c7a0bb9db72b9385431cc3 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 350f729ee5dc95ba3dd7018542db1dbfb00b382e..f8d31dfc86c0cbc10b474e1773ddbf682b3c9b66 100644 +index 181ebdf4bc5665aa808eebc29c005533a7bf06fe..4fb3f9fb2703189f96ca6863f57b0517f373378a 100644 --- a/Source/WebCore/loader/NavigationScheduler.cpp +++ b/Source/WebCore/loader/NavigationScheduler.cpp -@@ -803,7 +803,7 @@ void NavigationScheduler::startTimer() +@@ -828,7 +828,7 @@ void NavigationScheduler::startTimer() Seconds delay = 1_s * m_redirect->delay(); m_timer.startOneShot(delay); @@ -5450,7 +5361,7 @@ index 350f729ee5dc95ba3dd7018542db1dbfb00b382e..f8d31dfc86c0cbc10b474e1773ddbf68 } diff --git a/Source/WebCore/loader/ProgressTracker.cpp b/Source/WebCore/loader/ProgressTracker.cpp -index 1832114445abe51041c241f40df9731d6e0a2f8d..3213517c4cd0b235ff12c098e34389987e6ea090 100644 +index 44b78d002e37a4850da17b2a265664b9aeaaacfe..011b696dbcd01673623384e6e4dc62e439e9c1dc 100644 --- a/Source/WebCore/loader/ProgressTracker.cpp +++ b/Source/WebCore/loader/ProgressTracker.cpp @@ -163,6 +163,8 @@ void ProgressTracker::progressCompleted(LocalFrame& frame) @@ -5472,10 +5383,10 @@ index 1832114445abe51041c241f40df9731d6e0a2f8d..3213517c4cd0b235ff12c098e3438998 } diff --git a/Source/WebCore/loader/cache/CachedResourceLoader.cpp b/Source/WebCore/loader/cache/CachedResourceLoader.cpp -index b78b91913be6fd14efc7621e251bb5adab05c315..b7f3252e4f201c2c44bca48997f20a3205301486 100644 +index 32762aa1a9fc99396eef869712e246f1f6f8b2b9..4421250371bbd354efea32ba3339daca5830da45 100644 --- a/Source/WebCore/loader/cache/CachedResourceLoader.cpp +++ b/Source/WebCore/loader/cache/CachedResourceLoader.cpp -@@ -1180,8 +1180,11 @@ ResourceErrorOr> CachedResourceLoader::requ +@@ -1121,8 +1121,11 @@ ResourceErrorOr> CachedResourceLoader::requ request.updateReferrerPolicy(document ? document->referrerPolicy() : ReferrerPolicy::Default); @@ -5489,7 +5400,7 @@ index b78b91913be6fd14efc7621e251bb5adab05c315..b7f3252e4f201c2c44bca48997f20a32 if (RefPtr documentLoader = m_documentLoader.get()) { bool madeHTTPS { request.resourceRequest().wasSchemeOptimisticallyUpgraded() }; -@@ -1816,8 +1819,9 @@ Vector> CachedResourceLoader::allCachedSVGImages() const +@@ -1757,8 +1760,9 @@ Vector> CachedResourceLoader::allCachedSVGImages() const ResourceErrorOr> CachedResourceLoader::preload(CachedResource::Type type, CachedResourceRequest&& request) { @@ -5502,10 +5413,10 @@ index b78b91913be6fd14efc7621e251bb5adab05c315..b7f3252e4f201c2c44bca48997f20a32 RefPtr document = m_document.get(); ASSERT(document); diff --git a/Source/WebCore/page/ChromeClient.h b/Source/WebCore/page/ChromeClient.h -index 7f6d70366b7841b4e5215a3b8709554fe995ba64..342de0c124714355bd5beaea78310500a9c184b7 100644 +index 21c2bbf6c049ef1af19cd78f769af84608914761..afd373b6c553fce8020f1e3832615e4e4347c82e 100644 --- a/Source/WebCore/page/ChromeClient.h +++ b/Source/WebCore/page/ChromeClient.h -@@ -379,7 +379,7 @@ public: +@@ -385,7 +385,7 @@ public: #endif #if ENABLE(ORIENTATION_EVENTS) @@ -5515,10 +5426,10 @@ index 7f6d70366b7841b4e5215a3b8709554fe995ba64..342de0c124714355bd5beaea78310500 virtual RefPtr createColorChooser(ColorChooserClient&, const Color&) = 0; diff --git a/Source/WebCore/page/EventHandler.cpp b/Source/WebCore/page/EventHandler.cpp -index d501516df133e721bc2dcf580b4dd1533b3368f3..63fac9ac1a69d522ed733dae33945ca6b685e4e4 100644 +index f936eb54cafd3a196082b3cc5df4a0541bad75ec..06c159e98ce09f261293b47b4136c212f9656db5 100644 --- a/Source/WebCore/page/EventHandler.cpp +++ b/Source/WebCore/page/EventHandler.cpp -@@ -4515,6 +4515,12 @@ bool EventHandler::handleDrag(const MouseEventWithHitTestResults& event, CheckDr +@@ -4538,6 +4538,12 @@ bool EventHandler::handleDrag(const MouseEventWithHitTestResults& event, CheckDr if (!document) return false; @@ -5531,7 +5442,7 @@ index d501516df133e721bc2dcf580b4dd1533b3368f3..63fac9ac1a69d522ed733dae33945ca6 dragState().dataTransfer = DataTransfer::createForDrag(*document); auto hasNonDefaultPasteboardData = HasNonDefaultPasteboardData::No; -@@ -5119,6 +5125,7 @@ static HitTestResult hitTestResultInFrame(LocalFrame* frame, const LayoutPoint& +@@ -5142,6 +5148,7 @@ static HitTestResult hitTestResultInFrame(LocalFrame* frame, const LayoutPoint& return result; } @@ -5539,7 +5450,7 @@ index d501516df133e721bc2dcf580b4dd1533b3368f3..63fac9ac1a69d522ed733dae33945ca6 Expected EventHandler::handleTouchEvent(const PlatformTouchEvent& event) { Ref frame = m_frame.get(); -@@ -5192,7 +5199,7 @@ Expected EventHandler::handleTouchEvent(co +@@ -5215,7 +5222,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; @@ -5548,7 +5459,7 @@ index d501516df133e721bc2dcf580b4dd1533b3368f3..63fac9ac1a69d522ed733dae33945ca6 bool pointerCancelled = false; #endif RefPtr touchTarget; -@@ -5239,7 +5246,7 @@ Expected EventHandler::handleTouchEvent(co +@@ -5262,7 +5269,7 @@ Expected EventHandler::handleTouchEvent(co // we also remove it from the map. touchTarget = m_originatingTouchPointTargets.take(touchPointTargetKey); @@ -5557,7 +5468,7 @@ index d501516df133e721bc2dcf580b4dd1533b3368f3..63fac9ac1a69d522ed733dae33945ca6 HitTestResult result = hitTestResultAtPoint(pagePoint, hitType | HitTestRequest::Type::AllowChildFrameContent); pointerTarget = result.targetElement(); pointerCancelled = (pointerTarget != touchTarget); -@@ -5262,7 +5269,7 @@ Expected EventHandler::handleTouchEvent(co +@@ -5285,7 +5292,7 @@ Expected EventHandler::handleTouchEvent(co if (!targetFrame) continue; @@ -5566,7 +5477,7 @@ index d501516df133e721bc2dcf580b4dd1533b3368f3..63fac9ac1a69d522ed733dae33945ca6 // 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) { -@@ -5352,6 +5359,7 @@ Expected EventHandler::handleTouchEvent(co +@@ -5372,6 +5379,7 @@ Expected EventHandler::handleTouchEvent(co return swallowedEvent; } @@ -5575,10 +5486,10 @@ index d501516df133e721bc2dcf580b4dd1533b3368f3..63fac9ac1a69d522ed733dae33945ca6 #if ENABLE(TOUCH_EVENTS) diff --git a/Source/WebCore/page/FocusController.cpp b/Source/WebCore/page/FocusController.cpp -index 79636a0a8528bfd4e86d8373f9ab7e96e9ac8346..f2a2ebeb6a7e41a3900d35dabde88f9fd7c4ae82 100644 +index e6db35ac9107cd946660165d18fb9598b1a51fa7..e7befc07490d52f518aab1a212edde1b8f1f621d 100644 --- a/Source/WebCore/page/FocusController.cpp +++ b/Source/WebCore/page/FocusController.cpp -@@ -595,13 +595,14 @@ bool FocusController::relinquishFocusToChrome(FocusDirection direction) +@@ -613,13 +613,14 @@ bool FocusController::relinquishFocusToChrome(FocusDirection direction) return false; Ref page = m_page.get(); @@ -5596,10 +5507,10 @@ index 79636a0a8528bfd4e86d8373f9ab7e96e9ac8346..f2a2ebeb6a7e41a3900d35dabde88f9f } diff --git a/Source/WebCore/page/FrameSnapshotting.cpp b/Source/WebCore/page/FrameSnapshotting.cpp -index 3474800864049dcbe6c84746c4216e72a68fa48f..8f5d536921eeb41c77fe689c036dcb77e625e431 100644 +index c0f0a17079be7a5574c40fdbbdeeedd3a45ef078..b6480851049f40e9f64926dda0407bb4d3be97e7 100644 --- a/Source/WebCore/page/FrameSnapshotting.cpp +++ b/Source/WebCore/page/FrameSnapshotting.cpp -@@ -115,7 +115,7 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& +@@ -120,7 +120,7 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& // Other paint behaviors are set by paintContentsForSnapshot. frame.view()->setPaintBehavior(paintBehavior); @@ -5608,7 +5519,7 @@ index 3474800864049dcbe6c84746c4216e72a68fa48f..8f5d536921eeb41c77fe689c036dcb77 if (options.flags.contains(SnapshotFlags::PaintWith3xBaseScale)) scaleFactor = 3; -@@ -134,6 +134,8 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& +@@ -139,6 +139,8 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& return nullptr; buffer->context().translate(-imageRect.location()); @@ -5617,7 +5528,7 @@ index 3474800864049dcbe6c84746c4216e72a68fa48f..8f5d536921eeb41c77fe689c036dcb77 if (!clipRects.isEmpty()) { Path clipPath; -@@ -142,7 +144,10 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& +@@ -147,7 +149,10 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& buffer->context().clipPath(clipPath); } @@ -5630,19 +5541,19 @@ index 3474800864049dcbe6c84746c4216e72a68fa48f..8f5d536921eeb41c77fe689c036dcb77 } diff --git a/Source/WebCore/page/FrameSnapshotting.h b/Source/WebCore/page/FrameSnapshotting.h -index fd9df9d9564fe29c64342fbf77082ad595612e90..af5687c79e2a5be20cde653107e5827c1d8981e5 100644 +index 77611af092f6afa507b1d42a98c3c1bef7736e0f..e0f94ad9b728bc02b08f15a43dcabd65099aec7e 100644 --- a/Source/WebCore/page/FrameSnapshotting.h +++ b/Source/WebCore/page/FrameSnapshotting.h -@@ -58,6 +58,7 @@ enum class SnapshotFlags : uint16_t { - PaintWith3xBaseScale = 1 << 10, +@@ -59,6 +59,7 @@ enum class SnapshotFlags : uint16_t { ExcludeText = 1 << 11, FixedAndStickyLayersOnly = 1 << 12, -+ OmitDeviceScaleFactor = 1 << 13, + DraggableElement = 1 << 13, ++ OmitDeviceScaleFactor = 1 << 14, }; struct SnapshotOptions { diff --git a/Source/WebCore/page/History.cpp b/Source/WebCore/page/History.cpp -index f2dfb5966f445ee52b4430e65e0accb7e41bb913..1d25db7e91beaaea3c083f315ec3f21b317f271f 100644 +index 3513f1c7cc2763d0c15a4fc85a24e341f30dcc6a..0c6c68d1189743d6104a38b47295c9af3a691049 100644 --- a/Source/WebCore/page/History.cpp +++ b/Source/WebCore/page/History.cpp @@ -32,6 +32,7 @@ @@ -5663,7 +5574,7 @@ index f2dfb5966f445ee52b4430e65e0accb7e41bb913..1d25db7e91beaaea3c083f315ec3f21b } diff --git a/Source/WebCore/page/LocalFrame.cpp b/Source/WebCore/page/LocalFrame.cpp -index 018a1445506e0b3cae8b7d4056f37bcdd21eb1dd..a1cd251e84d2faa8b58b5ebbf5077bacb1e5bf6e 100644 +index 4fa15f4aaf094cd582d17258955ec9dd7a6e4b4d..7843f89d325eabba5e68f0a2ffd9a346b7b0561e 100644 --- a/Source/WebCore/page/LocalFrame.cpp +++ b/Source/WebCore/page/LocalFrame.cpp @@ -42,6 +42,7 @@ @@ -6073,15 +5984,15 @@ index 018a1445506e0b3cae8b7d4056f37bcdd21eb1dd..a1cd251e84d2faa8b58b5ebbf5077bac #undef FRAME_RELEASE_LOG_ERROR diff --git a/Source/WebCore/page/LocalFrame.h b/Source/WebCore/page/LocalFrame.h -index 92a21d8efb29d588a7a530e823079ad299cbd40a..3a925cc017500b37abee9d2d26662f9eccc2eebc 100644 +index d70f9309f700129769d61d7fa697c6e49f55bb44..2647e60e824395d50cd0da90a32f9ead0bfe4892 100644 --- a/Source/WebCore/page/LocalFrame.h +++ b/Source/WebCore/page/LocalFrame.h @@ -29,6 +29,7 @@ - #include "DOMPasteAccess.h" - #include "Frame.h" -+#include "IntDegrees.h" - #include "ScrollbarMode.h" + #include + #include ++#include + #include #include #include @@ -122,8 +123,8 @@ enum { @@ -6141,10 +6052,10 @@ index 92a21d8efb29d588a7a530e823079ad299cbd40a..3a925cc017500b37abee9d2d26662f9e const UniqueRef m_viewportArguments; diff --git a/Source/WebCore/page/Page.cpp b/Source/WebCore/page/Page.cpp -index 6b60173931046a6361e7f0d388c6663166236dfe..fe3175f67a82a3328e1a3c1bc74521daa792dedc 100644 +index 83b514b6eb976273023e990d1d47d13ab7fc7119..a893465443a91d6b51ffdfeccba050e5834f8546 100644 --- a/Source/WebCore/page/Page.cpp +++ b/Source/WebCore/page/Page.cpp -@@ -667,6 +667,45 @@ void Page::setOverrideViewportArguments(const std::optional& +@@ -673,6 +673,45 @@ void Page::setOverrideViewportArguments(const std::optional& localTopDocument->updateViewportArguments(); } @@ -6190,7 +6101,7 @@ index 6b60173931046a6361e7f0d388c6663166236dfe..fe3175f67a82a3328e1a3c1bc74521da ScrollingCoordinator* Page::scrollingCoordinator() { if (!m_scrollingCoordinator && m_settings->scrollingCoordinatorEnabled()) { -@@ -4292,6 +4331,26 @@ void Page::setUseDarkAppearanceOverride(std::optional valueOverride) +@@ -4337,6 +4376,26 @@ void Page::setUseDarkAppearanceOverride(std::optional valueOverride) appearanceDidChange(); } @@ -6218,10 +6129,10 @@ index 6b60173931046a6361e7f0d388c6663166236dfe..fe3175f67a82a3328e1a3c1bc74521da { if (insets == m_fullscreenInsets) diff --git a/Source/WebCore/page/Page.h b/Source/WebCore/page/Page.h -index cd29ee00f3499c50379f02981fe85e7bdd950d93..10b653145543c308afd3ed1497f622d7adf08e9d 100644 +index cceec7c43b52e8eb1ee7636c8696d25900242fce..ff3981aa969c09def9499dc83b9794e05cd5086a 100644 --- a/Source/WebCore/page/Page.h +++ b/Source/WebCore/page/Page.h -@@ -391,6 +391,9 @@ public: +@@ -396,6 +396,9 @@ public: const ViewportArguments* overrideViewportArguments() const { return m_overrideViewportArguments.get(); } WEBCORE_EXPORT void setOverrideViewportArguments(const std::optional&); @@ -6231,7 +6142,7 @@ index cd29ee00f3499c50379f02981fe85e7bdd950d93..10b653145543c308afd3ed1497f622d7 static void refreshPlugins(bool reload); WEBCORE_EXPORT PluginData& pluginData(); WEBCORE_EXPORT Ref protectedPluginData(); -@@ -488,6 +491,10 @@ public: +@@ -493,6 +496,10 @@ public: #if ENABLE(DRAG_SUPPORT) DragController& dragController() { return m_dragController.get(); } const DragController& dragController() const { return m_dragController.get(); } @@ -6242,7 +6153,7 @@ index cd29ee00f3499c50379f02981fe85e7bdd950d93..10b653145543c308afd3ed1497f622d7 #endif FocusController& focusController() const { return m_focusController; } #if ENABLE(CONTEXT_MENUS) -@@ -671,6 +678,10 @@ public: +@@ -676,6 +683,10 @@ public: WEBCORE_EXPORT void setUseColorAppearance(bool useDarkAppearance, bool useElevatedUserInterfaceLevel); bool defaultUseDarkAppearance() const { return m_useDarkAppearance; } void setUseDarkAppearanceOverride(std::optional); @@ -6253,7 +6164,7 @@ index cd29ee00f3499c50379f02981fe85e7bdd950d93..10b653145543c308afd3ed1497f622d7 #if ENABLE(TEXT_AUTOSIZING) float textAutosizingWidth() const { return m_textAutosizingWidth; } -@@ -1148,6 +1159,11 @@ public: +@@ -1153,6 +1164,11 @@ public: WEBCORE_EXPORT void setInteractionRegionsEnabled(bool); #endif @@ -6265,7 +6176,7 @@ index cd29ee00f3499c50379f02981fe85e7bdd950d93..10b653145543c308afd3ed1497f622d7 #if ENABLE(DEVICE_ORIENTATION) && PLATFORM(IOS_FAMILY) DeviceOrientationUpdateProvider* deviceOrientationUpdateProvider() const { return m_deviceOrientationUpdateProvider.get(); } #endif -@@ -1437,6 +1453,9 @@ private: +@@ -1454,6 +1470,9 @@ private: #if ENABLE(DRAG_SUPPORT) const UniqueRef m_dragController; @@ -6275,7 +6186,7 @@ index cd29ee00f3499c50379f02981fe85e7bdd950d93..10b653145543c308afd3ed1497f622d7 #endif const UniqueRef m_focusController; #if ENABLE(CONTEXT_MENUS) -@@ -1515,6 +1534,8 @@ private: +@@ -1532,6 +1551,8 @@ private: bool m_useElevatedUserInterfaceLevel { false }; bool m_useDarkAppearance { false }; std::optional m_useDarkAppearanceOverride; @@ -6284,7 +6195,7 @@ index cd29ee00f3499c50379f02981fe85e7bdd950d93..10b653145543c308afd3ed1497f622d7 #if ENABLE(TEXT_AUTOSIZING) float m_textAutosizingWidth { 0 }; -@@ -1696,6 +1717,11 @@ private: +@@ -1713,6 +1734,11 @@ private: #endif std::unique_ptr m_overrideViewportArguments; @@ -6297,7 +6208,7 @@ index cd29ee00f3499c50379f02981fe85e7bdd950d93..10b653145543c308afd3ed1497f622d7 #if ENABLE(DEVICE_ORIENTATION) && PLATFORM(IOS_FAMILY) RefPtr m_deviceOrientationUpdateProvider; diff --git a/Source/WebCore/page/PageConsoleClient.cpp b/Source/WebCore/page/PageConsoleClient.cpp -index e3f5517e30956f17aea53324fc3c5ce11238254c..278ddea00b98d979797e1e82a5cb3f97239b63bc 100644 +index ae9e4dc7c8573235ad05777d0d4e7d1a0d3a9f94..d5fb47d14fea90b4fb87ffeb65148bca3d9257cf 100644 --- a/Source/WebCore/page/PageConsoleClient.cpp +++ b/Source/WebCore/page/PageConsoleClient.cpp @@ -456,4 +456,9 @@ Ref PageConsoleClient::protectedPage() const @@ -6323,7 +6234,7 @@ index 153fc36199f26adbfb61cbef6744ffe31a68b951..cc667e06700013fd5e994467e19536d2 Ref protectedPage() const; diff --git a/Source/WebCore/page/PointerCaptureController.cpp b/Source/WebCore/page/PointerCaptureController.cpp -index f01c803f6afa4e2949378bc9a758af4a55681dc5..9d35c28b4ff837f4333ac50bc917513bbf2b5773 100644 +index a4644c7a7a7f5bd55701972c4a8e716b82bde08f..5a0b18b5c842db0cfe63fd42ec402c27782ac5a0 100644 --- a/Source/WebCore/page/PointerCaptureController.cpp +++ b/Source/WebCore/page/PointerCaptureController.cpp @@ -207,7 +207,7 @@ bool PointerCaptureController::preventsCompatibilityMouseEventsForIdentifier(Poi @@ -6345,7 +6256,7 @@ index f01c803f6afa4e2949378bc9a758af4a55681dc5..9d35c28b4ff837f4333ac50bc917513b #endif diff --git a/Source/WebCore/page/PointerCaptureController.h b/Source/WebCore/page/PointerCaptureController.h -index 2c3ae18820bd2be191dbd8d6bd513c0302a1f580..21538527135e659493ee812aa61c943ae37c0b74 100644 +index 5dae225aa25a0421b7e52c9f280795fd6d703113..6482208d93440ec2d5ef8654bc4125745df87464 100644 --- a/Source/WebCore/page/PointerCaptureController.h +++ b/Source/WebCore/page/PointerCaptureController.h @@ -60,7 +60,7 @@ public: @@ -6354,7 +6265,7 @@ index 2c3ae18820bd2be191dbd8d6bd513c0302a1f580..21538527135e659493ee812aa61c943a -#if ENABLE(TOUCH_EVENTS) && (PLATFORM(IOS_FAMILY) || PLATFORM(WPE)) +#if ENABLE(TOUCH_EVENTS) - void dispatchEventForTouchAtIndex(EventTarget&, const PlatformTouchEvent&, unsigned, bool isPrimary, WindowProxy&, const IntPoint&); + void dispatchEventForTouchAtIndex(EventTarget&, const PlatformTouchEvent&, unsigned, bool isPrimary, WindowProxy&, const DoublePoint&); #endif @@ -81,12 +81,12 @@ private: @@ -6417,10 +6328,10 @@ index c45965ee6b7fe6c066871b87d7883e639a849adb..16c3a5b027222d1de5bb540c71c0b6aa } diff --git a/Source/WebCore/page/csp/ContentSecurityPolicy.cpp b/Source/WebCore/page/csp/ContentSecurityPolicy.cpp -index 5ba13e25cbe12ea5c7e543a3e9377f662cbfe50e..b5ab59f41db851cb03a56c9dfaf76e47182f8e85 100644 +index 0b7b89981e028a0eb80b279470e7f9bc8161a419..a3b806975380f8eff991b54be481d0173bd76b58 100644 --- a/Source/WebCore/page/csp/ContentSecurityPolicy.cpp +++ b/Source/WebCore/page/csp/ContentSecurityPolicy.cpp -@@ -374,6 +374,8 @@ template +@@ -352,6 +352,8 @@ template bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposition, Predicate&& predicate, Args&&... args) const requires (!std::is_convertible_v) { @@ -6429,7 +6340,7 @@ index 5ba13e25cbe12ea5c7e543a3e9377f662cbfe50e..b5ab59f41db851cb03a56c9dfaf76e47 bool isReportOnly = disposition == ContentSecurityPolicy::Disposition::ReportOnly; for (auto& policy : m_policies) { if (policy->isReportOnly() != isReportOnly) -@@ -387,6 +389,8 @@ bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposit +@@ -365,6 +367,8 @@ bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposit template bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposition, ViolatedDirectiveCallback&& callback, Predicate&& predicate, Args&&... args) const { @@ -6438,7 +6349,7 @@ index 5ba13e25cbe12ea5c7e543a3e9377f662cbfe50e..b5ab59f41db851cb03a56c9dfaf76e47 bool isReportOnly = disposition == ContentSecurityPolicy::Disposition::ReportOnly; bool isAllowed = true; for (auto& policy : m_policies) { -@@ -403,6 +407,8 @@ bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposit +@@ -381,6 +385,8 @@ bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposit template bool ContentSecurityPolicy::allPoliciesAllow(NOESCAPE const ViolatedDirectiveCallback& callback, Predicate&& predicate, Args&&... args) const { @@ -6449,10 +6360,10 @@ index 5ba13e25cbe12ea5c7e543a3e9377f662cbfe50e..b5ab59f41db851cb03a56c9dfaf76e47 if (const ContentSecurityPolicyDirective* violatedDirective = (policy.get()->*predicate)(std::forward(args)...)) { diff --git a/Source/WebCore/page/wpe/DragControllerWPE.cpp b/Source/WebCore/page/wpe/DragControllerWPE.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..803239911006cfb3b03ea911c003f2d233f67600 +index 0000000000000000000000000000000000000000..7bea08826a16de4774a55c81ccba0d81f7d72472 --- /dev/null +++ b/Source/WebCore/page/wpe/DragControllerWPE.cpp -@@ -0,0 +1,80 @@ +@@ -0,0 +1,82 @@ +/* + * Copyright (C) 2007-20 Apple Inc. All rights reserved. + * @@ -6488,6 +6399,8 @@ index 0000000000000000000000000000000000000000..803239911006cfb3b03ea911c003f2d2 +#include "Element.h" +#include "Frame.h" +#include "FrameDestructionObserverInlines.h" ++#include "LocalFrameInlines.h" ++#include "NodeInlines.h" +#include "Pasteboard.h" +#include "markup.h" + @@ -6534,18 +6447,9 @@ index 0000000000000000000000000000000000000000..803239911006cfb3b03ea911c003f2d2 + +} diff --git a/Source/WebCore/platform/DragData.h b/Source/WebCore/platform/DragData.h -index f76eab27bd22db72e23cd53d98fe721f0d7e48b7..9757103efe88c0fed3b68671c3042faa29b31433 100644 +index 31e1e62bf7726eb5914be18671ef9ebab401fff2..56243558488229b44ca7a18f444945b3d60ebeff 100644 --- a/Source/WebCore/platform/DragData.h +++ b/Source/WebCore/platform/DragData.h -@@ -47,7 +47,7 @@ typedef void* DragDataRef; - - #elif PLATFORM(WIN) - typedef struct IDataObject* DragDataRef; --#elif PLATFORM(GTK) -+#elif PLATFORM(GTK) || PLATFORM(WPE) - namespace WebCore { - class SelectionData; - } @@ -92,8 +92,8 @@ public: // is initialized by the decoder and not in the constructor. DragData() = default; @@ -6567,44 +6471,10 @@ index f76eab27bd22db72e23cd53d98fe721f0d7e48b7..9757103efe88c0fed3b68671c3042faa bool m_disallowFileAccess { false }; }; diff --git a/Source/WebCore/platform/Pasteboard.h b/Source/WebCore/platform/Pasteboard.h -index f8de8815d483bd3ac684c018159c593798c0495a..e84ce8eec7424fbc3456623289588b1832d9fb7f 100644 +index 42fd947c994a56dc28d3813dae9461f5a6a99896..fd7ac7ace714da74fc7b48770d738dc839b39d32 100644 --- a/Source/WebCore/platform/Pasteboard.h +++ b/Source/WebCore/platform/Pasteboard.h -@@ -46,7 +46,7 @@ OBJC_CLASS NSString; - OBJC_CLASS NSArray; - #endif - --#if PLATFORM(GTK) -+#if PLATFORM(GTK) || PLATFORM(WPE) - #include "SelectionData.h" - #endif - -@@ -200,6 +200,11 @@ public: - explicit Pasteboard(std::unique_ptr&&, const String& name); - #endif - -+#if PLATFORM(WPE) && ENABLE(DRAG_SUPPORT) -+ explicit Pasteboard(std::unique_ptr&&, SelectionData&); -+ explicit Pasteboard(std::unique_ptr&&, SelectionData&&); -+#endif -+ - #if PLATFORM(WIN) - explicit Pasteboard(std::unique_ptr&&, IDataObject*); - explicit Pasteboard(std::unique_ptr&&, WCDataObject*); -@@ -266,6 +271,12 @@ public: - static std::unique_ptr createForGlobalSelection(std::unique_ptr&&); - #endif - -+#if PLATFORM(WPE) -+ const SelectionData& selectionData() const { -+ return *m_selectionData; -+ } -+#endif -+ - #if PLATFORM(IOS_FAMILY) - explicit Pasteboard(std::unique_ptr&&, int64_t changeCount); - explicit Pasteboard(std::unique_ptr&&, const String& pasteboardName); -@@ -311,6 +322,7 @@ public: +@@ -311,6 +311,7 @@ public: COMPtr dataObject() const { return m_dataObject; } WEBCORE_EXPORT void setExternalDataObject(IDataObject*); const DragDataMap& dragDataMap() const { return m_dragDataMap; } @@ -6612,18 +6482,7 @@ index f8de8815d483bd3ac684c018159c593798c0495a..e84ce8eec7424fbc3456623289588b18 void writeURLToWritableDataObject(const URL&, const String&); COMPtr writableDataObject() const { return m_writableDataObject; } void writeImageToDataObject(Element&, const URL&); // FIXME: Layering violation. -@@ -366,6 +378,10 @@ private: - int64_t m_changeCount { 0 }; - #endif - -+#if PLATFORM(WPE) -+ std::optional m_selectionData; -+#endif -+ - #if PLATFORM(COCOA) - String m_pasteboardName; - int64_t m_changeCount; -@@ -381,6 +397,7 @@ private: +@@ -381,6 +382,7 @@ private: COMPtr m_dataObject; COMPtr m_writableDataObject; DragDataMap m_dragDataMap; @@ -6632,7 +6491,7 @@ index f8de8815d483bd3ac684c018159c593798c0495a..e84ce8eec7424fbc3456623289588b18 }; diff --git a/Source/WebCore/platform/PlatformKeyboardEvent.h b/Source/WebCore/platform/PlatformKeyboardEvent.h -index 241c4f1cc29a854e8329ba5227b7aaf0cca8138e..0a2a487dd91ddeec37fb996ecc6039fc70c4ba8d 100644 +index 0982cbbac76c9bf9cbc063fb8bf418f259e4f180..8e344ac89745c91502c0117fb3415cc63c0e8f92 100644 --- a/Source/WebCore/platform/PlatformKeyboardEvent.h +++ b/Source/WebCore/platform/PlatformKeyboardEvent.h @@ -135,6 +135,7 @@ namespace WebCore { @@ -6681,10 +6540,10 @@ index ef0abc9a93e878897ffc9d2497a3da0fca5b37b7..abd96c6d1a6c3ab9e0121c1e78f2f75a +} // namespace WebCore +#endif diff --git a/Source/WebCore/platform/PlatformScreen.h b/Source/WebCore/platform/PlatformScreen.h -index 26ca6a098bf39cff7e9a6505e84dbdbbd2aafc24..63d4fa7e39689a47269ae369bcf335642af6e669 100644 +index 40020a964792d52cb6167292b480cd2b7ade9b2f..ea58cebdea8d903890a5cf97ed4cc683616d86f2 100644 --- a/Source/WebCore/platform/PlatformScreen.h +++ b/Source/WebCore/platform/PlatformScreen.h -@@ -165,10 +165,14 @@ WEBCORE_EXPORT float screenScaleFactor(UIScreen * = nullptr); +@@ -161,10 +161,14 @@ WEBCORE_EXPORT float screenScaleFactor(UIScreen * = nullptr); #endif #if ENABLE(TOUCH_EVENTS) @@ -6715,10 +6574,10 @@ index 23f011953c66f401553bedfaef3485af215ae083..a73da2ebe47f0d8dc57f3d0159e8f299 // TouchCancelled touchPoints subsequently void setTouchPoints(Vector& touchPoints) { m_touchPoints = touchPoints; } diff --git a/Source/WebCore/platform/PlatformTouchPoint.h b/Source/WebCore/platform/PlatformTouchPoint.h -index 34715d27b529750fc866db87cd330b5184286771..3eefa218af075f76d98012cdeae7e4b344850116 100644 +index 7db61e8c8092f018719e424a6b8700bbc2588f6a..7e90ae9b7e0616601a56d4881a3092f77cae3773 100644 --- a/Source/WebCore/platform/PlatformTouchPoint.h +++ b/Source/WebCore/platform/PlatformTouchPoint.h -@@ -49,7 +49,7 @@ public: +@@ -47,7 +47,7 @@ public: { } @@ -6726,9 +6585,9 @@ index 34715d27b529750fc866db87cd330b5184286771..3eefa218af075f76d98012cdeae7e4b3 +#if !ENABLE(IOS_TOUCH_EVENTS) // FIXME: since WPE currently does not send touch stationary events, we need to be able to // create a PlatformTouchPoint of type TouchCancelled artificially - PlatformTouchPoint(unsigned id, State state, IntPoint screenPos, IntPoint pos) + PlatformTouchPoint(unsigned id, State state, DoublePoint screenPos, DoublePoint pos) diff --git a/Source/WebCore/platform/Skia.cmake b/Source/WebCore/platform/Skia.cmake -index c39b57e01190b833be46452c3d964fe243c216d3..6e4af1509037697421cf0c80e2459da0bad79ba8 100644 +index da490f391aa2e1876e792d66c08125e86e8a4719..eb8913849dd8934e4b2d60e9c18527ffc3d38264 100644 --- a/Source/WebCore/platform/Skia.cmake +++ b/Source/WebCore/platform/Skia.cmake @@ -14,6 +14,7 @@ list(APPEND WebCore_PRIVATE_FRAMEWORK_HEADERS @@ -6753,10 +6612,10 @@ index 492c5e76290c2379cda40b9663f5f67ff8f66360..096752985edf39960eb4be6eb733ebe3 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 +index 724ec3ea72c056f6666ff8413eab113082f2170c..98d054dbdce598de8949b812fdb3cc7f1d0e4182 100644 --- a/Source/WebCore/platform/graphics/ImageAdapter.h +++ b/Source/WebCore/platform/graphics/ImageAdapter.h -@@ -61,11 +61,12 @@ typedef struct HBITMAP__ *HBITMAP; +@@ -62,6 +62,8 @@ typedef struct HBITMAP__ *HBITMAP; #include #endif @@ -6765,11 +6624,6 @@ index f5d16bcb2d300d6e54b90583a4e9489862a7dfd0..e9c945854a011e9ec1de96cc8dfbc08e 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 @@ -6796,7 +6650,7 @@ index 515ddea3cd42796efa9f41ad74be07a7447c337e..36db42e2a0822d5609b39046191f05a1 namespace WebCore { diff --git a/Source/WebCore/platform/graphics/win/ComplexTextControllerUniscribe.cpp b/Source/WebCore/platform/graphics/win/ComplexTextControllerUniscribe.cpp -index 6239b3ab731ffc0826216ddda46d951d8dace5a0..270423347baa7e6fa47ad1da2154c10423037ae2 100644 +index 63d2978699a1ff636465def5191395a055e06c32..4c2cf5bd8a83f4ffe23920b77008008a5b517bd0 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, @@ -6830,7 +6684,7 @@ index 6239b3ab731ffc0826216ddda46d951d8dace5a0..270423347baa7e6fa47ad1da2154c104 + return 1; +} + - void ComplexTextController::collectComplexTextRunsForCharacters(std::span cp, unsigned stringLocation, const Font* font) + void ComplexTextController::collectComplexTextRunsForCharacters(std::span cp, unsigned stringLocation, const Font* font) { if (!font) { @@ -197,6 +224,8 @@ void ComplexTextController::collectComplexTextRunsForCharacters(std::span&& @@ -7581,7 +7435,7 @@ index a3eefa06801c54642ce6ec1c3bc7675b491ccf5c..46a8197a447b4eba385eb6b89b4a9491 *source, *type, diff --git a/Source/WebCore/platform/network/ResourceResponseBase.h b/Source/WebCore/platform/network/ResourceResponseBase.h -index 545befb0776b62d546b7b7a28093e6f1c4478d2c..a12b31f99f380ea5fbd9b82916e397cab8b6298c 100644 +index 5f2c4f0f190d30faaed7841c3f613129b20f0383..c46eb6544af2e7be7a1c58e090b09af1be2cbc93 100644 --- a/Source/WebCore/platform/network/ResourceResponseBase.h +++ b/Source/WebCore/platform/network/ResourceResponseBase.h @@ -256,6 +256,11 @@ protected: @@ -7622,15 +7476,16 @@ index 545befb0776b62d546b7b7a28093e6f1c4478d2c..a12b31f99f380ea5fbd9b82916e397ca 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 632ec5128ee28f9d5ea33bdfaa311bad56ec6396..837d67d61ba14e509cf9f72320cf56457263501b 100644 +index 8f91f105fb83605e259a25bccd7e6cf1e39fbc2f..b4e36cc919db381fdc241b3b6d3217f3aa3a5a0a 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 +@@ -574,6 +574,28 @@ bool NetworkStorageSession::setCookieFromDOM(const URL& firstParty, const SameSi return false; } +void NetworkStorageSession::setCookiesFromResponse(const URL& firstParty, const SameSiteInfo& sameSiteInfo, const URL& url, const String& setCookieValue) +{ ++ auto thirdPartyCookieBlockingDecision = ThirdPartyCookieBlockingDecision::None; + Vector cookieValues = setCookieValue.split('\n'); + size_t count = cookieValues.size(); + auto* cookies = [NSMutableArray arrayWithCapacity:count]; @@ -7642,7 +7497,12 @@ index 632ec5128ee28f9d5ea33bdfaa311bad56ec6396..837d67d61ba14e509cf9f72320cf5645 + [cookies addObject:parsedCookies[0]]; + } + NSURL *cookieURL = url.createNSURL().get(); -+ setHTTPCookiesForURL(cookieStorage().get(), cookies, cookieURL, firstParty.createNSURL().get(), sameSiteInfo); ++#if ENABLE(OPT_IN_PARTITIONED_COOKIES) && defined(CFN_COOKIE_ACCEPTS_POLICY_PARTITION) && CFN_COOKIE_ACCEPTS_POLICY_PARTITION ++ String partitionKey = isOptInCookiePartitioningEnabled() ? cookiePartitionIdentifier(firstParty) : String { }; ++#else ++ String partitionKey; ++#endif ++ setHTTPCookiesForURL(cookieStorage().get(), cookies, cookieURL, firstParty.createNSURL().get(), nsStringNilIfEmpty(partitionKey), sameSiteInfo, thirdPartyCookieBlockingDecision); +} + static NSHTTPCookieAcceptPolicy httpCookieAcceptPolicy(CFHTTPCookieStorageRef cookieStorage) @@ -7662,7 +7522,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 78584cd0fb75884e96d489d9da6a24f6ddedbeca..ebbb8c3e9a469d11e7433ac6ef85300e5b6d2ad4 100644 +index a765e8f33e07b492c5db681bdb8d0a1b560f3a89..aeecd7d3e5f2b22b039c377178ce93b2d3aee109 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 @@ -7679,7 +7539,7 @@ index 78584cd0fb75884e96d489d9da6a24f6ddedbeca..ebbb8c3e9a469d11e7433ac6ef85300e { switch (cookieDatabase().acceptPolicy()) { diff --git a/Source/WebCore/platform/network/soup/NetworkStorageSessionSoup.cpp b/Source/WebCore/platform/network/soup/NetworkStorageSessionSoup.cpp -index 5b3deb017487d8362e11479ea814e02e9221fd23..cb829266402ce26b193a62f7fa39d83681302a1e 100644 +index a3cc889b1440c6656d3888842bd1438a0d919c42..a6a0a2b96265ff15f1614abf7cf5392b1cb01247 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) @@ -7710,7 +7570,7 @@ index 5b3deb017487d8362e11479ea814e02e9221fd23..cb829266402ce26b193a62f7fa39d836 { GUniquePtr targetCookie(cookie.toSoupCookie()); diff --git a/Source/WebCore/platform/win/ClipboardUtilitiesWin.cpp b/Source/WebCore/platform/win/ClipboardUtilitiesWin.cpp -index 72e1784eff239b69db25a598934a671e7aead036..d647b0d8510edeed5aa27d780a06dd7e5cb413dd 100644 +index d78fc19ce5b5318256e9cb15dfea650fd29a3bb6..4256c020089b5e26886b680d945b695ce3a3bfc7 100644 --- a/Source/WebCore/platform/win/ClipboardUtilitiesWin.cpp +++ b/Source/WebCore/platform/win/ClipboardUtilitiesWin.cpp @@ -40,6 +40,7 @@ @@ -7774,7 +7634,7 @@ index 0379437d84807e4a8d3846afac5ec8a70e743e70..5b0461bf12535d4900ffaddc2a878262 if (!m_dragDataMap.isEmpty() || !m_platformDragData) return m_dragDataMap; diff --git a/Source/WebCore/platform/win/KeyEventWin.cpp b/Source/WebCore/platform/win/KeyEventWin.cpp -index d450bf9d0fd1f0bf8f28db483ac9d3d60fa9d114..72a59403a0b5493aea4a8e28eb15eac24b652b09 100644 +index 77a314281e749ca453edd66343d6dcf2979f658d..7f442dee6c8f3626d1d18227934817a0958fe0fa 100644 --- a/Source/WebCore/platform/win/KeyEventWin.cpp +++ b/Source/WebCore/platform/win/KeyEventWin.cpp @@ -243,10 +243,16 @@ PlatformKeyboardEvent::PlatformKeyboardEvent(HWND, WPARAM code, LPARAM keyData, @@ -7798,7 +7658,7 @@ index d450bf9d0fd1f0bf8f28db483ac9d3d60fa9d114..72a59403a0b5493aea4a8e28eb15eac2 OptionSet PlatformKeyboardEvent::currentStateOfModifierKeys() diff --git a/Source/WebCore/platform/win/PasteboardWin.cpp b/Source/WebCore/platform/win/PasteboardWin.cpp -index e862b1201f827c58d0f6971bd1071e0c0d34d44d..3166f7a8dcb1f9c2d2d505eee846c17b370e76a7 100644 +index aac283d800d41c5185e1de655b6cbe4693c827a6..5ced4f99b3481ab88d8de7c12d44730bddf513d3 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) @@ -7849,307 +7709,11 @@ index e862b1201f827c58d0f6971bd1071e0c0d34d44d..3166f7a8dcb1f9c2d2d505eee846c17b +} + } // namespace WebCore -diff --git a/Source/WebCore/platform/wpe/DragDataWPE.cpp b/Source/WebCore/platform/wpe/DragDataWPE.cpp -new file mode 100644 -index 0000000000000000000000000000000000000000..fbd32d390129129fd5b213f7f9c3e96bdca9355b ---- /dev/null -+++ b/Source/WebCore/platform/wpe/DragDataWPE.cpp -@@ -0,0 +1,92 @@ -+/* -+ * 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 "DragData.h" -+#include "SelectionData.h" -+ -+namespace WebCore { -+ -+bool DragData::canSmartReplace() const -+{ -+ return false; -+} -+ -+bool DragData::containsColor() const -+{ -+ return false; -+} -+ -+bool DragData::containsFiles() const -+{ -+ return m_platformDragData->hasFilenames(); -+} -+ -+unsigned DragData::numberOfFiles() const -+{ -+ return m_platformDragData->filenames().size(); -+} -+ -+Vector DragData::asFilenames() const -+{ -+ return m_platformDragData->filenames(); -+} -+ -+bool DragData::containsPlainText() const -+{ -+ return m_platformDragData->hasText(); -+} -+ -+String DragData::asPlainText() const -+{ -+ return m_platformDragData->text(); -+} -+ -+Color DragData::asColor() const -+{ -+ return Color(); -+} -+ -+bool DragData::containsCompatibleContent(DraggingPurpose) const -+{ -+ return containsPlainText() || containsURL() || containsColor() || containsFiles(); -+} -+ -+bool DragData::containsURL(FilenameConversionPolicy filenamePolicy) const -+{ -+ return !asURL(filenamePolicy).isEmpty(); -+} -+ -+String DragData::asURL(FilenameConversionPolicy filenamePolicy, String* title) const -+{ -+ if (!m_platformDragData->hasURL()) -+ return String(); -+ if (filenamePolicy != ConvertFilenames) { -+ if (m_platformDragData->url().protocolIsFile()) -+ return { }; -+ } -+ -+ if (title) -+ *title = m_platformDragData->urlLabel(); -+ return m_platformDragData->url().string(); -+} -+ -+bool DragData::shouldMatchStyleOnDrop() const -+{ -+ return false; -+} -+ -+} -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 -+++ b/Source/WebCore/platform/wpe/PasteboardWPE.cpp -@@ -36,6 +36,10 @@ - #include "SharedBuffer.h" - #include - -+#if ENABLE(DRAG_SUPPORT) -+#include "DragData.h" -+#endif -+ - namespace WebCore { - - std::unique_ptr Pasteboard::createForCopyAndPaste(std::unique_ptr&& context) -@@ -55,8 +59,17 @@ Pasteboard::Pasteboard(std::unique_ptr&& context) - { - } - --void Pasteboard::writeString(const String&, const String&) -+void Pasteboard::writeString(const String& type, const String& text) - { -+ if (m_selectionData) { -+ if (type == "Files"_s || type == "text/uri-list"_s) -+ m_selectionData->setURIList(text); -+ else if (type == "text/html"_s) -+ m_selectionData->setMarkup(text); -+ else if (type == "text/plain"_s) -+ m_selectionData->setText(text); -+ return; -+ } - notImplemented(); - } - -@@ -69,6 +82,11 @@ void Pasteboard::writePlainText(const String& text, SmartReplaceOption) - - void Pasteboard::write(const PasteboardURL& pasteboardURL) - { -+ if (m_selectionData) { -+ m_selectionData->clearAll(); -+ m_selectionData->setURL(pasteboardURL.url, pasteboardURL.title); -+ return; -+ } - ASSERT(!pasteboardURL.url.isEmpty()); - SelectionData data; - data.setURL(pasteboardURL.url, pasteboardURL.title); -@@ -82,6 +100,15 @@ void Pasteboard::writeTrustworthyWebURLsPboardType(const PasteboardURL&) - - void Pasteboard::write(const PasteboardImage& pasteboardImage) - { -+ if (m_selectionData) { -+ m_selectionData->clearAll(); -+ if (!pasteboardImage.url.url.isEmpty()) { -+ m_selectionData->setURL(pasteboardImage.url.url, pasteboardImage.url.title); -+ m_selectionData->setMarkup(pasteboardImage.url.markup); -+ } -+ m_selectionData->setImage(pasteboardImage.image.get()); -+ return; -+ } - SelectionData data; - if (!pasteboardImage.url.url.isEmpty()) { - data.setURL(pasteboardImage.url.url, pasteboardImage.url.title); -@@ -98,6 +125,12 @@ void Pasteboard::write(const PasteboardBuffer&) - - void Pasteboard::write(const PasteboardWebContent& pasteboardContent) - { -+ if (m_selectionData) { -+ m_selectionData->clearAll(); -+ m_selectionData->setText(pasteboardContent.text); -+ m_selectionData->setMarkup(pasteboardContent.markup); -+ return; -+ } - SelectionData data; - data.setText(pasteboardContent.text); - data.setMarkup(pasteboardContent.markup); -@@ -191,6 +224,26 @@ bool Pasteboard::hasData() - - Vector Pasteboard::typesSafeForBindings(const String& origin) - { -+ if (m_selectionData) { -+ ListHashSet types; -+ if (auto& buffer = m_selectionData->customData()) { -+ auto customData = PasteboardCustomData::fromSharedBuffer(*buffer); -+ if (customData.origin() == origin) { -+ for (auto& type : customData.orderedTypes()) -+ types.add(type); -+ } -+ } -+ -+ if (m_selectionData->hasText()) -+ types.add("text/plain"_s); -+ if (m_selectionData->hasMarkup()) -+ types.add("text/html"_s); -+ if (m_selectionData->hasURIList()) -+ types.add("text/uri-list"_s); -+ -+ return copyToVector(types); -+ } -+ - return platformStrategies()->pasteboardStrategy()->typesSafeForDOMToReadAndWrite(m_name, origin, context()); - } - -@@ -201,6 +254,13 @@ Vector Pasteboard::typesForLegacyUnsafeBindings() - - String Pasteboard::readOrigin() - { -+ if (m_selectionData) { -+ if (auto& buffer = m_selectionData->customData()) -+ return PasteboardCustomData::fromSharedBuffer(*buffer).origin(); -+ -+ return { }; -+ } -+ - // FIXME: cache custom data? - if (auto buffer = platformStrategies()->pasteboardStrategy()->readBufferFromClipboard(m_name, PasteboardCustomData::wpeType())) - return PasteboardCustomData::fromSharedBuffer(*buffer).origin(); -@@ -210,11 +270,27 @@ String Pasteboard::readOrigin() - - String Pasteboard::readString(const String& type) - { -+ if (m_selectionData) { -+ if (type == "text/plain"_s) -+ return m_selectionData->text();; -+ if (type == "text/html"_s) -+ return m_selectionData->markup(); -+ if (type == "Files"_s || type == "text/uri-list"_s) -+ return m_selectionData->uriList(); -+ return { }; -+ } -+ - return platformStrategies()->pasteboardStrategy()->readTextFromClipboard(m_name, type); - } - - String Pasteboard::readStringInCustomData(const String& type) - { -+ if (m_selectionData) { -+ if (auto& buffer = m_selectionData->customData()) -+ return PasteboardCustomData::fromSharedBuffer(*buffer).readStringInCustomData(type); -+ -+ return { }; -+ } - // FIXME: cache custom data? - if (auto buffer = platformStrategies()->pasteboardStrategy()->readBufferFromClipboard(m_name, PasteboardCustomData::wpeType())) - return PasteboardCustomData::fromSharedBuffer(*buffer).readStringInCustomData(type); -@@ -244,6 +320,17 @@ void Pasteboard::writeMarkup(const String&) - - void Pasteboard::writeCustomData(const Vector& data) - { -+ if (m_selectionData) { -+ if (!data.isEmpty()) { -+ const auto& customData = data[0]; -+ customData.forEachPlatformString([this] (auto& type, auto& string) { -+ writeString(type, string); -+ }); -+ if (customData.hasSameOriginCustomData() || !customData.origin().isEmpty()) -+ m_selectionData->setCustomData(customData.createSharedBuffer()); -+ } -+ return; -+ } - m_changeCount = platformStrategies()->pasteboardStrategy()->writeCustomData(data, m_name, context()); - } - -@@ -257,6 +344,35 @@ int64_t Pasteboard::changeCount() const - return platformStrategies()->pasteboardStrategy()->changeCount(m_name); - } - -+#if ENABLE(DRAG_SUPPORT) -+Pasteboard::Pasteboard(std::unique_ptr&& context, SelectionData&& selectionData) -+ : m_context(WTFMove(context)) -+ , m_selectionData(WTFMove(selectionData)) -+{ -+} -+ -+Pasteboard::Pasteboard(std::unique_ptr&& context, SelectionData& selectionData) -+ : m_context(WTFMove(context)) -+ , m_selectionData(selectionData) -+{ -+} -+ -+std::unique_ptr Pasteboard::createForDragAndDrop(std::unique_ptr&& context) -+{ -+ return makeUnique(WTFMove(context), SelectionData()); -+} -+ -+std::unique_ptr Pasteboard::create(const DragData& dragData) -+{ -+ RELEASE_ASSERT(dragData.platformData()); -+ return makeUnique(dragData.createPasteboardContext(), *dragData.platformData()); -+} -+ -+void Pasteboard::setDragImage(DragImage, const IntPoint&) -+{ -+} -+#endif -+ - } // namespace WebCore - - #endif // PLATFORM(WPE) diff --git a/Source/WebCore/rendering/RenderTextControl.cpp b/Source/WebCore/rendering/RenderTextControl.cpp -index 1238b6ac1b2aa43f0b1f7a74e48e9436d67934fa..261ee362bbdc4fb70450a61da3c872ac3d3a8cc7 100644 +index c147c2d3efffba27f8e50a073cff4742585217a9..ce295eff5d8b4e3ebe53991950980f6eb17e79bd 100644 --- a/Source/WebCore/rendering/RenderTextControl.cpp +++ b/Source/WebCore/rendering/RenderTextControl.cpp -@@ -230,13 +230,13 @@ void RenderTextControl::layoutExcludedChildren(RelayoutChildren relayoutChildren +@@ -232,13 +232,13 @@ void RenderTextControl::layoutExcludedChildren(RelayoutChildren relayoutChildren } } @@ -8165,7 +7729,7 @@ index 1238b6ac1b2aa43f0b1f7a74e48e9436d67934fa..261ee362bbdc4fb70450a61da3c872ac { if (auto innerTextElement = this->innerTextElement(); innerTextElement && innerTextElement->renderer()) diff --git a/Source/WebCore/rendering/RenderTextControl.h b/Source/WebCore/rendering/RenderTextControl.h -index 82e32c398dfdfa3e1fdb5dda86e98c827a290f84..c9eddb5739b1cd2ab5b744f57ecb8eb01079ad55 100644 +index 2a4b9ac7e9572ef2755b6226607567ec37339656..f2f86f9099e024b9befe94a9b0ea981178ecea5d 100644 --- a/Source/WebCore/rendering/RenderTextControl.h +++ b/Source/WebCore/rendering/RenderTextControl.h @@ -38,8 +38,8 @@ public: @@ -8179,7 +7743,7 @@ index 82e32c398dfdfa3e1fdb5dda86e98c827a290f84..c9eddb5739b1cd2ab5b744f57ecb8eb0 #endif diff --git a/Source/WebCore/workers/WorkerConsoleClient.cpp b/Source/WebCore/workers/WorkerConsoleClient.cpp -index e675901b3a15cbdefdb731c949f8a4234af5dede..97d38d8ef0fb379ab689cb2dacba6563bcaf1e85 100644 +index a9e64a0a2aabebe6c150e959034e3a5aa41cc0b0..dfc1a928eb486f9021373fc4b49e7dabe921fbae 100644 --- a/Source/WebCore/workers/WorkerConsoleClient.cpp +++ b/Source/WebCore/workers/WorkerConsoleClient.cpp @@ -124,4 +124,6 @@ void WorkerConsoleClient::recordEnd(JSC::JSGlobalObject*, Ref&& @@ -8190,7 +7754,7 @@ index e675901b3a15cbdefdb731c949f8a4234af5dede..97d38d8ef0fb379ab689cb2dacba6563 + } // namespace WebCore diff --git a/Source/WebCore/workers/WorkerConsoleClient.h b/Source/WebCore/workers/WorkerConsoleClient.h -index db95c8273bd0deb3f903a45d02fc07bbbd8ab305..bf88228b4c838b90d11d430cc9429d5130631afa 100644 +index 248649b615c7aed9b37cae14ab58152eb5ca34f1..85f3e2b7f5519822f53564a8c63b8bcb05de8594 100644 --- a/Source/WebCore/workers/WorkerConsoleClient.h +++ b/Source/WebCore/workers/WorkerConsoleClient.h @@ -58,6 +58,7 @@ private: @@ -8199,10 +7763,10 @@ index db95c8273bd0deb3f903a45d02fc07bbbd8ab305..bf88228b4c838b90d11d430cc9429d51 void screenshot(JSC::JSGlobalObject*, Ref&&) override; + void bindingCalled(JSC::JSGlobalObject*, const String& name, const String& arg) override; - WorkerOrWorkletGlobalScope& m_globalScope; + WeakRef m_globalScope; }; diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp -index 2d8a4462ea7897aca4455d526fcb5e0fd6b3dcd3..53f0de10cdfb27736855fa837d9e4a650c18c90a 100644 +index a9ca48e0e4b059f536957c7d0cdcc189dd91b243..e72b63e6f376bdc57a9a3155e862f1ef8d193baf 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp @@ -97,6 +97,8 @@ @@ -8214,7 +7778,7 @@ index 2d8a4462ea7897aca4455d526fcb5e0fd6b3dcd3..53f0de10cdfb27736855fa837d9e4a65 #endif #if ENABLE(APPLE_PAY_REMOTE_UI) -@@ -1232,6 +1234,14 @@ void NetworkConnectionToWebProcess::clearPageSpecificData(PageIdentifier pageID) +@@ -1259,6 +1261,14 @@ void NetworkConnectionToWebProcess::clearPageSpecificData(PageIdentifier pageID) storageSession->clearPageSpecificDataForResourceLoadStatistics(pageID); } @@ -8230,10 +7794,10 @@ index 2d8a4462ea7897aca4455d526fcb5e0fd6b3dcd3..53f0de10cdfb27736855fa837d9e4a65 { if (CheckedPtr storageSession = m_networkProcess->storageSession(m_sessionID)) diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h -index 96e3d7d1370b114591191dea8e0e042c9f06c0c8..a4ddbd6483b8bd1ffb8f360cad5552dbcbcfc018 100644 +index a42ac4c2ba8b4c78b86a9f1ba8975ced3da6889e..6224d5c9b6e2a24c3a6184318a11de19ba2ebf73 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h -@@ -388,6 +388,8 @@ private: +@@ -390,6 +390,8 @@ private: void clearPageSpecificData(WebCore::PageIdentifier); @@ -8243,20 +7807,20 @@ index 96e3d7d1370b114591191dea8e0e042c9f06c0c8..a4ddbd6483b8bd1ffb8f360cad5552db void logUserInteraction(RegistrableDomain&&); diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in -index 26d43eb49dc956a104782a5e10df084dc2b1e7e8..aa92cfd23b61eae7ee90c9fb7846189f9406e379 100644 +index 66ae06fcc90ef78dd7b3d945ba0a8b22f0033eef..699ff83841f2f34229dd7d9e7e910f166c331e3c 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in -@@ -80,6 +80,8 @@ messages -> NetworkConnectionToWebProcess WantsDispatchMessage { +@@ -83,6 +83,8 @@ messages -> NetworkConnectionToWebProcess WantsDispatchMessage { ClearPageSpecificData(WebCore::PageIdentifier pageID); + SetCookieFromResponse(URL firstParty, struct WebCore::SameSiteInfo sameSiteInfo, URL url, String setCookieValue); -+ - RemoveStorageAccessForFrame(WebCore::FrameIdentifier frameID, WebCore::PageIdentifier pageID); ++ + [EnabledBy=StorageAccessAPIEnabled] RemoveStorageAccessForFrame(WebCore::FrameIdentifier frameID, WebCore::PageIdentifier pageID); LogUserInteraction(WebCore::RegistrableDomain domain) ResourceLoadStatisticsUpdated(Vector statistics) -> () diff --git a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm -index 23fbdde0494dae83161c9b715f870db48b8caff1..df27897d278c7bb755378ef309376b50d79124cb 100644 +index 22d457d4b9ae51d1ab65b0c35212281ab995e40b..d10078ae0149fd7fb6cdca4b2e4d0412dd9e8fac 100644 --- a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm +++ b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm @@ -1136,6 +1136,14 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)data @@ -8275,7 +7839,7 @@ index 23fbdde0494dae83161c9b715f870db48b8caff1..df27897d278c7bb755378ef309376b50 #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 7b1879c44551fae26e338352189adc5b9add0266..dac8034d87d3bcbbb56576c2ef37713ea0ef85ff 100644 +index d5081a775c5f38d8f8075ea193514aa4eeaee25a..2ba1958d599b575ab0b1ad9db43de53f709aabdf 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 @@ -8287,7 +7851,7 @@ index 7b1879c44551fae26e338352189adc5b9add0266..dac8034d87d3bcbbb56576c2ef37713e handleCookieHeaders(request.resourceRequest(), receivedResponse); diff --git a/Source/WebKit/NetworkProcess/mac/com.apple.WebKit.NetworkProcess.sb.in b/Source/WebKit/NetworkProcess/mac/com.apple.WebKit.NetworkProcess.sb.in -index aebce13abcf8f93c8fa48936120c2065f0a664b1..7b003cf0d65d0179b165fcbce775cfd5b6a19374 100644 +index a0ad3ac58fb9efcef3fb408aa60cc9fa0353e048..39c5df74feaef3573ecd905c1812c77dfa7c217c 100644 --- a/Source/WebKit/NetworkProcess/mac/com.apple.WebKit.NetworkProcess.sb.in +++ b/Source/WebKit/NetworkProcess/mac/com.apple.WebKit.NetworkProcess.sb.in @@ -451,9 +451,11 @@ @@ -8306,7 +7870,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 375489f4b3944b7b6f4ac03b1784aef8ca116206..6cce36bd75b872b45b7a6ecc07e2adff00bbc7a4 100644 +index dc1bbe1c62fbc33a69afc0e57225129f1dfdd6f2..4f2d2f6af0e9ea7c7f7ca5be42772e888aa3674b 100644 --- a/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.cpp +++ b/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.cpp @@ -461,6 +461,8 @@ void NetworkDataTaskSoup::didSendRequest(GRefPtr&& inputStream) @@ -8319,10 +7883,10 @@ index 375489f4b3944b7b6f4ac03b1784aef8ca116206..6cce36bd75b872b45b7a6ecc07e2adff } diff --git a/Source/WebKit/PlatformGTK.cmake b/Source/WebKit/PlatformGTK.cmake -index d39a11b312cc9237973ee095e995afa379da1b0d..e5fc9f02cea359414154756b27958019cbb57374 100644 +index 45b68fab2f81dc5d50df4a62113d8f0aeb10c9e2..78c9821d94e6e2b9a4257920bda2e9a4a8dd0ee7 100644 --- a/Source/WebKit/PlatformGTK.cmake +++ b/Source/WebKit/PlatformGTK.cmake -@@ -320,6 +320,9 @@ list(APPEND WebKit_SYSTEM_INCLUDE_DIRECTORIES +@@ -322,6 +322,9 @@ list(APPEND WebKit_SYSTEM_INCLUDE_DIRECTORIES ${GSTREAMER_PBUTILS_INCLUDE_DIRS} ${GTK_INCLUDE_DIRS} ${LIBSOUP_INCLUDE_DIRS} @@ -8332,7 +7896,7 @@ index d39a11b312cc9237973ee095e995afa379da1b0d..e5fc9f02cea359414154756b27958019 ) list(APPEND WebKit_INTERFACE_INCLUDE_DIRECTORIES -@@ -363,6 +366,9 @@ if (USE_LIBWEBRTC) +@@ -365,6 +368,9 @@ if (USE_LIBWEBRTC) list(APPEND WebKit_SYSTEM_INCLUDE_DIRECTORIES "${THIRDPARTY_DIR}/libwebrtc/Source/" "${THIRDPARTY_DIR}/libwebrtc/Source/webrtc" @@ -8342,7 +7906,7 @@ index d39a11b312cc9237973ee095e995afa379da1b0d..e5fc9f02cea359414154756b27958019 ) endif () -@@ -414,6 +420,12 @@ else () +@@ -416,6 +422,12 @@ else () set(WebKitGTK_ENUM_HEADER_TEMPLATE ${WEBKIT_DIR}/UIProcess/API/gtk/WebKitEnumTypesGtk3.h.in) endif () @@ -8356,10 +7920,10 @@ index d39a11b312cc9237973ee095e995afa379da1b0d..e5fc9f02cea359414154756b27958019 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 650a918189ad22a6fa76404c80ebca477331c193..74fc793279465c3aa9a586130a98c182e1334568 100644 +index 9accd2551b67ad54960f4c2da7b9692263693cde..89fdaf77834fbe059876d746e4c1f0ab5e0ed9be 100644 --- a/Source/WebKit/PlatformWPE.cmake +++ b/Source/WebKit/PlatformWPE.cmake -@@ -221,6 +221,7 @@ set(WPE_API_HEADER_TEMPLATES +@@ -224,6 +224,7 @@ set(WPE_API_HEADER_TEMPLATES ${WEBKIT_DIR}/UIProcess/API/glib/WebKitWindowProperties.h.in ${WEBKIT_DIR}/UIProcess/API/glib/WebKitWebsitePolicies.h.in ${WEBKIT_DIR}/UIProcess/API/glib/webkit.h.in @@ -8367,7 +7931,7 @@ index 650a918189ad22a6fa76404c80ebca477331c193..74fc793279465c3aa9a586130a98c182 ) if (ENABLE_2022_GLIB_API) -@@ -432,8 +433,17 @@ list(APPEND WebKit_SYSTEM_INCLUDE_DIRECTORIES +@@ -434,8 +435,17 @@ list(APPEND WebKit_SYSTEM_INCLUDE_DIRECTORIES ${GIO_UNIX_INCLUDE_DIRS} ${GLIB_INCLUDE_DIRS} ${LIBSOUP_INCLUDE_DIRS} @@ -8386,7 +7950,7 @@ index 650a918189ad22a6fa76404c80ebca477331c193..74fc793279465c3aa9a586130a98c182 WPE::libwpe ${GLIB_LIBRARIES} diff --git a/Source/WebKit/PlatformWin.cmake b/Source/WebKit/PlatformWin.cmake -index 8429fc8b2e3721830edf197b3369f4f21bb70a9a..35e99519b9a23ac19757a8b67fe477ce26c06cd0 100644 +index 86a1febedca9fcbe7203db8cec94e8db1ef25a43..72ca856202231c8aba8efcee26a6278131141952 100644 --- a/Source/WebKit/PlatformWin.cmake +++ b/Source/WebKit/PlatformWin.cmake @@ -54,8 +54,13 @@ list(APPEND WebKit_SOURCES @@ -8409,9 +7973,9 @@ index 8429fc8b2e3721830edf197b3369f4f21bb70a9a..35e99519b9a23ac19757a8b67fe477ce WebProcess/WebCoreSupport/win/WebPopupMenuWin.cpp + WebProcess/WebCoreSupport/win/WebDragClientWin.cpp - WebProcess/WebPage/AcceleratedSurface.cpp + WebProcess/WebPage/CoordinatedGraphics/DrawingAreaCoordinatedGraphics.cpp -@@ -121,6 +127,36 @@ list(APPEND WebKit_PRIVATE_LIBRARIES +@@ -119,6 +125,36 @@ list(APPEND WebKit_PRIVATE_LIBRARIES comctl32 ) @@ -8449,10 +8013,10 @@ index 8429fc8b2e3721830edf197b3369f4f21bb70a9a..35e99519b9a23ac19757a8b67fe477ce WebProcess/EntryPoint/win/WebProcessMain.cpp diff --git a/Source/WebKit/Shared/AuxiliaryProcess.h b/Source/WebKit/Shared/AuxiliaryProcess.h -index 35fd0f0397cd92c5bf025d89d7f7c8139c03c69d..b1559f1bb2d2d7a74d0b534f81352e366498bfc8 100644 +index 98f4a9734edcf45750ed6badce6f9f31e93c25b7..59da50b79c30d8ac1d494a37d973251f67782bc3 100644 --- a/Source/WebKit/Shared/AuxiliaryProcess.h +++ b/Source/WebKit/Shared/AuxiliaryProcess.h -@@ -216,6 +216,11 @@ struct AuxiliaryProcessInitializationParameters { +@@ -220,6 +220,11 @@ struct AuxiliaryProcessInitializationParameters { IPC::Connection::Identifier connectionIdentifier; HashMap extraInitializationData; WTF::AuxiliaryProcessType processType; @@ -8517,7 +8081,7 @@ index 60a6308dbbd3f9a09c82214dcd97f86dda9d2078..31036d180a6f319d17c53a363d58d8ba #if USE(APPKIT) diff --git a/Source/WebKit/Shared/NativeWebMouseEvent.h b/Source/WebKit/Shared/NativeWebMouseEvent.h -index a39b6dd673e1639f9fe64c23dd054f0ff57f7464..4026f6244889e5a0ee85edb72696d0be20ba531d 100644 +index af507e2f942f1c94e60ad94623f0f0e097f4194b..b974bb08d79f7204e803ece42f13de207c58527c 100644 --- a/Source/WebKit/Shared/NativeWebMouseEvent.h +++ b/Source/WebKit/Shared/NativeWebMouseEvent.h @@ -87,6 +87,11 @@ public: @@ -8547,10 +8111,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 6bb3cc33a43da97e85efe8afb8e7ea2400dd4787..79e2b2f0196d3edb62b2219d59229bb2822e549f 100644 +index 73afacba3fe3705757e56fce7da5fbb6d44c8285..ebab11b20dfa8b6c7ed3e2a63fc3ac9f3857e068 100644 --- a/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in +++ b/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in -@@ -2841,6 +2841,9 @@ class WebCore::AuthenticationChallenge { +@@ -2849,6 +2849,9 @@ class WebCore::AuthenticationChallenge { class WebCore::DragData { #if PLATFORM(COCOA) String pasteboardName(); @@ -8560,7 +8124,7 @@ index 6bb3cc33a43da97e85efe8afb8e7ea2400dd4787..79e2b2f0196d3edb62b2219d59229bb2 #endif WebCore::IntPoint clientPosition(); WebCore::IntPoint globalPosition(); -@@ -3649,6 +3652,7 @@ enum class WebCore::WasPrivateRelayed : bool; +@@ -3708,6 +3711,7 @@ enum class WebCore::WasPrivateRelayed : bool; String httpStatusText; String httpVersion; WebCore::HTTPHeaderMap httpHeaderFields; @@ -8674,7 +8238,7 @@ index 8e4e2d6d5ebb08fba210fe0a328d45290348dd11..32a43192ec1e918c33b1b046b71d2ec5 const String& text() const { return m_text; } diff --git a/Source/WebKit/Shared/WebMouseEvent.h b/Source/WebKit/Shared/WebMouseEvent.h -index 20a6e465457151b02daa22e6bc059cf0e117ece5..ef4b1f737aaa683bc13c447aa4ca77e5cf0d64d7 100644 +index 249ed5bf7a9ca037349cae92a063e718d015333d..6c179a0545456967f26ebf64f701e0290acaa589 100644 --- a/Source/WebKit/Shared/WebMouseEvent.h +++ b/Source/WebKit/Shared/WebMouseEvent.h @@ -70,6 +70,7 @@ public: @@ -8682,14 +8246,14 @@ index 20a6e465457151b02daa22e6bc059cf0e117ece5..ef4b1f737aaa683bc13c447aa4ca77e5 WebMouseEventButton button() const { return m_button; } unsigned short buttons() const { return m_buttons; } + void playwrightSetButtons(unsigned short buttons) { m_buttons = buttons; } - const WebCore::IntPoint& position() const { return m_position; } // Relative to the view. - void setPosition(const WebCore::IntPoint& position) { m_position = position; } - const WebCore::IntPoint& globalPosition() const { return m_globalPosition; } + const WebCore::DoublePoint& position() const { return m_position; } // Relative to the view. + void setPosition(const WebCore::DoublePoint& position) { m_position = position; } + const WebCore::DoublePoint& globalPosition() const { return m_globalPosition; } diff --git a/Source/WebKit/Shared/WebPageCreationParameters.h b/Source/WebKit/Shared/WebPageCreationParameters.h -index 02b701cbe003b0f9e16b9712e98c4157ebedbd69..70269920251e4d31f2c73fddc2f8cecbff453498 100644 +index bc4cf118f1c9120d22d282a42348c5f3c011eb0d..3f95916695e08056dc87a700a32f4f445e2474a6 100644 --- a/Source/WebKit/Shared/WebPageCreationParameters.h +++ b/Source/WebKit/Shared/WebPageCreationParameters.h -@@ -303,6 +303,8 @@ struct WebPageCreationParameters { +@@ -306,6 +306,8 @@ struct WebPageCreationParameters { WebCore::ShouldRelaxThirdPartyCookieBlocking shouldRelaxThirdPartyCookieBlocking { WebCore::ShouldRelaxThirdPartyCookieBlocking::No }; bool httpsUpgradeEnabled { true }; @@ -8699,10 +8263,10 @@ index 02b701cbe003b0f9e16b9712e98c4157ebedbd69..70269920251e4d31f2c73fddc2f8cecb #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 822229ba75d9afe290cc72966cd763bd476f76e1..4487e71729e4c874ffec33374f25aee2ce289c3e 100644 +index 1f43e0af331267900f098910e81954807aae4d13..1214484d35b1599fa08f0678d012b291292e6d5e 100644 --- a/Source/WebKit/Shared/WebPageCreationParameters.serialization.in +++ b/Source/WebKit/Shared/WebPageCreationParameters.serialization.in -@@ -225,6 +225,8 @@ enum class WebCore::UserInterfaceLayoutDirection : bool; +@@ -228,6 +228,8 @@ enum class WebCore::UserInterfaceLayoutDirection : bool; bool httpsUpgradeEnabled; @@ -8793,10 +8357,10 @@ 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 a978050575b323345d0de29d1abd1fa0bc993e77..f7ead71d28f77932edb9e7f8958b25e99b50fead 100644 +index 209c0c3ab2c3bdd0df08e4b6b8083943caa8815b..ddf661fe8f14923908bda6de57ed703be2f839a6 100644 --- a/Source/WebKit/Sources.txt +++ b/Source/WebKit/Sources.txt -@@ -392,6 +392,7 @@ UIProcess/AboutSchemeHandler.cpp +@@ -393,6 +393,7 @@ UIProcess/AboutSchemeHandler.cpp UIProcess/AuxiliaryProcessProxy.cpp UIProcess/BackgroundProcessResponsivenessTimer.cpp UIProcess/BrowsingContextGroup.cpp @@ -8804,7 +8368,7 @@ index a978050575b323345d0de29d1abd1fa0bc993e77..f7ead71d28f77932edb9e7f8958b25e9 UIProcess/DeviceIdHashSaltStorage.cpp UIProcess/DisplayLink.cpp UIProcess/DisplayLinkProcessProxyClient.cpp -@@ -401,6 +402,8 @@ UIProcess/FrameLoadState.cpp +@@ -402,6 +403,8 @@ UIProcess/FrameLoadState.cpp UIProcess/FrameProcess.cpp UIProcess/GeolocationPermissionRequestManagerProxy.cpp UIProcess/GeolocationPermissionRequestProxy.cpp @@ -8813,7 +8377,7 @@ index a978050575b323345d0de29d1abd1fa0bc993e77..f7ead71d28f77932edb9e7f8958b25e9 UIProcess/LegacyGlobalSettings.cpp UIProcess/MediaKeySystemPermissionRequestManagerProxy.cpp UIProcess/MediaKeySystemPermissionRequestProxy.cpp -@@ -408,10 +411,12 @@ UIProcess/ModelElementController.cpp +@@ -409,10 +412,12 @@ UIProcess/ModelElementController.cpp UIProcess/OverrideLanguages.cpp UIProcess/PageClient.cpp UIProcess/PageLoadState.cpp @@ -8826,7 +8390,7 @@ index a978050575b323345d0de29d1abd1fa0bc993e77..f7ead71d28f77932edb9e7f8958b25e9 UIProcess/RemotePageDrawingAreaProxy.cpp UIProcess/RemotePageFullscreenManagerProxy.cpp UIProcess/RemotePagePlaybackSessionManagerProxy.cpp -@@ -456,6 +461,8 @@ UIProcess/WebOpenPanelResultListenerProxy.cpp +@@ -457,6 +462,8 @@ UIProcess/WebOpenPanelResultListenerProxy.cpp UIProcess/WebPageDiagnosticLoggingClient.cpp UIProcess/WebPageGroup.cpp UIProcess/WebPageInjectedBundleClient.cpp @@ -8835,7 +8399,7 @@ index a978050575b323345d0de29d1abd1fa0bc993e77..f7ead71d28f77932edb9e7f8958b25e9 UIProcess/WebPageProxy.cpp UIProcess/WebPageProxyMessageReceiverRegistration.cpp UIProcess/WebPageProxyTesting.cpp -@@ -609,6 +616,9 @@ UIProcess/Inspector/WebPageDebuggable.cpp +@@ -616,6 +623,9 @@ UIProcess/Inspector/WebPageDebuggable.cpp UIProcess/Inspector/WebPageInspectorController.cpp UIProcess/Inspector/Agents/InspectorBrowserAgent.cpp @@ -8846,10 +8410,10 @@ index a978050575b323345d0de29d1abd1fa0bc993e77..f7ead71d28f77932edb9e7f8958b25e9 UIProcess/Media/AudioSessionRoutingArbitratorProxy.cpp UIProcess/Media/MediaUsageManager.cpp diff --git a/Source/WebKit/SourcesCocoa.txt b/Source/WebKit/SourcesCocoa.txt -index 4e2c7431cfc5c1b952232c30d6aa7f46815813c7..095c1b78812df547ab8cbe62e4ac801b5eb98968 100644 +index 50aa813e87673ff3f3c643dc2db924f5dcc1b613..cba747901f72fb63a6c3a0d1c08fd41fd7953827 100644 --- a/Source/WebKit/SourcesCocoa.txt +++ b/Source/WebKit/SourcesCocoa.txt -@@ -270,6 +270,7 @@ UIProcess/API/Cocoa/_WKArchiveExclusionRule.mm +@@ -268,6 +268,7 @@ UIProcess/API/Cocoa/_WKArchiveExclusionRule.mm UIProcess/API/Cocoa/_WKAttachment.mm UIProcess/API/Cocoa/_WKAutomationSession.mm UIProcess/API/Cocoa/_WKAutomationSessionConfiguration.mm @@ -8857,7 +8421,7 @@ index 4e2c7431cfc5c1b952232c30d6aa7f46815813c7..095c1b78812df547ab8cbe62e4ac801b UIProcess/API/Cocoa/_WKContentRuleListAction.mm UIProcess/API/Cocoa/_WKContextMenuElementInfo.mm UIProcess/API/Cocoa/_WKCustomHeaderFields.mm @no-unify -@@ -467,6 +468,7 @@ UIProcess/Inspector/ios/WKInspectorHighlightView.mm +@@ -466,6 +467,7 @@ UIProcess/Inspector/ios/WKInspectorHighlightView.mm UIProcess/Inspector/ios/WKInspectorNodeSearchGestureRecognizer.mm UIProcess/Inspector/mac/RemoteWebInspectorUIProxyMac.mm @@ -8866,7 +8430,7 @@ index 4e2c7431cfc5c1b952232c30d6aa7f46815813c7..095c1b78812df547ab8cbe62e4ac801b UIProcess/Inspector/mac/WKInspectorResourceURLSchemeHandler.mm UIProcess/Inspector/mac/WKInspectorViewController.mm diff --git a/Source/WebKit/SourcesGTK.txt b/Source/WebKit/SourcesGTK.txt -index 68733a26a5cb0cd8f944b5ab48eacb256e147976..b8d320e417303329b9a6fa56ce4c3f6fccc286b8 100644 +index b93e726f6694cf9b5232e364eceb300e22e00ac9..569b8847373c994d514ee315fdfda8281695c6a0 100644 --- a/Source/WebKit/SourcesGTK.txt +++ b/Source/WebKit/SourcesGTK.txt @@ -122,6 +122,7 @@ UIProcess/API/glib/WebKitAutomationSession.cpp @no-unify @@ -8885,7 +8449,7 @@ index 68733a26a5cb0cd8f944b5ab48eacb256e147976..b8d320e417303329b9a6fa56ce4c3f6f UIProcess/glib/ScreenManager.cpp UIProcess/glib/SystemSettingsManagerProxy.cpp UIProcess/glib/WebPageProxyGLib.cpp -@@ -272,6 +274,7 @@ UIProcess/gtk/DisplayX11.cpp @no-unify +@@ -271,6 +273,7 @@ UIProcess/gtk/DisplayX11.cpp @no-unify UIProcess/gtk/DisplayWayland.cpp @no-unify UIProcess/gtk/WebDateTimePickerGtk.cpp UIProcess/gtk/HardwareAccelerationManager.cpp @@ -8893,7 +8457,7 @@ index 68733a26a5cb0cd8f944b5ab48eacb256e147976..b8d320e417303329b9a6fa56ce4c3f6f UIProcess/gtk/KeyBindingTranslator.cpp UIProcess/gtk/PointerLockManager.cpp @no-unify UIProcess/gtk/PointerLockManagerWayland.cpp @no-unify -@@ -285,6 +288,8 @@ UIProcess/gtk/ViewGestureControllerGtk.cpp +@@ -284,6 +287,8 @@ UIProcess/gtk/ViewGestureControllerGtk.cpp UIProcess/gtk/WebColorPickerGtk.cpp UIProcess/gtk/WebContextMenuProxyGtk.cpp UIProcess/gtk/WebDataListSuggestionsDropdownGtk.cpp @@ -8903,10 +8467,10 @@ index 68733a26a5cb0cd8f944b5ab48eacb256e147976..b8d320e417303329b9a6fa56ce4c3f6f UIProcess/gtk/WebPasteboardProxyGtk.cpp UIProcess/gtk/WebPopupMenuProxyGtk.cpp diff --git a/Source/WebKit/SourcesWPE.txt b/Source/WebKit/SourcesWPE.txt -index f3647727106bd3786d17adaf0c5f373184ace9fb..419a5a7002ed6567f0110fa63ff723691f027669 100644 +index 33853de0d6c3e3ed4329c6a4a7f49d9303367b99..a7a1363d7d9b6e8f073e0f3c01b29240dcafb5cd 100644 --- a/Source/WebKit/SourcesWPE.txt +++ b/Source/WebKit/SourcesWPE.txt -@@ -124,6 +124,7 @@ UIProcess/API/glib/WebKitAuthenticationRequest.cpp @no-unify +@@ -123,6 +123,7 @@ UIProcess/API/glib/WebKitAuthenticationRequest.cpp @no-unify UIProcess/API/glib/WebKitAutomationSession.cpp @no-unify UIProcess/API/glib/WebKitBackForwardList.cpp @no-unify UIProcess/API/glib/WebKitBackForwardListItem.cpp @no-unify @@ -8914,7 +8478,7 @@ index f3647727106bd3786d17adaf0c5f373184ace9fb..419a5a7002ed6567f0110fa63ff72369 UIProcess/API/glib/WebKitContextMenuClient.cpp @no-unify UIProcess/API/glib/WebKitCookieManager.cpp @no-unify UIProcess/API/glib/WebKitCredential.cpp @no-unify -@@ -157,6 +158,7 @@ UIProcess/API/glib/WebKitOptionMenu.cpp @no-unify +@@ -156,6 +157,7 @@ UIProcess/API/glib/WebKitOptionMenu.cpp @no-unify UIProcess/API/glib/WebKitOptionMenuItem.cpp @no-unify UIProcess/API/glib/WebKitPermissionRequest.cpp @no-unify UIProcess/API/glib/WebKitPermissionStateQuery.cpp @no-unify @@ -8922,7 +8486,7 @@ index f3647727106bd3786d17adaf0c5f373184ace9fb..419a5a7002ed6567f0110fa63ff72369 UIProcess/API/glib/WebKitPolicyDecision.cpp @no-unify UIProcess/API/glib/WebKitPrivate.cpp @no-unify UIProcess/API/glib/WebKitProtocolHandler.cpp @no-unify -@@ -226,6 +228,7 @@ UIProcess/glib/DisplayVBlankMonitorDRM.cpp +@@ -225,6 +227,7 @@ UIProcess/glib/DisplayVBlankMonitorDRM.cpp UIProcess/glib/DisplayVBlankMonitorThreaded.cpp UIProcess/glib/DisplayVBlankMonitorTimer.cpp UIProcess/glib/FenceMonitor.cpp @@ -8930,9 +8494,9 @@ index f3647727106bd3786d17adaf0c5f373184ace9fb..419a5a7002ed6567f0110fa63ff72369 UIProcess/glib/ScreenManager.cpp UIProcess/glib/SystemSettingsManagerProxy.cpp UIProcess/glib/WebPageProxyGLib.cpp -@@ -258,9 +261,15 @@ UIProcess/soup/WebProcessPoolSoup.cpp +@@ -257,9 +260,15 @@ UIProcess/soup/WebProcessPoolSoup.cpp - UIProcess/wpe/AcceleratedBackingStoreDMABuf.cpp + UIProcess/wpe/AcceleratedBackingStore.cpp UIProcess/wpe/DisplayVBlankMonitorWPE.cpp +UIProcess/wpe/InspectorTargetProxyWPE.cpp UIProcess/wpe/ScreenManagerWPE.cpp @@ -8946,17 +8510,8 @@ index f3647727106bd3786d17adaf0c5f373184ace9fb..419a5a7002ed6567f0110fa63ff72369 UIProcess/wpe/WebPageProxyWPE.cpp UIProcess/wpe/WebPasteboardProxyWPE.cpp UIProcess/wpe/WebPreferencesWPE.cpp -@@ -292,6 +301,8 @@ WebProcess/WebCoreSupport/glib/WebEditorClientGLib.cpp - - WebProcess/WebCoreSupport/soup/WebFrameNetworkingContext.cpp - -+WebProcess/WebCoreSupport/wpe/WebDragClientWPE.cpp -+ - WebProcess/WebCoreSupport/wpe/WebEditorClientWPE.cpp - - WebProcess/WebPage/AcceleratedSurface.cpp diff --git a/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp b/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp -index 1182f2afe2c857598449d3dee6c1be028746c595..4256f65734adf9c68a30d70cc49c0b3a6fec1b1a 100644 +index 7d9d8e51d0f4531602ab90d8097e0bc322d8ba51..0d8fe5bfe9eda08897dd5a3b340710ed0f3d0500 100644 --- a/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp +++ b/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp @@ -273,6 +273,11 @@ RefPtr PageConfiguration::protectedRelatedPage() const @@ -8972,7 +8527,7 @@ index 1182f2afe2c857598449d3dee6c1be028746c595..4256f65734adf9c68a30d70cc49c0b3a { return m_data.pageToCloneSessionStorageFrom.get(); diff --git a/Source/WebKit/UIProcess/API/APIPageConfiguration.h b/Source/WebKit/UIProcess/API/APIPageConfiguration.h -index 43a17d77b13b0e51217506425607b8e0698062e4..96c7747d26cfa1d196796c784c046f6c17666969 100644 +index 9f241e19040ceeb2c525d2a58cfedd05da582427..0c79bc4e29952f3dd5ccb7522bf6a8f00dc662a6 100644 --- a/Source/WebKit/UIProcess/API/APIPageConfiguration.h +++ b/Source/WebKit/UIProcess/API/APIPageConfiguration.h @@ -160,6 +160,10 @@ public: @@ -8986,7 +8541,7 @@ index 43a17d77b13b0e51217506425607b8e0698062e4..96c7747d26cfa1d196796c784c046f6c WebKit::WebPageProxy* pageToCloneSessionStorageFrom() const; void setPageToCloneSessionStorageFrom(WeakPtr&&); -@@ -516,6 +520,7 @@ private: +@@ -519,6 +523,7 @@ private: #endif RefPtr pageGroup; WeakPtr relatedPage; @@ -9042,10 +8597,10 @@ index 2fe09a750bf32bd6f124603af08e16adba8a6c7c..5ca91400765fd53559b4f4cefa172b5a 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 b5c743d02f89afbe0c9a90e5f7e746fbd31fbbb2..29dbedfcf6929788b4810b8e98acaee26d1a686d 100644 +index 89357e7184a9d01f03e8bdf17c555ce94bcc39b3..d9a2d352f51eaebecaadd2bc59c27b3e1313f706 100644 --- a/Source/WebKit/UIProcess/API/APIUIClient.h +++ b/Source/WebKit/UIProcess/API/APIUIClient.h -@@ -114,6 +114,7 @@ public: +@@ -116,6 +116,7 @@ public: virtual void runJavaScriptAlert(WebKit::WebPageProxy&, const WTF::String&, WebKit::WebFrameProxy*, WebKit::FrameInfoData&&, Function&& completionHandler) { completionHandler(); } virtual void runJavaScriptConfirm(WebKit::WebPageProxy&, const WTF::String&, WebKit::WebFrameProxy*, WebKit::FrameInfoData&&, Function&& completionHandler) { completionHandler(false); } virtual void runJavaScriptPrompt(WebKit::WebPageProxy&, const WTF::String&, const WTF::String&, WebKit::WebFrameProxy*, WebKit::FrameInfoData&&, Function&& completionHandler) { completionHandler(WTF::String()); } @@ -9097,10 +8652,10 @@ index 026121d114c5fcad84c1396be8d692625beaa3bd..edd6e5cae033124c589959a42522fde0 } #endif diff --git a/Source/WebKit/UIProcess/API/C/WKPage.cpp b/Source/WebKit/UIProcess/API/C/WKPage.cpp -index e15be39b349437382ef59f303986dac5246856dd..08b0290d847a046a5861475b7a5015513b22551f 100644 +index 9c51cbad3099f8e589262e8bfa320ef8e05fa1c1..cd914594389ee319f5f67082dd221fb7a7b2a3f6 100644 --- a/Source/WebKit/UIProcess/API/C/WKPage.cpp +++ b/Source/WebKit/UIProcess/API/C/WKPage.cpp -@@ -1943,6 +1943,13 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient +@@ -1927,6 +1927,13 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient m_client.addMessageToConsole(toAPI(&page), toAPI(message.impl()), m_client.base.clientInfo); } @@ -9114,7 +8669,7 @@ index e15be39b349437382ef59f303986dac5246856dd..08b0290d847a046a5861475b7a501551 void setStatusText(WebPageProxy* page, const String& text) final { if (!m_client.setStatusText) -@@ -1972,6 +1979,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient +@@ -1964,6 +1971,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient { if (!m_client.didNotHandleKeyEvent) return; @@ -9124,7 +8679,7 @@ index e15be39b349437382ef59f303986dac5246856dd..08b0290d847a046a5861475b7a501551 } diff --git a/Source/WebKit/UIProcess/API/C/WKPageUIClient.h b/Source/WebKit/UIProcess/API/C/WKPageUIClient.h -index fc43c44a85a0fc6bf5f8c643bd120a16ce762914..ee86fd213d25682f9b6553ec7da99bc8a812212b 100644 +index ae4a98c2fe5782eb2356dc8b6b486f6b44db6db3..62b2f3c351fe287af11c1ee2815f1fabe044d537 100644 --- a/Source/WebKit/UIProcess/API/C/WKPageUIClient.h +++ b/Source/WebKit/UIProcess/API/C/WKPageUIClient.h @@ -98,6 +98,7 @@ typedef void (*WKPageRunBeforeUnloadConfirmPanelCallback)(WKPageRef page, WKStri @@ -9135,7 +8690,7 @@ index fc43c44a85a0fc6bf5f8c643bd120a16ce762914..ee86fd213d25682f9b6553ec7da99bc8 typedef void (*WKPageRequestStorageAccessConfirmCallback)(WKPageRef page, WKFrameRef frame, WKStringRef requestingDomain, WKStringRef currentDomain, WKPageRequestStorageAccessConfirmResultListenerRef listener, const void *clientInfo); typedef void (*WKPageTakeFocusCallback)(WKPageRef page, WKFocusDirection direction, const void *clientInfo); typedef void (*WKPageFocusCallback)(WKPageRef page, const void *clientInfo); -@@ -1365,6 +1366,7 @@ typedef struct WKPageUIClientV14 { +@@ -1366,6 +1367,7 @@ typedef struct WKPageUIClientV14 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; @@ -9143,7 +8698,7 @@ index fc43c44a85a0fc6bf5f8c643bd120a16ce762914..ee86fd213d25682f9b6553ec7da99bc8 } WKPageUIClientV14; typedef struct WKPageUIClientV15 { -@@ -1472,6 +1474,7 @@ typedef struct WKPageUIClientV15 { +@@ -1473,6 +1475,7 @@ typedef struct WKPageUIClientV15 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; @@ -9151,7 +8706,7 @@ index fc43c44a85a0fc6bf5f8c643bd120a16ce762914..ee86fd213d25682f9b6553ec7da99bc8 // Version 15. WKPageDecidePolicyForSpeechRecognitionPermissionRequestCallback decidePolicyForSpeechRecognitionPermissionRequest; -@@ -1583,6 +1586,7 @@ typedef struct WKPageUIClientV16 { +@@ -1584,6 +1587,7 @@ typedef struct WKPageUIClientV16 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; @@ -9159,7 +8714,7 @@ index fc43c44a85a0fc6bf5f8c643bd120a16ce762914..ee86fd213d25682f9b6553ec7da99bc8 // Version 15. WKPageDecidePolicyForSpeechRecognitionPermissionRequestCallback decidePolicyForSpeechRecognitionPermissionRequest; -@@ -1697,6 +1701,7 @@ typedef struct WKPageUIClientV17 { +@@ -1698,6 +1702,7 @@ typedef struct WKPageUIClientV17 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; @@ -9167,7 +8722,7 @@ index fc43c44a85a0fc6bf5f8c643bd120a16ce762914..ee86fd213d25682f9b6553ec7da99bc8 // Version 15. WKPageDecidePolicyForSpeechRecognitionPermissionRequestCallback decidePolicyForSpeechRecognitionPermissionRequest; -@@ -1811,6 +1816,7 @@ typedef struct WKPageUIClientV18 { +@@ -1812,6 +1817,7 @@ typedef struct WKPageUIClientV18 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; @@ -9175,7 +8730,7 @@ index fc43c44a85a0fc6bf5f8c643bd120a16ce762914..ee86fd213d25682f9b6553ec7da99bc8 // Version 15. WKPageDecidePolicyForSpeechRecognitionPermissionRequestCallback decidePolicyForSpeechRecognitionPermissionRequest; -@@ -1927,6 +1933,7 @@ typedef struct WKPageUIClientV19 { +@@ -1928,6 +1934,7 @@ typedef struct WKPageUIClientV19 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; @@ -9629,7 +9184,7 @@ index 0000000000000000000000000000000000000000..e0b1da48465c850f541532ed961d1b77 +WebKit::WebPageProxy* webkitBrowserInspectorCreateNewPageInContext(WebKitWebContext*); +void webkitBrowserInspectorQuitApplication(); diff --git a/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp b/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp -index ef24e41f2c62e77e701a6e2b698c60004eaf0789..fd332a5b07f1dea7792eeee3bd9b5efb48afc0c5 100644 +index c17d1921232a7956c2ff786e5b5dca603b5f46fa..59f740194668b1fe3d4f16b1127947224c628b19 100644 --- a/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp +++ b/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp @@ -101,6 +101,10 @@ private: @@ -9713,10 +9268,10 @@ 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 449a8b1247c7397cd9709e425f8824a90bc77404..b374e3f5104572d44a6fbbb8384d3031815a2202 100644 +index ea27692f82e791c65828f9e88b795f96a365fa09..f15ae92ac80fc785b9da9014ea0966e2a998ecc4 100644 --- a/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp +++ b/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp -@@ -39,6 +39,7 @@ +@@ -40,6 +40,7 @@ #include "WebContextMenuItem.h" #include "WebContextMenuItemData.h" #include "WebFrameProxy.h" @@ -9724,7 +9279,7 @@ index 449a8b1247c7397cd9709e425f8824a90bc77404..b374e3f5104572d44a6fbbb8384d3031 #include "WebKitAuthenticationRequestPrivate.h" #include "WebKitBackForwardListPrivate.h" #include "WebKitContextMenuClient.h" -@@ -154,6 +155,7 @@ enum { +@@ -155,6 +156,7 @@ enum { CLOSE, SCRIPT_DIALOG, @@ -9732,7 +9287,7 @@ index 449a8b1247c7397cd9709e425f8824a90bc77404..b374e3f5104572d44a6fbbb8384d3031 DECIDE_POLICY, PERMISSION_REQUEST, -@@ -524,6 +526,13 @@ GRefPtr WebKitWebViewClient::showOptionMenu(WebKitPopupMenu& p +@@ -529,6 +531,13 @@ GRefPtr WebKitWebViewClient::showOptionMenu(WebKitPopupMenu& p void WebKitWebViewClient::frameDisplayed(WKWPE::View&) { @@ -9746,7 +9301,7 @@ index 449a8b1247c7397cd9709e425f8824a90bc77404..b374e3f5104572d44a6fbbb8384d3031 { SetForScope inFrameDisplayedGuard(m_webView->priv->inFrameDisplayed, true); for (const auto& callback : m_webView->priv->frameDisplayedCallbacks) { -@@ -540,6 +549,13 @@ void WebKitWebViewClient::frameDisplayed(WKWPE::View&) +@@ -545,6 +554,13 @@ void WebKitWebViewClient::frameDisplayed(WKWPE::View&) } } @@ -9760,7 +9315,7 @@ index 449a8b1247c7397cd9709e425f8824a90bc77404..b374e3f5104572d44a6fbbb8384d3031 void WebKitWebViewClient::willStartLoad(WKWPE::View&) { webkitWebViewWillStartLoad(m_webView); -@@ -631,7 +647,7 @@ static gboolean webkitWebViewDecidePolicy(WebKitWebView*, WebKitPolicyDecision* +@@ -636,7 +652,7 @@ static gboolean webkitWebViewDecidePolicy(WebKitWebView*, WebKitPolicyDecision* static gboolean webkitWebViewPermissionRequest(WebKitWebView*, WebKitPermissionRequest* request) { @@ -9769,7 +9324,7 @@ index 449a8b1247c7397cd9709e425f8824a90bc77404..b374e3f5104572d44a6fbbb8384d3031 if (WEBKIT_IS_POINTER_LOCK_PERMISSION_REQUEST(request)) { webkit_permission_request_allow(request); return TRUE; -@@ -954,6 +970,10 @@ static void webkitWebViewConstructed(GObject* object) +@@ -959,6 +975,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); @@ -9780,7 +9335,7 @@ index 449a8b1247c7397cd9709e425f8824a90bc77404..b374e3f5104572d44a6fbbb8384d3031 webkitWebViewCreatePage(webView, WTFMove(configuration)); webkitWebContextWebViewCreated(priv->context.get(), webView); -@@ -2022,6 +2042,15 @@ static void webkit_web_view_class_init(WebKitWebViewClass* webViewClass) +@@ -2044,6 +2064,15 @@ static void webkit_web_view_class_init(WebKitWebViewClass* webViewClass) G_TYPE_BOOLEAN, 1, WEBKIT_TYPE_SCRIPT_DIALOG); @@ -9796,7 +9351,7 @@ index 449a8b1247c7397cd9709e425f8824a90bc77404..b374e3f5104572d44a6fbbb8384d3031 /** * WebKitWebView::decide-policy: * @web_view: the #WebKitWebView on which the signal is emitted -@@ -2812,6 +2841,23 @@ void webkitWebViewRunJavaScriptBeforeUnloadConfirm(WebKitWebView* webView, const +@@ -2834,6 +2863,23 @@ void webkitWebViewRunJavaScriptBeforeUnloadConfirm(WebKitWebView* webView, const webkit_script_dialog_unref(webView->priv->currentScriptDialog); } @@ -9821,7 +9376,7 @@ index 449a8b1247c7397cd9709e425f8824a90bc77404..b374e3f5104572d44a6fbbb8384d3031 { if (!webView->priv->currentScriptDialog) diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebViewPrivate.h b/Source/WebKit/UIProcess/API/glib/WebKitWebViewPrivate.h -index c80a5073b50ae7273985c9165cffc1d361a2ff03..7b1ce505325c735a0bf1c23f031ad1ee10639b91 100644 +index 4e16a2a088f6e48f43362850cc047da7de3c88ef..5ca5ae9572d2ed962d80a5dd67f8f379cd9e8117 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 @@ -9845,7 +9400,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 92218ae7cac5f31db31d9ecde99094331a465ebf..e9d4e786b8640d93fb35187cba96496f6bbb1d32 100644 +index 9a36fe28ce7a5aac1b45350e3fc9e72b1a00dac1..cc661fa57e7ec1469e1ec003d783028d59836f06 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 @@ -9870,7 +9425,7 @@ index 92218ae7cac5f31db31d9ecde99094331a465ebf..e9d4e786b8640d93fb35187cba96496f 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 3cafc1ed805da8f947dc8c8c28327116f1d3c8a4..a8ed97eb566e4f966f85b03ea55d8dfccc608e58 100644 +index ae0d906fb4bb5ff323f1e494f866c1f24b4d1e0e..e62064b583cef05f6d7c5a47b0130a4fc7e21ec8 100644 --- a/Source/WebKit/UIProcess/API/gtk/PageClientImpl.h +++ b/Source/WebKit/UIProcess/API/gtk/PageClientImpl.h @@ -104,7 +104,7 @@ private: @@ -9983,7 +9538,7 @@ 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 b58f8dd27e62f047857ed2f36fcd6dbd80e8038d..897d4f038d6dcbd9a4a37dbdda91fae46f56f521 100644 +index da61eac252a18988d3d70a7f842e75d6b35c9b0d..3a2f4b339811c9cedd0de342d7593130699b0026 100644 --- a/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBase.cpp +++ b/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBase.cpp @@ -2884,6 +2884,11 @@ void webkitWebViewBaseResetClickCounter(WebKitWebViewBase* webkitWebViewBase) @@ -10014,7 +9569,7 @@ index b58f8dd27e62f047857ed2f36fcd6dbd80e8038d..897d4f038d6dcbd9a4a37dbdda91fae4 #if !USE(GTK4) diff --git a/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBasePrivate.h b/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBasePrivate.h -index 8a71b1d7d9b9ef8bfe4a6935e3788e40a5fe6d5d..dad4a47725cc3a5d9d3fd6b1f4ec23c866cfe973 100644 +index 9553631aa4fc49456ae6061abae2a896da9a6988..5b4fd77cb616b5ade74889bb35ff75299272b005 100644 --- a/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBasePrivate.h +++ b/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBasePrivate.h @@ -27,6 +27,7 @@ @@ -10024,7 +9579,7 @@ index 8a71b1d7d9b9ef8bfe4a6935e3788e40a5fe6d5d..dad4a47725cc3a5d9d3fd6b1f4ec23c8 +#include "AcceleratedBackingStore.h" #include "APIPageConfiguration.h" #include "InputMethodState.h" - #include "RendererBufferFormat.h" + #include "RendererBufferDescription.h" @@ -104,7 +105,7 @@ void webkitWebViewBaseStartDrag(WebKitWebViewBase*, WebCore::SelectionData&&, Op void webkitWebViewBaseDidPerformDragControllerAction(WebKitWebViewBase*); #endif @@ -10037,7 +9592,7 @@ index 8a71b1d7d9b9ef8bfe4a6935e3788e40a5fe6d5d..dad4a47725cc3a5d9d3fd6b1f4ec23c8 @@ -145,3 +146,5 @@ void webkitWebViewBaseSetPlugID(WebKitWebViewBase*, const String&); #endif - WebKit::RendererBufferFormat webkitWebViewBaseGetRendererBufferFormat(WebKitWebViewBase*); + WebKit::RendererBufferDescription webkitWebViewBaseGetRendererBufferDescription(WebKitWebViewBase*); + +WebKit::AcceleratedBackingStore* webkitWebViewBaseGetAcceleratedBackingStore(WebKitWebViewBase*); diff --git a/Source/WebKit/UIProcess/API/wpe/APIViewClient.h b/Source/WebKit/UIProcess/API/wpe/APIViewClient.h @@ -10067,7 +9622,7 @@ index 9091ae5198e765c2cfe0584d121afe4f88df3c0e..b0efedec419673ef2bfd0fd79406774e 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 ab67db0aab214edea4f3c7ff80e1fa27c7c0a95b..069d41ca9eb33fdc851fc96a7b260ceca9fdfa9a 100644 +index 0cc3b527fb27dcae943b6bae522ad5fd2925843c..b2c4aea4996b73da224f9f320522b3f74567ed08 100644 --- a/Source/WebKit/UIProcess/API/wpe/PageClientImpl.cpp +++ b/Source/WebKit/UIProcess/API/wpe/PageClientImpl.cpp @@ -35,9 +35,12 @@ @@ -10096,7 +9651,7 @@ index ab67db0aab214edea4f3c7ff80e1fa27c7c0a95b..069d41ca9eb33fdc851fc96a7b260cec namespace WebKit { WTF_MAKE_TZONE_ALLOCATED_IMPL(PageClientImpl); -@@ -302,14 +311,14 @@ Ref PageClientImpl::createContextMenuProxy(WebPageProxy& pa +@@ -305,14 +314,14 @@ Ref PageClientImpl::createContextMenuProxy(WebPageProxy& pa } #endif @@ -10115,7 +9670,7 @@ index ab67db0aab214edea4f3c7ff80e1fa27c7c0a95b..069d41ca9eb33fdc851fc96a7b260cec } RefPtr PageClientImpl::createDateTimePicker(WebPageProxy& page) -@@ -551,6 +560,37 @@ void PageClientImpl::selectionDidChange() +@@ -554,6 +563,37 @@ void PageClientImpl::selectionDidChange() m_view.selectionDidChange(); } @@ -10153,7 +9708,7 @@ index ab67db0aab214edea4f3c7ff80e1fa27c7c0a95b..069d41ca9eb33fdc851fc96a7b260cec WebKitWebResourceLoadManager* PageClientImpl::webResourceLoadManager() { return m_view.webResourceLoadManager(); -@@ -561,4 +601,11 @@ void PageClientImpl::callAfterNextPresentationUpdate(CompletionHandler&& +@@ -564,4 +604,11 @@ void PageClientImpl::callAfterNextPresentationUpdate(CompletionHandler&& m_view.callAfterNextPresentationUpdate(WTFMove(callback)); } @@ -10166,10 +9721,10 @@ index ab67db0aab214edea4f3c7ff80e1fa27c7c0a95b..069d41ca9eb33fdc851fc96a7b260cec + } // namespace WebKit diff --git a/Source/WebKit/UIProcess/API/wpe/PageClientImpl.h b/Source/WebKit/UIProcess/API/wpe/PageClientImpl.h -index 000b7c9950785f55fdfdcaa186026903a506c79e..2f0f821890ab40bcfa68b105710484674600a2db 100644 +index 8c257afe81fd1719434e74a3deea50991132b55b..14fd72fef0cc90464212f41770577c0c645bef9e 100644 --- a/Source/WebKit/UIProcess/API/wpe/PageClientImpl.h +++ b/Source/WebKit/UIProcess/API/wpe/PageClientImpl.h -@@ -184,9 +184,15 @@ private: +@@ -185,9 +185,15 @@ private: void didChangeWebPageID() const override; void selectionDidChange() override; @@ -10184,8 +9739,8 @@ index 000b7c9950785f55fdfdcaa186026903a506c79e..2f0f821890ab40bcfa68b10571048467 +#endif + WKWPE::View& m_view; + DefaultUndoController m_undoController; #if ENABLE(FULLSCREEN_API) - std::unique_ptr m_fullscreenClientForTesting; diff --git a/Source/WebKit/UIProcess/API/wpe/WebKitBrowserInspector.h b/Source/WebKit/UIProcess/API/wpe/WebKitBrowserInspector.h new file mode 100644 index 0000000000000000000000000000000000000000..273c5105cdf1638955cea01128c9bbab3e64436c @@ -10274,7 +9829,7 @@ index 0000000000000000000000000000000000000000..273c5105cdf1638955cea01128c9bbab + +#endif diff --git a/Source/WebKit/UIProcess/API/wpe/WebKitWebViewBackend.cpp b/Source/WebKit/UIProcess/API/wpe/WebKitWebViewBackend.cpp -index 763bda5b29304f7ed7133c0a8158e6c8b94c5ea1..8ed962e8c1af62b9b73a68348d0d88765429861d 100644 +index 08e754111893d5f3c5dac7502b48ad2b3c760540..283f05d35729aee22659ab4b661686159ff9ad3b 100644 --- a/Source/WebKit/UIProcess/API/wpe/WebKitWebViewBackend.cpp +++ b/Source/WebKit/UIProcess/API/wpe/WebKitWebViewBackend.cpp @@ -54,6 +54,7 @@ struct _WebKitWebViewBackend { @@ -10303,8 +9858,8 @@ index 763bda5b29304f7ed7133c0a8158e6c8b94c5ea1..8ed962e8c1af62b9b73a68348d0d8876 +} + namespace WTF { - - template <> WebKitWebViewBackend* refGPtr(WebKitWebViewBackend* ptr) + WTF_DEFINE_GREF_TRAITS(WebKitWebViewBackend, webkitWebViewBackendRef, webkitWebViewBackendUnref) + } diff --git a/Source/WebKit/UIProcess/API/wpe/WebKitWebViewBackend.h b/Source/WebKit/UIProcess/API/wpe/WebKitWebViewBackend.h index 16dcc1f69c38cd8ad630bc49d6d69feaa3aa811e..98677028c19c12c3b6d513bb5e45375a1528fe8a 100644 --- a/Source/WebKit/UIProcess/API/wpe/WebKitWebViewBackend.h @@ -10335,10 +9890,10 @@ index 16dcc1f69c38cd8ad630bc49d6d69feaa3aa811e..98677028c19c12c3b6d513bb5e45375a #endif /* WebKitWebViewBackend_h */ diff --git a/Source/WebKit/UIProcess/API/wpe/WebKitWebViewBackendPrivate.h b/Source/WebKit/UIProcess/API/wpe/WebKitWebViewBackendPrivate.h -index e4b92ace1531090ae38a7aec3d3d4febf19aee84..b66b573f9148c39c5ce2738add6cd01a9a352be8 100644 +index 8987c95d8493e446e871545fc84306b279bb39b6..3e12d7f309f5bb76bde712b383d7c7c9bd682f26 100644 --- a/Source/WebKit/UIProcess/API/wpe/WebKitWebViewBackendPrivate.h +++ b/Source/WebKit/UIProcess/API/wpe/WebKitWebViewBackendPrivate.h -@@ -31,3 +31,5 @@ template <> void derefGPtr(WebKitWebViewBackend* ptr); +@@ -28,3 +28,5 @@ WTF_DECLARE_GREF_TRAITS(WebKitWebViewBackend) } void webkitWebViewBackendUnref(WebKitWebViewBackend*); @@ -10361,10 +9916,10 @@ index 0fa1f2e970ed0c0232df9b9c7b8b4bcd0ceac655..d68c6747edc699de7d84bae2a63b927d 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 d0a40e3e9f4d59f894c0574926ee13b2d83767ab..3dfd8e059273d2a14494b76892e5abfe66475c2e 100644 +index b9346fab9e82ff29fc881f04b12cafa7af720877..2207a280d491df89b5832210ed4a7cfe541eb53d 100644 --- a/Source/WebKit/UIProcess/Automation/WebAutomationSession.h +++ b/Source/WebKit/UIProcess/Automation/WebAutomationSession.h -@@ -287,6 +287,8 @@ public: +@@ -289,6 +289,8 @@ public: void didDestroyFrame(WebCore::FrameIdentifier); @@ -10373,7 +9928,7 @@ index d0a40e3e9f4d59f894c0574926ee13b2d83767ab..3dfd8e059273d2a14494b76892e5abfe RefPtr webPageProxyForHandle(const String&); String handleForWebFrameID(std::optional); String handleForWebPageProxy(const WebPageProxy&); -@@ -338,7 +340,6 @@ private: +@@ -340,7 +342,6 @@ private: // Get base64-encoded PNG data from a bitmap. static std::optional platformGetBase64EncodedPNGData(WebCore::ShareableBitmap::Handle&&); @@ -10382,7 +9937,7 @@ index d0a40e3e9f4d59f894c0574926ee13b2d83767ab..3dfd8e059273d2a14494b76892e5abfe // Save base64-encoded file contents to a local file path and return the path. // This reuses the basename of the remote file path so that the filename exposed to DOM API remains the same. diff --git a/Source/WebKit/UIProcess/AuxiliaryProcessProxy.cpp b/Source/WebKit/UIProcess/AuxiliaryProcessProxy.cpp -index d5186484fee73b83dbcd611a9695d92a0a6f9579..e26cea229aeb1f6aad23f1265baccfe67e508a13 100644 +index 536a0b78e8b1ab110093fea33384e7bad528917b..ca4f5944f90868886bba20411e47b18598cc50a0 100644 --- a/Source/WebKit/UIProcess/AuxiliaryProcessProxy.cpp +++ b/Source/WebKit/UIProcess/AuxiliaryProcessProxy.cpp @@ -173,7 +173,11 @@ void AuxiliaryProcessProxy::getLaunchOptions(ProcessLauncher::LaunchOptions& lau @@ -10559,10 +10114,10 @@ index 89d125f7742f81ead8c50f218ecb1771b8000636..baa6cf58ad502c6c033ee6293a6cc8d4 namespace WebKit { diff --git a/Source/WebKit/UIProcess/Cocoa/UIDelegate.h b/Source/WebKit/UIProcess/Cocoa/UIDelegate.h -index c633008451c5719866d79300c49d60f979ffb581..9cc370de9b38f70970a6ca11a43651cbb2549529 100644 +index 13a5af461910a6875d18466115c3dea34891a0fa..44c5376282be9bc8ba49fe830c1c0a5c8b9c8103 100644 --- a/Source/WebKit/UIProcess/Cocoa/UIDelegate.h +++ b/Source/WebKit/UIProcess/Cocoa/UIDelegate.h -@@ -103,6 +103,7 @@ private: +@@ -105,6 +105,7 @@ private: void runJavaScriptAlert(WebPageProxy&, const WTF::String&, WebFrameProxy*, FrameInfoData&&, Function&& completionHandler) final; void runJavaScriptConfirm(WebPageProxy&, const WTF::String&, WebFrameProxy*, FrameInfoData&&, Function&& completionHandler) final; void runJavaScriptPrompt(WebPageProxy&, const WTF::String&, const WTF::String&, WebFrameProxy*, FrameInfoData&&, Function&&) final; @@ -10570,7 +10125,7 @@ index c633008451c5719866d79300c49d60f979ffb581..9cc370de9b38f70970a6ca11a43651cb 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; -@@ -226,6 +227,7 @@ private: +@@ -228,6 +229,7 @@ private: bool webViewRunJavaScriptAlertPanelWithMessageInitiatedByFrameCompletionHandler : 1; bool webViewRunJavaScriptConfirmPanelWithMessageInitiatedByFrameCompletionHandler : 1; bool webViewRunJavaScriptTextInputPanelWithPromptDefaultTextInitiatedByFrameCompletionHandler : 1; @@ -10579,10 +10134,10 @@ index c633008451c5719866d79300c49d60f979ffb581..9cc370de9b38f70970a6ca11a43651cb bool webViewRequestStorageAccessPanelForDomainUnderCurrentDomainForQuirkDomainsCompletionHandler : 1; bool webViewRunBeforeUnloadConfirmPanelWithMessageInitiatedByFrameCompletionHandler : 1; diff --git a/Source/WebKit/UIProcess/Cocoa/UIDelegate.mm b/Source/WebKit/UIProcess/Cocoa/UIDelegate.mm -index 6c934950b89e32e9ff58992497a313062658c235..c3f1b8d1193ae36efe9e5700f85eee4e86e8c548 100644 +index f9e39d7adef59446ddb76696835a633956a85f93..3c2fdd7b27a6da2cdc75c4754e41f33d18226073 100644 --- a/Source/WebKit/UIProcess/Cocoa/UIDelegate.mm +++ b/Source/WebKit/UIProcess/Cocoa/UIDelegate.mm -@@ -135,6 +135,7 @@ void UIDelegate::setDelegate(id delegate) +@@ -136,6 +136,7 @@ void UIDelegate::setDelegate(id delegate) m_delegateMethods.webViewRunJavaScriptAlertPanelWithMessageInitiatedByFrameCompletionHandler = [delegate respondsToSelector:@selector(webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:)]; m_delegateMethods.webViewRunJavaScriptConfirmPanelWithMessageInitiatedByFrameCompletionHandler = [delegate respondsToSelector:@selector(webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler:)]; m_delegateMethods.webViewRunJavaScriptTextInputPanelWithPromptDefaultTextInitiatedByFrameCompletionHandler = [delegate respondsToSelector:@selector(webView:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler:)]; @@ -10590,7 +10145,7 @@ index 6c934950b89e32e9ff58992497a313062658c235..c3f1b8d1193ae36efe9e5700f85eee4e 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:)]; -@@ -500,6 +501,15 @@ void UIDelegate::UIClient::runJavaScriptPrompt(WebPageProxy& page, const WTF::St +@@ -502,6 +503,15 @@ void UIDelegate::UIClient::runJavaScriptPrompt(WebPageProxy& page, const WTF::St }).get()]; } @@ -10607,10 +10162,10 @@ index 6c934950b89e32e9ff58992497a313062658c235..c3f1b8d1193ae36efe9e5700f85eee4e { RefPtr uiDelegate = m_uiDelegate.get(); diff --git a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm -index 70f71e49666eb0bc9a732c5893d27179b051bd72..a6c273ea6793f213aadc64485fe7c1fb23854b72 100644 +index 11ff0246d535faf1d75779e3b2959b36096e8793..6282913dea0fc0c4ece3e729f6a4fa27edc4c04e 100644 --- a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm +++ b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm -@@ -43,7 +43,9 @@ +@@ -44,7 +44,9 @@ #import "NativeWebKeyboardEvent.h" #import "NativeWebMouseEvent.h" #import "NavigationState.h" @@ -10620,9 +10175,9 @@ index 70f71e49666eb0bc9a732c5893d27179b051bd72..a6c273ea6793f213aadc64485fe7c1fb #import "PlatformXRSystem.h" #import "PlaybackSessionManagerProxy.h" #import "RemoteLayerTreeTransaction.h" -@@ -351,11 +353,86 @@ bool WebPageProxy::scrollingUpdatesDisabledForTesting() +@@ -356,11 +358,86 @@ bool WebPageProxy::scrollingUpdatesDisabledForTesting() - void WebPageProxy::startDrag(const DragItem& dragItem, ShareableBitmap::Handle&& dragImageHandle, const std::optional& elementID) + void WebPageProxy::startDrag(const DragItem& dragItem, ShareableBitmap::Handle&& dragImageHandle, const std::optional& nodeID) { + if (m_interceptDrags) { + NSPasteboard *pasteboard = [NSPasteboard pasteboardWithName: m_overrideDragPasteboardName.createNSString().get()]; @@ -10656,7 +10211,7 @@ index 70f71e49666eb0bc9a732c5893d27179b051bd72..a6c273ea6793f213aadc64485fe7c1fb + } + if (RefPtr pageClient = this->pageClient()) - pageClient->startDrag(dragItem, WTFMove(dragImageHandle), elementID); + pageClient->startDrag(dragItem, WTFMove(dragImageHandle), nodeID); } -#endif @@ -10709,10 +10264,10 @@ index 70f71e49666eb0bc9a732c5893d27179b051bd72..a6c273ea6793f213aadc64485fe7c1fb #if ENABLE(ATTACHMENT_ELEMENT) diff --git a/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm -index 3b6663cb77c2c20e2e6822bfdbf822cea3022fdc..d0e67aa4714f4f8e01c3c2e1ef3fd1adc6274d20 100644 +index c742dd123d0df51dda15233b3b5b84d148f23b94..e8161d0567c67f9319e8ba54931cd40dcc43dd6d 100644 --- a/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm +++ b/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm -@@ -442,7 +442,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END +@@ -443,7 +443,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END auto screenProperties = WebCore::collectScreenProperties(); parameters.screenProperties = WTFMove(screenProperties); #if PLATFORM(MAC) @@ -10721,7 +10276,7 @@ index 3b6663cb77c2c20e2e6822bfdbf822cea3022fdc..d0e67aa4714f4f8e01c3c2e1ef3fd1ad #endif #if PLATFORM(VISION) -@@ -841,8 +841,8 @@ void WebProcessPool::registerNotificationObservers() +@@ -850,8 +850,8 @@ void WebProcessPool::registerNotificationObservers() }]; m_scrollerStyleNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSPreferredScrollerStyleDidChangeNotification object:nil queue:[NSOperationQueue currentQueue] usingBlock:^(NSNotification *notification) { @@ -10733,7 +10288,7 @@ index 3b6663cb77c2c20e2e6822bfdbf822cea3022fdc..d0e67aa4714f4f8e01c3c2e1ef3fd1ad 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 f356f65a1515e9ed17e51d4bde50a8c0758fd6e1..0caab4689c42a89d1d5060a3bdf9a0502e73b8ab 100644 +index c25814a7b7c26ad90c1b51ff10d334200da9472b..d9b6654e62a6766824a6df45b73408c3141e2893 100644 --- a/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp +++ b/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp @@ -33,6 +33,7 @@ @@ -10744,7 +10299,7 @@ index f356f65a1515e9ed17e51d4bde50a8c0758fd6e1..0caab4689c42a89d1d5060a3bdf9a050 #include "WebPageProxy.h" #include "WebPreferences.h" #include "WebProcessPool.h" -@@ -40,15 +41,26 @@ +@@ -40,9 +41,15 @@ #include #include #include @@ -10760,18 +10315,7 @@ index f356f65a1515e9ed17e51d4bde50a8c0758fd6e1..0caab4689c42a89d1d5060a3bdf9a050 #endif #if USE(GLIB_EVENT_LOOP) - #include - #endif - -+#if PLATFORM(WIN) -+#include -+#include -+#endif -+ - namespace WebKit { - using namespace WebCore; - -@@ -182,6 +194,11 @@ void DrawingAreaProxyCoordinatedGraphics::deviceScaleFactorDidChange(CompletionH +@@ -182,6 +189,11 @@ void DrawingAreaProxyCoordinatedGraphics::deviceScaleFactorDidChange(CompletionH sendWithAsyncReply(Messages::DrawingArea::SetDeviceScaleFactor(page()->deviceScaleFactor()), WTFMove(completionHandler)); } @@ -10783,7 +10327,7 @@ index f356f65a1515e9ed17e51d4bde50a8c0758fd6e1..0caab4689c42a89d1d5060a3bdf9a050 void DrawingAreaProxyCoordinatedGraphics::setBackingStoreIsDiscardable(bool isBackingStoreDiscardable) { #if !PLATFORM(WPE) -@@ -243,6 +260,59 @@ void DrawingAreaProxyCoordinatedGraphics::updateAcceleratedCompositingMode(uint6 +@@ -243,6 +255,44 @@ void DrawingAreaProxyCoordinatedGraphics::updateAcceleratedCompositingMode(uint6 updateAcceleratedCompositingMode(layerTreeContext); } @@ -10824,26 +10368,11 @@ index f356f65a1515e9ed17e51d4bde50a8c0758fd6e1..0caab4689c42a89d1d5060a3bdf9a050 + protectedPage()->inspectorController().didPaint(WTFMove(skImage)); +} +#endif // PLATFORM(GTK) -+ -+#if PLATFORM(WIN) -+void DrawingAreaProxyCoordinatedGraphics::captureFrame() -+{ -+ if (!m_backingStore) -+ return; -+ auto surface = m_backingStore->surface(); -+ if (!surface) -+ return; -+ auto image = surface->makeImageSnapshot(); -+ if (!image) -+ return; -+ protectedPage()->inspectorController().didPaint(WTFMove(image)); -+} -+#endif // PLATFORM(WIN) + bool DrawingAreaProxyCoordinatedGraphics::alwaysUseCompositing() const { if (!page()) -@@ -310,6 +380,12 @@ void DrawingAreaProxyCoordinatedGraphics::didUpdateGeometry() +@@ -310,6 +360,12 @@ void DrawingAreaProxyCoordinatedGraphics::didUpdateGeometry() // we need to resend the new size here. if (m_lastSentSize != size()) sendUpdateGeometry(); @@ -10983,7 +10512,7 @@ index bdc3efe299296ef6ef10d32672e22fba05ca230c..a24e9c7e4b0da65099326b6921ed03b4 // This can cause the DownloadProxy object to be deleted. if (RefPtr downloadProxyMap = m_downloadProxyMap.get()) diff --git a/Source/WebKit/UIProcess/Downloads/DownloadProxy.h b/Source/WebKit/UIProcess/Downloads/DownloadProxy.h -index 9a92a8cde3b5d1da0fbbf5fe7c549cebb8a7f2f7..9ce201ca2d7aa002c7bd389f1fe03edfb306df5d 100644 +index 8a40cba30a1f6ea119c5c77fa328491fabce1135..ccf3059e5487ffe2750ec7478082bab307dadd70 100644 --- a/Source/WebKit/UIProcess/Downloads/DownloadProxy.h +++ b/Source/WebKit/UIProcess/Downloads/DownloadProxy.h @@ -166,6 +166,7 @@ private: @@ -10995,10 +10524,10 @@ index 9a92a8cde3b5d1da0fbbf5fe7c549cebb8a7f2f7..9ce201ca2d7aa002c7bd389f1fe03edf } // namespace WebKit diff --git a/Source/WebKit/UIProcess/DrawingAreaProxy.h b/Source/WebKit/UIProcess/DrawingAreaProxy.h -index c30c32f55aff3552bb05112dd53b9b8b62dd4a3f..32417be5f8cf613f58db900ce615518751b1509a 100644 +index 75c5092b62c6911e0c2417549189667cd9e2cfb1..82d85bdbae12db42d583ebbce6c3fc1251239df9 100644 --- a/Source/WebKit/UIProcess/DrawingAreaProxy.h +++ b/Source/WebKit/UIProcess/DrawingAreaProxy.h -@@ -94,6 +94,7 @@ public: +@@ -96,6 +96,7 @@ public: const WebCore::IntSize& size() const { return m_size; } bool setSize(const WebCore::IntSize&, const WebCore::IntSize& scrollOffset = { }); @@ -11008,10 +10537,10 @@ index c30c32f55aff3552bb05112dd53b9b8b62dd4a3f..32417be5f8cf613f58db900ce6155187 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..c7a4a2f5fbd8c99d2b53fe0d3b1645c865d46634 +index 0000000000000000000000000000000000000000..3733d0c6e8875f325bd249232b8f8205256400f8 --- /dev/null +++ b/Source/WebKit/UIProcess/Inspector/Agents/InspectorScreencastAgent.cpp -@@ -0,0 +1,326 @@ +@@ -0,0 +1,341 @@ +/* + * Copyright (C) 2020 Microsoft Corporation. + * @@ -11069,6 +10598,10 @@ index 0000000000000000000000000000000000000000..c7a4a2f5fbd8c99d2b53fe0d3b1645c8 +#include +#endif + ++#if PLATFORM(WIN) ++#include "DrawingAreaProxyWC.h" ++#endif ++ +WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN + +namespace WebKit { @@ -11089,7 +10622,7 @@ index 0000000000000000000000000000000000000000..c7a4a2f5fbd8c99d2b53fe0d3b1645c8 +{ +} + -+void InspectorScreencastAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*) ++void InspectorScreencastAgent::didCreateFrontendAndBackend() +{ +} + @@ -11268,7 +10801,7 @@ index 0000000000000000000000000000000000000000..c7a4a2f5fbd8c99d2b53fe0d3b1645c8 + if (!m_encoder && !m_screencast) + return; + -+ RunLoop::main().dispatchAfter(Seconds(1.0 / ScreencastEncoder::fps), [agent = WeakPtr { this }]() mutable { ++ RunLoop::mainSingleton().dispatchAfter(Seconds(1.0 / ScreencastEncoder::fps), [agent = WeakPtr { this }]() mutable { + if (!agent) + return; + if (!agent->m_page.hasPageClient()) @@ -11324,7 +10857,7 @@ index 0000000000000000000000000000000000000000..c7a4a2f5fbd8c99d2b53fe0d3b1645c8 +} +#endif + -+#if PLATFORM(GTK) || PLATFORM(WIN) ++#if PLATFORM(GTK) +void InspectorScreencastAgent::encodeFrame() +{ + if (!m_encoder && !m_screencast) @@ -11335,12 +10868,23 @@ index 0000000000000000000000000000000000000000..c7a4a2f5fbd8c99d2b53fe0d3b1645c8 +} +#endif + ++#if PLATFORM(WIN) ++void InspectorScreencastAgent::encodeFrame() ++{ ++ if (!m_encoder && !m_screencast) ++ return; ++ ++ if (auto* drawingArea = m_page.drawingArea()) ++ static_cast(drawingArea)->captureFrame(); ++} ++#endif ++ +} // namespace WebKit + +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..852c199da066320f730226653a47c8e5185dc560 +index 0000000000000000000000000000000000000000..e48ba1cc781e24afaa765661c713cef19ea2215b --- /dev/null +++ b/Source/WebKit/UIProcess/Inspector/Agents/InspectorScreencastAgent.h @@ -0,0 +1,106 @@ @@ -11406,12 +10950,12 @@ index 0000000000000000000000000000000000000000..852c199da066320f730226653a47c8e5 + +class InspectorScreencastAgent : public Inspector::InspectorAgentBase, public Inspector::ScreencastBackendDispatcherHandler, public CanMakeWeakPtr { + WTF_MAKE_NONCOPYABLE(InspectorScreencastAgent); -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(InspectorScreencastAgent); +public: + InspectorScreencastAgent(Inspector::BackendDispatcher& backendDispatcher, Inspector::FrontendRouter& frontendRouter, WebPageProxy& page); + ~InspectorScreencastAgent() override; + -+ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void didCreateFrontendAndBackend() override; + void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; + +#if USE(SKIA) @@ -11452,7 +10996,7 @@ index 0000000000000000000000000000000000000000..852c199da066320f730226653a47c8e5 +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/Inspector/Agents/ScreencastEncoder.cpp b/Source/WebKit/UIProcess/Inspector/Agents/ScreencastEncoder.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..203cedeea9cbec6964fd297884e03ef791632fe5 +index 0000000000000000000000000000000000000000..177f7f7c9a117bc5028f85b766427739e2e9cbc9 --- /dev/null +++ b/Source/WebKit/UIProcess/Inspector/Agents/ScreencastEncoder.cpp @@ -0,0 +1,398 @@ @@ -11590,7 +11134,7 @@ index 0000000000000000000000000000000000000000..203cedeea9cbec6964fd297884e03ef7 + +class ScreencastEncoder::VPXFrame { + WTF_MAKE_NONCOPYABLE(VPXFrame); -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(ScreencastEncoder::VPXFrame); +public: +#if USE(SKIA) + explicit VPXFrame(sk_sp&& surface) @@ -11844,7 +11388,7 @@ index 0000000000000000000000000000000000000000..203cedeea9cbec6964fd297884e03ef7 + + flushLastFrame(); + m_vpxCodec->finishAsync([protectRef = Ref { *this }, callback = WTFMove(callback)] () mutable { -+ RunLoop::main().dispatch([callback = WTFMove(callback)] { ++ RunLoop::mainSingleton().dispatch([callback = WTFMove(callback)] { + callback(); + }); + }); @@ -11856,7 +11400,7 @@ index 0000000000000000000000000000000000000000..203cedeea9cbec6964fd297884e03ef7 +WTF_ALLOW_UNSAFE_BUFFER_USAGE_END diff --git a/Source/WebKit/UIProcess/Inspector/Agents/ScreencastEncoder.h b/Source/WebKit/UIProcess/Inspector/Agents/ScreencastEncoder.h new file mode 100644 -index 0000000000000000000000000000000000000000..433af017b68b71cfb68c3ebcc0bd2aeb9efc40f7 +index 0000000000000000000000000000000000000000..a414e07b64a5b6ee58a777822761c87ec05605ac --- /dev/null +++ b/Source/WebKit/UIProcess/Inspector/Agents/ScreencastEncoder.h @@ -0,0 +1,80 @@ @@ -11904,7 +11448,7 @@ index 0000000000000000000000000000000000000000..433af017b68b71cfb68c3ebcc0bd2aeb + +class ScreencastEncoder : public ThreadSafeRefCounted { + WTF_MAKE_NONCOPYABLE(ScreencastEncoder); -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(ScreencastEncoder); +public: + static constexpr int fps = 25; + @@ -12169,10 +11713,10 @@ 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 9e1e41b6a796082231a32b04fe8b13270b02eb3e..58b4d514f7a06b7fb7cdae111f99a9009f018187 100644 +index 187e59d58ba020cbac2246acc669ef95cafd75b4..bb2498bdb082721ce539dd935b5aebada963b1ba 100644 --- a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp +++ b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp -@@ -26,13 +26,23 @@ +@@ -26,14 +26,22 @@ #include "config.h" #include "WebPageInspectorController.h" @@ -12189,14 +11733,13 @@ index 9e1e41b6a796082231a32b04fe8b13270b02eb3e..58b4d514f7a06b7fb7cdae111f99a900 +#include "WebPageInspectorInputAgent.h" #include "WebPageInspectorTarget.h" #include "WebPageProxy.h" -+#include "WebPreferences.h" + #include "WebsiteDataStore.h" +#include -+#include +#include #include #include #include -@@ -52,34 +62,115 @@ static String getTargetID(const ProvisionalPageProxy& provisionalPage) +@@ -53,34 +61,115 @@ static String getTargetID(const ProvisionalPageProxy& provisionalPage) WTF_MAKE_TZONE_ALLOCATED_IMPL(WebPageInspectorController); @@ -12319,7 +11862,7 @@ index 9e1e41b6a796082231a32b04fe8b13270b02eb3e..58b4d514f7a06b7fb7cdae111f99a900 } bool WebPageInspectorController::hasLocalFrontend() const -@@ -93,6 +184,17 @@ void WebPageInspectorController::connectFrontend(Inspector::FrontendChannel& fro +@@ -94,6 +183,14 @@ void WebPageInspectorController::connectFrontend(Inspector::FrontendChannel& fro bool connectingFirstFrontend = !m_frontendRouter->hasFrontends(); @@ -12330,14 +11873,11 @@ index 9e1e41b6a796082231a32b04fe8b13270b02eb3e..58b4d514f7a06b7fb7cdae111f99a900 + disconnectAllFrontends(); + connectingFirstFrontend = true; + } -+ -+ if (connectingFirstFrontend) -+ adjustPageSettings(); + m_frontendRouter->connectFrontend(frontendChannel); if (connectingFirstFrontend) -@@ -112,8 +214,10 @@ void WebPageInspectorController::disconnectFrontend(FrontendChannel& frontendCha +@@ -113,8 +210,10 @@ void WebPageInspectorController::disconnectFrontend(FrontendChannel& frontendCha m_frontendRouter->disconnectFrontend(frontendChannel); bool disconnectingLastFrontend = !m_frontendRouter->hasFrontends(); @@ -12349,7 +11889,7 @@ index 9e1e41b6a796082231a32b04fe8b13270b02eb3e..58b4d514f7a06b7fb7cdae111f99a900 Ref inspectedPage = m_inspectedPage.get(); inspectedPage->didChangeInspectorFrontendCount(m_frontendRouter->frontendCount()); -@@ -137,6 +241,8 @@ void WebPageInspectorController::disconnectAllFrontends() +@@ -138,6 +237,8 @@ void WebPageInspectorController::disconnectAllFrontends() // Disconnect any remaining remote frontends. m_frontendRouter->disconnectAllFrontends(); @@ -12358,7 +11898,7 @@ index 9e1e41b6a796082231a32b04fe8b13270b02eb3e..58b4d514f7a06b7fb7cdae111f99a900 Ref inspectedPage = m_inspectedPage.get(); inspectedPage->didChangeInspectorFrontendCount(m_frontendRouter->frontendCount()); -@@ -165,6 +271,66 @@ void WebPageInspectorController::setIndicating(bool indicating) +@@ -166,6 +267,66 @@ void WebPageInspectorController::setIndicating(bool indicating) } #endif @@ -12425,7 +11965,7 @@ index 9e1e41b6a796082231a32b04fe8b13270b02eb3e..58b4d514f7a06b7fb7cdae111f99a900 void WebPageInspectorController::createInspectorTarget(const String& targetId, Inspector::InspectorTargetType type) { addTarget(InspectorTargetProxy::create(protectedInspectedPage(), targetId, type)); -@@ -184,6 +350,52 @@ void WebPageInspectorController::sendMessageToInspectorFrontend(const String& ta +@@ -185,6 +346,52 @@ void WebPageInspectorController::sendMessageToInspectorFrontend(const String& ta checkedTargetAgent()->sendMessageFromTargetToFrontend(targetId, message); } @@ -12478,7 +12018,7 @@ index 9e1e41b6a796082231a32b04fe8b13270b02eb3e..58b4d514f7a06b7fb7cdae111f99a900 bool WebPageInspectorController::shouldPauseLoading(const ProvisionalPageProxy& provisionalPage) const { if (!m_frontendRouter->hasFrontends()) -@@ -203,7 +415,7 @@ void WebPageInspectorController::setContinueLoadingCallback(const ProvisionalPag +@@ -204,7 +411,7 @@ void WebPageInspectorController::setContinueLoadingCallback(const ProvisionalPag void WebPageInspectorController::didCreateProvisionalPage(ProvisionalPageProxy& provisionalPage) { @@ -12487,38 +12027,8 @@ index 9e1e41b6a796082231a32b04fe8b13270b02eb3e..58b4d514f7a06b7fb7cdae111f99a900 } void WebPageInspectorController::willDestroyProvisionalPage(const ProvisionalPageProxy& provisionalPage) -@@ -288,4 +500,29 @@ void WebPageInspectorController::browserExtensionsDisabled(HashSet&& ext - enabledBrowserAgent->extensionsDisabled(WTFMove(extensionIDs)); - } - -+void WebPageInspectorController::adjustPageSettings() -+{ -+ // Set this to true as otherwise updating any preferences will override its -+ // value in the Web Process to false (and InspectorController sets it locally -+ // to true when frontend is connected). -+ m_inspectedPage->preferences().setDeveloperExtrasEnabled(true); -+ -+ // Navigation to cached pages doesn't fire some of the events (e.g. execution context created) -+ // that inspector depends on. So we disable the cache when front-end connects. -+ m_inspectedPage->preferences().setUsesBackForwardCache(false); -+ -+ // Enable popup debugging. -+ // TODO: allow to set preferences over the inspector protocol or find a better place for this. -+ m_inspectedPage->preferences().setJavaScriptCanOpenWindowsAutomatically(true); -+ -+ // Enable media stream. -+ if (!m_inspectedPage->preferences().mediaDevicesEnabled()) { -+ m_inspectedPage->preferences().setMediaDevicesEnabled(true); -+ m_inspectedPage->preferences().setPeerConnectionEnabled(true); -+ } -+ -+ // Disable local storage partitioning. See https://github.com/microsoft/playwright/issues/32230 -+ m_inspectedPage->preferences().setStorageBlockingPolicy(static_cast(WebCore::StorageBlockingPolicy::AllowAll)); -+} -+ - } // namespace WebKit diff --git a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h -index 0e6766e155e83f9f5fde89080448b3f08a3a27e5..8ed8b9baf74866fad6498a1a13c6db96f0e2b460 100644 +index 0e6766e155e83f9f5fde89080448b3f08a3a27e5..de641a1629d40ed31b78502b1c9883beed12ba34 100644 --- a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h +++ b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h @@ -26,19 +26,39 @@ @@ -12635,7 +12145,7 @@ index 0e6766e155e83f9f5fde89080448b3f08a3a27e5..8ed8b9baf74866fad6498a1a13c6db96 bool shouldPauseLoading(const ProvisionalPageProxy&) const; void setContinueLoadingCallback(const ProvisionalPageProxy&, WTF::Function&&); -@@ -86,12 +153,13 @@ public: +@@ -86,7 +153,7 @@ public: void browserExtensionsDisabled(HashSet&&); private: @@ -12644,13 +12154,7 @@ index 0e6766e155e83f9f5fde89080448b3f08a3a27e5..8ed8b9baf74866fad6498a1a13c6db96 CheckedPtr checkedTargetAgent() { return m_targetAgent; } WebPageAgentContext webPageAgentContext(); void createLazyAgents(); - - void addTarget(std::unique_ptr&&); -+ void adjustPageSettings(); - - const Ref m_frontendRouter; - const Ref m_backendDispatcher; -@@ -102,9 +170,16 @@ private: +@@ -102,9 +169,16 @@ private: CheckedPtr m_targetAgent; HashMap> m_targets; @@ -12730,7 +12234,7 @@ index 0000000000000000000000000000000000000000..6a04ee480bc3a8270a7de20b1cd0da71 +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/InspectorDialogAgent.cpp b/Source/WebKit/UIProcess/InspectorDialogAgent.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..663f92f0df76042cf6385b056f8a917d688259f9 +index 0000000000000000000000000000000000000000..1c1a7640e41edfc9faf85e2bcd63e7eafbc4dc50 --- /dev/null +++ b/Source/WebKit/UIProcess/InspectorDialogAgent.cpp @@ -0,0 +1,88 @@ @@ -12785,7 +12289,7 @@ index 0000000000000000000000000000000000000000..663f92f0df76042cf6385b056f8a917d + disable(); +} + -+void InspectorDialogAgent::didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) ++void InspectorDialogAgent::didCreateFrontendAndBackend() +{ +} + @@ -12824,7 +12328,7 @@ index 0000000000000000000000000000000000000000..663f92f0df76042cf6385b056f8a917d +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/InspectorDialogAgent.h b/Source/WebKit/UIProcess/InspectorDialogAgent.h new file mode 100644 -index 0000000000000000000000000000000000000000..d0e11ed81a6257c011df23d5870da7403f8e9fe4 +index 0000000000000000000000000000000000000000..7f5e7e87c2c1f76c5c3046cb5393d808d419a32f --- /dev/null +++ b/Source/WebKit/UIProcess/InspectorDialogAgent.h @@ -0,0 +1,70 @@ @@ -12876,12 +12380,12 @@ index 0000000000000000000000000000000000000000..d0e11ed81a6257c011df23d5870da740 + +class InspectorDialogAgent : public Inspector::InspectorAgentBase, public Inspector::DialogBackendDispatcherHandler { + WTF_MAKE_NONCOPYABLE(InspectorDialogAgent); -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(InspectorDialogAgent); +public: + InspectorDialogAgent(Inspector::BackendDispatcher& backendDispatcher, Inspector::FrontendRouter& frontendRouter, WebPageProxy& page); + ~InspectorDialogAgent() override; + -+ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void didCreateFrontendAndBackend() override; + void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; + + Inspector::Protocol::ErrorStringOr enable() override; @@ -12900,10 +12404,10 @@ index 0000000000000000000000000000000000000000..d0e11ed81a6257c011df23d5870da740 +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/InspectorPlaywrightAgent.cpp b/Source/WebKit/UIProcess/InspectorPlaywrightAgent.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..8026d3e9aaca3434fc1d7316a19cd92827566a67 +index 0000000000000000000000000000000000000000..13e45ef13e4d43dce84a8847bde2518af300a4d0 --- /dev/null +++ b/Source/WebKit/UIProcess/InspectorPlaywrightAgent.cpp -@@ -0,0 +1,1009 @@ +@@ -0,0 +1,1040 @@ +/* + * Copyright (C) 2019 Microsoft Corporation. + * @@ -12956,6 +12460,7 @@ index 0000000000000000000000000000000000000000..8026d3e9aaca3434fc1d7316a19cd928 +#include "WebPageInspectorTarget.h" +#include "WebPageMessages.h" +#include "WebPageProxy.h" ++#include "WebPreferences.h" +#include "WebProcessPool.h" +#include "WebProcessProxy.h" +#include "WebsiteDataRecord.h" @@ -12965,6 +12470,7 @@ index 0000000000000000000000000000000000000000..8026d3e9aaca3434fc1d7316a19cd928 +#include +#include +#include ++#include +#include +#include +#include @@ -12982,7 +12488,7 @@ index 0000000000000000000000000000000000000000..8026d3e9aaca3434fc1d7316a19cd928 +namespace WebKit { + +class InspectorPlaywrightAgent::PageProxyChannel : public FrontendChannel { -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(InspectorPlaywrightAgent); +public: + PageProxyChannel(FrontendChannel& frontendChannel, String browserContextID, String pageProxyID, WebPageProxy& page) + : m_browserContextID(browserContextID) @@ -13148,6 +12654,33 @@ index 0000000000000000000000000000000000000000..8026d3e9aaca3434fc1d7316a19cd928 + .release(); +} + ++void adjustInspectedPagePreferences(WebPreferences& preferences, std::optional enableStoragePartitioning) ++{ ++ // Set this to true as otherwise updating any preferences will override its ++ // value in the Web Process to false (and InspectorController sets it locally ++ // to true when frontend is connected). ++ preferences.setDeveloperExtrasEnabled(true); ++ ++ // Navigation to cached pages doesn't fire some of the events (e.g. execution context created) ++ // that inspector depends on. So we disable the cache when front-end connects. ++ preferences.setUsesBackForwardCache(false); ++ ++ // Enable popup debugging. ++ // TODO: allow to set preferences over the inspector protocol or find a better place for this. ++ preferences.setJavaScriptCanOpenWindowsAutomatically(true); ++ ++ // Enable media stream. ++ if (!preferences.mediaDevicesEnabled()) { ++ preferences.setMediaDevicesEnabled(true); ++ preferences.setPeerConnectionEnabled(true); ++ } ++ ++ if (!enableStoragePartitioning || !*enableStoragePartitioning) { ++ // Disable local storage partitioning. See https://github.com/microsoft/playwright/issues/32230 ++ preferences.setStorageBlockingPolicy(static_cast(WebCore::StorageBlockingPolicy::AllowAll)); ++ } ++} ++ +} // namespace + +BrowserContext::BrowserContext() = default; @@ -13156,7 +12689,7 @@ index 0000000000000000000000000000000000000000..8026d3e9aaca3434fc1d7316a19cd928 + +class InspectorPlaywrightAgent::BrowserContextDeletion { + WTF_MAKE_NONCOPYABLE(BrowserContextDeletion); -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(InspectorPlaywrightAgent::BrowserContextDeletion); +public: + BrowserContextDeletion(std::unique_ptr&& context, size_t numberOfPages, Ref&& callback) + : m_browserContext(WTFMove(context)) @@ -13295,6 +12828,7 @@ index 0000000000000000000000000000000000000000..8026d3e9aaca3434fc1d7316a19cd928 + + // Auto-connect to all new pages. + auto pageProxyChannel = makeUnique(*m_frontendChannel, browserContextID, pageProxyID, page); ++ adjustInspectedPagePreferences(page.preferences(), browserContext->enableStoragePartitioning); + page.inspectorController().connectFrontend(*pageProxyChannel); + // Always pause new targets if controlled remotely. + page.inspectorController().setPauseOnStart(true); @@ -13480,13 +13014,14 @@ index 0000000000000000000000000000000000000000..8026d3e9aaca3434fc1d7316a19cd928 + +} + -+Inspector::Protocol::ErrorStringOr InspectorPlaywrightAgent::createContext(const String& proxyServer, const String& proxyBypassList) ++Inspector::Protocol::ErrorStringOr InspectorPlaywrightAgent::createContext(const String& proxyServer, const String& proxyBypassList, std::optional&& enableStoragePartitioning) +{ + String errorString; + std::unique_ptr browserContext = m_client->createBrowserContext(errorString, proxyServer, proxyBypassList); + if (!browserContext) + return makeUnexpected(errorString); + ++ browserContext->enableStoragePartitioning = WTFMove(enableStoragePartitioning); + // Ensure network process. + browserContext->dataStore->networkProcess(); + browserContext->dataStore->setDownloadInstrumentation(this); @@ -13915,7 +13450,7 @@ index 0000000000000000000000000000000000000000..8026d3e9aaca3434fc1d7316a19cd928 +#endif // ENABLE(REMOTE_INSPECTOR) diff --git a/Source/WebKit/UIProcess/InspectorPlaywrightAgent.h b/Source/WebKit/UIProcess/InspectorPlaywrightAgent.h new file mode 100644 -index 0000000000000000000000000000000000000000..f9185788a118f57e98bec149909a206dc1aa5d99 +index 0000000000000000000000000000000000000000..23cf8d2dbcb21b21f3ffedbd5ef369c796cae34a --- /dev/null +++ b/Source/WebKit/UIProcess/InspectorPlaywrightAgent.h @@ -0,0 +1,141 @@ @@ -13987,7 +13522,7 @@ index 0000000000000000000000000000000000000000..f9185788a118f57e98bec149909a206d + , public Inspector::PlaywrightBackendDispatcherHandler + , public DownloadInstrumentation { + WTF_MAKE_NONCOPYABLE(InspectorPlaywrightAgent); -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(InspectorPlaywrightAgent); +public: + explicit InspectorPlaywrightAgent(std::unique_ptr client); + ~InspectorPlaywrightAgent() override; @@ -14014,7 +13549,7 @@ index 0000000000000000000000000000000000000000..f9185788a118f57e98bec149909a206d + Inspector::Protocol::ErrorStringOr disable() override; + Inspector::Protocol::ErrorStringOr getInfo() override; + void close(Ref&&) override; -+ Inspector::Protocol::ErrorStringOr createContext(const String& proxyServer, const String& proxyBypassList) override; ++ Inspector::Protocol::ErrorStringOr createContext(const String& proxyServer, const String& proxyBypassList, std::optional&& enableStoragePartitioning) override; + void deleteContext(const String& browserContextID, Ref&& callback) override; + Inspector::Protocol::ErrorStringOr createPage(const String& browserContextID) override; + void navigate(const String& url, const String& pageProxyID, const String& frameId, const String& referrer, Ref&&) override; @@ -14062,10 +13597,10 @@ index 0000000000000000000000000000000000000000..f9185788a118f57e98bec149909a206d +#endif // ENABLE(REMOTE_INSPECTOR) diff --git a/Source/WebKit/UIProcess/InspectorPlaywrightAgentClient.h b/Source/WebKit/UIProcess/InspectorPlaywrightAgentClient.h new file mode 100644 -index 0000000000000000000000000000000000000000..e7a3dcc533294bb6e12f65d79b5b716bd3c12236 +index 0000000000000000000000000000000000000000..af71e4077eb0c6f95396de7bfef89a3efb5f12d9 --- /dev/null +++ b/Source/WebKit/UIProcess/InspectorPlaywrightAgentClient.h -@@ -0,0 +1,73 @@ +@@ -0,0 +1,74 @@ +/* + * Copyright (C) 2019 Microsoft Corporation. + * @@ -14113,7 +13648,7 @@ index 0000000000000000000000000000000000000000..e7a3dcc533294bb6e12f65d79b5b716b + +class BrowserContext { + WTF_MAKE_NONCOPYABLE(BrowserContext); -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(BrowserContext); +public: + BrowserContext(); + ~BrowserContext(); @@ -14122,6 +13657,7 @@ index 0000000000000000000000000000000000000000..e7a3dcc533294bb6e12f65d79b5b716b + RefPtr processPool; + HashSet pages; + WeakPtr geolocationProvider; ++ std::optional enableStoragePartitioning; +}; + +class InspectorPlaywrightAgentClient { @@ -14140,7 +13676,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 f5c20099e6f746c3d26035313bf67641927549ec..4d3a40809a303bcb49bcc65cce7b7ce06ac7a96f 100644 +index 2f2a79c4780491bb12741cc1e5fabcc37f33310a..09a92636f4bb6b0cf3d86b310c544b23c4d981c1 100644 --- a/Source/WebKit/UIProcess/Launcher/glib/ProcessLauncherGLib.cpp +++ b/Source/WebKit/UIProcess/Launcher/glib/ProcessLauncherGLib.cpp @@ -168,6 +168,13 @@ void ProcessLauncher::launchProcess() @@ -14169,7 +13705,7 @@ index f5c20099e6f746c3d26035313bf67641927549ec..4d3a40809a303bcb49bcc65cce7b7ce0 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 71f80f5414e3e0cec1588fb3b4432665aa6facde..23bbdb9f5eac73ebd0cc5aea33bb08bcc67b60dc 100644 +index 6723ee0d9943be07bc8ad09d2b678838aca968df..0d7fb3c7b1a4c877a2ff2f2189d12c7d00eb8f7b 100644 --- a/Source/WebKit/UIProcess/Launcher/win/ProcessLauncherWin.cpp +++ b/Source/WebKit/UIProcess/Launcher/win/ProcessLauncherWin.cpp @@ -91,14 +91,21 @@ void ProcessLauncher::launchProcess() @@ -14196,7 +13732,7 @@ index 71f80f5414e3e0cec1588fb3b4432665aa6facde..23bbdb9f5eac73ebd0cc5aea33bb08bc 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 48b9ebe975bff1b8179ac26a31b4724a028ecb94..f61094aa9f8db7832f3adcb3fd77bb19005abc40 100644 +index 840ce36d9401a3b997d0a3b6f9cc1f1528cb92fb..9a30a5ada8821c8b5de53cb20b1ea2785621e6b0 100644 --- a/Source/WebKit/UIProcess/PageClient.h +++ b/Source/WebKit/UIProcess/PageClient.h @@ -74,6 +74,11 @@ @@ -14211,7 +13747,7 @@ index 48b9ebe975bff1b8179ac26a31b4724a028ecb94..f61094aa9f8db7832f3adcb3fd77bb19 OBJC_CLASS AVPlayerViewController; OBJC_CLASS CALayer; OBJC_CLASS NSFileWrapper; -@@ -381,7 +386,16 @@ public: +@@ -387,7 +392,16 @@ public: virtual void selectionDidChange() = 0; #endif @@ -14301,7 +13837,7 @@ index 0000000000000000000000000000000000000000..95b682567eba682f927317cd3327a531 +#endif // ENABLE(FULLSCREEN_API) diff --git a/Source/WebKit/UIProcess/PlaywrightFullScreenManagerProxyClient.h b/Source/WebKit/UIProcess/PlaywrightFullScreenManagerProxyClient.h new file mode 100644 -index 0000000000000000000000000000000000000000..a8a92a6c5f4b03724decc97828291f6f27cfc6aa +index 0000000000000000000000000000000000000000..f855bb5ff6e91cc3383fb9a96d32392ff7aa5493 --- /dev/null +++ b/Source/WebKit/UIProcess/PlaywrightFullScreenManagerProxyClient.h @@ -0,0 +1,56 @@ @@ -14341,7 +13877,7 @@ index 0000000000000000000000000000000000000000..a8a92a6c5f4b03724decc97828291f6f +class WebPageProxy; + +class PlaywrightFullScreenManagerProxyClient : public WebFullScreenManagerProxyClient { -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(PlaywrightFullScreenManagerProxyClient); +public: + PlaywrightFullScreenManagerProxyClient(WebPageProxy&); + ~PlaywrightFullScreenManagerProxyClient() override = default; @@ -14375,10 +13911,10 @@ 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..be84b2a5ce00ec2e242785fe8b00a08c44900215 +index 0000000000000000000000000000000000000000..2d2e6bb485c9a95b871d9545b8720cfd061dd789 --- /dev/null +++ b/Source/WebKit/UIProcess/RemoteInspectorPipe.cpp -@@ -0,0 +1,230 @@ +@@ -0,0 +1,229 @@ +/* + * Copyright (C) 2019 Microsoft Corporation. + * @@ -14493,8 +14029,7 @@ index 0000000000000000000000000000000000000000..be84b2a5ce00ec2e242785fe8b00a08c +} // namespace + +class RemoteInspectorPipe::RemoteFrontendChannel : public Inspector::FrontendChannel { -+ WTF_MAKE_FAST_ALLOCATED; -+ ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(RemoteInspectorPipe::RemoteFrontendChannel); +public: + RemoteFrontendChannel() + : m_senderQueue(WorkQueue::create("Inspector pipe writer"_s)) @@ -14571,7 +14106,7 @@ index 0000000000000000000000000000000000000000..be84b2a5ce00ec2e242785fe8b00a08c + while (!m_terminated) { + size_t size = ReadBytes(buffer.get(), bufSize, false); + if (!size) { -+ RunLoop::main().dispatch([this] { ++ RunLoop::mainSingleton().dispatch([this] { + if (!m_terminated) + m_playwrightAgent.disconnectFrontend(); + }); @@ -14590,7 +14125,7 @@ index 0000000000000000000000000000000000000000..be84b2a5ce00ec2e242785fe8b00a08c + + if (end > start) { + String message = String::fromUTF8({ line.mutableSpan().data() + start, end - start }); -+ RunLoop::main().dispatch([this, message = WTFMove(message)] { ++ RunLoop::mainSingleton().dispatch([this, message = WTFMove(message)] { + if (!m_terminated) + m_playwrightAgent.dispatchMessageFromFrontend(message); + }); @@ -14611,7 +14146,7 @@ index 0000000000000000000000000000000000000000..be84b2a5ce00ec2e242785fe8b00a08c +WTF_ALLOW_UNSAFE_BUFFER_USAGE_END diff --git a/Source/WebKit/UIProcess/RemoteInspectorPipe.h b/Source/WebKit/UIProcess/RemoteInspectorPipe.h new file mode 100644 -index 0000000000000000000000000000000000000000..6d04f9290135069359ce6bf8726546482fd1dc95 +index 0000000000000000000000000000000000000000..23626aa70d5a14e6484c81e05b146b375379be4f --- /dev/null +++ b/Source/WebKit/UIProcess/RemoteInspectorPipe.h @@ -0,0 +1,65 @@ @@ -14658,7 +14193,7 @@ index 0000000000000000000000000000000000000000..6d04f9290135069359ce6bf872654648 + +class RemoteInspectorPipe { + WTF_MAKE_NONCOPYABLE(RemoteInspectorPipe); -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(RemoteInspectorPipe); +public: + explicit RemoteInspectorPipe(InspectorPlaywrightAgent&); + ~RemoteInspectorPipe(); @@ -14724,6 +14259,18 @@ index 22c8a561850249008f998405712e88f1c2229336..70e7f76c32af780642d254003ee83420 #endif namespace WebKit { +diff --git a/Source/WebKit/UIProcess/WebAuthentication/Mock/MockNfcService.mm b/Source/WebKit/UIProcess/WebAuthentication/Mock/MockNfcService.mm +index f378efe041b8a150baae5d9136d13599f4fe83da..543523b3d03d36cd4be8a8e30cc87242a76766a3 100644 +--- a/Source/WebKit/UIProcess/WebAuthentication/Mock/MockNfcService.mm ++++ b/Source/WebKit/UIProcess/WebAuthentication/Mock/MockNfcService.mm +@@ -36,6 +36,7 @@ + #import + #import + #import ++#import + + #import "NearFieldSoftLink.h" + diff --git a/Source/WebKit/UIProcess/WebContextMenuProxy.h b/Source/WebKit/UIProcess/WebContextMenuProxy.h index 697a350812e1bf73dd44cc3d723a6a291f9d59d1..a8e1edd710d88f48632d51fd05aa964732d727d3 100644 --- a/Source/WebKit/UIProcess/WebContextMenuProxy.h @@ -14738,7 +14285,7 @@ index 697a350812e1bf73dd44cc3d723a6a291f9d59d1..a8e1edd710d88f48632d51fd05aa9647 RefPtr protectedPage() const; diff --git a/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..653b3550dda624df82de679315711fd1dd4ed0b6 +index 0000000000000000000000000000000000000000..058b9a31fc23cc36a228ef63cc89a80ccf3fa62e --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.cpp @@ -0,0 +1,159 @@ @@ -14793,7 +14340,7 @@ index 0000000000000000000000000000000000000000..653b3550dda624df82de679315711fd1 +{ +} + -+void WebPageInspectorEmulationAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*) ++void WebPageInspectorEmulationAgent::didCreateFrontendAndBackend() +{ +} + @@ -14817,7 +14364,7 @@ index 0000000000000000000000000000000000000000..653b3550dda624df82de679315711fd1 + if (deviceScaleFactor) + m_page.setCustomDeviceScaleFactor(deviceScaleFactor.value(), [] { }); + m_page.setUseFixedLayout(fixedlayout); -+ if (!m_page.pageClient()->isViewVisible() && m_page.configuration().relatedPage()) { ++ if (!m_page.pageClient()->isActiveViewVisible() && m_page.configuration().relatedPage()) { + m_commandsToRunWhenShown.append([this, width, height, callback = WTFMove(callback)]() mutable { + setSize(width, height, WTFMove(callback)); + }); @@ -14903,7 +14450,7 @@ index 0000000000000000000000000000000000000000..653b3550dda624df82de679315711fd1 +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h new file mode 100644 -index 0000000000000000000000000000000000000000..d00d00ce8fd800dc1497b36b8a495c5b9aef6f58 +index 0000000000000000000000000000000000000000..14db923537ba3cad1961a748ae5e1d66c7c9322c --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorEmulationAgent.h @@ -0,0 +1,76 @@ @@ -14954,12 +14501,12 @@ index 0000000000000000000000000000000000000000..d00d00ce8fd800dc1497b36b8a495c5b + +class WebPageInspectorEmulationAgent : public Inspector::InspectorAgentBase, public Inspector::EmulationBackendDispatcherHandler { + WTF_MAKE_NONCOPYABLE(WebPageInspectorEmulationAgent); -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(WebPageInspectorEmulationAgent); +public: + WebPageInspectorEmulationAgent(Inspector::BackendDispatcher& backendDispatcher, WebPageProxy& page); + ~WebPageInspectorEmulationAgent() override; + -+ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void didCreateFrontendAndBackend() override; + void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; + + void setDeviceMetricsOverride(int width, int height, bool fixedlayout, std::optional&& deviceScaleFactor, Ref&&) override; @@ -14985,7 +14532,7 @@ index 0000000000000000000000000000000000000000..d00d00ce8fd800dc1497b36b8a495c5b +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..6cb20d95c1cd8682b025cfdf4ac74f49fd8e9cda +index 0000000000000000000000000000000000000000..dc8d041e863fa9f4522a5c48d2d8bccd32c12c42 --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp @@ -0,0 +1,394 @@ @@ -15038,7 +14585,7 @@ index 0000000000000000000000000000000000000000..6cb20d95c1cd8682b025cfdf4ac74f49 + +template +class CallbackList { -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(CallbackList); +public: + ~CallbackList() + { @@ -15098,7 +14645,7 @@ index 0000000000000000000000000000000000000000..6cb20d95c1cd8682b025cfdf4ac74f49 + m_wheelCallbacks->sendSuccess(); +} + -+void WebPageInspectorInputAgent::didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) ++void WebPageInspectorInputAgent::didCreateFrontendAndBackend() +{ + m_keyboardCallbacks = makeUnique(); + m_mouseCallbacks = makeUnique(); @@ -15300,7 +14847,7 @@ index 0000000000000000000000000000000000000000..6cb20d95c1cd8682b025cfdf4ac74f49 +{ + float rotationAngle = 0.0; + float force = 1.0; -+ const WebCore::IntSize radius(1, 1); ++ const WebCore::DoubleSize radius(1, 1); + + uint8_t unsignedModifiers = modifiers ? static_cast(*modifiers) : 0; + OptionSet eventModifiers; @@ -15385,7 +14932,7 @@ index 0000000000000000000000000000000000000000..6cb20d95c1cd8682b025cfdf4ac74f49 +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h new file mode 100644 -index 0000000000000000000000000000000000000000..26a2a3c0791c334f811ec99a630314f8e8521d02 +index 0000000000000000000000000000000000000000..1afb1a81f0da0d35cf2e9375e691b016eeebe10b --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h @@ -0,0 +1,87 @@ @@ -15437,7 +14984,7 @@ index 0000000000000000000000000000000000000000..26a2a3c0791c334f811ec99a630314f8 + +class WebPageInspectorInputAgent : public Inspector::InspectorAgentBase, public Inspector::InputBackendDispatcherHandler { + WTF_MAKE_NONCOPYABLE(WebPageInspectorInputAgent); -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(WebPageInspectorInputAgent); +public: + WebPageInspectorInputAgent(Inspector::BackendDispatcher& backendDispatcher, WebPageProxy& page); + ~WebPageInspectorInputAgent() override; @@ -15446,7 +14993,7 @@ index 0000000000000000000000000000000000000000..26a2a3c0791c334f811ec99a630314f8 + void didProcessAllPendingMouseEvents(); + void didProcessAllPendingWheelEvents(); + -+ void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override; ++ void didCreateFrontendAndBackend() override; + void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override; + + // Protocol handler @@ -15477,18 +15024,18 @@ index 0000000000000000000000000000000000000000..26a2a3c0791c334f811ec99a630314f8 + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp -index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda102142943 100644 +index 562c341402b4fc434c868b291f7e9f8b24bd4df8..b1d1ab5ab4ace57ea81355db8e188c77e449676e 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.cpp +++ b/Source/WebKit/UIProcess/WebPageProxy.cpp -@@ -206,6 +206,7 @@ - #include +@@ -208,6 +208,7 @@ #include #include + #include +#include #include #include #include -@@ -218,6 +219,7 @@ +@@ -220,6 +221,7 @@ #include #include #include @@ -15496,7 +15043,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 #include #include #include -@@ -242,6 +244,7 @@ +@@ -244,6 +246,7 @@ #include #include #include @@ -15504,7 +15051,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 #include #include #include -@@ -250,10 +253,13 @@ +@@ -252,10 +255,13 @@ #include #include #include @@ -15518,17 +15065,16 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 #include #include #include -@@ -349,6 +355,9 @@ - #if USE(GBM) - #include "AcceleratedBackingStoreDMABuf.h" +@@ -346,7 +352,7 @@ + #include "ViewSnapshotStore.h" #endif -+#endif -+ + +-#if PLATFORM(GTK) +#if PLATFORM(GTK) || PLATFORM(WPE) #include #endif -@@ -474,6 +483,8 @@ static constexpr Seconds tryCloseTimeoutDelay = 50_ms; +@@ -476,6 +482,8 @@ static constexpr Seconds tryCloseTimeoutDelay = 50_ms; static constexpr Seconds audibleActivityClearDelay = 10_s; #endif @@ -15537,7 +15083,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 DEFINE_DEBUG_ONLY_GLOBAL(WTF::RefCountedLeakCounter, webPageProxyCounter, ("WebPageProxy")); #if PLATFORM(COCOA) -@@ -1006,6 +1017,10 @@ WebPageProxy::~WebPageProxy() +@@ -1007,6 +1015,10 @@ WebPageProxy::~WebPageProxy() #endif internals().updatePlayingMediaDidChangeTimer.stop(); @@ -15548,7 +15094,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 } Ref WebPageProxy::Internals::protectedPage() const -@@ -1590,7 +1605,7 @@ void WebPageProxy::didAttachToRunningProcess() +@@ -1591,7 +1603,7 @@ void WebPageProxy::didAttachToRunningProcess() #if ENABLE(FULLSCREEN_API) ASSERT(!m_fullScreenManager); @@ -15557,7 +15103,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 #endif #if ENABLE(VIDEO_PRESENTATION_MODE) ASSERT(!m_playbackSessionManager); -@@ -1754,6 +1769,7 @@ void WebPageProxy::initializeWebPage(const Site& site, WebCore::SandboxFlags eff +@@ -1755,6 +1767,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, *protectedDrawingArea(), m_mainFrame->frameID(), std::nullopt)), 0); @@ -15565,7 +15111,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 #if ENABLE(WINDOW_PROXY_PROPERTY_ACCESS_NOTIFICATION) internals().frameLoadStateObserver = WebPageProxyFrameLoadStateObserver::create(); -@@ -2036,6 +2052,21 @@ Ref WebPageProxy::ensureProtectedRunningProcess() +@@ -2037,6 +2050,21 @@ Ref WebPageProxy::ensureProtectedRunningProcess() return ensureRunningProcess(); } @@ -15584,10 +15130,10 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 + return navigation; +} + - RefPtr WebPageProxy::loadRequest(WebCore::ResourceRequest&& request, ShouldOpenExternalURLsPolicy shouldOpenExternalURLsPolicy, IsPerformingHTTPFallback isPerformingHTTPFallback, std::unique_ptr&& lastNavigationAction, API::Object* userData) + RefPtr WebPageProxy::loadRequest(WebCore::ResourceRequest&& request, ShouldOpenExternalURLsPolicy shouldOpenExternalURLsPolicy, IsPerformingHTTPFallback isPerformingHTTPFallback, std::unique_ptr&& lastNavigationAction, API::Object* userData, bool isRequestFromClientOrUserInput) { if (m_isClosed) -@@ -2152,11 +2183,29 @@ void WebPageProxy::loadRequestWithNavigationShared(Ref&& proces +@@ -2156,11 +2184,29 @@ void WebPageProxy::loadRequestWithNavigationShared(Ref&& proces navigation->setIsLoadedWithNavigationShared(true); protectedProcess->markProcessAsRecentlyUsed(); @@ -15621,7 +15167,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 }); } -@@ -2712,6 +2761,63 @@ void WebPageProxy::setControlledByAutomation(bool controlled) +@@ -2716,6 +2762,63 @@ void WebPageProxy::setControlledByAutomation(bool controlled) protectedWebsiteDataStore()->protectedNetworkProcess()->send(Messages::NetworkProcess::SetSessionIsControlledByAutomation(m_websiteDataStore->sessionID(), m_controlledByAutomation), 0); } @@ -15685,7 +15231,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 void WebPageProxy::createInspectorTarget(IPC::Connection& connection, const String& targetId, Inspector::InspectorTargetType type) { MESSAGE_CHECK_BASE(!targetId.isEmpty(), connection); -@@ -2999,6 +3105,24 @@ void WebPageProxy::updateActivityState(OptionSet flagsToUpdate) +@@ -3003,6 +3106,24 @@ void WebPageProxy::updateActivityState(OptionSet flagsToUpdate) bool wasVisible = isViewVisible(); RefPtr pageClient = this->pageClient(); internals().activityState.remove(flagsToUpdate); @@ -15710,7 +15256,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 if (flagsToUpdate & ActivityState::IsFocused && pageClient->isViewFocused()) internals().activityState.add(ActivityState::IsFocused); if (flagsToUpdate & ActivityState::WindowIsActive && pageClient->isViewWindowActive()) -@@ -3762,7 +3886,7 @@ void WebPageProxy::performDragOperation(DragData& dragData, const String& dragSt +@@ -3769,7 +3890,7 @@ void WebPageProxy::performDragOperation(DragData& dragData, const String& dragSt if (!hasRunningProcess()) return; @@ -15719,7 +15265,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 URL url { dragData.asURL() }; if (url.protocolIsFile()) protectedLegacyMainFrameProcess()->assumeReadAccessToBaseURL(*this, url.string(), [] { }); -@@ -3790,6 +3914,8 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag +@@ -3797,6 +3918,8 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag if (!hasRunningProcess()) return; @@ -15728,7 +15274,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 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; -@@ -3801,7 +3927,7 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag +@@ -3808,7 +3931,7 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag dragData.setClientPosition(remoteUserInputEventData->transformedPoint); performDragControllerAction(action, dragData, remoteUserInputEventData->targetFrameID); }; @@ -15737,7 +15283,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 ASSERT(dragData.platformData()); sendWithAsyncReplyToProcessContainingFrame(frameID, Messages::WebPage::PerformDragControllerAction(action, dragData.clientPosition(), dragData.globalPosition(), dragData.draggingSourceOperationMask(), *dragData.platformData(), dragData.flags()), WTFMove(completionHandler)); #else -@@ -3836,14 +3962,35 @@ void WebPageProxy::didPerformDragControllerAction(std::optionalpageClient()) pageClient->didPerformDragControllerAction(); @@ -15746,27 +15292,26 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 + m_inspectorController->didProcessAllPendingMouseEvents(); } --#if PLATFORM(GTK) -+#if PLATFORM(GTK) || PLATFORM(WPE) + #if PLATFORM(GTK) || PLATFORM(WPE) void WebPageProxy::startDrag(SelectionData&& selectionData, OptionSet dragOperationMask, std::optional&& dragImageHandle, IntPoint&& dragImageHotspot) { -- if (RefPtr pageClient = this->pageClient()) { -- RefPtr dragImage = dragImageHandle ? ShareableBitmap::create(WTFMove(*dragImageHandle)) : nullptr; -- pageClient->startDrag(WTFMove(selectionData), dragOperationMask, WTFMove(dragImage), WTFMove(dragImageHotspot)); + if (m_interceptDrags) { + m_dragSelectionData = WTFMove(selectionData); + m_dragSourceOperationMask = dragOperationMask; + } else { -+#if PLATFORM(GTK) + #if PLATFORM(GTK) +- if (RefPtr pageClient = this->pageClient()) { +- RefPtr dragImage = dragImageHandle ? ShareableBitmap::create(WTFMove(*dragImageHandle)) : nullptr; +- pageClient->startDrag(WTFMove(selectionData), dragOperationMask, WTFMove(dragImage), WTFMove(dragImageHotspot)); + if (RefPtr pageClient = this->pageClient()) { + RefPtr dragImage = dragImageHandle ? ShareableBitmap::create(WTFMove(*dragImageHandle)) : nullptr; + pageClient->startDrag(WTFMove(selectionData), dragOperationMask, WTFMove(dragImage), WTFMove(dragImageHotspot)); + } +#endif -+ } + } + didStartDrag(); +} -+#endif + #endif + +#if PLATFORM(WIN) && ENABLE(DRAG_SUPPORT) +void WebPageProxy::startDrag(WebCore::DragDataMap&& dragDataMap) @@ -15774,10 +15319,11 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 + if (m_interceptDrags) { + m_dragSelectionData = WTFMove(dragDataMap); + m_dragSourceOperationMask = WebCore::anyDragOperation(); - } ++ } didStartDrag(); } -@@ -3865,6 +4012,24 @@ void WebPageProxy::dragEnded(const IntPoint& clientPosition, const IntPoint& glo + #endif +@@ -3874,6 +4016,24 @@ void WebPageProxy::dragEnded(const IntPoint& clientPosition, const IntPoint& glo setDragCaretRect({ }); } @@ -15802,7 +15348,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 void WebPageProxy::didStartDrag() { if (!hasRunningProcess()) -@@ -3872,6 +4037,26 @@ void WebPageProxy::didStartDrag() +@@ -3881,6 +4041,26 @@ void WebPageProxy::didStartDrag() discardQueuedMouseEvents(); send(Messages::WebPage::DidStartDrag()); @@ -15829,12 +15375,12 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 } void WebPageProxy::dragCancelled() -@@ -4043,26 +4228,47 @@ void WebPageProxy::processNextQueuedMouseEvent() +@@ -4052,26 +4232,47 @@ void WebPageProxy::processNextQueuedMouseEvent() process->startResponsivenessTimer(); } - std::optional> sandboxExtensions; -+ m_lastMousePositionForDrag = event->position(); ++ m_lastMousePositionForDrag = roundedIntPoint(event->position()); + if (!m_dragSelectionData) { + std::optional> sandboxExtensions; @@ -15869,9 +15415,9 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 + internals().coalescedMouseEvents.clear(); + } else { +#if PLATFORM(WIN) || PLATFORM(COCOA) -+ DragData dragData(*m_dragSelectionData, event->position(), event->globalPosition(), m_dragSourceOperationMask); ++ DragData dragData(*m_dragSelectionData, roundedIntPoint(event->position()), roundedIntPoint(event->globalPosition()), m_dragSourceOperationMask); +#else -+ DragData dragData(&*m_dragSelectionData, event->position(), event->globalPosition(), m_dragSourceOperationMask); ++ DragData dragData(&*m_dragSelectionData, roundedIntPoint(event->position()), roundedIntPoint(event->globalPosition()), m_dragSourceOperationMask); +#endif + if (eventType == WebEventType::MouseMove) { + dragUpdated(dragData); @@ -15882,14 +15428,14 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 + performDragOperation(dragData, ""_s, WTFMove(sandboxExtensionHandle), WTFMove(sandboxExtensionsForUpload)); + } + m_dragSelectionData = std::nullopt; -+ dragEnded(event->position(), event->globalPosition(), m_dragSourceOperationMask); ++ dragEnded(roundedIntPoint(event->position()), roundedIntPoint(event->globalPosition()), m_dragSourceOperationMask); + } + didReceiveEventIPC(process->connection(), eventType, true, std::nullopt); + } } void WebPageProxy::doAfterProcessingAllPendingMouseEvents(WTF::Function&& action) -@@ -4260,6 +4466,8 @@ void WebPageProxy::wheelEventHandlingCompleted(bool wasHandled) +@@ -4269,6 +4470,8 @@ void WebPageProxy::wheelEventHandlingCompleted(bool wasHandled) if (RefPtr automationSession = m_configuration->processPool().automationSession()) automationSession->wheelEventsFlushedForPage(*this); @@ -15898,7 +15444,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 } void WebPageProxy::cacheWheelEventScrollingAccelerationCurve(const NativeWebWheelEvent& nativeWheelEvent) -@@ -4396,7 +4604,7 @@ static TrackingType mergeTrackingTypes(TrackingType a, TrackingType b) +@@ -4405,7 +4608,7 @@ static TrackingType mergeTrackingTypes(TrackingType a, TrackingType b) void WebPageProxy::updateTouchEventTracking(const WebTouchEvent& touchStartEvent) { @@ -15907,7 +15453,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 for (auto& touchPoint : touchStartEvent.touchPoints()) { auto location = touchPoint.locationInRootView(); auto update = [this, location](TrackingType& trackingType, EventTrackingRegions::EventType eventType) { -@@ -5078,6 +5286,7 @@ Ref WebPageProxy::navigationOriginatingPage(const FrameInfoData& f +@@ -5104,6 +5307,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) { @@ -15915,7 +15461,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 if (!hasRunningProcess()) return completionHandler(PolicyDecision { }); -@@ -6080,6 +6289,7 @@ void WebPageProxy::viewScaleFactorDidChange(IPC::Connection& connection, double +@@ -6118,6 +6322,7 @@ void WebPageProxy::viewScaleFactorDidChange(IPC::Connection& connection, double MESSAGE_CHECK_BASE(scaleFactorIsValid(scaleFactor), connection); if (!legacyMainFrameProcess().hasConnection(connection)) return; @@ -15923,7 +15469,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 forEachWebContentProcess([&] (auto& process, auto pageID) { if (&process == &legacyMainFrameProcess()) -@@ -6731,6 +6941,7 @@ void WebPageProxy::didDestroyNavigationShared(Ref&& process, We +@@ -6769,6 +6974,7 @@ void WebPageProxy::didDestroyNavigationShared(Ref&& process, We RefPtr protectedPageClient { pageClient() }; m_navigationState->didDestroyNavigation(process->coreProcessIdentifier(), navigationID); @@ -15931,7 +15477,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 } void WebPageProxy::didStartProvisionalLoadForFrame(IPC::Connection& connection, FrameIdentifier frameID, FrameInfoData&& frameInfo, ResourceRequest&& request, std::optional navigationID, URL&& url, URL&& unreachableURL, const UserData& userData, WallTime timestamp) -@@ -7079,6 +7290,8 @@ void WebPageProxy::didFailProvisionalLoadForFrameShared(Ref&& p +@@ -7126,6 +7332,8 @@ void WebPageProxy::didFailProvisionalLoadForFrameShared(Ref&& p m_failingProvisionalLoadURL = { }; @@ -15940,7 +15486,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 // If the provisional page's load fails then we destroy the provisional page. if (m_provisionalPage && m_provisionalPage->mainFrame() == &frame && (willContinueLoading == WillContinueLoading::No || protectedPreferences()->siteIsolationEnabled())) m_provisionalPage = nullptr; -@@ -8642,6 +8855,8 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w +@@ -8665,6 +8873,8 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w if (RefPtr page = originatingFrameInfo->page()) openerAppInitiatedState = page->lastNavigationWasAppInitiated(); @@ -15949,7 +15495,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 auto completionHandler = [ this, protectedThis = Ref { *this }, -@@ -8721,6 +8936,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w +@@ -8749,6 +8959,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w configuration->setOpenedMainFrameName(openedMainFrameName); if (!protectedPreferences()->siteIsolationEnabled()) configuration->setRelatedPage(*this); @@ -15957,7 +15503,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 if (RefPtr openerFrame = WebFrameProxy::webFrame(originatingFrameInfoData.frameID); navigationActionData.hasOpener && openerFrame) { configuration->setOpenerInfo({ { -@@ -8748,6 +8964,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w +@@ -8778,6 +8989,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w void WebPageProxy::showPage() { m_uiClient->showPage(this); @@ -15965,7 +15511,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 } bool WebPageProxy::hasOpenedPage() const -@@ -8898,6 +9115,10 @@ void WebPageProxy::closePage() +@@ -8916,6 +9128,10 @@ void WebPageProxy::closePage() if (isClosed()) return; @@ -15976,7 +15522,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 WEBPAGEPROXY_RELEASE_LOG(Process, "closePage:"); if (RefPtr pageClient = this->pageClient()) pageClient->clearAllEditCommands(); -@@ -8936,6 +9157,8 @@ void WebPageProxy::runJavaScriptAlert(IPC::Connection& connection, FrameIdentifi +@@ -8954,6 +9170,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 { @@ -15985,7 +15531,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 page.m_uiClient->runJavaScriptAlert(page, WTFMove(message), frame, WTFMove(frameInfo), [reply = WTFMove(reply), completion = WTFMove(completion)]() mutable { reply(); completion(); -@@ -8958,6 +9181,8 @@ void WebPageProxy::runJavaScriptConfirm(IPC::Connection& connection, FrameIdenti +@@ -8976,6 +9194,8 @@ void WebPageProxy::runJavaScriptConfirm(IPC::Connection& connection, FrameIdenti if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this, message, std::nullopt); } @@ -15994,7 +15540,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 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 { -@@ -8982,6 +9207,8 @@ void WebPageProxy::runJavaScriptPrompt(IPC::Connection& connection, FrameIdentif +@@ -9000,6 +9220,8 @@ void WebPageProxy::runJavaScriptPrompt(IPC::Connection& connection, FrameIdentif if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this, message, defaultValue); } @@ -16003,7 +15549,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 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 { -@@ -9123,6 +9350,8 @@ void WebPageProxy::runBeforeUnloadConfirmPanel(IPC::Connection& connection, Fram +@@ -9142,6 +9364,8 @@ void WebPageProxy::runBeforeUnloadConfirmPanel(IPC::Connection& connection, Fram return; } } @@ -16012,7 +15558,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 // Since runBeforeUnloadConfirmPanel() can spin a nested run loop we need to turn off the responsiveness timer and the tryClose timer. webProcess->stopResponsivenessTimer(); -@@ -9758,6 +9987,11 @@ void WebPageProxy::resourceLoadDidCompleteWithError(ResourceLoadInfo&& loadInfo, +@@ -9787,6 +10011,11 @@ void WebPageProxy::resourceLoadDidCompleteWithError(ResourceLoadInfo&& loadInfo, } #if ENABLE(FULLSCREEN_API) @@ -16024,7 +15570,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 WebFullScreenManagerProxy* WebPageProxy::fullScreenManager() { return m_fullScreenManager.get(); -@@ -9890,6 +10124,17 @@ void WebPageProxy::requestDOMPasteAccess(IPC::Connection& connection, DOMPasteAc +@@ -9919,6 +10148,17 @@ void WebPageProxy::requestDOMPasteAccess(IPC::Connection& connection, DOMPasteAc } } @@ -16042,7 +15588,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 protectedPageClient()->requestDOMPasteAccess(pasteAccessCategory, requiresInteraction, elementRect, originIdentifier, WTFMove(completionHandler)); } -@@ -10900,6 +11145,8 @@ void WebPageProxy::mouseEventHandlingCompleted(std::optional event +@@ -10930,6 +11170,8 @@ void WebPageProxy::mouseEventHandlingCompleted(std::optional event if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->mouseEventsFlushedForPage(*this); didFinishProcessingAllPendingMouseEvents(); @@ -16051,7 +15597,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 } } -@@ -10935,6 +11182,7 @@ void WebPageProxy::keyEventHandlingCompleted(std::optional eventTy +@@ -10965,6 +11207,7 @@ void WebPageProxy::keyEventHandlingCompleted(std::optional eventTy if (!canProcessMoreKeyEvents) { if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->keyboardEventsFlushedForPage(*this); @@ -16059,7 +15605,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 } } -@@ -11369,7 +11617,10 @@ void WebPageProxy::dispatchProcessDidTerminate(WebProcessProxy& process, Process +@@ -11399,7 +11642,10 @@ void WebPageProxy::dispatchProcessDidTerminate(WebProcessProxy& process, Process if (protectedPreferences()->siteIsolationEnabled()) protectedBrowsingContextGroup()->processDidTerminate(*this, process); @@ -16071,16 +15617,16 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 if (m_loaderClient) handledByClient = reason != ProcessTerminationReason::RequestedByClient && m_loaderClient->processDidCrash(*this); else -@@ -12022,6 +12273,8 @@ WebPageCreationParameters WebPageProxy::creationParameters(WebProcessProxy& proc - parameters.canUseCredentialStorage = m_canUseCredentialStorage; +@@ -12060,6 +12306,8 @@ WebPageCreationParameters WebPageProxy::creationParameters(WebProcessProxy& proc parameters.httpsUpgradeEnabled = preferences->upgradeKnownHostsToHTTPSEnabled() ? m_configuration->httpsUpgradeEnabled() : false; + parameters.allowJSHandleInPageContentWorld = m_configuration->allowJSHandleInPageContentWorld(); + + parameters.shouldPauseInInspectorWhenShown = m_inspectorController->shouldPauseInInspectorWhenShown(); #if ENABLE(APP_HIGHLIGHTS) parameters.appHighlightsVisible = appHighlightsVisibility() ? HighlightVisibility::Visible : HighlightVisibility::Hidden; -@@ -12190,8 +12443,47 @@ void WebPageProxy::allowGamepadAccess() +@@ -12222,8 +12470,47 @@ void WebPageProxy::allowGamepadAccess() #endif // ENABLE(GAMEPAD) @@ -16128,7 +15674,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 if (negotiatedLegacyTLS == NegotiatedLegacyTLS::Yes) { m_navigationClient->shouldAllowLegacyTLS(*this, authenticationChallenge.get(), [this, protectedThis = Ref { *this }, authenticationChallenge] (bool shouldAllowLegacyTLS) { if (shouldAllowLegacyTLS) -@@ -12287,6 +12579,12 @@ void WebPageProxy::requestGeolocationPermissionForFrame(IPC::Connection& connect +@@ -12319,6 +12606,12 @@ void WebPageProxy::requestGeolocationPermissionForFrame(IPC::Connection& connect request->deny(); }; @@ -16141,7 +15687,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 // 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 -@@ -12353,6 +12651,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi +@@ -12385,6 +12678,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi shouldChangeDeniedToPrompt = false; if (sessionID().isEphemeral()) { @@ -16154,7 +15700,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 completionHandler(shouldChangeDeniedToPrompt ? PermissionState::Prompt : PermissionState::Denied); return; } -@@ -12367,6 +12671,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi +@@ -12399,6 +12698,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi return; } @@ -16168,7 +15714,7 @@ index 26edaf188b554c9d0622ee3419d343e0e500e7f9..a884af86f2c56693a1aba8246168fda1 completionHandler(shouldChangeDeniedToPrompt ? PermissionState::Prompt : PermissionState::Denied); return; diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h -index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172ef8a2def 100644 +index 5f12f747f3f7adea0c385b1609019bef2999ce1e..da1f2559e995cfdf97803c77627fa453c61783ae 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.h +++ b/Source/WebKit/UIProcess/WebPageProxy.h @@ -28,6 +28,7 @@ @@ -16200,7 +15746,7 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 namespace API { class Attachment; -@@ -115,6 +130,7 @@ class DragData; +@@ -114,6 +129,7 @@ class DragData; class Exception; class FloatPoint; class FloatQuad; @@ -16208,7 +15754,7 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 class FloatRect; class FloatSize; class FontAttributeChanges; -@@ -745,6 +761,8 @@ public: +@@ -764,6 +780,8 @@ public: void setControlledByAutomation(bool); WebPageInspectorController& inspectorController() { return m_inspectorController.get(); } @@ -16217,7 +15763,7 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 #if PLATFORM(IOS_FAMILY) void showInspectorIndication(); -@@ -778,6 +796,7 @@ public: +@@ -797,6 +815,7 @@ public: bool hasSleepDisabler() const; #if ENABLE(FULLSCREEN_API) @@ -16225,7 +15771,7 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 WebFullScreenManagerProxy* fullScreenManager(); RefPtr protectedFullScreenManager(); void setFullScreenClientForTesting(std::unique_ptr&&); -@@ -870,6 +889,12 @@ public: +@@ -889,6 +908,12 @@ public: void setPageLoadStateObserver(RefPtr&&); @@ -16238,16 +15784,16 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 void initializeWebPage(const WebCore::Site&, WebCore::SandboxFlags); void setDrawingArea(RefPtr&&); -@@ -901,6 +926,8 @@ public: +@@ -920,6 +945,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); + RefPtr loadRequest(WebCore::ResourceRequest&&, WebCore::ShouldOpenExternalURLsPolicy, WebCore::IsPerformingHTTPFallback, std::unique_ptr&&, API::Object* userData = nullptr, bool isRequestFromClientOrUserInput = true); + RefPtr loadRequestForInspector(WebCore::ResourceRequest&&, WebFrameProxy*); + 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); -@@ -990,6 +1017,7 @@ public: +@@ -1012,6 +1039,7 @@ public: PageClient* pageClient() const; RefPtr protectedPageClient() const; @@ -16255,24 +15801,23 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 void setViewNeedsDisplay(const WebCore::Region&); void requestScroll(const WebCore::FloatPoint& scrollPosition, const WebCore::IntPoint& scrollOrigin, WebCore::ScrollIsAnimated); -@@ -1635,17 +1663,23 @@ public: +@@ -1663,10 +1691,13 @@ public: void didStartDrag(); void dragCancelled(); void setDragCaretRect(const WebCore::IntRect&); + void setInterceptDrags(bool shouldIntercept); + bool cancelDragIfNeeded(); #if PLATFORM(COCOA) - void startDrag(const WebCore::DragItem&, WebCore::ShareableBitmapHandle&& dragImageHandle, const std::optional&); + void startDrag(const WebCore::DragItem&, WebCore::ShareableBitmapHandle&& dragImageHandle, const std::optional&); void setPromisedDataForImage(IPC::Connection&, const String& pasteboardName, WebCore::SharedMemoryHandle&& imageHandle, const String& filename, const String& extension, const String& title, const String& url, const String& visibleURL, WebCore::SharedMemoryHandle&& archiveHandle, const String& originIdentifier); + void releaseInspectorDragPasteboard(); #endif --#if PLATFORM(GTK) -+#if PLATFORM(GTK) || PLATFORM(WPE) + #if PLATFORM(GTK) || PLATFORM(WPE) void startDrag(WebCore::SelectionData&&, OptionSet, std::optional&& dragImage, WebCore::IntPoint&& dragImageHotspot); - #endif +@@ -1674,6 +1705,9 @@ public: #if ENABLE(MODEL_PROCESS) - void modelDragEnded(const WebCore::ElementIdentifier); + void modelDragEnded(const WebCore::NodeIdentifier); #endif +#if PLATFORM(WIN) + void startDrag(WebCore::DragDataMap&& dragDataMap); @@ -16280,7 +15825,7 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 #endif void processDidBecomeUnresponsive(); -@@ -1897,6 +1931,7 @@ public: +@@ -1921,6 +1955,7 @@ public: void setViewportSizeForCSSViewportUnits(const WebCore::FloatSize&); WebCore::FloatSize viewportSizeForCSSViewportUnits() const; @@ -16288,7 +15833,7 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 void didReceiveAuthenticationChallengeProxy(Ref&&, NegotiatedLegacyTLS); void negotiatedLegacyTLS(); void didNegotiateModernTLS(const URL&); -@@ -1930,6 +1965,8 @@ public: +@@ -1953,6 +1988,8 @@ public: #if PLATFORM(COCOA) || PLATFORM(GTK) RefPtr takeViewSnapshot(std::optional&&); RefPtr takeViewSnapshot(std::optional&&, ForceSoftwareCapturingViewportSnapshot); @@ -16297,7 +15842,7 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 #endif void serializeAndWrapCryptoKey(IPC::Connection&, WebCore::CryptoKeyData&&, CompletionHandler>&&)>&&); -@@ -2956,6 +2993,7 @@ private: +@@ -3001,6 +3038,7 @@ private: RefPtr launchProcessForReload(); void requestNotificationPermission(const String& originString, CompletionHandler&&); @@ -16305,7 +15850,7 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 void didChangeContentSize(const WebCore::IntSize&); void didChangeIntrinsicContentSize(const WebCore::IntSize&); -@@ -3482,8 +3520,10 @@ private: +@@ -3526,8 +3564,10 @@ private: String m_openedMainFrameName; RefPtr m_inspector; @@ -16316,7 +15861,7 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 RefPtr m_fullScreenManager; std::unique_ptr m_fullscreenClient; #endif -@@ -3680,6 +3720,22 @@ private: +@@ -3725,6 +3765,22 @@ private: std::optional m_currentDragOperation; bool m_currentDragIsOverFileInput { false }; unsigned m_currentDragNumberOfFilesToBeAccepted { 0 }; @@ -16339,7 +15884,7 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 #endif bool m_mainFrameHasHorizontalScrollbar { false }; -@@ -3851,6 +3907,10 @@ private: +@@ -3897,6 +3953,10 @@ private: RefPtr messageBody; }; Vector m_pendingInjectedBundleMessages; @@ -16351,7 +15896,7 @@ index 9cf73b03b77ddb5e68cfa1866c368b3126405610..ffcd057469132f3be590d06469518172 #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 fcb0649584da102c1446029342c8f31c59628301..5559c5f690a568bb1b39fe4bc0ac181a8e1953b3 100644 +index 78928579347f7b44e1be855ef68e5521326f5d27..d1e5761093a8eb24424e9f37ea71dbadcf375d38 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.messages.in +++ b/Source/WebKit/UIProcess/WebPageProxy.messages.in @@ -35,6 +35,7 @@ messages -> WebPageProxy { @@ -16362,12 +15907,7 @@ index fcb0649584da102c1446029342c8f31c59628301..5559c5f690a568bb1b39fe4bc0ac181a DidReceiveEventIPC(enum:uint8_t WebKit::WebEventType eventType, bool handled, struct std::optional remoteUserInputEventData) SetCursor(WebCore::Cursor cursor) -@@ -333,10 +334,14 @@ messages -> WebPageProxy { - StartDrag(struct WebCore::DragItem dragItem, WebCore::ShareableBitmapHandle dragImage, std::optional elementID) - SetPromisedDataForImage(String pasteboardName, WebCore::SharedMemory::Handle imageHandle, String filename, String extension, String title, String url, String visibleURL, WebCore::SharedMemory::Handle archiveHandle, String originIdentifier) - #endif --#if PLATFORM(GTK) && ENABLE(DRAG_SUPPORT) -+#if (PLATFORM(GTK) || PLATFORM(WPE)) && ENABLE(DRAG_SUPPORT) +@@ -335,6 +336,10 @@ messages -> WebPageProxy { StartDrag(WebCore::SelectionData selectionData, OptionSet dragOperationMask, std::optional dragImage, WebCore::IntPoint dragImageHotspot) #endif @@ -16377,9 +15917,9 @@ index fcb0649584da102c1446029342c8f31c59628301..5559c5f690a568bb1b39fe4bc0ac181a + #if PLATFORM(IOS_FAMILY) && ENABLE(DRAG_SUPPORT) WillReceiveEditDragSnapshot() - DidReceiveEditDragSnapshot(struct std::optional textIndicator) + DidReceiveEditDragSnapshot(RefPtr textIndicator) diff --git a/Source/WebKit/UIProcess/WebProcessCache.cpp b/Source/WebKit/UIProcess/WebProcessCache.cpp -index 9619dd597aec77547cccdf0d3ce6c711335f6244..db169d23388fd7b1cb7b79eef4160301aee0e0ef 100644 +index b660187624f7a1aad8f86088f000a2a6b28d52f8..13444a265d2635083bece831e4cf7c0df944a712 100644 --- a/Source/WebKit/UIProcess/WebProcessCache.cpp +++ b/Source/WebKit/UIProcess/WebProcessCache.cpp @@ -100,6 +100,10 @@ bool WebProcessCache::canCacheProcess(WebProcessProxy& process) const @@ -16394,10 +15934,10 @@ index 9619dd597aec77547cccdf0d3ce6c711335f6244..db169d23388fd7b1cb7b79eef4160301 } diff --git a/Source/WebKit/UIProcess/WebProcessPool.cpp b/Source/WebKit/UIProcess/WebProcessPool.cpp -index a406b5a1e1523c07cb6dc2eebc493fa4f0026575..1f4fc38edd3756d5423e89a41c30a8ce7d5bf71f 100644 +index 3b4a8bd84890e0724d4e6323bfe399a19f5abcc9..c3ad63ee402eb53aa5dacf10fd1fdf955292f7b3 100644 --- a/Source/WebKit/UIProcess/WebProcessPool.cpp +++ b/Source/WebKit/UIProcess/WebProcessPool.cpp -@@ -439,10 +439,10 @@ void WebProcessPool::setAutomationClient(std::unique_ptr& +@@ -441,10 +441,10 @@ void WebProcessPool::setAutomationClient(std::unique_ptr& void WebProcessPool::setOverrideLanguages(Vector&& languages) { @@ -16410,7 +15950,7 @@ index a406b5a1e1523c07cb6dc2eebc493fa4f0026575..1f4fc38edd3756d5423e89a41c30a8ce #if ENABLE(GPU_PROCESS) if (RefPtr gpuProcess = GPUProcessProxy::singletonIfCreated()) -@@ -450,9 +450,10 @@ void WebProcessPool::setOverrideLanguages(Vector&& languages) +@@ -452,9 +452,10 @@ void WebProcessPool::setOverrideLanguages(Vector&& languages) #endif #if USE(SOUP) for (Ref networkProcess : NetworkProcessProxy::allNetworkProcesses()) @@ -16422,7 +15962,7 @@ index a406b5a1e1523c07cb6dc2eebc493fa4f0026575..1f4fc38edd3756d5423e89a41c30a8ce void WebProcessPool::fullKeyboardAccessModeChanged(bool fullKeyboardAccessEnabled) { -@@ -999,7 +1000,7 @@ void WebProcessPool::initializeNewWebProcess(WebProcessProxy& process, WebsiteDa +@@ -1001,7 +1002,7 @@ void WebProcessPool::initializeNewWebProcess(WebProcessProxy& process, WebsiteDa #endif parameters.cacheModel = LegacyGlobalSettings::singleton().cacheModel(); @@ -16432,10 +15972,10 @@ index a406b5a1e1523c07cb6dc2eebc493fa4f0026575..1f4fc38edd3756d5423e89a41c30a8ce parameters.urlSchemesRegisteredAsSecure = copyToVector(LegacyGlobalSettings::singleton().schemesToRegisterAsSecure()); diff --git a/Source/WebKit/UIProcess/WebProcessProxy.cpp b/Source/WebKit/UIProcess/WebProcessProxy.cpp -index 51546b2db1e5ca415ceb550e69dc2daa982df450..6d10b802e49a3a0b803eb8a630e7c1c83c53f79c 100644 +index da8a47cf02d65b8ed8969abeb3783e5766ee0d3a..8e93c8f7028bc7707bbca7a7ac42990a797c3abf 100644 --- a/Source/WebKit/UIProcess/WebProcessProxy.cpp +++ b/Source/WebKit/UIProcess/WebProcessProxy.cpp -@@ -208,6 +208,11 @@ Vector> WebProcessProxy::allProcesses() +@@ -207,6 +207,11 @@ Vector> WebProcessProxy::allProcesses() }); } @@ -16447,7 +15987,7 @@ index 51546b2db1e5ca415ceb550e69dc2daa982df450..6d10b802e49a3a0b803eb8a630e7c1c8 RefPtr WebProcessProxy::processForIdentifier(ProcessIdentifier identifier) { return allProcessMap().get(identifier); -@@ -577,6 +582,26 @@ void WebProcessProxy::getLaunchOptions(ProcessLauncher::LaunchOptions& launchOpt +@@ -531,6 +536,26 @@ void WebProcessProxy::getLaunchOptions(ProcessLauncher::LaunchOptions& launchOpt if (WebKit::isInspectorProcessPool(protectedProcessPool())) launchOptions.extraInitializationData.add("inspector-process"_s, "1"_s); @@ -16475,10 +16015,10 @@ index 51546b2db1e5ca415ceb550e69dc2daa982df450..6d10b802e49a3a0b803eb8a630e7c1c8 if (isPrewarmed()) diff --git a/Source/WebKit/UIProcess/WebProcessProxy.h b/Source/WebKit/UIProcess/WebProcessProxy.h -index 4512391119154ce5b0ebb892e853499bda9747d9..33404c266e230e0cf62a0d9a597a7b59a047a1f3 100644 +index 43998d80778262987ebeb21afdae7a6f5cb9a8f6..fbb50218cbeb07953790c85c5e4f2618b3f08b35 100644 --- a/Source/WebKit/UIProcess/WebProcessProxy.h +++ b/Source/WebKit/UIProcess/WebProcessProxy.h -@@ -185,6 +185,7 @@ public: +@@ -186,6 +186,7 @@ public: static void forWebPagesWithOrigin(PAL::SessionID, const WebCore::SecurityOriginData&, NOESCAPE const Function&); static Vector> allowedFirstPartiesForCookies(); @@ -16487,7 +16027,7 @@ index 4512391119154ce5b0ebb892e853499bda9747d9..33404c266e230e0cf62a0d9a597a7b59 void initializeWebProcess(WebProcessCreationParameters&&); diff --git a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp -index 606401f719eecd52737fc3f6f15a328f74f4f5d9..be13e62a1461f21543f0a51c57003779bb3c1c79 100644 +index 90b0872f0cf72f353ef14aa9d465460717d60daa..cc1b971dba3c37d30c01cde04151c5529222e627 100644 --- a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp +++ b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp @@ -321,15 +321,10 @@ SOAuthorizationCoordinator& WebsiteDataStore::soAuthorizationCoordinator(const W @@ -16539,7 +16079,7 @@ index 606401f719eecd52737fc3f6f15a328f74f4f5d9..be13e62a1461f21543f0a51c57003779 void WebsiteDataStore::hasAppBoundSession(CompletionHandler&& completionHandler) const { diff --git a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h -index 64d8fb4e83836c3dde325e77ded6b014e819cb10..c4b923995301697f8a52d72e74c842dcf4ec8161 100644 +index aad3f23761bef4b75b414606df5f09fd19657fe5..d8cf70a7944e0916fb13efd571992d2462309dad 100644 --- a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h +++ b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h @@ -98,6 +98,7 @@ class DeviceIdHashSaltStorage; @@ -16609,7 +16149,7 @@ index 64d8fb4e83836c3dde325e77ded6b014e819cb10..c4b923995301697f8a52d72e74c842dc String m_cookiePersistentStoragePath; SoupCookiePersistentStorageType m_cookiePersistentStorageType { SoupCookiePersistentStorageType::SQLite }; @@ -643,6 +663,10 @@ private: - RefPtr m_cookieStore; + const RefPtr m_cookieStore; RefPtr m_networkProcess; + std::optional m_allowDownloadForAutomation; @@ -16617,10 +16157,10 @@ index 64d8fb4e83836c3dde325e77ded6b014e819cb10..c4b923995301697f8a52d72e74c842dc + DownloadInstrumentation* m_downloadInstrumentation { nullptr }; + #if HAVE(APP_SSO) - std::unique_ptr m_soAuthorizationCoordinator; + const std::unique_ptr m_soAuthorizationCoordinator; #endif diff --git a/Source/WebKit/UIProcess/geoclue/GeoclueGeolocationProvider.cpp b/Source/WebKit/UIProcess/geoclue/GeoclueGeolocationProvider.cpp -index 19934de3173ecc0507c5e59956bcf6730a8a88b4..bc0e52f6f28caeca8b9b7aab14b7ff991d7e6b41 100644 +index de9b08ac0588999d167cf97dc172ee35a8f8ab56..bab2b6ca88d666f16be6c1c32d05be5e02b88119 100644 --- a/Source/WebKit/UIProcess/geoclue/GeoclueGeolocationProvider.cpp +++ b/Source/WebKit/UIProcess/geoclue/GeoclueGeolocationProvider.cpp @@ -114,6 +114,14 @@ void GeoclueGeolocationProvider::stop() @@ -16861,7 +16401,7 @@ index 0000000000000000000000000000000000000000..dfa06245b5411f2bc6ca7cebdb7c6d5f +#endif // ENABLE(REMOTE_INSPECTOR) diff --git a/Source/WebKit/UIProcess/glib/InspectorPlaywrightAgentClientGLib.h b/Source/WebKit/UIProcess/glib/InspectorPlaywrightAgentClientGLib.h new file mode 100644 -index 0000000000000000000000000000000000000000..441442d899e4088f5c24ae9f70c3e4ffa1e6d340 +index 0000000000000000000000000000000000000000..68bc7b610703bfcd3eff0d1c032092f6967fdc16 --- /dev/null +++ b/Source/WebKit/UIProcess/glib/InspectorPlaywrightAgentClientGLib.h @@ -0,0 +1,61 @@ @@ -16905,7 +16445,7 @@ index 0000000000000000000000000000000000000000..441442d899e4088f5c24ae9f70c3e4ff +namespace WebKit { + +class InspectorPlaywrightAgentClientGlib : public InspectorPlaywrightAgentClient { -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(InspectorPlaywrightAgentClientGlib); +public: + InspectorPlaywrightAgentClientGlib(const WTF::String& proxyURI, const char* const* ignoreHosts); + ~InspectorPlaywrightAgentClientGlib() override = default; @@ -16927,7 +16467,7 @@ index 0000000000000000000000000000000000000000..441442d899e4088f5c24ae9f70c3e4ff + +#endif // ENABLE(REMOTE_INSPECTOR) diff --git a/Source/WebKit/UIProcess/glib/WebProcessPoolGLib.cpp b/Source/WebKit/UIProcess/glib/WebProcessPoolGLib.cpp -index 4d782728182319b3d6e8e6d05580d96dd7ea19ac..c28dff07b154dba6ba82e94953b7b24e25a89106 100644 +index 044ddc0c3e957b4d6634de3710e9d1d55082be3f..a6a8854fd6076347f3f5540d2177006a94bfa530 100644 --- a/Source/WebKit/UIProcess/glib/WebProcessPoolGLib.cpp +++ b/Source/WebKit/UIProcess/glib/WebProcessPoolGLib.cpp @@ -126,6 +126,8 @@ static OptionSet availableInputDevices() @@ -16939,37 +16479,16 @@ index 4d782728182319b3d6e8e6d05580d96dd7ea19ac..c28dff07b154dba6ba82e94953b7b24e #if ENABLE(TOUCH_EVENTS) return AvailableInputDevices::Touchscreen; #else -diff --git a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h -index 5529f52048b24290f424e877cd9dbfb890e02ffb..c2b76b6188dd9596c4a1f31c137daff7d7644c7f 100644 ---- a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h -+++ b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h -@@ -32,6 +32,7 @@ - #include - - typedef struct _cairo cairo_t; -+typedef struct _cairo_surface cairo_surface_t; - - #if USE(GTK4) - typedef struct _GdkSnapshot GdkSnapshot; -@@ -62,6 +63,8 @@ public: - #else - virtual bool paint(cairo_t*, const WebCore::IntRect&) = 0; - #endif -+ virtual cairo_surface_t* surface() { return nullptr; } -+ - virtual void realize() { }; - virtual void unrealize() { }; - virtual int renderHostFileDescriptor() { return -1; } -diff --git a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStoreDMABuf.cpp b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStoreDMABuf.cpp -index 395a409b6302d4146606fc0b45a15cff63c7f327..666d9629e0155e8c03b0672f7a9274de7f04d642 100644 ---- a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStoreDMABuf.cpp -+++ b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStoreDMABuf.cpp -@@ -812,4 +812,30 @@ RefPtr AcceleratedBackingStoreDMABuf::bufferAsNativeImageF +diff --git a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp +index 8345d7f789d15be44e8ffb742ade501f95e011f9..5c354920d64d691ab87251966ff4912d19ee85a2 100644 +--- a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp ++++ b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp +@@ -848,4 +848,30 @@ RefPtr AcceleratedBackingStore::bufferAsNativeImageForTesting() con return m_committedBuffer->asNativeImageForTesting(); } +// Playwright begin -+cairo_surface_t* AcceleratedBackingStoreDMABuf::surface() ++cairo_surface_t* AcceleratedBackingStore::surface() +{ + RefPtr buffer = m_committedBuffer.get(); + if (!buffer) @@ -16995,25 +16514,34 @@ index 395a409b6302d4146606fc0b45a15cff63c7f327..666d9629e0155e8c03b0672f7a9274de +// Playwright end + } // namespace WebKit -diff --git a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStoreDMABuf.h b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStoreDMABuf.h -index 63dfbed03bd898c822d426a5449141fec2841226..eaf141843da3f02c5ba6fd0322f3175248a7195b 100644 ---- a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStoreDMABuf.h -+++ b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStoreDMABuf.h -@@ -98,6 +98,7 @@ private: - #else - bool paint(cairo_t*, const WebCore::IntRect&) override; - #endif -+ cairo_surface_t* surface() override; - void unrealize() override; - void update(const LayerTreeContext&) override; - RendererBufferFormat bufferFormat() const override; -@@ -253,6 +254,9 @@ private: +diff --git a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h +index d305b6bf990f6befccb3f3e7c22a9949c4be41d3..22b0558d5729cf52fadc64b9e191b8a1f7330608 100644 +--- a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h ++++ b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h +@@ -41,6 +41,7 @@ + #include + + typedef void *EGLImage; ++typedef struct _cairo_surface cairo_surface_t; + + #if USE(GBM) + struct gbm_bo; +@@ -84,6 +85,7 @@ public: + void unrealize(); + RendererBufferDescription bufferDescription() const; + RefPtr bufferAsNativeImageForTesting() const; ++ cairo_surface_t* surface(); + + private: + explicit AcceleratedBackingStore(WebPageProxy&); +@@ -251,6 +253,10 @@ private: RefPtr m_committedBuffer; Rects m_pendingDamageRects; HashMap> m_buffers; +// Playwright begin + RefPtr m_flippedSurface; +// Playwright end ++ }; } // namespace WebKit @@ -17327,7 +16855,7 @@ index 8afb6132fad823816f84328a8b0a1a514f998bf7..54b582e60f4b16b3c7ba038c8c52466c CorrectionPanel(); diff --git a/Source/WebKit/UIProcess/mac/InspectorPlaywrightAgentClientMac.h b/Source/WebKit/UIProcess/mac/InspectorPlaywrightAgentClientMac.h new file mode 100644 -index 0000000000000000000000000000000000000000..2aabc02a4b5432f68a6e85fd9689775608f05a67 +index 0000000000000000000000000000000000000000..edb4581e8f1f484976a9081d37cb61e54b9b81c5 --- /dev/null +++ b/Source/WebKit/UIProcess/mac/InspectorPlaywrightAgentClientMac.h @@ -0,0 +1,53 @@ @@ -17366,7 +16894,7 @@ index 0000000000000000000000000000000000000000..2aabc02a4b5432f68a6e85fd96897756 +namespace WebKit { + +class InspectorPlaywrightAgentClientMac : public InspectorPlaywrightAgentClient { -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(InspectorPlaywrightAgentClientMac); +public: + InspectorPlaywrightAgentClientMac(_WKBrowserInspectorDelegate* delegate, bool headless); + ~InspectorPlaywrightAgentClientMac() override = default; @@ -17535,7 +17063,7 @@ index 0000000000000000000000000000000000000000..8adbd51bfecad2a273117588bf50f8f7 + +#endif diff --git a/Source/WebKit/UIProcess/mac/PageClientImplMac.h b/Source/WebKit/UIProcess/mac/PageClientImplMac.h -index c0de66cc6e82f1a2459ba006b5ac0fd63d8da261..a01639bab522df3ecff9df5aa4b6218633b2f049 100644 +index 06d85d02ad62fdfaf211deb534126b0a5e966493..ab16123d697e7dab5907e4e5d4386cb84f03756e 100644 --- a/Source/WebKit/UIProcess/mac/PageClientImplMac.h +++ b/Source/WebKit/UIProcess/mac/PageClientImplMac.h @@ -31,9 +31,11 @@ @@ -17559,7 +17087,7 @@ index c0de66cc6e82f1a2459ba006b5ac0fd63d8da261..a01639bab522df3ecff9df5aa4b62186 PageClientImpl(NSView *, WKWebView *); virtual ~PageClientImpl(); -@@ -175,6 +179,9 @@ private: +@@ -176,6 +180,9 @@ private: void updateAcceleratedCompositingMode(const LayerTreeContext&) override; void didFirstLayerFlush(const LayerTreeContext&) override; @@ -17569,7 +17097,7 @@ index c0de66cc6e82f1a2459ba006b5ac0fd63d8da261..a01639bab522df3ecff9df5aa4b62186 RefPtr takeViewSnapshot(std::optional&&) override; RefPtr takeViewSnapshot(std::optional&&, ForceSoftwareCapturingViewportSnapshot) override; void wheelEventWasNotHandledByWebCore(const NativeWebWheelEvent&) override; -@@ -226,6 +233,10 @@ private: +@@ -227,6 +234,10 @@ private: void beganExitFullScreen(const WebCore::IntRect& initialFrame, const WebCore::IntRect& finalFrame, CompletionHandler&&) override; #endif @@ -17581,7 +17109,7 @@ index c0de66cc6e82f1a2459ba006b5ac0fd63d8da261..a01639bab522df3ecff9df5aa4b62186 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 0af0f031313b707c18ad675115385c42d5a1284f..4408666f4d7b4e67670f3edfabe9a1407e22afe2 100644 +index 59d3be9ffe66bcaf64687a69c00b077279ec1304..be0176b59dab6c2cdf279f0aa4e2ed8e26e9fbc7 100644 --- a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm +++ b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm @@ -107,6 +107,13 @@ namespace WebKit { @@ -17620,15 +17148,15 @@ index 0af0f031313b707c18ad675115385c42d5a1284f..4408666f4d7b4e67670f3edfabe9a140 if (CheckedPtr impl = m_impl.get()) @@ -192,6 +205,9 @@ void PageClientImpl::makeFirstResponder() - bool PageClientImpl::isViewVisible() + bool PageClientImpl::isViewVisible(NSView *view, NSWindow *viewWindow) { + if (_headless) + return true; + - RetainPtr activeView = this->activeView(); - RetainPtr activeViewWindow = activeWindow(); - -@@ -267,7 +283,8 @@ void PageClientImpl::didRelaunchProcess() + auto windowIsOccluded = [&]()->bool { + return m_impl && m_impl->windowOcclusionDetectionEnabled() && (viewWindow.occlusionState & NSWindowOcclusionStateVisible) != NSWindowOcclusionStateVisible; + }; +@@ -279,7 +295,8 @@ void PageClientImpl::didRelaunchProcess() void PageClientImpl::preferencesDidChange() { @@ -17638,7 +17166,7 @@ index 0af0f031313b707c18ad675115385c42d5a1284f..4408666f4d7b4e67670f3edfabe9a140 } void PageClientImpl::toolTipChanged(const String& oldToolTip, const String& newToolTip) -@@ -477,6 +494,8 @@ IntRect PageClientImpl::rootViewToAccessibilityScreen(const IntRect& rect) +@@ -489,6 +506,8 @@ IntRect PageClientImpl::rootViewToAccessibilityScreen(const IntRect& rect) void PageClientImpl::doneWithKeyEvent(const NativeWebKeyboardEvent& event, bool eventWasHandled) { @@ -17647,7 +17175,7 @@ index 0af0f031313b707c18ad675115385c42d5a1284f..4408666f4d7b4e67670f3edfabe9a140 checkedImpl()->doneWithKeyEvent(event.nativeEvent(), eventWasHandled); } -@@ -496,6 +515,8 @@ void PageClientImpl::computeHasVisualSearchResults(const URL& imageURL, Shareabl +@@ -508,6 +527,8 @@ void PageClientImpl::computeHasVisualSearchResults(const URL& imageURL, Shareabl RefPtr PageClientImpl::createPopupMenuProxy(WebPageProxy& page) { @@ -17656,7 +17184,7 @@ index 0af0f031313b707c18ad675115385c42d5a1284f..4408666f4d7b4e67670f3edfabe9a140 return WebPopupMenuProxyMac::create(m_view.get().get(), page.checkedPopupMenuClient().get()); } -@@ -621,6 +642,12 @@ CALayer *PageClientImpl::footerBannerLayer() const +@@ -633,6 +654,12 @@ CALayer *PageClientImpl::footerBannerLayer() const return m_impl->footerBannerLayer(); } @@ -17669,7 +17197,7 @@ index 0af0f031313b707c18ad675115385c42d5a1284f..4408666f4d7b4e67670f3edfabe9a140 RefPtr PageClientImpl::takeViewSnapshot(std::optional&&) { return checkedImpl()->takeViewSnapshot(); -@@ -832,6 +859,13 @@ void PageClientImpl::beganExitFullScreen(const IntRect& initialFrame, const IntR +@@ -844,6 +871,13 @@ void PageClientImpl::beganExitFullScreen(const IntRect& initialFrame, const IntR #endif // ENABLE(FULLSCREEN_API) @@ -17683,7 +17211,7 @@ index 0af0f031313b707c18ad675115385c42d5a1284f..4408666f4d7b4e67670f3edfabe9a140 void PageClientImpl::navigationGestureDidBegin() { checkedImpl()->dismissContentRelativeChildWindowsWithAnimation(true); -@@ -1012,6 +1046,9 @@ void PageClientImpl::requestScrollToRect(const WebCore::FloatRect& targetRect, c +@@ -1024,6 +1058,9 @@ void PageClientImpl::requestScrollToRect(const WebCore::FloatRect& targetRect, c bool PageClientImpl::windowIsFrontWindowUnderMouse(const NativeWebMouseEvent& event) { @@ -17716,18 +17244,30 @@ 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) +diff --git a/Source/WebKit/UIProcess/mac/WKSharingServicePickerDelegate.h b/Source/WebKit/UIProcess/mac/WKSharingServicePickerDelegate.h +index 27b84957c91a4c82f4bbfdfb152f1a4ba9fb4309..f0d71a99c30eefa5f56c9668c4df21430c91ee13 100644 +--- a/Source/WebKit/UIProcess/mac/WKSharingServicePickerDelegate.h ++++ b/Source/WebKit/UIProcess/mac/WKSharingServicePickerDelegate.h +@@ -26,6 +26,7 @@ + #if ENABLE(SERVICE_CONTROLS) - #import "AppKitSPI.h" -+#import "GPUProcessProxy.h" - #import "LayerTreeContext.h" - #import "NativeWebMouseEvent.h" - #import "VideoPresentationManagerProxy.h" + #import ++#import + #import + + namespace WebKit { +diff --git a/Source/WebKit/UIProcess/mac/WKTextAnimationManagerMac.mm b/Source/WebKit/UIProcess/mac/WKTextAnimationManagerMac.mm +index 1e32d91466e9785f20fd1d63b8fa93ee192d5f55..e355cb75d3c25f63f51d030190fb49f20831fda8 100644 +--- a/Source/WebKit/UIProcess/mac/WKTextAnimationManagerMac.mm ++++ b/Source/WebKit/UIProcess/mac/WKTextAnimationManagerMac.mm +@@ -35,6 +35,7 @@ + #import "WebViewImpl.h" + #import + #import ++#import + #import + + @interface WKTextAnimationTypeEffectData : NSObject 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 @@ -17948,8 +17488,20 @@ index 0000000000000000000000000000000000000000..dd52991f936aa1c046b404801ee97237 +} + +} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm b/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm +index a72ac702858542d08c4d687621d53df356386f46..e0e47b5e7d51c7ae65f12852a7f4811df2a464fb 100644 +--- a/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm ++++ b/Source/WebKit/UIProcess/mac/WebPageProxyMac.mm +@@ -61,6 +61,7 @@ + #import + #import + #import ++#import + #import + #import + #import diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.h b/Source/WebKit/UIProcess/mac/WebViewImpl.h -index ad45b454acf088d5bb375971aae99684ea06d3ab..a9b4c26971969cf352cc045b01505a1751b44e46 100644 +index e621bbe3154ae0406e3617d82345a2428ba11583..d11ba4f513a3b558ef996c3247f04cb5a0b498ad 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.h +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.h @@ -34,6 +34,7 @@ @@ -17960,7 +17512,7 @@ index ad45b454acf088d5bb375971aae99684ea06d3ab..a9b4c26971969cf352cc045b01505a17 #include #include #include -@@ -565,6 +566,9 @@ public: +@@ -572,6 +573,9 @@ public: void provideDataForPasteboard(NSPasteboard *, NSString *type); NSArray *namesOfPromisedFilesDroppedAtDestination(NSURL *dropDestination); @@ -17971,10 +17523,10 @@ index ad45b454acf088d5bb375971aae99684ea06d3ab..a9b4c26971969cf352cc045b01505a17 RefPtr takeViewSnapshot(ForceSoftwareCapturingViewportSnapshot); void saveBackForwardSnapshotForCurrentItem(); diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.mm b/Source/WebKit/UIProcess/mac/WebViewImpl.mm -index 1e906144a471367de596ccf5dd0ece2706306fe6..51cc9282b0b6d0b66df9f3b4fb69da7261b6e8d3 100644 +index e4a353131f828e0cce9f909a5feab718c1fa194d..be4338ae49c58c8570cf3921209ed903b4e114fa 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.mm +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.mm -@@ -2477,6 +2477,11 @@ WebCore::DestinationColorSpace WebViewImpl::colorSpace() +@@ -2488,6 +2488,11 @@ WebCore::DestinationColorSpace WebViewImpl::colorSpace() if (!m_colorSpace) m_colorSpace = [NSColorSpace sRGBColorSpace]; } @@ -17986,7 +17538,7 @@ index 1e906144a471367de596ccf5dd0ece2706306fe6..51cc9282b0b6d0b66df9f3b4fb69da72 ASSERT(m_colorSpace); return WebCore::DestinationColorSpace { [m_colorSpace CGColorSpace] }; -@@ -4693,6 +4698,17 @@ static RetainPtr takeWindowSnapshot(CGSWindowID windowID, bool captu +@@ -4723,6 +4728,17 @@ static RetainPtr takeWindowSnapshot(CGSWindowID windowID, bool captu return WebCore::cgWindowListCreateImage(CGRectNull, kCGWindowListOptionIncludingWindow, windowID, imageOptions); } @@ -18004,6 +17556,51 @@ index 1e906144a471367de596ccf5dd0ece2706306fe6..51cc9282b0b6d0b66df9f3b4fb69da72 RefPtr WebViewImpl::takeViewSnapshot() { return takeViewSnapshot(ForceSoftwareCapturingViewportSnapshot::No); +diff --git a/Source/WebKit/UIProcess/wc/DrawingAreaProxyWC.cpp b/Source/WebKit/UIProcess/wc/DrawingAreaProxyWC.cpp +index 3f4c7f86c46a39036d6c52f8b77e50d915b740da..a9990ef434d86b432e6e77911145c96b15dff06c 100644 +--- a/Source/WebKit/UIProcess/wc/DrawingAreaProxyWC.cpp ++++ b/Source/WebKit/UIProcess/wc/DrawingAreaProxyWC.cpp +@@ -35,6 +35,7 @@ + #include "MessageSenderInlines.h" + #include "UpdateInfo.h" + #include "WebPageProxy.h" ++#include "WebPageInspectorController.h" + #include + + namespace WebKit { +@@ -116,6 +117,19 @@ void DrawingAreaProxyWC::discardBackingStore() + m_backingStore = std::nullopt; + } + ++void DrawingAreaProxyWC::captureFrame() ++{ ++ if (!m_backingStore) ++ return; ++ auto surface = m_backingStore->surface(); ++ if (!surface) ++ return; ++ auto image = surface->makeImageSnapshot(); ++ if (!image) ++ return; ++ page()->inspectorController().didPaint(WTFMove(image)); ++} ++ + } // namespace WebKit + + #endif // USE(GRAPHICS_LAYER_WC) +diff --git a/Source/WebKit/UIProcess/wc/DrawingAreaProxyWC.h b/Source/WebKit/UIProcess/wc/DrawingAreaProxyWC.h +index 5ede972f4b6efe213ccdd866ef77594acfcbf162..67f66b7c685eea3478b6386e1ec449d94dfff17f 100644 +--- a/Source/WebKit/UIProcess/wc/DrawingAreaProxyWC.h ++++ b/Source/WebKit/UIProcess/wc/DrawingAreaProxyWC.h +@@ -50,6 +50,8 @@ public: + + void paint(PlatformPaintContextPtr, const WebCore::IntRect&, WebCore::Region& unpaintedRegion); + ++ void captureFrame(); ++ + private: + DrawingAreaProxyWC(WebPageProxy&, WebProcessProxy&); + diff --git a/Source/WebKit/UIProcess/win/InspectorPlaywrightAgentClientWin.cpp b/Source/WebKit/UIProcess/win/InspectorPlaywrightAgentClientWin.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4f54a9445e5a7ecdb750c5c521da4f397776e633 @@ -18101,7 +17698,7 @@ index 0000000000000000000000000000000000000000..4f54a9445e5a7ecdb750c5c521da4f39 +#endif // ENABLE(REMOTE_INSPECTOR) diff --git a/Source/WebKit/UIProcess/win/InspectorPlaywrightAgentClientWin.h b/Source/WebKit/UIProcess/win/InspectorPlaywrightAgentClientWin.h new file mode 100644 -index 0000000000000000000000000000000000000000..df18883b2b7d22d73540cb084d3dd5291231097d +index 0000000000000000000000000000000000000000..18be1f2e544e3069df48cdd1e55ad8536f57802a --- /dev/null +++ b/Source/WebKit/UIProcess/win/InspectorPlaywrightAgentClientWin.h @@ -0,0 +1,60 @@ @@ -18146,7 +17743,7 @@ index 0000000000000000000000000000000000000000..df18883b2b7d22d73540cb084d3dd529 +namespace WebKit { + +class InspectorPlaywrightAgentClientWin : public InspectorPlaywrightAgentClient { -+ WTF_MAKE_FAST_ALLOCATED; ++ WTF_DEPRECATED_MAKE_FAST_ALLOCATED(InspectorPlaywrightAgentClientWin); +public: + InspectorPlaywrightAgentClientWin(ConfigureDataStoreCallback, CreatePageCallback, QuitCallback); + ~InspectorPlaywrightAgentClientWin() override = default; @@ -18361,10 +17958,10 @@ index 0000000000000000000000000000000000000000..8b474c730139b44a13c9d5b2d13ee204 + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/win/WebView.cpp b/Source/WebKit/UIProcess/win/WebView.cpp -index 2487c6de5b717dfea02a6b5a127c815019582d3c..eae5b89de5f54e94f439a7fbe72547743ad96749 100644 +index bf8a0718e74bf9b1243dcb4bb504310e9ebbd6c2..33b865af669a3db4e3b23874f86cafa9794a71b7 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 +@@ -569,7 +569,7 @@ LRESULT WebView::onSizeEvent(HWND hwnd, UINT, WPARAM, LPARAM lParam, bool& handl float intrinsicDeviceScaleFactor = deviceScaleFactorForWindow(hwnd); if (m_page) m_page->setIntrinsicDeviceScaleFactor(intrinsicDeviceScaleFactor); @@ -18914,10 +18511,10 @@ index 9b688ad328317fea4fd96ce66e9714bad8f0f937..402a36a9c565e13ec298aa7f014f0d92 } // namespace WebKit diff --git a/Source/WebKit/WebKit.xcodeproj/project.pbxproj b/Source/WebKit/WebKit.xcodeproj/project.pbxproj -index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279ad3acae1 100644 +index 4a60ae331da2220209411fb34e9b5ee60e3171aa..13cadd4f6c6177121799653ed7bab89706eb8301 100644 --- a/Source/WebKit/WebKit.xcodeproj/project.pbxproj +++ b/Source/WebKit/WebKit.xcodeproj/project.pbxproj -@@ -1565,6 +1565,7 @@ +@@ -1562,6 +1562,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 */; }; @@ -18925,7 +18522,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 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 */; }; -@@ -2352,6 +2353,18 @@ +@@ -2350,6 +2351,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 */; }; @@ -18944,7 +18541,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 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, ); }; }; -@@ -2452,6 +2465,8 @@ +@@ -2450,6 +2463,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 */; }; @@ -18953,7 +18550,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 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 */; }; -@@ -2491,6 +2506,9 @@ +@@ -2489,6 +2504,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 */; }; @@ -18963,7 +18560,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 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, ); }; }; -@@ -6458,6 +6476,7 @@ +@@ -6469,6 +6487,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 = ""; }; @@ -18971,7 +18568,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 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 = ""; }; -@@ -8191,6 +8210,19 @@ +@@ -8205,6 +8224,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 = ""; }; @@ -18991,7 +18588,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 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 = ""; }; -@@ -8365,6 +8397,8 @@ +@@ -8382,6 +8414,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 = ""; }; @@ -19000,7 +18597,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 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 = ""; }; -@@ -8418,6 +8452,14 @@ +@@ -8435,6 +8469,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 = ""; }; @@ -19015,7 +18612,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 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 = ""; }; -@@ -8850,6 +8892,7 @@ +@@ -8898,6 +8940,7 @@ 3766F9EE189A1241003CF19B /* JavaScriptCore.framework in Frameworks */, 3766F9F1189A1254003CF19B /* libicucore.dylib in Frameworks */, 7B9FC5BB28A5233B007570E7 /* libWebKitPlatform.a in Frameworks */, @@ -19023,7 +18620,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 3766F9EF189A1244003CF19B /* QuartzCore.framework in Frameworks */, 37694525184FC6B600CDE21F /* Security.framework in Frameworks */, 37BEC4DD1948FC6A008B4286 /* WebCore.framework in Frameworks */, -@@ -12053,6 +12096,7 @@ +@@ -12121,6 +12164,7 @@ 99788ACA1F421DCA00C08000 /* _WKAutomationSessionConfiguration.mm */, 990D28A81C6404B000986977 /* _WKAutomationSessionDelegate.h */, 990D28AF1C65203900986977 /* _WKAutomationSessionInternal.h */, @@ -19031,7 +18628,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 5C4609E222430E4C009943C2 /* _WKContentRuleListAction.h */, 5C4609E322430E4D009943C2 /* _WKContentRuleListAction.mm */, 5C4609E422430E4D009943C2 /* _WKContentRuleListActionInternal.h */, -@@ -13433,6 +13477,7 @@ +@@ -13515,6 +13559,7 @@ E34B110C27C46BC6006D2F2E /* libWebCoreTestShim.dylib */, E34B110F27C46D09006D2F2E /* libWebCoreTestSupport.dylib */, DDE992F4278D06D900F60D26 /* libWebKitAdditions.a */, @@ -19039,7 +18636,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 57A9FF15252C6AEF006A2040 /* libWTF.a */, 5750F32A2032D4E500389347 /* LocalAuthentication.framework */, 570DAAB0230273D200E8FC04 /* NearField.framework */, -@@ -14015,6 +14060,12 @@ +@@ -14101,6 +14146,12 @@ children = ( 9197940423DBC4BB00257892 /* InspectorBrowserAgent.cpp */, 9197940323DBC4BB00257892 /* InspectorBrowserAgent.h */, @@ -19052,7 +18649,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 ); path = Agents; sourceTree = ""; -@@ -14023,6 +14074,7 @@ +@@ -14109,6 +14160,7 @@ isa = PBXGroup; children = ( A5D3504D1D78F0D2005124A9 /* RemoteWebInspectorUIProxyMac.mm */, @@ -19060,7 +18657,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 1CA8B935127C774E00576C2B /* WebInspectorUIProxyMac.mm */, 99A7ACE326012919006D57FD /* WKInspectorResourceURLSchemeHandler.h */, 99A7ACE42601291A006D57FD /* WKInspectorResourceURLSchemeHandler.mm */, -@@ -14778,6 +14830,7 @@ +@@ -14869,6 +14921,7 @@ E1513C65166EABB200149FCB /* AuxiliaryProcessProxy.h */, 46A2B6061E5675A200C3DEDA /* BackgroundProcessResponsivenessTimer.cpp */, 46A2B6071E5675A200C3DEDA /* BackgroundProcessResponsivenessTimer.h */, @@ -19068,7 +18665,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 5C6D69352AC3935D0099BDAF /* BrowsingContextGroup.cpp */, 5C6D69362AC3935D0099BDAF /* BrowsingContextGroup.h */, 5CA98549210BEB5A0057EB6B /* BrowsingWarning.h */, -@@ -14802,6 +14855,8 @@ +@@ -14893,6 +14946,8 @@ BC06F43912DBCCFB002D78DE /* GeolocationPermissionRequestProxy.cpp */, BC06F43812DBCCFB002D78DE /* GeolocationPermissionRequestProxy.h */, 2DD5A72A1EBF09A7009BA597 /* HiddenPageThrottlingAutoIncreasesCounter.h */, @@ -19077,7 +18674,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 5CEABA2B2333251400797797 /* LegacyGlobalSettings.cpp */, 5CEABA2A2333247700797797 /* LegacyGlobalSettings.h */, 31607F3819627002009B87DA /* LegacySessionStateCoding.h */, -@@ -14832,6 +14887,7 @@ +@@ -14923,6 +14978,7 @@ 4683569B21E81CC7006E27A3 /* ProvisionalPageProxy.cpp */, 4683569A21E81CC7006E27A3 /* ProvisionalPageProxy.h */, 411B89CB27B2B89600F9EBD3 /* QueryPermissionResultCallback.h */, @@ -19085,7 +18682,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 5CCB54DC2A4FEA6A0005FAA8 /* RemotePageDrawingAreaProxy.cpp */, 5CCB54DB2A4FEA6A0005FAA8 /* RemotePageDrawingAreaProxy.h */, FABBBC802D35AC6800820017 /* RemotePageFullscreenManagerProxy.cpp */, -@@ -14939,6 +14995,8 @@ +@@ -15030,6 +15086,8 @@ BC7B6204129A0A6700D174A4 /* WebPageGroup.h */, 2D9EA3101A96D9EB002D2807 /* WebPageInjectedBundleClient.cpp */, 2D9EA30E1A96CBFF002D2807 /* WebPageInjectedBundleClient.h */, @@ -19094,7 +18691,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 9B7F8A502C785725000057F3 /* WebPageLoadTiming.h */, BC111B0B112F5E4F00337BAB /* WebPageProxy.cpp */, BC032DCB10F4389F0058C15A /* WebPageProxy.h */, -@@ -15118,6 +15176,7 @@ +@@ -15216,6 +15274,7 @@ BC646C1911DD399F006455B0 /* WKBackForwardListItemRef.h */, BC646C1611DD399F006455B0 /* WKBackForwardListRef.cpp */, BC646C1711DD399F006455B0 /* WKBackForwardListRef.h */, @@ -19102,7 +18699,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 BCB9E24A1120E15C00A137E0 /* WKContext.cpp */, BCB9E2491120E15C00A137E0 /* WKContext.h */, 1AE52F9319201F6B00A1FA37 /* WKContextConfigurationRef.cpp */, -@@ -15694,6 +15753,9 @@ +@@ -15794,6 +15853,9 @@ 07EF07592745A8160066EA04 /* DisplayCaptureSessionManager.h */, 07EF07582745A8160066EA04 /* DisplayCaptureSessionManager.mm */, 7AFA6F682A9F57C50055322A /* DisplayLinkMac.cpp */, @@ -19112,7 +18709,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 1AFDE65B1954E8D500C48FFA /* LegacySessionStateCoding.cpp */, 0FCB4E5818BBE3D9000FCFC9 /* PageClientImplMac.h */, 0FCB4E5918BBE3D9000FCFC9 /* PageClientImplMac.mm */, -@@ -15717,6 +15779,8 @@ +@@ -15817,6 +15879,8 @@ E568B92120A3AC6A00E3C856 /* WebDataListSuggestionsDropdownMac.mm */, E55CD20124D09F1F0042DB9C /* WebDateTimePickerMac.h */, E55CD20224D09F1F0042DB9C /* WebDateTimePickerMac.mm */, @@ -19121,7 +18718,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 BC857E8512B71EBB00EDEB2E /* WebPageProxyMac.mm */, BC5750951268F3C6006F0F12 /* WebPopupMenuProxyMac.h */, BC5750961268F3C6006F0F12 /* WebPopupMenuProxyMac.mm */, -@@ -16826,6 +16890,7 @@ +@@ -16923,6 +16987,7 @@ 99788ACB1F421DDA00C08000 /* _WKAutomationSessionConfiguration.h in Headers */, 990D28AC1C6420CF00986977 /* _WKAutomationSessionDelegate.h in Headers */, 990D28B11C65208D00986977 /* _WKAutomationSessionInternal.h in Headers */, @@ -19129,7 +18726,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 5C4609E7224317B4009943C2 /* _WKContentRuleListAction.h in Headers */, 5C4609E8224317BB009943C2 /* _WKContentRuleListActionInternal.h in Headers */, 9B4CE9512CD99B7C00351173 /* _WKContentWorldConfiguration.h in Headers */, -@@ -17139,6 +17204,7 @@ +@@ -17242,6 +17307,7 @@ E170876C16D6CA6900F99226 /* BlobRegistryProxy.h in Headers */, 4F601432155C5AA2001FBDE0 /* BlockingResponseMap.h in Headers */, 1A5705111BE410E600874AF1 /* BlockSPI.h in Headers */, @@ -19137,7 +18734,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 5CA9854A210BEB640057EB6B /* BrowsingWarning.h in Headers */, A7E69BCC2B2117A100D43D3F /* BufferAndBackendInfo.h in Headers */, BC3065FA1259344E00E71278 /* CacheModel.h in Headers */, -@@ -17321,7 +17387,11 @@ +@@ -17425,7 +17491,11 @@ BC14DF77120B5B7900826C0C /* InjectedBundleScriptWorld.h in Headers */, CE550E152283752200D28791 /* InsertTextOptions.h in Headers */, 9197940523DBC4BB00257892 /* InspectorBrowserAgent.h in Headers */, @@ -19149,7 +18746,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 A5E391FD2183C1F800C8FB31 /* InspectorTargetProxy.h in Headers */, C5BCE5DF1C50766A00CDE3FA /* InteractionInformationAtPosition.h in Headers */, 2D4D2C811DF60BF3002EB10C /* InteractionInformationRequest.h in Headers */, -@@ -17581,6 +17651,7 @@ +@@ -17685,6 +17755,7 @@ 0F6E7C532C4C386800F1DB85 /* RemoteDisplayListRecorderMessages.h in Headers */, F451C0FE2703B263002BA03B /* RemoteDisplayListRecorderProxy.h in Headers */, A78A5FE42B0EB39E005036D3 /* RemoteImageBufferSetIdentifier.h in Headers */, @@ -19157,7 +18754,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 2D47B56D1810714E003A3AEE /* RemoteLayerBackingStore.h in Headers */, 2DDF731518E95060004F5A66 /* RemoteLayerBackingStoreCollection.h in Headers */, 1AB16AEA164B3A8800290D62 /* RemoteLayerTreeContext.h in Headers */, -@@ -17638,6 +17709,7 @@ +@@ -17740,6 +17811,7 @@ E1E552C516AE065F004ED653 /* SandboxInitializationParameters.h in Headers */, E36FF00327F36FBD004BE21A /* SandboxStateVariables.h in Headers */, 7BAB111025DD02B3008FC479 /* ScopedActiveMessageReceiveQueue.h in Headers */, @@ -19165,7 +18762,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 6D4DF20C2D824242001F964C /* ScreenTimeWebsiteDataSupport.h in Headers */, 463BB93A2B9D08D80098C5C3 /* ScriptMessageHandlerIdentifier.h in Headers */, F4E28A362C923814008120DD /* ScriptTrackingPrivacyFilter.h in Headers */, -@@ -18002,6 +18074,8 @@ +@@ -18106,6 +18178,8 @@ 939EF87029D112EE00F23AEE /* WebPageInlines.h in Headers */, 9197940823DBC4CB00257892 /* WebPageInspectorAgentBase.h in Headers */, A513F5402154A5D700662841 /* WebPageInspectorController.h in Headers */, @@ -19174,7 +18771,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 A543E30C215C8A8D00279CD9 /* WebPageInspectorTarget.h in Headers */, A543E30D215C8A9000279CD9 /* WebPageInspectorTargetController.h in Headers */, A543E307215AD13700279CD9 /* WebPageInspectorTargetFrontendChannel.h in Headers */, -@@ -20621,7 +20695,43 @@ +@@ -20746,7 +20820,43 @@ 522F792928D50EBB0069B45B /* HidService.mm in Sources */, 2749F6442146561B008380BF /* InjectedBundleNodeHandle.cpp in Sources */, 2749F6452146561E008380BF /* InjectedBundleRangeHandle.cpp in Sources */, @@ -19218,7 +18815,7 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 1C5DC45F2909B05A0061EC62 /* JSWebExtensionWrapperCocoa.mm in Sources */, C14D37FE24ACE086007FF014 /* LaunchServicesDatabaseManager.mm in Sources */, C1710CF724AA643200D7C112 /* LaunchServicesDatabaseObserver.mm in Sources */, -@@ -21023,6 +21133,8 @@ +@@ -21148,6 +21258,8 @@ 078B04A02CF18EAB00B453A6 /* WebPage+NavigationPreferences.swift in Sources */, 071467782DFE84E500F77867 /* WebPage+Transferable.swift in Sources */, 07CB79962CE9435700199C49 /* WebPage.swift in Sources */, @@ -19228,10 +18825,10 @@ index 54b3c159ca5c86663abb3c67cacce504aff0bc0b..d59f2a64a2be81756d44a1e6ebb7f279 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 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548ee42dec6c 100644 +index c445673e74cb8a98ffdf478325305432914f2f7a..5bcbaa36a7b191b480b9983aaf58b11410671dff 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 +@@ -272,6 +272,11 @@ void WebLoaderStrategy::scheduleLoad(ResourceLoader& resourceLoader, CachedResou } #endif @@ -19243,7 +18840,7 @@ index 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548e #if ENABLE(PDFJS) if (tryLoadingUsingPDFJSHandler(resourceLoader, trackingParameters)) return; -@@ -285,12 +290,16 @@ void WebLoaderStrategy::scheduleLoad(ResourceLoader& resourceLoader, CachedResou +@@ -286,12 +291,16 @@ void WebLoaderStrategy::scheduleLoad(ResourceLoader& resourceLoader, CachedResou } if (InspectorInstrumentationWebKit::shouldInterceptRequest(resourceLoader)) { @@ -19266,7 +18863,7 @@ index 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548e } WEBLOADERSTRATEGY_RELEASE_LOG_FORWARDABLE(WEBLOADERSTRATEGY_SCHEDULELOAD); -@@ -415,7 +424,7 @@ static void addParametersShared(const LocalFrame* frame, NetworkResourceLoadPara +@@ -416,7 +425,7 @@ static void addParametersShared(const LocalFrame* frame, NetworkResourceLoadPara parameters.linkPreconnectEarlyHintsEnabled = mainFrame->settings().linkPreconnectEarlyHintsEnabled(); } @@ -19275,12 +18872,12 @@ index 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548e { auto identifier = *resourceLoader.identifier(); -@@ -427,10 +436,10 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -428,10 +437,10 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL && resourceLoader.frameLoader()->notifier().isInitialRequestIdentifier(identifier) ? MainFrameMainResource::Yes : MainFrameMainResource::No; if (!page->allowsLoadFromURL(request.url(), mainFrameMainResource)) { -- RunLoop::protectedMain()->dispatch([resourceLoader = Ref { resourceLoader }, error = blockedError(request)] { -+ RunLoop::protectedMain()->dispatch([resourceLoader = Ref { resourceLoader }, error = platformStrategies()->loaderStrategy()->blockedError(request)] { +- RunLoop::mainSingleton().dispatch([resourceLoader = Ref { resourceLoader }, error = blockedError(request)] { ++ RunLoop::mainSingleton().dispatch([resourceLoader = Ref { resourceLoader }, error = platformStrategies()->loaderStrategy()->blockedError(request)] { resourceLoader->didFail(error); }); - return; @@ -19288,7 +18885,7 @@ index 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548e } } -@@ -440,14 +449,6 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -441,14 +450,6 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL LOG(NetworkScheduling, "(WebProcess) WebLoaderStrategy::scheduleLoad, url '%s' will be scheduled with the NetworkProcess with priority %d, storedCredentialsPolicy %i", resourceLoader.url().string().latin1().data(), static_cast(resourceLoader.request().priority()), (int)storedCredentialsPolicy); @@ -19303,7 +18900,7 @@ index 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548e loadParameters.identifier = identifier; loadParameters.parentPID = legacyPresentingApplicationPID(); loadParameters.contentSniffingPolicy = contentSniffingPolicy; -@@ -531,14 +532,11 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -532,14 +533,11 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL if (loadParameters.options.mode != FetchOptions::Mode::Navigate) { ASSERT(loadParameters.sourceOrigin); @@ -19321,7 +18918,7 @@ index 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548e loadParameters.isMainFrameNavigation = isMainFrameNavigation; if (loadParameters.isMainFrameNavigation && document) -@@ -589,6 +587,25 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -590,6 +588,25 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL if (RefPtr frameLoader = resourceLoader.frameLoader()) loadParameters.requiredCookiesVersion = frameLoader->requiredCookiesVersion(); @@ -19347,7 +18944,7 @@ index 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548e std::optional existingNetworkResourceLoadIdentifierToResume; if (loadParameters.isMainFrameNavigation) existingNetworkResourceLoadIdentifierToResume = std::exchange(m_existingNetworkResourceLoadIdentifierToResume, std::nullopt); -@@ -603,7 +620,7 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -604,7 +621,7 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL } auto loader = WebResourceLoader::create(resourceLoader, trackingParameters); @@ -19356,7 +18953,7 @@ index 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548e } void WebLoaderStrategy::scheduleInternallyFailedLoad(WebCore::ResourceLoader& resourceLoader) -@@ -1021,7 +1038,7 @@ void WebLoaderStrategy::didFinishPreconnection(WebCore::ResourceLoaderIdentifier +@@ -1022,7 +1039,7 @@ void WebLoaderStrategy::didFinishPreconnection(WebCore::ResourceLoaderIdentifier bool WebLoaderStrategy::isOnLine() const { @@ -19365,7 +18962,7 @@ index 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548e } void WebLoaderStrategy::addOnlineStateChangeListener(Function&& listener) -@@ -1047,6 +1064,11 @@ void WebLoaderStrategy::isResourceLoadFinished(CachedResource& resource, Complet +@@ -1048,6 +1065,11 @@ void WebLoaderStrategy::isResourceLoadFinished(CachedResource& resource, Complet void WebLoaderStrategy::setOnLineState(bool isOnLine) { @@ -19377,7 +18974,7 @@ index 8e116cc2cd63a59282b330acb00f42a926ecb33b..1b979a29a149803177300c93aec4548e if (m_isOnLine == isOnLine) return; -@@ -1055,6 +1077,12 @@ void WebLoaderStrategy::setOnLineState(bool isOnLine) +@@ -1056,6 +1078,12 @@ void WebLoaderStrategy::setOnLineState(bool isOnLine) listener(isOnLine); } @@ -19421,10 +19018,10 @@ index af1f8eb6b578367e8cb818b1c6020377c5a07a37..41f1a38348b11aa0d99fc9fa429fe6b8 } // namespace WebKit diff --git a/Source/WebKit/WebProcess/Network/WebResourceLoader.cpp b/Source/WebKit/WebProcess/Network/WebResourceLoader.cpp -index 6d448ac1d302be3e9ad938ade807d26ddd39d103..06d1995872db8de9edc0ad14f0ab556dec75eb7a 100644 +index 9e8966c65c8c0327b437446b749fe8c22c493e27..9f5afce37925d609f48febcd0054e2d8c0768c89 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 +@@ -204,9 +204,6 @@ void WebResourceLoader::didReceiveResponse(ResourceResponse&& response, PrivateR coreLoader->didReceiveResponse(ResourceResponse { inspectorResponse }, [this, protectedThis = Ref { *this }, interceptedRequestIdentifier, policyDecisionCompletionHandler = WTFMove(policyDecisionCompletionHandler), overrideData = WTFMove(overrideData)]() mutable { RefPtr coreLoader = m_coreLoader; @@ -19434,7 +19031,7 @@ index 6d448ac1d302be3e9ad938ade807d26ddd39d103..06d1995872db8de9edc0ad14f0ab556d if (!m_coreLoader || !coreLoader->identifier()) { m_interceptController.continueResponse(interceptedRequestIdentifier); return; -@@ -221,6 +218,8 @@ void WebResourceLoader::didReceiveResponse(ResourceResponse&& response, PrivateR +@@ -223,6 +220,8 @@ void WebResourceLoader::didReceiveResponse(ResourceResponse&& response, PrivateR } }); }); @@ -19444,10 +19041,10 @@ index 6d448ac1d302be3e9ad938ade807d26ddd39d103..06d1995872db8de9edc0ad14f0ab556d } diff --git a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp -index de8c8f2890b146c6dd4f34c121b92402900e8df5..11792945a16cca303a9b2d3c101edf835f60b398 100644 +index a3c66b8d8ae186866f984436460f32d5d6f4a347..ad99b0de84c581b369df06a27bac6edaf56b80cb 100644 --- a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp +++ b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp -@@ -534,6 +534,9 @@ void WebChromeClient::addMessageToConsole(MessageSource source, MessageLevel lev +@@ -539,6 +539,9 @@ void WebChromeClient::addMessageToConsole(MessageSource source, MessageLevel lev if (!page) return; @@ -19458,20 +19055,20 @@ index de8c8f2890b146c6dd4f34c121b92402900e8df5..11792945a16cca303a9b2d3c101edf83 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 dca2495f0196552e79c73bb0b7466b5c4fa0be46..999a7ab4eef2b8cc1a65b5a26cd69e369dcb7ab7 100644 +index 771f8c77b51976e3aa332fa10befd9fd5b258edc..f46b5bf3a6d5a1cb832b602e0c024dbde808b7b9 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 protectedPage()->allowedDragSourceActions(); } --#if !PLATFORM(COCOA) && !PLATFORM(GTK) +-#if !PLATFORM(COCOA) && !PLATFORM(GTK) && !PLATFORM(WPE) +#if !PLATFORM(COCOA) && !PLATFORM(GTK) && !PLATFORM(WPE) && !PLATFORM(WIN) - void WebDragClient::startDrag(DragItem, DataTransfer&, Frame&, const std::optional&) + 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 +index f0e2863e578c60cfc0ddc98a50b45061fad1fc0b..81e59abd8c5201d0550bff3ad8ecf284765cfc40 100644 --- a/Source/WebKit/WebProcess/WebCoreSupport/WebUserMediaClient.cpp +++ b/Source/WebKit/WebProcess/WebCoreSupport/WebUserMediaClient.cpp @@ -26,6 +26,7 @@ @@ -19483,7 +19080,7 @@ index bb924c778ae3999d549b14e6d810b32167baece4..341355502afeaf69827308fee41ce964 #include #include diff --git a/Source/WebKit/WebProcess/WebCoreSupport/mac/WebDragClientMac.mm b/Source/WebKit/WebProcess/WebCoreSupport/mac/WebDragClientMac.mm -index 0abeb42354ed65e20d5ad5ab72c31bf00e49d528..259caa5c016b7f5c7eab156bbe4f0ce105339f76 100644 +index 5819df852bf76f0ed4ce50105b93c0fd697123d6..2fc578b7ff70877738278fe7006f14262e82e7a2 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) @@ -19498,7 +19095,7 @@ index 0abeb42354ed65e20d5ad5ab72c31bf00e49d528..259caa5c016b7f5c7eab156bbe4f0ce1 diff --git a/Source/WebKit/WebProcess/WebCoreSupport/win/WebDragClientWin.cpp b/Source/WebKit/WebProcess/WebCoreSupport/win/WebDragClientWin.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..2d2ab369bfc1b42695dbc207e1d6e05deb16990f +index 0000000000000000000000000000000000000000..741ef5b11fe8718529105339cda0fab597bad9de --- /dev/null +++ b/Source/WebKit/WebProcess/WebCoreSupport/win/WebDragClientWin.cpp @@ -0,0 +1,58 @@ @@ -19537,7 +19134,7 @@ index 0000000000000000000000000000000000000000..2d2ab369bfc1b42695dbc207e1d6e05d +#include "WebPageProxyMessages.h" +#include +#include -+#include ++#include +#include +#include +#include @@ -19551,7 +19148,7 @@ index 0000000000000000000000000000000000000000..2d2ab369bfc1b42695dbc207e1d6e05d +{ +} + -+void WebDragClient::startDrag(DragItem, DataTransfer& dataTransfer, Frame& frame, const std::optional&) ++void WebDragClient::startDrag(DragItem, DataTransfer& dataTransfer, Frame& frame, const std::optional&) +{ + m_page->willStartDrag(); + m_page->send(Messages::WebPageProxy::StartDrag(dataTransfer.pasteboard().createDragDataMap())); @@ -19560,73 +19157,8 @@ index 0000000000000000000000000000000000000000..2d2ab369bfc1b42695dbc207e1d6e05d +}; // namespace WebKit. + +#endif // ENABLE(DRAG_SUPPORT) -diff --git a/Source/WebKit/WebProcess/WebCoreSupport/wpe/WebDragClientWPE.cpp b/Source/WebKit/WebProcess/WebCoreSupport/wpe/WebDragClientWPE.cpp -new file mode 100644 -index 0000000000000000000000000000000000000000..d069b5eba1a64add1a2c370f8d519826ec7be213 ---- /dev/null -+++ b/Source/WebKit/WebProcess/WebCoreSupport/wpe/WebDragClientWPE.cpp -@@ -0,0 +1,59 @@ -+/* -+ * Copyright (C) 2011 Igalia S.L. -+ * -+ * Redistribution and use in source and binary forms, with or without -+ * modification, are permitted provided that the following conditions -+ * are met: -+ * 1. Redistributions of source code must retain the above copyright -+ * notice, this list of conditions and the following disclaimer. -+ * 2. Redistributions in binary form must reproduce the above copyright -+ * notice, this list of conditions and the following disclaimer in the -+ * documentation and/or other materials provided with the distribution. -+ * -+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' -+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS -+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -+ * THE POSSIBILITY OF SUCH DAMAGE. -+ */ -+ -+#include "config.h" -+#include "WebDragClient.h" -+ -+#if ENABLE(DRAG_SUPPORT) -+ -+#include "WebPage.h" -+#include "WebPageProxyMessages.h" -+#include -+#include -+#include -+#include -+#include -+#include -+ -+#include -+ -+namespace WebKit { -+using namespace WebCore; -+ -+void WebDragClient::didConcludeEditDrag() -+{ -+} -+ -+void WebDragClient::startDrag(DragItem, DataTransfer& dataTransfer, Frame&, const std::optional&) -+{ -+ m_page->willStartDrag(); -+ -+ std::optional handle; -+ m_page->send(Messages::WebPageProxy::StartDrag(dataTransfer.pasteboard().selectionData(), dataTransfer.sourceOperationMask(), WTFMove(handle), dataTransfer.dragLocation())); -+} -+ -+}; // namespace WebKit. -+ -+#endif // ENABLE(DRAG_SUPPORT) diff --git a/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/DrawingAreaCoordinatedGraphics.cpp b/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/DrawingAreaCoordinatedGraphics.cpp -index 5fed15fd5dbbfbaff305ce26a8bcf5ba3d0435d9..fec04b9a4d640dcf47e0a2312d8b71234bf10d09 100644 +index f3fad38c1e055231a008724c50312248bcaccbaf..5c6f7c4f260904cc3c88c2c28e626d90432c0ac6 100644 --- a/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/DrawingAreaCoordinatedGraphics.cpp +++ b/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/DrawingAreaCoordinatedGraphics.cpp @@ -39,6 +39,7 @@ @@ -19638,10 +19170,10 @@ index 5fed15fd5dbbfbaff305ce26a8bcf5ba3d0435d9..fec04b9a4d640dcf47e0a2312d8b7123 #include #include diff --git a/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/LayerTreeHost.h b/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/LayerTreeHost.h -index 5c8694cb4229ee87c50f68f8c24b47e5598a5aee..581637fb988e0d9b465a7029fb1bac9884c09b6c 100644 +index 4ffe3d98d023cd6f415782d59e40d52bb15ab1be..d8d38f57fc57ab51585aaeea8c9c1e92bef5ce2d 100644 --- a/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/LayerTreeHost.h +++ b/Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/LayerTreeHost.h -@@ -145,6 +145,7 @@ public: +@@ -140,6 +140,7 @@ public: #if PLATFORM(WPE) && USE(GBM) && ENABLE(WPE_PLATFORM) void preferredBufferFormatsDidChange(); #endif @@ -19650,7 +19182,7 @@ index 5c8694cb4229ee87c50f68f8c24b47e5598a5aee..581637fb988e0d9b465a7029fb1bac98 void updateRootLayer(); WebCore::FloatRect visibleContentsRect() const; diff --git a/Source/WebKit/WebProcess/WebPage/DrawingArea.cpp b/Source/WebKit/WebProcess/WebPage/DrawingArea.cpp -index e4b6fc981ce302979fd3280e9602bcbea91b9d1f..9ed589d3f29b17c18018798642f34fa375afc1a2 100644 +index 04f3e2cebec36db9487ee5b5d4f33eb2ca8db3b8..4677b87d3cdfdc98e4b10747bf483df3ec1feec0 100644 --- a/Source/WebKit/WebProcess/WebPage/DrawingArea.cpp +++ b/Source/WebKit/WebProcess/WebPage/DrawingArea.cpp @@ -27,6 +27,7 @@ @@ -19662,7 +19194,7 @@ index e4b6fc981ce302979fd3280e9602bcbea91b9d1f..9ed589d3f29b17c18018798642f34fa3 #include "WebPage.h" #include "WebPageCreationParameters.h" diff --git a/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp b/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp -index c42601a113c5f4f390630b3f20328de17f4f7d31..1a8f3dcccae5c09220f0f519d275eeaba83b1a5e 100644 +index 7ff2b1ad2dd19a2b25c5f5c3afbe7f580dd02c4e..eceb689621ed65f4e19461489e43e9f1690f0ea1 100644 --- a/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp +++ b/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp @@ -44,6 +44,7 @@ @@ -19687,31 +19219,31 @@ index c42601a113c5f4f390630b3f20328de17f4f7d31..1a8f3dcccae5c09220f0f519d275eeab String WebCookieJar::cookiesInPartitionedCookieStorage(const WebCore::Document&, const URL&, const WebCore::SameSiteInfo&) const diff --git a/Source/WebKit/WebProcess/WebPage/WebCookieJar.h b/Source/WebKit/WebProcess/WebPage/WebCookieJar.h -index d5bac4fe6d5103b4e752a7219d7870d4cddcaf27..033effbb445f5da6db5798594e2dbef34afec363 100644 +index a7ad18fc1201e5de2cc1528539a765599ecfc41e..ebea233bc54ab6e56fb7093ff63e2368e8670ccd 100644 --- a/Source/WebKit/WebProcess/WebPage/WebCookieJar.h +++ b/Source/WebKit/WebProcess/WebPage/WebCookieJar.h -@@ -77,6 +77,8 @@ public: - - void clearCache() final; +@@ -81,6 +81,8 @@ public: + void setOptInCookiePartitioningEnabled(bool); + #endif + void setCookieFromResponse(WebCore::ResourceLoader&, const String& setCookieValue); + - #if HAVE(ALLOW_ONLY_PARTITIONED_COOKIES) - void setOptInCookiePartitioningEnabled(bool); - #endif + private: + WebCookieJar(); + diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp -index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98af4924ff6 100644 +index 504f82d3895a6389ed6f2375942ee0df7eef0595..100e1126196fe8c795f1822eb5bace4680bc5ae0 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp +++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp -@@ -244,6 +244,7 @@ +@@ -247,6 +247,7 @@ #include #include #include +#include #include + #include #include - #include -@@ -1165,6 +1166,12 @@ WebPage::WebPage(PageIdentifier pageID, WebPageCreationParameters&& parameters) +@@ -1178,6 +1179,12 @@ WebPage::WebPage(PageIdentifier pageID, WebPageCreationParameters&& parameters) setLinkDecorationFilteringData(WTFMove(parameters.linkDecorationFilteringData)); setAllowedQueryParametersForAdvancedPrivacyProtections(WTFMove(parameters.allowedQueryParametersForAdvancedPrivacyProtections)); #endif @@ -19724,7 +19256,7 @@ index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98a if (parameters.windowFeatures) { page->applyWindowFeatures(*parameters.windowFeatures); page->chrome().show(); -@@ -2098,6 +2105,22 @@ void WebPage::loadDidCommitInAnotherProcess(WebCore::FrameIdentifier frameID, st +@@ -2114,6 +2121,22 @@ void WebPage::loadDidCommitInAnotherProcess(WebCore::FrameIdentifier frameID, st frame->loadDidCommitInAnotherProcess(layerHostingContextIdentifier); } @@ -19747,7 +19279,7 @@ index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98a 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); -@@ -2298,7 +2321,9 @@ void WebPage::stopLoading() +@@ -2314,7 +2337,9 @@ void WebPage::stopLoading() void WebPage::stopLoadingDueToProcessSwap() { SetForScope isStoppingLoadingDueToProcessSwap(m_isStoppingLoadingDueToProcessSwap, true); @@ -19757,7 +19289,7 @@ index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98a } bool WebPage::defersLoading() const -@@ -2874,7 +2899,7 @@ void WebPage::viewportPropertiesDidChange(const ViewportArguments& viewportArgum +@@ -2890,7 +2915,7 @@ void WebPage::viewportPropertiesDidChange(const ViewportArguments& viewportArgum #if PLATFORM(IOS_FAMILY) if (m_viewportConfiguration.setViewportArguments(viewportArguments)) viewportConfigurationChanged(); @@ -19766,7 +19298,7 @@ index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98a // Adjust view dimensions when using fixed layout. RefPtr localMainFrame = this->localMainFrame(); RefPtr view = localMainFrame ? localMainFrame->view() : nullptr; -@@ -3631,6 +3656,13 @@ void WebPage::flushDeferredScrollEvents() +@@ -3682,6 +3707,13 @@ void WebPage::flushDeferredScrollEvents() protectedCorePage()->flushDeferredScrollEvents(); } @@ -19780,7 +19312,7 @@ index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98a void WebPage::flushDeferredDidReceiveMouseEvent() { if (auto info = std::exchange(m_deferredDidReceiveMouseEvent, std::nullopt)) -@@ -3905,6 +3937,97 @@ void WebPage::touchEvent(const WebTouchEvent& touchEvent, CompletionHandler eventModifiers; + eventModifiers = eventModifiers.fromRaw(modifiers); + @@ -19878,7 +19410,7 @@ index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98a #endif void WebPage::cancelPointer(WebCore::PointerID pointerId, const WebCore::IntPoint& documentPoint) -@@ -3993,6 +4116,16 @@ void WebPage::sendMessageToTargetBackend(const String& targetId, const String& m +@@ -4044,6 +4167,16 @@ void WebPage::sendMessageToTargetBackend(const String& targetId, const String& m m_inspectorTargetController->sendMessageToTargetBackend(targetId, message); } @@ -19895,7 +19427,7 @@ index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98a void WebPage::insertNewlineInQuotedContent() { RefPtr frame = corePage()->focusController().focusedOrMainFrame(); -@@ -4236,6 +4369,7 @@ void WebPage::setMainFrameDocumentVisualUpdatesAllowed(bool allowed) +@@ -4287,6 +4420,7 @@ void WebPage::setMainFrameDocumentVisualUpdatesAllowed(bool allowed) void WebPage::show() { send(Messages::WebPageProxy::ShowPage()); @@ -19903,7 +19435,7 @@ index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98a } void WebPage::setIsTakingSnapshotsForApplicationSuspension(bool isTakingSnapshotsForApplicationSuspension) -@@ -5456,7 +5590,7 @@ RefPtr WebPage::protectedNotificationPermi +@@ -5508,7 +5642,7 @@ RefPtr WebPage::protectedNotificationPermi #if ENABLE(DRAG_SUPPORT) @@ -19912,7 +19444,7 @@ index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98a 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) -@@ -7952,6 +8086,10 @@ void WebPage::didCommitLoad(WebFrame* frame) +@@ -8028,6 +8162,10 @@ void WebPage::didCommitLoad(WebFrame* frame) m_needsFixedContainerEdgesUpdate = true; flushDeferredDidReceiveMouseEvent(); @@ -19923,7 +19455,7 @@ index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98a } void WebPage::didFinishDocumentLoad(WebFrame& frame) -@@ -8261,6 +8399,9 @@ Ref WebPage::createDocumentLoader(LocalFrame& frame, ResourceReq +@@ -8337,6 +8475,9 @@ Ref WebPage::createDocumentLoader(LocalFrame& frame, ResourceReq m_allowsContentJavaScriptFromMostRecentNavigation = m_internals->pendingWebsitePolicies->allowsContentJavaScript; WebsitePoliciesData::applyToDocumentLoader(*std::exchange(m_internals->pendingWebsitePolicies, std::nullopt), documentLoader); } @@ -19934,18 +19466,18 @@ index 8913ab7dc32c76a251b57449ff31cc53f9a6b9e0..04beed37ef9f9702a41a75acc17cc98a return documentLoader; diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.h b/Source/WebKit/WebProcess/WebPage/WebPage.h -index 47921d36d270fc271bf966dfe183129993ac7336..91da48500f900ddb3b0eb4e99dbd43ef92230dcf 100644 +index 302e2b70e2e181e4883a90945770162b2e873eaf..d5c5e96f65253e2a7251981a2e8d9a43b7be6e6d 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.h +++ b/Source/WebKit/WebProcess/WebPage/WebPage.h @@ -46,6 +46,7 @@ - #include + #include #include #include +#include #include #include #include -@@ -1286,11 +1287,11 @@ public: +@@ -1316,11 +1317,11 @@ public: void clearSelection(); void restoreSelectionInFocusedEditableElement(); @@ -19959,9 +19491,9 @@ index 47921d36d270fc271bf966dfe183129993ac7336..91da48500f900ddb3b0eb4e99dbd43ef void performDragControllerAction(std::optional, DragControllerAction, WebCore::DragData&&, CompletionHandler, WebCore::DragHandlingMethod, bool, unsigned, WebCore::IntRect, WebCore::IntRect, std::optional)>&&); void performDragOperation(WebCore::DragData&&, SandboxExtensionHandle&&, Vector&&, CompletionHandler&&); #endif -@@ -1308,6 +1309,9 @@ public: +@@ -1338,6 +1339,9 @@ public: #if ENABLE(MODEL_PROCESS) - void modelDragEnded(WebCore::ElementIdentifier); + void modelDragEnded(WebCore::NodeIdentifier); #endif +#if PLATFORM(MAC) + void setDragPasteboardName(const String& pasteboardName) { m_page->setDragPasteboardName(pasteboardName); } @@ -19969,7 +19501,7 @@ index 47921d36d270fc271bf966dfe183129993ac7336..91da48500f900ddb3b0eb4e99dbd43ef #endif #if ENABLE(MODEL_PROCESS) -@@ -1394,8 +1398,11 @@ public: +@@ -1424,8 +1428,11 @@ public: void gestureEvent(WebCore::FrameIdentifier, const WebGestureEvent&, CompletionHandler, bool, std::optional)>&&); #endif @@ -19982,7 +19514,7 @@ index 47921d36d270fc271bf966dfe183129993ac7336..91da48500f900ddb3b0eb4e99dbd43ef void dynamicViewportSizeUpdate(const DynamicViewportSizeUpdate&); bool scaleWasSetByUIProcess() const { return m_scaleWasSetByUIProcess; } void willStartUserTriggeredZooming(); -@@ -1548,6 +1555,8 @@ public: +@@ -1587,6 +1594,8 @@ public: void connectInspector(const String& targetId, Inspector::FrontendChannel::ConnectionType); void disconnectInspector(const String& targetId); void sendMessageToTargetBackend(const String& targetId, const String& message); @@ -19991,7 +19523,7 @@ index 47921d36d270fc271bf966dfe183129993ac7336..91da48500f900ddb3b0eb4e99dbd43ef void insertNewlineInQuotedContent(); -@@ -1972,6 +1981,7 @@ public: +@@ -2012,6 +2021,7 @@ public: void showContextMenuFromFrame(const FrameInfoData&, const ContextMenuContextData&, const UserData&); #endif void loadRequest(LoadParameters&&); @@ -19999,7 +19531,7 @@ index 47921d36d270fc271bf966dfe183129993ac7336..91da48500f900ddb3b0eb4e99dbd43ef void setObscuredContentInsets(const WebCore::FloatBoxExtent&); -@@ -2176,6 +2186,7 @@ private: +@@ -2223,6 +2233,7 @@ private: void updatePotentialTapSecurityOrigin(const WebTouchEvent&, bool wasHandled); #elif ENABLE(TOUCH_EVENTS) void touchEvent(const WebTouchEvent&, CompletionHandler, bool)>&&); @@ -20007,7 +19539,7 @@ index 47921d36d270fc271bf966dfe183129993ac7336..91da48500f900ddb3b0eb4e99dbd43ef #endif void cancelPointer(WebCore::PointerID, const WebCore::IntPoint&); -@@ -2947,6 +2958,7 @@ private: +@@ -3004,6 +3015,7 @@ private: bool m_isAppNapEnabled { true }; Markable m_pendingNavigationID; @@ -20016,10 +19548,10 @@ index 47921d36d270fc271bf966dfe183129993ac7336..91da48500f900ddb3b0eb4e99dbd43ef 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 a2bdf0b62ddcb75a5c834b8aed481e1b14d5737a..e09d2ae7bbe7a1f0b34b84d6a97d2593e89b39f4 100644 +index c39f816831ea04bb7d070e81891b933307c3a62f..228a2020c40a94133699bd2a1433daca568a8d38 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.messages.in +++ b/Source/WebKit/WebProcess/WebPage/WebPage.messages.in -@@ -60,10 +60,13 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -61,10 +61,13 @@ messages -> WebPage WantsAsyncDispatchMessage { MouseEvent(WebCore::FrameIdentifier frameID, WebKit::WebMouseEvent event, std::optional> sandboxExtensions) SetLastKnownMousePosition(WebCore::FrameIdentifier frameID, WebCore::IntPoint eventPoint, WebCore::IntPoint globalPoint); @@ -20034,7 +19566,7 @@ index a2bdf0b62ddcb75a5c834b8aed481e1b14d5737a..e09d2ae7bbe7a1f0b34b84d6a97d2593 SetOverrideViewportArguments(struct std::optional arguments) DynamicViewportSizeUpdate(struct WebKit::DynamicViewportSizeUpdate target) -@@ -155,6 +158,7 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -159,6 +162,7 @@ messages -> WebPage WantsAsyncDispatchMessage { ConnectInspector(String targetId, Inspector::FrontendChannel::ConnectionType connectionType) DisconnectInspector(String targetId) SendMessageToTargetBackend(String targetId, String message) @@ -20042,7 +19574,7 @@ index a2bdf0b62ddcb75a5c834b8aed481e1b14d5737a..e09d2ae7bbe7a1f0b34b84d6a97d2593 #if ENABLE(REMOTE_INSPECTOR) SetIndicating(bool indicating); -@@ -165,6 +169,7 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -169,6 +173,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) @@ -20050,7 +19582,7 @@ index a2bdf0b62ddcb75a5c834b8aed481e1b14d5737a..e09d2ae7bbe7a1f0b34b84d6a97d2593 #endif CancelPointer(WebCore::PointerID pointerId, WebCore::IntPoint documentPoint) -@@ -190,6 +195,7 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -194,6 +199,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) @@ -20058,7 +19590,7 @@ index a2bdf0b62ddcb75a5c834b8aed481e1b14d5737a..e09d2ae7bbe7a1f0b34b84d6a97d2593 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) -@@ -354,10 +360,10 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -358,10 +364,10 @@ messages -> WebPage WantsAsyncDispatchMessage { RemoveLayerForFindOverlay() -> () # Drag and drop. @@ -20071,8 +19603,8 @@ index a2bdf0b62ddcb75a5c834b8aed481e1b14d5737a..e09d2ae7bbe7a1f0b34b84d6a97d2593 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 -@@ -377,6 +383,10 @@ messages -> WebPage WantsAsyncDispatchMessage { - ModelDragEnded(WebCore::ElementIdentifier elementID) +@@ -381,6 +387,10 @@ messages -> WebPage WantsAsyncDispatchMessage { + ModelDragEnded(WebCore::NodeIdentifier nodeID) #endif +#if PLATFORM(MAC) && ENABLE(DRAG_SUPPORT) @@ -20121,10 +19653,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 e36cd746c4a8d35d076b68fd76ccd211a79d63a6..156cacab648a4ad53b854bf880c924bbe13ca4b6 100644 +index d228567dab3bece6f0f640122df955debbb20645..8abfdcceff676c32b149d34654ccb133d0b20b6d 100644 --- a/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm +++ b/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm -@@ -701,21 +701,37 @@ String WebPage::platformUserAgent(const URL&) const +@@ -709,21 +709,37 @@ String WebPage::platformUserAgent(const URL&) const bool WebPage::hoverSupportedByPrimaryPointingDevice() const { @@ -20213,7 +19745,7 @@ index ea3a03b5ee6d4ecadd771314c6059268db917087..91be6f4c687157afcfdaa431d7a1a6ff } diff --git a/Source/WebKit/WebProcess/WebProcess.cpp b/Source/WebKit/WebProcess/WebProcess.cpp -index fe47c90bccb9a567803485e7313e093b48ad4d4c..ec76a5edc15a914d1f00955493625f6c135d75ce 100644 +index f718a75783df828282cebdb35ba6ef7aaafceaab..f2c23623972f5f05fe358968d45712de95e78287 100644 --- a/Source/WebKit/WebProcess/WebProcess.cpp +++ b/Source/WebKit/WebProcess/WebProcess.cpp @@ -93,6 +93,7 @@ @@ -20224,7 +19756,7 @@ index fe47c90bccb9a567803485e7313e093b48ad4d4c..ec76a5edc15a914d1f00955493625f6c #include #include #include -@@ -388,6 +389,14 @@ void WebProcess::initializeProcess(const AuxiliaryProcessInitializationParameter +@@ -389,6 +390,14 @@ void WebProcess::initializeProcess(const AuxiliaryProcessInitializationParameter { JSC::Options::AllowUnfinalizedAccessScope scope; JSC::Options::allowNonSPTagging() = false; @@ -20239,7 +19771,7 @@ index fe47c90bccb9a567803485e7313e093b48ad4d4c..ec76a5edc15a914d1f00955493625f6c JSC::Options::notifyOptionsChanged(); } -@@ -395,6 +404,8 @@ void WebProcess::initializeProcess(const AuxiliaryProcessInitializationParameter +@@ -396,6 +405,8 @@ void WebProcess::initializeProcess(const AuxiliaryProcessInitializationParameter platformInitializeProcess(parameters); updateCPULimit(); @@ -20248,7 +19780,7 @@ index fe47c90bccb9a567803485e7313e093b48ad4d4c..ec76a5edc15a914d1f00955493625f6c } void WebProcess::initializeConnection(IPC::Connection* connection) -@@ -979,6 +990,8 @@ void WebProcess::createWebPage(PageIdentifier pageID, WebPageCreationParameters& +@@ -1000,6 +1011,8 @@ void WebProcess::createWebPage(PageIdentifier pageID, WebPageCreationParameters& accessibilityRelayProcessSuspended(false); } ASSERT(result.iterator->value); @@ -20258,10 +19790,10 @@ index fe47c90bccb9a567803485e7313e093b48ad4d4c..ec76a5edc15a914d1f00955493625f6c void WebProcess::removeWebPage(PageIdentifier pageID) diff --git a/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm b/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm -index 01f0546169ab61e79b730ac1fe4bded1b80ade9d..f274262554df2cc924e85b79467e16110e12af43 100644 +index 1e94fdb5528a634e1766b92f687c5d4f3b360a08..48633d0eef6b94013ad08b655b6f631df9e6144b 100644 --- a/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm +++ b/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm -@@ -4219,7 +4219,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END +@@ -4220,7 +4220,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END _private->handlingMouseDownEvent = NO; } @@ -20271,7 +19803,7 @@ index 01f0546169ab61e79b730ac1fe4bded1b80ade9d..f274262554df2cc924e85b79467e1611 - (void)touch:(WebEvent *)event { diff --git a/Source/WebKitLegacy/mac/WebView/WebView.mm b/Source/WebKitLegacy/mac/WebView/WebView.mm -index 30baa8fa7a4f030680c683eebb7e93f9723da373..f236d24511f25dcbcf84af26fe29d8eb3742512d 100644 +index 0a8524884313332dbfbacf5e76faffaa20615e04..a1123977f9a775d067dacac6f18a7923d30174c2 100644 --- a/Source/WebKitLegacy/mac/WebView/WebView.mm +++ b/Source/WebKitLegacy/mac/WebView/WebView.mm @@ -3972,7 +3972,7 @@ + (void)_doNotStartObservingNetworkReachability @@ -20326,7 +19858,7 @@ index 0000000000000000000000000000000000000000..a9db9ec38d05e36517414248237e885b + LIBVPX_LIBRARIES +) diff --git a/Source/cmake/OptionsGTK.cmake b/Source/cmake/OptionsGTK.cmake -index 7b9783fe14ac1eca2c6a48e42b74e6f294359b6d..584c9f2ea30a8f670b18ef271ca2f0681c637fdc 100644 +index cca9439b5b8b47d7ad594fb52169732b8fe04dcb..a35b4483dfe7f38d40d9754ad808797080bbff14 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") @@ -20385,12 +19917,12 @@ index 7b9783fe14ac1eca2c6a48e42b74e6f294359b6d..584c9f2ea30a8f670b18ef271ca2f068 WEBKIT_OPTION_DEPEND(ENABLE_WEBXR ENABLE_GAMEPAD) diff --git a/Source/cmake/OptionsWPE.cmake b/Source/cmake/OptionsWPE.cmake -index d04d2c2504b829f240633a9c7e3de15876190ba0..9f3a7febd7374a60a55c1d6aaee50cc26df59fc6 100644 +index 0f218a220f020c02da23801959bbd7e72225117e..64800eeb278be31595b13b0a0a9ecf23a4ed5f0c 100644 --- a/Source/cmake/OptionsWPE.cmake +++ b/Source/cmake/OptionsWPE.cmake -@@ -23,6 +23,9 @@ find_package(WebP REQUIRED COMPONENTS demux) - find_package(WPE REQUIRED) - find_package(ZLIB REQUIRED) +@@ -27,6 +27,9 @@ if (ANDROID) + find_package(Android REQUIRED COMPONENTS Android Log) + endif () +set(CMAKE_THREAD_PREFER_PTHREAD TRUE) +set(THREADS_PREFER_PTHREAD_FLAG TRUE) @@ -20398,7 +19930,7 @@ index d04d2c2504b829f240633a9c7e3de15876190ba0..9f3a7febd7374a60a55c1d6aaee50cc2 WEBKIT_OPTION_BEGIN() SET_AND_EXPOSE_TO_BUILD(ENABLE_DEVELOPER_MODE ${DEVELOPER_MODE}) -@@ -81,6 +84,22 @@ else () +@@ -86,6 +89,22 @@ else () WEBKIT_OPTION_DEFAULT_PORT_VALUE(USE_SKIA PRIVATE OFF) endif () @@ -20421,7 +19953,7 @@ index d04d2c2504b829f240633a9c7e3de15876190ba0..9f3a7febd7374a60a55c1d6aaee50cc2 # 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 +135,11 @@ WEBKIT_OPTION_DEPEND(USE_QT6 ENABLE_WPE_PLATFORM) +@@ -121,6 +140,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) @@ -20434,7 +19966,7 @@ index d04d2c2504b829f240633a9c7e3de15876190ba0..9f3a7febd7374a60a55c1d6aaee50cc2 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 1a8e2da4c2a377e0dbfd7f7b1bc7b5ce597d4a4e..cee24362b16102e6efb9e017c77810ca4bdf32d7 100644 +index 4bf1704596f5555a9e23939873525d96d803ff80..e3710ad8b4100c4f30f84c3364230c6ef51575d6 100644 --- a/Source/cmake/OptionsWin.cmake +++ b/Source/cmake/OptionsWin.cmake @@ -55,6 +55,10 @@ find_package(ZLIB 1.2.11 REQUIRED) @@ -20464,7 +19996,7 @@ index 1a8e2da4c2a377e0dbfd7f7b1bc7b5ce597d4a4e..cee24362b16102e6efb9e017c77810ca set(USE_ANGLE_EGL ON) diff --git a/Source/cmake/WebKitCompilerFlags.cmake b/Source/cmake/WebKitCompilerFlags.cmake -index d8fe5503609122c55c2c675934b3678cfe709cf0..2e706bb436d6c01689bec8cc7b7de3c125ca258a 100644 +index 77a292fb8b15af7bc544de6e0307f11106cfd161..872541a17703827da5020df4129a65b3dd9d8e80 100644 --- a/Source/cmake/WebKitCompilerFlags.cmake +++ b/Source/cmake/WebKitCompilerFlags.cmake @@ -122,7 +122,7 @@ macro(WEBKIT_ADD_TARGET_CXX_FLAGS _target) @@ -20879,7 +20411,7 @@ index d3fbb968ee463f86c64fecb855b46c8634b4b72d..01dbbfbb93f2cfa6eb6440cce9794ec9 g_clear_object(&interfaceSettings); diff --git a/Tools/MiniBrowser/wpe/main.cpp b/Tools/MiniBrowser/wpe/main.cpp -index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a59760455329d316e33 100644 +index abc3c2e649f13c0843f74214f79f2a5ea5e826c1..7a2993feb2c5017ca08572a35a74bca7c01f87e9 100644 --- a/Tools/MiniBrowser/wpe/main.cpp +++ b/Tools/MiniBrowser/wpe/main.cpp @@ -52,6 +52,9 @@ static gboolean headlessMode; @@ -20902,7 +20434,7 @@ index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a5976045532 { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &uriArguments, nullptr, "[URL]" }, { nullptr, 0, 0, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr } }; -@@ -288,15 +294,38 @@ static void filterSavedCallback(WebKitUserContentFilterStore *store, GAsyncResul +@@ -295,15 +301,38 @@ static void filterSavedCallback(WebKitUserContentFilterStore *store, GAsyncResul g_main_loop_quit(data->mainLoop); } @@ -20943,7 +20475,7 @@ index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a5976045532 { auto backend = createViewBackend(defaultWindowWidthLegacyAPI, defaultWindowHeightLegacyAPI); WebKitWebViewBackend* viewBackend = nullptr; -@@ -311,12 +340,27 @@ static WebKitWebView* createWebView(WebKitWebView* webView, WebKitNavigationActi +@@ -318,12 +347,27 @@ static WebKitWebView* createWebView(WebKitWebView* webView, WebKitNavigationActi }, backend.release()); } @@ -20977,7 +20509,7 @@ index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a5976045532 #if ENABLE_WPE_PLATFORM if (auto* wpeView = webkit_web_view_get_wpe_view(newWebView)) { -@@ -328,9 +372,13 @@ static WebKitWebView* createWebView(WebKitWebView* webView, WebKitNavigationActi +@@ -335,9 +379,13 @@ static WebKitWebView* createWebView(WebKitWebView* webView, WebKitNavigationActi g_signal_connect(newWebView, "create", G_CALLBACK(createWebView), user_data); g_signal_connect(newWebView, "close", G_CALLBACK(webViewClose), user_data); @@ -20993,7 +20525,7 @@ index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a5976045532 return newWebView; } -@@ -418,13 +466,105 @@ void loadConfigFile(WPESettings* settings) +@@ -425,13 +473,105 @@ void loadConfigFile(WPESettings* settings) } #endif @@ -21100,7 +20632,7 @@ index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a5976045532 webkit_network_session_set_itp_enabled(networkSession, enableITP); if (proxy) { -@@ -451,10 +591,18 @@ static void activate(GApplication* application, WPEToolingBackends::ViewBackend* +@@ -458,10 +598,18 @@ static void activate(GApplication* application, WPEToolingBackends::ViewBackend* webkit_cookie_manager_set_persistent_storage(cookieManager, cookiesFile, storageType); } } @@ -21121,7 +20653,7 @@ index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a5976045532 webkit_website_data_manager_set_itp_enabled(manager, enableITP); if (proxy) { -@@ -485,6 +633,7 @@ static void activate(GApplication* application, WPEToolingBackends::ViewBackend* +@@ -492,6 +640,7 @@ static void activate(GApplication* application, WPEToolingBackends::ViewBackend* } #endif @@ -21129,7 +20661,7 @@ index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a5976045532 WebKitUserContentManager* userContentManager = nullptr; if (contentFilter) { GFile* contentFilterFile = g_file_new_for_commandline_arg(contentFilter); -@@ -563,6 +712,15 @@ static void activate(GApplication* application, WPEToolingBackends::ViewBackend* +@@ -570,6 +719,15 @@ static void activate(GApplication* application, WPEToolingBackends::ViewBackend* "autoplay", WEBKIT_AUTOPLAY_ALLOW, nullptr); @@ -21145,7 +20677,7 @@ index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a5976045532 auto* webView = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, "backend", viewBackend, "web-context", webContext, -@@ -609,12 +767,16 @@ static void activate(GApplication* application, WPEToolingBackends::ViewBackend* +@@ -616,12 +774,16 @@ static void activate(GApplication* application, WPEToolingBackends::ViewBackend* } #endif @@ -21164,7 +20696,7 @@ index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a5976045532 g_hash_table_add(openViews, webView); WebKitColor color; -@@ -622,16 +784,11 @@ static void activate(GApplication* application, WPEToolingBackends::ViewBackend* +@@ -629,16 +791,11 @@ static void activate(GApplication* application, WPEToolingBackends::ViewBackend* webkit_web_view_set_background_color(webView, &color); if (uriArguments) { @@ -21186,7 +20718,7 @@ index 247a2dc5cf369dbf26434358de0898cb36cba100..1aff934e53a808df82ba4a5976045532 webkit_web_view_load_uri(webView, "https://wpewebkit.org"); g_object_unref(webContext); -@@ -728,8 +885,14 @@ int main(int argc, char *argv[]) +@@ -735,8 +892,14 @@ int main(int argc, char *argv[]) } } @@ -21229,10 +20761,10 @@ 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 567ccd8d7a4bdb3c576c60230123b78465da6505..75bf76270063878f3f1ded7b47eb2f8cc90271d8 100644 +index 0d0c43579d26bb9738300a3b6bd142b4a0f424be..ac0e2abe9d1ad3bac7b855eeb50fed5de96cf4ad 100644 --- a/Tools/WebKitTestRunner/TestController.cpp +++ b/Tools/WebKitTestRunner/TestController.cpp -@@ -713,6 +713,7 @@ PlatformWebView* TestController::createOtherPlatformWebView(PlatformWebView* par +@@ -735,6 +735,7 @@ PlatformWebView* TestController::createOtherPlatformWebView(PlatformWebView* par nullptr, // requestStorageAccessConfirm nullptr, // shouldAllowDeviceOrientationAndMotionAccess nullptr, // runWebAuthenticationPanel @@ -21240,7 +20772,7 @@ index 567ccd8d7a4bdb3c576c60230123b78465da6505..75bf76270063878f3f1ded7b47eb2f8c nullptr, // decidePolicyForSpeechRecognitionPermissionRequest nullptr, // decidePolicyForMediaKeySystemPermissionRequest nullptr, // queryPermission -@@ -1188,6 +1189,7 @@ void TestController::createWebViewWithOptions(const TestOptions& options) +@@ -1218,6 +1219,7 @@ void TestController::createWebViewWithOptions(const TestOptions& options) nullptr, // requestStorageAccessConfirm shouldAllowDeviceOrientationAndMotionAccess, runWebAuthenticationPanel, From adaef276a4dcef21d639fffed092769c028c3ff1 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 2 Oct 2025 12:02:26 -0700 Subject: [PATCH 004/250] Revert "fix(trace): should survive ping as the first command after restart" (#37689) --- packages/trace-viewer/src/sw/main.ts | 43 ++++++++++++++-------------- tests/library/trace-viewer.spec.ts | 20 ------------- 2 files changed, 21 insertions(+), 42 deletions(-) diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index c07323d05..0e7ff887c 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -93,19 +93,6 @@ async function doFetch(event: FetchEvent): Promise { const request = event.request; const client = await self.clients.get(event.clientId); - const urlInScope = request.url.startsWith(self.registration.scope) ? new URL(unwrapPopoutUrl(request.url)) : undefined; - const relativePath = urlInScope?.pathname.substring(scopePath.length - 1); - - if (relativePath !== '/contexts' && !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, () => {}); - } - } - // When trace viewer is deployed over https, we will force upgrade // insecure http subresources to https. Otherwise, these will fail // to load inside our https snapshots. @@ -113,7 +100,9 @@ async function doFetch(event: FetchEvent): Promise { // the https urls. const isDeployedAsHttps = self.registration.scope.startsWith('https://'); - if (urlInScope && relativePath) { + if (request.url.startsWith(self.registration.scope)) { + const url = new URL(unwrapPopoutUrl(request.url)); + const relativePath = url.pathname.substring(scopePath.length - 1); if (relativePath === '/ping') { await gc(); return new Response(null, { status: 200 }); @@ -123,12 +112,12 @@ async function doFetch(event: FetchEvent): Promise { return new Response(null, { status: 200 }); } - const traceUrl = urlInScope.searchParams.get('trace'); + const traceUrl = url.searchParams.get('trace'); if (relativePath === '/contexts') { try { - const limit = urlInScope.searchParams.has('limit') ? +urlInScope.searchParams.get('limit')! : undefined; - const traceModel = await loadTrace(traceUrl!, urlInScope.searchParams.get('traceFileName'), client, limit, (done: number, total: number) => { + const limit = url.searchParams.has('limit') ? +url.searchParams.get('limit')! : undefined; + const traceModel = await loadTrace(traceUrl!, url.searchParams.get('traceFileName'), client, limit, (done: number, total: number) => { client.postMessage({ method: 'progress', params: { done, total } }); }); return new Response(JSON.stringify(traceModel!.contextEntries), { @@ -143,12 +132,22 @@ 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) return new Response(null, { status: 404 }); const pageOrFrameId = relativePath.substring('/snapshotInfo/'.length); - return snapshotServer.serveSnapshotInfo(pageOrFrameId, urlInScope.searchParams); + return snapshotServer.serveSnapshotInfo(pageOrFrameId, url.searchParams); } if (relativePath.startsWith('/snapshot/')) { @@ -156,7 +155,7 @@ async function doFetch(event: FetchEvent): Promise { if (!snapshotServer) return new Response(null, { status: 404 }); const pageOrFrameId = relativePath.substring('/snapshot/'.length); - const response = snapshotServer.serveSnapshot(pageOrFrameId, urlInScope.searchParams, urlInScope.href); + const response = snapshotServer.serveSnapshot(pageOrFrameId, url.searchParams, url.href); if (isDeployedAsHttps) response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests'); return response; @@ -167,7 +166,7 @@ async function doFetch(event: FetchEvent): Promise { if (!snapshotServer) return new Response(null, { status: 404 }); const pageOrFrameId = relativePath.substring('/closest-screenshot/'.length); - return snapshotServer.serveClosestScreenshot(pageOrFrameId, urlInScope.searchParams); + return snapshotServer.serveClosestScreenshot(pageOrFrameId, url.searchParams); } if (relativePath.startsWith('/sha1/')) { @@ -176,13 +175,13 @@ async function doFetch(event: FetchEvent): Promise { for (const trace of loadedTraces.values()) { const blob = await trace.traceModel.resourceForSha1(sha1); if (blob) - return new Response(blob, { status: 200, headers: downloadHeaders(urlInScope.searchParams) }); + return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) }); } return new Response(null, { status: 404 }); } if (relativePath.startsWith('/file/')) { - const path = urlInScope.searchParams.get('path')!; + const path = url.searchParams.get('path')!; const traceViewerServer = clientIdToTraceUrls.get(event.clientId ?? '')?.traceViewerServer; if (!traceViewerServer) throw new Error('client is not initialized'); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 7a9a45e9c..eea630c49 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -2056,23 +2056,3 @@ test('should survive service worker restart', async ({ page, runAndTrace, server const snapshot2 = await traceViewer.snapshotFrame('Set content'); await expect(snapshot2.locator('body')).toHaveText('Old world'); }); - -test('should survive ping after 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 response1 = await fetch('restartServiceWorker'); - const response2 = await fetch('ping'); - return response1.status + '/' + response2.status; - }); - expect(status).toBe('200/200'); - - const snapshot2 = await traceViewer.snapshotFrame('Set content'); - await expect(snapshot2.locator('body')).toHaveText('Old world'); -}); From 0193454cab5e8d7eb9964ca09e13de8769df02bf Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 2 Oct 2025 12:11:32 -0700 Subject: [PATCH 005/250] Revert "fix(trace): survive sw restart" (#37690) --- 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, 2 insertions(+), 79 deletions(-) diff --git a/package-lock.json b/package-lock.json index d539f5cb9..c049aa314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4797,12 +4797,6 @@ "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", @@ -8374,7 +8368,6 @@ "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 699014a40..57a3dfd40 100644 --- a/packages/trace-viewer/package.json +++ b/packages/trace-viewer/package.json @@ -4,7 +4,6 @@ "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 0e7ff887c..cdce3261f 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import * as idbKeyval from 'idb-keyval'; - import { splitProgress } from './progress'; import { unwrapPopoutUrl } from './snapshotRenderer'; import { SnapshotServer } from './snapshotServer'; @@ -35,13 +33,10 @@ 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(); -} +const clientIdToTraceUrls = new Map, traceViewerServer: TraceViewerServer }>(); async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, limit: number | undefined, progress: (done: number, total: number) => undefined): Promise { await gc(); @@ -54,7 +49,6 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, client: clientIdToTraceUrls.set(clientId, data); } data.traceUrls.add(traceUrl); - await saveClientIdParams(); const traceModel = new TraceModel(); try { @@ -107,10 +101,6 @@ 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'); @@ -132,16 +122,6 @@ 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) @@ -241,36 +221,6 @@ 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 eea630c49..e888559a0 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -2037,22 +2037,3 @@ 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 7f3c4f658db7df6fe6ea267a8c8322fca95a543b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 2 Oct 2025 20:41:09 +0100 Subject: [PATCH 006/250] chore: remove the wrong heap test (#37682) --- tests/stress/heap.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/stress/heap.spec.ts b/tests/stress/heap.spec.ts index d3d6f41a1..d07edddab 100644 --- a/tests/stress/heap.spec.ts +++ b/tests/stress/heap.spec.ts @@ -81,18 +81,6 @@ 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 530761fe7c03c8cf52a5ab9409bf44f79064e0df Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 2 Oct 2025 15:48:49 -0700 Subject: [PATCH 007/250] docs: use VS Code images for test agents (#37693) --- .../images/test-agents/generator-prompt.png | Bin 0 -> 34718 bytes docs/src/images/test-agents/healer-prompt.png | Bin 0 -> 28393 bytes .../src/images/test-agents/planner-prompt.png | Bin 0 -> 32863 bytes docs/src/test-agents-js.md | 240 +++++++++--------- examples/todomvc/tests/seed.spec.ts | 2 - 5 files changed, 118 insertions(+), 124 deletions(-) create mode 100644 docs/src/images/test-agents/generator-prompt.png create mode 100644 docs/src/images/test-agents/healer-prompt.png create mode 100644 docs/src/images/test-agents/planner-prompt.png diff --git a/docs/src/images/test-agents/generator-prompt.png b/docs/src/images/test-agents/generator-prompt.png new file mode 100644 index 0000000000000000000000000000000000000000..56c67cf3ce8b2e027acd979f812980b9629925b0 GIT binary patch literal 34718 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 DEo27{ literal 0 HcmV?d00001 diff --git a/docs/src/images/test-agents/healer-prompt.png b/docs/src/images/test-agents/healer-prompt.png new file mode 100644 index 0000000000000000000000000000000000000000..5b6116972cfb69d06226a14385b9c9db685f8110 GIT binary patch literal 28393 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 ZlXEJpo-%tM0#~jnE2znrJ~V#)KLAt<*;oJo literal 0 HcmV?d00001 diff --git a/docs/src/test-agents-js.md b/docs/src/test-agents-js.md index 3b7c7d467..b608ab790 100644 --- a/docs/src/test-agents-js.md +++ b/docs/src/test-agents-js.md @@ -5,108 +5,114 @@ title: "Agents" # Playwright Agents -## Test Coverage in 1-2-3 +Playwright comes with three Playwright Agents out of the box: **🎭 planner**, **🎭 generator** and **🎭 healer**. -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. +These agents can be used independently, sequentially, or as the chained calls in the agentic loop. +Using them sequentially will produce test coverage for your product. -1. **Plan**: A planning agent explores the app and produces a test plan in `specs/*.md`. +* **🎭 planner** explores the app and produces a Markdown test plan -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. +* **🎭 generator** transforms the Markdown plan into the Playwright Test files -3. **Heal**: A healing agent executes the test suite and automatically repairs failing tests by applying diffs in place. +* **🎭 healer** executes the test suite and automatically repairs failing tests ### Getting Started -In order to use Playwright Agents, you must add their definitions to your project using +Start with adding Playwright Agent definitions to your project using the `init-agents` command. These definitions should be regenerated whenever Playwright -is updated. +is updated to pick up new tools and instructions. -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 +```bash tab=bash-vscode npx playwright init-agents --loop=vscode -# Claude Code +``` + +```bash tab=bash-claude npx playwright init-agents --loop=claude -# opencode +``` + +```bash tab=bash-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: +Once the agents have been generated, you can use your AI tool of choice to command these agents to build Playwright Tests. + -## 1. Plan +## 🎭 Planner -The planning agent explores your app environment and produces a test plan for one or many scenarios and user flows. +Planner agent explores your app and produces a test plan for one or many scenarios and user flows. **Input** -* 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) +* A clear request to the planner (e.g., “Generate a plan for guest checkout.”) +* A `seed test` that sets up the environment necessary to interact with your app +* *(optional)* A Product Requirement Document (PRD) for context -**Example Prompt** +**Prompt** + +planner prompt -```markdown - Generate a test plan for "Guest Checkout" scenario. - Use `seed.spec.ts` as a seed test for the plan. +> - Notice how the `seed.spec.ts` is included in the context of the planner. +> - Planner will run this test to execute all the initialization necessary for your test including the global setup, project dependencies and all the necessary fixtures and hooks. +> - Planner will also use this seed test as an example of all the generated tests. Alternatively, you can mention the file name in the prompt. + +```js title="Example: seed.spec.ts" +import { test, expect } from './fixtures'; + +test('seed', async ({ page }) => { + // this test uses custom fixtures from ./fixtures +}); ``` **Output** -* A Markdown test plan saved to `specs/[scenario name].md`. The plan is human-readable but precise enough for test generation. +* A Markdown test plan saved as `specs/basic-operations.md`. +* The plan is human-readable but precise enough for test generation.
-Example: specs/guest-checkout.md +Example: specs/basic-operations.md ```markdown -# Feature: Guest Checkout +# TodoMVC Application - Basic Operations Test Plan -## Purpose -Allow a user to purchase without creating an account. +## Application Overview -## Preconditions -- Test seed `tests/seed.spec.ts`. -- Payment sandbox credentials available via env vars. +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: -## Scenarios +- **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 -### 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. +## Test Scenarios -**Expected** -- Cart count increments after item is added. -- Checkout page shows item, price, tax, and total. -- Order confirmation number appears; status is "Processing". +### 1. Adding New Todos -### SC-2: Tax and shipping recalculation on address change -**Steps** -1. Start checkout with a CA address. -2. Change state to NY. +**Seed:** `tests/seed.spec.ts` -**Expected** -- Tax and shipping values recalculate. +#### 1.1 Add Valid Todo -## Data -- Product SKU: `WM-123` -- Payment: sandbox card `4111 1111 1111 1111`, valid expiry, CVV `123`. +**Steps:** +1. Click in the "What needs to be done?" input field +2. Type "Buy groceries" +3. Press Enter key -## Methodology -*Optional notes about testing methodology* +**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 +... ```
-## 2. Generate +## 🎭 Generator -The generating agent uses the Markdown plan to produce executable Playwright tests. -It verifies selectors and assertions live against the application. Playwright supports +Generator agent uses the Markdown plan to produce executable Playwright Tests. +It verifies selectors and assertions live as it performs the scenarios. Playwright supports generation hints and provides a catalog of assertions for efficient structural and behavioral validation. @@ -114,11 +120,12 @@ behavioral validation. * Markdown plan from `specs/` -**Example Prompt** +**Prompt** -```markdown - Generate tests for the guest checkout plan under `specs/`. -``` +generator prompt + +> - Notice how the `basic-operations.md` is included in the context of the generator. +> - This is how generator knows where to get the test plan from. Alternatively, you can mention the file name in the prompt. **Output** @@ -126,57 +133,50 @@ behavioral validation. * Generated tests may include initial errors that can be healed automatically by the healer agent
-Example: tests/guest-checkout.spec.ts +Example: tests/add-valid-todo.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(); +// 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(); }); }); ```
-## 3. Heal +## 🎭 Healer -When a test fails, the healing agent: +When the test fails, the healer agent: * Replays the failing steps * Inspects the current UI to locate equivalent elements or flows @@ -187,15 +187,13 @@ When a test fails, the healing agent: * Failing test name -**Example Prompt** +**Prompt** -```markdown - Fix all failing tests for the guest checkout scenario. -``` +healer prompt **Output** -* A passing test, or a skipped test if the healer was unable to ensure correct functionality +* A passing test, or a skipped test if the healer believes the that functionality is broken. ## Artifacts and Conventions @@ -203,26 +201,24 @@ The static agent definitions and generated files follow a simple, auditable stru ```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 + .github/ # agent definitions + specs/ # human-readable test plans + basic-operations.md + tests/ # generated Playwright tests + seed.spec.ts # seed test for environment + tests/create/add-valid-todo.spec.ts playwright.config.ts ``` ### Agent Definitions -Agent definitions are collections of instructions and MCP tools. They are provided by +Under the hood, 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 --loop=claude +npx playwright init-agents --loop=vscode ``` ### Specs in `specs/` diff --git a/examples/todomvc/tests/seed.spec.ts b/examples/todomvc/tests/seed.spec.ts index 43fd45c83..24cf19912 100644 --- a/examples/todomvc/tests/seed.spec.ts +++ b/examples/todomvc/tests/seed.spec.ts @@ -1,5 +1,3 @@ -/* eslint-disable notice/notice */ - import { test, expect } from './fixtures'; test('seed', async ({ page }) => { From 42a623f9b6f1023316301f85f5aeeffd3c651e04 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 2 Oct 2025 17:32:30 -0700 Subject: [PATCH 008/250] chore: move best practices into the journal (#37694) --- .../.claude/agents/playwright-test-generator.md | 15 ++------------- .../\360\237\216\255 generator.chatmode.md" | 13 +------------ packages/playwright/src/agents/generator.md | 13 +------------ packages/playwright/src/mcp/test/testContext.ts | 14 ++++++++++++++ tests/mcp/generator.spec.ts | 16 ++++++++++++---- 5 files changed, 30 insertions(+), 41 deletions(-) diff --git a/examples/todomvc/.claude/agents/playwright-test-generator.md b/examples/todomvc/.claude/agents/playwright-test-generator.md index 0296e3e5e..b82e9bc3a 100644 --- a/examples/todomvc/.claude/agents/playwright-test-generator.md +++ b/examples/todomvc/.claude/agents/playwright-test-generator.md @@ -24,6 +24,7 @@ application behavior. - Test title must match the scenario name - Includes a comment with the step text before each step execution. Do not duplicate comments if step requires multiple actions. + - Always use best practices from the log when generating tests. For following plan: @@ -55,16 +56,4 @@ application behavior. }); }); ``` - - -# 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 + \ 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" index 88947303d..6ada1226f 100644 --- "a/examples/todomvc/.github/chatmodes/\360\237\216\255 generator.chatmode.md" +++ "b/examples/todomvc/.github/chatmodes/\360\237\216\255 generator.chatmode.md" @@ -21,6 +21,7 @@ application behavior. - Test title must match the scenario name - Includes a comment with the step text before each step execution. Do not duplicate comments if step requires multiple actions. + - Always use best practices from the log when generating tests. For following plan: @@ -53,17 +54,5 @@ application behavior. }); ``` - -# 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/packages/playwright/src/agents/generator.md b/packages/playwright/src/agents/generator.md index dabbb46f1..3e83d145e 100644 --- a/packages/playwright/src/agents/generator.md +++ b/packages/playwright/src/agents/generator.md @@ -46,6 +46,7 @@ application behavior. - Test title must match the scenario name - Includes a comment with the step text before each step execution. Do not duplicate comments if step requires multiple actions. + - Always use best practices from the log when generating tests. For following plan: @@ -79,18 +80,6 @@ application behavior. ``` -# 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 diff --git a/packages/playwright/src/mcp/test/testContext.ts b/packages/playwright/src/mcp/test/testContext.ts index 614842561..a2204e24d 100644 --- a/packages/playwright/src/mcp/test/testContext.ts +++ b/packages/playwright/src/mcp/test/testContext.ts @@ -65,6 +65,7 @@ export class GeneratorJournal { \`\`\`ts ${step.code} \`\`\``).join('\n\n')); + result.push(bestPracticesMarkdown); return result.join('\n\n'); } } @@ -177,3 +178,16 @@ export class TestContext { async close() { } } + +const bestPracticesMarkdown = ` +# Best practices +- Do not improvise, do not add directives that were not asked for +- Use clear, descriptive assertions to validate the expected behavior +- Use reliable locators from this log +- Use local variables for locators that are used multiple times +- Use Playwright waiting assertions and best practices from this log +- NEVER! use page.waitForLoadState() +- NEVER! use page.waitForNavigation() +- NEVER! use page.waitForTimeout() +- NEVER! use page.evaluate() +`; diff --git a/tests/mcp/generator.spec.ts b/tests/mcp/generator.spec.ts index 1ec5ca47e..8f7ffec24 100644 --- a/tests/mcp/generator.spec.ts +++ b/tests/mcp/generator.spec.ts @@ -101,7 +101,7 @@ test('generator_setup_page', async ({ startClient }) => { expect(await client.callTool({ name: 'generator_read_log', arguments: {}, - })).toHaveTextResponse(`# Plan + })).toHaveTextResponse(expect.stringContaining(`# Plan Test plan @@ -125,7 +125,11 @@ Test plan ### Click submit button \`\`\`ts await page.getByRole('button', { name: 'Submit' }).click(); -\`\`\``); +\`\`\` + + +# Best practices +`)); }); test('click after generator_log_action', async ({ startClient }) => { @@ -164,7 +168,7 @@ test('click after generator_log_action', async ({ startClient }) => { expect(await client.callTool({ name: 'generator_read_log', arguments: {}, - })).toHaveTextResponse(`# Plan + })).toHaveTextResponse(expect.stringContaining(`# Plan Test plan @@ -188,7 +192,11 @@ Test plan ### Click submit button \`\`\`ts await page.getByRole('button', { name: 'Submit' }).click(); -\`\`\``); +\`\`\` + + +# Best practices +`)); }); test('generator_setup_page is required', async ({ startClient }) => { From 258b4dcb1972d7842be6254f83d7392793c6385d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 3 Oct 2025 08:50:27 +0100 Subject: [PATCH 009/250] chore: fix context._disableRecorder (#37685) --- .../server/dispatchers/browserContextDispatcher.ts | 2 +- packages/playwright-core/src/server/recorder.ts | 5 +++-- tests/library/inspector/recorder-api.spec.ts | 12 ++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 4b32fbd9b..4555d4c10 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -327,7 +327,7 @@ export class BrowserContextDispatcher extends Dispatcher { - const recorder = Recorder.existingForContext(this._context); + const recorder = await Recorder.existingForContext(this._context); if (recorder) recorder.setMode('none'); } diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index ebd738c64..cb29fcfd3 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -100,8 +100,9 @@ export class Recorder extends EventEmitter implements Instrume return recorderPromise; } - static existingForContext(context: BrowserContext): Recorder | undefined { - return (context as any)[recorderSymbol] as Recorder; + static async existingForContext(context: BrowserContext): Promise { + const recorderPromise = (context as any)[recorderSymbol] as Promise | undefined; + return await recorderPromise; } private static async _create(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams = {}): Promise { diff --git a/tests/library/inspector/recorder-api.spec.ts b/tests/library/inspector/recorder-api.spec.ts index 25e7f0694..d195e8cfb 100644 --- a/tests/library/inspector/recorder-api.spec.ts +++ b/tests/library/inspector/recorder-api.spec.ts @@ -139,3 +139,15 @@ test('should type', async ({ context }) => { expect(normalizeCode(fillActions[0].code)).toEqual(`await page.getByRole('textbox').fill('Hello');`); }); + +test('should disable recorder', async ({ context }) => { + const log = await startRecording(context); + const page = await context.newPage(); + await page.setContent(``); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.getByRole('button', { name: 'Submit' }).click(); + expect(log.action('click')).toHaveLength(2); + await (context as any)._disableRecorder(); + await page.getByRole('button', { name: 'Submit' }).click(); + expect(log.action('click')).toHaveLength(2); +}); From 6fa48c17db6b679c3e5e64296b2959815a0d21ae Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 3 Oct 2025 05:33:18 -0700 Subject: [PATCH 010/250] fix(library): don't dirty page state when calling page.close() (#37608) --- packages/playwright-core/src/server/page.ts | 4 +- tests/library/beforeunload.spec.ts | 58 +++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 5df5bf461..fd5a9fa9a 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -755,7 +755,9 @@ export class Page extends SdkObject { this.closeReason = options.reason; const runBeforeUnload = !!options.runBeforeUnload; if (this._closedState !== 'closing') { - this._closedState = 'closing'; + // If runBeforeUnload is true, we don't know if we will close, so don't modify the state + if (!runBeforeUnload) + this._closedState = 'closing'; // This might throw if the browser context containing the page closes // while we are trying to close the page. await this.delegate.closePage(runBeforeUnload).catch(e => debugLogger.log('error', e)); diff --git a/tests/library/beforeunload.spec.ts b/tests/library/beforeunload.spec.ts index 05134bd42..e39fdd2e3 100644 --- a/tests/library/beforeunload.spec.ts +++ b/tests/library/beforeunload.spec.ts @@ -126,3 +126,61 @@ it('should not stall on click when dismissing beforeunload', async ({ page, serv await page.getByRole('link').click({ timeout: 5000 }); await expect(page).toHaveURL(server.PREFIX + '/frames/one-frame.html'); }); + +it('should support dismissing the dialog multiple times', async ({ page, server }) => { + await page.goto(server.PREFIX + '/beforeunload.html'); + + // We have to interact with a page so that 'beforeunload' handlers + // fire (in Firefox). + await page.click('body'); + + const [dialog] = await Promise.all([ + page.waitForEvent('dialog'), + page.close({ runBeforeUnload: true }) + ]); + + await dialog.dismiss(); + + const [dialog2] = await Promise.all([ + page.waitForEvent('dialog'), + page.close({ runBeforeUnload: true }) + ]); + + await dialog2.dismiss(); +}); + +it('should support closing the page after a previous dismiss', async ({ page, server }) => { + await page.goto(server.PREFIX + '/beforeunload.html'); + + // We have to interact with a page so that 'beforeunload' handlers + // fire (in Firefox). + await page.click('body'); + + const [dialog] = await Promise.all([ + page.waitForEvent('dialog'), + page.close({ runBeforeUnload: true }) + ]); + + await dialog.dismiss(); + + await page.close(); + await expect(page.isClosed()).toBe(true); +}); + +it('should support closing the page via a subsequent onbeforeunload dialog', async ({ page, server }) => { + await page.goto(server.PREFIX + '/beforeunload.html'); + + // We have to interact with a page so that 'beforeunload' handlers + // fire (in Firefox). + await page.click('body'); + + const [dialog] = await Promise.all([ + page.waitForEvent('dialog'), + page.close({ runBeforeUnload: true }) + ]); + + await dialog.accept(); + + await page.waitForEvent('close'); + await expect(page.isClosed()).toBe(true); +}); From c85932f0b3f6f7e6f6cacfc5a069c531d325f192 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 3 Oct 2025 08:25:29 -0700 Subject: [PATCH 011/250] chore(bidi): use firefox browser type with moz-firefox- channel (#37696) --- packages/playwright-client/types/types.d.ts | 1 - .../playwright-core/src/browserServerImpl.ts | 4 ++-- .../playwright-core/src/client/playwright.ts | 5 +---- .../playwright-core/src/inProcessFactory.ts | 1 - .../playwright-core/src/protocol/validator.ts | 1 - .../src/server/bidi/bidiFirefox.ts | 2 +- .../playwright-core/src/server/browserType.ts | 6 ++--- .../dispatchers/playwrightDispatcher.ts | 2 -- .../src/server/firefox/firefox.ts | 22 +++++++++++++++++-- .../playwright-core/src/server/playwright.ts | 4 +--- .../src/server/registry/index.ts | 8 +++---- packages/playwright-core/types/types.d.ts | 1 - packages/playwright/src/index.ts | 4 ++-- packages/protocol/src/channels.d.ts | 1 - packages/protocol/src/protocol.yml | 1 - tests/bidi/playwright.config.ts | 6 ++--- tests/config/browserTest.ts | 6 ++--- tests/config/utils.ts | 4 ++-- .../browsercontext-network-event.spec.ts | 4 ++-- .../browsercontext-timezone-id.spec.ts | 6 ++--- tests/library/channels.spec.ts | 11 ---------- tests/library/favicon.spec.ts | 4 ++-- tests/library/har.spec.ts | 4 ++-- tests/library/page-close.spec.ts | 4 ++-- tests/page/page-basic.spec.ts | 4 ++-- tests/page/page-event-console.spec.ts | 4 ++-- tests/page/page-event-pageerror.spec.ts | 4 ++-- tests/page/page-goto.spec.ts | 4 ++-- tests/page/page-route.spec.ts | 10 ++++----- tests/page/pageTestApi.ts | 4 ++-- utils/generate_types/overrides.d.ts | 1 - 31 files changed, 67 insertions(+), 76 deletions(-) diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index d4e4f52c7..86af0171b 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -16291,7 +16291,6 @@ export type AndroidKey = export const _electron: Electron; export const _android: Android; export const _bidiChromium: BrowserType; -export const _bidiFirefox: BrowserType; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {}; diff --git a/packages/playwright-core/src/browserServerImpl.ts b/packages/playwright-core/src/browserServerImpl.ts index 0057d26ac..d0e3229d7 100644 --- a/packages/playwright-core/src/browserServerImpl.ts +++ b/packages/playwright-core/src/browserServerImpl.ts @@ -32,9 +32,9 @@ import type { WebSocketEventEmitter } from './utilsBundle'; import type { Browser } from './server/browser'; export class BrowserServerLauncherImpl implements BrowserServerLauncher { - private _browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiFirefox' | '_bidiChromium'; + private _browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiChromium'; - constructor(browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiFirefox' | '_bidiChromium') { + constructor(browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiChromium') { this._browserName = browserName; } diff --git a/packages/playwright-core/src/client/playwright.ts b/packages/playwright-core/src/client/playwright.ts index 48c634908..5b0ed7dcc 100644 --- a/packages/playwright-core/src/client/playwright.ts +++ b/packages/playwright-core/src/client/playwright.ts @@ -30,7 +30,6 @@ export class Playwright extends ChannelOwner { readonly _android: Android; readonly _electron: Electron; readonly _bidiChromium: BrowserType; - readonly _bidiFirefox: BrowserType; readonly chromium: BrowserType; readonly firefox: BrowserType; readonly webkit: BrowserType; @@ -60,8 +59,6 @@ export class Playwright extends ChannelOwner { this._electron._playwright = this; this._bidiChromium = BrowserType.from(initializer._bidiChromium); this._bidiChromium._playwright = this; - this._bidiFirefox = BrowserType.from(initializer._bidiFirefox); - this._bidiFirefox._playwright = this; this.devices = this._connection.localUtils()?.devices ?? {}; this.selectors = new Selectors(this._connection._platform); this.errors = { TimeoutError }; @@ -72,7 +69,7 @@ export class Playwright extends ChannelOwner { } private _browserTypes(): BrowserType[] { - return [this.chromium, this.firefox, this.webkit, this._bidiChromium, this._bidiFirefox]; + return [this.chromium, this.firefox, this.webkit, this._bidiChromium]; } _preLaunchedBrowser(): Browser { diff --git a/packages/playwright-core/src/inProcessFactory.ts b/packages/playwright-core/src/inProcessFactory.ts index 8a77643c8..3c621133e 100644 --- a/packages/playwright-core/src/inProcessFactory.ts +++ b/packages/playwright-core/src/inProcessFactory.ts @@ -43,7 +43,6 @@ export function createInProcessPlaywright(): PlaywrightAPI { playwrightAPI.webkit._serverLauncher = new BrowserServerLauncherImpl('webkit'); playwrightAPI._android._serverLauncher = new AndroidServerLauncherImpl(); playwrightAPI._bidiChromium._serverLauncher = new BrowserServerLauncherImpl('_bidiChromium'); - playwrightAPI._bidiFirefox._serverLauncher = new BrowserServerLauncherImpl('_bidiFirefox'); // Switch to async dispatch after we got Playwright object. dispatcherConnection.onmessage = message => setImmediate(() => clientConnection.dispatch(message)); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 3913c0545..d9d3153dc 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -378,7 +378,6 @@ scheme.PlaywrightInitializer = tObject({ firefox: tChannel(['BrowserType']), webkit: tChannel(['BrowserType']), _bidiChromium: tChannel(['BrowserType']), - _bidiFirefox: tChannel(['BrowserType']), android: tChannel(['Android']), electron: tChannel(['Electron']), utils: tOptional(tChannel(['LocalUtils'])), diff --git a/packages/playwright-core/src/server/bidi/bidiFirefox.ts b/packages/playwright-core/src/server/bidi/bidiFirefox.ts index 9201a5ffc..09406299c 100644 --- a/packages/playwright-core/src/server/bidi/bidiFirefox.ts +++ b/packages/playwright-core/src/server/bidi/bidiFirefox.ts @@ -34,7 +34,7 @@ import type { RecentLogsCollector } from '../utils/debugLogger'; export class BidiFirefox extends BrowserType { constructor(parent: SdkObject) { - super(parent, '_bidiFirefox'); + super(parent, 'firefox'); } override executablePath(): string { diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index f8b86e40c..e37049178 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -92,7 +92,7 @@ export abstract class BrowserType extends SdkObject { } } - async _innerLaunchWithRetries(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise { + private async _innerLaunchWithRetries(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise { try { return await this._innerLaunch(progress, options, persistent, protocolLogger, userDataDir); } catch (error) { @@ -106,7 +106,7 @@ export abstract class BrowserType extends SdkObject { } } - async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, maybeUserDataDir?: string): Promise { + private async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, maybeUserDataDir?: string): Promise { options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined; const browserLogsCollector = new RecentLogsCollector(); const { browserProcess, userDataDir, artifactsDir, transport } = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, maybeUserDataDir); @@ -317,7 +317,7 @@ export abstract class BrowserType extends SdkObject { } } - _rewriteStartupLog(error: Error): Error { + private _rewriteStartupLog(error: Error): Error { if (!isProtocolError(error)) return error; return this.doRewriteStartupLog(error); diff --git a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts index d26042339..32d359133 100644 --- a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts @@ -54,14 +54,12 @@ export class PlaywrightDispatcher extends Dispatcher { + if (options.channel?.startsWith('moz-')) + return this._bidiFirefox.launch(progress, options, protocolLogger); + return super.launch(progress, options, protocolLogger); + } + + override async launchPersistentContext(progress: Progress, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { cdpPort?: number, internalIgnoreHTTPSErrors?: boolean, socksProxyPort?: number }): Promise { + if (options.channel?.startsWith('moz-')) + return this._bidiFirefox.launchPersistentContext(progress, userDataDir, options); + return super.launchPersistentContext(progress, userDataDir, options); } override connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise { diff --git a/packages/playwright-core/src/server/playwright.ts b/packages/playwright-core/src/server/playwright.ts index bdd98bd8a..c8d33d478 100644 --- a/packages/playwright-core/src/server/playwright.ts +++ b/packages/playwright-core/src/server/playwright.ts @@ -43,7 +43,6 @@ export class Playwright extends SdkObject { readonly firefox: BrowserType; readonly webkit: BrowserType; readonly _bidiChromium: BrowserType; - readonly _bidiFirefox: BrowserType; readonly options: PlaywrightOptions; readonly debugController: DebugController; private _allPages = new Set(); @@ -61,8 +60,7 @@ export class Playwright extends SdkObject { }, null); this.chromium = new Chromium(this); this._bidiChromium = new BidiChromium(this); - this._bidiFirefox = new BidiFirefox(this); - this.firefox = new Firefox(this); + this.firefox = new Firefox(this, new BidiFirefox(this)); this.webkit = new WebKit(this); this.electron = new Electron(this); this.android = new Android(this, new AdbBackend()); diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 700cfdd9b..25f3ecffd 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -404,8 +404,6 @@ const DOWNLOAD_PATHS: Record = { 'win64': 'builds/android/%s/android.zip', }, // TODO(bidi): implement downloads. - '_bidiFirefox': { - } as DownloadPaths, '_bidiChromium': { } as DownloadPaths, }; @@ -502,7 +500,7 @@ function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] { }); } -export type BrowserName = 'chromium' | 'firefox' | 'webkit' | '_bidiFirefox' | '_bidiChromium'; +export type BrowserName = 'chromium' | 'firefox' | 'webkit' | '_bidiChromium'; type InternalTool = 'ffmpeg' | 'winldd' | 'firefox-beta' | 'chromium-tip-of-tree' | 'chromium-headless-shell' | 'chromium-tip-of-tree-headless-shell' | 'android'; type BidiChannel = 'moz-firefox' | 'moz-firefox-beta' | 'moz-firefox-nightly' | 'bidi-chrome-canary' | 'bidi-chrome-stable' | 'bidi-chromium'; type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary'; @@ -958,13 +956,13 @@ export class Registry { return executablePath; } if (shouldThrow) - throw new Error(`Cannot find Firefox installation for channel '${name}' at the standard system paths.`); + throw new Error(`Cannot find Firefox installation for channel '${name}' at the standard system paths. ${`Tried paths:\n ${prefixes.map(p => path.join(p, suffix)).join('\n ')}`}`); return undefined; }; return { type: 'channel', name, - browserName: '_bidiFirefox', + browserName: 'firefox', directory: undefined, executablePath: (sdkLanguage: string) => executablePath(sdkLanguage, false), executablePathOrDie: (sdkLanguage: string) => executablePath(sdkLanguage, true)!, diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d4e4f52c7..86af0171b 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -16291,7 +16291,6 @@ export type AndroidKey = export const _electron: Electron; export const _android: Android; export const _bidiChromium: BrowserType; -export const _bidiFirefox: BrowserType; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {}; diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 6c412c0d9..c9ffdd817 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -100,8 +100,8 @@ const playwrightFixtures: Fixtures = ({ playwright._defaultLaunchOptions = undefined; }, { scope: 'worker', auto: true, box: true }], - browser: [async ({ playwright, browserName, _browserOptions, connectOptions }, use, testInfo) => { - if (!['chromium', 'firefox', 'webkit', '_bidiChromium', '_bidiFirefox'].includes(browserName)) + browser: [async ({ playwright, browserName, _browserOptions, connectOptions }, use) => { + if (!['chromium', 'firefox', 'webkit', '_bidiChromium'].includes(browserName)) throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`); if (connectOptions) { diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index a3bcb262f..f166090f9 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -627,7 +627,6 @@ export type PlaywrightInitializer = { firefox: BrowserTypeChannel, webkit: BrowserTypeChannel, _bidiChromium: BrowserTypeChannel, - _bidiFirefox: BrowserTypeChannel, android: AndroidChannel, electron: ElectronChannel, utils?: LocalUtilsChannel, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index d0b79ba72..9ee2ae7f8 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -795,7 +795,6 @@ Playwright: firefox: BrowserType webkit: BrowserType _bidiChromium: BrowserType - _bidiFirefox: BrowserType android: Android electron: Electron utils: LocalUtils? diff --git a/tests/bidi/playwright.config.ts b/tests/bidi/playwright.config.ts index e4dc01072..0f489eeea 100644 --- a/tests/bidi/playwright.config.ts +++ b/tests/bidi/playwright.config.ts @@ -71,18 +71,18 @@ const config: Config { if (browserName === '_bidiChromium') return process.env.BIDI_CRPATH; - if (browserName === '_bidiFirefox') + if (browserName === 'firefox') return process.env.BIDI_FFPATH; }; const browserToChannels = { '_bidiChromium': ['bidi-chromium', 'bidi-chrome-canary', 'bidi-chrome-stable'], - '_bidiFirefox': ['moz-firefox', 'moz-firefox-beta', 'moz-firefox-nightly'], + 'firefox': ['moz-firefox', 'moz-firefox-beta', 'moz-firefox-nightly'], }; for (const [key, channels] of Object.entries(browserToChannels)) { diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index 2872e3757..0fdc040f0 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -66,15 +66,15 @@ const test = baseTest.extend await run(playwright[browserName]); }, { scope: 'worker' }], - allowsThirdParty: [async ({ browserName }, run) => { - if (browserName === 'firefox' || browserName as any === '_bidiFirefox') + allowsThirdParty: [async ({ browserName, channel }, run) => { + if (browserName === 'firefox' || channel?.startsWith('moz-firefox')) await run(true); else await run(false); }, { scope: 'worker' }], defaultSameSiteCookieValue: [async ({ browserName, platform, channel }, run) => { - if (browserName === 'chromium' || browserName as any === '_bidiChromium' || browserName as any === '_bidiFirefox') + if (browserName === 'chromium' || browserName as any === '_bidiChromium' || channel?.startsWith('moz-firefox')) await run('Lax'); else if (browserName === 'webkit' && (platform === 'linux' || channel === 'webkit-wsl')) await run('Lax'); diff --git a/tests/config/utils.ts b/tests/config/utils.ts index 6fb3fc6a0..41571186a 100644 --- a/tests/config/utils.ts +++ b/tests/config/utils.ts @@ -53,7 +53,7 @@ export async function verifyViewport(page: Page, width: number, height: number) expect(await page.evaluate('window.innerHeight')).toBe(height); } -export function expectedSSLError(browserName: string, platform: string, channel: string): RegExp { +export function expectedSSLError(browserName: string, platform: string, channel: string | undefined): RegExp { if (browserName === 'chromium') return /net::(ERR_CERT_AUTHORITY_INVALID|ERR_CERT_INVALID)/; if (browserName === 'webkit') { @@ -64,7 +64,7 @@ export function expectedSSLError(browserName: string, platform: string, channel: else return /Unacceptable TLS certificate|Operation was cancelled/; } - if (browserName === '_bidiFirefox') + if (channel?.startsWith('moz-firefox')) return /MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT/; return /SSL_ERROR_UNKNOWN/; } diff --git a/tests/library/browsercontext-network-event.spec.ts b/tests/library/browsercontext-network-event.spec.ts index 6eaf9e624..0265d1caa 100644 --- a/tests/library/browsercontext-network-event.spec.ts +++ b/tests/library/browsercontext-network-event.spec.ts @@ -104,8 +104,8 @@ 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, headless }) => { - it.skip(headless && browserName !== 'firefox' && browserName as any !== '_bidiFirefox', 'headless browsers, except firefox, do not request favicons'); +it('should not fire events for favicon or favicon redirects', async ({ context, page, server, browserName, headless, channel }) => { + it.skip(headless && browserName !== 'firefox' && !channel?.startsWith('moz-firefox'), 'headless browsers, except firefox, do not request favicons'); it.skip(!headless && browserName === 'webkit', 'headed webkit does not have a favicon feature'); const favicon = `/no-cache/favicon.ico`; const hashedFaviconUrl = `/favicon-hashed.ico`; diff --git a/tests/library/browsercontext-timezone-id.spec.ts b/tests/library/browsercontext-timezone-id.spec.ts index 91b274fc6..567da9e23 100644 --- a/tests/library/browsercontext-timezone-id.spec.ts +++ b/tests/library/browsercontext-timezone-id.spec.ts @@ -45,13 +45,13 @@ it('should work @smoke', async ({ browser, browserName }) => { } }); -it('should throw for invalid timezone IDs when creating pages', async ({ browser, browserName }) => { +it('should throw for invalid timezone IDs when creating pages', async ({ browser, browserName, channel }) => { for (const timezoneId of ['Foo/Bar', 'Baz/Qux']) { - if (browserName as any === '_bidiChromium' || browserName as any === '_bidiFirefox') { + if (browserName as any === '_bidiChromium' || channel?.startsWith('moz-firefox')) { 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') + else if (channel?.startsWith('moz-firefox')) 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; diff --git a/tests/library/channels.spec.ts b/tests/library/channels.spec.ts index 827f9fe0b..13ec20d3f 100644 --- a/tests/library/channels.spec.ts +++ b/tests/library/channels.spec.ts @@ -46,7 +46,6 @@ it('should scope context handles', async ({ browserType, server, expectScopeStat { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [] } ] }, @@ -69,7 +68,6 @@ it('should scope context handles', async ({ browserType, server, expectScopeStat { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [ { _guid: 'browser-context', objects: [ @@ -106,7 +104,6 @@ it('should scope CDPSession handles', async ({ browserType, browserName, expectS { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [] } ] }, @@ -125,7 +122,6 @@ it('should scope CDPSession handles', async ({ browserType, browserName, expectS { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [ { _guid: 'cdp-session', objects: [] }, @@ -152,7 +148,6 @@ it('should scope browser handles', async ({ browserType, expectScopeState }) => { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'electron', objects: [] }, { _guid: 'localUtils', objects: [] }, { _guid: 'Playwright', objects: [] }, @@ -169,7 +164,6 @@ it('should scope browser handles', async ({ browserType, expectScopeState }) => { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [ @@ -206,7 +200,6 @@ it('should not generate dispatchers for subresources w/o listeners', async ({ pa { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [ @@ -289,10 +282,6 @@ it('exposeFunction should not leak', async ({ page, expectScopeState, server }) '_guid': 'browser-type', 'objects': [], }, - { - '_guid': 'browser-type', - 'objects': [], - }, { '_guid': 'browser-type', 'objects': [ diff --git a/tests/library/favicon.spec.ts b/tests/library/favicon.spec.ts index e8c5d56c7..9d8f1aadc 100644 --- a/tests/library/favicon.spec.ts +++ b/tests/library/favicon.spec.ts @@ -17,8 +17,8 @@ import { contextTest as it } from '../config/browserTest'; -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('should load svg favicon with prefer-color-scheme', async ({ page, server, browserName, headless, asset, channel }) => { + it.skip(headless && browserName !== 'firefox' && !channel?.startsWith('moz-firefox'), 'headless browsers, except firefox, do not request favicons'); it.skip(!headless && browserName === 'webkit', '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 d85ba463c..227589697 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -698,8 +698,8 @@ it('should contain http2 for http2 requests', async ({ contextFactory }, testInf server.close(); }); -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('should filter favicon and favicon redirects', async ({ server, browserName, headless, asset, contextFactory, channel }, testInfo) => { + it.skip(headless && browserName !== 'firefox' && !channel?.startsWith('moz-firefox'), 'headless browsers, except firefox, do not request favicons'); 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/page-close.spec.ts b/tests/library/page-close.spec.ts index e06dfabe9..4b23924f0 100644 --- a/tests/library/page-close.spec.ts +++ b/tests/library/page-close.spec.ts @@ -140,7 +140,7 @@ test('should not throw UnhandledPromiseRejection when page closes', async ({ pag ]).catch(e => {}); }); -test('interrupt request.response() and request.allHeaders() on page.close', async ({ page, server, browserName }) => { +test('interrupt request.response() and request.allHeaders() on page.close', async ({ page, server, browserName, channel }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/27227' }); server.setRoute('/one-style.css', (req, res) => { res.setHeader('Content-Type', 'text/css'); @@ -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' || browserName as any === '_bidiFirefox') + if (browserName === 'firefox' || channel?.startsWith('moz-firefox')) 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 715c03b54..8cdd65ac1 100644 --- a/tests/page/page-basic.spec.ts +++ b/tests/page/page-basic.spec.ts @@ -108,7 +108,7 @@ it('page.frame should respect url', async function({ page, server }) { expect(page.frame({ url: /empty/ }).url()).toBe(server.EMPTY_PAGE); }); -it('should have sane user agent', async ({ page, browserName, isElectron, isAndroid }) => { +it('should have sane user agent', async ({ page, browserName, isElectron, isAndroid, channel }) => { it.skip(isAndroid); it.skip(isElectron); @@ -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' || browserName as any === '_bidiFirefox') { + if (browserName === 'firefox' || channel?.startsWith('moz-firefox')) { const [engine, browser] = part3.split(' '); expect(engine.startsWith('Gecko')).toBe(true); expect(browser.startsWith('Firefox')).toBe(true); diff --git a/tests/page/page-event-console.spec.ts b/tests/page/page-event-console.spec.ts index 081f74393..387e4e570 100644 --- a/tests/page/page-event-console.spec.ts +++ b/tests/page/page-event-console.spec.ts @@ -191,7 +191,7 @@ it('should use object previews for arrays and objects', async ({ page, browserNa expect(text).toEqual('Array JSHandle@object JSHandle@object'); }); -it('should use object previews for errors', async ({ page, browserName }) => { +it('should use object previews for errors', async ({ page, browserName, channel }) => { let text: string; page.on('console', message => { text = message.text(); @@ -201,7 +201,7 @@ it('should use object previews for errors', async ({ page, browserName }) => { expect(text).toContain('.evaluate'); if (browserName as any === '_bidiChromium') expect(text).toEqual('error'); - if (browserName === 'webkit' || browserName as any === '_bidiFirefox') + if (browserName === 'webkit' || channel?.startsWith('moz-firefox')) expect(text).toEqual('Error: Exception'); if (browserName === 'firefox') expect(text).toEqual('Error'); diff --git a/tests/page/page-event-pageerror.spec.ts b/tests/page/page-event-pageerror.spec.ts index 310f4917f..ea1bf0524 100644 --- a/tests/page/page-event-pageerror.spec.ts +++ b/tests/page/page-event-pageerror.spec.ts @@ -17,7 +17,7 @@ import { test as it, expect } from './pageTest'; -it('should fire', async ({ page, server, browserName }) => { +it('should fire', async ({ page, server, browserName, channel }) => { const url = server.PREFIX + '/error.html'; const [error] = await Promise.all([ page.waitForEvent('pageerror'), @@ -25,7 +25,7 @@ it('should fire', async ({ page, server, browserName }) => { ]); expect(error.name).toBe('Error'); expect(error.message).toBe('Fancy error!'); - if (browserName === 'chromium' || browserName === '_bidiChromium' || browserName === '_bidiFirefox') { + if (browserName === 'chromium' || browserName === '_bidiChromium' || channel?.startsWith('moz-firefox')) { expect(error.stack).toBe(`Error: Fancy error! at c (myscript.js:14:11) at b (myscript.js:10:5) diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index 954228de0..354c97005 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -292,12 +292,12 @@ it('should work when page calls history API in beforeunload', async ({ page, ser expect(response.status()).toBe(200); }); -it('should fail when navigating to bad url', async ({ mode, page, browserName }) => { +it('should fail when navigating to bad url', async ({ page, browserName, channel }) => { let error = null; 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') + else if (channel?.startsWith('moz-firefox')) 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 00b9754bd..8dc56af5d 100644 --- a/tests/page/page-route.spec.ts +++ b/tests/page/page-route.spec.ts @@ -344,17 +344,17 @@ it('should send referer', async ({ page, server }) => { expect(request.headers['referer']).toBe('http://google.com/'); }); -it('should fail navigation when aborting main resource', async ({ page, server, browserName, isMac, macVersion }) => { +it('should fail navigation when aborting main resource', async ({ page, server, browserName, isMac, macVersion, channel }) => { await page.route('**/*', route => route.abort()); let error = null; await page.goto(server.EMPTY_PAGE).catch(e => error = e); expect(error).toBeTruthy(); if (browserName === 'webkit') expect(error.message).toContain(isMac && macVersion < 11 ? 'Request intercepted' : 'Blocked by Web Inspector'); + else if (channel?.startsWith('moz-firefox')) + expect(error.message).toContain('NS_ERROR_ABORT'); 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'); }); @@ -609,7 +609,7 @@ it('should not fulfill with redirect status', async ({ page, server, browserName } }); -it('should support cors with GET', async ({ page, server, browserName }) => { +it('should support cors with GET', async ({ page, server, browserName, channel }) => { await page.goto(server.EMPTY_PAGE); await page.route('**/cars*', async (route, request) => { const headers = { 'access-control-allow-origin': request.url().endsWith('allow') ? '*' : 'none' }; @@ -638,7 +638,7 @@ it('should support cors with GET', async ({ page, server, browserName }) => { expect(error.message).toContain('Failed'); if (browserName === 'webkit') expect(error.message).toContain('TypeError'); - if (browserName === 'firefox' || browserName === '_bidiFirefox') + if (browserName === 'firefox' || channel?.startsWith('moz-firefox')) expect(error.message).toContain('NetworkError'); } }); diff --git a/tests/page/pageTestApi.ts b/tests/page/pageTestApi.ts index f29af2c92..bde24e623 100644 --- a/tests/page/pageTestApi.ts +++ b/tests/page/pageTestApi.ts @@ -25,11 +25,11 @@ export type PageTestFixtures = { export type PageWorkerFixtures = { headless: boolean; - channel: string; + channel: string | undefined; screenshot: ScreenshotMode | { mode: ScreenshotMode } & Pick; trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'retain-on-first-failure' | 'on-all-retries' | /** deprecated */ 'retry-with-trace'; video: VideoMode | { mode: VideoMode, size: ViewportSize }; - browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiFirefox' | '_bidiChromium'; + browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiChromium'; browserVersion: string; browserMajorVersion: number; electronMajorVersion: number; diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 8c7863a3c..84b556b9e 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -398,7 +398,6 @@ export type AndroidKey = export const _electron: Electron; export const _android: Android; export const _bidiChromium: BrowserType; -export const _bidiFirefox: BrowserType; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {}; From 83d5165cec218d23c94eaa2629161f029dfc96aa Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 3 Oct 2025 13:44:22 -0700 Subject: [PATCH 012/250] chore(bidi): use bidi- channels with playwright.chromium (#37702) --- packages/playwright-client/types/types.d.ts | 1 - .../playwright-core/src/browserServerImpl.ts | 4 ++-- .../playwright-core/src/client/playwright.ts | 5 +---- .../playwright-core/src/inProcessFactory.ts | 1 - .../playwright-core/src/protocol/validator.ts | 1 - .../src/server/bidi/bidiChromium.ts | 2 +- .../src/server/chromium/chromium.ts | 18 +++++++++++++++++- .../dispatchers/browserContextDispatcher.ts | 4 ++-- .../server/dispatchers/playwrightDispatcher.ts | 2 -- .../playwright-core/src/server/playwright.ts | 4 +--- .../src/server/registry/index.ts | 13 +++++-------- packages/playwright-core/types/types.d.ts | 1 - packages/playwright/src/index.ts | 2 +- packages/protocol/src/channels.d.ts | 1 - packages/protocol/src/protocol.yml | 1 - tests/bidi/playwright.config.ts | 6 +++--- tests/config/browserTest.ts | 4 ++-- .../browsercontext-network-event.spec.ts | 2 +- .../library/browsercontext-timezone-id.spec.ts | 6 +++--- tests/library/channels.spec.ts | 11 ----------- tests/library/favicon.spec.ts | 2 +- tests/library/har.spec.ts | 2 +- tests/library/page-close.spec.ts | 2 +- tests/page/page-basic.spec.ts | 2 +- tests/page/page-event-console.spec.ts | 10 +++++----- tests/page/page-event-network.spec.ts | 2 +- tests/page/page-event-pageerror.spec.ts | 2 +- tests/page/page-goto.spec.ts | 6 ++++-- tests/page/page-route.spec.ts | 4 ++-- tests/page/pageTestApi.ts | 2 +- utils/generate_types/overrides.d.ts | 1 - 31 files changed, 57 insertions(+), 67 deletions(-) diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 86af0171b..ad72bca33 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -16290,7 +16290,6 @@ export type AndroidKey = export const _electron: Electron; export const _android: Android; -export const _bidiChromium: BrowserType; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {}; diff --git a/packages/playwright-core/src/browserServerImpl.ts b/packages/playwright-core/src/browserServerImpl.ts index d0e3229d7..1851cdf89 100644 --- a/packages/playwright-core/src/browserServerImpl.ts +++ b/packages/playwright-core/src/browserServerImpl.ts @@ -32,9 +32,9 @@ import type { WebSocketEventEmitter } from './utilsBundle'; import type { Browser } from './server/browser'; export class BrowserServerLauncherImpl implements BrowserServerLauncher { - private _browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiChromium'; + private _browserName: 'chromium' | 'firefox' | 'webkit'; - constructor(browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiChromium') { + constructor(browserName: 'chromium' | 'firefox' | 'webkit') { this._browserName = browserName; } diff --git a/packages/playwright-core/src/client/playwright.ts b/packages/playwright-core/src/client/playwright.ts index 5b0ed7dcc..de987c0e5 100644 --- a/packages/playwright-core/src/client/playwright.ts +++ b/packages/playwright-core/src/client/playwright.ts @@ -29,7 +29,6 @@ import type { BrowserContextOptions, LaunchOptions } from 'playwright-core'; export class Playwright extends ChannelOwner { readonly _android: Android; readonly _electron: Electron; - readonly _bidiChromium: BrowserType; readonly chromium: BrowserType; readonly firefox: BrowserType; readonly webkit: BrowserType; @@ -57,8 +56,6 @@ export class Playwright extends ChannelOwner { this._android._playwright = this; this._electron = Electron.from(initializer.electron); this._electron._playwright = this; - this._bidiChromium = BrowserType.from(initializer._bidiChromium); - this._bidiChromium._playwright = this; this.devices = this._connection.localUtils()?.devices ?? {}; this.selectors = new Selectors(this._connection._platform); this.errors = { TimeoutError }; @@ -69,7 +66,7 @@ export class Playwright extends ChannelOwner { } private _browserTypes(): BrowserType[] { - return [this.chromium, this.firefox, this.webkit, this._bidiChromium]; + return [this.chromium, this.firefox, this.webkit]; } _preLaunchedBrowser(): Browser { diff --git a/packages/playwright-core/src/inProcessFactory.ts b/packages/playwright-core/src/inProcessFactory.ts index 3c621133e..032d20122 100644 --- a/packages/playwright-core/src/inProcessFactory.ts +++ b/packages/playwright-core/src/inProcessFactory.ts @@ -42,7 +42,6 @@ export function createInProcessPlaywright(): PlaywrightAPI { playwrightAPI.firefox._serverLauncher = new BrowserServerLauncherImpl('firefox'); playwrightAPI.webkit._serverLauncher = new BrowserServerLauncherImpl('webkit'); playwrightAPI._android._serverLauncher = new AndroidServerLauncherImpl(); - playwrightAPI._bidiChromium._serverLauncher = new BrowserServerLauncherImpl('_bidiChromium'); // Switch to async dispatch after we got Playwright object. dispatcherConnection.onmessage = message => setImmediate(() => clientConnection.dispatch(message)); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index d9d3153dc..b8e7c3299 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -377,7 +377,6 @@ scheme.PlaywrightInitializer = tObject({ chromium: tChannel(['BrowserType']), firefox: tChannel(['BrowserType']), webkit: tChannel(['BrowserType']), - _bidiChromium: tChannel(['BrowserType']), android: tChannel(['Android']), electron: tChannel(['Electron']), utils: tOptional(tChannel(['LocalUtils'])), diff --git a/packages/playwright-core/src/server/bidi/bidiChromium.ts b/packages/playwright-core/src/server/bidi/bidiChromium.ts index 0af0dfb4d..e05e02387 100644 --- a/packages/playwright-core/src/server/bidi/bidiChromium.ts +++ b/packages/playwright-core/src/server/bidi/bidiChromium.ts @@ -33,7 +33,7 @@ import type * as types from '../types'; export class BidiChromium extends BrowserType { constructor(parent: SdkObject) { - super(parent, '_bidiChromium'); + super(parent, 'chromium'); } override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions, browserLogsCollector: RecentLogsCollector): Promise { diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index 3db67148c..4adebfe57 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -44,7 +44,9 @@ import type { SdkObject } from '../instrumentation'; import type { Progress } from '../progress'; import type { ProtocolError } from '../protocolError'; import type { ConnectionTransport, ProtocolRequest } from '../transport'; +import type { BrowserContext } from '../browserContext'; import type * as types from '../types'; +import type * as channels from '@protocol/channels'; import type http from 'http'; import type stream from 'stream'; @@ -52,14 +54,28 @@ const ARTIFACTS_FOLDER = path.join(os.tmpdir(), 'playwright-artifacts-'); export class Chromium extends BrowserType { private _devtools: CRDevTools | undefined; + private _bidiChromium: BrowserType; - constructor(parent: SdkObject) { + constructor(parent: SdkObject, bidiChromium: BrowserType) { super(parent, 'chromium'); + this._bidiChromium = bidiChromium; if (debugMode() === 'inspector') this._devtools = this._createDevTools(); } + override launch(progress: Progress, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise { + if (options.channel?.startsWith('bidi-')) + return this._bidiChromium.launch(progress, options, protocolLogger); + return super.launch(progress, options, protocolLogger); + } + + override async launchPersistentContext(progress: Progress, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { cdpPort?: number, internalIgnoreHTTPSErrors?: boolean, socksProxyPort?: number }): Promise { + if (options.channel?.startsWith('bidi-')) + return this._bidiChromium.launchPersistentContext(progress, userDataDir, options); + return super.launchPersistentContext(progress, userDataDir, options); + } + override async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray }) { return await this._connectOverCDPInternal(progress, endpointURL, options); } diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 4555d4c10..b2bf606a1 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -25,7 +25,7 @@ import { Dispatcher } from './dispatcher'; import { FrameDispatcher } from './frameDispatcher'; import { APIRequestContextDispatcher, RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers'; import { BindingCallDispatcher, PageDispatcher, WorkerDispatcher } from './pageDispatcher'; -import { CRBrowserContext } from '../chromium/crBrowser'; +import { CRBrowser, CRBrowserContext } from '../chromium/crBrowser'; import { serializeError } from '../errors'; import { TracingDispatcher } from './tracingDispatcher'; import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher'; @@ -136,7 +136,7 @@ export class BrowserContextDispatcher extends Dispatcher this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this, serviceWorker) })); diff --git a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts index 32d359133..7ca3ebf15 100644 --- a/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/playwrightDispatcher.ts @@ -53,13 +53,11 @@ export class PlaywrightDispatcher extends Dispatcher(); @@ -58,8 +57,7 @@ export class Playwright extends SdkObject { onPageOpen: page => this._allPages.add(page), onPageClose: page => this._allPages.delete(page), }, null); - this.chromium = new Chromium(this); - this._bidiChromium = new BidiChromium(this); + this.chromium = new Chromium(this, new BidiChromium(this)); this.firefox = new Firefox(this, new BidiFirefox(this)); this.webkit = new WebKit(this); this.electron = new Electron(this); diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 25f3ecffd..3fb031c01 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -403,9 +403,6 @@ const DOWNLOAD_PATHS: Record = { 'mac15-arm64': 'builds/android/%s/android.zip', 'win64': 'builds/android/%s/android.zip', }, - // TODO(bidi): implement downloads. - '_bidiChromium': { - } as DownloadPaths, }; export const registryDirectory = (() => { @@ -500,7 +497,7 @@ function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] { }); } -export type BrowserName = 'chromium' | 'firefox' | 'webkit' | '_bidiChromium'; +export type BrowserName = 'chromium' | 'firefox' | 'webkit'; type InternalTool = 'ffmpeg' | 'winldd' | 'firefox-beta' | 'chromium-tip-of-tree' | 'chromium-headless-shell' | 'chromium-tip-of-tree-headless-shell' | 'android'; type BidiChannel = 'moz-firefox' | 'moz-firefox-beta' | 'moz-firefox-nightly' | 'bidi-chrome-canary' | 'bidi-chrome-stable' | 'bidi-chromium'; type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary'; @@ -737,9 +734,9 @@ export class Registry { 'win32': `\\Google\\Chrome SxS\\Application\\chrome.exe`, })); this._executables.push({ - type: 'browser', - name: '_bidiChromium', - browserName: '_bidiChromium', + type: 'channel', + name: 'bidi-chromium', + browserName: 'chromium', directory: chromium.dir, executablePath: () => chromiumExecutable, executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium', chromiumExecutable, chromium.installByDefault, sdkLanguage), @@ -1005,7 +1002,7 @@ export class Registry { return { type: 'channel', name, - browserName: '_bidiChromium', + browserName: 'chromium', directory: undefined, executablePath: (sdkLanguage: string) => executablePath(sdkLanguage, false), executablePathOrDie: (sdkLanguage: string) => executablePath(sdkLanguage, true)!, diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 86af0171b..ad72bca33 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -16290,7 +16290,6 @@ export type AndroidKey = export const _electron: Electron; export const _android: Android; -export const _bidiChromium: BrowserType; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {}; diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index c9ffdd817..0e66ab3d0 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -101,7 +101,7 @@ const playwrightFixtures: Fixtures = ({ }, { scope: 'worker', auto: true, box: true }], browser: [async ({ playwright, browserName, _browserOptions, connectOptions }, use) => { - if (!['chromium', 'firefox', 'webkit', '_bidiChromium'].includes(browserName)) + if (!['chromium', 'firefox', 'webkit'].includes(browserName)) throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`); if (connectOptions) { diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index f166090f9..66fef77a3 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -626,7 +626,6 @@ export type PlaywrightInitializer = { chromium: BrowserTypeChannel, firefox: BrowserTypeChannel, webkit: BrowserTypeChannel, - _bidiChromium: BrowserTypeChannel, android: AndroidChannel, electron: ElectronChannel, utils?: LocalUtilsChannel, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 9ee2ae7f8..73aeba935 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -794,7 +794,6 @@ Playwright: chromium: BrowserType firefox: BrowserType webkit: BrowserType - _bidiChromium: BrowserType android: Android electron: Electron utils: LocalUtils? diff --git a/tests/bidi/playwright.config.ts b/tests/bidi/playwright.config.ts index 0f489eeea..e4f479870 100644 --- a/tests/bidi/playwright.config.ts +++ b/tests/bidi/playwright.config.ts @@ -71,17 +71,17 @@ const config: Config { - if (browserName === '_bidiChromium') + if (browserName === 'chromium') return process.env.BIDI_CRPATH; if (browserName === 'firefox') return process.env.BIDI_FFPATH; }; const browserToChannels = { - '_bidiChromium': ['bidi-chromium', 'bidi-chrome-canary', 'bidi-chrome-stable'], + 'chromium': ['bidi-chromium', 'bidi-chrome-canary', 'bidi-chrome-stable'], 'firefox': ['moz-firefox', 'moz-firefox-beta', 'moz-firefox-nightly'], }; diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index 0fdc040f0..2fe17120e 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -67,14 +67,14 @@ const test = baseTest.extend }, { scope: 'worker' }], allowsThirdParty: [async ({ browserName, channel }, run) => { - if (browserName === 'firefox' || channel?.startsWith('moz-firefox')) + if (browserName === 'firefox') await run(true); else await run(false); }, { scope: 'worker' }], defaultSameSiteCookieValue: [async ({ browserName, platform, channel }, run) => { - if (browserName === 'chromium' || browserName as any === '_bidiChromium' || channel?.startsWith('moz-firefox')) + if (browserName === 'chromium' || channel?.startsWith('moz-firefox')) await run('Lax'); else if (browserName === 'webkit' && (platform === 'linux' || channel === 'webkit-wsl')) await run('Lax'); diff --git a/tests/library/browsercontext-network-event.spec.ts b/tests/library/browsercontext-network-event.spec.ts index 0265d1caa..fff847671 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, headless, channel }) => { - it.skip(headless && browserName !== 'firefox' && !channel?.startsWith('moz-firefox'), 'headless browsers, except firefox, do not request favicons'); + it.skip(headless && browserName !== 'firefox', 'headless browsers, except firefox, do not request favicons'); it.skip(!headless && browserName === 'webkit', 'headed webkit does not have a favicon feature'); const favicon = `/no-cache/favicon.ico`; const hashedFaviconUrl = `/favicon-hashed.ico`; diff --git a/tests/library/browsercontext-timezone-id.spec.ts b/tests/library/browsercontext-timezone-id.spec.ts index 567da9e23..1f6b7c6dc 100644 --- a/tests/library/browsercontext-timezone-id.spec.ts +++ b/tests/library/browsercontext-timezone-id.spec.ts @@ -45,11 +45,11 @@ it('should work @smoke', async ({ browser, browserName }) => { } }); -it('should throw for invalid timezone IDs when creating pages', async ({ browser, browserName, channel }) => { +it('should throw for invalid timezone IDs when creating pages', async ({ browser, channel }) => { for (const timezoneId of ['Foo/Bar', 'Baz/Qux']) { - if (browserName as any === '_bidiChromium' || channel?.startsWith('moz-firefox')) { + if (channel?.startsWith('bidi-chrom') || channel?.startsWith('moz-firefox')) { const error = await browser.newContext({ timezoneId }).catch(e => e); - if (browserName as any === '_bidiChromium') + if (channel?.startsWith('bidi-chrom')) expect(error.message).toContain(`Invalid timezone "${timezoneId}"`); else if (channel?.startsWith('moz-firefox')) 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}`); diff --git a/tests/library/channels.spec.ts b/tests/library/channels.spec.ts index 13ec20d3f..17901000f 100644 --- a/tests/library/channels.spec.ts +++ b/tests/library/channels.spec.ts @@ -45,7 +45,6 @@ it('should scope context handles', async ({ browserType, server, expectScopeStat { _guid: 'android', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [] } ] }, @@ -67,7 +66,6 @@ it('should scope context handles', async ({ browserType, server, expectScopeStat { _guid: 'android', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [ { _guid: 'browser-context', objects: [ @@ -103,7 +101,6 @@ it('should scope CDPSession handles', async ({ browserType, browserName, expectS { _guid: 'android', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [] } ] }, @@ -121,7 +118,6 @@ it('should scope CDPSession handles', async ({ browserType, browserName, expectS { _guid: 'android', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [ { _guid: 'cdp-session', objects: [] }, @@ -147,7 +143,6 @@ it('should scope browser handles', async ({ browserType, expectScopeState }) => { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'electron', objects: [] }, { _guid: 'localUtils', objects: [] }, { _guid: 'Playwright', objects: [] }, @@ -163,7 +158,6 @@ it('should scope browser handles', async ({ browserType, expectScopeState }) => { _guid: 'android', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [ @@ -199,7 +193,6 @@ it('should not generate dispatchers for subresources w/o listeners', async ({ pa { _guid: 'android', objects: [] }, { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [] }, - { _guid: 'browser-type', objects: [] }, { _guid: 'browser-type', objects: [ { _guid: 'browser', objects: [ @@ -278,10 +271,6 @@ it('exposeFunction should not leak', async ({ page, expectScopeState, server }) '_guid': 'browser-type', 'objects': [], }, - { - '_guid': 'browser-type', - 'objects': [], - }, { '_guid': 'browser-type', 'objects': [ diff --git a/tests/library/favicon.spec.ts b/tests/library/favicon.spec.ts index 9d8f1aadc..e12a9897c 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, headless, asset, channel }) => { - it.skip(headless && browserName !== 'firefox' && !channel?.startsWith('moz-firefox'), 'headless browsers, except firefox, do not request favicons'); + it.skip(headless && browserName !== 'firefox', 'headless browsers, except firefox, do not request favicons'); it.skip(!headless && browserName === 'webkit', '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 227589697..fd3f47264 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, headless, asset, contextFactory, channel }, testInfo) => { - it.skip(headless && browserName !== 'firefox' && !channel?.startsWith('moz-firefox'), 'headless browsers, except firefox, do not request favicons'); + it.skip(headless && browserName !== 'firefox', 'headless browsers, except firefox, do not request favicons'); 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/page-close.spec.ts b/tests/library/page-close.spec.ts index 4b23924f0..92d9dda2e 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' || channel?.startsWith('moz-firefox')) + if (browserName === 'firefox') 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 8cdd65ac1..c93ee813e 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' || channel?.startsWith('moz-firefox')) { + if (browserName === 'firefox') { const [engine, browser] = part3.split(' '); expect(engine.startsWith('Gecko')).toBe(true); expect(browser.startsWith('Firefox')).toBe(true); diff --git a/tests/page/page-event-console.spec.ts b/tests/page/page-event-console.spec.ts index 387e4e570..4c1e7e5a7 100644 --- a/tests/page/page-event-console.spec.ts +++ b/tests/page/page-event-console.spec.ts @@ -197,13 +197,13 @@ it('should use object previews for errors', async ({ page, browserName, channel text = message.text(); }); await page.evaluate(() => console.log(new Error('Exception'))); - if (browserName === 'chromium') - expect(text).toContain('.evaluate'); - if (browserName as any === '_bidiChromium') + if (channel?.startsWith('bidi-chrom')) expect(text).toEqual('error'); - if (browserName === 'webkit' || channel?.startsWith('moz-firefox')) + else if (browserName === 'chromium') + expect(text).toContain('.evaluate'); + else if (browserName === 'webkit' || channel?.startsWith('moz-firefox')) expect(text).toEqual('Error: Exception'); - if (browserName === 'firefox') + else if (browserName === 'firefox') expect(text).toEqual('Error'); }); diff --git a/tests/page/page-event-network.spec.ts b/tests/page/page-event-network.spec.ts index c92ef40b5..5618431c9 100644 --- a/tests/page/page-event-network.spec.ts +++ b/tests/page/page-event-network.spec.ts @@ -54,7 +54,7 @@ it('Page.Events.RequestFailed @smoke', async ({ page, server, browserName, platf expect(failedRequests[0].url()).toContain('one-style.css'); expect(await failedRequests[0].response()).toBe(null); expect(failedRequests[0].resourceType()).toBe('stylesheet'); - if (browserName === 'chromium' || browserName === '_bidiChromium') { + if (browserName === 'chromium') { expect(failedRequests[0].failure().errorText).toBe('net::ERR_EMPTY_RESPONSE'); } else if (browserName === 'webkit') { if (platform === 'linux' || channel === 'webkit-wsl') diff --git a/tests/page/page-event-pageerror.spec.ts b/tests/page/page-event-pageerror.spec.ts index ea1bf0524..f2f129f0d 100644 --- a/tests/page/page-event-pageerror.spec.ts +++ b/tests/page/page-event-pageerror.spec.ts @@ -25,7 +25,7 @@ it('should fire', async ({ page, server, browserName, channel }) => { ]); expect(error.name).toBe('Error'); expect(error.message).toBe('Fancy error!'); - if (browserName === 'chromium' || browserName === '_bidiChromium' || channel?.startsWith('moz-firefox')) { + if (browserName === 'chromium' || channel?.startsWith('moz-firefox')) { expect(error.stack).toBe(`Error: Fancy error! at c (myscript.js:14:11) at b (myscript.js:10:5) diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index 354c97005..c70a3a4e7 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -295,10 +295,12 @@ it('should work when page calls history API in beforeunload', async ({ page, ser it('should fail when navigating to bad url', async ({ page, browserName, channel }) => { let error = null; await page.goto('asdfasdf').catch(e => error = e); - if (browserName === 'chromium' || browserName === 'webkit') - expect(error.message).toContain('Cannot navigate to invalid URL'); + if (channel?.startsWith('bidi-chrom')) + expect(error.message).toContain('Invalid URL'); else if (channel?.startsWith('moz-firefox')) expect(error.message).toContain('NS_ERROR_MALFORMED_URI'); + else if (browserName === 'chromium' || browserName === 'webkit') + expect(error.message).toContain('Cannot navigate to invalid URL'); else expect(error.message).toContain('Invalid url'); }); diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts index 8dc56af5d..db0b49490 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, channel } const response = await fetch('https://example.com/cars?reject', { mode: 'cors' }); return response.json(); }).catch(e => e); - if (browserName === 'chromium' || browserName === '_bidiChromium') + if (browserName === 'chromium') expect(error.message).toContain('Failed'); if (browserName === 'webkit') expect(error.message).toContain('TypeError'); - if (browserName === 'firefox' || channel?.startsWith('moz-firefox')) + if (browserName === 'firefox') expect(error.message).toContain('NetworkError'); } }); diff --git a/tests/page/pageTestApi.ts b/tests/page/pageTestApi.ts index bde24e623..1b7c8e0b4 100644 --- a/tests/page/pageTestApi.ts +++ b/tests/page/pageTestApi.ts @@ -29,7 +29,7 @@ export type PageWorkerFixtures = { screenshot: ScreenshotMode | { mode: ScreenshotMode } & Pick; trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'retain-on-first-failure' | 'on-all-retries' | /** deprecated */ 'retry-with-trace'; video: VideoMode | { mode: VideoMode, size: ViewportSize }; - browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiChromium'; + browserName: 'chromium' | 'firefox' | 'webkit'; browserVersion: string; browserMajorVersion: number; electronMajorVersion: number; diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 84b556b9e..2dad53c84 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -397,7 +397,6 @@ export type AndroidKey = export const _electron: Electron; export const _android: Android; -export const _bidiChromium: BrowserType; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {}; From 2c95eb1e60e2d1f458bc123ec0a11a575a93ea59 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 3 Oct 2025 14:33:10 -0700 Subject: [PATCH 013/250] docs: pageErrors should return strings in Java and C# (#37703) --- docs/src/api/class-page.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 82965993c..c45b55a32 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2687,10 +2687,18 @@ Returns up to (currently) 200 last console messages from this page. See [`event: ## async method: Page.pageErrors * since: v1.56 +* langs: js, python - returns: <[Array]<[Error]>> Returns up to (currently) 200 last page errors from this page. See [`event: Page.pageError`] for more details. +## async method: Page.pageErrors +* since: v1.56 +* langs: csharp, java +- returns: <[Array]<[string]>> + +Returns up to (currently) 200 last page errors from this page. See [`event: Page.pageError`] for more details. + ## method: Page.locator * since: v1.14 From eee0d95b6c5278920d4900ae636917bf641eb529 Mon Sep 17 00:00:00 2001 From: stefanseeger Date: Mon, 6 Oct 2025 12:26:24 +0200 Subject: [PATCH 014/250] docs: Enhance `docs/src/api/class-browsercontext.md` (#37680) Signed-off-by: stefanseeger Co-authored-by: Dmitry Gozman --- docs/src/api/class-browsercontext.md | 6 +++--- packages/playwright-client/types/types.d.ts | 8 ++++---- packages/playwright-core/types/types.d.ts | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 97c930322..c49bef583 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -327,9 +327,9 @@ await context.AddCookiesAsync(new[] { cookie1, cookie2 }); - `cookies` <[Array]<[Object]>> - `name` <[string]> - `value` <[string]> - - `url` ?<[string]> Either url or domain / path are required. Optional. - - `domain` ?<[string]> For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url or domain / path are required. Optional. - - `path` ?<[string]> Either url or domain / path are required Optional. + - `url` ?<[string]> Either `url` or both `domain` and `path` are required. Optional. + - `domain` ?<[string]> For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either `url` or both `domain` and `path` are required. Optional. + - `path` ?<[string]> Either `url` or both `domain` and `path` are required. Optional. - `expires` ?<[float]> Unix time in seconds. Optional. - `httpOnly` ?<[boolean]> Optional. - `secure` ?<[boolean]> Optional. diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index ad72bca33..c510663fc 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -8802,18 +8802,18 @@ export interface BrowserContext { value: string; /** - * Either url or domain / path are required. Optional. + * Either `url` or both `domain` and `path` are required. Optional. */ url?: string; /** - * For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url - * or domain / path are required. Optional. + * For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either + * `url` or both `domain` and `path` are required. Optional. */ domain?: string; /** - * Either url or domain / path are required Optional. + * Either `url` or both `domain` and `path` are required. Optional. */ path?: string; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index ad72bca33..c510663fc 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8802,18 +8802,18 @@ export interface BrowserContext { value: string; /** - * Either url or domain / path are required. Optional. + * Either `url` or both `domain` and `path` are required. Optional. */ url?: string; /** - * For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url - * or domain / path are required. Optional. + * For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either + * `url` or both `domain` and `path` are required. Optional. */ domain?: string; /** - * Either url or domain / path are required Optional. + * Either `url` or both `domain` and `path` are required. Optional. */ path?: string; From 30ddf7ad6b7981dafb14b05cff670f00e5fbc48a Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:50:56 +0200 Subject: [PATCH 015/250] test: roll stable-test-runner to 1.57.0-alpha-2025-10-06 (#37716) 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 c23c8fc83..5292749b3 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-29" + "@playwright/test": "^1.57.0-alpha-2025-10-06" } }, "node_modules/@playwright/test": { - "version": "1.56.0-alpha-2025-09-29", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-09-29.tgz", - "integrity": "sha512-tQvJn0aqEQMpuiJkdl0Y2QfRaF9UYWxsb0FijzRcpZ4N4e+nBe05O5QCKcwlZuo+kTRE3jkMJAEDSWbVI3yVGQ==", + "version": "1.57.0-alpha-2025-10-06", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0-alpha-2025-10-06.tgz", + "integrity": "sha512-gato/vSv2fqDrGr892zWtwRTQT/NqKESDbKvWA8mIP8XaTEr1qa5EYn9QxHCb5nxVQ6m1pCVoeS2lLbTzjulaQ==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.0-alpha-2025-09-29" + "playwright": "1.57.0-alpha-2025-10-06" }, "bin": { "playwright": "cli.js" @@ -38,12 +38,12 @@ } }, "node_modules/playwright": { - "version": "1.56.0-alpha-2025-09-29", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-09-29.tgz", - "integrity": "sha512-5JRWGuKNMW0Yl1DXsMn8XFlwLu5gGAyyxNNs++LSY04IYxTZ2RAShkaDRacUf3bPOEO7YhkV9QPhdCUDEeetFg==", + "version": "1.57.0-alpha-2025-10-06", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0-alpha-2025-10-06.tgz", + "integrity": "sha512-vj8RQLQzG+cL+QKwInvab9wGueHj2H3MTmEoyb9BNacSnLKvR8I3PLsWmRDpgjsdFgQFTi4sMvmpebRLYz6ccQ==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-alpha-2025-09-29" + "playwright-core": "1.57.0-alpha-2025-10-06" }, "bin": { "playwright": "cli.js" @@ -56,9 +56,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.0-alpha-2025-09-29", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-09-29.tgz", - "integrity": "sha512-miREetirQbDx9wgH+/Z9RzxycL/6BfTjk/H3eSOacFcZtkMVXvF6Mzbp/0s6z/nyVBvgvqqdULaEzK98zsIR/g==", + "version": "1.57.0-alpha-2025-10-06", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0-alpha-2025-10-06.tgz", + "integrity": "sha512-PLegKftwYd2VZG+2GF4F9HPl1YNZL6EE2T3eMmGr/ZekWSNvb7DRin/USfrLVxrLD5IHXGk/NIvFpa+p2EPQ1g==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -70,11 +70,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.56.0-alpha-2025-09-29", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-alpha-2025-09-29.tgz", - "integrity": "sha512-tQvJn0aqEQMpuiJkdl0Y2QfRaF9UYWxsb0FijzRcpZ4N4e+nBe05O5QCKcwlZuo+kTRE3jkMJAEDSWbVI3yVGQ==", + "version": "1.57.0-alpha-2025-10-06", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0-alpha-2025-10-06.tgz", + "integrity": "sha512-gato/vSv2fqDrGr892zWtwRTQT/NqKESDbKvWA8mIP8XaTEr1qa5EYn9QxHCb5nxVQ6m1pCVoeS2lLbTzjulaQ==", "requires": { - "playwright": "1.56.0-alpha-2025-09-29" + "playwright": "1.57.0-alpha-2025-10-06" } }, "fsevents": { @@ -84,18 +84,18 @@ "optional": true }, "playwright": { - "version": "1.56.0-alpha-2025-09-29", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-09-29.tgz", - "integrity": "sha512-5JRWGuKNMW0Yl1DXsMn8XFlwLu5gGAyyxNNs++LSY04IYxTZ2RAShkaDRacUf3bPOEO7YhkV9QPhdCUDEeetFg==", + "version": "1.57.0-alpha-2025-10-06", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0-alpha-2025-10-06.tgz", + "integrity": "sha512-vj8RQLQzG+cL+QKwInvab9wGueHj2H3MTmEoyb9BNacSnLKvR8I3PLsWmRDpgjsdFgQFTi4sMvmpebRLYz6ccQ==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.56.0-alpha-2025-09-29" + "playwright-core": "1.57.0-alpha-2025-10-06" } }, "playwright-core": { - "version": "1.56.0-alpha-2025-09-29", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-09-29.tgz", - "integrity": "sha512-miREetirQbDx9wgH+/Z9RzxycL/6BfTjk/H3eSOacFcZtkMVXvF6Mzbp/0s6z/nyVBvgvqqdULaEzK98zsIR/g==" + "version": "1.57.0-alpha-2025-10-06", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0-alpha-2025-10-06.tgz", + "integrity": "sha512-PLegKftwYd2VZG+2GF4F9HPl1YNZL6EE2T3eMmGr/ZekWSNvb7DRin/USfrLVxrLD5IHXGk/NIvFpa+p2EPQ1g==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index ff193cab8..f75edebb9 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-29" + "@playwright/test": "^1.57.0-alpha-2025-10-06" } } From 1a20269abd096081ad14808c891820930b4e0a75 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 6 Oct 2025 13:07:10 +0100 Subject: [PATCH 016/250] chore: disable RenderDocument feature (#37715) --- .../playwright-core/src/server/chromium/chromiumSwitches.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts index 5f2526c65..75a715aea 100644 --- a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts +++ b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts @@ -40,6 +40,8 @@ const disabledFeatures = (assistantMode?: boolean) => [ 'Translate', // See https://issues.chromium.org/u/1/issues/435410220 'AutoDeElevate', + // See https://github.com/microsoft/playwright/issues/37714 + 'RenderDocument', assistantMode ? 'AutomationControlled' : '', ].filter(Boolean); From 957cf544ff7e4137143ab52c7609c2ee1df8c987 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 6 Oct 2025 05:26:19 -0700 Subject: [PATCH 017/250] docs: v1.56 release notes (#37687) --- docs/src/release-notes-js.md | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index 202475742..39f494d2b 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -6,6 +6,65 @@ toc_max_heading_level: 2 import LiteYouTube from '@site/src/components/LiteYouTube'; +## Version 1.56 + + + +### Playwright Agents + +Introducing Playwright Agents, three custom agent definitions designed to guide LLMs through the core process of building a Playwright test: + +* **🎭 planner** explores the app and produces a Markdown test plan + +* **🎭 generator** transforms the Markdown plan into the Playwright Test files + +* **🎭 healer** executes the test suite and automatically repairs failing tests + +Run `npx playwright init-agents` with your client of choice to generate the latest agent definitions: + +```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 +``` + +[Learn more about Playwright Agents](./test-agents.md) + +### New APIs + +- New methods [`method: Page.consoleMessages`] and [`method: Page.pageErrors`] for retrieving the most recent console messages from the page +- New method [`method: Page.requests`] for retrieving the most recent network requests from the page +- Added [`--test-list` and `--test-list-invert`](./test-cli.md#test-list) to allow manual specification of specific tests from a file + +### UI Mode and HTML Reporter + +- Added option to `'html'` reporter to disable the "Copy prompt" button +- Added option to `'html'` reporter and UI Mode to merge files, collapsing test and describe blocks into a single unified list +- Added option to UI Mode mirroring the `--update-snapshots` options +- Added option to UI Mode to run only a single worker at a time + +### Breaking Changes + +- Event [`event: BrowserContext.backgroundPage`] has been deprecated and will not be emitted. Method [`method: BrowserContext.backgroundPages`] will return an empty list + +### Miscellaneous + +- Aria snapshots render and compare `input` `placeholder` +- Added environment variable `PLAYWRIGHT_TEST` to Playwright worker processes to allow discriminating on testing status + +### Browser Versions + +- Chromium 141.0.7390.37 +- Mozilla Firefox 142.0.1 +- WebKit 26.0 + ## Version 1.55 ### New APIs From ce1082ec9fe917dbdeb1965fa0eb0a8325f619b5 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 6 Oct 2025 15:16:01 +0200 Subject: [PATCH 018/250] fix: `exposeBinding` should work in parallel (#37721) --- .../src/server/browserContext.ts | 22 +++++++++---------- tests/page/page-expose-function.spec.ts | 11 ++++++++++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 1866e05a2..9edfd3a43 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -93,7 +93,7 @@ export abstract class BrowserContext extends SdkObject { _closeReason: string | undefined; readonly clock: Clock; _clientCertificatesProxy: ClientCertificatesProxy | undefined; - private _playwrightBindingExposed = false; + private _playwrightBindingExposed?: Promise; readonly dialogManager: DialogManager; constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { @@ -304,19 +304,19 @@ export abstract class BrowserContext extends SdkObject { } async exposePlaywrightBindingIfNeeded() { - if (this._playwrightBindingExposed) - return; - this._playwrightBindingExposed = true; - await this.doExposePlaywrightBinding(); + this._playwrightBindingExposed ??= (async () => { + await this.doExposePlaywrightBinding(); - this.bindingsInitScript = PageBinding.createInitScript(); - this.initScripts.push(this.bindingsInitScript); - await this.doAddInitScript(this.bindingsInitScript); - await this.safeNonStallingEvaluateInAllFrames(this.bindingsInitScript.source, 'main'); + this.bindingsInitScript = PageBinding.createInitScript(); + this.initScripts.push(this.bindingsInitScript); + await this.doAddInitScript(this.bindingsInitScript); + await this.safeNonStallingEvaluateInAllFrames(this.bindingsInitScript.source, 'main'); + })(); + return await this._playwrightBindingExposed; } - needsPlaywrightBinding() { - return this._playwrightBindingExposed; + needsPlaywrightBinding(): boolean { + return this._playwrightBindingExposed !== undefined; } async exposeBinding(progress: Progress, name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource, forClient?: unknown): Promise { diff --git a/tests/page/page-expose-function.spec.ts b/tests/page/page-expose-function.spec.ts index e9334c4df..8bafdff2b 100644 --- a/tests/page/page-expose-function.spec.ts +++ b/tests/page/page-expose-function.spec.ts @@ -292,3 +292,14 @@ it('should fail with busted Array.prototype.toJSON', async ({ page }) => { expect.soft(await page.evaluate(() => ([] as any).toJSON())).toBe('"[]"'); }); + +it('exposeBinding should work in parallel', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37712' } }, async ({ page }) => { + await Promise.all([ + page.exposeBinding('foo', () => 42), + page.exposeBinding('bar', () => 42), + ]); + await page.evaluate(() => { + (window as any).foo(); + (window as any).bar(); + }); +}); From 15869b4ca5e79e1b9afbd229ff9a3d28739233e7 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 6 Oct 2025 07:29:51 -0700 Subject: [PATCH 019/250] devops: fix NPM release step (#37727) --- .github/workflows/publish_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 27dbe70a5..b2f9cb2e7 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -50,7 +50,7 @@ jobs: node utils/build/update_canary_version.js --beta --commit-timestamp utils/publish_all_packages.sh --beta - name: "publish release to NPM" - if: contains(github.ref, 'release') && github.event_name == 'release' + if: github.event_name == 'release' && github.event.action == 'published' run: utils/publish_all_packages.sh --release - name: Azure Login From 3270ee68944a04801c3359e5fedc162bc44f5d95 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 6 Oct 2025 17:40:56 +0200 Subject: [PATCH 020/250] chore: update https-proxy-agent (#37713) --- .../playwright-core/ThirdPartyNotices.txt | 153 ++++-------------- .../bundles/utils/package-lock.json | 22 ++- .../bundles/utils/package.json | 2 +- .../socksClientCertificatesInterceptor.ts | 10 +- .../src/server/utils/network.ts | 10 +- tests/config/testserver/index.ts | 10 +- tests/library/fetch-proxy.spec.ts | 34 ++++ tests/library/har.spec.ts | 7 +- 8 files changed, 104 insertions(+), 144 deletions(-) diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index 57cf609a1..f3fd74df4 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -5,6 +5,7 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. - agent-base@6.0.2 (https://github.com/TooTallNate/node-agent-base) +- agent-base@7.1.4 (https://github.com/TooTallNate/proxy-agents) - balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match) - brace-expansion@1.1.12 (https://github.com/juliangruber/brace-expansion) - buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32) @@ -20,7 +21,7 @@ This project incorporates components from the projects listed below. The origina - end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream) - get-stream@5.2.0 (https://github.com/sindresorhus/get-stream) - graceful-fs@4.2.10 (https://github.com/isaacs/node-graceful-fs) -- https-proxy-agent@5.0.1 (https://github.com/TooTallNate/node-https-proxy-agent) +- https-proxy-agent@7.0.6 (https://github.com/TooTallNate/proxy-agents) - ip-address@9.0.5 (https://github.com/beaugunderson/ip-address) - is-docker@2.2.1 (https://github.com/sindresorhus/is-docker) - is-wsl@2.2.0 (https://github.com/sindresorhus/is-wsl) @@ -199,6 +200,33 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF agent-base@6.0.2 AND INFORMATION +%% agent-base@7.1.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER 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 agent-base@7.1.4 AND INFORMATION + %% balanced-match@1.0.2 NOTICES AND INFORMATION BEGIN HERE ========================================= (MIT) @@ -565,124 +593,11 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ========================================= END OF graceful-fs@4.2.10 AND INFORMATION -%% https-proxy-agent@5.0.1 NOTICES AND INFORMATION BEGIN HERE +%% https-proxy-agent@7.0.6 NOTICES AND INFORMATION BEGIN HERE ========================================= -https-proxy-agent -================ -### An HTTP(s) proxy `http.Agent` implementation for HTTPS -[![Build Status](https://github.com/TooTallNate/node-https-proxy-agent/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-https-proxy-agent/actions?workflow=Node+CI) - -This module provides an `http.Agent` implementation that connects to a specified -HTTP or HTTPS proxy server, and can be used with the built-in `https` module. - -Specifically, this `Agent` implementation connects to an intermediary "proxy" -server and issues the [CONNECT HTTP method][CONNECT], which tells the proxy to -open a direct TCP connection to the destination server. - -Since this agent implements the CONNECT HTTP method, it also works with other -protocols that use this method when connecting over proxies (i.e. WebSockets). -See the "Examples" section below for more. - - -Installation ------------- - -Install with `npm`: - -``` bash -$ npm install https-proxy-agent -``` - - -Examples --------- - -#### `https` module example - -``` js -var url = require('url'); -var https = require('https'); -var HttpsProxyAgent = require('https-proxy-agent'); - -// HTTP/HTTPS proxy to connect to -var proxy = process.env.http_proxy || 'http://168.63.76.32:3128'; -console.log('using proxy server %j', proxy); - -// HTTPS endpoint for the proxy to connect to -var endpoint = process.argv[2] || 'https://graph.facebook.com/tootallnate'; -console.log('attempting to GET %j', endpoint); -var options = url.parse(endpoint); - -// create an instance of the `HttpsProxyAgent` class with the proxy server information -var agent = new HttpsProxyAgent(proxy); -options.agent = agent; - -https.get(options, function (res) { - console.log('"response" event!', res.headers); - res.pipe(process.stdout); -}); -``` - -#### `ws` WebSocket connection example - -``` js -var url = require('url'); -var WebSocket = require('ws'); -var HttpsProxyAgent = require('https-proxy-agent'); - -// HTTP/HTTPS proxy to connect to -var proxy = process.env.http_proxy || 'http://168.63.76.32:3128'; -console.log('using proxy server %j', proxy); - -// WebSocket endpoint for the proxy to connect to -var endpoint = process.argv[2] || 'ws://echo.websocket.org'; -var parsed = url.parse(endpoint); -console.log('attempting to connect to WebSocket %j', endpoint); - -// create an instance of the `HttpsProxyAgent` class with the proxy server information -var options = url.parse(proxy); - -var agent = new HttpsProxyAgent(options); - -// finally, initiate the WebSocket connection -var socket = new WebSocket(endpoint, { agent: agent }); - -socket.on('open', function () { - console.log('"open" event!'); - socket.send('hello world'); -}); - -socket.on('message', function (data, flags) { - console.log('"message" event! %j %j', data, flags); - socket.close(); -}); -``` - -API ---- - -### new HttpsProxyAgent(Object options) - -The `HttpsProxyAgent` class implements an `http.Agent` subclass that connects -to the specified "HTTP(s) proxy server" in order to proxy HTTPS and/or WebSocket -requests. This is achieved by using the [HTTP `CONNECT` method][CONNECT]. - -The `options` argument may either be a string URI of the proxy server to use, or an -"options" object with more specific properties: - - * `host` - String - Proxy host to connect to (may use `hostname` as well). Required. - * `port` - Number - Proxy port to connect to. Required. - * `protocol` - String - If `https:`, then use TLS to connect to the proxy. - * `headers` - Object - Additional HTTP headers to be sent on the HTTP CONNECT method. - * Any other options given are passed to the `net.connect()`/`tls.connect()` functions. - - -License -------- - (The MIT License) -Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net> +Copyright (c) 2013 Nathan Rajlich Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -702,10 +617,8 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 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. - -[CONNECT]: http://en.wikipedia.org/wiki/HTTP_tunnel#HTTP_CONNECT_Tunneling ========================================= -END OF https-proxy-agent@5.0.1 AND INFORMATION +END OF https-proxy-agent@7.0.6 AND INFORMATION %% ip-address@9.0.5 NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -1497,6 +1410,6 @@ END OF yazl@2.5.1 AND INFORMATION SUMMARY BEGIN HERE ========================================= -Total Packages: 44 +Total Packages: 45 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json index 7900f7c09..6434eec23 100644 --- a/packages/playwright-core/bundles/utils/package-lock.json +++ b/packages/playwright-core/bundles/utils/package-lock.json @@ -14,7 +14,7 @@ "diff": "^7.0.0", "dotenv": "^16.4.5", "graceful-fs": "4.2.10", - "https-proxy-agent": "5.0.1", + "https-proxy-agent": "7.0.6", "jpeg-js": "0.4.4", "mime": "^3.0.0", "minimatch": "^3.1.2", @@ -236,15 +236,25 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" } }, "node_modules/ip-address": { diff --git a/packages/playwright-core/bundles/utils/package.json b/packages/playwright-core/bundles/utils/package.json index c0559de57..a38fc7abc 100644 --- a/packages/playwright-core/bundles/utils/package.json +++ b/packages/playwright-core/bundles/utils/package.json @@ -9,7 +9,7 @@ "diff": "^7.0.0", "dotenv": "^16.4.5", "graceful-fs": "4.2.10", - "https-proxy-agent": "5.0.1", + "https-proxy-agent": "7.0.6", "jpeg-js": "0.4.4", "mime": "^3.0.0", "minimatch": "^3.1.2", diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 846d2ca3d..71251557c 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -129,10 +129,14 @@ class SocksProxyConnection { async connect() { const proxyAgent = this.socksProxy.getProxyAgent(this.host, this.port); - if (proxyAgent) - this._serverEncrypted = await proxyAgent.callback(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false }); - else + if (proxyAgent) { + if ('callback' in proxyAgent) + this._serverEncrypted = await proxyAgent.callback(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false }); + else + this._serverEncrypted = await proxyAgent.connect(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false }); + } else { this._serverEncrypted = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port); + } this._serverEncrypted.once('close', this._serverCloseEventListener); this._serverEncrypted.once('error', error => this._browserEncrypted.destroy(error)); diff --git a/packages/playwright-core/src/server/utils/network.ts b/packages/playwright-core/src/server/utils/network.ts index 157a6f708..e6b0e61ae 100644 --- a/packages/playwright-core/src/server/utils/network.ts +++ b/packages/playwright-core/src/server/utils/network.ts @@ -62,9 +62,7 @@ export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.Inco method: options.method }; } else { - (parsedProxyURL as any).secureProxy = parsedProxyURL.protocol === 'https:'; - - options.agent = new HttpsProxyAgent(parsedProxyURL); + options.agent = new HttpsProxyAgent(url.format(parsedProxyURL)); options.rejectUnauthorized = false; } } @@ -160,11 +158,11 @@ export function createProxyAgent(proxy?: ProxySettings, forUrl?: URL) { if (forUrl && ['ws:', 'wss:'].includes(forUrl.protocol)) { // Force CONNECT method for WebSockets. - return new HttpsProxyAgent(proxyOpts); + return new HttpsProxyAgent(url.format(proxyOpts)); } - // TODO: We should use HttpProxyAgent conditional on proxyOpts.protocol instead of always using CONNECT method. - return new HttpsProxyAgent(proxyOpts); + // TODO: This branch should be different from above. We should use HttpProxyAgent conditional on proxyOpts.protocol instead of always using CONNECT method. + return new HttpsProxyAgent(url.format(proxyOpts)); } export function createHttpServer(requestListener?: (req: http.IncomingMessage, res: http.ServerResponse) => void): http.Server; diff --git a/tests/config/testserver/index.ts b/tests/config/testserver/index.ts index 177ea131d..c05bd82ec 100644 --- a/tests/config/testserver/index.ts +++ b/tests/config/testserver/index.ts @@ -64,12 +64,16 @@ export class TestServer { return server; } - static async createHTTPS(dirPath: string, port: number, loopback?: string): Promise { - const server = new TestServer(dirPath, port, loopback, { + static async certOptions() { + return { key: await fs.promises.readFile(path.join(__dirname, 'key.pem')), cert: await fs.promises.readFile(path.join(__dirname, 'cert.pem')), passphrase: 'aaaa', - }); + }; + } + + static async createHTTPS(dirPath: string, port: number, loopback?: string): Promise { + const server = new TestServer(dirPath, port, loopback, await this.certOptions()); await server.waitUntilReady(); return server; } diff --git a/tests/library/fetch-proxy.spec.ts b/tests/library/fetch-proxy.spec.ts index 149bd2f2b..5d35bef85 100644 --- a/tests/library/fetch-proxy.spec.ts +++ b/tests/library/fetch-proxy.spec.ts @@ -14,7 +14,9 @@ * limitations under the License. */ +import https from 'node:https'; import { contextTest as it, expect } from '../config/browserTest'; +import { TestServer } from '../config/testserver'; it.skip(({ mode }) => mode !== 'default'); @@ -137,3 +139,35 @@ it('should use socks proxy', async ({ playwright, server, socksPort }) => { const response = await request.get(server.EMPTY_PAGE); expect(await response.text()).toContain('Served by the SOCKS proxy'); }); + +it('should send correct ALPN protocol to HTTPS proxy', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37676' } }, async ({ playwright, server, nodeVersion }) => { + it.skip(nodeVersion.major < 22, 'ALPNCallback is supported starting from Node 22'); + + let offeredProtocols: string[]; + const proxy = https.createServer({ + ...(await TestServer.certOptions()), + ALPNCallback: protocols => { + offeredProtocols = protocols.protocols; + return protocols[0]; + }, + }); + + const port = await new Promise(resolve => { + proxy.listen(0, () => { + const { port } = proxy.address() as any; + resolve(port); + }); + }); + + const request = await playwright.request.newContext({ + proxy: { server: `https://localhost:${port}` }, + ignoreHTTPSErrors: true, + }); + + await expect(request.get(server.EMPTY_PAGE)).rejects.toThrowError(); + + expect(offeredProtocols).toContain('http/1.1'); + + proxy.close(); + await request.dispose(); +}); diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index fd3f47264..c3eb51172 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -16,12 +16,12 @@ */ import { browserTest as it, expect } from '../config/browserTest'; -import * as path from 'path'; import fs from 'fs'; import type { BrowserContext, BrowserContextOptions } from 'playwright-core'; import type { AddressInfo } from 'net'; import type { Log } from '../../packages/trace/src/har'; import { parseHar } from '../config/utils'; +import { TestServer } from '../config/testserver'; const { createHttp2Server } = require('../../packages/playwright-core/lib/utils'); async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise, testInfo: any, options: { outputPath?: string } & Partial> = {}) { @@ -676,10 +676,7 @@ it('should return security details directly from response', async ({ contextFact }); it('should contain http2 for http2 requests', async ({ contextFactory }, testInfo) => { - const server = createHttp2Server({ - key: await fs.promises.readFile(path.join(__dirname, '..', 'config', 'testserver', 'key.pem')), - cert: await fs.promises.readFile(path.join(__dirname, '..', 'config', 'testserver', 'cert.pem')), - }); + const server = createHttp2Server(await TestServer.certOptions()); server.on('stream', stream => { stream.respond({ 'content-type': 'text/html; charset=utf-8', From c0243cb82276577fa74c532996f5840a342ac220 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 6 Oct 2025 08:54:44 -0700 Subject: [PATCH 021/250] chore(mcp): fix file download test flake (#37725) --- tests/mcp/files.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp/files.spec.ts b/tests/mcp/files.spec.ts index 6677a4402..25f9286ab 100644 --- a/tests/mcp/files.spec.ts +++ b/tests/mcp/files.spec.ts @@ -146,6 +146,6 @@ test('navigating to download link emits download', async ({ startClient, server, url: server.PREFIX + '/download', }, })).toHaveResponse({ - downloads: expect.stringContaining(`- Downloaded file test.txt to`), + downloads: expect.stringMatching(`- Downloaded file test\.txt to|- Downloading file test\.txt`), }); }); From cd090717bfd1e2478980c0693a815ef1e279a540 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 6 Oct 2025 08:59:43 -0700 Subject: [PATCH 022/250] chore: roll stable-test-runner to v1.56.0-beta-1759754009000 (#37723) --- .../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 5292749b3..46451ba6e 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.57.0-alpha-2025-10-06" + "@playwright/test": "1.56.0-beta-1759754009000" } }, "node_modules/@playwright/test": { - "version": "1.57.0-alpha-2025-10-06", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0-alpha-2025-10-06.tgz", - "integrity": "sha512-gato/vSv2fqDrGr892zWtwRTQT/NqKESDbKvWA8mIP8XaTEr1qa5EYn9QxHCb5nxVQ6m1pCVoeS2lLbTzjulaQ==", + "version": "1.56.0-beta-1759754009000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-beta-1759754009000.tgz", + "integrity": "sha512-76zn3ZwPQCWL7LRQk+YE2QOHC1zsRIX3LLJrCRu0upM7avFNuZfrSUTgyoo4gJRAk/rlglpeCUVmwAMQ2TaxNQ==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.57.0-alpha-2025-10-06" + "playwright": "1.56.0-beta-1759754009000" }, "bin": { "playwright": "cli.js" @@ -38,12 +38,12 @@ } }, "node_modules/playwright": { - "version": "1.57.0-alpha-2025-10-06", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0-alpha-2025-10-06.tgz", - "integrity": "sha512-vj8RQLQzG+cL+QKwInvab9wGueHj2H3MTmEoyb9BNacSnLKvR8I3PLsWmRDpgjsdFgQFTi4sMvmpebRLYz6ccQ==", + "version": "1.56.0-beta-1759754009000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-beta-1759754009000.tgz", + "integrity": "sha512-qzJggi6tENdKypgsjVar4tVQa+vnh+R0yAd3qnBy/TwK/gLNbBlRSW6zX81QR6Y9/kPXNJzujmPeSiyBOvL24g==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0-alpha-2025-10-06" + "playwright-core": "1.56.0-beta-1759754009000" }, "bin": { "playwright": "cli.js" @@ -56,9 +56,9 @@ } }, "node_modules/playwright-core": { - "version": "1.57.0-alpha-2025-10-06", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0-alpha-2025-10-06.tgz", - "integrity": "sha512-PLegKftwYd2VZG+2GF4F9HPl1YNZL6EE2T3eMmGr/ZekWSNvb7DRin/USfrLVxrLD5IHXGk/NIvFpa+p2EPQ1g==", + "version": "1.56.0-beta-1759754009000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-beta-1759754009000.tgz", + "integrity": "sha512-vkZajl5x77nAt53oKh21RbXD4ErY24O2KFII0sC3UKLyM6UzJdT0KZ3u0oxZ0GXAy/PtnOcC+5NMNZYUFa7tLQ==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -70,11 +70,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.57.0-alpha-2025-10-06", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0-alpha-2025-10-06.tgz", - "integrity": "sha512-gato/vSv2fqDrGr892zWtwRTQT/NqKESDbKvWA8mIP8XaTEr1qa5EYn9QxHCb5nxVQ6m1pCVoeS2lLbTzjulaQ==", + "version": "1.56.0-beta-1759754009000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-beta-1759754009000.tgz", + "integrity": "sha512-76zn3ZwPQCWL7LRQk+YE2QOHC1zsRIX3LLJrCRu0upM7avFNuZfrSUTgyoo4gJRAk/rlglpeCUVmwAMQ2TaxNQ==", "requires": { - "playwright": "1.57.0-alpha-2025-10-06" + "playwright": "1.56.0-beta-1759754009000" } }, "fsevents": { @@ -84,18 +84,18 @@ "optional": true }, "playwright": { - "version": "1.57.0-alpha-2025-10-06", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0-alpha-2025-10-06.tgz", - "integrity": "sha512-vj8RQLQzG+cL+QKwInvab9wGueHj2H3MTmEoyb9BNacSnLKvR8I3PLsWmRDpgjsdFgQFTi4sMvmpebRLYz6ccQ==", + "version": "1.56.0-beta-1759754009000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-beta-1759754009000.tgz", + "integrity": "sha512-qzJggi6tENdKypgsjVar4tVQa+vnh+R0yAd3qnBy/TwK/gLNbBlRSW6zX81QR6Y9/kPXNJzujmPeSiyBOvL24g==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.57.0-alpha-2025-10-06" + "playwright-core": "1.56.0-beta-1759754009000" } }, "playwright-core": { - "version": "1.57.0-alpha-2025-10-06", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0-alpha-2025-10-06.tgz", - "integrity": "sha512-PLegKftwYd2VZG+2GF4F9HPl1YNZL6EE2T3eMmGr/ZekWSNvb7DRin/USfrLVxrLD5IHXGk/NIvFpa+p2EPQ1g==" + "version": "1.56.0-beta-1759754009000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-beta-1759754009000.tgz", + "integrity": "sha512-vkZajl5x77nAt53oKh21RbXD4ErY24O2KFII0sC3UKLyM6UzJdT0KZ3u0oxZ0GXAy/PtnOcC+5NMNZYUFa7tLQ==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index f75edebb9..14535a804 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.57.0-alpha-2025-10-06" + "@playwright/test": "1.56.0-beta-1759754009000" } } From dfc931601a4614e5e8f5ca88d23c1e9fddb07528 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Mon, 6 Oct 2025 18:22:24 +0200 Subject: [PATCH 023/250] test(bidi): fix the csv reporter (#37729) --- tests/bidi/csvReporter.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/bidi/csvReporter.ts b/tests/bidi/csvReporter.ts index 54eee194b..67aa26c85 100644 --- a/tests/bidi/csvReporter.ts +++ b/tests/bidi/csvReporter.ts @@ -58,8 +58,16 @@ class CsvReporter implements Reporter { row.push('fixme' + (fixme.description ? `: ${fixme.description}` : '')); } else { const result = test.results.find(r => r.error); - const errorMessage = stripAnsi(result?.error?.message.replace(/\s+/g, ' ').trim().substring(0, 1024)); - row.push(csvEscape(errorMessage ?? '')); + if (result) { + const errorMessage = stripAnsi(result.error?.message.replace(/\s+/g, ' ').trim().substring(0, 1024) ?? ''); + row.push(csvEscape(errorMessage)); + } else { + const fail = test.annotations.find(a => a.type === 'fail'); + if (fail) + row.push(csvEscape(`Should have failed: ${fail.description}`)); + else + row.push(''); + } } rows.push(row); } From e232ae1455488778c721cb6e3f629f3531ba4699 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 6 Oct 2025 09:34:57 -0700 Subject: [PATCH 024/250] test(bidi): always use a channel for bidi chromium (#37732) --- tests/bidi/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bidi/playwright.config.ts b/tests/bidi/playwright.config.ts index e4f479870..c2f414dfc 100644 --- a/tests/bidi/playwright.config.ts +++ b/tests/bidi/playwright.config.ts @@ -108,7 +108,7 @@ for (const [key, channels] of Object.entries(browserToChannels)) { use: { browserName, headless: !headed, - channel: channel === 'bidi-chromium' ? undefined : channel, + channel, video: 'off', launchOptions: { executablePath, From 26d0024c00af335644527e0479a59454adf9e246 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 6 Oct 2025 10:25:20 -0700 Subject: [PATCH 025/250] docs: add agents video to agents page (#37731) --- docs/src/test-agents-js.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/test-agents-js.md b/docs/src/test-agents-js.md index b608ab790..254afc83b 100644 --- a/docs/src/test-agents-js.md +++ b/docs/src/test-agents-js.md @@ -3,8 +3,12 @@ id: test-agents title: "Agents" --- +import LiteYouTube from '@site/src/components/LiteYouTube'; + # Playwright Agents +## Introduction + Playwright comes with three Playwright Agents out of the box: **🎭 planner**, **🎭 generator** and **🎭 healer**. These agents can be used independently, sequentially, or as the chained calls in the agentic loop. @@ -16,6 +20,11 @@ Using them sequentially will produce test coverage for your product. * **🎭 healer** executes the test suite and automatically repairs failing tests + + ### Getting Started Start with adding Playwright Agent definitions to your project using From d963d12fbdecd4a5fdea9026110e150f9a5cdaff Mon Sep 17 00:00:00 2001 From: Nil Gallego Date: Tue, 7 Oct 2025 09:43:50 +0100 Subject: [PATCH 026/250] =?UTF-8?q?fix(trace-viewer):=20decode=20+=20quote?= =?UTF-8?q?=20report=20path=20in=20fallback=20=E2=80=9CCopy=20Command?= =?UTF-8?q?=E2=80=9D=20(file://)=20=20(#37710)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/trace-viewer/index.html | 4 ++-- tests/playwright-test/reporter-html.spec.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/trace-viewer/index.html b/packages/trace-viewer/index.html index a131ab208..d665c73e9 100644 --- a/packages/trace-viewer/index.html +++ b/packages/trace-viewer/index.html @@ -36,13 +36,13 @@ // Best-effort to show the report path in the dialog. if (isTraceViewerInsidePlaywrightReport) { const reportPath = (() => { - const base = window.location.pathname.replace(/\/trace\/index\.html$/, ''); + const base = decodeURIComponent(window.location.pathname).replace(/\/trace\/index\.html$/, ''); if (navigator.platform === 'Win32') return base.replace(/^\//, '').replace(/\//g, '\\\\'); return base; })(); const reportLink = document.createElement('div'); - const command = `npx playwright show-report ${reportPath}`; + const command = `npx playwright show-report "${reportPath}"`; reportLink.innerHTML = `You can open the report via ${command} from your Playwright project. `; fallbackErrorDialog.insertBefore(reportLink, fallbackErrorDialog.children[1]); reportLink.querySelector('button').addEventListener('click', () => navigator.clipboard.writeText(command)); diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 8992eac3c..8e53892c1 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -2943,6 +2943,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { await expect(page.locator('.test-case-path')).toHaveText('Root describe'); }); + test('should print a user-friendly warning when opening a trace via file:// protocol', async ({ runInlineTest, showReport, page }) => { await runInlineTest({ 'playwright.config.ts': ` @@ -2965,10 +2966,16 @@ for (const useIntermediateMergeReport of [true, false] as const) { const reportPath = path.join(test.info().outputPath(), 'playwright-report'); await page.goto(url.pathToFileURL(path.join(reportPath, 'index.html')).toString()); await page.getByRole('link', { name: 'View trace' }).click(); - await expect(page.locator('#fallback-error')).toContainText('The Playwright Trace Viewer must be loaded over the http:// or https:// protocols.'); - await expect(page.locator('#fallback-error')).toContainText(`npx playwright show-report ${reportPath.replace(/\\/g, '\\\\')}`); + await expect(page.locator('#fallback-error')).toContainText( + 'The Playwright Trace Viewer must be loaded over the http:// or https:// protocols.' + ); + const expectedReportPath = reportPath.replace(/\\/g, '\\\\'); + await expect(page.locator('#fallback-error')).toContainText( + `npx playwright show-report "${expectedReportPath}"` + ); }); + test('should not collate identical file names in different project directories', async ({ runInlineTest, page }) => { await runInlineTest({ 'playwright.config.ts': ` From 232ea015fa2ba45caf84a452910aca09aa857e60 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 7 Oct 2025 11:20:21 +0200 Subject: [PATCH 027/250] fix: httpRequest for http url via https proxy (#37743) --- .../src/server/utils/network.ts | 1 + ...playwright-cli-install-should-work.spec.ts | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/packages/playwright-core/src/server/utils/network.ts b/packages/playwright-core/src/server/utils/network.ts index e6b0e61ae..a10bcc7ea 100644 --- a/packages/playwright-core/src/server/utils/network.ts +++ b/packages/playwright-core/src/server/utils/network.ts @@ -58,6 +58,7 @@ export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.Inco path: parsedUrl.href, host: parsedProxyURL.hostname, port: parsedProxyURL.port, + protocol: parsedProxyURL.protocol || 'http:', headers: options.headers, method: options.method }; diff --git a/tests/installation/playwright-cli-install-should-work.spec.ts b/tests/installation/playwright-cli-install-should-work.spec.ts index c3116aae3..a4593a98b 100755 --- a/tests/installation/playwright-cli-install-should-work.spec.ts +++ b/tests/installation/playwright-cli-install-should-work.spec.ts @@ -16,7 +16,9 @@ import { test, expect } from './npmTest'; import { chromium } from '@playwright/test'; import path from 'path'; +import https from 'https'; import { TestProxy } from '../config/proxy'; +import { TestServer } from '../config/testserver'; test.use({ isolateBrowsers: true }); @@ -90,6 +92,30 @@ test('install command should ignore HTTP_PROXY', { annotation: { type: 'issue', }); }); +test('install command should work with HTTPS proxy for HTTP downloads', async ({ exec }) => { + await exec('npm i playwright'); + const httpsProxyServer = https.createServer(await TestServer.certOptions()); + + let requestCount = 0; + httpsProxyServer.on('request', (_req, res) => { + requestCount++; + res.statusCode = 502; + res.end(); + }); + + await new Promise(resolve => httpsProxyServer.listen(0, resolve)); + await exec('npx playwright install chromium', { + env: { + PLAYWRIGHT_DOWNLOAD_HOST: 'http://example.com', + ALL_PROXY: `https://localhost:${(httpsProxyServer.address() as any).port}`, + NODE_TLS_REJECT_UNAUTHORIZED: '0', + }, + expectToExitWithError: true, + }); + expect(requestCount).toBeGreaterThan(0); + httpsProxyServer.close(); +}); + test('should be able to remove browsers', async ({ exec, checkInstalledSoftwareOnDisk }) => { await exec('npm i playwright'); await exec('npx playwright install chromium'); From eb42923b1632acb612f95c4482a8ce50babc8313 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 7 Oct 2025 17:18:15 +0200 Subject: [PATCH 028/250] fix: step id not found error log (#37748) --- packages/playwright/src/worker/testInfo.ts | 13 +++++++++---- tests/playwright-test/reporter-attachment.spec.ts | 12 +++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index e4add9bba..c007255bc 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -459,15 +459,20 @@ export class TestInfoImpl implements TestInfo { }); this._attach( await normalizeAndSaveAttachment(this.outputPath(), name, options), - step.group ? undefined : step.stepId + step.stepId ); step.complete({}); } _attach(attachment: TestInfo['attachments'][0], stepId: string | undefined) { const index = this._attachmentsPush(attachment) - 1; - if (stepId) { - this._stepMap.get(stepId)!.attachmentIndices.push(index); + + let step = stepId ? this._stepMap.get(stepId) : undefined; + if (!!step?.group) + step = undefined; + + if (step) { + step.attachmentIndices.push(index); } else { const stepId = `attach@${createGuid()}`; this._tracing.appendBeforeActionForStep({ stepId, title: `Attach ${escapeWithQuotes(attachment.name, '"')}`, category: 'test.attach', stack: [] }); @@ -480,7 +485,7 @@ export class TestInfoImpl implements TestInfo { contentType: attachment.contentType, path: attachment.path, body: attachment.body?.toString('base64'), - stepId, + stepId: step?.stepId, }); } diff --git a/tests/playwright-test/reporter-attachment.spec.ts b/tests/playwright-test/reporter-attachment.spec.ts index 65a536e77..8935acd73 100644 --- a/tests/playwright-test/reporter-attachment.spec.ts +++ b/tests/playwright-test/reporter-attachment.spec.ts @@ -323,7 +323,7 @@ test('render text attachment with multiple lines', async ({ runInlineTest }) => 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 }) => { +test('attaching inside boxed fixture should not log error', { annotation: [{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37147' }, { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37747' }] }, async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': ` import { test as base } from '@playwright/test'; @@ -334,6 +334,11 @@ test('attaching inside boxed fixture should not log error', { annotation: { type body: 'foo', contentType: 'text/plain', }); + testInfo.attachments.push({ + name: 'my attachment 2', + body: Buffer.from('bar'), + contentType: 'text/plain', + }); await use(); }, { box: true }], }); @@ -343,9 +348,6 @@ test('attaching inside boxed fixture should not log error', { annotation: { type }); `, }, { reporter: 'line' }, {}); - const text = result.output; - expect(text).toContain(' attachment #1: my attachment (text/plain) ──────────────────────────────────────────────────────'); - expect(text).toContain(' foo'); - expect(text).toContain(' ────────────────────────────────────────────────────────────────────────────────────────────────'); + expect(result.output).not.toContain('step id not found'); expect(result.exitCode).toBe(1); }); From 490fd6b347c6a587e1b9af57475d7e587194a1f0 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 7 Oct 2025 17:24:51 +0200 Subject: [PATCH 029/250] chore: update socks-proxy-agent (#37752) --- .../playwright-core/ThirdPartyNotices.txt | 291 +----------------- .../bundles/utils/package-lock.json | 38 +-- .../bundles/utils/package.json | 2 +- .../socksClientCertificatesInterceptor.ts | 10 +- .../src/server/utils/network.ts | 15 +- 5 files changed, 34 insertions(+), 322 deletions(-) diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index f3fd74df4..6af0b4b42 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -4,7 +4,6 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. -- agent-base@6.0.2 (https://github.com/TooTallNate/node-agent-base) - agent-base@7.1.4 (https://github.com/TooTallNate/proxy-agents) - balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match) - brace-expansion@1.1.12 (https://github.com/juliangruber/brace-expansion) @@ -41,7 +40,7 @@ This project incorporates components from the projects listed below. The origina - retry@0.12.0 (https://github.com/tim-kos/node-retry) - signal-exit@3.0.7 (https://github.com/tapjs/signal-exit) - smart-buffer@4.2.0 (https://github.com/JoshGlazebrook/smart-buffer) -- socks-proxy-agent@6.1.1 (https://github.com/TooTallNate/node-socks-proxy-agent) +- socks-proxy-agent@8.0.5 (https://github.com/TooTallNate/proxy-agents) - socks@2.8.3 (https://github.com/JoshGlazebrook/socks) - sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js) - wrappy@1.0.2 (https://github.com/npm/wrappy) @@ -50,156 +49,6 @@ This project incorporates components from the projects listed below. The origina - yauzl@3.2.0 (https://github.com/thejoshwolfe/yauzl) - yazl@2.5.1 (https://github.com/thejoshwolfe/yazl) -%% agent-base@6.0.2 NOTICES AND INFORMATION BEGIN HERE -========================================= -agent-base -========== -### Turn a function into an [`http.Agent`][http.Agent] instance -[![Build Status](https://github.com/TooTallNate/node-agent-base/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-agent-base/actions?workflow=Node+CI) - -This module provides an `http.Agent` generator. That is, you pass it an async -callback function, and it returns a new `http.Agent` instance that will invoke the -given callback function when sending outbound HTTP requests. - -#### Some subclasses: - -Here's some more interesting uses of `agent-base`. -Send a pull request to list yours! - - * [`http-proxy-agent`][http-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTP endpoints - * [`https-proxy-agent`][https-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTPS endpoints - * [`pac-proxy-agent`][pac-proxy-agent]: A PAC file proxy `http.Agent` implementation for HTTP and HTTPS - * [`socks-proxy-agent`][socks-proxy-agent]: A SOCKS proxy `http.Agent` implementation for HTTP and HTTPS - - -Installation ------------- - -Install with `npm`: - -``` bash -$ npm install agent-base -``` - - -Example -------- - -Here's a minimal example that creates a new `net.Socket` connection to the server -for every HTTP request (i.e. the equivalent of `agent: false` option): - -```js -var net = require('net'); -var tls = require('tls'); -var url = require('url'); -var http = require('http'); -var agent = require('agent-base'); - -var endpoint = 'http://nodejs.org/api/'; -var parsed = url.parse(endpoint); - -// This is the important part! -parsed.agent = agent(function (req, opts) { - var socket; - // `secureEndpoint` is true when using the https module - if (opts.secureEndpoint) { - socket = tls.connect(opts); - } else { - socket = net.connect(opts); - } - return socket; -}); - -// Everything else works just like normal... -http.get(parsed, function (res) { - console.log('"response" event!', res.headers); - res.pipe(process.stdout); -}); -``` - -Returning a Promise or using an `async` function is also supported: - -```js -agent(async function (req, opts) { - await sleep(1000); - // etc… -}); -``` - -Return another `http.Agent` instance to "pass through" the responsibility -for that HTTP request to that agent: - -```js -agent(function (req, opts) { - return opts.secureEndpoint ? https.globalAgent : http.globalAgent; -}); -``` - - -API ---- - -## Agent(Function callback[, Object options]) → [http.Agent][] - -Creates a base `http.Agent` that will execute the callback function `callback` -for every HTTP request that it is used as the `agent` for. The callback function -is responsible for creating a `stream.Duplex` instance of some kind that will be -used as the underlying socket in the HTTP request. - -The `options` object accepts the following properties: - - * `timeout` - Number - Timeout for the `callback()` function in milliseconds. Defaults to Infinity (optional). - -The callback function should have the following signature: - -### callback(http.ClientRequest req, Object options, Function cb) → undefined - -The ClientRequest `req` can be accessed to read request headers and -and the path, etc. The `options` object contains the options passed -to the `http.request()`/`https.request()` function call, and is formatted -to be directly passed to `net.connect()`/`tls.connect()`, or however -else you want a Socket to be created. Pass the created socket to -the callback function `cb` once created, and the HTTP request will -continue to proceed. - -If the `https` module is used to invoke the HTTP request, then the -`secureEndpoint` property on `options` _will be set to `true`_. - - -License -------- - -(The MIT License) - -Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net> - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER 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. - -[http-proxy-agent]: https://github.com/TooTallNate/node-http-proxy-agent -[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent -[pac-proxy-agent]: https://github.com/TooTallNate/node-pac-proxy-agent -[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent -[http.Agent]: https://nodejs.org/api/http.html#http_class_http_agent -========================================= -END OF agent-base@6.0.2 AND INFORMATION - %% agent-base@7.1.4 NOTICES AND INFORMATION BEGIN HERE ========================================= (The MIT License) @@ -1082,141 +931,11 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF smart-buffer@4.2.0 AND INFORMATION -%% socks-proxy-agent@6.1.1 NOTICES AND INFORMATION BEGIN HERE +%% socks-proxy-agent@8.0.5 NOTICES AND INFORMATION BEGIN HERE ========================================= -socks-proxy-agent -================ -### A SOCKS proxy `http.Agent` implementation for HTTP and HTTPS -[![Build Status](https://github.com/TooTallNate/node-socks-proxy-agent/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-socks-proxy-agent/actions?workflow=Node+CI) - -This module provides an `http.Agent` implementation that connects to a -specified SOCKS proxy server, and can be used with the built-in `http` -and `https` modules. - -It can also be used in conjunction with the `ws` module to establish a WebSocket -connection over a SOCKS proxy. See the "Examples" section below. - -Installation ------------- - -Install with `npm`: - -``` bash -$ npm install socks-proxy-agent -``` - - -Examples --------- - -#### TypeScript example - -```ts -import https from 'https'; -import { SocksProxyAgent } from 'socks-proxy-agent'; - -const info = { - host: 'br41.nordvpn.com', - userId: 'your-name@gmail.com', - password: 'abcdef12345124' -}; -const agent = new SocksProxyAgent(info); - -https.get('https://jsonip.org', { agent }, (res) => { - console.log(res.headers); - res.pipe(process.stdout); -}); -``` - -#### `http` module example - -```js -var url = require('url'); -var http = require('http'); -var SocksProxyAgent = require('socks-proxy-agent'); - -// SOCKS proxy to connect to -var proxy = process.env.socks_proxy || 'socks://127.0.0.1:1080'; -console.log('using proxy server %j', proxy); - -// HTTP endpoint for the proxy to connect to -var endpoint = process.argv[2] || 'http://nodejs.org/api/'; -console.log('attempting to GET %j', endpoint); -var opts = url.parse(endpoint); - -// create an instance of the `SocksProxyAgent` class with the proxy server information -var agent = new SocksProxyAgent(proxy); -opts.agent = agent; - -http.get(opts, function (res) { - console.log('"response" event!', res.headers); - res.pipe(process.stdout); -}); -``` - -#### `https` module example - -```js -var url = require('url'); -var https = require('https'); -var SocksProxyAgent = require('socks-proxy-agent'); - -// SOCKS proxy to connect to -var proxy = process.env.socks_proxy || 'socks://127.0.0.1:1080'; -console.log('using proxy server %j', proxy); - -// HTTP endpoint for the proxy to connect to -var endpoint = process.argv[2] || 'https://encrypted.google.com/'; -console.log('attempting to GET %j', endpoint); -var opts = url.parse(endpoint); - -// create an instance of the `SocksProxyAgent` class with the proxy server information -var agent = new SocksProxyAgent(proxy); -opts.agent = agent; - -https.get(opts, function (res) { - console.log('"response" event!', res.headers); - res.pipe(process.stdout); -}); -``` - -#### `ws` WebSocket connection example - -``` js -var WebSocket = require('ws'); -var SocksProxyAgent = require('socks-proxy-agent'); - -// SOCKS proxy to connect to -var proxy = process.env.socks_proxy || 'socks://127.0.0.1:1080'; -console.log('using proxy server %j', proxy); - -// WebSocket endpoint for the proxy to connect to -var endpoint = process.argv[2] || 'ws://echo.websocket.org'; -console.log('attempting to connect to WebSocket %j', endpoint); - -// create an instance of the `SocksProxyAgent` class with the proxy server information -var agent = new SocksProxyAgent(proxy); - -// initiate the WebSocket connection -var socket = new WebSocket(endpoint, { agent: agent }); - -socket.on('open', function () { - console.log('"open" event!'); - socket.send('hello world'); -}); - -socket.on('message', function (data, flags) { - console.log('"message" event! %j %j', data, flags); - socket.close(); -}); -``` - -License -------- - (The MIT License) -Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net> +Copyright (c) 2013 Nathan Rajlich Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -1237,7 +956,7 @@ CLAIM, DAMAGES OR OTHER 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 socks-proxy-agent@6.1.1 AND INFORMATION +END OF socks-proxy-agent@8.0.5 AND INFORMATION %% socks@2.8.3 NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -1410,6 +1129,6 @@ END OF yazl@2.5.1 AND INFORMATION SUMMARY BEGIN HERE ========================================= -Total Packages: 45 +Total Packages: 44 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json index 6434eec23..3d2a48a6e 100644 --- a/packages/playwright-core/bundles/utils/package-lock.json +++ b/packages/playwright-core/bundles/utils/package-lock.json @@ -24,7 +24,7 @@ "proxy-from-env": "1.1.0", "retry": "0.12.0", "signal-exit": "3.0.7", - "socks-proxy-agent": "6.1.1", + "socks-proxy-agent": "8.0.5", "ws": "8.17.1", "yaml": "^2.6.0" }, @@ -139,14 +139,12 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/balanced-match": { @@ -248,15 +246,6 @@ "node": ">= 14" } }, - "node_modules/https-proxy-agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -404,16 +393,17 @@ } }, "node_modules/socks-proxy-agent": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", - "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.1", - "socks": "^2.6.1" + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, "node_modules/sprintf-js": { diff --git a/packages/playwright-core/bundles/utils/package.json b/packages/playwright-core/bundles/utils/package.json index a38fc7abc..f0023dc38 100644 --- a/packages/playwright-core/bundles/utils/package.json +++ b/packages/playwright-core/bundles/utils/package.json @@ -19,7 +19,7 @@ "proxy-from-env": "1.1.0", "retry": "0.12.0", "signal-exit": "3.0.7", - "socks-proxy-agent": "6.1.1", + "socks-proxy-agent": "8.0.5", "ws": "8.17.1", "yaml": "^2.6.0" }, diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 71251557c..416f5078e 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -129,14 +129,10 @@ class SocksProxyConnection { async connect() { const proxyAgent = this.socksProxy.getProxyAgent(this.host, this.port); - if (proxyAgent) { - if ('callback' in proxyAgent) - this._serverEncrypted = await proxyAgent.callback(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false }); - else - this._serverEncrypted = await proxyAgent.connect(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false }); - } else { + if (proxyAgent) + this._serverEncrypted = await proxyAgent.connect(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false }); + else this._serverEncrypted = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port); - } this._serverEncrypted.once('close', this._serverCloseEventListener); this._serverEncrypted.once('error', error => this._browserEncrypted.destroy(error)); diff --git a/packages/playwright-core/src/server/utils/network.ts b/packages/playwright-core/src/server/utils/network.ts index a10bcc7ea..2fec2ea89 100644 --- a/packages/playwright-core/src/server/utils/network.ts +++ b/packages/playwright-core/src/server/utils/network.ts @@ -149,10 +149,17 @@ export function createProxyAgent(proxy?: ProxySettings, forUrl?: URL) { const proxyOpts = url.parse(proxyServer); if (proxyOpts.protocol?.startsWith('socks')) { - return new SocksProxyAgent({ - host: proxyOpts.hostname, - port: proxyOpts.port || undefined, - }); + const socksProxyURL = new URL(proxyServer); + + // SocksProxyAgent distinguishes between socks5 and socks5h. + // socks5h is what we want, it means that hostnames are resolved by the proxy. + // browsers behave the same way, even if socks5 is specified. + if (proxyOpts.protocol === 'socks5:') + socksProxyURL.protocol = 'socks5h:'; + else if (proxyOpts.protocol === 'socks4:') + socksProxyURL.protocol = 'socks4a:'; + + return new SocksProxyAgent(socksProxyURL); } if (proxy.username) proxyOpts.auth = `${proxy.username}:${proxy.password || ''}`; From 952b643e03526006d845fe64a01f2cb5b4435353 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 7 Oct 2025 11:18:29 -0700 Subject: [PATCH 030/250] chore(mcp): minimal vscode version notice (#37755) --- packages/playwright/src/agents/generateAgents.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/agents/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts index 7fe10b761..015cc749f 100644 --- a/packages/playwright/src/agents/generateAgents.ts +++ b/packages/playwright/src/agents/generateAgents.ts @@ -16,7 +16,7 @@ import fs from 'fs'; import path from 'path'; -import { yaml } from 'playwright-core/lib/utilsBundle'; +import { colors, yaml } from 'playwright-core/lib/utilsBundle'; interface AgentHeader { name: string; @@ -280,6 +280,8 @@ export async function initVSCodeRepo() { cwd: '${workspaceFolder}', }; await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2)); + // eslint-disable-next-line no-console + console.log(colors.yellow(`${colors.bold('Note:')} Playwright Agents require VSCode version 1.105+ or VSCode Insiders`)); } export async function initOpencodeRepo() { From ef257ecffde63896f12a195665967cb635828a7a Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 7 Oct 2025 12:20:34 -0700 Subject: [PATCH 031/250] chore(mcp): fallback to cwd when resolving test config (#37757) --- .../playwright/src/mcp/test/testBackend.ts | 2 +- tests/mcp/fixtures.ts | 18 +++++++++++------- tests/mcp/test-list.spec.ts | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/playwright/src/mcp/test/testBackend.ts b/packages/playwright/src/mcp/test/testBackend.ts index 3338c08b9..8ac54f001 100644 --- a/packages/playwright/src/mcp/test/testBackend.ts +++ b/packages/playwright/src/mcp/test/testBackend.ts @@ -58,7 +58,7 @@ export class TestServerBackend implements mcp.ServerBackend { return; } - throw new Error('No config option or MCP root path provided'); + this._context.initialize(rootPath, resolveConfigLocation(undefined)); } async listTools(): Promise { diff --git a/tests/mcp/fixtures.ts b/tests/mcp/fixtures.ts index 28e296e32..9f55b0c64 100644 --- a/tests/mcp/fixtures.ts +++ b/tests/mcp/fixtures.ts @@ -48,6 +48,8 @@ type CDPServer = { export type StartClient = (options?: { clientName?: string, args?: string[], + omitArgs?: string[], + cwd?: string, config?: Config, roots?: { name: string, uri: string }[], rootsResponseDelay?: number, @@ -84,14 +86,14 @@ export const test = serverTest.extend { - const args: string[] = mcpArgs ?? []; + let args: string[] = mcpArgs ?? []; if (mcpHeadless) args.push('--headless'); if (mcpServerType === 'test-mcp') { if (!options?.args?.some(arg => arg.startsWith('--config'))) - args.push('--config', test.info().outputPath()); + args.push(`--config=${test.info().outputPath()}`); } else { if (process.env.CI && process.platform === 'linux') args.push('--no-sandbox'); @@ -106,6 +108,8 @@ export const test = serverTest.extend !options.omitArgs?.includes(arg)); const client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined); if (options?.roots) { @@ -118,7 +122,7 @@ export const test = serverTest.extend { if (process.env.PWDEBUGIMPL) @@ -182,18 +186,18 @@ export const test = serverTest.extend { const profilesDir = test.info().outputPath('ms-playwright'); const transport = new StdioClientTransport({ command: 'node', - args: [...(mcpServerType === 'test-mcp' ? testMcpServerPath : mcpServerPath), ...args], - cwd: test.info().outputPath(), + args: [...(mcpServerType === 'test-mcp' ? testMcpServerPath : mcpServerPath), ...options.args], + cwd: options.cwd, stderr: 'pipe', env: { - ...env, + ...options.env, DEBUG_COLORS: '0', DEBUG_HIDE_DATE: '1', PWMCP_PROFILES_DIR_FOR_TEST: profilesDir, diff --git a/tests/mcp/test-list.spec.ts b/tests/mcp/test-list.spec.ts index af94d470b..561e9bea8 100644 --- a/tests/mcp/test-list.spec.ts +++ b/tests/mcp/test-list.spec.ts @@ -45,3 +45,21 @@ test('test_list', async ({ startClient }) => { [id=] a.test.ts:6:11 › example2 Total: 4 tests in 1 file`); }); + +test('test_list config in cwd', async ({ startClient }) => { + await writeFiles({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + `, + }); + + const { client } = await startClient({ + omitArgs: ['--config'], + }); + expect(await client.callTool({ + name: 'test_list', + })).toHaveTextResponse(`Listing tests: + [id=] a.test.ts:3:11 › passes +Total: 1 test in 1 file`); +}); From 4997cafbdf77c765a19666f301c4d22933a56490 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 7 Oct 2025 12:57:42 -0700 Subject: [PATCH 032/250] docs: mention VS Code insiders in the agents docs (#37758) --- docs/src/test-agents-js.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/test-agents-js.md b/docs/src/test-agents-js.md index 254afc83b..28e65b361 100644 --- a/docs/src/test-agents-js.md +++ b/docs/src/test-agents-js.md @@ -43,6 +43,10 @@ npx playwright init-agents --loop=claude npx playwright init-agents --loop=opencode ``` +:::note +VS Code v1.105 (currently on the [VS Code Insiders channel](https://code.visualstudio.com/insiders/)) is needed for the agentic experience in VS Code. It will become stable shortly, we are a bit ahead of the curve with this functionality! +::: + Once the agents have been generated, you can use your AI tool of choice to command these agents to build Playwright Tests. From e4679234a3bcac8782e1a7f371e09165dd64a9ed Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 7 Oct 2025 13:23:37 -0700 Subject: [PATCH 033/250] chore: rename agents to test agents (#37759) --- docs/src/release-notes-js.md | 8 ++++---- docs/src/test-agents-js.md | 8 ++++---- packages/playwright/src/agents/generateAgents.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index 39f494d2b..7c05fe471 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -10,12 +10,12 @@ import LiteYouTube from '@site/src/components/LiteYouTube'; -### Playwright Agents +### Playwright Test Agents -Introducing Playwright Agents, three custom agent definitions designed to guide LLMs through the core process of building a Playwright test: +Introducing Playwright Test Agents, three custom agent definitions designed to guide LLMs through the core process of building a Playwright test: * **🎭 planner** explores the app and produces a Markdown test plan @@ -35,7 +35,7 @@ npx playwright init-agents --loop=claude npx playwright init-agents --loop=opencode ``` -[Learn more about Playwright Agents](./test-agents.md) +[Learn more about Playwright Test Agents](./test-agents.md) ### New APIs diff --git a/docs/src/test-agents-js.md b/docs/src/test-agents-js.md index 28e65b361..e19faad56 100644 --- a/docs/src/test-agents-js.md +++ b/docs/src/test-agents-js.md @@ -5,11 +5,11 @@ title: "Agents" import LiteYouTube from '@site/src/components/LiteYouTube'; -# Playwright Agents +# Playwright Test Agents ## Introduction -Playwright comes with three Playwright Agents out of the box: **🎭 planner**, **🎭 generator** and **🎭 healer**. +Playwright comes with three Playwright Test Agents out of the box: **🎭 planner**, **🎭 generator** and **🎭 healer**. These agents can be used independently, sequentially, or as the chained calls in the agentic loop. Using them sequentially will produce test coverage for your product. @@ -22,12 +22,12 @@ Using them sequentially will produce test coverage for your product. ### Getting Started -Start with adding Playwright Agent definitions to your project using +Start with adding Playwright Test Agent definitions to your project using the `init-agents` command. These definitions should be regenerated whenever Playwright is updated to pick up new tools and instructions. diff --git a/packages/playwright/src/agents/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts index 015cc749f..efa0ed080 100644 --- a/packages/playwright/src/agents/generateAgents.ts +++ b/packages/playwright/src/agents/generateAgents.ts @@ -281,7 +281,7 @@ export async function initVSCodeRepo() { }; await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2)); // eslint-disable-next-line no-console - console.log(colors.yellow(`${colors.bold('Note:')} Playwright Agents require VSCode version 1.105+ or VSCode Insiders`)); + console.log(colors.yellow(`${colors.bold('Note:')} Playwright Test Agents require VSCode version 1.105+ or VSCode Insiders`)); } export async function initOpencodeRepo() { From 47f3da37a27217f16c36669261cb214864413209 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 7 Oct 2025 14:35:08 -0700 Subject: [PATCH 034/250] feat(mcp): support test-id-attribute option (#37760) --- packages/playwright/src/mcp/browser/config.ts | 3 ++ .../playwright/src/mcp/browser/context.ts | 4 ++ packages/playwright/src/mcp/config.d.ts | 5 +++ packages/playwright/src/mcp/program.ts | 1 + tests/mcp/click.spec.ts | 27 ++++++++++++ tests/mcp/generator.spec.ts | 41 +++++++++++++++++++ 6 files changed, 81 insertions(+) diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index 5ffe2f8d3..d79cd236d 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -59,6 +59,7 @@ export type CLIOptions = { secrets?: Record; sharedBrowserContext?: boolean; storageState?: string; + testIdAttribute?: string; timeoutAction?: number; timeoutNavigation?: number; userAgent?: string; @@ -236,6 +237,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { sharedBrowserContext: cliOptions.sharedBrowserContext, outputDir: cliOptions.outputDir, imageResponses: cliOptions.imageResponses, + testIdAttribute: cliOptions.testIdAttribute, timeouts: { action: cliOptions.timeoutAction, navigation: cliOptions.timeoutNavigation, @@ -277,6 +279,7 @@ function configFromEnv(): Config { 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.testIdAttribute = envToString(process.env.PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE); 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); diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts index 015a0da51..657194f8e 100644 --- a/packages/playwright/src/mcp/browser/context.ts +++ b/packages/playwright/src/mcp/browser/context.ts @@ -18,6 +18,7 @@ import fs from 'fs'; import path from 'path'; import { debug } from 'playwright-core/lib/utilsBundle'; +import { selectors } from 'playwright-core'; import { logUnhandledError } from '../log'; import { Tab } from './tab'; @@ -233,6 +234,9 @@ export class Context { 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. + + if (this.config.testIdAttribute) + selectors.setTestIdAttribute(this.config.testIdAttribute); const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName); const { browserContext } = result; await this._setupRequestInterception(browserContext); diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index dd373b988..7a43731f3 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -149,6 +149,11 @@ export type Config = { blockedOrigins?: string[]; }; + /** + * Specify the attribute to use for test ids, defaults to "data-testid". + */ + testIdAttribute?: string; + timeouts?: { /* * Configures default action timeout: https://playwright.dev/docs/api/class-page#page-set-default-timeout. Defaults to 5000ms. diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index 7f7c2cf06..8f979cc3d 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -58,6 +58,7 @@ export function decorateCommand(command: Command, version: string) { .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('--test-id-attribute ', 'specify the attribute to use for test ids, defaults to "data-testid"') .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') diff --git a/tests/mcp/click.spec.ts b/tests/mcp/click.spec.ts index 6f6a06dbd..0335ccf8c 100644 --- a/tests/mcp/click.spec.ts +++ b/tests/mcp/click.spec.ts @@ -153,3 +153,30 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => { pageState: expect.stringContaining(`- generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:true`), }); }); + +test('browser_click (test id attribute)', async ({ startClient, server, mcpBrowser }) => { + server.setContent('/', ` + Title + + `, 'text/html'); + + const { client } = await startClient({ + args: [ + '--test-id-attribute', 'data-tid', + ], + }); + 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.getByTestId('submit').click();`, + }); +}); diff --git a/tests/mcp/generator.spec.ts b/tests/mcp/generator.spec.ts index 8f7ffec24..d0feace8e 100644 --- a/tests/mcp/generator.spec.ts +++ b/tests/mcp/generator.spec.ts @@ -276,3 +276,44 @@ test('generator_write_test', async ({ startClient }, testInfo) => { const code = fs.readFileSync(testInfo.outputPath('a.test.ts'), 'utf8'); expect(code).toBe(`// Test content`); }); + +test('should respect custom test id', async ({ startClient }) => { + await writeFiles({ + 'playwright.config.ts': ` + import { defineConfig } from '@playwright/test'; + export default defineConfig({ + use: { + testIdAttribute: 'data-tid' + } + }); + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test.beforeEach(async ({ page }) => { + await page.setContent(''); + }); + test('template', async ({ page }) => { + }); + `, + }); + + const { client } = await startClient(); + await client.callTool({ + name: 'generator_setup_page', + arguments: { + plan: 'Test plan', + seedFile: 'a.test.ts', + }, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Submit button', + ref: 'e2', + intent: 'Click submit button', + }, + })).toHaveResponse({ + code: `await page.getByTestId('submit').click();`, + }); +}); From 2c0c709f60ef9dd12894fa487c84eb44345ebc2c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 7 Oct 2025 15:51:08 -0700 Subject: [PATCH 035/250] chore: simplify trace sw (#37706) --- docs/src/test-cli-js.md | 5 +- packages/playwright-core/src/cli/program.ts | 9 +- .../src/server/trace/viewer/traceViewer.ts | 36 +++--- packages/playwright/src/runner/testServer.ts | 2 +- packages/trace-viewer/src/sw/main.ts | 32 ++--- packages/trace-viewer/src/sw/traceModel.ts | 2 - packages/trace-viewer/src/types/entries.ts | 1 - .../src/ui/liveWorkbenchLoader.tsx | 5 +- packages/trace-viewer/src/ui/modelUtil.ts | 42 +----- packages/trace-viewer/src/ui/snapshotTab.tsx | 14 +- .../trace-viewer/src/ui/uiModeTraceView.tsx | 5 +- .../trace-viewer/src/ui/workbenchLoader.tsx | 102 +++++++-------- tests/config/traceViewerFixtures.ts | 8 +- tests/config/utils.ts | 2 +- tests/library/trace-viewer.spec.ts | 122 +++++------------- 15 files changed, 142 insertions(+), 245 deletions(-) diff --git a/docs/src/test-cli-js.md b/docs/src/test-cli-js.md index d45ee1d6a..de9ff5ff3 100644 --- a/docs/src/test-cli-js.md +++ b/docs/src/test-cli-js.md @@ -254,12 +254,15 @@ Analyze and view test traces for debugging. [Read more about Trace Viewer](./tra #### Syntax ```bash -npx playwright show-trace [options] +npx playwright show-trace [options] [trace] ``` #### Examples ```bash +# Open trace viewer without a specific trace (can load traces via UI) +npx playwright show-trace + # View a trace file npx playwright show-trace trace.zip diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 5dc18774b..9056896f8 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -375,13 +375,13 @@ program }); program - .command('show-trace [trace...]') + .command('show-trace [trace]') .option('-b, --browser ', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium') .option('-h, --host ', 'Host to serve trace on; specifying this option opens trace in a browser tab') .option('-p, --port ', 'Port to serve trace on, 0 for any free port; specifying this option opens trace in a browser tab') .option('--stdin', 'Accept trace URLs over stdin to update the viewer') .description('show trace viewer') - .action(function(traces, options) { + .action(function(trace, options) { if (options.browser === 'cr') options.browser = 'chromium'; if (options.browser === 'ff') @@ -396,12 +396,13 @@ program }; if (options.port !== undefined || options.host !== undefined) - runTraceInBrowser(traces, openOptions).catch(logErrorAndExit); + runTraceInBrowser(trace, openOptions).catch(logErrorAndExit); else - runTraceViewerApp(traces, options.browser, openOptions, true).catch(logErrorAndExit); + runTraceViewerApp(trace, options.browser, openOptions, true).catch(logErrorAndExit); }).addHelpText('afterAll', ` Examples: + $ show-trace $ show-trace https://example.com/trace.zip`); type Options = { diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 3c367e4ad..48a504209 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -52,16 +52,16 @@ export type TraceViewerAppOptions = { persistentContextOptions?: Parameters[2]; }; -function validateTraceUrls(traceUrls: string[]) { - for (const traceUrl of traceUrls) { - let traceFile = traceUrl; - // If .json is requested, we'll synthesize it. - if (traceUrl.endsWith('.json')) - traceFile = traceUrl.substring(0, traceUrl.length - '.json'.length); - - if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceFile) && !fs.existsSync(traceFile + '.trace')) - throw new Error(`Trace file ${traceUrl} does not exist!`); - } +function validateTraceUrl(traceUrl: string | undefined) { + if (!traceUrl) + return; + let traceFile = traceUrl; + // If .json is requested, we'll synthesize it. + if (traceUrl.endsWith('.json')) + traceFile = traceUrl.substring(0, traceUrl.length - '.json'.length); + + if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceFile) && !fs.existsSync(traceFile + '.trace')) + throw new Error(`Trace file ${traceUrl} does not exist!`); } export async function startTraceViewerServer(options?: TraceViewerServerOptions): Promise { @@ -108,11 +108,11 @@ export async function startTraceViewerServer(options?: TraceViewerServerOptions) return server; } -export async function installRootRedirect(server: HttpServer, traceUrls: string[], options: TraceViewerRedirectOptions) { +export async function installRootRedirect(server: HttpServer, traceUrl: string | undefined, options: TraceViewerRedirectOptions) { const params = new URLSearchParams(); if (path.sep !== path.posix.sep) params.set('pathSeparator', path.sep); - for (const traceUrl of traceUrls) + if (traceUrl) params.append('trace', traceUrl); if (server.wsGuid()) params.append('ws', server.wsGuid()!); @@ -146,20 +146,20 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[ }); } -export async function runTraceViewerApp(traceUrls: string[], browserName: string, options: TraceViewerServerOptions & { headless?: boolean }, exitOnClose?: boolean) { - validateTraceUrls(traceUrls); +export async function runTraceViewerApp(traceUrl: string | undefined, browserName: string, options: TraceViewerServerOptions & { headless?: boolean }, exitOnClose?: boolean) { + validateTraceUrl(traceUrl); const server = await startTraceViewerServer(options); - await installRootRedirect(server, traceUrls, options); + await installRootRedirect(server, traceUrl, options); const page = await openTraceViewerApp(server.urlPrefix('precise'), browserName, options); if (exitOnClose) page.on('close', () => gracefullyProcessExitDoNotHang(0)); return page; } -export async function runTraceInBrowser(traceUrls: string[], options: TraceViewerServerOptions) { - validateTraceUrls(traceUrls); +export async function runTraceInBrowser(traceUrl: string | undefined, options: TraceViewerServerOptions) { + validateTraceUrl(traceUrl); const server = await startTraceViewerServer(options); - await installRootRedirect(server, traceUrls, options); + await installRootRedirect(server, traceUrl, options); await openTraceInBrowser(server.urlPrefix('human-readable')); } diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 08fc4415e..733956ed6 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -262,7 +262,7 @@ export class TestServerDispatcher implements TestServerInterface { export async function runUIMode(configFile: string | undefined, configCLIOverrides: ConfigCLIOverrides, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise { const configLocation = resolveConfigLocation(configFile); return await innerRunTestServer(configLocation, configCLIOverrides, options, async (server: HttpServer, cancelPromise: ManualPromise) => { - await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html' }); + await installRootRedirect(server, undefined, { ...options, webApp: 'uiMode.html' }); if (options.host !== undefined || options.port !== undefined) { await openTraceInBrowser(server.urlPrefix('human-readable')); } else { diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index cdce3261f..64d22a426 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -36,19 +36,19 @@ const scopePath = new URL(self.registration.scope).pathname; const loadedTraces = new Map(); -const clientIdToTraceUrls = new Map, traceViewerServer: TraceViewerServer }>(); +type ClientData = { + traceUrl: string; + traceViewerServer: TraceViewerServer +}; +const clientIdToTraceUrls = new Map(); -async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, limit: number | undefined, progress: (done: number, total: number) => undefined): Promise { - await gc(); +async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, progress: (done: number, total: number) => undefined): Promise { const clientId = client?.id ?? ''; - let data = clientIdToTraceUrls.get(clientId); - if (!data) { - const clientURL = new URL(client?.url ?? self.registration.scope); - const traceViewerServerBaseUrl = new URL(clientURL.searchParams.get('server') ?? '../', clientURL); - data = { limit, traceUrls: new Set(), traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) }; - clientIdToTraceUrls.set(clientId, data); - } - data.traceUrls.add(traceUrl); + const clientURL = new URL(client?.url ?? self.registration.scope); + const traceViewerServerBaseUrl = new URL(clientURL.searchParams.get('server') ?? '../', clientURL); + const data: ClientData = { traceUrl, traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) }; + clientIdToTraceUrls.set(clientId, data); + await gc(); const traceModel = new TraceModel(); try { @@ -106,8 +106,7 @@ async function doFetch(event: FetchEvent): Promise { if (relativePath === '/contexts') { try { - const limit = url.searchParams.has('limit') ? +url.searchParams.get('limit')! : undefined; - const traceModel = await loadTrace(traceUrl!, url.searchParams.get('traceFileName'), client, limit, (done: number, total: number) => { + const traceModel = await loadTrace(traceUrl!, url.searchParams.get('traceFileName'), client, (done: number, total: number) => { client.postMessage({ method: 'progress', params: { done, total } }); }); return new Response(JSON.stringify(traceModel!.contextEntries), { @@ -209,12 +208,7 @@ async function gc() { clientIdToTraceUrls.delete(clientId); continue; } - if (data.limit !== undefined) { - const ordered = [...data.traceUrls]; - // Leave the newest requested traces. - data.traceUrls = new Set(ordered.slice(ordered.length - data.limit)); - } - data.traceUrls.forEach(url => usedTraces.add(url)); + usedTraces.add(data.traceUrl); } for (const traceUrl of loadedTraces.keys()) { diff --git a/packages/trace-viewer/src/sw/traceModel.ts b/packages/trace-viewer/src/sw/traceModel.ts index dc18e4e6c..e86129c77 100644 --- a/packages/trace-viewer/src/sw/traceModel.ts +++ b/packages/trace-viewer/src/sw/traceModel.ts @@ -61,7 +61,6 @@ export class TraceModel { let done = 0; for (const ordinal of ordinals) { const contextEntry = createEmptyContext(); - contextEntry.traceUrl = backend.traceURL(); contextEntry.hasSource = hasSource; const modernizer = new TraceModernizer(contextEntry, this._snapshotStorage); @@ -138,7 +137,6 @@ function stripEncodingFromContentType(contentType: string) { function createEmptyContext(): ContextEntry { return { origin: 'testRunner', - traceUrl: '', startTime: Number.MAX_SAFE_INTEGER, wallTime: Number.MAX_SAFE_INTEGER, endTime: 0, diff --git a/packages/trace-viewer/src/types/entries.ts b/packages/trace-viewer/src/types/entries.ts index a28efe402..7d36030e6 100644 --- a/packages/trace-viewer/src/types/entries.ts +++ b/packages/trace-viewer/src/types/entries.ts @@ -22,7 +22,6 @@ import type * as trace from '@trace/trace'; export type ContextEntry = { origin: 'testRunner'|'library'; - traceUrl: string; startTime: number; endTime: number; browserName: string; diff --git a/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx b/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx index ba44a66c3..608ece9fc 100644 --- a/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx @@ -36,7 +36,7 @@ export const LiveWorkbenchLoader: React.FC<{ traceJson: string }> = ({ traceJson const model = await loadSingleTraceFile(traceJson); setModel(model); } catch { - const model = new MultiTraceModel([]); + const model = new MultiTraceModel('', []); setModel(model); } finally { setCounter(counter + 1); @@ -54,8 +54,7 @@ export const LiveWorkbenchLoader: React.FC<{ traceJson: string }> = ({ traceJson async function loadSingleTraceFile(traceJson: string): Promise { const params = new URLSearchParams(); params.set('trace', traceJson); - params.set('limit', '1'); const response = await fetch(`contexts?${params.toString()}`); const contextEntries = await response.json() as ContextEntry[]; - return new MultiTraceModel(contextEntries); + return new MultiTraceModel(traceJson, contextEntries); } diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index 5f8c3da6e..4db70cd4c 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -85,12 +85,14 @@ export class MultiTraceModel { readonly sources: Map; resources: ResourceSnapshot[]; readonly actionCounters: Map; + readonly traceUrl: string; - constructor(contexts: ContextEntry[]) { + constructor(traceUrl: string, contexts: ContextEntry[]) { contexts.forEach(contextEntry => indexModel(contextEntry)); const libraryContext = contexts.find(context => context.origin === 'library'); + this.traceUrl = traceUrl; this.browserName = libraryContext?.browserName || ''; this.sdkLanguage = libraryContext?.sdkLanguage; this.channel = libraryContext?.channel; @@ -110,7 +112,7 @@ export class MultiTraceModel { this.hasSource = contexts.some(c => c.hasSource); this.hasStepData = contexts.some(context => context.origin === 'testRunner'); this.resources = [...contexts.map(c => c.resources)].flat(); - this.attachments = this.actions.flatMap(action => action.attachments?.map(attachment => ({ ...attachment, traceUrl: action.context.traceUrl })) ?? []); + this.attachments = this.actions.flatMap(action => action.attachments?.map(attachment => ({ ...attachment, traceUrl })) ?? []); this.visibleAttachments = this.attachments.filter(attachment => !attachment.name.startsWith('_')); this.events.sort((a1, a2) => a1.time - a2.time); @@ -179,30 +181,9 @@ function indexModel(context: ContextEntry) { } function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) { - const traceFileToContexts = new Map(); - for (const context of contexts) { - const traceFile = context.traceUrl; - let list = traceFileToContexts.get(traceFile); - if (!list) { - list = []; - traceFileToContexts.set(traceFile, list); - } - list.push(context); - } - const result: ActionTraceEventInContext[] = []; - let traceFileId = 0; - for (const [, contexts] of traceFileToContexts) { - // Action ids are unique only within a trace file. If there are - // traces from more than one file we make the ids unique across the - // files. The code does not update snapshot ids as they are always - // retrieved from a particular trace file. - if (traceFileToContexts.size > 1) - makeCallIdsUniqueAcrossTraceFiles(contexts, ++traceFileId); - // Align action times across runner and library contexts within each trace file. - const actions = mergeActionsAndUpdateTimingSameTrace(contexts); - result.push(...actions); - } + const actions = mergeActionsAndUpdateTimingSameTrace(contexts); + result.push(...actions); result.sort((a1, a2) => { if (a2.parentId === a1.callId) @@ -229,17 +210,6 @@ function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) { return result; } -function makeCallIdsUniqueAcrossTraceFiles(contexts: ContextEntry[], traceFileId: number) { - for (const context of contexts) { - for (const action of context.actions) { - if (action.callId) - action.callId = `${traceFileId}:${action.callId}`; - if (action.parentId) - action.parentId = `${traceFileId}:${action.parentId}`; - } - } -} - let lastTmpStepId = 0; function mergeActionsAndUpdateTimingSameTrace(contexts: ContextEntry[]): ActionTraceEventInContext[] { diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 2839d9877..041a4e988 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -17,7 +17,7 @@ import './snapshotTab.css'; import * as React from 'react'; import type { ActionTraceEvent } from '@trace/trace'; -import { context, type MultiTraceModel, nextActionByStartTime, previousActionByEndTime } from './modelUtil'; +import { type MultiTraceModel, nextActionByStartTime, previousActionByEndTime } from './modelUtil'; import { Toolbar } from '@web/components/toolbar'; import { ToolbarButton } from '@web/components/toolbarButton'; import { clsx, useMeasure, useSetting } from '@web/uiUtils'; @@ -47,7 +47,7 @@ export const SnapshotTabsView: React.FunctionComponent<{ setIsInspecting: (isInspecting: boolean) => void, highlightedElement: HighlightedElement, setHighlightedElement: (element: HighlightedElement) => void, -}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedElement, setHighlightedElement }) => { +}> = ({ action, model, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedElement, setHighlightedElement }) => { const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action'); const [shouldPopulateCanvasFromScreenshot] = useSetting('shouldPopulateCanvasFromScreenshot', false); @@ -57,8 +57,8 @@ export const SnapshotTabsView: React.FunctionComponent<{ }, [action]); const { snapshotInfoUrl, snapshotUrl, popoutUrl } = React.useMemo(() => { const snapshot = snapshots[snapshotTab]; - return snapshot ? extendSnapshot(snapshot, shouldPopulateCanvasFromScreenshot) : { snapshotInfoUrl: undefined, snapshotUrl: undefined, popoutUrl: undefined }; - }, [snapshots, snapshotTab, shouldPopulateCanvasFromScreenshot]); + return model && snapshot ? extendSnapshot(model.traceUrl, snapshot, shouldPopulateCanvasFromScreenshot) : { snapshotInfoUrl: undefined, snapshotUrl: undefined, popoutUrl: undefined }; + }, [snapshots, snapshotTab, shouldPopulateCanvasFromScreenshot, model]); const snapshotUrls = React.useMemo((): SnapshotUrls | undefined => snapshotInfoUrl !== undefined ? { snapshotInfoUrl, snapshotUrl, popoutUrl } : undefined, [snapshotInfoUrl, snapshotUrl, popoutUrl]); @@ -414,9 +414,9 @@ export function collectSnapshots(action: ActionTraceEvent | undefined): Snapshot const isUnderTest = new URLSearchParams(window.location.search).has('isUnderTest'); const serverParam = new URLSearchParams(window.location.search).get('server'); -export function extendSnapshot(snapshot: Snapshot, shouldPopulateCanvasFromScreenshot: boolean): SnapshotUrls { +export function extendSnapshot(traceUrl: string, snapshot: Snapshot, shouldPopulateCanvasFromScreenshot: boolean): SnapshotUrls { const params = new URLSearchParams(); - params.set('trace', context(snapshot.action).traceUrl); + params.set('trace', traceUrl); params.set('name', snapshot.snapshotName); if (isUnderTest) params.set('isUnderTest', 'true'); @@ -436,7 +436,7 @@ export function extendSnapshot(snapshot: Snapshot, shouldPopulateCanvasFromScree popoutParams.set('r', snapshotUrl); if (serverParam) popoutParams.set('server', serverParam); - popoutParams.set('trace', context(snapshot.action).traceUrl); + popoutParams.set('trace', traceUrl); if (snapshot.point) { popoutParams.set('pointX', String(snapshot.point.x)); popoutParams.set('pointY', String(snapshot.point.y)); diff --git a/packages/trace-viewer/src/ui/uiModeTraceView.tsx b/packages/trace-viewer/src/ui/uiModeTraceView.tsx index 7c4a964a5..382e6e669 100644 --- a/packages/trace-viewer/src/ui/uiModeTraceView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTraceView.tsx @@ -75,7 +75,7 @@ export const TraceView: React.FC<{ const model = await loadSingleTraceFile(traceLocation); setModel({ model, isLive: true }); } catch { - const model = new MultiTraceModel([]); + const model = new MultiTraceModel('', []); model.errorDescriptors.push(...result.errors.flatMap(error => !!error.message ? [{ message: error.message }] : [])); setModel({ model, isLive: false }); } finally { @@ -113,8 +113,7 @@ const outputDirForTestCase = (testCase: reporterTypes.TestCase): string | undefi async function loadSingleTraceFile(url: string): Promise { const params = new URLSearchParams(); params.set('trace', url); - params.set('limit', '1'); const response = await fetch(`contexts?${params.toString()}`); const contextEntries = await response.json() as ContextEntry[]; - return new MultiTraceModel(contextEntries); + return new MultiTraceModel(url, contextEntries); } diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx index dec7ae059..9def0cfdb 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -15,7 +15,6 @@ */ import * as React from 'react'; -import type { ContextEntry } from '../types/entries'; import { MultiTraceModel } from './modelUtil'; import './workbenchLoader.css'; import { Workbench } from './workbench'; @@ -27,8 +26,8 @@ import { DefaultSettingsView } from './defaultSettingsView'; export const WorkbenchLoader: React.FunctionComponent<{ }> = () => { const [isServer, setIsServer] = React.useState(false); - const [traceURLs, setTraceURLs] = React.useState([]); - const [uploadedTraceNames, setUploadedTraceNames] = React.useState([]); + const [traceURL, setTraceURL] = React.useState(); + const [uploadedTraceName, setUploadedTraceName] = React.useState(); const [model, setModel] = React.useState(emptyModel); const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 }); const [dragOver, setDragOver] = React.useState(false); @@ -37,25 +36,19 @@ export const WorkbenchLoader: React.FunctionComponent<{ const [showProgressDialog, setShowProgressDialog] = React.useState(false); const processTraceFiles = React.useCallback((files: FileList) => { - const blobUrls = []; - const fileNames = []; const url = new URL(window.location.href); - for (let i = 0; i < files.length; i++) { - const file = files.item(i); - if (!file) - continue; - const blobTraceURL = URL.createObjectURL(file); - blobUrls.push(blobTraceURL); - fileNames.push(file.name); - url.searchParams.append('trace', blobTraceURL); - url.searchParams.append('traceFileName', file.name); - } + if (!files.length) + return; + const file = files.item(0)!; + const blobTraceURL = URL.createObjectURL(file); + url.searchParams.append('trace', blobTraceURL); + url.searchParams.append('traceFileName', file.name); const href = url.toString(); // Snapshot loaders will inherit the trace url from the query parameters, // so set it here. window.history.pushState({}, '', href); - setTraceURLs(blobUrls); - setUploadedTraceNames(fileNames); + setTraceURL(blobTraceURL); + setUploadedTraceName(file.name); setDragOver(false); setProcessingErrorMessage(null); }, []); @@ -106,15 +99,13 @@ export const WorkbenchLoader: React.FunctionComponent<{ React.useEffect(() => { const params = new URL(window.location.href).searchParams; - const newTraceURLs = params.getAll('trace'); + const url = params.get('trace'); setIsServer(params.has('isServer')); // Don't accept file:// URLs - this means we re opened locally. - for (const url of newTraceURLs) { - if (url.startsWith('file:')) { - setFileForLocalModeError(url || null); - return; - } + if (url?.startsWith('file:')) { + setFileForLocalModeError(url || null); + return; } if (params.has('isServer')) { @@ -123,52 +114,52 @@ export const WorkbenchLoader: React.FunctionComponent<{ wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:'); const testServerConnection = new TestServerConnection(new WebSocketTestServerTransport(wsURL)); testServerConnection.onLoadTraceRequested(async params => { - setTraceURLs(params.traceUrl ? [params.traceUrl] : []); + setTraceURL(params.traceUrl); setDragOver(false); setProcessingErrorMessage(null); }); testServerConnection.initialize({}).catch(() => {}); - } else if (!newTraceURLs.some(url => url.startsWith('blob:'))) { + } else if (url && !url.startsWith('blob:')) { // Don't re-use blob file URLs on page load (results in Fetch error) - setTraceURLs(newTraceURLs); + setTraceURL(url); } }, []); React.useEffect(() => { (async () => { - if (traceURLs.length) { - const swListener = (event: any) => { - if (event.data.method === 'progress') - setProgress(event.data.params); - }; + if (!traceURL) { + setModel(emptyModel); + return; + } + + const swListener = (event: any) => { + if (event.data.method === 'progress') + setProgress(event.data.params); + }; + try { navigator.serviceWorker.addEventListener('message', swListener); setProgress({ done: 0, total: 1 }); - const contextEntries: ContextEntry[] = []; - for (let i = 0; i < traceURLs.length; i++) { - const url = traceURLs[i]; - const params = new URLSearchParams(); - params.set('trace', url); - if (uploadedTraceNames.length) - params.set('traceFileName', uploadedTraceNames[i]); - params.set('limit', String(traceURLs.length)); - const response = await fetch(`contexts?${params.toString()}`); - if (!response.ok) { - if (!isServer) - setTraceURLs([]); - setProcessingErrorMessage((await response.json()).error); - return; - } - contextEntries.push(...(await response.json())); + + const params = new URLSearchParams(); + params.set('trace', traceURL); + if (uploadedTraceName) + params.set('traceFileName', uploadedTraceName); + const response = await fetch(`contexts?${params.toString()}`); + if (!response.ok) { + if (!isServer) + setTraceURL(undefined); + setProcessingErrorMessage((await response.json()).error); + return; } - navigator.serviceWorker.removeEventListener('message', swListener); - const model = new MultiTraceModel(contextEntries); + const contextEntries = await response.json(); + const model = new MultiTraceModel(traceURL, contextEntries); setProgress({ done: 0, total: 0 }); setModel(model); - } else { - setModel(emptyModel); + } finally { + navigator.serviceWorker.removeEventListener('message', swListener); } })(); - }, [isServer, traceURLs, uploadedTraceNames]); + }, [isServer, traceURL, uploadedTraceName]); const showLoading = progress.done !== progress.total && progress.total !== 0 && !processingErrorMessage; @@ -184,7 +175,7 @@ export const WorkbenchLoader: React.FunctionComponent<{ } }, [showLoading]); - const showFileUploadDropArea = !!(!isServer && !dragOver && !fileForLocalModeError && (!traceURLs.length || processingErrorMessage)); + const showFileUploadDropArea = !!(!isServer && !dragOver && !fileForLocalModeError && (!traceURL || processingErrorMessage)); return
{ event.preventDefault(); setDragOver(true); }}>
@@ -222,14 +213,13 @@ export const WorkbenchLoader: React.FunctionComponent<{
Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere, it opens it locally.
} - {isServer && !traceURLs.length &&
+ {isServer && !traceURL &&
Select test to see the trace
} {dragOver &&
; }; -export const emptyModel = new MultiTraceModel([]); +export const emptyModel = new MultiTraceModel('', []); diff --git a/tests/config/traceViewerFixtures.ts b/tests/config/traceViewerFixtures.ts index 9dedc6c57..7f07b9c9c 100644 --- a/tests/config/traceViewerFixtures.ts +++ b/tests/config/traceViewerFixtures.ts @@ -30,7 +30,7 @@ type BaseWorkerFixtures = { }; export type TraceViewerFixtures = { - showTraceViewer: (trace: string[], options?: {host?: string, port?: number}) => Promise; + showTraceViewer: (trace: string | undefined, options?: {host?: string, port?: number}) => Promise; runAndTrace: (body: () => Promise, optsOverrides?: Parameters[0]) => Promise; }; @@ -153,8 +153,8 @@ export const traceViewerFixtures: Fixtures { const browsers: Browser[] = []; const contextImpls: any[] = []; - await use(async (traces: string[], { host, port } = {}) => { - const pageImpl = await runTraceViewerApp(traces, browserName, { headless, host, port }); + await use(async (trace: string | undefined, { host, port } = {}) => { + const pageImpl = await runTraceViewerApp(trace, browserName, { headless, host, port }); const contextImpl = pageImpl.browserContext; const browser = await playwright.chromium.connectOverCDP(contextImpl._browser.options.wsEndpoint); browsers.push(browser); @@ -173,7 +173,7 @@ export const traceViewerFixtures: Fixtures {}); - const model = new MultiTraceModel(traceModel.contextEntries); + const model = new MultiTraceModel(file, traceModel.contextEntries); const actions = model.filteredActions([]); const { rootItem } = buildActionTree(actions); const actionTree: string[] = []; diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index e888559a0..e42c0faa2 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -91,20 +91,20 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s }); test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) => { - const traceViewer = await showTraceViewer([testInfo.outputPath()]); + const traceViewer = await showTraceViewer(testInfo.outputPath()); await expect(traceViewer.page).toHaveTitle('Playwright Trace Viewer'); }); test('should open two trace viewers', async ({ showTraceViewer }, testInfo) => { const port = testInfo.workerIndex + 48321; - const traceViewer1 = await showTraceViewer([testInfo.outputPath()], { host: 'localhost', port }); + const traceViewer1 = await showTraceViewer(testInfo.outputPath(), { host: 'localhost', port }); await expect(traceViewer1.page).toHaveTitle('Playwright Trace Viewer'); - const traceViewer2 = await showTraceViewer([testInfo.outputPath()], { host: 'localhost', port }); + const traceViewer2 = await showTraceViewer(testInfo.outputPath(), { host: 'localhost', port }); await expect(traceViewer2.page).toHaveTitle('Playwright Trace Viewer'); }); test('should open trace viewer on specific host', async ({ showTraceViewer }, testInfo) => { - const traceViewer = await showTraceViewer([testInfo.outputPath()], { host: '127.0.0.1' }); + const traceViewer = await showTraceViewer(testInfo.outputPath(), { host: '127.0.0.1' }); await expect(traceViewer.page).toHaveTitle('Playwright Trace Viewer'); await expect(traceViewer.page).toHaveURL(/127.0.0.1/); }); @@ -152,7 +152,7 @@ test('should show tracing.group in the action list with location', async ({ runA }); test('should open simple trace viewer', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await expect(traceViewer.actionTitles).toHaveText([ /Create page/, /Navigate to "data:"/, @@ -231,12 +231,12 @@ test('should show action context on locators and other common actions', async ({ }); test('should complain about newer version of trace in old viewer', async ({ showTraceViewer, asset }, testInfo) => { - const traceViewer = await showTraceViewer([asset('trace-from-the-future.zip')]); + const traceViewer = await showTraceViewer(asset('trace-from-the-future.zip')); await expect(traceViewer.page.getByText('The trace was created by a newer version of Playwright and is not supported by this version of the viewer.')).toBeVisible(); }); test('should properly synchronize local and remote time', async ({ showTraceViewer, asset }, testInfo) => { - const traceViewer = await showTraceViewer([asset('trace-remote-time-diff.zip')]); + const traceViewer = await showTraceViewer(asset('trace-remote-time-diff.zip')); // The total duration should be sub 10s, rather than 16h. await expect.poll(async () => parseInt(await traceViewer.page.locator('.timeline-time').last().innerText(), 10) @@ -244,7 +244,7 @@ test('should properly synchronize local and remote time', async ({ showTraceView }); test('should contain action info', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('Click'); await traceViewer.page.getByText('Log', { exact: true }).click(); await expect(traceViewer.logLines).toContainText([ @@ -261,7 +261,7 @@ test('should render network bars', async ({ page, runAndTrace, server }) => { }); test('should render console', async ({ showTraceViewer, browserName }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.showConsoleTab(); await expect(traceViewer.consoleLineMessages.nth(0)).toHaveText('Info'); @@ -292,7 +292,7 @@ test('should render console', async ({ showTraceViewer, browserName }) => { }); test('should open console errors on click', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await expect(traceViewer.actionIconsText('Evaluate')).toHaveText(['2', '1']); await expect(traceViewer.page.getByRole('tabpanel', { name: 'Console' })).toBeHidden(); await traceViewer.actionIcons('Evaluate').click(); @@ -300,7 +300,7 @@ test('should open console errors on click', async ({ showTraceViewer }) => { }); test('should show params and return value', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('Evaluate'); await expect(traceViewer.callLines).toHaveText([ '', @@ -326,7 +326,7 @@ test('should show params and return value', async ({ showTraceViewer }) => { }); test('should show null as a param', async ({ showTraceViewer, browserName }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('Evaluate', 1); await expect(traceViewer.callLines).toHaveText([ '', @@ -340,7 +340,7 @@ test('should show null as a param', async ({ showTraceViewer, browserName }) => }); test('should have correct snapshot size', async ({ showTraceViewer }, testInfo) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('SET VIEWPORT'); await traceViewer.selectSnapshot('Before'); await expect(traceViewer.snapshotContainer).toHaveCSS('width', '1280px'); @@ -351,7 +351,7 @@ test('should have correct snapshot size', async ({ showTraceViewer }, testInfo) }); test('should have correct stack trace', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('Click'); await traceViewer.showSourceTab(); @@ -362,7 +362,7 @@ test('should have correct stack trace', async ({ showTraceViewer }) => { }); test('should have network requests', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('Navigate'); await traceViewer.showNetworkTab(); await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]); @@ -840,7 +840,7 @@ test('should preserve currentSrc', async ({ browser, server, showTraceViewer }) await page.context().tracing.stop({ path: traceFile }); await page.close(); - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); const frame = await traceViewer.snapshotFrame('Set content'); await expect(frame.locator('#target1')).toHaveAttribute('src', server.PREFIX + '/digits/3.png'); await expect(frame.locator('#target2')).toHaveAttribute('src', server.PREFIX + '/digits/6.png'); @@ -986,7 +986,7 @@ test('should highlight expect failure', async ({ page, server, runAndTrace }) => }); test('should show action source', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('Click'); await traceViewer.showSourceTab(); @@ -1024,7 +1024,7 @@ test('should follow redirects', async ({ page, runAndTrace, server, asset }) => }); test('should include metainfo', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.page.getByRole('tab', { name: 'Metadata' }).click(); const callLine = traceViewer.metadataTab.locator('.call-line'); await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/); @@ -1038,68 +1038,12 @@ test('should include metainfo', async ({ showTraceViewer }) => { await expect(callLine.getByText('events')).toHaveText(/events:[\d]+/); }); -test('should open two trace files', async ({ context, page, request, server, showTraceViewer }, testInfo) => { - await (request as any)._tracing.start({ snapshots: true }); - await context.tracing.start({ snapshots: true, sources: true }); - { - const response = await request.get(server.PREFIX + '/simple.json'); - await expect(response).toBeOK(); - } - await page.goto(server.PREFIX + '/input/button.html'); - { - const response = await request.head(server.PREFIX + '/simplezip.json'); - await expect(response).toBeOK(); - } - await page.locator('button').click(); - await page.locator('button').click(); - { - const response = await request.post(server.PREFIX + '/one-style.css'); - await expect(response).toBeOK(); - } - const apiTrace = testInfo.outputPath('api.zip'); - const contextTrace = testInfo.outputPath('context.zip'); - await (request as any)._tracing.stop({ path: apiTrace }); - await context.tracing.stop({ path: contextTrace }); - - const traceViewer = await showTraceViewer([contextTrace, apiTrace]); - - await traceViewer.selectAction('GET'); - await traceViewer.selectAction('HEAD'); - await traceViewer.selectAction('POST'); - await expect(traceViewer.actionTitles).toHaveText([ - /GET "\/simple\.json"/, - /Navigate to "\/input\/button\.html"/, - /HEAD "\/simplezip\.json"/, - /Click.*locator\('button'\)/, - /Click.*locator\('button'\)/, - /POST "\/one-style\.css"/, - ]); - - await traceViewer.page.getByRole('tab', { name: 'Metadata' }).click(); - const callLine = traceViewer.page.locator('.call-line'); - // Should get metadata from the context trace - await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/); - // duration in the metadata section - await expect(callLine.getByText('duration').first()).toHaveText(/duration:[\dms]+/); - await expect(callLine.getByText('engine')).toHaveText(/engine:[\w]+/); - await expect(callLine.getByText('platform')).toHaveText(/platform:[\w]+/); - await expect(callLine.getByText('width')).toHaveText(/width:[\d]+/); - await expect(callLine.getByText('height')).toHaveText(/height:[\d]+/); - await expect(callLine.getByText('pages')).toHaveText(/pages:1/); - await expect(callLine.getByText('actions')).toHaveText(/actions:6/); - await expect(callLine.getByText('events')).toHaveText(/events:[\d]+/); -}); - -test('should open two trace files of the same test (v6)', async ({ showTraceViewer, asset }) => { - const traceViewer = await showTraceViewer([asset('test-trace1.zip'), asset('test-trace2.zip')]); +test('should open v6 trace file', async ({ showTraceViewer, asset }) => { + const traceViewer = await showTraceViewer(asset('test-trace1.zip')); // Same actions from different test runs should not be merged. await expect(traceViewer.actionTitles).toHaveText([ - /Before Hooks/, /Before Hooks/, /page.goto/, // Legacy trace does not have titles - /page.goto/, // Legacy trace does not have titles - /expect.toBe/, - /After Hooks/, /expect.toBe/, /After Hooks/, ]); @@ -1210,13 +1154,13 @@ test('should update highlight when typing snapshot', async ({ page, runAndTrace, }); test('should open trace-1.31', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([path.join(__dirname, '../assets/trace-1.31.zip')]); + const traceViewer = await showTraceViewer(path.join(__dirname, '../assets/trace-1.31.zip')); const snapshot = await traceViewer.snapshotFrame('Click'); await expect(snapshot.locator('[__playwright_target__]')).toHaveText(['Submit']); }); test('should open trace-1.37', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([path.join(__dirname, '../assets/trace-1.37.zip')]); + const traceViewer = await showTraceViewer(path.join(__dirname, '../assets/trace-1.37.zip')); const snapshot = await traceViewer.snapshotFrame('page.goto'); await expect(snapshot.locator('div')).toHaveCSS('background-color', 'rgb(255, 0, 0)'); @@ -1422,7 +1366,7 @@ test('should preserve noscript when javascript is disabled', async ({ browser, s await page.context().tracing.stop({ path: traceFile }); await page.close(); - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); const frame = await traceViewer.snapshotFrame('Set content'); await expect(frame.getByText('javascript is disabled!')).toBeVisible(); }); @@ -1439,7 +1383,7 @@ test('should remove noscript by default', async ({ browser, server, showTraceVie await page.context().tracing.stop({ path: traceFile }); await page.close(); - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); const frame = await traceViewer.snapshotFrame('Set content'); await expect(frame.getByText('Always visible')).toBeVisible(); await expect(frame.getByText('Enable JavaScript to run this app.')).toBeHidden(); @@ -1457,7 +1401,7 @@ test('should remove noscript when javaScriptEnabled is set to true', async ({ br await page.context().tracing.stop({ path: traceFile }); await page.close(); - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); const frame = await traceViewer.snapshotFrame('Set content'); await expect(frame.getByText('Always visible')).toBeVisible(); await expect(frame.getByText('Enable JavaScript to run this app.')).toBeHidden(); @@ -1482,7 +1426,7 @@ test('should open snapshot in new browser context', async ({ browser, page, runA }); test('should show similar actions from legacy library-only trace', async ({ showTraceViewer, asset }) => { - const traceViewer = await showTraceViewer([asset('trace-library-1.46.zip')]); + const traceViewer = await showTraceViewer(asset('trace-library-1.46.zip')); await traceViewer.showAllActions(); await expect(traceViewer.actionTitles).toHaveText([ /page\.setContent/, @@ -1568,7 +1512,7 @@ test('should not record network actions', { test('should show baseURL in metadata pane', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31847' }, }, async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('Evaluate'); await traceViewer.showMetadataTab(); await expect(traceViewer.metadataTab).toContainText('baseURL:https://example.com'); @@ -1577,7 +1521,7 @@ test('should show baseURL in metadata pane', { test('should not leak recorders', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33086' }, }, async ({ showTraceViewer, platform }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); const aliveCount = async () => { return await traceViewer.page.evaluate(() => { @@ -1767,14 +1711,14 @@ test('should show a modal dialog', async ({ runAndTrace, page, platform, browser }); test('should open settings dialog', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('Navigate'); await traceViewer.showSettings(); await expect(traceViewer.settingsDialog).toBeVisible(); }); test('should toggle theme color', async ({ showTraceViewer, page }) => { - const traceViewer = await showTraceViewer([traceFile]); + const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('Navigate'); await traceViewer.showSettings(); @@ -1824,7 +1768,7 @@ test('should toggle canvas rendering', async ({ runAndTrace, page }) => { }); test('should render blob trace received from message', async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([], { host: 'localhost' }); + const traceViewer = await showTraceViewer(undefined, { host: 'localhost' }); await expect(traceViewer.page.locator('.drop-target')).toBeVisible(); await expect(traceViewer.actionTitles).not.toBeVisible(); @@ -1864,7 +1808,7 @@ test('should render blob trace received from message', async ({ showTraceViewer }); test("shouldn't render not-blob trace received from message", async ({ showTraceViewer }) => { - const traceViewer = await showTraceViewer([], { host: 'localhost' }); + const traceViewer = await showTraceViewer(undefined, { host: 'localhost' }); await expect(traceViewer.page.locator('.drop-target')).toBeVisible(); await expect(traceViewer.actionTitles).not.toBeVisible(); @@ -1933,7 +1877,7 @@ test('should render locator descriptions', async ({ runAndTrace, page }) => { test('should load trace from HTTP with progress indicator', async ({ showTraceViewer, server }) => { const [traceViewer, res] = await Promise.all([ - showTraceViewer([server.PREFIX]), + showTraceViewer(server.PREFIX), new Promise(resolve => { server.setRoute('/', (req, res) => resolve(res)); }), From 9f3fce1ca1113df6522a1ffd205c9a1f71330e1c Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 8 Oct 2025 08:58:53 +0200 Subject: [PATCH 036/250] chore: migrate proxy handling to WHATWG URL (#37745) --- .../src/server/utils/network.ts | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/packages/playwright-core/src/server/utils/network.ts b/packages/playwright-core/src/server/utils/network.ts index 2fec2ea89..ee5d7ec42 100644 --- a/packages/playwright-core/src/server/utils/network.ts +++ b/packages/playwright-core/src/server/utils/network.ts @@ -52,8 +52,8 @@ export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.Inco const proxyURL = getProxyForUrl(params.url); if (proxyURL) { - const parsedProxyURL = url.parse(proxyURL); if (params.url.startsWith('http:')) { + const parsedProxyURL = url.parse(proxyURL); options = { path: parsedUrl.href, host: parsedProxyURL.hostname, @@ -63,7 +63,7 @@ export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.Inco method: options.method }; } else { - options.agent = new HttpsProxyAgent(url.format(parsedProxyURL)); + options.agent = new HttpsProxyAgent(normalizeProxyURL(proxyURL)); options.rejectUnauthorized = false; } } @@ -136,41 +136,44 @@ function shouldBypassProxy(url: URL, bypass?: string): boolean { return domains.some(d => domain.endsWith(d)); } +function normalizeProxyURL(proxy: string): URL { + proxy = proxy.trim(); + // Browsers allow to specify proxy without a protocol, defaulting to http. + if (!/^\w+:\/\//.test(proxy)) + proxy = 'http://' + proxy; + return new URL(proxy); +} + export function createProxyAgent(proxy?: ProxySettings, forUrl?: URL) { if (!proxy) return; if (forUrl && proxy.bypass && shouldBypassProxy(forUrl, proxy.bypass)) return; - // Browsers allow to specify proxy without a protocol, defaulting to http. - let proxyServer = proxy.server.trim(); - if (!/^\w+:\/\//.test(proxyServer)) - proxyServer = 'http://' + proxyServer; - - const proxyOpts = url.parse(proxyServer); - if (proxyOpts.protocol?.startsWith('socks')) { - const socksProxyURL = new URL(proxyServer); - + const proxyURL = normalizeProxyURL(proxy.server); + if (proxyURL.protocol?.startsWith('socks')) { // SocksProxyAgent distinguishes between socks5 and socks5h. // socks5h is what we want, it means that hostnames are resolved by the proxy. // browsers behave the same way, even if socks5 is specified. - if (proxyOpts.protocol === 'socks5:') - socksProxyURL.protocol = 'socks5h:'; - else if (proxyOpts.protocol === 'socks4:') - socksProxyURL.protocol = 'socks4a:'; + if (proxyURL.protocol === 'socks5:') + proxyURL.protocol = 'socks5h:'; + else if (proxyURL.protocol === 'socks4:') + proxyURL.protocol = 'socks4a:'; - return new SocksProxyAgent(socksProxyURL); + return new SocksProxyAgent(proxyURL); + } + if (proxy.username) { + proxyURL.username = proxy.username; + proxyURL.password = proxy.password || ''; } - if (proxy.username) - proxyOpts.auth = `${proxy.username}:${proxy.password || ''}`; if (forUrl && ['ws:', 'wss:'].includes(forUrl.protocol)) { // Force CONNECT method for WebSockets. - return new HttpsProxyAgent(url.format(proxyOpts)); + return new HttpsProxyAgent(proxyURL); } - // TODO: This branch should be different from above. We should use HttpProxyAgent conditional on proxyOpts.protocol instead of always using CONNECT method. - return new HttpsProxyAgent(url.format(proxyOpts)); + // TODO: This branch should be different from above. We should use HttpProxyAgent conditional on proxyURL.protocol instead of always using CONNECT method. + return new HttpsProxyAgent(proxyURL); } export function createHttpServer(requestListener?: (req: http.IncomingMessage, res: http.ServerResponse) => void): http.Server; From 6d7eafb1697611ecd845cade3a1ca9c8ec2249cd Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 8 Oct 2025 17:32:51 +0100 Subject: [PATCH 037/250] test: cft bot (#37770) --- .github/workflows/tests_cft.yml | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .github/workflows/tests_cft.yml diff --git a/.github/workflows/tests_cft.yml b/.github/workflows/tests_cft.yml new file mode 100644 index 000000000..b39914d39 --- /dev/null +++ b/.github/workflows/tests_cft.yml @@ -0,0 +1,85 @@ +name: tests CfT + +on: + workflow_dispatch: + inputs: + ref: + description: Playwright SHA / ref to test. Use 'refs/pull/PULL_REQUEST_ID/head' to test a PR. Defaults to the current branch. + required: false + default: '' + +env: + FORCE_COLOR: 1 + +jobs: + test_cft: + permissions: + contents: read # This is required for actions/checkout to succeed + strategy: + fail-fast: false + matrix: + os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, macos-14-large, macos-14-xlarge, macos-15-large, macos-15-xlarge, windows-2022, windows-2025] + name: CfT ${{ matrix.os }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + if: github.event_name != 'workflow_dispatch' + - uses: actions/checkout@v5 + if: github.event_name == 'workflow_dispatch' + with: + ref: ${{ github.event.inputs.ref }} + - uses: actions/setup-node@v5 + with: + node-version: 20 + - run: npm ci + - run: npm run build + - run: npx playwright install --with-deps chromium ffmpeg + if: contains(matrix.os, 'ubuntu') + - run: npx playwright install chromium ffmpeg + if: !contains(matrix.os, 'ubuntu') + - name: Install CfT beta + run: echo "CRPATH=$(npx -y @puppeteer/browsers install chrome@beta --with-deps | tail -n1 | sed 's/^[^ ]* *//')" >> "$GITHUB_ENV" + - name: Run tests linux + run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run ctest + if: contains(matrix.os, 'ubuntu') + env: + PWTEST_CHANNEL: chromium + - name: Run tests non-linux + run: npm run ctest + if: !contains(matrix.os, 'ubuntu') + env: + PWTEST_CHANNEL: chromium + + test_cft_headless_shell: + permissions: + contents: read # This is required for actions/checkout to succeed + strategy: + fail-fast: false + matrix: + os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, macos-14-large, macos-14-xlarge, macos-15-large, macos-15-xlarge, windows-2022, windows-2025] + name: CfT headless shell ${{ matrix.os }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + if: github.event_name != 'workflow_dispatch' + - uses: actions/checkout@v5 + if: github.event_name == 'workflow_dispatch' + with: + ref: ${{ github.event.inputs.ref }} + - uses: actions/setup-node@v5 + with: + node-version: 20 + - run: npm ci + - run: npm run build + - run: npx playwright install --with-deps chromium ffmpeg + if: contains(matrix.os, 'ubuntu') + - run: npx playwright install chromium ffmpeg + if: !contains(matrix.os, 'ubuntu') + - name: Install CfT headless shell beta + run: echo "CRPATH=$(npx -y @puppeteer/browsers install chrome-headless-shell@beta --with-deps | tail -n1 | sed 's/^[^ ]* *//')" >> "$GITHUB_ENV" + - name: Run tests linux + run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run ctest + if: contains(matrix.os, 'ubuntu') + - name: Run tests non-linux + run: npm run ctest + if: !contains(matrix.os, 'ubuntu') From 485aaf7fff96e4c9505e26af27e6db283622da62 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 8 Oct 2025 10:43:15 -0700 Subject: [PATCH 038/250] test: allow for new properties on window.performance (#37773) --- tests/page/page-evaluate.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/page/page-evaluate.spec.ts b/tests/page/page-evaluate.spec.ts index d1598f150..d4c2d0993 100644 --- a/tests/page/page-evaluate.spec.ts +++ b/tests/page/page-evaluate.spec.ts @@ -385,7 +385,7 @@ it('should properly serialize PerformanceMeasure object', async ({ page }) => { it('should properly serialize window.performance object', async ({ page }) => { it.skip(!!process.env.PW_CLOCK); - expect(await page.evaluate(() => performance)).toEqual({ + expect(await page.evaluate(() => performance)).toEqual(expect.objectContaining({ 'navigation': { 'redirectCount': 0, 'type': expect.any(Number), @@ -414,7 +414,7 @@ it('should properly serialize window.performance object', async ({ page }) => { 'unloadEventEnd': expect.any(Number), 'unloadEventStart': expect.any(Number), } - }); + })); }); it('should return undefined for non-serializable objects', async ({ page }) => { From e396b9f6beeea3524636256b3ac050e48d4aa2f2 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 9 Oct 2025 20:52:19 +0100 Subject: [PATCH 039/250] chore(mcp): merge refLocator and generateSelector (#37781) --- packages/playwright/src/mcp/browser/tab.ts | 31 ++++++++++--------- .../src/mcp/browser/tools/evaluate.ts | 9 +++--- .../playwright/src/mcp/browser/tools/form.ts | 5 ++- .../src/mcp/browser/tools/keyboard.ts | 9 +++--- .../src/mcp/browser/tools/screenshot.ts | 10 +++--- .../src/mcp/browser/tools/snapshot.ts | 25 +++++++-------- .../playwright/src/mcp/browser/tools/utils.ts | 11 ------- .../src/mcp/browser/tools/verify.ts | 7 ++--- 8 files changed, 46 insertions(+), 61 deletions(-) diff --git a/packages/playwright/src/mcp/browser/tab.ts b/packages/playwright/src/mcp/browser/tab.ts index e655dffc8..2831c6849 100644 --- a/packages/playwright/src/mcp/browser/tab.ts +++ b/packages/playwright/src/mcp/browser/tab.ts @@ -16,7 +16,7 @@ import { EventEmitter } from 'events'; import * as playwright from 'playwright-core'; -import { ManualPromise } from 'playwright-core/lib/utils'; +import { asLocator, ManualPromise } from 'playwright-core/lib/utils'; import { callOnPageNoTrace, waitForCompletion } from './tools/utils'; import { logUnhandledError } from '../log'; @@ -25,10 +25,8 @@ import { handleDialog } from './tools/dialogs'; import { uploadFile } from './tools/files'; import type { Context } from './context'; - -type PageEx = playwright.Page & { - _snapshotForAI: () => Promise; -}; +import type { Page } from '../../../../playwright-core/src/client/page'; +import type { Locator } from '../../../../playwright-core/src/client/locator'; export const TabEvents = { modalState: 'modalState' @@ -49,7 +47,7 @@ export type TabSnapshot = { export class Tab extends EventEmitter { readonly context: Context; - readonly page: playwright.Page; + readonly page: Page; private _lastTitle = 'about:blank'; private _consoleMessages: ConsoleMessage[] = []; private _recentConsoleMessages: ConsoleMessage[] = []; @@ -62,7 +60,7 @@ export class Tab extends EventEmitter { constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) { super(); this.context = context; - this.page = page; + this.page = page as Page; this._onPageClose = onPageClose; page.on('console', event => this._handleConsoleMessage(messageToConsoleMessage(event))); page.on('pageerror', error => this._handleConsoleMessage(pageErrorToConsoleMessage(error))); @@ -222,7 +220,7 @@ export class Tab extends EventEmitter { async captureSnapshot(): Promise { let tabSnapshot: TabSnapshot | undefined; const modalStates = await this._raceAgainstModalStates(async () => { - const snapshot = await (this.page as PageEx)._snapshotForAI(); + const snapshot = await this.page._snapshotForAI(); tabSnapshot = { url: this.page.url(), title: await this.page.title(), @@ -272,17 +270,20 @@ export class Tab extends EventEmitter { await this._raceAgainstModalStates(() => waitForCompletion(this, callback)); } - async refLocator(params: { element: string, ref: string }): Promise { + async refLocator(params: { element: string, ref: string }): Promise<{ locator: Locator, resolved: string }> { 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}]`)) + async refLocators(params: { element: string, ref: string }[]): Promise<{ locator: Locator, resolved: string }[]> { + return Promise.all(params.map(async param => { + try { + const locator = this.page.locator(`aria-ref=${param.ref}`).describe(param.element) as Locator; + const { resolvedSelector } = await locator._resolveSelector(); + return { locator, resolved: asLocator('javascript', resolvedSelector) }; + } catch (e) { 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) { diff --git a/packages/playwright/src/mcp/browser/tools/evaluate.ts b/packages/playwright/src/mcp/browser/tools/evaluate.ts index f952230ec..416a2eeb1 100644 --- a/packages/playwright/src/mcp/browser/tools/evaluate.ts +++ b/packages/playwright/src/mcp/browser/tools/evaluate.ts @@ -17,9 +17,8 @@ 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'; +import type { Tab } from '../tab'; const evaluateSchema = z.object({ function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'), @@ -40,16 +39,16 @@ const evaluate = defineTabTool({ handle: async (tab, params, response) => { response.setIncludeSnapshot(); - let locator: playwright.Locator | undefined; + let locator: Awaited> | 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)});`); + response.addCode(`await page.${locator.resolved}.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 receiver = locator?.locator ?? tab.page; const result = await receiver._evaluateFunction(params.function); response.addResult(JSON.stringify(result, null, 2) || 'undefined'); }); diff --git a/packages/playwright/src/mcp/browser/tools/form.ts b/packages/playwright/src/mcp/browser/tools/form.ts index b2d957e99..f128789d6 100644 --- a/packages/playwright/src/mcp/browser/tools/form.ts +++ b/packages/playwright/src/mcp/browser/tools/form.ts @@ -16,7 +16,6 @@ import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; -import { generateLocator } from './utils'; import * as codegen from '../codegen'; const fillForm = defineTabTool({ @@ -39,8 +38,8 @@ const fillForm = defineTabTool({ 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)}`; + const { locator, resolved } = await tab.refLocator({ element: field.name, ref: field.ref }); + const locatorSource = `await page.${resolved}`; if (field.type === 'textbox' || field.type === 'slider') { const secret = tab.context.lookupSecret(field.value); await locator.fill(secret.value); diff --git a/packages/playwright/src/mcp/browser/tools/keyboard.ts b/packages/playwright/src/mcp/browser/tools/keyboard.ts index 898e18c46..0010863bc 100644 --- a/packages/playwright/src/mcp/browser/tools/keyboard.ts +++ b/packages/playwright/src/mcp/browser/tools/keyboard.ts @@ -17,7 +17,6 @@ import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; import { elementSchema } from './snapshot'; -import { generateLocator } from './utils'; const pressKey = defineTabTool({ capability: 'core', @@ -60,22 +59,22 @@ const type = defineTabTool({ }, handle: async (tab, params, response) => { - const locator = await tab.refLocator(params); + const { locator, resolved } = 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(${secret.code});`); + response.addCode(`await page.${resolved}.pressSequentially(${secret.code});`); await locator.pressSequentially(secret.value); } else { - response.addCode(`await page.${await generateLocator(locator)}.fill(${secret.code});`); + response.addCode(`await page.${resolved}.fill(${secret.code});`); await locator.fill(secret.value); } if (params.submit) { response.setIncludeSnapshot(); - response.addCode(`await page.${await generateLocator(locator)}.press('Enter');`); + response.addCode(`await page.${resolved}.press('Enter');`); await locator.press('Enter'); } }); diff --git a/packages/playwright/src/mcp/browser/tools/screenshot.ts b/packages/playwright/src/mcp/browser/tools/screenshot.ts index 7aa37db99..51f51a5e8 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, dateAsFileName } from './utils'; +import { dateAsFileName } from './utils'; import type * as playwright from 'playwright-core'; @@ -60,14 +60,14 @@ const screenshot = defineTabTool({ 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; + const ref = 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)});`); + if (ref) + response.addCode(`await page.${ref.resolved}.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); + const buffer = ref ? await ref.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 diff --git a/packages/playwright/src/mcp/browser/tools/snapshot.ts b/packages/playwright/src/mcp/browser/tools/snapshot.ts index 5900ada32..e944bc403 100644 --- a/packages/playwright/src/mcp/browser/tools/snapshot.ts +++ b/packages/playwright/src/mcp/browser/tools/snapshot.ts @@ -17,7 +17,6 @@ import { z } from '../../sdk/bundle'; import { defineTabTool, defineTool } from './tool'; import * as javascript from '../codegen'; -import { generateLocator } from './utils'; const snapshot = defineTool({ capability: 'core', @@ -59,7 +58,7 @@ const click = defineTabTool({ handle: async (tab, params, response) => { response.setIncludeSnapshot(); - const locator = await tab.refLocator(params); + const { locator, resolved } = await tab.refLocator(params); const options = { button: params.button, modifiers: params.modifiers, @@ -68,9 +67,9 @@ const click = defineTabTool({ const optionsAttr = formatted !== '{}' ? formatted : ''; if (params.doubleClick) - response.addCode(`await page.${await generateLocator(locator)}.dblclick(${optionsAttr});`); + response.addCode(`await page.${resolved}.dblclick(${optionsAttr});`); else - response.addCode(`await page.${await generateLocator(locator)}.click(${optionsAttr});`); + response.addCode(`await page.${resolved}.click(${optionsAttr});`); await tab.waitForCompletion(async () => { if (params.doubleClick) @@ -99,16 +98,16 @@ const drag = defineTabTool({ handle: async (tab, params, response) => { response.setIncludeSnapshot(); - const [startLocator, endLocator] = await tab.refLocators([ + const [start, end] = await tab.refLocators([ { ref: params.startRef, element: params.startElement }, { ref: params.endRef, element: params.endElement }, ]); await tab.waitForCompletion(async () => { - await startLocator.dragTo(endLocator); + await start.locator.dragTo(end.locator); }); - response.addCode(`await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});`); + response.addCode(`await page.${start.resolved}.dragTo(page.${end.resolved});`); }, }); @@ -125,8 +124,8 @@ const hover = defineTabTool({ handle: async (tab, params, response) => { response.setIncludeSnapshot(); - const locator = await tab.refLocator(params); - response.addCode(`await page.${await generateLocator(locator)}.hover();`); + const { locator, resolved } = await tab.refLocator(params); + response.addCode(`await page.${resolved}.hover();`); await tab.waitForCompletion(async () => { await locator.hover(); @@ -151,8 +150,8 @@ const selectOption = defineTabTool({ 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)});`); + const { locator, resolved } = await tab.refLocator(params); + response.addCode(`await page.${resolved}.selectOption(${javascript.formatObject(params.values)});`); await tab.waitForCompletion(async () => { await locator.selectOption(params.values); @@ -171,8 +170,8 @@ const pickLocator = defineTabTool({ }, handle: async (tab, params, response) => { - const locator = await tab.refLocator(params); - response.addResult(await generateLocator(locator)); + const { resolved } = await tab.refLocator(params); + response.addResult(resolved); }, }); diff --git a/packages/playwright/src/mcp/browser/tools/utils.ts b/packages/playwright/src/mcp/browser/tools/utils.ts index e68aa6990..170bc06df 100644 --- a/packages/playwright/src/mcp/browser/tools/utils.ts +++ b/packages/playwright/src/mcp/browser/tools/utils.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { asLocator } from 'playwright-core/lib/utils'; - import type * as playwright from 'playwright-core'; import type { Tab } from '../tab'; @@ -74,15 +72,6 @@ export async function waitForCompletion(tab: Tab, callback: () => Promise) } } -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 index fa4d0ba0d..1da87d5af 100644 --- a/packages/playwright/src/mcp/browser/tools/verify.ts +++ b/packages/playwright/src/mcp/browser/tools/verify.ts @@ -17,7 +17,6 @@ import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; import * as javascript from '../codegen'; -import { generateLocator } from './utils'; const verifyElement = defineTabTool({ capability: 'testing', @@ -83,7 +82,7 @@ const verifyList = defineTabTool({ }, handle: async (tab, params, response) => { - const locator = await tab.refLocator({ ref: params.ref, element: params.element }); + 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); @@ -118,8 +117,8 @@ const verifyValue = defineTabTool({ }, handle: async (tab, params, response) => { - const locator = await tab.refLocator({ ref: params.ref, element: params.element }); - const locatorSource = `page.${await generateLocator(locator)}`; + const { locator, resolved } = await tab.refLocator({ ref: params.ref, element: params.element }); + const locatorSource = `page.${resolved}`; if (params.type === 'textbox' || params.type === 'slider' || params.type === 'combobox') { const value = await locator.inputValue(); if (value !== params.value) { From 47fbd3a51a89ffe7aaa80137861c01bae533833c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 9 Oct 2025 13:50:16 -0700 Subject: [PATCH 040/250] chore: remove HMR support from trace viewer (#37785) --- CONTRIBUTING.md | 10 ---- .../src/server/recorder/recorderApp.ts | 2 +- .../src/server/trace/viewer/traceViewer.ts | 12 +---- packages/playwright/src/reporters/html.ts | 25 --------- packages/trace-viewer/src/sw/main.ts | 29 ++++------- .../trace-viewer/src/sw/traceModelBackends.ts | 51 +++++++++---------- packages/trace-viewer/src/ui/snapshotTab.tsx | 3 -- 7 files changed, 35 insertions(+), 97 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a945d5726..67468d397 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,16 +31,6 @@ npm run watch npx playwright install ``` -**Experimental dev mode with Hot Module Replacement for recorder/trace-viewer/UI Mode** - -``` -PW_HMR=1 npm run watch -PW_HMR=1 npx playwright show-trace -PW_HMR=1 npm run ctest -- --ui -PW_HMR=1 npx playwright codegen -PW_HMR=1 npx playwright show-report -``` - Playwright is a multi-package repository that uses npm workspaces. For browser APIs, look at [`packages/playwright-core`](https://github.com/microsoft/playwright/blob/main/packages/playwright-core). For test runner, see [`packages/playwright`](https://github.com/microsoft/playwright/blob/main/packages/playwright). Note that some files are generated by the build, so the watch process might override your changes if done in the wrong file. For example, TypeScript types for the API are generated from the [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src). diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index 5d1ffb81b..a216dc700 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -107,7 +107,7 @@ export class RecorderApp { delete (inspectedContext as any)[recorderAppSymbol]; }); - await this._page.mainFrame().goto(progress, process.env.PW_HMR ? 'http://localhost:44225' : 'https://playwright/index.html'); + await this._page.mainFrame().goto(progress, 'https://playwright/index.html'); }); const url = this._recorder.url(); diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 48a504209..53438cf7c 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -69,10 +69,6 @@ export async function startTraceViewerServer(options?: TraceViewerServerOptions) server.routePrefix('/trace', (request, response) => { const url = new URL('http://localhost' + request.url!); const relativePath = url.pathname.slice('/trace'.length); - if (process.env.PW_HMR) { - // When running in Vite HMR mode, port is hardcoded in build.js - response.appendHeader('Access-Control-Allow-Origin', 'http://localhost:44223'); - } if (relativePath.endsWith('/stall.js')) return true; if (relativePath.startsWith('/file')) { @@ -131,13 +127,7 @@ export async function installRootRedirect(server: HttpServer, traceUrl: string | for (const reporter of options.reporter || []) params.append('reporter', reporter); - let baseUrl = '.'; - if (process.env.PW_HMR) { - baseUrl = 'http://localhost:44223'; // port is hardcoded in build.js - params.set('server', server.urlPrefix('precise')); - } - - const urlPath = `${baseUrl}/trace/${options.webApp || 'index.html'}?${params.toString()}`; + const urlPath = `./trace/${options.webApp || 'index.html'}?${params.toString()}`; server.routePath('/', (_, response) => { response.statusCode = 302; response.setHeader('Location', urlPath); diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 22d2a144d..898034c2e 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -315,31 +315,6 @@ class HtmlBuilder { singleTestId = testFile.tests[0].testId; } - if (process.env.PW_HMR === '1') { - const redirectFile = path.join(this._reportFolder, 'index.html'); - - await this._writeReportData(redirectFile); - - async function redirect() { - const hmrURL = new URL('http://localhost:44224'); // dev server, port is harcoded in build.js - const popup = window.open(hmrURL); - const listener = (evt: MessageEvent) => { - if (evt.source === popup && evt.data === 'ready') { - const element = document.getElementById('playwrightReportBase64'); - popup!.postMessage(element?.textContent ?? '', hmrURL.origin); - window.removeEventListener('message', listener); - // This is generally not allowed - window.close(); - } - }; - window.addEventListener('message', listener); - } - - fs.appendFileSync(redirectFile, ``); - - return { ok, singleTestId }; - } - // Copy app. const appFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'vite', 'htmlReport'); await copyFileAndMakeWritable(path.join(appFolder, 'index.html'), path.join(this._reportFolder, 'index.html')); diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 64d22a426..731cb8e55 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -18,7 +18,7 @@ import { splitProgress } from './progress'; import { unwrapPopoutUrl } from './snapshotRenderer'; import { SnapshotServer } from './snapshotServer'; import { TraceModel } from './traceModel'; -import { FetchTraceModelBackend, TraceViewerServer, ZipTraceModelBackend } from './traceModelBackends'; +import { FetchTraceModelBackend, traceFileURL, ZipTraceModelBackend } from './traceModelBackends'; import { TraceVersionError } from './traceModernizer'; // @ts-ignore @@ -33,28 +33,19 @@ self.addEventListener('activate', function(event: any) { }); const scopePath = new URL(self.registration.scope).pathname; - const loadedTraces = new Map(); - -type ClientData = { - traceUrl: string; - traceViewerServer: TraceViewerServer -}; -const clientIdToTraceUrls = new Map(); +const clientIdToTraceUrls = new Map(); async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, progress: (done: number, total: number) => undefined): Promise { const clientId = client?.id ?? ''; - const clientURL = new URL(client?.url ?? self.registration.scope); - const traceViewerServerBaseUrl = new URL(clientURL.searchParams.get('server') ?? '../', clientURL); - const data: ClientData = { traceUrl, traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) }; - clientIdToTraceUrls.set(clientId, data); + clientIdToTraceUrls.set(clientId, traceUrl); await gc(); const traceModel = new TraceModel(); try { // Allow 10% to hop from sw to page. const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]); - const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl, data.traceViewerServer) : new ZipTraceModelBackend(traceUrl, data.traceViewerServer, fetchProgress); + const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress); await traceModel.load(backend, unzipProgress); } catch (error: any) { // eslint-disable-next-line no-console @@ -161,11 +152,9 @@ async function doFetch(event: FetchEvent): Promise { if (relativePath.startsWith('/file/')) { const path = url.searchParams.get('path')!; - const traceViewerServer = clientIdToTraceUrls.get(event.clientId ?? '')?.traceViewerServer; - if (!traceViewerServer) - throw new Error('client is not initialized'); - const response = await traceViewerServer.readFile(path); - if (!response) + const fileURL = traceFileURL(path); + const response = await fetch(fileURL); + if (response.status === 404) return new Response(null, { status: 404 }); return response; } @@ -202,13 +191,13 @@ async function gc() { const clients = await self.clients.matchAll(); const usedTraces = new Set(); - for (const [clientId, data] of clientIdToTraceUrls) { + for (const [clientId, traceUrl] of clientIdToTraceUrls) { // @ts-ignore if (!clients.find(c => c.id === clientId)) { clientIdToTraceUrls.delete(clientId); continue; } - usedTraces.add(data.traceUrl); + usedTraces.add(traceUrl); } for (const traceUrl of loadedTraces.keys()) { diff --git a/packages/trace-viewer/src/sw/traceModelBackends.ts b/packages/trace-viewer/src/sw/traceModelBackends.ts index 91aad60c9..da60e9c1e 100644 --- a/packages/trace-viewer/src/sw/traceModelBackends.ts +++ b/packages/trace-viewer/src/sw/traceModelBackends.ts @@ -28,11 +28,12 @@ export class ZipTraceModelBackend implements TraceModelBackend { private _entriesPromise: Promise>; private _traceURL: string; - constructor(traceURL: string, server: TraceViewerServer, progress: Progress) { + constructor(traceURL: string, progress: Progress) { this._traceURL = traceURL; zipjs.configure({ baseURL: self.location.href } as any); + this._zipReader = new zipjs.ZipReader( - new zipjs.HttpReader(formatUrl(traceURL, server), { mode: 'cors', preventHeadRequest: true } as any), + new zipjs.HttpReader(this._resolveTraceURL(traceURL), { mode: 'cors', preventHeadRequest: true } as any), { useWebWorkers: false }); this._entriesPromise = this._zipReader.getEntries({ onprogress: progress }).then(entries => { const map = new Map(); @@ -42,6 +43,18 @@ export class ZipTraceModelBackend implements TraceModelBackend { }); } + private _resolveTraceURL(traceURL: string): string { + let url: string; + if (traceURL.startsWith('http') || traceURL.startsWith('blob')) { + url = traceURL; + if (url.startsWith('https://www.dropbox.com/')) + url = 'https://dl.dropboxusercontent.com/' + url.substring('https://www.dropbox.com/'.length); + } else { + url = traceFileURL(traceURL); + } + return url; + } + isLive() { return false; } @@ -84,12 +97,10 @@ export class ZipTraceModelBackend implements TraceModelBackend { export class FetchTraceModelBackend implements TraceModelBackend { private _entriesPromise: Promise>; private _path: string; - private _server: TraceViewerServer; - constructor(path: string, server: TraceViewerServer) { + constructor(path: string) { this._path = path; - this._server = server; - this._entriesPromise = server.readFile(path).then(async response => { + this._entriesPromise = this._readFile(path).then(async response => { if (!response) throw new Error('File not found'); const json = await response.json(); @@ -133,31 +144,17 @@ export class FetchTraceModelBackend implements TraceModelBackend { const fileName = entries.get(entryName); if (!fileName) return; - return this._server.readFile(fileName); + return this._readFile(fileName); } -} - -function formatUrl(trace: string, server: TraceViewerServer) { - let url = trace.startsWith('http') || trace.startsWith('blob') ? trace : server.getFileURL(trace).toString(); - // Dropbox does not support cors. - if (url.startsWith('https://www.dropbox.com/')) - url = 'https://dl.dropboxusercontent.com/' + url.substring('https://www.dropbox.com/'.length); - return url; -} -export class TraceViewerServer { - constructor(private readonly baseUrl: URL) {} - - getFileURL(path: string): URL { - const url = new URL('trace/file', this.baseUrl); - url.searchParams.set('path', path); - return url; - } - - async readFile(path: string): Promise { - const response = await fetch(this.getFileURL(path)); + private async _readFile(path: string): Promise { + const response = await fetch(traceFileURL(path)); if (response.status === 404) return; return response; } } + +export function traceFileURL(path: string): string { + return `file?path=${encodeURIComponent(path)}`; +} diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 041a4e988..ce0f1d689 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -412,7 +412,6 @@ export function collectSnapshots(action: ActionTraceEvent | undefined): Snapshot } const isUnderTest = new URLSearchParams(window.location.search).has('isUnderTest'); -const serverParam = new URLSearchParams(window.location.search).get('server'); export function extendSnapshot(traceUrl: string, snapshot: Snapshot, shouldPopulateCanvasFromScreenshot: boolean): SnapshotUrls { const params = new URLSearchParams(); @@ -434,8 +433,6 @@ export function extendSnapshot(traceUrl: string, snapshot: Snapshot, shouldPopul const popoutParams = new URLSearchParams(); popoutParams.set('r', snapshotUrl); - if (serverParam) - popoutParams.set('server', serverParam); popoutParams.set('trace', traceUrl); if (snapshot.point) { popoutParams.set('pointX', String(snapshot.point.x)); From e8bf68833fa50298f1c346079f0a1c1edf36c97c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 9 Oct 2025 14:49:58 -0700 Subject: [PATCH 041/250] fix: do not advance real time while forwarding (#37705) --- packages/injected/src/clock.ts | 81 +++++++++++++++++++++++--------- tests/library/unit/clock.spec.ts | 66 ++++++++++++++------------ 2 files changed, 95 insertions(+), 52 deletions(-) diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index 753996a36..eb788a426 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -59,6 +59,13 @@ type Time = { type LogEntryType = 'fastForward' |'install' | 'pauseAt' | 'resume' | 'runFor' | 'setFixedTime' | 'setSystemTime'; +type RealTimeTimer = { + callAt: Ticks; + cancel: () => void; + promise: Promise | undefined; + dispose: () => Promise; +}; + export class ClockController { readonly _now: Time; private _duringTick = false; @@ -68,7 +75,7 @@ export class ClockController { readonly disposables: (() => void)[] = []; private _log: { type: LogEntryType, time: number, param?: number }[] = []; private _realTime: { startTicks: EmbedderTicks, lastSyncTicks: EmbedderTicks } | undefined; - private _currentRealTimeTimer: { callAt: Ticks, dispose: () => void } | undefined; + private _currentRealTimeTimer: RealTimeTimer | undefined; constructor(embedder: Embedder) { this._timers = new Map(); @@ -145,7 +152,9 @@ export class ClockController { this._replayLogOnce(); if (ticks < 0) throw new TypeError('Negative ticks are not supported'); - await this._runTo(shiftTicks(this._now.ticks, ticks)); + await this._runWithDisabledRealTimeSync(async () => { + await this._runTo(shiftTicks(this._now.ticks, ticks)); + }); } private async _runTo(to: Ticks) { @@ -169,15 +178,16 @@ export class ClockController { async pauseAt(time: number): Promise { this._replayLogOnce(); - this._innerPause(); + await this._innerPause(); const toConsume = time - this._now.time; await this._innerFastForwardTo(shiftTicks(this._now.ticks, toConsume)); return toConsume; } - private _innerPause() { + private async _innerPause() { this._realTime = undefined; - this._updateRealTimeTimer(); + await this._currentRealTimeTimer?.dispose(); + this._currentRealTimeTimer = undefined; } resume() { @@ -192,38 +202,64 @@ export class ClockController { } private _updateRealTimeTimer() { - if (!this._realTime) { - this._currentRealTimeTimer?.dispose(); - this._currentRealTimeTimer = undefined; + if (this._currentRealTimeTimer?.promise) { + // In progress, safe to return as it will call itself once promise is resolved. return; } const firstTimer = this._firstTimer(); // Either run the next timer or move time in 100ms chunks. - const callAt = Math.min(firstTimer ? firstTimer.callAt : this._now.ticks + maxTimeout, this._now.ticks + 100) as Ticks; - if (this._currentRealTimeTimer && this._currentRealTimeTimer.callAt < callAt) - return; + const nextTick = Math.min(firstTimer ? firstTimer.callAt : this._now.ticks + maxTimeout, this._now.ticks + 100) as Ticks; + const callAt = this._currentRealTimeTimer ? Math.min(this._currentRealTimeTimer.callAt, nextTick) as Ticks : nextTick; if (this._currentRealTimeTimer) { - this._currentRealTimeTimer.dispose(); + // Cancel and reschedule. + this._currentRealTimeTimer.cancel(); this._currentRealTimeTimer = undefined; } - this._currentRealTimeTimer = { + const realTimeTimer: RealTimeTimer = { callAt, - dispose: this._embedder.setTimeout(() => { - this._currentRealTimeTimer = undefined; + promise: undefined, + cancel: this._embedder.setTimeout(() => { this._syncRealTime(); // eslint-disable-next-line no-console - void this._runTo(this._now.ticks).catch(e => console.error(e)).then(() => this._updateRealTimeTimer()); + realTimeTimer.promise = this._runTo(this._now.ticks).catch(e => console.error(e)); + void realTimeTimer.promise.then(() => { + this._currentRealTimeTimer = undefined; + if (this._realTime) + this._updateRealTimeTimer(); + }); }, callAt - this._now.ticks), + dispose: async () => { + realTimeTimer.cancel(); + await realTimeTimer.promise; + } }; + + this._currentRealTimeTimer = realTimeTimer; + } + + private async _runWithDisabledRealTimeSync(fn: () => Promise) { + if (!this._realTime) { + await fn(); + return; + } + + await this._innerPause(); + try { + await fn(); + } finally { + this._innerResume(); + } } async fastForward(ticks: number) { this._replayLogOnce(); - await this._innerFastForwardTo(shiftTicks(this._now.ticks, ticks | 0)); + await this._runWithDisabledRealTimeSync(async () => { + await this._innerFastForwardTo(shiftTicks(this._now.ticks, ticks | 0)); + }); } private async _innerFastForwardTo(to: Ticks) { @@ -396,10 +432,8 @@ export class ClockController { this._advanceNow(shiftTicks(this._now.ticks, param!)); } else if (type === 'pauseAt') { isPaused = true; - this._innerPause(); this._innerSetTime(asWallTime(param!)); } else if (type === 'resume') { - this._innerResume(); isPaused = false; } else if (type === 'setFixedTime') { this._innerSetFixedTime(asWallTime(param!)); @@ -408,8 +442,13 @@ export class ClockController { } } - if (!isPaused && lastLogTime > 0) - this._advanceNow(shiftTicks(this._now.ticks, this._embedder.dateNow() - lastLogTime)); + if (!isPaused) { + if (lastLogTime > 0) + this._advanceNow(shiftTicks(this._now.ticks, this._embedder.dateNow() - lastLogTime)); + this._innerResume(); + } else { + this._realTime = undefined; + } this._log.length = 0; } diff --git a/tests/library/unit/clock.spec.ts b/tests/library/unit/clock.spec.ts index a346fe3d5..30f640e04 100644 --- a/tests/library/unit/clock.spec.ts +++ b/tests/library/unit/clock.spec.ts @@ -1384,6 +1384,41 @@ it.describe('fastForward', () => { expect(shortTimers[1].callCount).toBe(1); expect(shortTimers[2].callCount).toBe(1); }); + + it('does not rewind back in time', async ({ clock }) => { + const stub = createStub(); + const gotTime = await new Promise(done => { + clock.setTimeout(() => { + stub(clock.Date.now()); + }, 10); + clock.setTimeout(() => { + stub(clock.Date.now()); + }, 10); + clock.resume(); + setTimeout(async () => { + // Call fast-forward right after the real time sync happens, + // but before all the callbacks are processed. + await clock.fastForward(1000); + setTimeout(() => { + done(clock.Date.now()); + }, 20); + }, 10); + }); + expect(stub.callCount).toBe(2); + expect(gotTime).toBeGreaterThan(1010); + }); + + it('error does not break the clock', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(() => { + stub(clock.Date.now()); + }, 1000); + const error = await clock.fastForward(-1000).catch(e => e); + expect(error.message).toContain('Cannot fast-forward to the past'); + await clock.fastForward(2000); + expect(stub.callCount).toBe(1); + expect(stub.calledWith(2000)).toBeTruthy(); + }); }); it.describe('pauseAt', () => { @@ -1595,37 +1630,6 @@ it.describe('Intl API', () => { }); }); -it('works with concurrent runFor calls', async ({ clock }) => { - clock.setSystemTime(0); - - const log: string[] = []; - for (let t = 500; t > 0; t -= 100) { - clock.setTimeout(() => { - log.push(`${t}: ${clock.Date.now()}`); - clock.setTimeout(() => { - log.push(`${t}+0: ${clock.Date.now()}`); - }, 0); - }, t); - } - - await Promise.all([ - clock.runFor(500), - clock.runFor(600), - ]); - expect(log).toEqual([ - `100: 100`, - `100+0: 101`, - `200: 200`, - `200+0: 201`, - `300: 300`, - `300+0: 301`, - `400: 400`, - `400+0: 401`, - `500: 500`, - `500+0: 501`, - ]); -}); - it('works with slow setTimeout in busy embedder', async ({ installEx }) => { const { originals, api, clock } = installEx({ now: 0 }); await clock.pauseAt(0); From d5c299dc73f2855d26d2973643c99d2f0914b290 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 9 Oct 2025 15:24:59 -0700 Subject: [PATCH 042/250] chore: remove unwrap popout url (#37787) --- .../src/server/trace/viewer/traceViewer.ts | 2 -- packages/playwright/src/reporters/html.ts | 2 -- packages/trace-viewer/snapshot.html | 6 +++--- packages/trace-viewer/src/sw/main.ts | 5 ++--- packages/trace-viewer/src/sw/snapshotRenderer.ts | 14 +++----------- packages/trace-viewer/src/ui/snapshotTab.tsx | 6 ------ tests/library/trace-viewer.spec.ts | 4 ++-- 7 files changed, 10 insertions(+), 29 deletions(-) diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 53438cf7c..52e432e2a 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -69,8 +69,6 @@ export async function startTraceViewerServer(options?: TraceViewerServerOptions) server.routePrefix('/trace', (request, response) => { const url = new URL('http://localhost' + request.url!); const relativePath = url.pathname.slice('/trace'.length); - if (relativePath.endsWith('/stall.js')) - return true; if (relativePath.startsWith('/file')) { try { const filePath = url.searchParams.get('path')!; diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 898034c2e..153eb16a1 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -222,8 +222,6 @@ export function startHtmlReportServer(folder: string): HttpServer { return false; } } - if (relativePath.endsWith('/stall.js')) - return true; if (relativePath === '/') relativePath = '/index.html'; const absolutePath = path.join(folder, ...relativePath.split('/')); diff --git a/packages/trace-viewer/snapshot.html b/packages/trace-viewer/snapshot.html index 3f27586af..60a5d82cf 100644 --- a/packages/trace-viewer/snapshot.html +++ b/packages/trace-viewer/snapshot.html @@ -16,6 +16,7 @@ + - diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 731cb8e55..5c3a6ed85 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -15,7 +15,6 @@ */ import { splitProgress } from './progress'; -import { unwrapPopoutUrl } from './snapshotRenderer'; import { SnapshotServer } from './snapshotServer'; import { TraceModel } from './traceModel'; import { FetchTraceModelBackend, traceFileURL, ZipTraceModelBackend } from './traceModelBackends'; @@ -86,7 +85,7 @@ async function doFetch(event: FetchEvent): Promise { const isDeployedAsHttps = self.registration.scope.startsWith('https://'); if (request.url.startsWith(self.registration.scope)) { - const url = new URL(unwrapPopoutUrl(request.url)); + const url = new URL(request.url); const relativePath = url.pathname.substring(scopePath.length - 1); if (relativePath === '/ping') { await gc(); @@ -163,7 +162,7 @@ async function doFetch(event: FetchEvent): Promise { return fetch(event.request); } - const snapshotUrl = unwrapPopoutUrl(client!.url); + const snapshotUrl = client!.url; const traceUrl = new URL(snapshotUrl).searchParams.get('trace')!; const { snapshotServer } = loadedTraces.get(traceUrl) || {}; if (!snapshotServer) diff --git a/packages/trace-viewer/src/sw/snapshotRenderer.ts b/packages/trace-viewer/src/sw/snapshotRenderer.ts index a0980ac7b..2fb69eb19 100644 --- a/packages/trace-viewer/src/sw/snapshotRenderer.ts +++ b/packages/trace-viewer/src/sw/snapshotRenderer.ts @@ -255,7 +255,7 @@ declare global { } function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefined)[]) { - function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string, viewport: ViewportSize, ...targetIds: (string | undefined)[]) { + function applyPlaywrightAttributes(viewport: ViewportSize, ...targetIds: (string | undefined)[]) { const searchParams = new URLSearchParams(location.search); const shouldPopulateCanvasFromScreenshot = searchParams.has('shouldPopulateCanvasFromScreenshot'); const isUnderTest = searchParams.has('isUnderTest'); @@ -344,7 +344,7 @@ function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefine iframe.setAttribute('src', 'data:text/html,'); } else { // Retain query parameters to inherit name=, time=, pointX=, pointY= and other values from parent. - const url = new URL(unwrapPopoutUrl(window.location.href)); + const url = new URL(window.location.href); // We can be loading iframe from within iframe, reset base to be absolute. const index = url.pathname.lastIndexOf('/snapshot/'); if (index !== -1) @@ -558,7 +558,7 @@ function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefine window.addEventListener('DOMContentLoaded', onDOMContentLoaded); } - return `\n(${applyPlaywrightAttributes.toString()})(${unwrapPopoutUrl.toString()}, ${JSON.stringify(viewport)}${targetIds.map(id => `, "${id}"`).join('')})`; + return `\n(${applyPlaywrightAttributes.toString()})(${JSON.stringify(viewport)}${targetIds.map(id => `, "${id}"`).join('')})`; } @@ -632,11 +632,3 @@ function escapeURLsInStyleSheet(text: string): string { }; return text.replace(urlToEscapeRegex1, replacer).replace(urlToEscapeRegex2, replacer); } - -// /snapshot.html?r= is used for "pop out snapshot" feature. -export function unwrapPopoutUrl(url: string) { - const u = new URL(url); - if (u.pathname.endsWith('/snapshot.html')) - return u.searchParams.get('r')!; - return url; -} diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index ce0f1d689..82c68078c 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -434,12 +434,6 @@ export function extendSnapshot(traceUrl: string, snapshot: Snapshot, shouldPopul const popoutParams = new URLSearchParams(); popoutParams.set('r', snapshotUrl); popoutParams.set('trace', traceUrl); - if (snapshot.point) { - popoutParams.set('pointX', String(snapshot.point.x)); - popoutParams.set('pointY', String(snapshot.point.y)); - if (snapshot.hasInputTarget) - params.set('hasInputTarget', '1'); - } const popoutUrl = new URL(`snapshot.html?${popoutParams.toString()}`, window.location.href).toString(); return { snapshotInfoUrl, snapshotUrl, popoutUrl }; } diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index e42c0faa2..9080a5e68 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -499,7 +499,7 @@ test('should popup snapshot', async ({ page, runAndTrace, server }) => { const popupPromise = traceViewer.page.context().waitForEvent('page'); await traceViewer.page.getByTitle('Open snapshot in a new tab').click(); const popup = await popupPromise; - await expect(popup.getByText('hello äöü 🙂')).toBeVisible(); + await expect(popup.frameLocator('iframe').getByText('hello äöü 🙂')).toBeVisible(); }); test('should capture iframe with sandbox attribute', async ({ page, server, runAndTrace }) => { @@ -1421,7 +1421,7 @@ test('should open snapshot in new browser context', async ({ browser, page, runA // doesn't share sw.bundle.js const newPage = await browser.newPage(); await newPage.goto(popup.url()); - await expect(newPage.getByText('hello')).toBeVisible(); + await expect(newPage.frameLocator('iframe').getByText('hello')).toBeVisible(); await newPage.close(); }); From 7f31ab6ac8c73c681e84517be6461e5aee9684d9 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Fri, 10 Oct 2025 01:48:41 +0200 Subject: [PATCH 043/250] test(bidi): unskip some Firefox BiDi tests (#37751) --- .../moz-firefox-nightly-library.txt | 20 ------------------- .../expectations/moz-firefox-nightly-page.txt | 3 --- tests/library/browsercontext-basic.spec.ts | 4 ++-- tests/library/browsercontext-device.spec.ts | 2 +- tests/library/browsercontext-events.spec.ts | 8 ++++---- tests/library/browsercontext-pages.spec.ts | 8 ++++---- .../browsercontext-viewport-mobile.spec.ts | 2 +- tests/library/browsercontext-viewport.spec.ts | 4 ++-- tests/library/browsertype-launch.spec.ts | 4 ++-- tests/library/defaultbrowsercontext-2.spec.ts | 4 ++-- tests/library/emulation-focus.spec.ts | 4 ++-- tests/library/headful.spec.ts | 4 ++-- tests/library/resource-timing.spec.ts | 4 ++-- tests/library/screenshot.spec.ts | 8 ++++---- tests/library/trace-viewer.spec.ts | 4 ++-- tests/page/frame-goto.spec.ts | 4 ++-- tests/page/page-event-load.spec.ts | 8 ++++---- tests/page/page-fill.spec.ts | 4 ++-- tests/page/page-focus.spec.ts | 4 ++-- tests/page/page-goto.spec.ts | 4 ++-- tests/page/page-network-request.spec.ts | 4 ++-- tests/page/page-screenshot.spec.ts | 4 ++-- tests/page/page-wait-for-response.spec.ts | 4 ++-- tests/page/wheel.spec.ts | 4 ++-- 24 files changed, 50 insertions(+), 73 deletions(-) diff --git a/tests/bidi/expectations/moz-firefox-nightly-library.txt b/tests/bidi/expectations/moz-firefox-nightly-library.txt index f7a77cf6a..9b9922396 100644 --- a/tests/bidi/expectations/moz-firefox-nightly-library.txt +++ b/tests/bidi/expectations/moz-firefox-nightly-library.txt @@ -1,11 +1,5 @@ library/browsercontext-credentials.spec.ts › should fail without credentials [timeout] library/browsercontext-credentials.spec.ts › should work with setHTTPCredentials [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-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/browsertype-connect.spec.ts › launchServer › should be able to connect 20 times to a single server without warnings [timeout] @@ -36,36 +30,22 @@ library/chromium/launcher.spec.ts › should report console messages from conten library/chromium/launcher.spec.ts › should return background pages [timeout] library/chromium/launcher.spec.ts › should return background pages when recording video [timeout] library/chromium/launcher.spec.ts › should support request/response events when using backgroundPage() [timeout] -library/defaultbrowsercontext-2.spec.ts › should handle exception [timeout] -library/defaultbrowsercontext-2.spec.ts › should support geolocation and permissions options [timeout] library/fetch-proxy.spec.ts › context request should pick up proxy credentials [timeout] -library/geolocation.spec.ts › should isolate contexts [timeout] -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/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] library/inspector/cli-codegen-3.spec.ts › cli codegen › should generate frame locators (1) [timeout] library/inspector/cli-codegen-3.spec.ts › cli codegen › should generate frame locators (2) [timeout] library/inspector/cli-codegen-3.spec.ts › cli codegen › should generate frame locators (3) [timeout] library/inspector/cli-codegen-3.spec.ts › cli codegen › should generate frame locators (4) [timeout] -library/page-clock.spec.ts › popup › should run time before popup [timeout] -library/page-clock.spec.ts › popup › should tick after popup [timeout] -library/page-clock.spec.ts › popup › should tick before popup [timeout] library/page-event-crash.spec.ts › should emit crash event when page crashes [timeout] library/page-event-crash.spec.ts › should throw on any action after page crashes [timeout] library/page-event-crash.spec.ts › should cancel waitForEvent when page crashes [timeout] library/page-event-crash.spec.ts › should cancel navigation when page crashes [timeout] library/page-event-crash.spec.ts › should be able to close context when page crashes [timeout] library/popup.spec.ts › should not throw when click closes popup [timeout] -library/popup.spec.ts › should use viewport size from window features [timeout] -library/trace-viewer.spec.ts › should serve css without content-type [timeout] library/unroute-behavior.spec.ts › context.unroute should not wait for pending handlers to complete [timeout] library/unroute-behavior.spec.ts › context.unrouteAll should not wait for pending handlers to complete if behavior is ignoreErrors [timeout] library/unroute-behavior.spec.ts › context.unrouteAll should wait for pending handlers to complete [timeout] -library/video.spec.ts › screencast › saveAs should throw when no video frames [timeout] library/video.spec.ts › screencast › should be 800x450 by default [timeout] library/video.spec.ts › screencast › should be 800x600 with null viewport [timeout] library/video.spec.ts › screencast › should capture css transformation [timeout] diff --git a/tests/bidi/expectations/moz-firefox-nightly-page.txt b/tests/bidi/expectations/moz-firefox-nightly-page.txt index 680d5a6e9..e553b9a48 100644 --- a/tests/bidi/expectations/moz-firefox-nightly-page.txt +++ b/tests/bidi/expectations/moz-firefox-nightly-page.txt @@ -4,8 +4,6 @@ page/interception.spec.ts › should intercept worker requests when enabled afte page/page-add-init-script.spec.ts › init script should run only once in popup [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-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] page/page-filechooser.spec.ts › should emit event once [timeout] page/page-filechooser.spec.ts › should emit event via prepend [timeout] @@ -37,7 +35,6 @@ page/workers.spec.ts › should clear upon navigation [timeout] page/workers.spec.ts › should dispatch console messages when page has workers [timeout] page/workers.spec.ts › should emit created and destroyed events [timeout] page/workers.spec.ts › should evaluate [timeout] -page/workers.spec.ts › should report errors [timeout] page/workers.spec.ts › should report network activity [timeout] page/workers.spec.ts › should support extra http headers [timeout] page/workers.spec.ts › should support offline [timeout] \ No newline at end of file diff --git a/tests/library/browsercontext-basic.spec.ts b/tests/library/browsercontext-basic.spec.ts index 4e56b4bb4..c5e15b31b 100644 --- a/tests/library/browsercontext-basic.spec.ts +++ b/tests/library/browsercontext-basic.spec.ts @@ -255,8 +255,8 @@ it('should be able to navigate after disabling javascript', async ({ browser, se await context.close(); }); -it('should not hang on promises after disabling javascript', async ({ browserName, contextFactory }) => { - it.fixme(browserName === 'firefox'); +it('should not hang on promises after disabling javascript', async ({ browserName, contextFactory, channel }) => { + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); const context = await contextFactory({ javaScriptEnabled: false }); const page = await context.newPage(); expect(await page.evaluate(() => 1)).toBe(1); diff --git a/tests/library/browsercontext-device.spec.ts b/tests/library/browsercontext-device.spec.ts index 1dd2aca71..11602d0ca 100644 --- a/tests/library/browsercontext-device.spec.ts +++ b/tests/library/browsercontext-device.spec.ts @@ -18,7 +18,7 @@ import { browserTest as it, expect } from '../config/browserTest'; it.describe('device', () => { - it.skip(({ browserName }) => browserName === 'firefox'); + it.skip(({ browserName, channel }) => browserName === 'firefox' && !channel?.startsWith('moz-firefox')); it('should work @smoke', async ({ playwright, browser, server }) => { const iPhone = playwright.devices['iPhone 6']; diff --git a/tests/library/browsercontext-events.spec.ts b/tests/library/browsercontext-events.spec.ts index 287945966..e1cb543b7 100644 --- a/tests/library/browsercontext-events.spec.ts +++ b/tests/library/browsercontext-events.spec.ts @@ -52,8 +52,8 @@ test('console event should work in popup', async ({ page }) => { expect(message.page()).toBe(popup); }); -test('console event should work in popup 2', async ({ page, browserName }) => { - test.fixme(browserName === 'firefox', 'console message from javascript: url is not reported at all'); +test('console event should work in popup 2', async ({ page, browserName, channel }) => { + test.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'console message from javascript: url is not reported at all'); const [, message, popup] = await Promise.all([ page.evaluate(async () => { @@ -69,8 +69,8 @@ test('console event should work in popup 2', async ({ page, browserName }) => { expect(message.page()).toBe(popup); }); -test('console event should work in immediately closed popup', async ({ page, browserName }) => { - test.fixme(browserName === 'firefox', 'console message is not reported at all'); +test('console event should work in immediately closed popup', async ({ page, browserName, channel }) => { + test.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'console message is not reported at all'); const [, message, popup] = await Promise.all([ page.evaluate(async () => { diff --git a/tests/library/browsercontext-pages.spec.ts b/tests/library/browsercontext-pages.spec.ts index c66fa9b99..bef78bf41 100644 --- a/tests/library/browsercontext-pages.spec.ts +++ b/tests/library/browsercontext-pages.spec.ts @@ -79,8 +79,8 @@ it('should click the button with deviceScaleFactor set', async ({ browser, serve await context.close(); }); -it('should click the button with offset with page scale', async ({ browser, server, browserName }) => { - it.skip(browserName === 'firefox'); +it('should click the button with offset with page scale', async ({ browser, server, browserName, channel }) => { + it.skip(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); const context = await browser.newContext({ viewport: { width: 400, height: 400 }, isMobile: true }); const page = await context.newPage(); @@ -102,8 +102,8 @@ it('should click the button with offset with page scale', async ({ browser, serv await context.close(); }); -it('should return bounding box with page scale', async ({ browser, server, browserName }) => { - it.skip(browserName === 'firefox'); +it('should return bounding box with page scale', async ({ browser, server, browserName, channel }) => { + it.skip(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); const context = await browser.newContext({ viewport: { width: 400, height: 400 }, isMobile: true }); const page = await context.newPage(); diff --git a/tests/library/browsercontext-viewport-mobile.spec.ts b/tests/library/browsercontext-viewport-mobile.spec.ts index 58c85d1a0..a4b8c242c 100644 --- a/tests/library/browsercontext-viewport-mobile.spec.ts +++ b/tests/library/browsercontext-viewport-mobile.spec.ts @@ -18,7 +18,7 @@ import { browserTest as it, expect } from '../config/browserTest'; it.describe('mobile viewport', () => { - it.skip(({ browserName }) => browserName === 'firefox'); + it.skip(({ browserName, channel }) => browserName === 'firefox' && !channel?.startsWith('moz-firefox')); it('should support mobile emulation', async ({ playwright, browser, server }) => { const iPhone = playwright.devices['iPhone 6']; diff --git a/tests/library/browsercontext-viewport.spec.ts b/tests/library/browsercontext-viewport.spec.ts index 1a9a1a27b..f32626275 100644 --- a/tests/library/browsercontext-viewport.spec.ts +++ b/tests/library/browsercontext-viewport.spec.ts @@ -186,9 +186,9 @@ browserTest('should be able to get correct orientation angle on non-mobile devic await context.close(); }); -it('should set window.screen.orientation.type for mobile devices', async ({ contextFactory, browserName, server }) => { +it('should set window.screen.orientation.type for mobile devices', async ({ contextFactory, browserName, server, channel }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31151' }); - it.skip(browserName === 'firefox', 'Firefox does not support mobile emulation'); + it.skip(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'Firefox does not support mobile emulation'); const context = await contextFactory(devices['iPhone 14']); const page = await context.newPage(); await page.goto(server.PREFIX + '/index.html'); diff --git a/tests/library/browsertype-launch.spec.ts b/tests/library/browsertype-launch.spec.ts index a112bb135..36cfa9c53 100644 --- a/tests/library/browsertype-launch.spec.ts +++ b/tests/library/browsertype-launch.spec.ts @@ -53,8 +53,8 @@ it('should throw if port option is passed for persistent context', async ({ brow expect(error!.message).toContain('Cannot specify a port without launching as a server.'); }); -it('should throw if page argument is passed', async ({ browserType, browserName }) => { - it.skip(browserName === 'firefox'); +it('should throw if page argument is passed', async ({ browserType, browserName, channel }) => { + it.skip(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); let waitError: Error | undefined; await browserType.launch({ args: ['http://example.com'] }).catch(e => waitError = e); diff --git a/tests/library/defaultbrowsercontext-2.spec.ts b/tests/library/defaultbrowsercontext-2.spec.ts index 6775d26b6..16db4880d 100644 --- a/tests/library/defaultbrowsercontext-2.spec.ts +++ b/tests/library/defaultbrowsercontext-2.spec.ts @@ -148,8 +148,8 @@ it('should have default URL when launching browser', async ({ launchPersistent } expect(urls).toEqual(['about:blank']); }); -it('should throw if page argument is passed', async ({ browserType, server, createUserDataDir, browserName }) => { - it.skip(browserName === 'firefox'); +it('should throw if page argument is passed', async ({ browserType, server, createUserDataDir, browserName, channel }) => { + it.skip(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); const options = { args: [server.EMPTY_PAGE] }; const error = await browserType.launchPersistentContext(await createUserDataDir(), options).catch(e => e); diff --git a/tests/library/emulation-focus.spec.ts b/tests/library/emulation-focus.spec.ts index 5af6cc7d1..1b3a89f03 100644 --- a/tests/library/emulation-focus.spec.ts +++ b/tests/library/emulation-focus.spec.ts @@ -199,10 +199,10 @@ browserTest('should not fire blur events when interacting with more than one pag expect(await page2.evaluate(() => !!window['gotBlur'])).toBe(false); }); -browserTest('should trigger hover state concurrently', async ({ browserType, browserName, headless }) => { +browserTest('should trigger hover state concurrently', async ({ browserType, browserName, headless, channel }) => { browserTest.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/27969' }); browserTest.skip(!headless, 'headed messes up with hover'); - browserTest.fixme(browserName === 'firefox'); + browserTest.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); const browser1 = await browserType.launch(); const context1 = await browser1.newContext(); diff --git a/tests/library/headful.spec.ts b/tests/library/headful.spec.ts index 06d69b2df..742334a77 100644 --- a/tests/library/headful.spec.ts +++ b/tests/library/headful.spec.ts @@ -288,9 +288,9 @@ it('should click bottom row w/ infobar in OOPIF', async ({ browserName, launchPe expect(await page.frames()[1].evaluate('window._clicked')).toBe(true); }); -it('headless and headful should use same default fonts', async ({ page, browserName, browserType }) => { +it('headless and headful should use same default fonts', async ({ page, browserName, browserType, channel }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/11177' }); - it.skip(browserName === 'firefox', 'Text is misaligned in headed vs headless'); + it.skip(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'Text is misaligned in headed vs headless'); const genericFontFamilies = [ 'standard', diff --git a/tests/library/resource-timing.spec.ts b/tests/library/resource-timing.spec.ts index 360dc5edb..229958cbe 100644 --- a/tests/library/resource-timing.spec.ts +++ b/tests/library/resource-timing.spec.ts @@ -100,9 +100,9 @@ it('should work for redirect', async ({ contextFactory, browserName, server }) = await context.close(); }); -it('should work when serving from memory cache', async ({ contextFactory, server, browserName }) => { +it('should work when serving from memory cache', async ({ contextFactory, server, browserName, channel }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright-java/issues/1080' }); - it.fixme(browserName === 'firefox', 'Response event is not fired in Firefox'); + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'Response event is not fired in Firefox'); server.setRoute('/one-style.css', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/css', diff --git a/tests/library/screenshot.spec.ts b/tests/library/screenshot.spec.ts index 98d8c96b6..0deace018 100644 --- a/tests/library/screenshot.spec.ts +++ b/tests/library/screenshot.spec.ts @@ -202,8 +202,8 @@ browserTest.describe('page screenshot', () => { browserTest.describe('element screenshot', () => { browserTest.skip(({ browserName, headless }) => browserName === 'firefox' && !headless); - browserTest('element screenshot should work with a mobile viewport', async ({ browser, server, browserName }) => { - browserTest.skip(browserName === 'firefox'); + browserTest('element screenshot should work with a mobile viewport', async ({ browser, server, browserName, channel }) => { + browserTest.skip(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); const context = await browser.newContext({ viewport: { width: 320, height: 480 }, isMobile: true }); const page = await context.newPage(); @@ -215,8 +215,8 @@ browserTest.describe('element screenshot', () => { await context.close(); }); - browserTest('element screenshot should work with device scale factor', async ({ browser, server, browserName, isMac }) => { - browserTest.skip(browserName === 'firefox'); + browserTest('element screenshot should work with device scale factor', async ({ browser, server, browserName, channel }) => { + browserTest.skip(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); const context = await browser.newContext({ viewport: { width: 320, height: 480 }, deviceScaleFactor: 2 }); const page = await context.newPage(); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 9080a5e68..2016cc16c 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -1642,8 +1642,8 @@ test('canvas clipping in iframe', async ({ runAndTrace, page, server }) => { await expect(canvas).toHaveAttribute('title', 'Canvas contents are displayed on a best-effort basis based on viewport screenshots taken during test execution.'); }); -test('should show only one pointer with multilevel iframes', async ({ page, runAndTrace, server, browserName }) => { - test.fixme(browserName === 'firefox', 'Elements in iframe are not marked'); +test('should show only one pointer with multilevel iframes', async ({ page, runAndTrace, server, browserName, channel }) => { + test.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'Elements in iframe are not marked'); server.setRoute('/level-0.html', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); diff --git a/tests/page/frame-goto.spec.ts b/tests/page/frame-goto.spec.ts index 6603282bb..e63cd363b 100644 --- a/tests/page/frame-goto.spec.ts +++ b/tests/page/frame-goto.spec.ts @@ -43,9 +43,9 @@ it('should reject when frame detaches', async ({ page, server, browserName }) => expect(error.message.toLowerCase()).toContain('frame was detached'); }); -it('should continue after client redirect', async ({ page, server, isAndroid, browserName }) => { +it('should continue after client redirect', async ({ page, server, isAndroid, browserName, channel }) => { it.fixme(isAndroid); - it.fixme(browserName === 'firefox', 'script.js is requested before navigationCommitted arrives'); + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'script.js is requested before navigationCommitted arrives'); server.setRoute('/frames/script.js', () => {}); const url = server.PREFIX + '/frames/child-redirect.html'; diff --git a/tests/page/page-event-load.spec.ts b/tests/page/page-event-load.spec.ts index ab81e9fc0..6bb0902df 100644 --- a/tests/page/page-event-load.spec.ts +++ b/tests/page/page-event-load.spec.ts @@ -17,9 +17,9 @@ import { test as it, expect } from './pageTest'; -it('should fire once', async ({ page, server, browserName }) => { +it('should fire once', async ({ page, server, browserName, channel }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/15086' }); - it.fixme(browserName === 'firefox', 'Firefox sometimes double fires.'); + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'Firefox sometimes double fires.'); let count = 0; page.on('load', () => count++); @@ -28,9 +28,9 @@ it('should fire once', async ({ page, server, browserName }) => { expect(count).toBe(1); }); -it('should fire once with iframe navigation', async ({ page, server, browserName }) => { +it('should fire once with iframe navigation', async ({ page, server, browserName, channel }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/15086' }); - it.fixme(browserName === 'firefox', 'Firefox sometimes double fires.'); + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'Firefox sometimes double fires.'); let requestCount = 0; server.setRoute('/tracker', (_, res) => { diff --git a/tests/page/page-fill.spec.ts b/tests/page/page-fill.spec.ts index 32dc69f55..24208962d 100644 --- a/tests/page/page-fill.spec.ts +++ b/tests/page/page-fill.spec.ts @@ -201,8 +201,8 @@ it('should fill contenteditable', async ({ page, server }) => { expect(await page.$eval('div[contenteditable]', div => div.textContent)).toBe('some value'); }); -it('should fill contenteditable with new lines', async ({ page, server, browserName }) => { - it.fixme(browserName === 'firefox', 'Firefox does not handle new lines in contenteditable'); +it('should fill contenteditable with new lines', async ({ page, server, browserName, channel }) => { + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'Firefox does not handle new lines in contenteditable'); await page.goto(server.EMPTY_PAGE); await page.setContent(`
`); diff --git a/tests/page/page-focus.spec.ts b/tests/page/page-focus.spec.ts index d2db5cf6b..e67fffab9 100644 --- a/tests/page/page-focus.spec.ts +++ b/tests/page/page-focus.spec.ts @@ -16,8 +16,8 @@ import { test as it, expect } from './pageTest'; -it('should work @smoke', async function({ page, browserName }) { - it.skip(browserName === 'firefox'); +it('should work @smoke', async function({ page, browserName, channel }) { + it.skip(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); await page.setContent(`
`); expect(await page.evaluate(() => document.activeElement.nodeName)).toBe('BODY'); diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index c70a3a4e7..dd0c53675 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -794,8 +794,8 @@ it('should properly wait for load', async ({ page, server, browserName }) => { ]); }); -it('should not resolve goto upon window.stop()', async ({ browserName, page, server }) => { - it.fixme(browserName === 'firefox', 'load/domcontentloaded events are flaky'); +it('should not resolve goto upon window.stop()', async ({ browserName, page, server, channel }) => { + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'load/domcontentloaded events are flaky'); it.skip(process.env.PW_CLOCK === 'frozen'); let response; diff --git a/tests/page/page-network-request.spec.ts b/tests/page/page-network-request.spec.ts index f6ddb0b49..480a60afe 100644 --- a/tests/page/page-network-request.spec.ts +++ b/tests/page/page-network-request.spec.ts @@ -498,10 +498,10 @@ it('should not allow to access frame on popup main request', async ({ page, serv await clicked; }); -it('page.reload return 304 status code', async ({ page, server, browserName }) => { +it('page.reload return 304 status code', async ({ page, server, browserName, channel }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28779' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29441' }); - it.fixme(browserName === 'firefox', 'Does not send second request'); + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'Does not send second request'); let requestNumber = 0; server.setRoute('/test.html', (req, res) => { ++requestNumber; diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index 1be6ce3e0..b77989502 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -324,8 +324,8 @@ it.describe('page screenshot', () => { } }); - it('should work for webgl', async ({ page, server, browserName, platform }) => { - it.fixme(browserName === 'firefox'); + it('should work for webgl', async ({ page, server, browserName, platform, channel }) => { + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); it.fixme(browserName === 'chromium' && platform === 'darwin' && os.arch() === 'arm64', 'SwiftShader is not available on macOS-arm64 - https://github.com/microsoft/playwright/issues/28216'); it.skip(browserName === 'webkit' && platform === 'darwin' && os.arch() === 'x64', 'Modernizr uses WebGL which is not available on Intel macOS - https://bugs.webkit.org/show_bug.cgi?id=278277'); diff --git a/tests/page/page-wait-for-response.spec.ts b/tests/page/page-wait-for-response.spec.ts index 75c06131a..162f4e529 100644 --- a/tests/page/page-wait-for-response.spec.ts +++ b/tests/page/page-wait-for-response.spec.ts @@ -117,9 +117,9 @@ it('should work with no timeout', async ({ page, server }) => { expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); }); -it('should work with re-rendered cached IMG elements', async ({ page, server, browserName }) => { +it('should work with re-rendered cached IMG elements', async ({ page, server, browserName, channel }) => { it.fixme(browserName === 'webkit'); - it.fixme(browserName === 'firefox'); + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); await page.goto(server.EMPTY_PAGE); await page.setContent(``); await page.$eval('img', img => img.remove()); diff --git a/tests/page/wheel.spec.ts b/tests/page/wheel.spec.ts index 7a1fccc2a..e33ad675d 100644 --- a/tests/page/wheel.spec.ts +++ b/tests/page/wheel.spec.ts @@ -67,9 +67,9 @@ it('should dispatch wheel events @smoke', async ({ page, server }) => { }); }); -it('should dispatch wheel events after context menu was opened', async ({ page, browserName, isWindows }) => { +it('should dispatch wheel events after context menu was opened', async ({ page, browserName, isWindows, channel }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/20823' }); - it.fixme(browserName === 'firefox'); + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox')); it.skip(browserName === 'chromium' && isWindows, 'context menu support is best-effort for Linux and MacOS'); await page.setContent(`
`); From ea3fab65d84f2d3cbf78bbc4d3aa1eceb59e1d67 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 9 Oct 2025 19:08:55 -0700 Subject: [PATCH 044/250] chore(trace): pass trace param to sha1 requests (#37788) --- packages/trace-viewer/src/sw/main.ts | 177 ++++++++++-------- .../trace-viewer/src/ui/attachmentsTab.tsx | 45 +++-- packages/trace-viewer/src/ui/errorsTab.tsx | 13 +- packages/trace-viewer/src/ui/filmStrip.tsx | 16 +- .../src/ui/liveWorkbenchLoader.tsx | 5 +- packages/trace-viewer/src/ui/modelUtil.ts | 8 +- .../src/ui/networkResourceDetails.tsx | 13 +- packages/trace-viewer/src/ui/sourceTab.tsx | 8 +- packages/trace-viewer/src/ui/timeline.tsx | 2 +- .../trace-viewer/src/ui/traceModelContext.tsx | 20 ++ .../trace-viewer/src/ui/uiModeTraceView.tsx | 26 +-- packages/trace-viewer/src/ui/workbench.tsx | 9 +- .../trace-viewer/src/ui/workbenchLoader.tsx | 5 +- 13 files changed, 210 insertions(+), 137 deletions(-) create mode 100644 packages/trace-viewer/src/ui/traceModelContext.tsx diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 5c3a6ed85..79b928d06 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -20,7 +20,27 @@ import { TraceModel } from './traceModel'; import { FetchTraceModelBackend, traceFileURL, ZipTraceModelBackend } from './traceModelBackends'; import { TraceVersionError } from './traceModernizer'; -// @ts-ignore +type Client = { + id: string; + url: string; + postMessage(message: any): void; +}; + +type ServiceWorkerGlobalScope = { + addEventListener(event: 'install', listener: (event: any) => void): void; + addEventListener(event: 'activate', listener: (event: any) => void): void; + addEventListener(event: 'fetch', listener: (event: any) => void): void; + registration: { + scope: string; + }; + clients: { + claim(): Promise; + get(id: string): Promise; + matchAll(): Promise; + }; + skipWaiting(): Promise; +}; + declare const self: ServiceWorkerGlobalScope; self.addEventListener('install', function(event: any) { @@ -31,19 +51,27 @@ self.addEventListener('activate', function(event: any) { event.waitUntil(self.clients.claim()); }); +type LoadedTrace = { + traceModel: TraceModel; + snapshotServer: SnapshotServer; +}; + const scopePath = new URL(self.registration.scope).pathname; -const loadedTraces = new Map(); +const loadedTraces = new Map(); const clientIdToTraceUrls = new Map(); +const isDeployedAsHttps = self.registration.scope.startsWith('https://'); -async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, progress: (done: number, total: number) => undefined): Promise { - const clientId = client?.id ?? ''; +async function loadTrace(traceUrl: string, traceFileName: string | null, client: Client): Promise { + const clientId = client.id; clientIdToTraceUrls.set(clientId, traceUrl); await gc(); const traceModel = new TraceModel(); try { // Allow 10% to hop from sw to page. - const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]); + const [fetchProgress, unzipProgress] = splitProgress((done: number, total: number) => { + client.postMessage({ method: 'progress', params: { done, total } }); + }, [0.5, 0.4, 0.1]); const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress); await traceModel.load(backend, unzipProgress); } catch (error: any) { @@ -75,93 +103,89 @@ async function doFetch(event: FetchEvent): Promise { } const request = event.request; - const client = await self.clients.get(event.clientId); + const client = await self.clients.get(event.clientId) as Client | undefined; // When trace viewer is deployed over https, we will force upgrade // insecure http subresources to https. Otherwise, these will fail // to load inside our https snapshots. // In this case, we also match http resources from the archive by // the https urls. - const isDeployedAsHttps = self.registration.scope.startsWith('https://'); - - if (request.url.startsWith(self.registration.scope)) { - const url = new URL(request.url); - const relativePath = url.pathname.substring(scopePath.length - 1); - if (relativePath === '/ping') { - await gc(); - return new Response(null, { status: 200 }); - } + const url = new URL(request.url); - const traceUrl = url.searchParams.get('trace'); + let relativePath: string | undefined; + if (request.url.startsWith(self.registration.scope)) + relativePath = url.pathname.substring(scopePath.length - 1); - if (relativePath === '/contexts') { - try { - const traceModel = await loadTrace(traceUrl!, url.searchParams.get('traceFileName'), client, (done: number, total: number) => { - client.postMessage({ method: 'progress', params: { done, total } }); - }); - return new Response(JSON.stringify(traceModel!.contextEntries), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); - } catch (error: any) { - return new Response(JSON.stringify({ error: error?.message }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); - } - } + if (relativePath === '/ping') + return new Response(null, { status: 200 }); - if (relativePath.startsWith('/snapshotInfo/')) { - const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; - if (!snapshotServer) - return new Response(null, { status: 404 }); - const pageOrFrameId = relativePath.substring('/snapshotInfo/'.length); - return snapshotServer.serveSnapshotInfo(pageOrFrameId, url.searchParams); + if (relativePath === '/contexts') { + const traceUrl = url.searchParams.get('trace'); + if (!client || !traceUrl) { + return new Response('Something went wrong, trace is requested as a part of the navigation', { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); } - if (relativePath.startsWith('/snapshot/')) { - const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; - if (!snapshotServer) - return new Response(null, { status: 404 }); - const pageOrFrameId = relativePath.substring('/snapshot/'.length); - const response = snapshotServer.serveSnapshot(pageOrFrameId, url.searchParams, url.href); - if (isDeployedAsHttps) - response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests'); - return response; + try { + const traceModel = await loadTrace(traceUrl, url.searchParams.get('traceFileName'), client); + return new Response(JSON.stringify(traceModel.contextEntries), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error: any) { + return new Response(JSON.stringify({ error: error?.message }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); } + } - if (relativePath.startsWith('/closest-screenshot/')) { - const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; - if (!snapshotServer) - return new Response(null, { status: 404 }); - const pageOrFrameId = relativePath.substring('/closest-screenshot/'.length); - return snapshotServer.serveClosestScreenshot(pageOrFrameId, url.searchParams); - } + if (relativePath?.startsWith('/snapshotInfo/')) { + const { snapshotServer } = loadedTrace(url); + if (!snapshotServer) + return new Response(null, { status: 404 }); + const pageOrFrameId = relativePath.substring('/snapshotInfo/'.length); + return snapshotServer.serveSnapshotInfo(pageOrFrameId, url.searchParams); + } - if (relativePath.startsWith('/sha1/')) { - // Sha1 for sources is based on the file path, can't load it of a random model. - const sha1 = relativePath.slice('/sha1/'.length); - for (const trace of loadedTraces.values()) { - const blob = await trace.traceModel.resourceForSha1(sha1); - if (blob) - return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) }); - } + if (relativePath?.startsWith('/snapshot/')) { + const { snapshotServer } = loadedTrace(url); + if (!snapshotServer) return new Response(null, { status: 404 }); - } + const pageOrFrameId = relativePath.substring('/snapshot/'.length); + const response = snapshotServer.serveSnapshot(pageOrFrameId, url.searchParams, url.href); + if (isDeployedAsHttps) + response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests'); + return response; + } - if (relativePath.startsWith('/file/')) { - const path = url.searchParams.get('path')!; - const fileURL = traceFileURL(path); - const response = await fetch(fileURL); - if (response.status === 404) - return new Response(null, { status: 404 }); - return response; - } + if (relativePath?.startsWith('/closest-screenshot/')) { + const { snapshotServer } = loadedTrace(url); + if (!snapshotServer) + return new Response(null, { status: 404 }); + const pageOrFrameId = relativePath.substring('/closest-screenshot/'.length); + return snapshotServer.serveClosestScreenshot(pageOrFrameId, url.searchParams); + } - // Fallback for static assets. - return fetch(event.request); + if (relativePath?.startsWith('/sha1/')) { + const { traceModel } = loadedTrace(url); + const blob = await traceModel?.resourceForSha1(relativePath.slice('/sha1/'.length)); + if (blob) + return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) }); + return new Response(null, { status: 404 }); + } + + if (relativePath?.startsWith('/file/')) { + const path = url.searchParams.get('path')!; + return await fetch(traceFileURL(path)); } + // Fallback for static assets. + if (relativePath) + return fetch(event.request); + const snapshotUrl = client!.url; const traceUrl = new URL(snapshotUrl).searchParams.get('trace')!; const { snapshotServer } = loadedTraces.get(traceUrl) || {}; @@ -186,6 +210,13 @@ function downloadHeaders(searchParams: URLSearchParams): Headers | undefined { return headers; } +const emptyLoadedTrace = { traceModel: undefined, snapshotServer: undefined }; + +function loadedTrace(url: URL): LoadedTrace | { traceModel: undefined, snapshotServer: undefined } { + const traceUrl = url.searchParams.get('trace'); + return traceUrl ? loadedTraces.get(traceUrl) ?? emptyLoadedTrace : emptyLoadedTrace; +} + async function gc() { const clients = await self.clients.matchAll(); const usedTraces = new Set(); diff --git a/packages/trace-viewer/src/ui/attachmentsTab.tsx b/packages/trace-viewer/src/ui/attachmentsTab.tsx index fbe67b6d9..3a9322ecd 100644 --- a/packages/trace-viewer/src/ui/attachmentsTab.tsx +++ b/packages/trace-viewer/src/ui/attachmentsTab.tsx @@ -17,14 +17,16 @@ import * as React from 'react'; import './attachmentsTab.css'; import { ImageDiffView } from '@web/shared/imageDiffView'; -import type { Attachment, MultiTraceModel } from './modelUtil'; import { PlaceholderPanel } from './placeholderPanel'; -import type { AfterActionTraceEventAttachment } from '@trace/trace'; import { CodeMirrorWrapper, lineHeight } from '@web/components/codeMirrorWrapper'; import { isTextualMimeType } from '@isomorphic/mimeType'; import { Expandable } from '@web/components/expandable'; import { linkifyText } from '@web/renderUtils'; import { clsx, useFlash } from '@web/uiUtils'; +import { TraceModelContext } from './traceModelContext'; + +import type { Attachment, MultiTraceModel } from './modelUtil'; +import type { AfterActionTraceEventAttachment } from '@trace/trace'; type ExpandableAttachmentProps = { attachment: Attachment; @@ -32,6 +34,7 @@ type ExpandableAttachmentProps = { }; const ExpandableAttachment: React.FunctionComponent = ({ attachment, reveal }) => { + const model = React.useContext(TraceModelContext); const [expanded, setExpanded] = React.useState(false); const [attachmentText, setAttachmentText] = React.useState(null); const [placeholder, setPlaceholder] = React.useState(null); @@ -51,14 +54,14 @@ const ExpandableAttachment: React.FunctionComponent = React.useEffect(() => { if (expanded && attachmentText === null && placeholder === null) { setPlaceholder('Loading ...'); - fetch(attachmentURL(attachment)).then(response => response.text()).then(text => { + fetch(attachmentURL(model, attachment)).then(response => response.text()).then(text => { setAttachmentText(text); setPlaceholder(null); }).catch(e => { setPlaceholder('Failed to load: ' + e.message); }); } - }, [expanded, attachmentText, placeholder, attachment]); + }, [model, expanded, attachmentText, placeholder, attachment]); const snippetHeight = React.useMemo(() => { const lineCount = attachmentText ? attachmentText.split('\n').length : 0; @@ -67,7 +70,7 @@ const ExpandableAttachment: React.FunctionComponent = const title = {linkifyText(attachment.name)} - {hasContent &&
download} + {hasContent && download} ; if (!isTextAttachment || !hasContent) @@ -91,9 +94,9 @@ const ExpandableAttachment: React.FunctionComponent = }; export const AttachmentsTab: React.FunctionComponent<{ - model: MultiTraceModel | undefined, revealedAttachment?: [AfterActionTraceEventAttachment, number], -}> = ({ model, revealedAttachment }) => { +}> = ({ revealedAttachment }) => { + const model = React.useContext(TraceModelContext); const { diffMap, screenshots, attachments } = React.useMemo(() => { const attachments = new Set(model?.visibleAttachments ?? []); const screenshots = new Set(); @@ -127,15 +130,15 @@ export const AttachmentsTab: React.FunctionComponent<{ {expected && actual &&
Image diff
} {expected && actual && } ; })} {screenshots.size ?
Screenshots
: undefined} {[...screenshots.values()].map((a, i) => { - const url = attachmentURL(a); + const url = attachmentURL(model, a); return
@@ -157,21 +160,17 @@ function isEqualAttachment(a: Attachment, b: AfterActionTraceEventAttachment): b return a.name === b.name && a.path === b.path && a.sha1 === b.sha1; } -export function attachmentURL(attachment: Attachment, queryParams: Record = {}) { - const params = new URLSearchParams(queryParams); - if (attachment.sha1) { - params.set('trace', attachment.traceUrl); - return 'sha1/' + attachment.sha1 + '?' + params.toString(); - } - params.set('path', attachment.path!); - return 'file?' + params.toString(); +export function attachmentURL(model: MultiTraceModel | undefined, attachment: Attachment) { + if (model && attachment.sha1) + return model.createRelativeUrl(`sha1/${attachment.sha1}`) ; + return `file?path=${encodeURIComponent(attachment.path!)}`; } -function downloadURL(attachment: Attachment) { - const params = { dn: attachment.name } as Record; +function downloadURL(model: MultiTraceModel | undefined, attachment: Attachment) { + let suffix = attachment.contentType ? `&dn=${encodeURIComponent(attachment.name)}` : ''; if (attachment.contentType) - params.dct = attachment.contentType; - return attachmentURL(attachment, params); + suffix += `&dct=${encodeURIComponent(attachment.contentType)}`; + return attachmentURL(model, attachment) + suffix; } function attachmentKey(attachment: Attachment, index: number) { diff --git a/packages/trace-viewer/src/ui/errorsTab.tsx b/packages/trace-viewer/src/ui/errorsTab.tsx index 3b67545af..717714a82 100644 --- a/packages/trace-viewer/src/ui/errorsTab.tsx +++ b/packages/trace-viewer/src/ui/errorsTab.tsx @@ -27,6 +27,7 @@ import { copyPrompt, stripAnsiEscapes } from '@web/shared/prompts'; import { MetadataWithCommitInfo } from '@testIsomorphic/types'; import { calculateSha1 } from './sourceTab'; import type { StackFrame } from '@protocol/channels'; +import { TraceModelContext } from './traceModelContext'; const CopyPromptButton: React.FC<{ prompt: string }> = ({ prompt }) => { return ( @@ -85,17 +86,17 @@ function ErrorView({ message, error, sdkLanguage, revealInSource }: { message: s export const ErrorsTab: React.FunctionComponent<{ errorsModel: ErrorsTabModel, - model?: modelUtil.MultiTraceModel, wallTime: number, sdkLanguage: Language, revealInSource: (error: modelUtil.ErrorDescription) => void, testRunMetadata: MetadataWithCommitInfo | undefined, -}> = ({ errorsModel, model, sdkLanguage, revealInSource, wallTime, testRunMetadata }) => { +}> = ({ errorsModel, sdkLanguage, revealInSource, wallTime, testRunMetadata }) => { + const model = React.useContext(TraceModelContext); const errorContext = useAsyncMemo(async () => { const attachment = model?.attachments.find(a => a.name === 'error-context'); if (!attachment) return; - return await fetch(attachmentURL(attachment)).then(r => r.text()); + return await fetch(attachmentURL(model, attachment)).then(r => r.text()); }, [model], undefined); const buildCodeFrame = React.useCallback(async (error: modelUtil.ErrorDescription) => { @@ -103,8 +104,8 @@ export const ErrorsTab: React.FunctionComponent<{ if (!location) return; - let response = await fetch(`sha1/src@${await calculateSha1(location.file)}.txt`); - if (response.status === 404) + let response = model ? await fetch(model.createRelativeUrl(`sha1/src@${await calculateSha1(location.file)}.txt`)) : undefined; + if (!response || response.status === 404) response = await fetch(`file?path=${encodeURIComponent(location.file)}`); if (response.status >= 400) return; @@ -118,7 +119,7 @@ export const ErrorsTab: React.FunctionComponent<{ linesAbove: 100, linesBelow: 100, }); - }, []); + }, [model]); const prompt = useAsyncMemo( () => copyPrompt( diff --git a/packages/trace-viewer/src/ui/filmStrip.tsx b/packages/trace-viewer/src/ui/filmStrip.tsx index 19ed90505..73b8b1410 100644 --- a/packages/trace-viewer/src/ui/filmStrip.tsx +++ b/packages/trace-viewer/src/ui/filmStrip.tsx @@ -19,9 +19,10 @@ import type { Boundaries, Size } from './geometry'; import * as React from 'react'; import { useMeasure, upperBound } from '@web/uiUtils'; import type { PageEntry } from '../types/entries'; -import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil'; +import type { ActionTraceEventInContext } from './modelUtil'; import { renderAction } from './actionList'; import type { Language } from '@isomorphic/locatorGenerators'; +import { TraceModelContext } from './traceModelContext'; export type FilmStripPreviewPoint = { x: number; @@ -35,10 +36,10 @@ const frameMargin = 2.5; const rowHeight = tileSize.height + frameMargin * 2; export const FilmStrip: React.FunctionComponent<{ - model?: MultiTraceModel, boundaries: Boundaries, previewPoint?: FilmStripPreviewPoint, -}> = ({ model, boundaries, previewPoint }) => { +}> = ({ boundaries, previewPoint }) => { + const model = React.useContext(TraceModelContext); const [measure, ref] = useMeasure(); const lanesRef = React.useRef(null); @@ -70,14 +71,14 @@ export const FilmStrip: React.FunctionComponent<{ key={index} /> : null) }
- {previewPoint?.x !== undefined && + {model && previewPoint?.x !== undefined &&
{previewPoint.action &&
{renderAction(previewPoint.action, previewPoint)}
} {previewImage && previewSize &&
- +
}
} @@ -89,6 +90,7 @@ const FilmStripLane: React.FunctionComponent<{ page: PageEntry, width: number, }> = ({ boundaries, page, width }) => { + const model = React.useContext(TraceModelContext); const viewportSize = { width: 0, height: 0 }; const screencastFrames = page.screencastFrames; for (const frame of screencastFrames) { @@ -113,7 +115,7 @@ const FilmStripLane: React.FunctionComponent<{ frames.push(
= ({ traceJson }) => { const [model, setModel] = React.useState(undefined); @@ -48,7 +49,9 @@ export const LiveWorkbenchLoader: React.FC<{ traceJson: string }> = ({ traceJson }; }, [traceJson, counter]); - return ; + return + + ; }; async function loadSingleTraceFile(traceJson: string): Promise { diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index 4db70cd4c..c78bee156 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -59,7 +59,7 @@ export type ErrorDescription = { message: string; }; -export type Attachment = trace.AfterActionTraceEventAttachment & { traceUrl: string }; +export type Attachment = trace.AfterActionTraceEventAttachment; export class MultiTraceModel { readonly startTime: number; @@ -128,6 +128,12 @@ export class MultiTraceModel { } } + createRelativeUrl(path: string) { + const url = new URL('http://localhost/' + path); + url.searchParams.set('trace', this.traceUrl); + return url.toString().substring('http://localhost/'.length); + } + failedAction() { // This find innermost action for nested ones. return this.actions.findLast(a => a.error); diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index 5c87cb8d7..b0a257b1e 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -26,6 +26,7 @@ import { getAPIRequestCodeGen } from './codegen'; import type { Language } from '@isomorphic/locatorGenerators'; import { msToString, useAsyncMemo } from '@web/uiUtils'; import type { Entry } from '@trace/har'; +import { TraceModelContext } from './traceModelContext'; type RequestBody = { text: string, mimeType?: string } | null; @@ -37,13 +38,14 @@ export const NetworkResourceDetails: React.FunctionComponent<{ onClose: () => void; }> = ({ resource, sdkLanguage, startTimeOffset, onClose }) => { const [selectedTab, setSelectedTab] = React.useState('request'); + const model = React.useContext(TraceModelContext); const requestBody = useAsyncMemo(async () => { - if (resource.request.postData) { + if (model && resource.request.postData) { const requestContentTypeHeader = resource.request.headers.find(q => q.name.toLowerCase() === 'content-type'); const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : ''; if (resource.request.postData._sha1) { - const response = await fetch(`sha1/${resource.request.postData._sha1}`); + const response = await fetch(model.createRelativeUrl(`sha1/${resource.request.postData._sha1}`)); return { text: formatBody(await response.text(), requestContentType), mimeType: requestContentType }; } else { return { text: formatBody(resource.request.postData.text, requestContentType), mimeType: requestContentType }; @@ -145,14 +147,15 @@ const ResponseTab: React.FunctionComponent<{ const BodyTab: React.FunctionComponent<{ resource: ResourceSnapshot; }> = ({ resource }) => { + const model = React.useContext(TraceModelContext); const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string, font?: BufferSource } | null>(null); React.useEffect(() => { const readResources = async () => { - if (resource.response.content._sha1) { + if (model && resource.response.content._sha1) { const useBase64 = resource.response.content.mimeType.includes('image'); const isFont = resource.response.content.mimeType.includes('font'); - const response = await fetch(`sha1/${resource.response.content._sha1}`); + const response = await fetch(model.createRelativeUrl(`sha1/${resource.response.content._sha1}`)); if (useBase64) { const blob = await response.blob(); const reader = new FileReader(); @@ -172,7 +175,7 @@ const BodyTab: React.FunctionComponent<{ }; readResources(); - }, [resource]); + }, [resource, model]); return
{!resource.response.content._sha1 &&
Response body is not available for this request.
} diff --git a/packages/trace-viewer/src/ui/sourceTab.tsx b/packages/trace-viewer/src/ui/sourceTab.tsx index 4b03fd8c1..11c7c571f 100644 --- a/packages/trace-viewer/src/ui/sourceTab.tsx +++ b/packages/trace-viewer/src/ui/sourceTab.tsx @@ -26,8 +26,10 @@ import type { StackFrame } from '@protocol/channels'; import { CopyToClipboard } from './copyToClipboard'; import { ToolbarButton } from '@web/components/toolbarButton'; import { Toolbar } from '@web/components/toolbar'; +import { TraceModelContext } from './traceModelContext'; function useSources(stack: StackFrame[] | undefined, selectedFrame: number, sources: Map, rootDir?: string, fallbackLocation?: SourceLocation) { + const model = React.useContext(TraceModelContext); return useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[], location?: SourceLocation }>(async () => { const actionLocation = stack?.[selectedFrame]; const location = actionLocation?.file ? actionLocation : fallbackLocation; @@ -53,8 +55,8 @@ function useSources(stack: StackFrame[] | undefined, selectedFrame: number, sour } else if (source.content === undefined || (location === fallbackLocation)) { const sha1 = await calculateSha1(file); try { - let response = await fetch(`sha1/src@${sha1}.txt`); - if (response.status === 404) + let response = model ? await fetch(model.createRelativeUrl(`sha1/src@${sha1}.txt`)) : undefined; + if (!response || response.status === 404) response = await fetch(`file?path=${encodeURIComponent(file)}`); if (response.status >= 400) source.content = ``; @@ -64,7 +66,7 @@ function useSources(stack: StackFrame[] | undefined, selectedFrame: number, sour source.content = ``; } } - return { source, highlight, targetLine, fileName, location }; + return { model, source, highlight, targetLine, fileName, location }; }, [stack, selectedFrame, rootDir, fallbackLocation], { source: { errors: [], content: 'Loading\u2026' }, highlight: [] }); } diff --git a/packages/trace-viewer/src/ui/timeline.tsx b/packages/trace-viewer/src/ui/timeline.tsx index 05da35e44..6b3344d99 100644 --- a/packages/trace-viewer/src/ui/timeline.tsx +++ b/packages/trace-viewer/src/ui/timeline.tsx @@ -250,7 +250,7 @@ export const Timeline: React.FunctionComponent<{ }) }
- +
{ bars .filter(bar => !bar.action || bar.action.class !== 'Test') diff --git a/packages/trace-viewer/src/ui/traceModelContext.tsx b/packages/trace-viewer/src/ui/traceModelContext.tsx new file mode 100644 index 000000000..ab4adf95a --- /dev/null +++ b/packages/trace-viewer/src/ui/traceModelContext.tsx @@ -0,0 +1,20 @@ +/* + 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 type { MultiTraceModel } from './modelUtil'; + +export const TraceModelContext = React.createContext(undefined); diff --git a/packages/trace-viewer/src/ui/uiModeTraceView.tsx b/packages/trace-viewer/src/ui/uiModeTraceView.tsx index 382e6e669..e72a40a77 100644 --- a/packages/trace-viewer/src/ui/uiModeTraceView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTraceView.tsx @@ -24,6 +24,7 @@ import type { ContextEntry } from '../types/entries'; import type { SourceLocation } from './modelUtil'; import { MultiTraceModel } from './modelUtil'; import { Workbench } from './workbench'; +import { TraceModelContext } from './traceModelContext'; export const TraceView: React.FC<{ item: { treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase }, @@ -88,18 +89,19 @@ export const TraceView: React.FC<{ }; }, [outputDir, item, setModel, counter, setCounter, pathSeparator]); - return ; + return + ; + ; }; const outputDirForTestCase = (testCase: reporterTypes.TestCase): string | undefined => { diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 86ce06f04..bd23dcac7 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -47,9 +47,9 @@ import { MetadataWithCommitInfo } from '@testIsomorphic/types'; import type { ActionGroup } from '@isomorphic/protocolFormatter'; import { DialogToolbarButton } from '@web/components/dialogToolbarButton'; import { SettingsView } from './settingsView'; +import { TraceModelContext } from './traceModelContext'; export const Workbench: React.FunctionComponent<{ - model?: modelUtil.MultiTraceModel, showSourcesFirst?: boolean, rootDir?: string, fallbackLocation?: modelUtil.SourceLocation, @@ -61,7 +61,8 @@ export const Workbench: React.FunctionComponent<{ onOpenExternally?: (location: modelUtil.SourceLocation) => void, revealSource?: boolean, testRunMetadata?: MetadataWithCommitInfo, -}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource, testRunMetadata }) => { +}> = ({ showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource, testRunMetadata }) => { + const model = React.useContext(TraceModelContext); const [selectedCallId, setSelectedCallId] = React.useState(undefined); const [revealedError, setRevealedError] = React.useState(undefined); const [revealedAttachment, setRevealedAttachment] = React.useState<[attachment: AfterActionTraceEventAttachment, renderCounter: number] | undefined>(undefined); @@ -199,7 +200,7 @@ export const Workbench: React.FunctionComponent<{ id: 'errors', title: 'Errors', errorCount: errorsModel.errors.size, - render: () => { + render: () => { if (error.action) setSelectedAction(error.action); else @@ -249,7 +250,7 @@ export const Workbench: React.FunctionComponent<{ id: 'attachments', title: 'Attachments', count: model?.visibleAttachments.length, - render: () => + render: () => }; const tabs: TabbedPaneTabModel[] = [ diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx index 9def0cfdb..19da4a51e 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -22,6 +22,7 @@ import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorp import { DialogToolbarButton } from '@web/components/dialogToolbarButton'; import { Dialog } from '@web/shared/dialog'; import { DefaultSettingsView } from './defaultSettingsView'; +import { TraceModelContext } from './traceModelContext'; export const WorkbenchLoader: React.FunctionComponent<{ }> = () => { @@ -189,7 +190,9 @@ export const WorkbenchLoader: React.FunctionComponent<{
- + + + {fileForLocalModeError &&
Trace Viewer uses Service Workers to show traces. To view trace:
From ccb908adac882d7c5f142e71693c875957bc6130 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 10 Oct 2025 09:53:50 +0200 Subject: [PATCH 045/250] fix(shadow dom): follow assigned slot when hit-testing (#37777) --- packages/injected/src/injectedScript.ts | 4 +++- tests/page/page-click.spec.ts | 26 +++++++++++++++++++++++++ tests/page/selectors-css.spec.ts | 13 +++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index 76969f4ed..3ed79366a 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -989,7 +989,9 @@ export class InjectedScript { const hitParents: Element[] = []; while (hitElement && hitElement !== targetElement) { hitParents.push(hitElement); - hitElement = parentElementOrShadowHost(hitElement); + // Prefer the composed tree over the light-dom tree, as browser performs hit testing on the composed tree. + // Note that we will still eventually climb to the light-dom parent, as any element distributed to a slot is a direct child of the shadow host that contains the slot. + hitElement = hitElement.assignedSlot ?? parentElementOrShadowHost(hitElement); } if (hitElement === targetElement) return 'done'; diff --git a/tests/page/page-click.spec.ts b/tests/page/page-click.spec.ts index f048ea6c5..b9fa76c3d 100644 --- a/tests/page/page-click.spec.ts +++ b/tests/page/page-click.spec.ts @@ -1261,3 +1261,29 @@ it('should set PointerEvent.pressure on pointermove', async ({ page, isLinux, he [0, 50, 50], ]); }); + +it('should click into shadow root with slotted div', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37768' } }, async ({ page }) => { + await page.setContent(` + + +
Foo
+
+ `); + + await page.getByRole('button', { name: 'Foo' }).click(); +}); + +it('should click shadow root button', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37768' } }, async ({ page }) => { + await page.setContent(` + + +
Foo
+
+ `); + + await page.locator('my-button').click(); +}); diff --git a/tests/page/selectors-css.spec.ts b/tests/page/selectors-css.spec.ts index 3c34bd5c5..95980e1fd 100644 --- a/tests/page/selectors-css.spec.ts +++ b/tests/page/selectors-css.spec.ts @@ -468,3 +468,16 @@ it('css on the handle should be relative', async ({ page }) => { expect(await div.$eval(`.find-me`, e => e.id)).toBe('target2'); expect(await page.$eval(`div >> .find-me`, e => e.id)).toBe('target2'); }); + +it('should use light DOM structure for child combinator with slotted content', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37768' } }, async ({ page }) => { + await page.setContent(` + + +
Foo
+
+ `); + expect(await page.$eval(`my-button > div`, e => e.className)).toBe('content'); + expect(await page.$eval(`my-button > .content`, e => e.textContent)).toBe('Foo'); +}); From 99515d7003c6e32a37935712e511ed39b1220ab8 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 10 Oct 2025 13:29:16 +0200 Subject: [PATCH 046/250] docs: update demo trace URL (#37794) Signed-off-by: Simon Knott --- docs/src/trace-viewer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/trace-viewer.md b/docs/src/trace-viewer.md index c597c16f6..dbd0604f2 100644 --- a/docs/src/trace-viewer.md +++ b/docs/src/trace-viewer.md @@ -68,7 +68,7 @@ pwsh bin/Debug/netX/playwright.ps1 show-trace https://example.com/trace.zip When using [trace.playwright.dev](https://trace.playwright.dev), you can also pass the URL of your uploaded trace at some accessible storage (e.g. inside your CI) as a query parameter. CORS (Cross-Origin Resource Sharing) rules might apply. ```txt -https://trace.playwright.dev/?trace=https://demo.playwright.dev/reports/todomvc/data/fa874b0d59cdedec675521c21124e93161d66533.zip +https://trace.playwright.dev/?trace=https://demo.playwright.dev/reports/todomvc/data/e6099cadf79aa753d5500aa9508f9d1dbd87b5ee.zip ``` ## Recording a trace From e5536694e8a96fe9a4fcdaa456672b9406ec57e3 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:08:45 +0200 Subject: [PATCH 047/250] Fix: Add unique key prop to React elements in FilmStrip component (#37817) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Skn0tt <14912729+Skn0tt@users.noreply.github.com> --- packages/trace-viewer/src/ui/actionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index 4b162a170..d4f2dd1a0 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -171,7 +171,7 @@ export function renderTitleForCall(action: ActionTraceEvent): { elements: React. elements.push(param); title.push(param); } else { - elements.push({param}); + elements.push({param}); title.push(param); } currentIndex = match.index + fullMatch.length; From bd60309b83b3855afa900bde670317fd1373c401 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 13 Oct 2025 14:29:45 +0100 Subject: [PATCH 048/250] feat: incremental aria snapshot (#37795) --- packages/injected/src/ariaSnapshot.ts | 118 +++++++++---- packages/injected/src/domUtils.ts | 16 +- packages/injected/src/injectedScript.ts | 18 +- packages/playwright-core/src/client/page.ts | 4 +- .../playwright-core/src/protocol/validator.ts | 2 + .../src/server/dispatchers/pageDispatcher.ts | 2 +- packages/playwright-core/src/server/page.ts | 14 +- .../playwright/src/mcp/browser/response.ts | 12 +- packages/playwright/src/mcp/browser/tab.ts | 4 +- .../src/mcp/browser/tools/snapshot.ts | 2 +- .../playwright/src/mcp/browser/tools/tabs.ts | 4 +- packages/protocol/src/channels.d.ts | 5 +- packages/protocol/src/protocol.yml | 7 + tests/mcp/click.spec.ts | 10 +- tests/mcp/core.spec.ts | 2 - tests/mcp/dialogs.spec.ts | 12 +- tests/mcp/session-log.spec.ts | 2 +- tests/mcp/snapshot-diff.spec.ts | 158 ++++++++++++++++++ tests/mcp/wait.spec.ts | 20 ++- tests/page/page-aria-snapshot-ai.spec.ts | 148 +++++++++++++++- 20 files changed, 480 insertions(+), 80 deletions(-) create mode 100644 tests/mcp/snapshot-diff.spec.ts diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index a1e1afd42..57e4c689f 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -495,21 +495,36 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode, collectAll: return results; } -export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTreeOptions): string { +function buildByRefMap(root: AriaNode | undefined, map: Map = new Map()): Map { + if (root?.ref) + map.set(root.ref, root); + for (const child of root?.children || []) { + if (typeof child !== 'string') + buildByRefMap(child, map); + } + return map; +} + +function arePropsEqual(a: AriaNode, b: AriaNode): boolean { + const aKeys = Object.keys(a.props); + const bKeys = Object.keys(b.props); + return aKeys.length === bKeys.length && aKeys.every(k => a.props[k] === b.props[k]); +} + +export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTreeOptions, previous?: AriaSnapshot): string { const options = toInternalOptions(publicOptions); 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, renderCursorPointer: boolean) => { - if (typeof ariaNode === 'string') { - if (parentAriaNode && !includeText(parentAriaNode, ariaNode)) - return; - const text = yamlEscapeValueIfNeeded(renderString(ariaNode)); - if (text) - lines.push(indent + '- text: ' + text); - return; - } + const previousByRef = buildByRefMap(previous?.root); + const visitText = (text: string, indent: string) => { + const escaped = yamlEscapeValueIfNeeded(renderString(text)); + if (escaped) + lines.push(indent + '- text: ' + escaped); + }; + + const createKey = (ariaNode: AriaNode, renderCursorPointer: boolean): string => { let key = ariaNode.role; // Yaml has a limit of 1024 characters per key, and we leave some space for role and attributes. if (ariaNode.name && ariaNode.name.length <= 900) { @@ -538,41 +553,84 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr if (ariaNode.selected === true) key += ` [selected]`; - let inCursorPointer = false; if (ariaNode.ref) { key += ` [ref=${ariaNode.ref}]`; - if (renderCursorPointer && hasPointerCursor(ariaNode)) { - inCursorPointer = true; + if (renderCursorPointer && hasPointerCursor(ariaNode)) key += ' [cursor=pointer]'; - } } + return key; + }; + + const getSingleInlinedTextChild = (ariaNode: AriaNode | undefined): string | undefined => { + return ariaNode?.children.length === 1 && typeof ariaNode.children[0] === 'string' && !Object.keys(ariaNode.props).length ? ariaNode.children[0] : undefined; + }; + const visit = (ariaNode: AriaNode, indent: string, renderCursorPointer: boolean, previousNode: AriaNode | undefined): { unchanged: boolean } => { + if (ariaNode.ref) + previousNode = previousByRef.get(ariaNode.ref); + + const linesBefore = lines.length; + const key = createKey(ariaNode, renderCursorPointer); const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded(key); - const hasProps = !!Object.keys(ariaNode.props).length; - if (!ariaNode.children.length && !hasProps) { + const inCursorPointer = renderCursorPointer && !!ariaNode.ref && hasPointerCursor(ariaNode); + const singleInlinedTextChild = getSingleInlinedTextChild(ariaNode); + + // Whether ariaNode's subtree is the same as previousNode's, and can be replaced with just a ref. + let unchanged = !!previousNode && key === createKey(previousNode, renderCursorPointer) && arePropsEqual(ariaNode, previousNode); + + if (!ariaNode.children.length && !Object.keys(ariaNode.props).length) { + // Leaf node without children. lines.push(escapedKey); - } else if (ariaNode.children.length === 1 && typeof ariaNode.children[0] === 'string' && !hasProps) { - const text = includeText(ariaNode, ariaNode.children[0]) ? renderString(ariaNode.children[0] as string) : null; - if (text) - lines.push(escapedKey + ': ' + yamlEscapeValueIfNeeded(text)); + } else if (singleInlinedTextChild !== undefined) { + // Leaf node with just some text inside. + // Unchanged when the previous node also had the same single text child. + unchanged = unchanged && getSingleInlinedTextChild(previousNode) === singleInlinedTextChild; + + const shouldInclude = includeText(ariaNode, singleInlinedTextChild); + if (shouldInclude) + lines.push(escapedKey + ': ' + yamlEscapeValueIfNeeded(renderString(singleInlinedTextChild))); else lines.push(escapedKey); } else { + // Node with (optional) props and some children. lines.push(escapedKey + ':'); 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 + ' ', renderCursorPointer && !inCursorPointer); + + // All children must be the same. + unchanged = unchanged && previousNode?.children.length === ariaNode.children.length; + + const childIndent = indent + ' '; + for (let childIndex = 0 ; childIndex < ariaNode.children.length; childIndex++) { + const child = ariaNode.children[childIndex]; + if (typeof child === 'string') { + unchanged = unchanged && previousNode?.children[childIndex] === child; + if (includeText(ariaNode, child)) + visitText(child, childIndent); + } else { + const previousChild = previousNode?.children[childIndex]; + const childResult = visit(child, childIndent, renderCursorPointer && !inCursorPointer, typeof previousChild !== 'string' ? previousChild : undefined); + unchanged = unchanged && childResult.unchanged; + } + } } + + if (unchanged && ariaNode.ref) { + // Replace the whole subtree with a single reference. + lines.splice(linesBefore); + lines.push(indent + `- ref=${ariaNode.ref} [unchanged]`); + } + + return { unchanged }; }; - const ariaNode = ariaSnapshot.root; - if (ariaNode.role === 'fragment') { - // Render fragment. - for (const child of ariaNode.children || []) - visit(child, ariaNode, '', !!options.renderCursorPointer); - } else { - visit(ariaNode, null, '', !!options.renderCursorPointer); + // Do not render the root fragment, just its children. + const nodesToRender = ariaSnapshot.root.role === 'fragment' ? ariaSnapshot.root.children : [ariaSnapshot.root]; + for (const nodeToRender of nodesToRender) { + if (typeof nodeToRender === 'string') + visitText(nodeToRender, ''); + else + visit(nodeToRender, '', !!options.renderCursorPointer, undefined); } return lines.join('\n'); } @@ -636,5 +694,5 @@ function textContributesInfo(node: AriaNode, text: string): boolean { } function hasPointerCursor(ariaNode: AriaNode): boolean { - return ariaNode.box.style?.cursor === 'pointer'; + return ariaNode.box.cursor === 'pointer'; } diff --git a/packages/injected/src/domUtils.ts b/packages/injected/src/domUtils.ts index eb2b6a25f..78381bb40 100644 --- a/packages/injected/src/domUtils.ts +++ b/packages/injected/src/domUtils.ts @@ -112,7 +112,10 @@ export type Box = { visible: boolean; inline: boolean; rect?: DOMRect; - style?: CSSStyleDeclaration; + // Note: we do not store the CSSStyleDeclaration object, because it is a live object + // and changes values over time. This does not work for caching or comparing to the + // old values. Instead, store all the properties separately. + cursor?: CSSStyleDeclaration['cursor']; }; export function computeBox(element: Element): Box { @@ -120,20 +123,21 @@ export function computeBox(element: Element): Box { const style = getElementComputedStyle(element); if (!style) return { visible: true, inline: false }; + const cursor = style.cursor; 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, inline: false, style }; + return { visible: true, inline: false, cursor }; if (child.nodeType === 3 /* Node.TEXT_NODE */ && isVisibleTextNode(child as Text)) - return { visible: true, inline: true, style }; + return { visible: true, inline: true, cursor }; } - return { visible: false, inline: false, style }; + return { visible: false, inline: false, cursor }; } if (!isElementStyleVisibilityVisible(element, style)) - return { style, visible: false, inline: false }; + return { cursor, visible: false, inline: false }; const rect = element.getBoundingClientRect(); - return { rect, style, visible: rect.width > 0 && rect.height > 0, inline: style.display === 'inline' }; + return { rect, cursor, visible: rect.width > 0 && rect.height > 0, inline: style.display === 'inline' }; } export function isElementVisible(element: Element): boolean { diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index 3ed79366a..a204af76c 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -92,7 +92,8 @@ export class InjectedScript { readonly window: Window & typeof globalThis; readonly document: Document; readonly consoleApi: ConsoleAPI; - private _lastAriaSnapshot: AriaSnapshot | undefined; + private _lastAriaSnapshotForTrack = new Map(); + private _lastAriaSnapshotForQuery: AriaSnapshot | undefined; // Recorder must use any external dependencies through InjectedScript. // Otherwise it will end up with a copy of all modules it uses, and any @@ -299,11 +300,18 @@ export class InjectedScript { return new Set(result.map(r => r.element)); } - ariaSnapshot(node: Node, options: AriaTreeOptions): string { + ariaSnapshot(node: Node, options: AriaTreeOptions & { track?: string, incremental?: boolean }): string { if (node.nodeType !== Node.ELEMENT_NODE) throw this.createStacklessError('Can only capture aria snapshot of Element nodes.'); - this._lastAriaSnapshot = generateAriaTree(node as Element, options); - return renderAriaTree(this._lastAriaSnapshot, options); + const ariaSnapshot = generateAriaTree(node as Element, options); + let previous: AriaSnapshot | undefined; + if (options.incremental) + previous = options.track ? this._lastAriaSnapshotForTrack.get(options.track) : this._lastAriaSnapshotForQuery; + const result = renderAriaTree(ariaSnapshot, options, previous); + if (options.track) + this._lastAriaSnapshotForTrack.set(options.track, ariaSnapshot); + this._lastAriaSnapshotForQuery = ariaSnapshot; + return result; } ariaSnapshotForRecorder(): { ariaSnapshot: string, refs: Map } { @@ -692,7 +700,7 @@ export class InjectedScript { _createAriaRefEngine() { const queryAll = (root: SelectorRoot, selector: string): Element[] => { - const result = this._lastAriaSnapshot?.elements?.get(selector); + const result = this._lastAriaSnapshotForQuery?.elements?.get(selector); return result && result.isConnected ? [result] : []; }; return { queryAll }; diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 739231637..c0edc605f 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -847,8 +847,8 @@ export class Page extends ChannelOwner implements api.Page return result.pdf; } - async _snapshotForAI(options: TimeoutOptions = {}): Promise { - const result = await this._channel.snapshotForAI({ timeout: this._timeoutSettings.timeout(options) }); + async _snapshotForAI(options: TimeoutOptions & { track?: string, mode?: 'full' | 'incremental' } = {}): Promise { + const result = await this._channel.snapshotForAI({ timeout: this._timeoutSettings.timeout(options), track: options.track, mode: options.mode }); return result.snapshot; } } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index b8e7c3299..83c272728 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1465,6 +1465,8 @@ scheme.PageRequestsResult = tObject({ requests: tArray(tChannel(['Request'])), }); scheme.PageSnapshotForAIParams = tObject({ + track: tOptional(tString), + mode: tOptional(tEnum(['full', 'incremental'])), timeout: tFloat, }); scheme.PageSnapshotForAIResult = tObject({ diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 97443690f..8ca3682e3 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -352,7 +352,7 @@ export class PageDispatcher extends Dispatcher { - return { snapshot: await this._page.snapshotForAI(progress) }; + return { snapshot: await this._page.snapshotForAI(progress, params) }; } async bringToFront(params: channels.PageBringToFrontParams, progress: Progress): Promise { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index fd5a9fa9a..1ad565ab0 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -859,9 +859,9 @@ export class Page extends SdkObject { await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {}))); } - async snapshotForAI(progress: Progress): Promise { + async snapshotForAI(progress: Progress, options: { track?: string, mode?: 'full' | 'incremental' }): Promise { this.lastSnapshotFrameIds = []; - const snapshot = await snapshotFrameForAI(progress, this.mainFrame(), 0, this.lastSnapshotFrameIds); + const snapshot = await snapshotFrameForAI(progress, this.mainFrame(), 0, this.lastSnapshotFrameIds, options); return snapshot.join('\n'); } } @@ -1037,18 +1037,18 @@ class FrameThrottler { } } -async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, frameOrdinal: number, frameIds: string[]): Promise { +async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, frameOrdinal: number, frameIds: string[], options: { track?: string, mode?: 'full' | 'incremental' }): Promise { // Only await the topmost navigations, inner frames will be empty when racing. const snapshot = await frame.retryWithProgressAndTimeouts(progress, [1000, 2000, 4000, 8000], async continuePolling => { try { const context = await progress.race(frame._utilityContext()); const injectedScript = await progress.race(context.injectedScript()); - const snapshotOrRetry = await progress.race(injectedScript.evaluate((injected, refPrefix) => { + const snapshotOrRetry = await progress.race(injectedScript.evaluate((injected, options) => { const node = injected.document.body; if (!node) return true; - return injected.ariaSnapshot(node, { mode: 'ai', refPrefix }); - }, frameOrdinal ? 'f' + frameOrdinal : '')); + return injected.ariaSnapshot(node, { mode: 'ai', ...options }); + }, { refPrefix: frameOrdinal ? 'f' + frameOrdinal : '', incremental: options.mode === 'incremental', track: options.track })); if (snapshotOrRetry === true) return continuePolling; return snapshotOrRetry; @@ -1080,7 +1080,7 @@ async function snapshotFrameForAI(progress: Progress, frame: frames.Frame, frame const frameOrdinal = frameIds.length + 1; frameIds.push(child.frame._id); try { - const childSnapshot = await snapshotFrameForAI(progress, child.frame, frameOrdinal, frameIds); + const childSnapshot = await snapshotFrameForAI(progress, child.frame, frameOrdinal, frameIds, options); result.push(line + ':', ...childSnapshot.map(l => leadingSpace + ' ' + l)); } catch { result.push(line); diff --git a/packages/playwright/src/mcp/browser/response.ts b/packages/playwright/src/mcp/browser/response.ts index bdf7d2a88..a52129136 100644 --- a/packages/playwright/src/mcp/browser/response.ts +++ b/packages/playwright/src/mcp/browser/response.ts @@ -28,7 +28,7 @@ export class Response { private _code: string[] = []; private _images: { contentType: string, data: Buffer }[] = []; private _context: Context; - private _includeSnapshot = false; + private _includeSnapshot: 'none' | 'full' | 'incremental' = 'none'; private _includeTabs = false; private _tabSnapshot: TabSnapshot | undefined; @@ -75,8 +75,8 @@ export class Response { return this._images; } - setIncludeSnapshot() { - this._includeSnapshot = true; + setIncludeSnapshot(full?: 'full') { + this._includeSnapshot = full ?? 'incremental'; } setIncludeTabs() { @@ -86,8 +86,8 @@ 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()) - this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot(); + if (this._includeSnapshot !== 'none' && this._context.currentTab()) + this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshot); for (const tab of this._context.tabs()) await tab.updateTitle(); } @@ -126,7 +126,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. diff --git a/packages/playwright/src/mcp/browser/tab.ts b/packages/playwright/src/mcp/browser/tab.ts index 2831c6849..dbb56babd 100644 --- a/packages/playwright/src/mcp/browser/tab.ts +++ b/packages/playwright/src/mcp/browser/tab.ts @@ -217,10 +217,10 @@ export class Tab extends EventEmitter { return this._requests; } - async captureSnapshot(): Promise { + async captureSnapshot(mode: 'full' | 'incremental'): Promise { let tabSnapshot: TabSnapshot | undefined; const modalStates = await this._raceAgainstModalStates(async () => { - const snapshot = await this.page._snapshotForAI(); + const snapshot = await this.page._snapshotForAI({ mode, track: 'response' }); tabSnapshot = { url: this.page.url(), title: await this.page.title(), diff --git a/packages/playwright/src/mcp/browser/tools/snapshot.ts b/packages/playwright/src/mcp/browser/tools/snapshot.ts index e944bc403..33abe3962 100644 --- a/packages/playwright/src/mcp/browser/tools/snapshot.ts +++ b/packages/playwright/src/mcp/browser/tools/snapshot.ts @@ -30,7 +30,7 @@ const snapshot = defineTool({ handle: async (context, params, response) => { await context.ensureTab(); - response.setIncludeSnapshot(); + response.setIncludeSnapshot('full'); }, }); diff --git a/packages/playwright/src/mcp/browser/tools/tabs.ts b/packages/playwright/src/mcp/browser/tools/tabs.ts index 768db50e5..6eec59d5a 100644 --- a/packages/playwright/src/mcp/browser/tools/tabs.ts +++ b/packages/playwright/src/mcp/browser/tools/tabs.ts @@ -45,14 +45,14 @@ const browserTabs = defineTool({ } case 'close': { await context.closeTab(params.index); - response.setIncludeSnapshot(); + response.setIncludeSnapshot('full'); return; } case 'select': { if (params.index === undefined) throw new Error('Tab index is required'); await context.selectTab(params.index); - response.setIncludeSnapshot(); + response.setIncludeSnapshot('full'); return; } } diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 66fef77a3..867b77d13 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -2548,10 +2548,13 @@ export type PageRequestsResult = { requests: RequestChannel[], }; export type PageSnapshotForAIParams = { + track?: string, + mode?: 'full' | 'incremental', timeout: number, }; export type PageSnapshotForAIOptions = { - + track?: string, + mode?: 'full' | 'incremental', }; export type PageSnapshotForAIResult = { snapshot: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 73aeba935..cbb7d1060 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1977,6 +1977,13 @@ Page: snapshotForAI: internal: true parameters: + track: string? + mode: + # defaults to "full" + type: enum? + literals: + - full + - incremental timeout: float returns: snapshot: string diff --git a/tests/mcp/click.spec.ts b/tests/mcp/click.spec.ts index 0335ccf8c..8e64ad5c3 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]`), }); }); diff --git a/tests/mcp/core.spec.ts b/tests/mcp/core.spec.ts index 033a21224..13dde2ba8 100644 --- a/tests/mcp/core.spec.ts +++ b/tests/mcp/core.spec.ts @@ -90,8 +90,6 @@ test('browser_select_option (multiple)', async ({ client, server }) => { })).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]`), }); diff --git a/tests/mcp/dialogs.spec.ts b/tests/mcp/dialogs.spec.ts index 4c2bf98eb..548a33d60 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,7 @@ 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 +- Page Snapshot:`), }); }); diff --git a/tests/mcp/session-log.spec.ts b/tests/mcp/session-log.spec.ts index 6f9a98ee6..22f17566a 100644 --- a/tests/mcp/session-log.spec.ts +++ b/tests/mcp/session-log.spec.ts @@ -42,7 +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"`), + pageState: expect.stringContaining(`Page Title: Title`), }); 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 000000000..4c4ffdae1 --- /dev/null +++ b/tests/mcp/snapshot-diff.spec.ts @@ -0,0 +1,158 @@ +/** + * 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: +\`\`\`yaml +- ref=e1 [unchanged] +\`\`\``), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button 1', + ref: 'e2', + }, + })).toHaveResponse({ + pageState: expect.stringContaining(`Page Snapshot: +\`\`\`yaml +- generic [ref=e1]: + - button "Button 1" [active] [ref=e2] + - button "Button 2new text" [ref=e105] + - ref=e4 [unchanged] +\`\`\``), + }); + + // browser_snapshot forces a full snapshot. + expect(await client.callTool({ + name: 'browser_snapshot', + })).toHaveResponse({ + pageState: expect.stringContaining(`Page Snapshot: +\`\`\`yaml +- generic [ref=e1]: + - button "Button 1" [active] [ref=e2] + - button "Button 2new text" [ref=e105] + - list [ref=e4]:${listitems} +\`\`\``), + }); +}); + +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(` + - button "Button 1" [ref=e2] + - button "Button 2" [active] [ref=e3]`), + }); +}); diff --git a/tests/mcp/wait.spec.ts b/tests/mcp/wait.spec.ts index 0388faf25..e844eac4e 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`), }); diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 982cc120c..0e9b8e907 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -19,7 +19,7 @@ import { asLocator } from 'playwright-core/lib/utils'; import { test as it, expect, unshift } from './pageTest'; -function snapshotForAI(page: any, options?: { timeout?: number }): Promise { +function snapshotForAI(page: any, options?: { timeout?: number, mode?: 'full' | 'incremental', track?: string }): Promise { return page._snapshotForAI(options); } @@ -448,3 +448,149 @@ it('should not remove generic nodes with title', async ({ page }) => { - generic "Element title" [ref=e2] `); }); + +it('should create incremental snapshots on multiple tracks', async ({ page }) => { + await page.setContent(`
        • a span
        `); + + expect(await snapshotForAI(page, { track: 'first', mode: 'incremental' })).toContainYaml(` + - list [ref=e2]: + - listitem [ref=e3]: + - button "a button" [ref=e4] + - listitem [ref=e5]: a span + `); + expect(await snapshotForAI(page, { track: 'second', mode: 'incremental' })).toContainYaml(` + - list [ref=e2]: + - listitem [ref=e3]: + - button "a button" [ref=e4] + - listitem [ref=e5]: a span + `); + expect(await snapshotForAI(page, { track: 'first', mode: 'incremental' })).toContainYaml(` + - ref=e2 [unchanged] + `); + + await page.evaluate(() => { + document.querySelector('span').textContent = 'changed span'; + document.getElementById('hidden-li').style.display = 'inline'; + }); + expect(await snapshotForAI(page, { track: 'first', mode: 'incremental' })).toContainYaml(` + - list [ref=e2]: + - ref=e3 [unchanged] + - listitem [ref=e5]: changed span + - listitem [ref=e6]: some text + `); + + await page.evaluate(() => { + document.querySelector('span').textContent = 'a span'; + document.getElementById('hidden-li').style.display = 'none'; + }); + expect(await snapshotForAI(page, { track: 'first', mode: 'incremental' })).toContainYaml(` + - list [ref=e2]: + - ref=e3 [unchanged] + - listitem [ref=e5]: a span + `); + expect(await snapshotForAI(page, { track: 'second', mode: 'incremental' })).toContainYaml(` + - ref=e2 [unchanged] + `); + + expect(await snapshotForAI(page, { track: 'second', mode: 'full' })).toContainYaml(` + - list [ref=e2]: + - listitem [ref=e3]: + - button "a button" [ref=e4] + - listitem [ref=e5]: a span + `); +}); + +it('should create incremental snapshot for attribute change', async ({ page }) => { + await page.setContent(``); + await page.evaluate(() => document.querySelector('button').focus()); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - button "a button" [active] [ref=e2] + `); + + await page.evaluate(() => document.querySelector('button').blur()); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - button "a button" [ref=e2] + `); +}); + +it('should create incremental snapshot for child removal', async ({ page }) => { + await page.setContent(`
      • some text
      • `); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - listitem [ref=e2]: + - button "a button" [ref=e3] + - text: some text + `); + + await page.evaluate(() => document.querySelector('span').remove()); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - listitem [ref=e2]: + - ref=e3 [unchanged] + `); +}); + +it('should create incremental snapshot for child addition', async ({ page }) => { + await page.setContent(`
      • some text
      • `); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - listitem [ref=e2]: + - button "a button" [ref=e3] + `); + + await page.evaluate(() => document.querySelector('span').style.display = 'inline'); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - listitem [ref=e2]: + - ref=e3 [unchanged] + - text: some text + `); +}); + +it('should create incremental snapshot for prop change', async ({ page }) => { + await page.setContent(`a link`); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - link "a link" [ref=e2] [cursor=pointer]: + - /url: about:blank + `); + + await page.evaluate(() => document.querySelector('a').setAttribute('href', 'https://playwright.dev')); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - link "a link" [ref=e2] [cursor=pointer]: + - /url: https://playwright.dev + `); +}); + +it('should create incremental snapshot for cursor change', async ({ page }) => { + await page.setContent(`a link`); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - link "a link" [ref=e2] [cursor=pointer]: + - /url: about:blank + `); + + await page.evaluate(() => document.querySelector('a').style.cursor = 'default'); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - link "a link" [ref=e2]: + - /url: about:blank + `); +}); + +it('should create incremental snapshot for name change', async ({ page }) => { + await page.setContent(``); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - button "a button" [ref=e2] + `); + + await page.evaluate(() => document.querySelector('span').textContent = 'new button'); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - button "new button" [ref=e3] + `); +}); + +it('should create incremental snapshot for text change', async ({ page }) => { + await page.setContent(`
      • an item
      • `); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - listitem [ref=e2]: an item + `); + + await page.evaluate(() => document.querySelector('span').textContent = 'new text'); + expect(await snapshotForAI(page, { mode: 'incremental' })).toContainYaml(` + - listitem [ref=e2]: new text + `); +}); From dce402c3c8a35c409e27e888971ac0c007362af0 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:47:42 +0200 Subject: [PATCH 049/250] test: roll stable-test-runner to 1.57.0-alpha-2025-10-13 (#37819) 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 46451ba6e..d06d92c8a 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-beta-1759754009000" + "@playwright/test": "^1.57.0-alpha-2025-10-13" } }, "node_modules/@playwright/test": { - "version": "1.56.0-beta-1759754009000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-beta-1759754009000.tgz", - "integrity": "sha512-76zn3ZwPQCWL7LRQk+YE2QOHC1zsRIX3LLJrCRu0upM7avFNuZfrSUTgyoo4gJRAk/rlglpeCUVmwAMQ2TaxNQ==", + "version": "1.57.0-alpha-2025-10-13", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0-alpha-2025-10-13.tgz", + "integrity": "sha512-FtniY0zDGGeR03kxF2+pmG8sHxfg6sNX3i8++71hCP2rMP7dMIrOfZK1sV/rp927tiYvy2xqzx1OjT63777ctg==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.0-beta-1759754009000" + "playwright": "1.57.0-alpha-2025-10-13" }, "bin": { "playwright": "cli.js" @@ -38,12 +38,12 @@ } }, "node_modules/playwright": { - "version": "1.56.0-beta-1759754009000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-beta-1759754009000.tgz", - "integrity": "sha512-qzJggi6tENdKypgsjVar4tVQa+vnh+R0yAd3qnBy/TwK/gLNbBlRSW6zX81QR6Y9/kPXNJzujmPeSiyBOvL24g==", + "version": "1.57.0-alpha-2025-10-13", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0-alpha-2025-10-13.tgz", + "integrity": "sha512-HBFohy8DmVB5ANLTD7PrD+6wP3RYMB9tVyv/EiVepnGAr8jqo7kLFjxQFWw3qkoCPXrh7519reHRxrKB2QYtUg==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-beta-1759754009000" + "playwright-core": "1.57.0-alpha-2025-10-13" }, "bin": { "playwright": "cli.js" @@ -56,9 +56,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.0-beta-1759754009000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-beta-1759754009000.tgz", - "integrity": "sha512-vkZajl5x77nAt53oKh21RbXD4ErY24O2KFII0sC3UKLyM6UzJdT0KZ3u0oxZ0GXAy/PtnOcC+5NMNZYUFa7tLQ==", + "version": "1.57.0-alpha-2025-10-13", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0-alpha-2025-10-13.tgz", + "integrity": "sha512-tiZ93cvZzc8Q6fu4o10vJ8EoyW1ZufboT4+vXAcVEVYnyrkLjwcrk+QpSAUHmdr/iasDIipgtckt4Vsm9MxMNQ==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -70,11 +70,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.56.0-beta-1759754009000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0-beta-1759754009000.tgz", - "integrity": "sha512-76zn3ZwPQCWL7LRQk+YE2QOHC1zsRIX3LLJrCRu0upM7avFNuZfrSUTgyoo4gJRAk/rlglpeCUVmwAMQ2TaxNQ==", + "version": "1.57.0-alpha-2025-10-13", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0-alpha-2025-10-13.tgz", + "integrity": "sha512-FtniY0zDGGeR03kxF2+pmG8sHxfg6sNX3i8++71hCP2rMP7dMIrOfZK1sV/rp927tiYvy2xqzx1OjT63777ctg==", "requires": { - "playwright": "1.56.0-beta-1759754009000" + "playwright": "1.57.0-alpha-2025-10-13" } }, "fsevents": { @@ -84,18 +84,18 @@ "optional": true }, "playwright": { - "version": "1.56.0-beta-1759754009000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-beta-1759754009000.tgz", - "integrity": "sha512-qzJggi6tENdKypgsjVar4tVQa+vnh+R0yAd3qnBy/TwK/gLNbBlRSW6zX81QR6Y9/kPXNJzujmPeSiyBOvL24g==", + "version": "1.57.0-alpha-2025-10-13", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0-alpha-2025-10-13.tgz", + "integrity": "sha512-HBFohy8DmVB5ANLTD7PrD+6wP3RYMB9tVyv/EiVepnGAr8jqo7kLFjxQFWw3qkoCPXrh7519reHRxrKB2QYtUg==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.56.0-beta-1759754009000" + "playwright-core": "1.57.0-alpha-2025-10-13" } }, "playwright-core": { - "version": "1.56.0-beta-1759754009000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-beta-1759754009000.tgz", - "integrity": "sha512-vkZajl5x77nAt53oKh21RbXD4ErY24O2KFII0sC3UKLyM6UzJdT0KZ3u0oxZ0GXAy/PtnOcC+5NMNZYUFa7tLQ==" + "version": "1.57.0-alpha-2025-10-13", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0-alpha-2025-10-13.tgz", + "integrity": "sha512-tiZ93cvZzc8Q6fu4o10vJ8EoyW1ZufboT4+vXAcVEVYnyrkLjwcrk+QpSAUHmdr/iasDIipgtckt4Vsm9MxMNQ==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index 14535a804..50fb4d503 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-beta-1759754009000" + "@playwright/test": "^1.57.0-alpha-2025-10-13" } } From e6e65259f7595e5ccd30c601184d2a71759c7e7b Mon Sep 17 00:00:00 2001 From: Chris O'Donnell Date: Mon, 13 Oct 2025 10:05:19 -0400 Subject: [PATCH 050/250] docs: improve testing library migration docs for within (#37796) Signed-off-by: Chris O'Donnell --- docs/src/testing-library-js.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/testing-library-js.md b/docs/src/testing-library-js.md index d7f7c05d8..bff0eb616 100644 --- a/docs/src/testing-library-js.md +++ b/docs/src/testing-library-js.md @@ -119,7 +119,7 @@ You can create a locator inside another locator with [`method: Locator.locator`] ```js // Testing Library -const messages = document.getElementById('messages'); +const messages = screen.getByTestId('messages'); const helloMessage = within(messages).getByText('hello'); // Playwright From 25ca30f7ff92e130308dcfaacd0011a8bebf47a1 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 13 Oct 2025 17:16:02 +0200 Subject: [PATCH 051/250] docs: discourage Download.createReadStream (#37831) --- docs/src/api/class-download.md | 4 ++++ packages/playwright-client/types/types.d.ts | 4 ++++ packages/playwright-core/types/types.d.ts | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/docs/src/api/class-download.md b/docs/src/api/class-download.md index 3333d3699..11b0c68e1 100644 --- a/docs/src/api/class-download.md +++ b/docs/src/api/class-download.md @@ -74,6 +74,10 @@ Upon successful cancellations, `download.failure()` would resolve to `'canceled' Returns a readable stream for a successful download, or throws for a failed/canceled download. +:::note +If you don't need a readable stream, it's usually simpler to read the file from disk after the download completed. See [`method: Download.path`]. +::: + ## async method: Download.delete * since: v1.8 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index c510663fc..a04bcc1fc 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -19106,6 +19106,10 @@ export interface Download { /** * Returns a readable stream for a successful download, or throws for a failed/canceled download. + * + * **NOTE** If you don't need a readable stream, it's usually simpler to read the file from disk after the download + * completed. See [download.path()](https://playwright.dev/docs/api/class-download#download-path). + * */ createReadStream(): Promise; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index c510663fc..a04bcc1fc 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -19106,6 +19106,10 @@ export interface Download { /** * Returns a readable stream for a successful download, or throws for a failed/canceled download. + * + * **NOTE** If you don't need a readable stream, it's usually simpler to read the file from disk after the download + * completed. See [download.path()](https://playwright.dev/docs/api/class-download#download-path). + * */ createReadStream(): Promise; From 8b1960775613accca4f6d81581860e714c1e3780 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Mon, 13 Oct 2025 19:47:53 +0200 Subject: [PATCH 052/250] test(bidi): remove newlines from fixme descriptions in the csv reporter (#37829) --- tests/bidi/csvReporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bidi/csvReporter.ts b/tests/bidi/csvReporter.ts index 67aa26c85..b84ca9e81 100644 --- a/tests/bidi/csvReporter.ts +++ b/tests/bidi/csvReporter.ts @@ -55,7 +55,7 @@ class CsvReporter implements Reporter { row.push(test.expectedStatus); row.push(test.outcome()); if (fixme) { - row.push('fixme' + (fixme.description ? `: ${fixme.description}` : '')); + row.push('fixme' + (fixme.description ? `: ${fixme.description.replace(/\s+/g, ' ')}` : '')); } else { const result = test.results.find(r => r.error); if (result) { From 271ab2669706536bb9a6100281143cdce94dce0b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 13 Oct 2025 18:48:21 +0100 Subject: [PATCH 053/250] test: produce merged report for MCP tests (#37828) --- .github/workflows/create_test_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create_test_report.yml b/.github/workflows/create_test_report.yml index 5afe85e0c..04170fd20 100644 --- a/.github/workflows/create_test_report.yml +++ b/.github/workflows/create_test_report.yml @@ -1,7 +1,7 @@ name: Publish Test Results on: workflow_run: - workflows: ["tests 1", "tests 2", "tests others"] + workflows: ["tests 1", "tests 2", "tests others", "MCP"] types: - completed jobs: From 4f9ba4b74e7c9c8afb99dcd6f54111505ae2ca27 Mon Sep 17 00:00:00 2001 From: Vladyslav Tkachenko <48068368+vladlearns@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:50:36 +0300 Subject: [PATCH 054/250] fix(test-runner): attachments push enumerable property (#37814) --- packages/playwright/src/worker/testInfo.ts | 8 ++++++- tests/playwright-test/reporter.spec.ts | 26 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index c007255bc..27ad9cc91 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -214,11 +214,17 @@ export class TestInfoImpl implements TestInfo { })(); this._attachmentsPush = this.attachments.push.bind(this.attachments); - this.attachments.push = (...attachments: TestInfo['attachments']) => { + const attachmentsPush = (...attachments: TestInfo['attachments']) => { for (const a of attachments) this._attach(a, this._parentStep()?.stepId); return this.attachments.length; }; + Object.defineProperty(this.attachments, 'push', { + value: attachmentsPush, + writable: true, + enumerable: false, + configurable: true + }); this._tracing = new TestTracing(this, workerParams.artifactsDir); diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index 9a04c246d..358368a24 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -674,6 +674,32 @@ test('should report annotations from test declaration', async ({ runInlineTest } ]); }); +test('attachments.push should not be enumerable to allow toEqual comparisons', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.js': ` + import { test, expect } from '@playwright/test'; + test('test.info().attachments.push should not be enumerable', async ({}) => { + + const attachments = test.info().attachments; + + const descriptor = Object.getOwnPropertyDescriptor(attachments, 'push'); + expect(descriptor).toBeTruthy(); + expect(descriptor.enumerable).toBe(false); + + expect(Object.keys(attachments)).not.toContain('push'); + + await test.info().attach('file1', { body: 'content1', contentType: 'text/plain' }); + await test.info().attach('file2', { body: 'content2', contentType: 'text/plain' }); + + const keys = Object.keys(attachments); + expect(keys.every(k => !isNaN(Number(k)))).toBe(true); + }); + `, + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + test('tests skipped in serial mode receive onTestBegin/onTestEnd', async ({ runInlineTest }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28321' }); From 2efc46a5d9a0d25fecbc93d89073b16c72af1f6e Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 13 Oct 2025 11:32:49 -0700 Subject: [PATCH 055/250] feat(uimode): add pick locator button to inspector tab (#37753) --- packages/trace-viewer/src/ui/inspectorTab.tsx | 7 +++++-- packages/trace-viewer/src/ui/workbench.tsx | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/trace-viewer/src/ui/inspectorTab.tsx b/packages/trace-viewer/src/ui/inspectorTab.tsx index f7392234e..cc93d2593 100644 --- a/packages/trace-viewer/src/ui/inspectorTab.tsx +++ b/packages/trace-viewer/src/ui/inspectorTab.tsx @@ -27,10 +27,11 @@ import type { Language } from '@isomorphic/locatorGenerators'; export const InspectorTab: React.FunctionComponent<{ sdkLanguage: Language, + isInspecting: boolean, setIsInspecting: (isInspecting: boolean) => void, highlightedElement: HighlightedElement, setHighlightedElement: (element: HighlightedElement) => void, -}> = ({ sdkLanguage, setIsInspecting, highlightedElement, setHighlightedElement }) => { +}> = ({ sdkLanguage, isInspecting, setIsInspecting, highlightedElement, setHighlightedElement }) => { const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState(); const onAriaEditorChange = React.useCallback((ariaSnapshot: string) => { const { errors } = parseAriaSnapshot(yaml, ariaSnapshot, { prettyErrors: false }); @@ -50,7 +51,9 @@ export const InspectorTab: React.FunctionComponent<{ return
        -
        Locator
        +
        Locator
        + setIsInspecting(!isInspecting)} /> +
        { copy(highlightedElement.locator || ''); }}> diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index bd23dcac7..b52974609 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -182,6 +182,7 @@ export const Workbench: React.FunctionComponent<{ title: 'Locator', render: () => , From e514ea666dfbc8fd641b5285aba49a003f22cbfb Mon Sep 17 00:00:00 2001 From: Chris <57954026+cpAdm@users.noreply.github.com> Date: Tue, 14 Oct 2025 01:36:36 +0200 Subject: [PATCH 056/250] fix(trace): Remove dangling semicolon (#37839) --- packages/trace-viewer/src/ui/uiModeTraceView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/trace-viewer/src/ui/uiModeTraceView.tsx b/packages/trace-viewer/src/ui/uiModeTraceView.tsx index e72a40a77..17d60de7d 100644 --- a/packages/trace-viewer/src/ui/uiModeTraceView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTraceView.tsx @@ -100,7 +100,7 @@ export const TraceView: React.FC<{ annotations={item.testCase?.annotations ?? []} onOpenExternally={onOpenExternally} revealSource={revealSource} - />; + /> ; }; From 3afe7ba8c020e1f69c703dce1801d258bd726f89 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 13 Oct 2025 18:44:41 -0700 Subject: [PATCH 057/250] chore(trace): survive the sw restart in most cases (#37833) --- .../src/utils/isomorphic/mimeType.ts | 2 +- packages/trace-viewer/src/sw/main.ts | 233 +++++++++++------- packages/trace-viewer/src/sw/progress.ts | 2 +- .../trace-viewer/src/ui/workbenchLoader.tsx | 3 - tests/library/trace-viewer.spec.ts | 19 ++ 5 files changed, 159 insertions(+), 100 deletions(-) diff --git a/packages/playwright-core/src/utils/isomorphic/mimeType.ts b/packages/playwright-core/src/utils/isomorphic/mimeType.ts index 45ac92d64..9db6ea937 100644 --- a/packages/playwright-core/src/utils/isomorphic/mimeType.ts +++ b/packages/playwright-core/src/utils/isomorphic/mimeType.ts @@ -430,7 +430,7 @@ const types: Map = new Map([ ['jpgm', 'video/jpm'], ['mj2', 'video/mj2'], ['mjp2', 'video/mj2'], - ['ts', 'video/mp2t'], + ['ts', 'application/typescript'], ['mp4', 'video/mp4'], ['mp4v', 'video/mp4'], ['mpg4', 'video/mp4'], diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 79b928d06..9ecdf88ea 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { splitProgress } from './progress'; +import { Progress, splitProgress } from './progress'; import { SnapshotServer } from './snapshotServer'; import { TraceModel } from './traceModel'; import { FetchTraceModelBackend, traceFileURL, ZipTraceModelBackend } from './traceModelBackends'; @@ -41,6 +41,13 @@ type ServiceWorkerGlobalScope = { skipWaiting(): Promise; }; +type FetchEvent = { + request: Request; + clientId: string | null; + resultingClientId: string | null; + respondWith(response: Promise): void; +}; + declare const self: ServiceWorkerGlobalScope; self.addEventListener('install', function(event: any) { @@ -57,22 +64,52 @@ type LoadedTrace = { }; const scopePath = new URL(self.registration.scope).pathname; -const loadedTraces = new Map(); +const loadedTraces = new Map>(); const clientIdToTraceUrls = new Map(); const isDeployedAsHttps = self.registration.scope.startsWith('https://'); -async function loadTrace(traceUrl: string, traceFileName: string | null, client: Client): Promise { - const clientId = client.id; +function simulateRestart() { + loadedTraces.clear(); + clientIdToTraceUrls.clear(); +} + +async function loadTraceOrError(clientId: string, url: URL, isContextRequest: boolean, progress: Progress): Promise<{ loadedTrace?: LoadedTrace, errorResponse?: Response }> { + try { + const loadedTrace = await loadTrace(clientId, url, isContextRequest, progress); + return { loadedTrace }; + } catch (error) { + return { + errorResponse: new Response(JSON.stringify({ error: error?.message }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }) + }; + } +} + +function loadTrace(clientId: string, url: URL, isContextRequest: boolean, progress: Progress): Promise { + const traceUrl = url.searchParams.get('trace')!; + if (!traceUrl) + throw new Error('trace parameter is missing'); + clientIdToTraceUrls.set(clientId, traceUrl); + const omitCache = isContextRequest && isLiveTrace(traceUrl); + const loadedTrace = omitCache ? undefined : loadedTraces.get(traceUrl); + if (loadedTrace) + return loadedTrace; + const promise = innerLoadTrace(traceUrl, progress); + loadedTraces.set(traceUrl, promise); + return promise; +} + +async function innerLoadTrace(traceUrl: string, progress: Progress): Promise { await gc(); const traceModel = new TraceModel(); try { // Allow 10% to hop from sw to page. - const [fetchProgress, unzipProgress] = splitProgress((done: number, total: number) => { - client.postMessage({ method: 'progress', params: { done, total } }); - }, [0.5, 0.4, 0.1]); - const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress); + const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]); + const backend = isLiveTrace(traceUrl) ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress); await traceModel.load(backend, unzipProgress); } catch (error: any) { // eslint-disable-next-line no-console @@ -80,122 +117,125 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, client: if (error?.message?.includes('Cannot find .trace file') && await traceModel.hasEntry('index.html')) throw new Error('Could not load trace. Did you upload a Playwright HTML report instead? Make sure to extract the archive first and then double-click the index.html file or put it on a web server.'); if (error instanceof TraceVersionError) - throw new Error(`Could not load trace from ${traceFileName || traceUrl}. ${error.message}`); - if (traceFileName) - throw new Error(`Could not load trace from ${traceFileName}. Make sure to upload a valid Playwright trace.`); + throw new Error(`Could not load trace from ${traceUrl}. ${error.message}`); throw new Error(`Could not load trace from ${traceUrl}. Make sure a valid Playwright Trace is accessible over this url.`); } const snapshotServer = new SnapshotServer(traceModel.storage(), sha1 => traceModel.resourceForSha1(sha1)); - loadedTraces.set(traceUrl, { traceModel, snapshotServer }); - return traceModel; + return { traceModel, snapshotServer }; } -// @ts-ignore async function doFetch(event: FetchEvent): Promise { + const request = event.request; + // In order to make Accessibility Insights for Web work. - if (event.request.url.startsWith('chrome-extension://')) - return fetch(event.request); + if (request.url.startsWith('chrome-extension://')) + return fetch(request); - if (event.request.headers.get('x-pw-serviceworker') === 'forward') { + if (request.headers.get('x-pw-serviceworker') === 'forward') { const request = new Request(event.request); request.headers.delete('x-pw-serviceworker'); return fetch(request); } - const request = event.request; - const client = await self.clients.get(event.clientId) as Client | undefined; - - // When trace viewer is deployed over https, we will force upgrade - // insecure http subresources to https. Otherwise, these will fail - // to load inside our https snapshots. - // In this case, we also match http resources from the archive by - // the https urls. const url = new URL(request.url); - let relativePath: string | undefined; if (request.url.startsWith(self.registration.scope)) relativePath = url.pathname.substring(scopePath.length - 1); + if (relativePath === '/restartServiceWorker') { + simulateRestart(); + return new Response(null, { status: 200 }); + } + if (relativePath === '/ping') return new Response(null, { status: 200 }); - if (relativePath === '/contexts') { - const traceUrl = url.searchParams.get('trace'); - if (!client || !traceUrl) { - return new Response('Something went wrong, trace is requested as a part of the navigation', { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); + const isNavigation = !!event.resultingClientId; + const client = event.clientId ? await self.clients.get(event.clientId) : undefined; + + if (isNavigation && !relativePath?.startsWith('/sha1/')) { + // Navigation request. Download is a /sha1/ navigation, ignore them here. + + // Snapshot iframe navigation request. + if (relativePath?.startsWith('/snapshot/')) { + // It is Ok to pass noop progress as the trace is likely already loaded. + const { errorResponse, loadedTrace } = await loadTraceOrError(event.resultingClientId!, url, false, noopProgress); + if (errorResponse) + return errorResponse; + const pageOrFrameId = relativePath.substring('/snapshot/'.length); + const response = loadedTrace!.snapshotServer.serveSnapshot(pageOrFrameId, url.searchParams, url.href); + if (isDeployedAsHttps) + response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests'); + return response; } - try { - const traceModel = await loadTrace(traceUrl, url.searchParams.get('traceFileName'), client); - return new Response(JSON.stringify(traceModel.contextEntries), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); - } catch (error: any) { - return new Response(JSON.stringify({ error: error?.message }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); - } + // Static content navigation request for trace viewer or popout. + return fetch(event.request); } - if (relativePath?.startsWith('/snapshotInfo/')) { - const { snapshotServer } = loadedTrace(url); - if (!snapshotServer) - return new Response(null, { status: 404 }); - const pageOrFrameId = relativePath.substring('/snapshotInfo/'.length); - return snapshotServer.serveSnapshotInfo(pageOrFrameId, url.searchParams); - } + if (!relativePath) { + // Out-of-scope sub-resource request => iframe snapshot sub-resources. + if (!client) + return new Response('Sub-resource without a client', { status: 500 }); - if (relativePath?.startsWith('/snapshot/')) { - const { snapshotServer } = loadedTrace(url); + const { snapshotServer } = await loadTrace(client.id, new URL(client.url), false, clientProgress(client)); if (!snapshotServer) return new Response(null, { status: 404 }); - const pageOrFrameId = relativePath.substring('/snapshot/'.length); - const response = snapshotServer.serveSnapshot(pageOrFrameId, url.searchParams, url.href); - if (isDeployedAsHttps) - response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests'); - return response; - } - if (relativePath?.startsWith('/closest-screenshot/')) { - const { snapshotServer } = loadedTrace(url); - if (!snapshotServer) - return new Response(null, { status: 404 }); - const pageOrFrameId = relativePath.substring('/closest-screenshot/'.length); - return snapshotServer.serveClosestScreenshot(pageOrFrameId, url.searchParams); + // When trace viewer is deployed over https, we will force upgrade + // insecure http sub-resources to https. Otherwise, these will fail + // to load inside our https snapshots. + // In this case, we also match http resources from the archive by + // the https urls. + const lookupUrls = [request.url]; + if (isDeployedAsHttps && request.url.startsWith('https://')) + lookupUrls.push(request.url.replace(/^https/, 'http')); + return snapshotServer.serveResource(lookupUrls, request.method, client.url); } - if (relativePath?.startsWith('/sha1/')) { - const { traceModel } = loadedTrace(url); - const blob = await traceModel?.resourceForSha1(relativePath.slice('/sha1/'.length)); - if (blob) - return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) }); - return new Response(null, { status: 404 }); + // These commands all require a loaded trace. + if (relativePath === '/contexts' || relativePath.startsWith('/snapshotInfo/') || relativePath.startsWith('/closest-screenshot/') || relativePath.startsWith('/sha1/')) { + if (!client) + return new Response('Sub-resource without a client', { status: 500 }); + + const isContextRequest = relativePath === '/contexts'; + const { errorResponse, loadedTrace } = await loadTraceOrError(client.id, url, isContextRequest, clientProgress(client)); + if (errorResponse) + return errorResponse; + + if (relativePath === '/contexts') { + return new Response(JSON.stringify(loadedTrace!.traceModel.contextEntries), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } + + if (relativePath.startsWith('/snapshotInfo/')) { + const pageOrFrameId = relativePath.substring('/snapshotInfo/'.length); + return loadedTrace!.snapshotServer.serveSnapshotInfo(pageOrFrameId, url.searchParams); + } + + if (relativePath.startsWith('/closest-screenshot/')) { + const pageOrFrameId = relativePath.substring('/closest-screenshot/'.length); + return loadedTrace!.snapshotServer.serveClosestScreenshot(pageOrFrameId, url.searchParams); + } + + if (relativePath.startsWith('/sha1/')) { + const blob = await loadedTrace!.traceModel.resourceForSha1(relativePath.slice('/sha1/'.length)); + if (blob) + return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) }); + return new Response(null, { status: 404 }); + } } + // Pass through to the server for file requests. if (relativePath?.startsWith('/file/')) { const path = url.searchParams.get('path')!; return await fetch(traceFileURL(path)); } - // Fallback for static assets. - if (relativePath) - return fetch(event.request); - - const snapshotUrl = client!.url; - const traceUrl = new URL(snapshotUrl).searchParams.get('trace')!; - const { snapshotServer } = loadedTraces.get(traceUrl) || {}; - if (!snapshotServer) - return new Response(null, { status: 404 }); - - const lookupUrls = [request.url]; - if (isDeployedAsHttps && request.url.startsWith('https://')) - lookupUrls.push(request.url.replace(/^https/, 'http')); - return snapshotServer.serveResource(lookupUrls, request.method, snapshotUrl); + // Static content for sub-resource. + return fetch(event.request); } function downloadHeaders(searchParams: URLSearchParams): Headers | undefined { @@ -210,19 +250,11 @@ function downloadHeaders(searchParams: URLSearchParams): Headers | undefined { return headers; } -const emptyLoadedTrace = { traceModel: undefined, snapshotServer: undefined }; - -function loadedTrace(url: URL): LoadedTrace | { traceModel: undefined, snapshotServer: undefined } { - const traceUrl = url.searchParams.get('trace'); - return traceUrl ? loadedTraces.get(traceUrl) ?? emptyLoadedTrace : emptyLoadedTrace; -} - async function gc() { const clients = await self.clients.matchAll(); const usedTraces = new Set(); for (const [clientId, traceUrl] of clientIdToTraceUrls) { - // @ts-ignore if (!clients.find(c => c.id === clientId)) { clientIdToTraceUrls.delete(clientId); continue; @@ -236,7 +268,18 @@ async function gc() { } } -// @ts-ignore +function clientProgress(client: Client): Progress { + return (done: number, total: number) => { + client.postMessage({ method: 'progress', params: { done, total } }); + }; +} + +function noopProgress(done: number, total: number): undefined { } + +function isLiveTrace(traceUrl: string): boolean { + return traceUrl.endsWith('.json'); +} + self.addEventListener('fetch', function(event: FetchEvent) { event.respondWith(doFetch(event)); }); diff --git a/packages/trace-viewer/src/sw/progress.ts b/packages/trace-viewer/src/sw/progress.ts index a6f856ee5..8828deef2 100644 --- a/packages/trace-viewer/src/sw/progress.ts +++ b/packages/trace-viewer/src/sw/progress.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -type Progress = (done: number, total: number) => undefined; +export type Progress = (done: number, total: number) => undefined; export function splitProgress(progress: Progress, weights: number[]): Progress[] { const doneList = new Array(weights.length).fill(0); diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx index 19da4a51e..165f4d6c7 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -43,7 +43,6 @@ export const WorkbenchLoader: React.FunctionComponent<{ const file = files.item(0)!; const blobTraceURL = URL.createObjectURL(file); url.searchParams.append('trace', blobTraceURL); - url.searchParams.append('traceFileName', file.name); const href = url.toString(); // Snapshot loaders will inherit the trace url from the query parameters, // so set it here. @@ -143,8 +142,6 @@ export const WorkbenchLoader: React.FunctionComponent<{ const params = new URLSearchParams(); params.set('trace', traceURL); - if (uploadedTraceName) - params.set('traceFileName', uploadedTraceName); const response = await fetch(`contexts?${params.toString()}`); if (!response.ok) { if (!isServer) diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 2016cc16c..477590138 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -1981,3 +1981,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 b2236c89bce8bd62f3fbf06add34185ffe0039f2 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 14 Oct 2025 18:15:33 +0100 Subject: [PATCH 058/250] fix(clock): do not advance into the past (#37820) --- packages/injected/src/clock.ts | 12 +++++++++++ tests/library/unit/clock.spec.ts | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index eb788a426..c623e0f54 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -90,6 +90,7 @@ export class ClockController { now(): number { this._replayLogOnce(); + // Sync real time to support calling Date.now() in a loop. this._syncRealTime(); return this._now.time; } @@ -111,6 +112,7 @@ export class ClockController { performanceNow(): DOMHighResTimeStamp { this._replayLogOnce(); + // Sync real time to support calling performance.now() in a loop. this._syncRealTime(); return this._now.ticks; } @@ -139,6 +141,12 @@ export class ClockController { } private _advanceNow(to: Ticks) { + if (this._now.ticks > to) { + // While running timers, `now` can advance by syncing with real time + // from within now() or performance.now(). + // This makes it possible for `now` to be ahead of where we want to advance it. + return; + } if (!this._now.isFixedTime) this._now.time = asWallTime(this._now.time + to - this._now.ticks); this._now.ticks = to; @@ -172,6 +180,7 @@ export class ClockController { } this._advanceNow(to); + if (firstException) throw firstException; } @@ -375,6 +384,9 @@ export class ClockController { } getTimeToNextFrame() { + // When `window.requestAnimationFrame` is the first call in the page, + // this place is the first API call, so replay the log. + this._replayLogOnce(); return 16 - this._now.ticks % 16; } diff --git a/tests/library/unit/clock.spec.ts b/tests/library/unit/clock.spec.ts index 30f640e04..a88750792 100644 --- a/tests/library/unit/clock.spec.ts +++ b/tests/library/unit/clock.spec.ts @@ -730,6 +730,29 @@ it.describe('runFor', () => { expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); + + it('does not rewind back in time', async ({ clock }) => { + const stub = createStub(); + const gotTime = await new Promise(done => { + clock.setTimeout(() => { + stub(clock.Date.now()); + }, 10); + clock.setTimeout(() => { + stub(clock.Date.now()); + }, 10); + clock.resume(); + setTimeout(async () => { + // Call fast-forward right after the real time sync happens, + // but before all the callbacks are processed. + await clock.runFor(1000); + setTimeout(() => { + done(clock.Date.now()); + }, 20); + }, 10); + }); + expect(stub.callCount).toBe(2); + expect(gotTime).toBeGreaterThan(1010); + }); }); it.describe('clearTimeout', () => { @@ -1419,6 +1442,18 @@ it.describe('fastForward', () => { expect(stub.callCount).toBe(1); expect(stub.calledWith(2000)).toBeTruthy(); }); + + it('error does not pause forever', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(() => { + stub(clock.Date.now()); + }, 1000); + clock.resume(); + const error = await clock.fastForward(-1000).catch(e => e); + expect(error.message).toContain('Cannot fast-forward to the past'); + await new Promise(f => setTimeout(f, 1500)); + expect(stub.callCount).toBe(1); + }); }); it.describe('pauseAt', () => { From 3f67e719aa202b3008a4782197720eaa4d16b599 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 14 Oct 2025 10:29:01 -0700 Subject: [PATCH 059/250] fix(glob): do not treat custom schema as special (#37851) --- .../playwright-core/src/utils/isomorphic/urlMatch.ts | 9 +++++++-- tests/page/interception.spec.ts | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts index b526ca732..e4b0cc6ed 100644 --- a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts +++ b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts @@ -132,8 +132,13 @@ function resolveGlobBase(baseURL: string | undefined, match: string): string { return token; // Handle special case of http*://, note that the new schema has to be // a web schema so that slashes are properly inserted after domain. - if (index === 0 && token.endsWith(':')) - return mapToken(token, 'http:'); + if (index === 0 && token.endsWith(':')) { + // Replace any pattern with http: + if (token.indexOf('*') !== -1 || token.indexOf('{') !== -1) + return mapToken(token, 'http:'); + // Preserve explicit schema as is as it may affect trailing slashes after domain. + return token; + } const questionIndex = token.indexOf('?'); if (questionIndex === -1) return mapToken(token, `$_${index}_$`); diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts index b8653f9ef..a0ef182de 100644 --- a/tests/page/interception.spec.ts +++ b/tests/page/interception.spec.ts @@ -127,6 +127,12 @@ it('should work with glob', async () => { expect(urlMatches(undefined, 'https://localhost:3000/?a=b', '**?a=b')).toBeTruthy(); expect(urlMatches(undefined, 'https://localhost:3000/?a=b', '**=b')).toBeTruthy(); + // Custom schema. + expect(urlMatches(undefined, 'my.custom.protocol://foo', 'my.custom.protocol://**')).toBeTruthy(); + expect(urlMatches(undefined, 'my.p://foo', 'my.{p,y}://**')).toBeFalsy(); + expect(urlMatches(undefined, 'my.p://foo/', 'my.{p,y}://**')).toBeTruthy(); + expect(urlMatches(undefined, 'file:///foo/', 'f*e://**')).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 8120834da787eed092979c60309f84eb4b691b67 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 14 Oct 2025 11:22:35 -0700 Subject: [PATCH 060/250] chore: use flexbox size for network details (#37842) --- packages/trace-viewer/src/ui/networkResourceDetails.css | 6 ++++-- packages/trace-viewer/src/ui/networkResourceDetails.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.css b/packages/trace-viewer/src/ui/networkResourceDetails.css index b3f7a5c04..26557db1e 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.css +++ b/packages/trace-viewer/src/ui/networkResourceDetails.css @@ -15,14 +15,16 @@ */ .network-request-details-tab { - width: 100%; - height: 100%; user-select: text; line-height: 24px; margin-left: 10px; overflow: auto; } +.network-request-details-tab > * { + flex: none; +} + .network-request-details-url { white-space: normal; word-wrap: break-word; diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index b0a257b1e..1dfdb1119 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -110,7 +110,7 @@ const RequestTab: React.FunctionComponent<{ startTimeOffset: number; requestBody: RequestBody, }> = ({ resource, startTimeOffset, requestBody }) => { - return
        + return
        General
        {`URL: ${resource.request.url}`}
        {`Method: ${resource.request.method}`}
        @@ -138,7 +138,7 @@ const RequestTab: React.FunctionComponent<{ const ResponseTab: React.FunctionComponent<{ resource: ResourceSnapshot; }> = ({ resource }) => { - return
        + return
        Response Headers
        {resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
        ; @@ -177,10 +177,10 @@ const BodyTab: React.FunctionComponent<{ readResources(); }, [resource, model]); - return
        + return
        {!resource.response.content._sha1 &&
        Response body is not available for this request.
        } {responseBody && responseBody.font && } - {responseBody && responseBody.dataUrl && } + {responseBody && responseBody.dataUrl &&
        } {responseBody && responseBody.text && }
        ; }; From 7e7455b904f265f06896ee2edfb51ae6d35317cb Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 14 Oct 2025 11:30:24 -0700 Subject: [PATCH 061/250] chore: include trace viewer webmanifest into npm (#37841) --- packages/playwright-core/.npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/playwright-core/.npmignore b/packages/playwright-core/.npmignore index e574cbd69..6c22b80f9 100644 --- a/packages/playwright-core/.npmignore +++ b/packages/playwright-core/.npmignore @@ -11,6 +11,7 @@ !lib/**/*.svg !lib/**/*.ttf !lib/utilsBundleImpl/xdg-open +!lib/**/manifest.webmanifest # Exclude injected files. A preprocessed version of these is included via lib/generated. # See packages/injected/src/README.md. lib/**/injected/ From 7fbba7563a5eb1f7358949cbaceb3000e15efedd Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 14 Oct 2025 14:34:41 -0700 Subject: [PATCH 062/250] chore(mcp): encourage relative path usage (#37857) --- packages/playwright/src/mcp/browser/config.ts | 2 +- packages/playwright/src/mcp/browser/tools/pdf.ts | 2 +- packages/playwright/src/mcp/browser/tools/screenshot.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index d79cd236d..0dec6a116 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -328,7 +328,7 @@ async function resolveFile(config: FullConfig, clientInfo: ClientInfo, fileName: fileName = fileName.split('\\').join('/'); 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`); + throw new Error(`Resolved file path ${resolvedFile} is outside of the output directory ${dir}. Use relative file names to stay within the output directory.`); return resolvedFile; } diff --git a/packages/playwright/src/mcp/browser/tools/pdf.ts b/packages/playwright/src/mcp/browser/tools/pdf.ts index 13127f031..accb071ff 100644 --- a/packages/playwright/src/mcp/browser/tools/pdf.ts +++ b/packages/playwright/src/mcp/browser/tools/pdf.ts @@ -20,7 +20,7 @@ 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.'), + filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified. Prefer relative file names to stay within the output directory.'), }); const pdf = defineTabTool({ diff --git a/packages/playwright/src/mcp/browser/tools/screenshot.ts b/packages/playwright/src/mcp/browser/tools/screenshot.ts index 51f51a5e8..18335b0b0 100644 --- a/packages/playwright/src/mcp/browser/tools/screenshot.ts +++ b/packages/playwright/src/mcp/browser/tools/screenshot.ts @@ -23,7 +23,7 @@ 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.'), + filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory.'), 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.'), From 45e1a89c79c66b1d1f82a0ba04a22beb3ddb978b Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Wed, 15 Oct 2025 00:46:39 +0200 Subject: [PATCH 063/250] test(bidi): update test expectations for Firefox BiDi (#37827) --- packages/playwright-core/src/server/bidi/bidiPage.ts | 2 ++ tests/library/browsercontext-page-event.spec.ts | 4 ++-- tests/library/browsercontext-proxy.spec.ts | 2 +- tests/library/har.spec.ts | 4 ++-- tests/library/screenshot.spec.ts | 4 ++-- tests/page/page-add-init-script.spec.ts | 4 ++-- tests/page/page-goto.spec.ts | 9 ++++++--- tests/page/page-screenshot.spec.ts | 4 ++-- 8 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index 56d4d7c70..2ec693768 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -423,6 +423,8 @@ export class BidiPage implements PageDelegate { } async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise { + if (color) + throw new Error('Not implemented'); } async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise { diff --git a/tests/library/browsercontext-page-event.spec.ts b/tests/library/browsercontext-page-event.spec.ts index 15e2b6616..83e01ff0c 100644 --- a/tests/library/browsercontext-page-event.spec.ts +++ b/tests/library/browsercontext-page-event.spec.ts @@ -170,7 +170,7 @@ it('should work with Shift-clicking', async ({ browser, server, browserName }) = await context.close(); }); -it('should work with Ctrl-clicking', async ({ browser, server, browserName }) => { +it('should work with Ctrl-clicking', async ({ browser, server, browserName, channel }) => { const context = await browser.newContext(); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); @@ -179,6 +179,6 @@ it('should work with Ctrl-clicking', async ({ browser, server, browserName }) => context.waitForEvent('page'), page.click('a', { modifiers: ['ControlOrMeta'] }), ]); - expect(await popup.opener()).toBe(browserName === 'firefox' ? page : null); + expect(await popup.opener()).toBe(browserName === 'firefox' && !channel?.startsWith('moz-firefox') ? page : null); await context.close(); }); diff --git a/tests/library/browsercontext-proxy.spec.ts b/tests/library/browsercontext-proxy.spec.ts index a1110d660..1e69fddde 100644 --- a/tests/library/browsercontext-proxy.spec.ts +++ b/tests/library/browsercontext-proxy.spec.ts @@ -173,7 +173,7 @@ it.describe('should proxy local network requests', () => { it('should use ipv6 proxy', async ({ contextFactory, server, proxyServer, browserName, channel }) => { - it.fail(browserName === 'firefox', 'page.goto: NS_ERROR_UNKNOWN_HOST'); + it.fail(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'page.goto: NS_ERROR_UNKNOWN_HOST'); it.fail(channel === 'webkit-wsl', 'WebKit on WSL does not support IPv6: https://github.com/microsoft/WSL/issues/10803'); proxyServer.forwardTo(server.PORT); const context = await contextFactory({ diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index c3eb51172..f51d290ce 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -859,7 +859,7 @@ it('should include redirects from API request', async ({ contextFactory, server expect(json.timings).toBeDefined(); }); -it('should not hang on resources served from cache', async ({ contextFactory, server, browserName }, testInfo) => { +it('should not hang on resources served from cache', async ({ contextFactory, server, browserName, channel }, testInfo) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/11435' }); server.setRoute('/one-style.css', (req, res) => { res.writeHead(200, { @@ -874,7 +874,7 @@ it('should not hang on resources served from cache', async ({ contextFactory, se const log = await getLog(); const entries = log.entries.filter(e => e.request.url.endsWith('one-style.css')); // In firefox no request events are fired for cached resources. - if (browserName === 'firefox') + if (browserName === 'firefox' && !channel?.startsWith('moz-firefox')) expect(entries.length).toBe(1); else expect(entries.length).toBe(2); diff --git a/tests/library/screenshot.spec.ts b/tests/library/screenshot.spec.ts index 0deace018..cd4402035 100644 --- a/tests/library/screenshot.spec.ts +++ b/tests/library/screenshot.spec.ts @@ -110,7 +110,7 @@ browserTest.describe('page screenshot', () => { await context.close(); }); - browserTest('should throw if screenshot size is too large with device scale factor', async ({ browser, browserName, isMac }) => { + browserTest('should throw if screenshot size is too large with device scale factor', async ({ browser, browserName, isMac, channel }) => { browserTest.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/16727' }); const context = await browser.newContext({ viewport: { width: 500, height: 500 }, deviceScaleFactor: 2 }); const page = await context.newPage(); @@ -122,7 +122,7 @@ browserTest.describe('page screenshot', () => { { await page.setContent(`
        `); const exception = await page.screenshot({ fullPage: true }).catch(e => e); - if (browserName === 'firefox' || (browserName === 'webkit' && !isMac)) + if ((browserName === 'firefox' && !channel?.startsWith('moz-firefox')) || (browserName === 'webkit' && !isMac)) expect(exception.message).toContain('Cannot take screenshot larger than 32767'); const image = await page.screenshot({ fullPage: true, scale: 'css' }); diff --git a/tests/page/page-add-init-script.spec.ts b/tests/page/page-add-init-script.spec.ts index a24c1dad3..0ef4294c4 100644 --- a/tests/page/page-add-init-script.spec.ts +++ b/tests/page/page-add-init-script.spec.ts @@ -84,7 +84,7 @@ it('should work after a cross origin navigation', async ({ page, server }) => { expect(await page.evaluate(() => window['result'])).toBe(123); }); -it('init script should run only once in iframe', async ({ page, server, browserName }) => { +it('init script should run only once in iframe', async ({ page, server, browserName, channel }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/26992' }); const messages = []; page.on('console', event => { @@ -95,6 +95,6 @@ it('init script should run only once in iframe', async ({ page, server, browserN await page.goto(server.PREFIX + '/frames/one-frame.html'); expect(messages).toEqual([ 'init script: /frames/one-frame.html', - 'init script: ' + (browserName === 'firefox' ? 'no url yet' : '/frames/frame.html'), + 'init script: ' + (browserName === 'firefox' && !channel?.startsWith('moz-firefox') ? 'no url yet' : '/frames/frame.html'), ]); }); diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index dd0c53675..2ef8e60bb 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -453,7 +453,7 @@ it('should disable timeout when its set to 0', async ({ page, server }) => { expect(loaded).toBe(true); }); -it('should fail when replaced by another navigation', async ({ page, server, browserName }) => { +it('should fail when replaced by another navigation', async ({ page, server, browserName, channel }) => { let anotherPromise; server.setRoute('/empty.html', (req, res) => { anotherPromise = page.goto(server.PREFIX + '/one-style.html'); @@ -466,8 +466,11 @@ it('should fail when replaced by another navigation', async ({ page, server, bro } else if (browserName === 'webkit') { expect(error.message).toContain(`page.goto: Navigation to "${server.PREFIX + '/empty.html'}" is interrupted by another navigation to "${server.PREFIX + '/one-style.html'}"`); } else if (browserName === 'firefox') { - // Firefox might yield either NS_BINDING_ABORTED or 'navigation interrupted by another one' - expect(error.message.includes(`page.goto: Navigation to "${server.PREFIX + '/empty.html'}" is interrupted by another navigation to "${server.PREFIX + '/one-style.html'}"`) || error.message.includes('NS_BINDING_ABORTED')).toBe(true); + if (channel?.startsWith('moz-firefox')) + expect(error.message).toContain('page.goto: Protocol error (browsingContext.navigate): unknown error'); + else + // Firefox might yield either NS_BINDING_ABORTED or 'navigation interrupted by another one' + expect(error.message.includes(`page.goto: Navigation to "${server.PREFIX + '/empty.html'}" is interrupted by another navigation to "${server.PREFIX + '/one-style.html'}"`) || error.message.includes('NS_BINDING_ABORTED')).toBe(true); } }); diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index b77989502..fba08fb10 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -240,8 +240,8 @@ it.describe('page screenshot', () => { await verifyViewport(page, 500, 500); }); - it('should allow transparency', async ({ page, browserName, platform, headless }) => { - it.fail(browserName === 'firefox'); + it('should allow transparency', async ({ page, browserName, channel }) => { + it.fail(browserName === 'firefox' || channel?.startsWith('bidi-chrom')); await page.setViewportSize({ width: 300, height: 300 }); await page.setContent(` From 6e775c7c406656a13774e403c4a1a94ffbba8baa Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 14 Oct 2025 16:41:33 -0700 Subject: [PATCH 064/250] fix: reset element not found error when element is present (#37860) --- packages/playwright-core/src/server/frames.ts | 6 ++++-- tests/page/expect-to-have-text.spec.ts | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index c705cef56..16a3b17e5 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1444,10 +1444,12 @@ export class Frame extends SdkObject { progress.log(log); // Note: missingReceived avoids `unexpected value "undefined"` when element was not found. if (matches === options.isNot) { - if (missingReceived) + if (missingReceived) { lastIntermediateResult.errorMessage = 'Error: element(s) not found'; - else + } else { + lastIntermediateResult.errorMessage = undefined; lastIntermediateResult.received = received; + } lastIntermediateResult.isSet = true; if (!missingReceived && !Array.isArray(received)) progress.log(` unexpected value "${renderUnexpectedValue(options.expression, received)}"`); diff --git a/tests/page/expect-to-have-text.spec.ts b/tests/page/expect-to-have-text.spec.ts index 6b80d522f..818dc77ff 100644 --- a/tests/page/expect-to-have-text.spec.ts +++ b/tests/page/expect-to-have-text.spec.ts @@ -167,6 +167,24 @@ test.describe('toHaveText with text', () => { expect(stripAnsi(error.message)).toContain('Expected: "Text"'); expect(stripAnsi(error.message)).toContain('Received: "Text content"'); }); + + test('do not show "element(s) not found" when the real failure is a string mismatch', async ({ page }) => { + await page.setContent(` +
        Initial
        + + `); + + const cell = page.locator('#field'); + const error = await expect(cell).toHaveText('Something', { timeout: 2000 }).catch(e => e); + expect(stripAnsi(error.message)).toContain('Expected: "Something"'); + expect(stripAnsi(error.message)).toContain('Received: "Final value"'); + }); }); test.describe('not.toHaveText', () => { From 1532d51db56741c4d9871d99354c4a939430bd89 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 14 Oct 2025 18:31:34 -0700 Subject: [PATCH 065/250] chore(mcp): cap image size (#37856) --- .../src/server/utils/comparators.ts | 33 +---- .../src/server/utils/imageUtils.ts | 135 ++++++++++++++++++ packages/playwright-core/src/utils.ts | 1 + .../src/mcp/browser/tools/screenshot.ts | 39 +++-- tests/mcp/screenshot.spec.ts | 45 +++++- 5 files changed, 215 insertions(+), 38 deletions(-) create mode 100644 packages/playwright-core/src/server/utils/imageUtils.ts diff --git a/packages/playwright-core/src/server/utils/comparators.ts b/packages/playwright-core/src/server/utils/comparators.ts index 747abb2ec..77426a2d2 100644 --- a/packages/playwright-core/src/server/utils/comparators.ts +++ b/packages/playwright-core/src/server/utils/comparators.ts @@ -21,6 +21,9 @@ import pixelmatch from '../../third_party/pixelmatch'; import { jpegjs } from '../../utilsBundle'; import { colors, diff } from '../../utilsBundle'; import { PNG } from '../../utilsBundle'; +import { padImageToSize } from './imageUtils'; + +import type { ImageData } from './imageUtils'; export type ImageComparatorOptions = { threshold?: number, maxDiffPixels?: number, maxDiffPixelRatio?: number, comparator?: string }; export type ComparatorResult = { diff?: Buffer; errorMessage: string; } | null; @@ -48,8 +51,6 @@ export function compareBuffersOrStrings(actualBuffer: Buffer | string, expectedB return null; } -type ImageData = { width: number, height: number, data: Buffer }; - function compareImages(mimeType: string, actualBuffer: Buffer | string, expectedBuffer: Buffer, options: ImageComparatorOptions = {}): ComparatorResult { if (!actualBuffer || !(actualBuffer instanceof Buffer)) return { errorMessage: 'Actual result should be a Buffer.' }; @@ -61,8 +62,8 @@ function compareImages(mimeType: string, actualBuffer: Buffer | string, expected let sizesMismatchError = ''; if (expected.width !== actual.width || expected.height !== actual.height) { sizesMismatchError = `Expected an image ${expected.width}px by ${expected.height}px, received ${actual.width}px by ${actual.height}px. `; - actual = resizeImage(actual, size); - expected = resizeImage(expected, size); + actual = padImageToSize(actual, size); + expected = padImageToSize(expected, size); } const diff = new PNG({ width: size.width, height: size.height }); let count; @@ -131,27 +132,3 @@ function compareText(actual: Buffer | string, expectedBuffer: Buffer): Comparato const errorMessage = coloredLines.join('\n'); return { errorMessage }; } - -function resizeImage(image: ImageData, size: { width: number, height: number }): ImageData { - if (image.width === size.width && image.height === size.height) - return image; - const buffer = new Uint8Array(size.width * size.height * 4); - for (let y = 0; y < size.height; y++) { - for (let x = 0; x < size.width; x++) { - const to = (y * size.width + x) * 4; - if (y < image.height && x < image.width) { - const from = (y * image.width + x) * 4; - buffer[to] = image.data[from]; - buffer[to + 1] = image.data[from + 1]; - buffer[to + 2] = image.data[from + 2]; - buffer[to + 3] = image.data[from + 3]; - } else { - buffer[to] = 0; - buffer[to + 1] = 0; - buffer[to + 2] = 0; - buffer[to + 3] = 0; - } - } - } - return { data: Buffer.from(buffer), width: size.width, height: size.height }; -} diff --git a/packages/playwright-core/src/server/utils/imageUtils.ts b/packages/playwright-core/src/server/utils/imageUtils.ts new file mode 100644 index 000000000..180185111 --- /dev/null +++ b/packages/playwright-core/src/server/utils/imageUtils.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. + */ + +export type ImageData = { width: number, height: number, data: Buffer }; + +export function padImageToSize(image: ImageData, size: { width: number, height: number }): ImageData { + if (image.width === size.width && image.height === size.height) + return image; + const buffer = new Uint8Array(size.width * size.height * 4); + for (let y = 0; y < size.height; y++) { + for (let x = 0; x < size.width; x++) { + const to = (y * size.width + x) * 4; + if (y < image.height && x < image.width) { + const from = (y * image.width + x) * 4; + buffer[to] = image.data[from]; + buffer[to + 1] = image.data[from + 1]; + buffer[to + 2] = image.data[from + 2]; + buffer[to + 3] = image.data[from + 3]; + } else { + buffer[to] = 0; + buffer[to + 1] = 0; + buffer[to + 2] = 0; + buffer[to + 3] = 0; + } + } + } + return { data: Buffer.from(buffer), width: size.width, height: size.height }; +} + +export function scaleImageToSize(image: ImageData, size: { width: number; height: number }): ImageData { + const { data: src, width: w1, height: h1 } = image; + const w2 = size.width | 0, h2 = size.height | 0; + if (w1 === w2 && h1 === h2) + return image; + + const clamp = (v: number, lo: number, hi: number) => (v < lo ? lo : v > hi ? hi : v); + + // Catmull–Rom weights + const weights = (t: number, o: Float32Array) => { + const t2 = t * t, t3 = t2 * t; + o[0] = -0.5 * t + 1.0 * t2 - 0.5 * t3; + o[1] = 1.0 - 2.5 * t2 + 1.5 * t3; + o[2] = 0.5 * t + 2.0 * t2 - 1.5 * t3; + o[3] = -0.5 * t2 + 0.5 * t3; + }; + + const srcRowStride = w1 * 4; + const dstRowStride = w2 * 4; + + // Precompute X: indices, weights, and byte offsets (idx*4) + const xIdx = new Int32Array(w2 * 4); + const xOff = new Int32Array(w2 * 4); // byte offsets = xIdx*4 + const xW = new Float32Array(w2 * 4); + const wx = new Float32Array(4); + const xScale = w1 / w2; + for (let x = 0; x < w2; x++) { + const sx = (x + 0.5) * xScale - 0.5; + const sxi = Math.floor(sx); + const t = sx - sxi; + weights(t, wx); + const b = x * 4; + const i0 = clamp(sxi - 1, 0, w1 - 1); + const i1 = clamp(sxi + 0, 0, w1 - 1); + const i2 = clamp(sxi + 1, 0, w1 - 1); + const i3 = clamp(sxi + 2, 0, w1 - 1); + xIdx[b + 0] = i0; xIdx[b + 1] = i1; xIdx[b + 2] = i2; xIdx[b + 3] = i3; + xOff[b + 0] = i0 << 2; xOff[b + 1] = i1 << 2; xOff[b + 2] = i2 << 2; xOff[b + 3] = i3 << 2; + xW[b + 0] = wx[0]; xW[b + 1] = wx[1]; xW[b + 2] = wx[2]; xW[b + 3] = wx[3]; + } + + // Precompute Y: indices, weights, and row-base byte offsets (y*rowStride) + const yIdx = new Int32Array(h2 * 4); + const yRow = new Int32Array(h2 * 4); // row base in bytes + const yW = new Float32Array(h2 * 4); + const wy = new Float32Array(4); + const yScale = h1 / h2; + for (let y = 0; y < h2; y++) { + const sy = (y + 0.5) * yScale - 0.5; + const syi = Math.floor(sy); + const t = sy - syi; + weights(t, wy); + const b = y * 4; + const j0 = clamp(syi - 1, 0, h1 - 1); + const j1 = clamp(syi + 0, 0, h1 - 1); + const j2 = clamp(syi + 1, 0, h1 - 1); + const j3 = clamp(syi + 2, 0, h1 - 1); + yIdx[b + 0] = j0; yIdx[b + 1] = j1; yIdx[b + 2] = j2; yIdx[b + 3] = j3; + yRow[b + 0] = j0 * srcRowStride; + yRow[b + 1] = j1 * srcRowStride; + yRow[b + 2] = j2 * srcRowStride; + yRow[b + 3] = j3 * srcRowStride; + yW[b + 0] = wy[0]; yW[b + 1] = wy[1]; yW[b + 2] = wy[2]; yW[b + 3] = wy[3]; + } + + const dst = new Uint8Array(w2 * h2 * 4); + + for (let y = 0; y < h2; y++) { + const yb = y * 4; + const rb0 = yRow[yb + 0], rb1 = yRow[yb + 1], rb2 = yRow[yb + 2], rb3 = yRow[yb + 3]; + const wy0 = yW[yb + 0], wy1 = yW[yb + 1], wy2 = yW[yb + 2], wy3 = yW[yb + 3]; + const dstBase = y * dstRowStride; + + for (let x = 0; x < w2; x++) { + const xb = x * 4; + const xo0 = xOff[xb + 0], xo1 = xOff[xb + 1], xo2 = xOff[xb + 2], xo3 = xOff[xb + 3]; + const wx0 = xW[xb + 0], wx1 = xW[xb + 1], wx2 = xW[xb + 2], wx3 = xW[xb + 3]; + const di = dstBase + (x << 2); + + // unrolled RGBA + for (let c = 0; c < 4; c++) { + const r0 = src[rb0 + xo0 + c] * wx0 + src[rb0 + xo1 + c] * wx1 + src[rb0 + xo2 + c] * wx2 + src[rb0 + xo3 + c] * wx3; + const r1 = src[rb1 + xo0 + c] * wx0 + src[rb1 + xo1 + c] * wx1 + src[rb1 + xo2 + c] * wx2 + src[rb1 + xo3 + c] * wx3; + const r2 = src[rb2 + xo0 + c] * wx0 + src[rb2 + xo1 + c] * wx1 + src[rb2 + xo2 + c] * wx2 + src[rb2 + xo3 + c] * wx3; + const r3 = src[rb3 + xo0 + c] * wx0 + src[rb3 + xo1 + c] * wx1 + src[rb3 + xo2 + c] * wx2 + src[rb3 + xo3 + c] * wx3; + const v = r0 * wy0 + r1 * wy1 + r2 * wy2 + r3 * wy3; + dst[di + c] = v < 0 ? 0 : v > 255 ? 255 : v | 0; + } + } + } + + return { data: Buffer.from(dst), width: w2, height: h2 }; +} diff --git a/packages/playwright-core/src/utils.ts b/packages/playwright-core/src/utils.ts index db049937c..78cd6eb6a 100644 --- a/packages/playwright-core/src/utils.ts +++ b/packages/playwright-core/src/utils.ts @@ -43,6 +43,7 @@ export * from './server/utils/expectUtils'; export * from './server/utils/fileUtils'; export * from './server/utils/hostPlatform'; export * from './server/utils/httpServer'; +export * from './server/utils/imageUtils'; export * from './server/utils/network'; export * from './server/utils/nodePlatform'; export * from './server/utils/processLauncher'; diff --git a/packages/playwright/src/mcp/browser/tools/screenshot.ts b/packages/playwright/src/mcp/browser/tools/screenshot.ts index 18335b0b0..0b357a7dc 100644 --- a/packages/playwright/src/mcp/browser/tools/screenshot.ts +++ b/packages/playwright/src/mcp/browser/tools/screenshot.ts @@ -14,6 +14,11 @@ * limitations under the License. */ +import fs from 'fs'; + +import { mkdirIfNeeded, scaleImageToSize } from 'playwright-core/lib/utils'; +import { jpegjs, PNG } from 'playwright-core/lib/utilsBundle'; + import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; import * as javascript from '../codegen'; @@ -51,7 +56,6 @@ const screenshot = defineTabTool({ type: fileType, quality: fileType === 'png' ? undefined : 90, scale: 'css', - path: fileName, ...(params.fullPage !== undefined && { fullPage: params.fullPage }) }; const isElementScreenshot = params.element && params.ref; @@ -68,19 +72,36 @@ const screenshot = defineTabTool({ response.addCode(`await page.screenshot(${javascript.formatObject(options)});`); const buffer = ref ? await ref.locator.screenshot(options) : await tab.page.screenshot(options); + + await mkdirIfNeeded(fileName); + await fs.promises.writeFile(fileName, buffer); + 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 - }); - } + response.addImage({ + contentType: fileType === 'png' ? 'image/png' : 'image/jpeg', + data: scaleImageToFitMessage(buffer, fileType) + }); } }); +export function scaleImageToFitMessage(buffer: Buffer, imageType: 'png' | 'jpeg'): Buffer { + // https://docs.claude.com/en/docs/build-with-claude/vision#evaluate-image-size + // Not more than 1.15 megapixel, linear size not more than 1568. + + const image = imageType === 'png' ? PNG.sync.read(buffer) : jpegjs.decode(buffer, { maxMemoryUsageInMB: 512 }); + const pixels = image.width * image.height; + + const shrink = Math.min(1568 / image.width, 1568 / image.height, Math.sqrt(1.15 * 1024 * 1024 / pixels)); + if (shrink > 1) + return buffer; + + const width = image.width * shrink | 0; + const height = image.height * shrink | 0; + const scaledImage = scaleImageToSize(image, { width, height }); + return imageType === 'png' ? PNG.sync.write(scaledImage as any) : jpegjs.encode(scaledImage, 80).data; +} + export default [ screenshot, ]; diff --git a/tests/mcp/screenshot.spec.ts b/tests/mcp/screenshot.spec.ts index 50b2eaeaf..18dbc5dbb 100644 --- a/tests/mcp/screenshot.spec.ts +++ b/tests/mcp/screenshot.spec.ts @@ -17,6 +17,7 @@ import fs from 'fs'; import { test, expect } from './fixtures'; +import { jpegjs, PNG } from 'packages/playwright-core/lib/utilsBundle'; test('browser_take_screenshot (viewport)', async ({ startClient, server }, testInfo) => { const { client } = await startClient({ @@ -264,11 +265,53 @@ test('browser_take_screenshot (fullPage: true)', async ({ startClient, server }, { text: expect.stringContaining('fullPage: true'), type: 'text', - } + }, + { + data: expect.any(String), + mimeType: 'image/png', + type: 'image', + }, ], }); }); +test('browser_take_screenshot size cap', async ({ startClient, server, mcpBrowser }, testInfo) => { + test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser ?? ''), 'Non-chrome has unusual full page size'); + + const { client } = await startClient({ + config: { outputDir: testInfo.outputPath('output') }, + }); + + const expectations = [ + { title: '2000x500', pageWidth: 2000, pageHeight: 500, expectedWidth: 1568, expectedHeight: 500 * 1568 / 2000 | 0 }, + { title: '2000x2000', pageWidth: 2000, pageHeight: 2000, expectedWidth: 1098, expectedHeight: 1098 }, + { title: '1280x800', pageWidth: 1280, pageHeight: 800, expectedWidth: 1280, expectedHeight: 800 }, + ]; + + for (const expectation of expectations) { + await test.step(expectation.title, async () => { + server.setContent('/', ``, 'text/html'); + await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX } }); + + const pngResult = await client.callTool({ + name: 'browser_take_screenshot', + arguments: { fullPage: true }, + }); + const png = PNG.sync.read(Buffer.from(pngResult.content?.[1]?.data, 'base64')); + expect(png.width).toBe(expectation.expectedWidth); + expect(png.height).toBe(expectation.expectedHeight); + + const jpegResult = await client.callTool({ + name: 'browser_take_screenshot', + arguments: { fullPage: true, type: 'jpeg' }, + }); + const jpeg = jpegjs.decode(Buffer.from(jpegResult.content?.[1]?.data, 'base64')); + expect(jpeg.width).toBe(expectation.expectedWidth); + expect(jpeg.height).toBe(expectation.expectedHeight); + }); + } +}); + test('browser_take_screenshot (fullPage with element should error)', async ({ startClient, server }, testInfo) => { const { client } = await startClient({ config: { outputDir: testInfo.outputPath('output') }, From ea2360e88a5d497faeb85507b18c2fdf81a4da2d Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 11:19:30 +0200 Subject: [PATCH 066/250] chore(agents): render tool names (#37848) --- packages/playwright/src/agents/healer.md | 4 ++-- packages/playwright/src/agents/planner.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/playwright/src/agents/healer.md b/packages/playwright/src/agents/healer.md index 2bd30a407..ee07a6cb1 100644 --- a/packages/playwright/src/agents/healer.md +++ b/packages/playwright/src/agents/healer.md @@ -24,8 +24,8 @@ resolving Playwright test failures. Your mission is to systematically identify, 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. +1. **Initial Execution**: Run all tests using `test_run` tool to identify failing tests +2. **Debug failed tests**: For each failing test run `test_debug`. 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 diff --git a/packages/playwright/src/agents/planner.md b/packages/playwright/src/agents/planner.md index 672f3f238..192962b29 100644 --- a/packages/playwright/src/agents/planner.md +++ b/packages/playwright/src/agents/planner.md @@ -38,7 +38,7 @@ You will: - Invoke the `planner_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 + - 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** From 068b62e1b395e3f86495ebe34c34bcf2813c61f6 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 15 Oct 2025 12:30:25 +0200 Subject: [PATCH 067/250] chore: skip progress and download size checks for chunked transfer (#37832) --- .../server/registry/oopDownloadBrowserMain.ts | 10 +++++-- tests/installation/playwright-cdn.spec.ts | 4 +-- ...playwright-cli-install-should-work.spec.ts | 30 +++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/server/registry/oopDownloadBrowserMain.ts b/packages/playwright-core/src/server/registry/oopDownloadBrowserMain.ts index 9c59d0e6f..ba227584d 100644 --- a/packages/playwright-core/src/server/registry/oopDownloadBrowserMain.ts +++ b/packages/playwright-core/src/server/registry/oopDownloadBrowserMain.ts @@ -46,6 +46,7 @@ function browserDirectoryToMarkerFilePath(browserDirectory: string): string { function downloadFile(options: DownloadParams): Promise { let downloadedBytes = 0; let totalBytes = 0; + let chunked = false; const promise = new ManualPromise(); httpRequest({ @@ -70,11 +71,15 @@ function downloadFile(options: DownloadParams): Promise { .on('error', handleError); return; } + + chunked = response.headers['transfer-encoding'] === 'chunked'; + log(`-- is chunked: ${chunked}`); + totalBytes = parseInt(response.headers['content-length'] || '0', 10); log(`-- total bytes: ${totalBytes}`); const file = fs.createWriteStream(options.zipPath); file.on('finish', () => { - if (downloadedBytes !== totalBytes) { + if (!chunked && downloadedBytes !== totalBytes) { log(`-- download failed, size mismatch: ${downloadedBytes} != ${totalBytes}`); promise.reject(new Error(`Download failed: size mismatch, file size: ${downloadedBytes}, expected size: ${totalBytes} URL: ${options.url}`)); } else { @@ -100,7 +105,8 @@ function downloadFile(options: DownloadParams): Promise { function onData(chunk: string) { downloadedBytes += chunk.length; - progress(downloadedBytes, totalBytes); + if (!chunked) + progress(downloadedBytes, totalBytes); } } diff --git a/tests/installation/playwright-cdn.spec.ts b/tests/installation/playwright-cdn.spec.ts index 1ebb49a01..a0d2ae6d6 100644 --- a/tests/installation/playwright-cdn.spec.ts +++ b/tests/installation/playwright-cdn.spec.ts @@ -24,12 +24,12 @@ const CDNS = [ 'https://cdn.playwright.dev', ]; -const DL_STAT_BLOCK = /^.*from url: (.*)$\n^.*to location: (.*)$\n^.*response status code: (.*)$\n^.*total bytes: (\d+)$\n^.*download complete, size: (\d+)$\n^.*SUCCESS downloading (\w+) .*$/gm; +const DL_STAT_BLOCK = /^.*from url: (.*)$\n^.*to location: (.*)$\n^.*response status code: (.*)$\n^.*is chunked: (.*)$\n^.*total bytes: (\d+)$\n^.*download complete, size: (\d+)$\n^.*SUCCESS downloading (\w+) .*$/gm; const parsedDownloads = (rawLogs: string) => { const out: { url: string, status: number, name: string }[] = []; for (const match of rawLogs.matchAll(DL_STAT_BLOCK)) { - const [, url, /* filepath */, status, /* size */, /* receivedBytes */, name] = match; + const [, url, /* filepath */, status, /* isChunked */, /* size */, /* receivedBytes */, name] = match; out.push({ url, status: Number.parseInt(status, 10), name: name.toLocaleLowerCase() }); } return out; diff --git a/tests/installation/playwright-cli-install-should-work.spec.ts b/tests/installation/playwright-cli-install-should-work.spec.ts index a4593a98b..2667564b9 100755 --- a/tests/installation/playwright-cli-install-should-work.spec.ts +++ b/tests/installation/playwright-cli-install-should-work.spec.ts @@ -16,7 +16,9 @@ import { test, expect } from './npmTest'; import { chromium } from '@playwright/test'; import path from 'path'; +import http from 'http'; import https from 'https'; +import { Writable } from 'stream'; import { TestProxy } from '../config/proxy'; import { TestServer } from '../config/testserver'; @@ -79,6 +81,34 @@ test('install command should work with HTTPS_PROXY', { annotation: { type: 'issu await proxy.stop(); }); +test('install command should work with mirror that uses chunked encoding', async ({ exec, checkInstalledSoftwareOnDisk }) => { + await exec('npm i playwright'); + const server = http.createServer(async (req, res) => { + try { + const upstream = await fetch('https://cdn.playwright.dev/dbazure/download/playwright' + req.url); + const headers = new Headers(upstream.headers); + headers.delete('content-length'); + res.writeHead(upstream.status, Object.fromEntries(headers)); + await upstream.body.pipeTo(Writable.toWeb(res)); + return; + } catch (e) { + console.error(e); + res.statusCode = 500; + res.end(String(e)); + return; + } + }); + await new Promise(resolve => server.listen(0, resolve)); + const result = await exec('npx playwright install chromium', { + env: { + PLAYWRIGHT_DOWNLOAD_HOST: `http://localhost:${(server.address() as any).port}`, + } + }); + expect(result).toHaveLoggedSoftwareDownload(['chromium', 'chromium-headless-shell', 'ffmpeg', ...extraInstalledSoftware]); + await checkInstalledSoftwareOnDisk(['chromium', 'chromium-headless-shell', 'ffmpeg', ...extraInstalledSoftware]); + server.close(); +}); + test('install command should ignore HTTP_PROXY', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/36412' } }, async ({ exec, checkInstalledSoftwareOnDisk }) => { await exec('npm i playwright'); await test.step('playwright install chromium', async () => { From 0927650254cc32229a57bea759ec77b54cf416b1 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 15 Oct 2025 06:30:33 -0700 Subject: [PATCH 068/250] chore(ct): add further checks to prevent React injection (#37847) --- packages/playwright-ct-core/src/viteUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/playwright-ct-core/src/viteUtils.ts b/packages/playwright-ct-core/src/viteUtils.ts index 2cb5ea4cf..f73d9e09c 100644 --- a/packages/playwright-ct-core/src/viteUtils.ts +++ b/packages/playwright-ct-core/src/viteUtils.ts @@ -166,11 +166,13 @@ export function hasJSComponents(components: ImportInfo[]): boolean { const importReactRE = /(^|\n|;)import\s+(\*\s+as\s+)?React(,|\s+)/; const compiledReactRE = /(const|var)\s+React\s*=/; +const runtimeImportRequire = /import\(['"`]react['"`]\)|require\(['"`]react['"`]\)/i; 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. // `__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)) { + 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) && !content.match(runtimeImportRequire)) { const code = `import React from 'react';\n${content}`; return { code, map: { mappings: '' } }; } From 7fef644b537db64733e4065d173192ee6944c813 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 15 Oct 2025 06:31:02 -0700 Subject: [PATCH 069/250] fix(ui-mode): recalculate dialog position (#37849) --- packages/web/src/shared/dialog.tsx | 6 ++++-- packages/web/src/uiUtils.ts | 24 +++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/web/src/shared/dialog.tsx b/packages/web/src/shared/dialog.tsx index b43984b09..7f0de8973 100644 --- a/packages/web/src/shared/dialog.tsx +++ b/packages/web/src/shared/dialog.tsx @@ -46,8 +46,8 @@ export const Dialog: React.FC> = ({ // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, setRecalculateDimensionsCount] = React.useState(0); - const dialogMeasure = useMeasureForRef(dialogRef); - const anchorMeasure = useMeasureForRef(anchor); + const [dialogMeasure] = useMeasureForRef(dialogRef); + const [anchorMeasure, recalculateAnchorMeasure] = useMeasureForRef(anchor); const position = dialogPosition(dialogMeasure, anchorMeasure, verticalOffset); React.useEffect(() => { @@ -77,6 +77,8 @@ export const Dialog: React.FC> = ({ return () => {}; }, [open, requestClose]); + React.useLayoutEffect(() => recalculateAnchorMeasure(), [open, recalculateAnchorMeasure]); + React.useEffect(() => { const onResize = () => setRecalculateDimensionsCount(count => count + 1); diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts index c24a1da6e..f7e94f1b5 100644 --- a/packages/web/src/uiUtils.ts +++ b/packages/web/src/uiUtils.ts @@ -38,26 +38,32 @@ export function useAsyncMemo(fn: () => Promise, deps: React.DependencyList // Tracks the element's bounding box. export function useMeasure() { const ref = React.useRef(null); - return [useMeasureForRef(ref), ref] as const; + const [measure] = useMeasureForRef(ref); + return [measure, ref] as const; } -export function useMeasureForRef(ref?: React.RefObject) { +export function useMeasureForRef(ref?: React.RefObject): [DOMRect, () => void] { const [measure, setMeasure] = React.useState(new DOMRect(0, 0, 10, 10)); + const recalculateMeasure = React.useCallback(() => { + const target = ref?.current; + if (target) + setMeasure(target.getBoundingClientRect()); + }, [ref]); + React.useLayoutEffect(() => { const target = ref?.current; if (!target) return; - const update = () => setMeasure(target.getBoundingClientRect()); - update(); - const resizeObserver = new ResizeObserver(update); + recalculateMeasure(); + const resizeObserver = new ResizeObserver(recalculateMeasure); resizeObserver.observe(target); - window.addEventListener('resize', update); + window.addEventListener('resize', recalculateMeasure); return () => { resizeObserver.disconnect(); - window.removeEventListener('resize', update); + window.removeEventListener('resize', recalculateMeasure); }; - }, [ref]); - return measure; + }, [recalculateMeasure, ref]); + return [measure, recalculateMeasure]; } export function msToString(ms: number): string { From 63d41d2ae75a0e8e26993dce9d827ed0608dcfee Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 15 Oct 2025 07:34:07 -0700 Subject: [PATCH 070/250] chore(test): hopefully fix ct tests on Windows (#37852) --- tests/components/test-all.spec.js | 36 ++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/tests/components/test-all.spec.js b/tests/components/test-all.spec.js index b67bb1b9e..a23d0f44c 100644 --- a/tests/components/test-all.spec.js +++ b/tests/components/test-all.spec.js @@ -1,10 +1,11 @@ const { test, expect } = require('@playwright/test'); -const { spawn } = require('child_process'); +const { spawn, execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); let activeChild = undefined; +const isWindows = process.platform === 'win32'; for (const dir of fs.readdirSync(__dirname)) { const folder = path.join(__dirname, dir); @@ -30,26 +31,41 @@ for (const dir of fs.readdirSync(__dirname)) { test.afterEach(async () => { // Make sure to kill server even if timeout occurs - if (activeChild) { - activeChild.kill(); - activeChild = undefined; - } + onExit(); }); async function run(command, args, folder) { const child = spawn(command, args, { cwd: folder, stdio: 'pipe', + env: process.env, shell: true, - env: process.env + // On non-windows platforms, `detached: true` makes child process a leader of a new + // process group, making it possible to kill child process tree with `.kill(-pid)` command. + // @see https://nodejs.org/api/child_process.html#child_process_options_detached + detached: !isWindows, }); activeChild = child; child.stdout.on('data', data => process.stdout.write(data)); child.stderr.on('data', data => process.stdout.write(data)); - process.on('exit', () => { - activeChild = undefined; - child.kill(); - }); + process.on('exit', onExit); const code = await new Promise(f => child.on('close', f)); expect(code).toEqual(0); } + +function onExit() { + if (activeChild) { + try { + if (activeChild.exitCode !== null || activeChild.signalCode !== null) + return; + + if (isWindows) { + execSync(`taskkill /pid ${activeChild.pid} /T /F`, { stdio: 'ignore' }); + } else { + process.kill(-activeChild.pid, 'SIGKILL'); + } + } finally { + activeChild = undefined; + } + } +} From 766a93b120ee436dee9fdd9c280ab2d990b03594 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 15 Oct 2025 19:37:51 +0100 Subject: [PATCH 071/250] chore: enable service worker networking by default (#37868) --- docs/src/network.md | 11 +- ...-workers-experimental-network-events-js.md | 235 ----- docs/src/service-workers-js.md | 132 +++ .../src/server/chromium/crServiceWorker.ts | 2 +- tests/library/chromium/chromium.spec.ts | 838 +++++++++--------- tests/library/chromium/extensions.spec.ts | 3 - 6 files changed, 572 insertions(+), 649 deletions(-) delete mode 100644 docs/src/service-workers-experimental-network-events-js.md create mode 100644 docs/src/service-workers-js.md diff --git a/docs/src/network.md b/docs/src/network.md index 6d0c676d6..7ee76e8c7 100644 --- a/docs/src/network.md +++ b/docs/src/network.md @@ -769,6 +769,11 @@ page.WebSocket += (_, ws) => Playwright's built-in [`method: BrowserContext.route`] and [`method: Page.route`] allow your tests to natively route requests and perform mocking and interception. -1. If you're using Playwright's native [`method: BrowserContext.route`] and [`method: Page.route`], and it appears network events are missing, disable Service Workers by setting [`option: Browser.newContext.serviceWorkers`] to `'block'`. -1. It might be that you are using a mock tool such as Mock Service Worker (MSW). While this tool works out of the box for mocking responses, it adds its own Service Worker that takes over the network requests, hence making them invisible to [`method: BrowserContext.route`] and [`method: Page.route`]. If you are interested in both network testing and mocking, consider using built-in [`method: BrowserContext.route`] and [`method: Page.route`] for [response mocking](#handle-requests). -1. If you're interested in not solely using Service Workers for testing and network mocking, but in routing and listening for requests made by Service Workers themselves, please see [this experimental feature](https://github.com/microsoft/playwright/issues/15684). +If you're using Playwright's native [`method: BrowserContext.route`] and [`method: Page.route`], and it appears network events are missing, disable Service Workers by setting [`option: Browser.newContext.serviceWorkers`] to `'block'`. + +It might be that you are using a mock tool such as Mock Service Worker (MSW). While this tool works out of the box for mocking responses, it adds its own Service Worker that takes over the network requests, hence making them invisible to [`method: BrowserContext.route`] and [`method: Page.route`]. If you are interested in both network testing and mocking, consider using built-in [`method: BrowserContext.route`] and [`method: Page.route`] for [response mocking](#handle-requests). + +###### +* langs: js + +If you're interested in not solely using Service Workers for testing and network mocking, but in routing and listening for requests made by Service Workers themselves, please see [this guide](./service-workers.md). diff --git a/docs/src/service-workers-experimental-network-events-js.md b/docs/src/service-workers-experimental-network-events-js.md deleted file mode 100644 index 43f32d7e6..000000000 --- a/docs/src/service-workers-experimental-network-events-js.md +++ /dev/null @@ -1,235 +0,0 @@ ---- -id: service-workers-experimental -title: "(Experimental) Service Worker Network Events" ---- - -## Introduction - -:::warning -If you're looking to do general network mocking, routing, and interception, please see the [Network Guide](./network.md) first. Playwright provides built-in APIs for this use case that don't require the information below. However, if you're interested in requests made by Service Workers themselves, please read below. -::: - -[Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) provide a browser-native method of handling requests made by a page with the native [Fetch API (`fetch`)](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) along with other network-requested assets (like scripts, css, and images). - -They can act as a **network proxy** between the page and the external network to perform caching logic or can provide users with an offline experience if the Service Worker adds a [FetchEvent](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent#examples) listener. - -Many sites that use Service Workers simply use them as a transparent optimization technique. While users might notice a faster experience, the app's implementation is unaware of their existence. Running the app with or without Service Workers enabled appears functionally equivalent. - -## How to Enable - -Playwright's inspection and routing of requests made by Service Workers are **experimental** and disabled by default. - -Set the `PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS` environment variable to `1` (or any other value) to enable the feature. Only Chrome/Chromium are currently supported. - -If you're using (or are interested in using this feature), please comment on [this issue](https://github.com/microsoft/playwright/issues/15684) letting us know your use case. - -## Service Worker Fetch - -### Accessing Service Workers and Waiting for Activation - -You can use [`method: BrowserContext.serviceWorkers`] to list the Service [Worker]s, or specifically watch for the Service [Worker] if you anticipate a page will trigger its [registration](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register): - -```js -const serviceWorkerPromise = context.waitForEvent('serviceworker'); -await page.goto('/example-with-a-service-worker.html'); -const serviceworker = await serviceWorkerPromise; -``` - -[`event: BrowserContext.serviceWorker`] is fired ***before*** the Service Worker's main script has been evaluated, so ***before*** calling service[`method: Worker.evaluate`] you should wait on its activation. - -There are more idiomatic methods of waiting for a Service Worker to be activated, but the following is an implementation agnostic method: - -```js -await page.evaluate(async () => { - const registration = await window.navigator.serviceWorker.getRegistration(); - if (registration.active?.state === 'activated') - return; - await new Promise(res => - window.navigator.serviceWorker.addEventListener('controllerchange', res), - ); -}); -``` - -### Network Events and Routing - -Any network request made by the **Service Worker** will have: - -* [`event: BrowserContext.request`] and its corresponding events ([`event: BrowserContext.requestFinished`] and [`event: BrowserContext.response`], or [`event: BrowserContext.requestFailed`]) -* [`method: BrowserContext.route`] will see the request -* [`method: Request.serviceWorker`] will be set to the Service [Worker] instance, and [`method: Request.frame`] will **throw** -* [`method: Response.fromServiceWorker`] will return `false` - -Additionally, any network request made by the **Page** (including its sub-[Frame]s) will have: - -* [`event: BrowserContext.request`] and its corresponding events ([`event: BrowserContext.requestFinished`] and [`event: BrowserContext.response`], or [`event: BrowserContext.requestFailed`]) -* [`event: Page.request`] and its corresponding events ([`event: Page.requestFinished`] and [`event: Page.response`], or [`event: Page.requestFailed`]) -* [`method: Page.route`] and [`method: Page.route`] will **not** see the request (if a Service Worker's fetch handler was registered) -* [`method: Request.serviceWorker`] will be set to `null`, and [`method: Request.frame`] will return the [Frame] -* [`method: Response.fromServiceWorker`] will return `true` (if a Service Worker's fetch handler was registered) - -Many Service Worker implementations simply execute the request from the page (possibly with some custom caching/offline logic omitted for simplicity): - -```js title="transparent-service-worker.js" -self.addEventListener('fetch', event => { - // actually make the request - const responsePromise = fetch(event.request); - // send it back to the page - event.respondWith(responsePromise); -}); - -self.addEventListener('activate', event => { - event.waitUntil(clients.claim()); -}); -``` - -If a page registers the above Service Worker: - -```html - - -``` - -On the first visit to the page via [`method: Page.goto`], the following Request/Response events would be emitted (along with the corresponding network lifecycle events): - -| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] | -| - | - | - | - | - | -| [`event: BrowserContext.request`] | [Frame] | index.html | Yes | | -| [`event: Page.request`] | [Frame] | index.html | Yes | | -| [`event: BrowserContext.request`] | Service [Worker] | transparent-service-worker.js | Yes | | -| [`event: BrowserContext.request`] | Service [Worker] | data.json | Yes | | -| [`event: BrowserContext.request`] | [Frame] | data.json | | Yes | -| [`event: Page.request`] | [Frame] | data.json | | Yes | - - - -Since the example Service Worker just acts a basic transparent "proxy": - -* There's 2 [`event: BrowserContext.request`] events for `data.json`; one [Frame]-owned, the other Service [Worker]-owned. -* Only the Service [Worker]-owned request for the resource was routable via [`method: BrowserContext.route`]; the [Frame]-owned events for `data.json` are not routeable, as they would not have even had the possibility to hit the external network since the Service Worker has a fetch handler registered. - -:::caution -It's important to note: calling [`method: Request.frame`] or [`method: Response.frame`] will **throw** an exception, if called on a [Request]/[Response] that has a non-null [`method: Request.serviceWorker`]. -::: - - -#### Advanced Example - -When a Service Worker handles a page's request, the Service Worker can make 0 to n requests to the external network. The Service Worker might respond directly from a cache, generate a response in memory, rewrite the request, make two requests and then combine into 1, etc. - -Consider the code snippets below to understand Playwright's view into the Request/Responses and how it impacts routing in some of these cases. - - -```js title="complex-service-worker.js" -self.addEventListener('install', function(event) { - event.waitUntil( - caches.open('v1').then(function(cache) { - // 1. Pre-fetches and caches /addressbook.json - return cache.add('/addressbook.json'); - }) - ); -}); - -// Opt to handle FetchEvent's from the page -self.addEventListener('fetch', event => { - event.respondWith( - (async () => { - // 1. Try to first serve directly from caches - const response = await caches.match(event.request); - if (response) - return response; - - // 2. Re-write request for /foo to /bar - if (event.request.url.endsWith('foo')) - return fetch('./bar'); - - // 3. Prevent tracker.js from being retrieved, and returns a placeholder response - if (event.request.url.endsWith('tracker.js')) { - return new Response('console.log("no trackers!")', { - status: 200, - headers: { 'Content-Type': 'text/javascript' }, - }); - } - - // 4. Otherwise, fallthrough, perform the fetch and respond - return fetch(event.request); - })() - ); -}); - -self.addEventListener('activate', event => { - event.waitUntil(clients.claim()); -}); -``` - -And a page that simply registers the Service Worker: - -```html - - -``` - -On the first visit to the page via [`method: Page.goto`], the following Request/Response events would be emitted: - -| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] | -| - | - | - | - | - | -| [`event: BrowserContext.request`] | [Frame] | index.html | Yes | | -| [`event: Page.request`] | [Frame] | index.html | Yes | | -| [`event: BrowserContext.request`] | Service [Worker] | complex-service-worker.js | Yes | | -| [`event: BrowserContext.request`] | Service [Worker] | addressbook.json | Yes | | - -It's important to note that [`cache.add`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/add) caused the Service Worker to make a request (Service [Worker]-owned), even before `addressbook.json` was asked for in the page. - -Once the Service Worker is activated and handling FetchEvents, if the page makes the following requests: - -```js -await page.evaluate(() => fetch('/addressbook.json')); -await page.evaluate(() => fetch('/foo')); -await page.evaluate(() => fetch('/tracker.js')); -await page.evaluate(() => fetch('/fallthrough.txt')); -``` - -The following Request/Response events would be emitted: - -| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] | -| - | - | - | - | - | -| [`event: BrowserContext.request`] | [Frame] | addressbook.json | | Yes | -| [`event: Page.request`] | [Frame] | addressbook.json | | Yes | -| [`event: BrowserContext.request`] | Service [Worker] | bar | Yes | | -| [`event: BrowserContext.request`] | [Frame] | foo | | Yes | -| [`event: Page.request`] | [Frame] | foo | | Yes | -| [`event: BrowserContext.request`] | [Frame] | tracker.js | | Yes | -| [`event: Page.request`] | [Frame] | tracker.js | | Yes | -| [`event: BrowserContext.request`] | Service [Worker] | fallthrough.txt | Yes | | -| [`event: BrowserContext.request`] | [Frame] | fallthrough.txt | | Yes | -| [`event: Page.request`] | [Frame] | fallthrough.txt | | Yes | - -It's important to note: - -* The page requested `/foo`, but the Service Worker requested `/bar`, so there are only [Frame]-owned events for `/foo`, but not `/bar`. -* Likewise, the Service Worker never hit the network for `tracker.js`, so only [Frame]-owned events were emitted for that request. - -## Routing Service Worker Requests Only - -```js -await context.route('**', async route => { - if (route.request().serviceWorker()) { - // NB: calling route.request().frame() here would THROW - return route.fulfill({ - contentType: 'text/plain', - status: 200, - body: 'from sw', - }); - } else { - return route.continue(); - } -}); -``` - -## Known Limitations - -Requests for updated Service Worker main script code currently cannot be routed (https://github.com/microsoft/playwright/issues/14711). - diff --git a/docs/src/service-workers-js.md b/docs/src/service-workers-js.md new file mode 100644 index 000000000..4db2e32a3 --- /dev/null +++ b/docs/src/service-workers-js.md @@ -0,0 +1,132 @@ +--- +id: service-workers +title: "Service Workers" +--- + +## Introduction + +:::warning +Service workers are only supported on Chromium-based browsers. +::: + + +:::note +If you're looking to do general network mocking, routing, and interception, please see the [Network Guide](./network.md) first. Playwright provides built-in APIs for this use case that don't require the information below. However, if you're interested in requests made by Service Workers themselves, please read below. +::: + +[Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) provide a browser-native method of handling requests made by a page with the native [Fetch API (`fetch`)](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) along with other network-requested assets (like scripts, css, and images). + +They can act as a **network proxy** between the page and the external network to perform caching logic or can provide users with an offline experience if the Service Worker adds a [FetchEvent](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent#examples) listener. + +Many sites that use Service Workers simply use them as a transparent optimization technique. While users might notice a faster experience, the app's implementation is unaware of their existence. Running the app with or without Service Workers enabled appears functionally equivalent. + +## How to Disable Service Workers + +Playwright allows to disable Service Workers during testing. This makes tests more predictable and performant. However, if your actual page uses a Service Worker, the behavior might be different. + +To disable service workers, set [`property: TestOptions.serviceWorkers`] to `'block'`. + +```js title="playwright.config.ts" +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + use: { + serviceWorkers: 'allow' + }, +}); +``` + +## Accessing Service Workers and Waiting for Activation + +You can use [`method: BrowserContext.serviceWorkers`] to list the Service [Worker]s, or specifically watch for the Service [Worker] if you anticipate a page will trigger its [registration](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register): + +```js +const serviceWorkerPromise = context.waitForEvent('serviceworker'); +await page.goto('/example-with-a-service-worker.html'); +const serviceworker = await serviceWorkerPromise; +``` + +[`event: BrowserContext.serviceWorker`] event is fired ***before*** the Service Worker has taken control over the page, so ***before*** evaluating in the worker with [`method: Worker.evaluate`] you should wait on its activation. + +There are more idiomatic methods of waiting for a Service Worker to be activated, but the following is an implementation agnostic method: + +```js +await page.evaluate(async () => { + const registration = await window.navigator.serviceWorker.getRegistration(); + if (registration.active?.state === 'activated') + return; + await new Promise(resolve => { + window.navigator.serviceWorker.addEventListener('controllerchange', resolve); + }); +}); +``` + +## Network Events and Routing + +Any network request made by the **Service Worker** is reported through the [BrowserContext] object: + +* [`event: BrowserContext.request`], [`event: BrowserContext.requestFinished`], [`event: BrowserContext.response`] and [`event: BrowserContext.requestFailed`] are fired +* [`method: BrowserContext.route`] sees the request +* [`method: Request.serviceWorker`] will be set to the Service [Worker] instance, and [`method: Request.frame`] will **throw** + +Additionally, for any network request made by the **Page**, method [`method: Response.fromServiceWorker`] return `true` when the request was handled a Service Worker's fetch handler. + +Consider a simple service worker that fetches every request made by the page: + +```js title="transparent-service-worker.js" +self.addEventListener('fetch', event => { + // actually make the request + const responsePromise = fetch(event.request); + // send it back to the page + event.respondWith(responsePromise); +}); + +self.addEventListener('activate', event => { + event.waitUntil(clients.claim()); +}); +``` + +If `index.html` registers this service worker, and then fetches `data.json`, the following Request/Response events would be emitted (along with the corresponding network lifecycle events): + +| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] | +| - | - | - | - | - | +| [`event: BrowserContext.request`] | [Frame] | index.html | Yes | | +| [`event: Page.request`] | [Frame] | index.html | Yes | | +| [`event: BrowserContext.request`] | Service [Worker] | transparent-service-worker.js | Yes | | +| [`event: BrowserContext.request`] | Service [Worker] | data.json | Yes | | +| [`event: BrowserContext.request`] | [Frame] | data.json | | Yes | +| [`event: Page.request`] | [Frame] | data.json | | Yes | + + + +Since the example Service Worker just acts a basic transparent "proxy": + +* There's 2 [`event: BrowserContext.request`] events for `data.json`; one [Frame]-owned, the other Service [Worker]-owned. +* Only the Service [Worker]-owned request for the resource was routable via [`method: BrowserContext.route`]; the [Frame]-owned events for `data.json` are not routeable, as they would not have even had the possibility to hit the external network since the Service Worker has a fetch handler registered. + +:::caution +It's important to note: calling [`method: Request.frame`] or [`method: Response.frame`] will **throw** an exception, if called on a [Request]/[Response] that has a non-null [`method: Request.serviceWorker`]. +::: + + +## Routing Service Worker Requests Only + +```js +await context.route('**', async route => { + if (route.request().serviceWorker()) { + // NB: calling route.request().frame() here would THROW + await route.fulfill({ + contentType: 'text/plain', + status: 200, + body: 'from sw', + }); + } else { + await route.continue(); + } +}); +``` + +## Known Limitations + +Requests for updated Service Worker main script code currently cannot be routed (https://github.com/microsoft/playwright/issues/14711). + diff --git a/packages/playwright-core/src/server/chromium/crServiceWorker.ts b/packages/playwright-core/src/server/chromium/crServiceWorker.ts index 23099867c..31cd98158 100644 --- a/packages/playwright-core/src/server/chromium/crServiceWorker.ts +++ b/packages/playwright-core/src/server/chromium/crServiceWorker.ts @@ -31,7 +31,7 @@ export class CRServiceWorker extends Worker { super(browserContext, url); this._session = session; this.browserContext = browserContext; - if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS) + if (!process.env.PLAYWRIGHT_DISABLE_SERVICE_WORKER_NETWORK) this._networkManager = new CRNetworkManager(null, this); session.once('Runtime.executionContextCreated', event => { this.createExecutionContext(new CRExecutionContext(session, event.context)); diff --git a/tests/library/chromium/chromium.spec.ts b/tests/library/chromium/chromium.spec.ts index f254ad3cf..5c55850bb 100644 --- a/tests/library/chromium/chromium.spec.ts +++ b/tests/library/chromium/chromium.spec.ts @@ -174,461 +174,455 @@ playwrightTest('should pass args with spaces', async ({ browserType, createUserD expect(userAgent).toBe('I am Foo'); }); -test.describe('PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1', () => { - test.skip(({ mode }) => mode !== 'default', 'Cannot set env variables in non-default'); - test.beforeAll(() => process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1'); - test.afterAll(() => delete process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS); - - test('serviceWorker(), and fromServiceWorker() work', async ({ context, page, server }) => { - const [worker, html, main, inWorker] = await Promise.all([ - context.waitForEvent('serviceworker'), - context.waitForEvent('request', r => r.url().endsWith('/sw.html')), - context.waitForEvent('request', r => r.url().endsWith('/sw.js')), - context.waitForEvent('request', r => r.url().endsWith('/request-from-within-worker.txt')), - page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html') - ]); +test('serviceWorker(), and fromServiceWorker() work', async ({ context, page, server }) => { + const [worker, html, main, inWorker] = await Promise.all([ + context.waitForEvent('serviceworker'), + context.waitForEvent('request', r => r.url().endsWith('/sw.html')), + context.waitForEvent('request', r => r.url().endsWith('/sw.js')), + context.waitForEvent('request', r => r.url().endsWith('/request-from-within-worker.txt')), + page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html') + ]); - expect(html.frame()).toBeTruthy(); - expect(html.serviceWorker()).toBe(null); - expect((await html.response()).fromServiceWorker()).toBe(false); + expect(html.frame()).toBeTruthy(); + expect(html.serviceWorker()).toBe(null); + expect((await html.response()).fromServiceWorker()).toBe(false); - expect(main.frame).toThrow(); - expect(main.serviceWorker()).toBe(worker); - expect((await main.response()).fromServiceWorker()).toBe(false); + expect(main.frame).toThrow(); + expect(main.serviceWorker()).toBe(worker); + expect((await main.response()).fromServiceWorker()).toBe(false); - expect(inWorker.frame).toThrow(); - expect(inWorker.serviceWorker()).toBe(worker); - expect((await inWorker.response()).fromServiceWorker()).toBe(false); + expect(inWorker.frame).toThrow(); + expect(inWorker.serviceWorker()).toBe(worker); + expect((await inWorker.response()).fromServiceWorker()).toBe(false); - await page.evaluate(() => window['activationPromise']); - const [innerSW, innerPage] = await Promise.all([ - context.waitForEvent('request', r => r.url().endsWith('/inner.txt') && !!r.serviceWorker()), - context.waitForEvent('request', r => r.url().endsWith('/inner.txt') && !r.serviceWorker()), - page.evaluate(() => fetch('/inner.txt')), - ]); - expect(innerPage.serviceWorker()).toBe(null); - expect((await innerPage.response()).fromServiceWorker()).toBe(true); + await page.evaluate(() => window['activationPromise']); + const [innerSW, innerPage] = await Promise.all([ + context.waitForEvent('request', r => r.url().endsWith('/inner.txt') && !!r.serviceWorker()), + context.waitForEvent('request', r => r.url().endsWith('/inner.txt') && !r.serviceWorker()), + page.evaluate(() => fetch('/inner.txt')), + ]); + expect(innerPage.serviceWorker()).toBe(null); + expect((await innerPage.response()).fromServiceWorker()).toBe(true); - expect(innerSW.serviceWorker()).toBe(worker); - expect((await innerSW.response()).fromServiceWorker()).toBe(false); - }); + expect(innerSW.serviceWorker()).toBe(worker); + expect((await innerSW.response()).fromServiceWorker()).toBe(false); +}); - test('should intercept service worker requests (main and within)', async ({ context, page, server }) => { - await context.route('**/request-from-within-worker', route => - route.fulfill({ - contentType: 'application/json', - status: 200, - body: '"intercepted!"', - }) - ); +test('should intercept service worker requests (main and within)', async ({ context, page, server }) => { + await context.route('**/request-from-within-worker', route => + route.fulfill({ + contentType: 'application/json', + status: 200, + body: '"intercepted!"', + }) + ); + + await context.route('**/sw.js', route => + route.fulfill({ + contentType: 'text/javascript', + status: 200, + body: ` + self.contentPromise = new Promise(res => fetch('/request-from-within-worker').then(r => r.json()).then(res)); + `, + }) + ); - await context.route('**/sw.js', route => - route.fulfill({ - contentType: 'text/javascript', - status: 200, - body: ` - self.contentPromise = new Promise(res => fetch('/request-from-within-worker').then(r => r.json()).then(res)); - `, - }) - ); + const [sw] = await Promise.all([ + context.waitForEvent('serviceworker'), + context.waitForEvent('response', r => r.url().endsWith('/request-from-within-worker')), + context.waitForEvent('request', r => r.url().endsWith('sw.js') && !!r.serviceWorker()), + context.waitForEvent('response', r => r.url().endsWith('sw.js') && !r.fromServiceWorker()), + page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), + ]); - const [sw] = await Promise.all([ - context.waitForEvent('serviceworker'), - context.waitForEvent('response', r => r.url().endsWith('/request-from-within-worker')), - context.waitForEvent('request', r => r.url().endsWith('sw.js') && !!r.serviceWorker()), - context.waitForEvent('response', r => r.url().endsWith('sw.js') && !r.fromServiceWorker()), - page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), - ]); + await expect(sw.evaluate(() => self['contentPromise'])).resolves.toBe('intercepted!'); +}); - await expect(sw.evaluate(() => self['contentPromise'])).resolves.toBe('intercepted!'); +test('should report failure (due to content-type) of main service worker request', async ({ server, page, context, browserMajorVersion }) => { + test.skip(browserMajorVersion < 104, 'Requires http://crrev.com/1012503 or later.'); + server.setRoute('/serviceworkers/fetch/sw.js', (req, res) => { + res.writeHead(200, 'OK', { 'Content-Type': 'text/html' }); + res.write(`console.log('hi from sw');`); + res.end(); }); + const [, main] = await Promise.all([ + server.waitForRequest('/serviceworkers/fetch/sw.js'), + context.waitForEvent('request', r => r.url().endsWith('sw.js')), + page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html'), + ]); + // This will timeout today + await main.response(); +}); - test('should report failure (due to content-type) of main service worker request', async ({ server, page, context, browserMajorVersion }) => { - test.skip(browserMajorVersion < 104, 'Requires http://crrev.com/1012503 or later.'); - server.setRoute('/serviceworkers/fetch/sw.js', (req, res) => { - res.writeHead(200, 'OK', { 'Content-Type': 'text/html' }); - res.write(`console.log('hi from sw');`); - res.end(); - }); - const [, main] = await Promise.all([ - server.waitForRequest('/serviceworkers/fetch/sw.js'), - context.waitForEvent('request', r => r.url().endsWith('sw.js')), - page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html'), - ]); - // This will timeout today - await main.response(); - }); +test('should report failure (due to redirect) of main service worker request', async ({ server, page, context, browserMajorVersion }) => { + test.skip(browserMajorVersion < 104, 'Requires http://crrev.com/1012503 or later.'); + server.setRedirect('/serviceworkers/empty/sw.js', '/dev/null'); + const [, main] = await Promise.all([ + server.waitForRequest('/serviceworkers/empty/sw.js'), + context.waitForEvent('request', r => r.url().endsWith('sw.js')), + page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), + ]); + // This will timeout today + const resp = await main.response(); + expect(resp.status()).toBe(302); +}); - test('should report failure (due to redirect) of main service worker request', async ({ server, page, context, browserMajorVersion }) => { - test.skip(browserMajorVersion < 104, 'Requires http://crrev.com/1012503 or later.'); - server.setRedirect('/serviceworkers/empty/sw.js', '/dev/null'); - const [, main] = await Promise.all([ - server.waitForRequest('/serviceworkers/empty/sw.js'), - context.waitForEvent('request', r => r.url().endsWith('sw.js')), - page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), - ]); - // This will timeout today - const resp = await main.response(); - expect(resp.status()).toBe(302); - }); +test('should intercept service worker importScripts', async ({ context, page, server }) => { + await context.route('**/import.js', route => + route.fulfill({ + contentType: 'text/javascript', + status: 200, + body: 'self.exportedValue = 47;', + }) + ); + + await context.route('**/sw.js', route => + route.fulfill({ + contentType: 'text/javascript', + status: 200, + body: ` + importScripts('/import.js'); + self.importedValue = self.exportedValue; + `, + }) + ); - test('should intercept service worker importScripts', async ({ context, page, server }) => { - await context.route('**/import.js', route => - route.fulfill({ - contentType: 'text/javascript', - status: 200, - body: 'self.exportedValue = 47;', - }) - ); + const [sw] = await Promise.all([ + context.waitForEvent('serviceworker'), + context.waitForEvent('response', r => r.url().endsWith('/import.js')), + page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), + ]); - await context.route('**/sw.js', route => - route.fulfill({ - contentType: 'text/javascript', - status: 200, - body: ` - importScripts('/import.js'); - self.importedValue = self.exportedValue; - `, - }) - ); - - const [sw] = await Promise.all([ - context.waitForEvent('serviceworker'), - context.waitForEvent('response', r => r.url().endsWith('/import.js')), - page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), - ]); + await expect(sw.evaluate(() => self['importedValue'])).resolves.toBe(47); +}); - await expect(sw.evaluate(() => self['importedValue'])).resolves.toBe(47); - }); +test('should report intercepted service worker requests in HAR', async ({ pageWithHar, server }) => { + const { context, page, getLog } = await pageWithHar(); + await context.route('**/request-from-within-worker', route => + route.fulfill({ + contentType: 'application/json', + headers: { + 'x-pw-test': 'request-within-worker', + }, + status: 200, + body: '"intercepted!"', + }) + ); + + await context.route('**/sw.js', route => + route.fulfill({ + contentType: 'text/javascript', + headers: { + 'x-pw-test': 'intercepted-main', + }, + status: 200, + body: ` + self.contentPromise = new Promise(res => fetch('/request-from-within-worker').then(r => r.json()).then(res)); + `, + }) + ); - test('should report intercepted service worker requests in HAR', async ({ pageWithHar, server }) => { - const { context, page, getLog } = await pageWithHar(); - await context.route('**/request-from-within-worker', route => - route.fulfill({ - contentType: 'application/json', - headers: { - 'x-pw-test': 'request-within-worker', - }, - status: 200, - body: '"intercepted!"', - }) - ); - - await context.route('**/sw.js', route => - route.fulfill({ - contentType: 'text/javascript', - headers: { - 'x-pw-test': 'intercepted-main', - }, - status: 200, - body: ` - self.contentPromise = new Promise(res => fetch('/request-from-within-worker').then(r => r.json()).then(res)); - `, - }) - ); + const [sw] = await Promise.all([ + context.waitForEvent('serviceworker'), + context.waitForEvent('response', r => r.url().endsWith('/request-from-within-worker')), + page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), + ]); - const [sw] = await Promise.all([ - context.waitForEvent('serviceworker'), - context.waitForEvent('response', r => r.url().endsWith('/request-from-within-worker')), - page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), - ]); + await expect(sw.evaluate(() => self['contentPromise'])).resolves.toBe('intercepted!'); - await expect(sw.evaluate(() => self['contentPromise'])).resolves.toBe('intercepted!'); + const log = await getLog(); + { + const sw = log.entries.filter(e => e.request.url.endsWith('sw.js')); + expect.soft(sw).toHaveLength(1); + expect.soft(sw[0].response.headers.filter(v => v.name === 'x-pw-test')).toEqual([{ name: 'x-pw-test', value: 'intercepted-main' }]); + } + { + const req = log.entries.filter(e => e.request.url.endsWith('request-from-within-worker')); + expect.soft(req).toHaveLength(1); + expect.soft(req[0].response.headers.filter(v => v.name === 'x-pw-test')).toEqual([{ name: 'x-pw-test', value: 'request-within-worker' }]); + expect.soft(req[0].response.content.text).toBe('"intercepted!"'); + } +}); - const log = await getLog(); - { - const sw = log.entries.filter(e => e.request.url.endsWith('sw.js')); - expect.soft(sw).toHaveLength(1); - expect.soft(sw[0].response.headers.filter(v => v.name === 'x-pw-test')).toEqual([{ name: 'x-pw-test', value: 'intercepted-main' }]); - } - { - const req = log.entries.filter(e => e.request.url.endsWith('request-from-within-worker')); - expect.soft(req).toHaveLength(1); - expect.soft(req[0].response.headers.filter(v => v.name === 'x-pw-test')).toEqual([{ name: 'x-pw-test', value: 'request-within-worker' }]); - expect.soft(req[0].response.content.text).toBe('"intercepted!"'); +test('should intercept only serviceworker request, not page', async ({ context, page, server }) => { + await context.route('**/data.json', async route => { + if (route.request().serviceWorker()) { + return route.fulfill({ + contentType: 'text/plain', + status: 200, + body: 'from sw', + }); + } else { + return route.continue(); } }); - test('should intercept only serviceworker request, not page', async ({ context, page, server }) => { - await context.route('**/data.json', async route => { - if (route.request().serviceWorker()) { - return route.fulfill({ - contentType: 'text/plain', - status: 200, - body: 'from sw', - }); - } else { - return route.continue(); - } - }); + const [sw] = await Promise.all([ + context.waitForEvent('serviceworker'), + page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html'), + ]); + await page.evaluate(() => window['activationPromise']); + const response = await page.evaluate(() => fetch('/data.json').then(r => r.text())); + const [url] = await sw.evaluate(() => self['intercepted']); + expect(url).toMatch(/\/data\.json$/); + expect(response).toBe('from sw'); +}); - const [sw] = await Promise.all([ - context.waitForEvent('serviceworker'), - page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html'), - ]); - await page.evaluate(() => window['activationPromise']); - const response = await page.evaluate(() => fetch('/data.json').then(r => r.text())); - const [url] = await sw.evaluate(() => self['intercepted']); - expect(url).toMatch(/\/data\.json$/); - expect(response).toBe('from sw'); +test('should produce network events, routing, and annotations for Service Worker', async ({ page, context, server }) => { + server.setRoute('/index.html', (req, res) => { + res.write(` + + `); + res.end(); }); + server.setRoute('/transparent-service-worker.js', (req, res) => { + res.writeHead(200, 'OK', { 'Content-Type': 'text/javascript' }); + res.write(` + self.addEventListener("fetch", (event) => { + // actually make the request + const responsePromise = fetch(event.request); + // send it back to the page + event.respondWith(responsePromise); + }); - test('should produce network events, routing, and annotations for Service Worker', async ({ page, context, server }) => { - server.setRoute('/index.html', (req, res) => { - res.write(` - - `); - res.end(); - }); - server.setRoute('/transparent-service-worker.js', (req, res) => { - res.writeHead(200, 'OK', { 'Content-Type': 'text/javascript' }); - res.write(` - self.addEventListener("fetch", (event) => { - // actually make the request - const responsePromise = fetch(event.request); - // send it back to the page - event.respondWith(responsePromise); - }); - - self.addEventListener("activate", (event) => { - event.waitUntil(clients.claim()); - }); - `); - res.end(); - }); + self.addEventListener("activate", (event) => { + event.waitUntil(clients.claim()); + }); + `); + res.end(); + }); - const routed = []; - const formatRequest = async ([scope, r]: ['page' | 'context', any]) => `| ${(scope === 'page' ? '[`event: Page.request`]' : '[`event: BrowserContext.request`]').padEnd('[`event: BrowserContext.request`]'.length, ' ')} | ${r.serviceWorker() ? 'Service [Worker]' : '[Frame]'.padEnd('Service [Worker]'.length, ' ')} | ${r.url().split('/').pop().padEnd(30, ' ')} | ${(routed.includes(r) ? 'Yes' : '').padEnd('Routed'.length, ' ')} | ${((await r.response()).fromServiceWorker() ? 'Yes' : '').padEnd('[`method: Response.fromServiceWorker`]'.length, ' ')} |`; - await context.route('**', async route => { - routed.push(route.request()); - await route.continue(); - }); - await page.route('**', async route => { - routed.push(route.request()); - await route.continue(); - }); - const requests = []; - page.on('request', r => requests.push(['page', r])); - context.on('request', r => requests.push(['context', r])); + const routed = []; + const formatRequest = async ([scope, r]: ['page' | 'context', any]) => `| ${(scope === 'page' ? '[`event: Page.request`]' : '[`event: BrowserContext.request`]').padEnd('[`event: BrowserContext.request`]'.length, ' ')} | ${r.serviceWorker() ? 'Service [Worker]' : '[Frame]'.padEnd('Service [Worker]'.length, ' ')} | ${r.url().split('/').pop().padEnd(30, ' ')} | ${(routed.includes(r) ? 'Yes' : '').padEnd('Routed'.length, ' ')} | ${((await r.response()).fromServiceWorker() ? 'Yes' : '').padEnd('[`method: Response.fromServiceWorker`]'.length, ' ')} |`; + await context.route('**', async route => { + routed.push(route.request()); + await route.continue(); + }); + await page.route('**', async route => { + routed.push(route.request()); + await route.continue(); + }); + const requests = []; + page.on('request', r => requests.push(['page', r])); + context.on('request', r => requests.push(['context', r])); - const [sw] = await Promise.all([ - context.waitForEvent('serviceworker'), - page.goto(server.PREFIX + '/index.html'), - ]); + const [sw] = await Promise.all([ + context.waitForEvent('serviceworker'), + page.goto(server.PREFIX + '/index.html'), + ]); + + await expect.poll(() => sw.evaluate(() => (self as any).registration.active?.state)).toBe('activated'); + + await page.evaluate(() => fetch('/data.json')); + + expect([ + '| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] |', + ...await Promise.all(requests.map(formatRequest))]) + .toEqual([ + '| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] |', + '| [`event: BrowserContext.request`] | [Frame] | index.html | Yes | |', + '| [`event: Page.request`] | [Frame] | index.html | Yes | |', + '| [`event: BrowserContext.request`] | Service [Worker] | transparent-service-worker.js | Yes | |', + '| [`event: BrowserContext.request`] | Service [Worker] | data.json | Yes | |', + '| [`event: BrowserContext.request`] | [Frame] | data.json | | Yes |', + '| [`event: Page.request`] | [Frame] | data.json | | Yes |', + ]); +}); - await expect.poll(() => sw.evaluate(() => (self as any).registration.active?.state)).toBe('activated'); - - await page.evaluate(() => fetch('/data.json')); - - expect([ - '| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] |', - ...await Promise.all(requests.map(formatRequest))]) - .toEqual([ - '| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] |', - '| [`event: BrowserContext.request`] | [Frame] | index.html | Yes | |', - '| [`event: Page.request`] | [Frame] | index.html | Yes | |', - '| [`event: BrowserContext.request`] | Service [Worker] | transparent-service-worker.js | Yes | |', - '| [`event: BrowserContext.request`] | Service [Worker] | data.json | Yes | |', - '| [`event: BrowserContext.request`] | [Frame] | data.json | | Yes |', - '| [`event: Page.request`] | [Frame] | data.json | | Yes |', - ]); +test('should produce network events, routing, and annotations for Service Worker (advanced)', async ({ page, context, server }) => { + server.setRoute('/index.html', (req, res) => { + res.write(` + + `); + res.end(); }); + server.setRoute('/complex-service-worker.js', (req, res) => { + res.writeHead(200, 'OK', { 'Content-Type': 'text/javascript' }); + res.write(` + self.addEventListener("install", function (event) { + event.waitUntil( + caches.open("v1").then(function (cache) { + // 1. Pre-fetches and caches /addressbook.json + return cache.add("/addressbook.json"); + }) + ); + }); - test('should produce network events, routing, and annotations for Service Worker (advanced)', async ({ page, context, server }) => { - server.setRoute('/index.html', (req, res) => { - res.write(` - - `); - res.end(); - }); - server.setRoute('/complex-service-worker.js', (req, res) => { - res.writeHead(200, 'OK', { 'Content-Type': 'text/javascript' }); - res.write(` - self.addEventListener("install", function (event) { - event.waitUntil( - caches.open("v1").then(function (cache) { - // 1. Pre-fetches and caches /addressbook.json - return cache.add("/addressbook.json"); - }) - ); - }); - - // Opt to handle FetchEvent's from the page - self.addEventListener("fetch", (event) => { - event.respondWith( - (async () => { - // 1. Try to first serve directly from caches - let response = await caches.match(event.request); - if (response) return response; - - // 2. Re-write request for /foo to /bar - if (event.request.url.endsWith("foo")) return fetch("./bar"); - - // 3. Prevent tracker.js from being retrieved, and returns a placeholder response - if (event.request.url.endsWith("tracker.js")) - return new Response('console.log("no trackers!")', { - status: 200, - headers: { "Content-Type": "text/javascript" }, - }); - - // 4. Otherwise, fallthrough, perform the fetch and respond - return fetch(event.request); - })() - ); - }); - - self.addEventListener("activate", (event) => { - event.waitUntil(clients.claim()); - }); - `); - res.end(); - }); - server.setRoute('/addressbook.json', (req, res) => { - res.write('{}'); - res.end(); - }); + // Opt to handle FetchEvent's from the page + self.addEventListener("fetch", (event) => { + event.respondWith( + (async () => { + // 1. Try to first serve directly from caches + let response = await caches.match(event.request); + if (response) return response; + + // 2. Re-write request for /foo to /bar + if (event.request.url.endsWith("foo")) return fetch("./bar"); + + // 3. Prevent tracker.js from being retrieved, and returns a placeholder response + if (event.request.url.endsWith("tracker.js")) + return new Response('console.log("no trackers!")', { + status: 200, + headers: { "Content-Type": "text/javascript" }, + }); - const routed = []; - const formatRequest = async ([scope, r]: ['page' | 'context', any]) => `| ${(scope === 'page' ? '[`event: Page.request`]' : '[`event: BrowserContext.request`]').padEnd('[`event: BrowserContext.request`]'.length, ' ')} | ${r.serviceWorker() ? 'Service [Worker]' : '[Frame]'.padEnd('Service [Worker]'.length, ' ')} | ${r.url().split('/').pop().padEnd(30, ' ')} | ${(routed.includes(r) ? 'Yes' : '').padEnd('Routed'.length, ' ')} | ${((await r.response()).fromServiceWorker() ? 'Yes' : '').padEnd('[`method: Response.fromServiceWorker`]'.length, ' ')} |`; - await context.route('**', async route => { - routed.push(route.request()); - await route.continue(); - }); - await page.route('**', async route => { - routed.push(route.request()); - await route.continue(); - }); - const requests = []; - page.on('request', r => requests.push(['page', r])); - context.on('request', r => requests.push(['context', r])); + // 4. Otherwise, fallthrough, perform the fetch and respond + return fetch(event.request); + })() + ); + }); - const [sw] = await Promise.all([ - context.waitForEvent('serviceworker'), - page.goto(server.PREFIX + '/index.html'), - ]); + self.addEventListener("activate", (event) => { + event.waitUntil(clients.claim()); + }); + `); + res.end(); + }); + server.setRoute('/addressbook.json', (req, res) => { + res.write('{}'); + res.end(); + }); - await expect.poll(() => sw.evaluate(() => (self as any).registration.active?.state)).toBe('activated'); - - await page.evaluate(() => fetch('/addressbook.json')); - await page.evaluate(() => fetch('/foo')); - await page.evaluate(() => fetch('/tracker.js')); - await page.evaluate(() => fetch('/fallthrough.txt')); - - expect([ - '| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] |', - ...await Promise.all(requests.map(formatRequest))]) - .toEqual([ - '| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] |', - '| [`event: BrowserContext.request`] | [Frame] | index.html | Yes | |', - '| [`event: Page.request`] | [Frame] | index.html | Yes | |', - '| [`event: BrowserContext.request`] | Service [Worker] | complex-service-worker.js | Yes | |', - '| [`event: BrowserContext.request`] | Service [Worker] | addressbook.json | Yes | |', - '| [`event: BrowserContext.request`] | [Frame] | addressbook.json | | Yes |', - '| [`event: Page.request`] | [Frame] | addressbook.json | | Yes |', - '| [`event: BrowserContext.request`] | Service [Worker] | bar | Yes | |', - '| [`event: BrowserContext.request`] | [Frame] | foo | | Yes |', - '| [`event: Page.request`] | [Frame] | foo | | Yes |', - '| [`event: BrowserContext.request`] | [Frame] | tracker.js | | Yes |', - '| [`event: Page.request`] | [Frame] | tracker.js | | Yes |', - '| [`event: BrowserContext.request`] | Service [Worker] | fallthrough.txt | Yes | |', - '| [`event: BrowserContext.request`] | [Frame] | fallthrough.txt | | Yes |', - '| [`event: Page.request`] | [Frame] | fallthrough.txt | | Yes |']); + const routed = []; + const formatRequest = async ([scope, r]: ['page' | 'context', any]) => `| ${(scope === 'page' ? '[`event: Page.request`]' : '[`event: BrowserContext.request`]').padEnd('[`event: BrowserContext.request`]'.length, ' ')} | ${r.serviceWorker() ? 'Service [Worker]' : '[Frame]'.padEnd('Service [Worker]'.length, ' ')} | ${r.url().split('/').pop().padEnd(30, ' ')} | ${(routed.includes(r) ? 'Yes' : '').padEnd('Routed'.length, ' ')} | ${((await r.response()).fromServiceWorker() ? 'Yes' : '').padEnd('[`method: Response.fromServiceWorker`]'.length, ' ')} |`; + await context.route('**', async route => { + routed.push(route.request()); + await route.continue(); + }); + await page.route('**', async route => { + routed.push(route.request()); + await route.continue(); }); + const requests = []; + page.on('request', r => requests.push(['page', r])); + context.on('request', r => requests.push(['context', r])); - test('should intercept service worker update requests', async ({ context, page, server }) => { - test.fixme(); - test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/14711' }); + const [sw] = await Promise.all([ + context.waitForEvent('serviceworker'), + page.goto(server.PREFIX + '/index.html'), + ]); - let version = 0; - server.setRoute('/worker.js', (req, res) => { - res.writeHead(200, 'OK', { 'Content-Type': 'text/javascript' }); - res.write(`self.PW_VERSION = ${version++};`); - res.end(); - }); + await expect.poll(() => sw.evaluate(() => (self as any).registration.active?.state)).toBe('activated'); + + await page.evaluate(() => fetch('/addressbook.json')); + await page.evaluate(() => fetch('/foo')); + await page.evaluate(() => fetch('/tracker.js')); + await page.evaluate(() => fetch('/fallthrough.txt')); + + expect([ + '| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] |', + ...await Promise.all(requests.map(formatRequest))]) + .toEqual([ + '| Event | Owner | URL | Routed | [`method: Response.fromServiceWorker`] |', + '| [`event: BrowserContext.request`] | [Frame] | index.html | Yes | |', + '| [`event: Page.request`] | [Frame] | index.html | Yes | |', + '| [`event: BrowserContext.request`] | Service [Worker] | complex-service-worker.js | Yes | |', + '| [`event: BrowserContext.request`] | Service [Worker] | addressbook.json | Yes | |', + '| [`event: BrowserContext.request`] | [Frame] | addressbook.json | | Yes |', + '| [`event: Page.request`] | [Frame] | addressbook.json | | Yes |', + '| [`event: BrowserContext.request`] | Service [Worker] | bar | Yes | |', + '| [`event: BrowserContext.request`] | [Frame] | foo | | Yes |', + '| [`event: Page.request`] | [Frame] | foo | | Yes |', + '| [`event: BrowserContext.request`] | [Frame] | tracker.js | | Yes |', + '| [`event: Page.request`] | [Frame] | tracker.js | | Yes |', + '| [`event: BrowserContext.request`] | Service [Worker] | fallthrough.txt | Yes | |', + '| [`event: BrowserContext.request`] | [Frame] | fallthrough.txt | | Yes |', + '| [`event: Page.request`] | [Frame] | fallthrough.txt | | Yes |']); +}); - server.setRoute('/home', (req, res) => { - res.write(` - - - - Service Worker Update Demo - - - - - - - `); - res.end(); - }); + let version = 0; + server.setRoute('/worker.js', (req, res) => { + res.writeHead(200, 'OK', { 'Content-Type': 'text/javascript' }); + res.write(`self.PW_VERSION = ${version++};`); + res.end(); + }); - const [sw] = await Promise.all([ - context.waitForEvent('serviceworker'), - page.goto(server.PREFIX + '/home'), - ]); + server.setRoute('/home', (req, res) => { + res.write(` + + + + Service Worker Update Demo + + + + + + + `); + res.end(); + }); - // Before triggering, let's intercept the update request - await context.route('**/worker.js', async route => { - await route.fulfill({ - status: 200, - body: `self.PW_VERSION = "intercepted";`, - contentType: 'text/javascript', - }); - }); + const [sw] = await Promise.all([ + context.waitForEvent('serviceworker'), + page.goto(server.PREFIX + '/home'), + ]); - const [updatedSW] = await Promise.all([ - context.waitForEvent('serviceworker'), - // currently times out here - context.waitForEvent('request', r => r.url().endsWith('worker.js')), - page.click('#update'), - ]); + await expect.poll(() => sw.evaluate(() => self['PW_VERSION'])).toBe(0); - await expect.poll(() => updatedSW.evaluate(() => self['PW_VERSION'])).toBe('intercepted'); + // Before triggering, let's intercept the update request + await context.route('**/worker.js', async route => { + await route.fulfill({ + status: 200, + body: `self.PW_VERSION = "intercepted";`, + contentType: 'text/javascript', + }); }); - test('setOffline', async ({ context, page, server }) => { - const [worker] = await Promise.all([ - context.waitForEvent('serviceworker'), - page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html') - ]); + const [updatedSW] = await Promise.all([ + context.waitForEvent('serviceworker'), + // currently times out here + context.waitForEvent('request', r => r.url().endsWith('worker.js')), + page.click('#update'), + ]); - await page.evaluate(() => window['activationPromise']); - await context.setOffline(true); - const [, error] = await Promise.all([ - context.waitForEvent('request', r => r.url().endsWith('/inner.txt') && !!r.serviceWorker()), - worker.evaluate(() => fetch('/inner.txt').catch(e => `REJECTED: ${e}`)), - ]); - expect(error).toMatch(/REJECTED.*Failed to fetch/); - }); + await expect.poll(() => updatedSW.evaluate(() => self['PW_VERSION'])).toBe('intercepted'); +}); - test('setExtraHTTPHeaders', async ({ context, page, server }) => { - const [worker] = await Promise.all([ - context.waitForEvent('serviceworker'), - page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html') - ]); +test('setOffline', async ({ context, page, server }) => { + const [worker] = await Promise.all([ + context.waitForEvent('serviceworker'), + page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html') + ]); - await page.evaluate(() => window['activationPromise']); - await context.setExtraHTTPHeaders({ 'x-custom-header': 'custom!' }); - const requestPromise = server.waitForRequest('/inner.txt'); - await worker.evaluate(() => fetch('/inner.txt')); - const req = await requestPromise; - expect(req.headers['x-custom-header']).toBe('custom!'); - }); + await page.evaluate(() => window['activationPromise']); + await context.setOffline(true); + const [, error] = await Promise.all([ + context.waitForEvent('request', r => r.url().endsWith('/inner.txt') && !!r.serviceWorker()), + worker.evaluate(() => fetch('/inner.txt').catch(e => `REJECTED: ${e}`)), + ]); + expect(error).toMatch(/REJECTED.*Failed to fetch/); +}); + +test('setExtraHTTPHeaders', async ({ context, page, server }) => { + const [worker] = await Promise.all([ + context.waitForEvent('serviceworker'), + page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html') + ]); + + await page.evaluate(() => window['activationPromise']); + await context.setExtraHTTPHeaders({ 'x-custom-header': 'custom!' }); + const requestPromise = server.waitForRequest('/inner.txt'); + await worker.evaluate(() => fetch('/inner.txt')); + const req = await requestPromise; + expect(req.headers['x-custom-header']).toBe('custom!'); }); test('should throw when connecting twice to an already running persistent context (--remote-debugging-port)', async ({ browserType, createUserDataDir, platform, isHeadlessShell }) => { @@ -660,3 +654,33 @@ test('should throw when connecting twice to an already running persistent contex await browser.close(); } }); + +test('PLAYWRIGHT_DISABLE_SERVICE_WORKER_NETWORK', async ({ mode, context, page, server }) => { + test.skip(mode !== 'default', 'no env in non-default mode'); + process.env.PLAYWRIGHT_DISABLE_SERVICE_WORKER_NETWORK = '1'; + + const urls = []; + context.on('request', r => { + expect(r.serviceWorker()).toBe(null); + urls.push(r.url()); + }); + + await Promise.all([ + context.waitForEvent('serviceworker'), + page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html') + ]); + expect(urls).toEqual([ + server.PREFIX + '/serviceworkers/fetch/sw.html', + server.PREFIX + '/serviceworkers/fetch/style.css', + ]); + + await page.evaluate(() => window['activationPromise']); + await page.evaluate(() => fetch('./inner.txt')); + expect(urls).toEqual([ + server.PREFIX + '/serviceworkers/fetch/sw.html', + server.PREFIX + '/serviceworkers/fetch/style.css', + server.PREFIX + '/serviceworkers/fetch/inner.txt', + ]); + + delete process.env.PLAYWRIGHT_DISABLE_SERVICE_WORKER_NETWORK; +}); diff --git a/tests/library/chromium/extensions.spec.ts b/tests/library/chromium/extensions.spec.ts index feb88267f..a54719e33 100644 --- a/tests/library/chromium/extensions.spec.ts +++ b/tests/library/chromium/extensions.spec.ts @@ -70,8 +70,6 @@ it.describe('MV3', () => { }); it('should support request/response events in the service worker', async ({ launchPersistentContext, asset, server }) => { - it.fixme(true, 'Waiting for https://issues.chromium.org/u/1/issues/407795731 getting fixed.'); - process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1'; server.setRoute('/empty.html', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/html', 'x-response-foobar': 'BarFoo' }); res.end(`hello world!`); @@ -102,7 +100,6 @@ it.describe('MV3', () => { expect(await response.allHeaders()).toEqual(expect.objectContaining({ 'x-response-foobar': 'BarFoo' })); await context.close(); - delete process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS; }); it('should report console messages from content script', { From 9eefb6e7dc7eb42ba51d4f884d052b7cc04ea1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=8C=E3=81=A9=E3=82=89?= <61941819+ogadra@users.noreply.github.com> Date: Thu, 16 Oct 2025 03:45:01 +0900 Subject: [PATCH 072/250] fix: allow screenshot capture when filename is an empty string (#37678) --- .../src/mcp/browser/tools/screenshot.ts | 2 +- tests/mcp/screenshot.spec.ts | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/mcp/browser/tools/screenshot.ts b/packages/playwright/src/mcp/browser/tools/screenshot.ts index 0b357a7dc..596cb7d19 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({ throw new Error('fullPage cannot be used with element screenshots.'); const fileType = params.type || 'png'; - const fileName = await tab.context.outputFile(params.filename ?? dateAsFileName(fileType), { origin: 'llm', reason: 'Saving screenshot' }); + 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/tests/mcp/screenshot.spec.ts b/tests/mcp/screenshot.spec.ts index 18dbc5dbb..2fb287e81 100644 --- a/tests/mcp/screenshot.spec.ts +++ b/tests/mcp/screenshot.spec.ts @@ -176,6 +176,49 @@ test('browser_take_screenshot (default type should be png)', async ({ startClien 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 is empty string)', 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: '', + }, + })).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( + new RegExp(`^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({ From 1604a2e2556440b7105fad6d78c23e813323fde0 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 15 Oct 2025 11:51:01 -0700 Subject: [PATCH 073/250] feat: add Locator.description() getter (#37870) --- docs/src/api/class-locator.md | 48 +++++++++++++++++++ packages/playwright-client/types/types.d.ts | 17 +++++++ .../playwright-core/src/client/locator.ts | 6 ++- .../src/utils/isomorphic/locatorGenerators.ts | 28 ++++++++--- packages/playwright-core/types/types.d.ts | 17 +++++++ tests/page/locator-convenience.spec.ts | 29 +++++++++++ 6 files changed, 138 insertions(+), 7 deletions(-) diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index a5a92939b..ac86bae5a 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -620,6 +620,54 @@ await button.ClickAsync(); Locator description. +## method: Locator.description +* since: v1.57 +- returns: <[null]|[string]> + +Returns locator description previously set with [`method: Locator.describe`]. + +**Usage** + +```js +const button = page.getByRole('button').describe('Subscribe button'); +console.log(button.description()); // "Subscribe button" + +const input = page.getByRole('textbox'); +console.log(input.description()); // null +``` + +```python async +button = page.get_by_role("button").describe("Subscribe button") +print(button.description()) # "Subscribe button" + +input = page.get_by_role("textbox") +print(input.description()) # None +``` + +```python sync +button = page.get_by_role("button").describe("Subscribe button") +print(button.description()) # "Subscribe button" + +input = page.get_by_role("textbox") +print(input.description()) # None +``` + +```java +Locator button = page.getByRole(AriaRole.BUTTON).describe("Subscribe button"); +System.out.println(button.description()); // "Subscribe button" + +Locator input = page.getByRole(AriaRole.TEXTBOX); +System.out.println(input.description()); // null +``` + +```csharp +var button = Page.GetByRole(AriaRole.Button).Describe("Subscribe button"); +Console.WriteLine(button.Description()); // "Subscribe button" + +var input = Page.GetByRole(AriaRole.Textbox); +Console.WriteLine(input.Description()); // null +``` + ## async method: Locator.dispatchEvent * since: v1.14 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index a04bcc1fc..28c4b14a9 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -12936,6 +12936,23 @@ export interface Locator { */ describe(description: string): Locator; + /** + * Returns locator description previously set with + * [locator.describe(description)](https://playwright.dev/docs/api/class-locator#locator-describe). + * + * **Usage** + * + * ```js + * const button = page.getByRole('button').describe('Subscribe button'); + * console.log(button.description()); // "Subscribe button" + * + * const input = page.getByRole('textbox'); + * console.log(input.description()); // null + * ``` + * + */ + description(): null|string; + /** * Programmatically dispatch an event on the matching element. * diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index fd889c75f..db745c5d3 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -15,7 +15,7 @@ */ import { ElementHandle } from './elementHandle'; -import { asLocator } from '../utils/isomorphic/locatorGenerators'; +import { asLocator, locatorCustomDescription } from '../utils/isomorphic/locatorGenerators'; import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils'; import { escapeForTextSelector } from '../utils/isomorphic/stringUtils'; import { isString } from '../utils/isomorphic/rtti'; @@ -217,6 +217,10 @@ export class Locator implements api.Locator { return new Locator(this._frame, this._selector + ' >> internal:describe=' + JSON.stringify(description)); } + description(): string | null { + return locatorCustomDescription(this._selector) || null; + } + first(): Locator { return new Locator(this._frame, this._selector + ' >> nth=0'); } diff --git a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts index 58459d8ae..ae5885233 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts +++ b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts @@ -40,12 +40,9 @@ export interface LocatorFactory { export function asLocatorDescription(lang: Language, selector: string): string | undefined { try { const parsed = parseSelector(selector); - const lastPart = parsed.parts[parsed.parts.length - 1]; - if (lastPart?.name === 'internal:describe') { - const description = JSON.parse(lastPart.body as string); - if (typeof description === 'string') - return description; - } + const customDescription = parseCustomDescription(parsed); + if (customDescription) + return customDescription; return innerAsLocators(new generators[lang](), parsed, false, 1)[0]; } catch (e) { // Tolerate invalid input. @@ -53,6 +50,25 @@ export function asLocatorDescription(lang: Language, selector: string): string | } } +export function locatorCustomDescription(selector: string): string | undefined { + try { + const parsed = parseSelector(selector); + return parseCustomDescription(parsed); + } catch (e) { + return undefined; + } +} + +function parseCustomDescription(parsed: ParsedSelector): string | undefined { + const lastPart = parsed.parts[parsed.parts.length - 1]; + if (lastPart?.name === 'internal:describe') { + const description = JSON.parse(lastPart.body as string); + if (typeof description === 'string') + return description; + } + return undefined; +} + export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false): string { return asLocators(lang, selector, isFrameLocator, 1)[0]; } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index a04bcc1fc..28c4b14a9 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -12936,6 +12936,23 @@ export interface Locator { */ describe(description: string): Locator; + /** + * Returns locator description previously set with + * [locator.describe(description)](https://playwright.dev/docs/api/class-locator#locator-describe). + * + * **Usage** + * + * ```js + * const button = page.getByRole('button').describe('Subscribe button'); + * console.log(button.description()); // "Subscribe button" + * + * const input = page.getByRole('textbox'); + * console.log(input.description()); // null + * ``` + * + */ + description(): null|string; + /** * Programmatically dispatch an event on the matching element. * diff --git a/tests/page/locator-convenience.spec.ts b/tests/page/locator-convenience.spec.ts index d63b890fd..ec81f47d7 100644 --- a/tests/page/locator-convenience.spec.ts +++ b/tests/page/locator-convenience.spec.ts @@ -193,3 +193,32 @@ it('should return page', async ({ page, server }) => { const inFrame = page.frames()[1].locator('div'); expect(inFrame.page()).toBe(page); }); + +it('description should return null for locator without description', async ({ page }) => { + const locator = page.locator('button'); + expect(locator.description()).toBe(null); +}); + +it('description should return description for locator with simple description', async ({ page }) => { + const locator = page.locator('button').describe('Submit button'); + expect(locator.description()).toBe('Submit button'); +}); + +it('description should return description with special characters', async ({ page }) => { + const locator = page.locator('div').describe('Button with "quotes" and \'apostrophes\''); + expect(locator.description()).toBe('Button with "quotes" and \'apostrophes\''); +}); + +it('description should return description for chained locators', async ({ page }) => { + const locator = page.locator('form').locator('input').describe('Form input field'); + expect(locator.description()).toBe('Form input field'); +}); + +it('description should return description for locator with multiple describe calls', async ({ page }) => { + const locator1 = page.locator('foo').describe('First description'); + expect(locator1.description()).toBe('First description'); + const locator2 = locator1.locator('button').describe('Second description'); + expect(locator2.description()).toBe('Second description'); + const locator3 = locator2.locator('button'); + expect(locator3.description()).toBe(null); +}); From 9f1fcc6d5ae3f6d22613bf35beb1a0579edc2acb Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 15 Oct 2025 16:39:14 -0700 Subject: [PATCH 074/250] chore: generate coverage.md with init-agents (#37876) --- .../.claude/agents/playwright-test-healer.md | 4 +- .../.claude/agents/playwright-test-planner.md | 2 +- .../ \360\237\216\255 planner.chatmode.md" | 2 +- .../\360\237\216\255 healer.chatmode.md" | 4 +- examples/todomvc/prompts/README.md | 3 + examples/todomvc/prompts/test-coverage.md | 29 +++++ .../playwright/src/agents/generateAgents.ts | 110 +++++++++++++++--- packages/playwright/src/mcp/test/seed.ts | 32 ++--- .../playwright/src/mcp/test/testContext.ts | 4 +- packages/playwright/src/program.ts | 11 +- 10 files changed, 157 insertions(+), 44 deletions(-) create mode 100644 examples/todomvc/prompts/README.md create mode 100644 examples/todomvc/prompts/test-coverage.md diff --git a/examples/todomvc/.claude/agents/playwright-test-healer.md b/examples/todomvc/.claude/agents/playwright-test-healer.md index 61efa5a1f..0a31a92a5 100644 --- a/examples/todomvc/.claude/agents/playwright-test-healer.md +++ b/examples/todomvc/.claude/agents/playwright-test-healer.md @@ -11,8 +11,8 @@ resolving Playwright test failures. Your mission is to systematically identify, 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. +1. **Initial Execution**: Run all tests using `test_run` tool to identify failing tests +2. **Debug failed tests**: For each failing test run `test_debug`. 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 diff --git a/examples/todomvc/.claude/agents/playwright-test-planner.md b/examples/todomvc/.claude/agents/playwright-test-planner.md index 9e468a85d..283a88d7f 100644 --- a/examples/todomvc/.claude/agents/playwright-test-planner.md +++ b/examples/todomvc/.claude/agents/playwright-test-planner.md @@ -16,7 +16,7 @@ You will: - Invoke the `planner_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 + - 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** 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" index 5eca16632..ed3dc5e01 100644 --- "a/examples/todomvc/.github/chatmodes/ \360\237\216\255 planner.chatmode.md" +++ "b/examples/todomvc/.github/chatmodes/ \360\237\216\255 planner.chatmode.md" @@ -13,7 +13,7 @@ You will: - Invoke the `planner_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 + - 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** 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" index 6604d7403..936786a7c 100644 --- "a/examples/todomvc/.github/chatmodes/\360\237\216\255 healer.chatmode.md" +++ "b/examples/todomvc/.github/chatmodes/\360\237\216\255 healer.chatmode.md" @@ -8,8 +8,8 @@ resolving Playwright test failures. Your mission is to systematically identify, 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. +1. **Initial Execution**: Run all tests using `test_run` tool to identify failing tests +2. **Debug failed tests**: For each failing test run `test_debug`. 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 diff --git a/examples/todomvc/prompts/README.md b/examples/todomvc/prompts/README.md new file mode 100644 index 000000000..245787209 --- /dev/null +++ b/examples/todomvc/prompts/README.md @@ -0,0 +1,3 @@ +# Prompts + +This is a directory for useful prompts. diff --git a/examples/todomvc/prompts/test-coverage.md b/examples/todomvc/prompts/test-coverage.md new file mode 100644 index 000000000..09d36ebae --- /dev/null +++ b/examples/todomvc/prompts/test-coverage.md @@ -0,0 +1,29 @@ + +# Produce test coverage + +Parameters: +- Task: the task to perform +- Seed file (optional): the seed file to use, defaults to tests/seed.spec.ts +- Test plan file (optional): the test plan file to write, under specs/ folder. + +1. Call #planner subagent with prompt: + + + + + + + +2. For each test case from the test plan file (1.1, 1.2, ...), Call #generator subagent with prompt: + + + + + + + + + +3. Call #healer subagent with prompt: + +Run all tests and fix the failing ones one after another. diff --git a/packages/playwright/src/agents/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts index efa0ed080..b349ad923 100644 --- a/packages/playwright/src/agents/generateAgents.ts +++ b/packages/playwright/src/agents/generateAgents.ts @@ -16,7 +16,12 @@ import fs from 'fs'; import path from 'path'; + import { colors, yaml } from 'playwright-core/lib/utilsBundle'; +import { mkdirIfNeeded } from 'playwright-core/lib/utils'; + +import { FullConfigInternal } from '../common/config'; +import { defaultSeedFile, findSeedFile, seedFileContent, seedProject } from '../mcp/test/seed'; interface AgentHeader { name: string; @@ -33,6 +38,8 @@ interface Agent { examples: string[]; } +/* eslint-disable no-console */ + class AgentParser { static async parseFile(filePath: string): Promise { const rawMarkdown = await fs.promises.readFile(filePath, 'utf-8'); @@ -186,18 +193,55 @@ async function loadAgents(): Promise { return Promise.all(files.filter(file => file.endsWith('.md')).map(file => AgentParser.parseFile(path.join(__dirname, file)))); } -async function writeFile(filePath: string, content: string) { - // eslint-disable-next-line no-console - console.log(`Writing file: ${filePath}`); +async function writeFile(filePath: string, content: string, icon: string, description: string) { + console.log(`- ${icon} ${path.relative(process.cwd(), filePath)} ${colors.dim('- ' + description)}`); + await mkdirIfNeeded(filePath); await fs.promises.writeFile(filePath, content, 'utf-8'); } -export async function initClaudeCodeRepo() { +async function initRepo(config: FullConfigInternal, projectName: string) { + const project = seedProject(config, projectName); + console.log(`🎭 Using project "${project.project.name}" as a primary project`); + + if (!fs.existsSync('specs')) { + await fs.promises.mkdir('specs'); + await writeFile(path.join('specs', 'README.md'), `# Specs + +This is a directory for test plans. +`, '📝', 'directory for test plans'); + } + + if (!fs.existsSync('prompts')) { + await fs.promises.mkdir('prompts'); + await writeFile(path.join('prompts', 'README.md'), `# Prompts + +This is a directory for useful prompts. +`, '📝', 'useful prompts'); + } + + let seedFile = await findSeedFile(project); + if (!seedFile) { + seedFile = defaultSeedFile(project); + await writeFile(seedFile, seedFileContent, '🌱', 'default environment seed file'); + } + + const coveragePromptFile = path.join('prompts', 'test-coverage.md'); + if (!fs.existsSync(coveragePromptFile)) + await writeFile(coveragePromptFile, coveragePrompt(seedFile), '📝', 'test coverage prompt'); +} + +function initRepoDone() { + console.log('✅ Done.'); +} + +export async function initClaudeCodeRepo(config: FullConfigInternal, projectName: string) { + await initRepo(config, projectName); + const agents = await loadAgents(); await fs.promises.mkdir('.claude/agents', { recursive: true }); for (const agent of agents) - await writeFile(`.claude/agents/playwright-test-${agent.header.name}.md`, saveAsClaudeCode(agent)); + await writeFile(`.claude/agents/playwright-test-${agent.header.name}.md`, saveAsClaudeCode(agent), '🤖', 'agent definition'); await writeFile('.mcp.json', JSON.stringify({ mcpServers: { @@ -206,7 +250,9 @@ export async function initClaudeCodeRepo() { args: commonMcpServers.playwrightTest.args, } } - }, null, 2)); + }, null, 2), '🔧', 'mcp configuration'); + + initRepoDone(); } const vscodeToolMap = new Map([ @@ -251,12 +297,13 @@ function saveAsVSCodeChatmode(agent: Agent): string { return lines.join('\n'); } -export async function initVSCodeRepo() { +export async function initVSCodeRepo(config: FullConfigInternal, projectName: string) { + await initRepo(config, projectName); const agents = await loadAgents(); await fs.promises.mkdir('.github/chatmodes', { recursive: true }); for (const agent of agents) - await writeFile(`.github/chatmodes/${agent.header.name === 'planner' ? ' ' : ''}🎭 ${agent.header.name}.chatmode.md`, saveAsVSCodeChatmode(agent)); + await writeFile(`.github/chatmodes/${agent.header.name === 'planner' ? ' ' : ''}🎭 ${agent.header.name}.chatmode.md`, saveAsVSCodeChatmode(agent), '🤖', 'chatmode definition'); await fs.promises.mkdir('.vscode', { recursive: true }); @@ -279,12 +326,14 @@ export async function initVSCodeRepo() { args: commonMcpServers.playwrightTest.args, cwd: '${workspaceFolder}', }; - await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2)); - // eslint-disable-next-line no-console - console.log(colors.yellow(`${colors.bold('Note:')} Playwright Test Agents require VSCode version 1.105+ or VSCode Insiders`)); + await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2), '🔧', 'mcp configuration'); + + initRepoDone(); } -export async function initOpencodeRepo() { +export async function initOpencodeRepo(config: FullConfigInternal, projectName: string) { + await initRepo(config, projectName); + const agents = await loadAgents(); await fs.promises.mkdir('.opencode/prompts', { recursive: true }); @@ -292,7 +341,40 @@ export async function initOpencodeRepo() { const prompt = [agent.instructions]; prompt.push(''); prompt.push(...agent.examples.map(example => `${example}`)); - await writeFile(`.opencode/prompts/playwright-test-${agent.header.name}.md`, prompt.join('\n')); + await writeFile(`.opencode/prompts/playwright-test-${agent.header.name}.md`, prompt.join('\n'), '🤖', 'agent definition'); } - await writeFile('opencode.json', saveAsOpencodeJson(agents)); + await writeFile('opencode.json', saveAsOpencodeJson(agents), '🔧', 'opencode configuration'); + + initRepoDone(); } + +const coveragePrompt = (seedFile: string) => ` +# Produce test coverage + +Parameters: +- Task: the task to perform +- Seed file (optional): the seed file to use, defaults to ${path.relative(process.cwd(), seedFile)} +- Test plan file (optional): the test plan file to write, under specs/ folder. + +1. Call #planner subagent with prompt: + + + + + + + +2. For each test case from the test plan file (1.1, 1.2, ...), Call #generator subagent with prompt: + + + + + + + + + +3. Call #healer subagent with prompt: + +Run all tests and fix the failing ones one after another. +`; diff --git a/packages/playwright/src/mcp/test/seed.ts b/packages/playwright/src/mcp/test/seed.ts index 346ef1dfd..04ebd74b8 100644 --- a/packages/playwright/src/mcp/test/seed.ts +++ b/packages/playwright/src/mcp/test/seed.ts @@ -18,7 +18,6 @@ import fs from 'fs'; import path from 'path'; import { mkdirIfNeeded } from 'playwright-core/lib/utils'; - import { collectFilesForProject, findTopLevelProjects } from '../../runner/projectUtils'; import type { FullConfigInternal, FullProjectInternal } from '../../common/config'; @@ -32,28 +31,31 @@ export function seedProject(config: FullConfigInternal, projectName?: string) { return project; } -export async function ensureSeedTest(project: FullProjectInternal, logNew: boolean) { +export async function findSeedFile(project: FullProjectInternal) { const files = await collectFilesForProject(project); - const seed = files.find(file => path.basename(file).includes('seed')); - if (seed) - return seed; + return files.find(file => path.basename(file).includes('seed')); +} +export function defaultSeedFile(project: FullProjectInternal) { const testDir = project.project.testDir; - const seedFile = path.resolve(testDir, 'seed.spec.ts'); + return path.resolve(testDir, 'seed.spec.ts'); +} - if (logNew) { - // eslint-disable-next-line no-console - console.log(`Writing file: ${path.relative(process.cwd(), seedFile)}`); - } +export async function ensureSeedFile(project: FullProjectInternal) { + const seedFile = await findSeedFile(project); + if (seedFile) + return seedFile; + const seedFilePath = defaultSeedFile(project); + await mkdirIfNeeded(seedFilePath); + await fs.promises.writeFile(seedFilePath, seedFileContent); + return seedFilePath; +} - await mkdirIfNeeded(seedFile); - await fs.promises.writeFile(seedFile, `import { test, expect } from '@playwright/test'; +export const seedFileContent = `import { test, expect } from '@playwright/test'; test.describe('Test group', () => { test('seed', async ({ page }) => { // generate code here. }); }); -`); - return seedFile; -} +`; diff --git a/packages/playwright/src/mcp/test/testContext.ts b/packages/playwright/src/mcp/test/testContext.ts index a2204e24d..60a2d16b8 100644 --- a/packages/playwright/src/mcp/test/testContext.ts +++ b/packages/playwright/src/mcp/test/testContext.ts @@ -24,7 +24,7 @@ import ListReporter from '../../reporters/list'; import { StringWriteStream } from './streams'; import { fileExistsAsync } from '../../util'; import { TestRunner, TestRunnerEvent } from '../../runner/testRunner'; -import { ensureSeedTest, seedProject } from './seed'; +import { ensureSeedFile, seedProject } from './seed'; import type { ProgressCallback } from '../sdk/server'; import type { ConfigLocation } from '../../common/config'; @@ -110,7 +110,7 @@ export class TestContext { const project = seedProject(config, projectName); if (!seedFile) { - seedFile = await ensureSeedTest(project, false); + seedFile = await ensureSeedFile(project); } else { const candidateFiles: string[] = []; const testDir = project.project.testDir; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 7ea4813d2..cd9c8e801 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -35,7 +35,6 @@ 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 { ensureSeedTest, seedProject } from './mcp/test/seed'; import { decorateCommand } from './mcp/program'; import { setupExitWatchdog } from './mcp/browser/watchdog'; import { initClaudeCodeRepo, initOpencodeRepo, initVSCodeRepo } from './agents/generateAgents'; @@ -189,19 +188,17 @@ function addInitAgentsCommand(program: Command) { command.option('-c, --config ', `Configuration file to find a project to use for seed test`); command.option('--project ', 'Project to use for seed test'); command.action(async opts => { + const config = await loadConfigFromFile(opts.config); if (opts.loop === 'opencode') { - await initOpencodeRepo(); + await initOpencodeRepo(config, opts.project); } else if (opts.loop === 'vscode') { - await initVSCodeRepo(); + await initVSCodeRepo(config, opts.project); } else if (opts.loop === 'claude') { - await initClaudeCodeRepo(); + await initClaudeCodeRepo(config, opts.project); } else { command.help(); return; } - const config = await loadConfigFromFile(opts.config); - const project = seedProject(config, opts.project); - await ensureSeedTest(project, true); }); } From 499e1c6d52ce665d8a3ca25667197d4b0855ff2a Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 16 Oct 2025 06:54:50 -0700 Subject: [PATCH 075/250] chore(ci): remove tests_cft (#37884) --- .github/workflows/tests_cft.yml | 85 --------------------------------- 1 file changed, 85 deletions(-) delete mode 100644 .github/workflows/tests_cft.yml diff --git a/.github/workflows/tests_cft.yml b/.github/workflows/tests_cft.yml deleted file mode 100644 index b39914d39..000000000 --- a/.github/workflows/tests_cft.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: tests CfT - -on: - workflow_dispatch: - inputs: - ref: - description: Playwright SHA / ref to test. Use 'refs/pull/PULL_REQUEST_ID/head' to test a PR. Defaults to the current branch. - required: false - default: '' - -env: - FORCE_COLOR: 1 - -jobs: - test_cft: - permissions: - contents: read # This is required for actions/checkout to succeed - strategy: - fail-fast: false - matrix: - os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, macos-14-large, macos-14-xlarge, macos-15-large, macos-15-xlarge, windows-2022, windows-2025] - name: CfT ${{ matrix.os }} - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v5 - if: github.event_name != 'workflow_dispatch' - - uses: actions/checkout@v5 - if: github.event_name == 'workflow_dispatch' - with: - ref: ${{ github.event.inputs.ref }} - - uses: actions/setup-node@v5 - with: - node-version: 20 - - run: npm ci - - run: npm run build - - run: npx playwright install --with-deps chromium ffmpeg - if: contains(matrix.os, 'ubuntu') - - run: npx playwright install chromium ffmpeg - if: !contains(matrix.os, 'ubuntu') - - name: Install CfT beta - run: echo "CRPATH=$(npx -y @puppeteer/browsers install chrome@beta --with-deps | tail -n1 | sed 's/^[^ ]* *//')" >> "$GITHUB_ENV" - - name: Run tests linux - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run ctest - if: contains(matrix.os, 'ubuntu') - env: - PWTEST_CHANNEL: chromium - - name: Run tests non-linux - run: npm run ctest - if: !contains(matrix.os, 'ubuntu') - env: - PWTEST_CHANNEL: chromium - - test_cft_headless_shell: - permissions: - contents: read # This is required for actions/checkout to succeed - strategy: - fail-fast: false - matrix: - os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, macos-14-large, macos-14-xlarge, macos-15-large, macos-15-xlarge, windows-2022, windows-2025] - name: CfT headless shell ${{ matrix.os }} - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v5 - if: github.event_name != 'workflow_dispatch' - - uses: actions/checkout@v5 - if: github.event_name == 'workflow_dispatch' - with: - ref: ${{ github.event.inputs.ref }} - - uses: actions/setup-node@v5 - with: - node-version: 20 - - run: npm ci - - run: npm run build - - run: npx playwright install --with-deps chromium ffmpeg - if: contains(matrix.os, 'ubuntu') - - run: npx playwright install chromium ffmpeg - if: !contains(matrix.os, 'ubuntu') - - name: Install CfT headless shell beta - run: echo "CRPATH=$(npx -y @puppeteer/browsers install chrome-headless-shell@beta --with-deps | tail -n1 | sed 's/^[^ ]* *//')" >> "$GITHUB_ENV" - - name: Run tests linux - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run ctest - if: contains(matrix.os, 'ubuntu') - - name: Run tests non-linux - run: npm run ctest - if: !contains(matrix.os, 'ubuntu') From deca3acca8e23156c9a871d10d070ab165de6239 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 16 Oct 2025 12:29:37 -0700 Subject: [PATCH 076/250] chore: refactor init-agents (#37877) --- .../agents/playwright-test-generator.md | 2 +- .../.claude/agents/playwright-test-healer.md | 2 +- .../.claude/agents/playwright-test-planner.md | 2 +- examples/todomvc/prompt.claudecode.md | 14 - .../playwright/src/agents/generateAgents.ts | 431 ++++++++++-------- packages/playwright/src/agents/generator.md | 5 +- packages/playwright/src/agents/healer.md | 7 +- packages/playwright/src/agents/planner.md | 5 +- packages/playwright/src/program.ts | 12 +- 9 files changed, 251 insertions(+), 229 deletions(-) delete mode 100644 examples/todomvc/prompt.claudecode.md diff --git a/examples/todomvc/.claude/agents/playwright-test-generator.md b/examples/todomvc/.claude/agents/playwright-test-generator.md index b82e9bc3a..276b2d737 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 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. -tools: Glob, Grep, Read, 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__generator_read_log, mcp__playwright-test__generator_setup_page, mcp__playwright-test__generator_write_test +tools: Read, Glob, Grep, 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__generator_read_log, mcp__playwright-test__generator_setup_page, mcp__playwright-test__generator_write_test model: sonnet color: blue --- diff --git a/examples/todomvc/.claude/agents/playwright-test-healer.md b/examples/todomvc/.claude/agents/playwright-test-healer.md index 0a31a92a5..18cd97997 100644 --- a/examples/todomvc/.claude/agents/playwright-test-healer.md +++ b/examples/todomvc/.claude/agents/playwright-test-healer.md @@ -1,7 +1,7 @@ --- 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 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. -tools: Glob, Grep, Read, Write, Edit, MultiEdit, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_generate_locator, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_snapshot, mcp__playwright-test__test_debug, mcp__playwright-test__test_list, mcp__playwright-test__test_run +tools: Read, Glob, Grep, Write, Edit, MultiEdit, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_generate_locator, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_snapshot, mcp__playwright-test__test_debug, mcp__playwright-test__test_list, mcp__playwright-test__test_run model: sonnet color: red --- diff --git a/examples/todomvc/.claude/agents/playwright-test-planner.md b/examples/todomvc/.claude/agents/playwright-test-planner.md index 283a88d7f..afca58af3 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 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. -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__planner_setup_page +tools: Read, Glob, Grep, 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__planner_setup_page model: sonnet color: green --- diff --git a/examples/todomvc/prompt.claudecode.md b/examples/todomvc/prompt.claudecode.md deleted file mode 100644 index 21ee6db85..000000000 --- a/examples/todomvc/prompt.claudecode.md +++ /dev/null @@ -1,14 +0,0 @@ -## Objective -Test basic functionality of todo app. - -## Test setup: tests/template.spec.ts:11 - -## Steps -- 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/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/packages/playwright/src/agents/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts index b349ad923..675457375 100644 --- a/packages/playwright/src/agents/generateAgents.ts +++ b/packages/playwright/src/agents/generateAgents.ts @@ -31,8 +31,8 @@ interface AgentHeader { tools: string[]; } - interface Agent { + source: string; header: AgentHeader; instructions: string; examples: string[]; @@ -41,11 +41,16 @@ interface Agent { /* eslint-disable no-console */ class AgentParser { + static async loadAgents(): Promise { + const files = await fs.promises.readdir(__dirname); + return Promise.all(files.filter(file => file.endsWith('.md')).map(file => AgentParser.parseFile(path.join(__dirname, file)))); + } + static async parseFile(filePath: string): Promise { - const rawMarkdown = await fs.promises.readFile(filePath, 'utf-8'); - const { header, content } = this.extractYamlAndContent(rawMarkdown); + const source = await fs.promises.readFile(filePath, 'utf-8'); + const { header, content } = this.extractYamlAndContent(source); const { instructions, examples } = this.extractInstructionsAndExamples(content); - return { header, instructions, examples }; + return { source, header, instructions, examples }; } static extractYamlAndContent(markdown: string): { header: AgentHeader; content: string } { @@ -102,95 +107,241 @@ class AgentParser { } } -const claudeToolMap = new Map([ - ['ls', ['Glob']], - ['grep', ['Grep']], - ['read', ['Read']], - ['edit', ['Edit', 'MultiEdit']], - ['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(tool: string): string { - const [first, second] = tool.split('/'); - if (!second) - return (claudeToolMap.get(first) || [first]).join(', '); - return `mcp__${first}__${second}`; +export class ClaudeGenerator { + static async init(config: FullConfigInternal, projectName: string) { + await initRepo(config, projectName); + + const agents = await AgentParser.loadAgents(); + + await fs.promises.mkdir('.claude/agents', { recursive: true }); + for (const agent of agents) + await writeFile(`.claude/agents/${agent.header.name}.md`, ClaudeGenerator.agentSpec(agent), '🤖', 'agent definition'); + + await writeFile('.mcp.json', JSON.stringify({ + mcpServers: { + 'playwright-test': { + command: 'npx', + args: ['playwright', 'run-test-mcp-server'], + } + } + }, null, 2), '🔧', 'mcp configuration'); + + initRepoDone(); } - const lines: string[] = []; - lines.push(`---`); - 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}`); - lines.push(`color: ${agent.header.color}`); - lines.push(`---`); - lines.push(''); - lines.push(agent.instructions); - return lines.join('\n'); + static agentSpec(agent: Agent): string { + const claudeToolMap = new Map([ + ['search', ['Glob', 'Grep']], + ['read', ['Read']], + ['edit', ['Edit', 'MultiEdit']], + ['write', ['Write']], + ]); + + function asClaudeTool(tool: string): string { + const [first, second] = tool.split('/'); + if (!second) + return (claudeToolMap.get(first) || [first]).join(', '); + return `mcp__${first}__${second}`; + } + + 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(tool)).join(', ')}`); + lines.push(`model: ${agent.header.model}`); + lines.push(`color: ${agent.header.color}`); + lines.push(`---`); + lines.push(''); + lines.push(agent.instructions); + return lines.join('\n'); + } } -const opencodeToolMap = new Map([ - ['ls', ['ls', 'glob']], - ['grep', ['grep']], - ['read', ['read']], - ['edit', ['edit']], - ['write', ['write']], -]); - -function saveAsOpencodeJson(agents: Agent[]): string { - 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}*${second}`] = true; +export class OpencodeGenerator { + static async init(config: FullConfigInternal, projectName: string) { + await initRepo(config, projectName); + + const agents = await AgentParser.loadAgents(); + + await fs.promises.mkdir('.opencode/prompts', { recursive: true }); + for (const agent of agents) { + const prompt = [agent.instructions]; + prompt.push(''); + prompt.push(...agent.examples.map(example => `${example}`)); + await writeFile(`.opencode/prompts/${agent.header.name}.md`, prompt.join('\n'), '🤖', 'agent definition'); } + await writeFile('opencode.json', OpencodeGenerator.configuration(agents), '🔧', 'opencode configuration'); + + initRepoDone(); } - const result: Record = {}; - result['$schema'] = 'https://opencode.ai/config.json'; - result['mcp'] = {}; - result['tools'] = { - 'playwright*': false, - }; - result['agent'] = {}; - for (const agent of agents) { - const tools: Record = {}; - result['agent']['playwright-test-' + agent.header.name] = { - description: agent.header.description, - mode: 'subagent', - prompt: `{file:.opencode/prompts/playwright-test-${agent.header.name}.md}`, - tools, + static configuration(agents: Agent[]): string { + const opencodeToolMap = new Map([ + ['search', ['ls', 'glob', 'grep']], + ['read', ['read']], + ['edit', ['edit']], + ['write', ['write']], + ]); + + const asOpencodeTool = (tools: Record, tool: string) => { + const [first, second] = tool.split('/'); + if (!second) { + for (const tool of opencodeToolMap.get(first) || [first]) + tools[tool] = true; + } else { + tools[`${first}*${second}`] = true; + } + }; + + const result: Record = {}; + result['$schema'] = 'https://opencode.ai/config.json'; + result['mcp'] = {}; + result['tools'] = { + 'playwright*': false, }; - for (const tool of agent.header.tools) - asOpencodeTool(tools, tool); + result['agent'] = {}; + for (const agent of agents) { + const tools: Record = {}; + result['agent'][agent.header.name] = { + description: agent.header.description, + mode: 'subagent', + prompt: `{file:.opencode/prompts/${agent.header.name}.md}`, + tools, + }; + for (const tool of agent.header.tools) + asOpencodeTool(tools, tool); + } + + result['mcp']['playwright-test'] = { + type: 'local', + command: ['npx', 'playwright', 'run-test-mcp-server'], + enabled: true, + }; + + return JSON.stringify(result, null, 2); } +} +export class AgentGenerator { + static async init(config: FullConfigInternal, projectName: string) { + const agentsFolder = process.env.AGENTS_FOLDER; + if (!agentsFolder) { + console.error('AGENTS_FOLDER environment variable is not set'); + return; + } - const server = commonMcpServers.playwrightTest; - result['mcp']['playwright-test'] = { - type: server.type, - command: [server.command, ...server.args], - enabled: true, - }; + await initRepo(config, projectName); + + const agents = await AgentParser.loadAgents(); + + await fs.promises.mkdir(agentsFolder, { recursive: true }); + for (const agent of agents) + await writeFile(`${agentsFolder}/${agent.header.name}.md`, agent.source, '🤖', 'agent definition'); + + console.log('🔧 MCP configuration'); + console.log(JSON.stringify({ + mcpServers: { + 'playwright-test': { + type: 'stdio', + command: 'npx', + args: [ + `--prefix=${path.resolve(process.cwd())}`, + 'playwright', + 'run-test-mcp-server', + `--headless`, + `--config=${path.resolve(process.cwd())}`, + ], + tools: ['*'] + } + } + }, null, 2)); - return JSON.stringify(result, null, 2); + initRepoDone(); + } } -async function loadAgents(): Promise { - const files = await fs.promises.readdir(__dirname); - return Promise.all(files.filter(file => file.endsWith('.md')).map(file => AgentParser.parseFile(path.join(__dirname, file)))); +export class VSCodeGenerator { + static async init(config: FullConfigInternal, projectName: string) { + await initRepo(config, projectName); + const agents = await AgentParser.loadAgents(); + + const nameMap = new Map([ + ['playwright-test-planner', ' 🎭 planner'], + ['playwright-test-generator', '🎭 generator'], + ['playwright-test-healer', '🎭 healer'], + ]); + + await fs.promises.mkdir('.github/chatmodes', { recursive: true }); + for (const agent of agents) + await writeFile(`.github/chatmodes/${nameMap.get(agent.header.name)}.chatmode.md`, VSCodeGenerator.agentSpec(agent), '🤖', 'chatmode definition'); + + 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'] = { + type: 'stdio', + command: 'npx', + args: ['playwright', 'run-test-mcp-server'], + cwd: '${workspaceFolder}', + }; + await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2), '🔧', 'mcp configuration'); + + initRepoDone(); + } + + static agentSpec(agent: Agent): string { + const vscodeToolMap = new Map([ + ['search', ['search/listDirectory', 'search/fileSearch', 'search/textSearch']], + ['read', ['search/readFile']], + ['edit', ['edit/editFiles']], + ['write', ['edit/createFile', 'edit/createDirectory']], + ]); + const vscodeToolsOrder = ['edit/createFile', 'edit/createDirectory', 'edit/editFiles', 'search/fileSearch', 'search/textSearch', 'search/listDirectory', 'search/readFile']; + const vscodeMcpName = 'playwright-test'; + + function asVscodeTool(tool: string): string | string[] { + const [first, second] = tool.split('/'); + if (second) + return `${vscodeMcpName}/${second}`; + return vscodeToolMap.get(first) || first; + } + const tools = agent.header.tools.map(asVscodeTool).flat().sort((a, b) => { + // VSCode insists 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}.`); + 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'); + } } async function writeFile(filePath: string, content: string, icon: string, description: string) { @@ -234,120 +385,6 @@ function initRepoDone() { console.log('✅ Done.'); } -export async function initClaudeCodeRepo(config: FullConfigInternal, projectName: string) { - await initRepo(config, projectName); - - const agents = await loadAgents(); - - await fs.promises.mkdir('.claude/agents', { recursive: true }); - for (const agent of agents) - await writeFile(`.claude/agents/playwright-test-${agent.header.name}.md`, saveAsClaudeCode(agent), '🤖', 'agent definition'); - - await writeFile('.mcp.json', JSON.stringify({ - mcpServers: { - 'playwright-test': { - command: commonMcpServers.playwrightTest.command, - args: commonMcpServers.playwrightTest.args, - } - } - }, null, 2), '🔧', 'mcp configuration'); - - initRepoDone(); -} - -const vscodeToolMap = new Map([ - ['ls', ['search/listDirectory', 'search/fileSearch']], - ['grep', ['search/textSearch']], - ['read', ['search/readFile']], - ['edit', ['edit/editFiles']], - ['write', ['edit/createFile', 'edit/createDirectory']], -]); -const vscodeToolsOrder = ['edit/createFile', 'edit/createDirectory', 'edit/editFiles', 'search/fileSearch', 'search/textSearch', 'search/listDirectory', 'search/readFile']; -const vscodeMcpName = 'playwright-test'; -function saveAsVSCodeChatmode(agent: Agent): string { - function asVscodeTool(tool: string): string | string[] { - const [first, second] = tool.split('/'); - if (second) - return `${vscodeMcpName}/${second}`; - return vscodeToolMap.get(first) || first; - } - const tools = agent.header.tools.map(asVscodeTool).flat().sort((a, b) => { - // VSCode insists 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}.`); - 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'); -} - -export async function initVSCodeRepo(config: FullConfigInternal, projectName: string) { - await initRepo(config, projectName); - const agents = await loadAgents(); - - await fs.promises.mkdir('.github/chatmodes', { recursive: true }); - for (const agent of agents) - await writeFile(`.github/chatmodes/${agent.header.name === 'planner' ? ' ' : ''}🎭 ${agent.header.name}.chatmode.md`, saveAsVSCodeChatmode(agent), '🤖', 'chatmode definition'); - - 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'] = { - type: 'stdio', - command: commonMcpServers.playwrightTest.command, - args: commonMcpServers.playwrightTest.args, - cwd: '${workspaceFolder}', - }; - await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2), '🔧', 'mcp configuration'); - - initRepoDone(); -} - -export async function initOpencodeRepo(config: FullConfigInternal, projectName: string) { - await initRepo(config, projectName); - - const agents = await loadAgents(); - - await fs.promises.mkdir('.opencode/prompts', { recursive: true }); - for (const agent of agents) { - const prompt = [agent.instructions]; - prompt.push(''); - prompt.push(...agent.examples.map(example => `${example}`)); - await writeFile(`.opencode/prompts/playwright-test-${agent.header.name}.md`, prompt.join('\n'), '🤖', 'agent definition'); - } - await writeFile('opencode.json', saveAsOpencodeJson(agents), '🔧', 'opencode configuration'); - - initRepoDone(); -} - const coveragePrompt = (seedFile: string) => ` # Produce test coverage @@ -356,7 +393,7 @@ Parameters: - Seed file (optional): the seed file to use, defaults to ${path.relative(process.cwd(), seedFile)} - Test plan file (optional): the test plan file to write, under specs/ folder. -1. Call #planner subagent with prompt: +1. Call #playwright-test-planner subagent with prompt: @@ -364,7 +401,7 @@ Parameters: -2. For each test case from the test plan file (1.1, 1.2, ...), Call #generator subagent with prompt: +2. For each test case from the test plan file (1.1, 1.2, ...), Call #playwright-test-generator subagent with prompt: @@ -374,7 +411,7 @@ Parameters: -3. Call #healer subagent with prompt: +3. Call #playwright-test-healer subagent with prompt: Run all tests and fix the failing ones one after another. `; diff --git a/packages/playwright/src/agents/generator.md b/packages/playwright/src/agents/generator.md index 3e83d145e..4a313bf56 100644 --- a/packages/playwright/src/agents/generator.md +++ b/packages/playwright/src/agents/generator.md @@ -1,12 +1,11 @@ --- -name: generator +name: playwright-test-generator description: Use this agent when you need to create automated browser tests using Playwright model: sonnet color: blue tools: - - ls - - grep - read + - search - playwright-test/browser_click - playwright-test/browser_drag - playwright-test/browser_evaluate diff --git a/packages/playwright/src/agents/healer.md b/packages/playwright/src/agents/healer.md index ee07a6cb1..92e08e86f 100644 --- a/packages/playwright/src/agents/healer.md +++ b/packages/playwright/src/agents/healer.md @@ -1,12 +1,11 @@ --- -name: healer +name: playwright-test-healer description: Use this agent when you need to debug and fix failing Playwright tests -color: red model: sonnet +color: red tools: - - ls - - grep - read + - search - write - edit - playwright-test/browser_console_messages diff --git a/packages/playwright/src/agents/planner.md b/packages/playwright/src/agents/planner.md index 192962b29..ae58a7bfe 100644 --- a/packages/playwright/src/agents/planner.md +++ b/packages/playwright/src/agents/planner.md @@ -1,12 +1,11 @@ --- -name: planner +name: playwright-test-planner description: Use this agent when you need to create comprehensive test plan for a web application or website model: sonnet color: green tools: - - ls - - grep - read + - search - write - playwright-test/browser_click - playwright-test/browser_close diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index cd9c8e801..707550c15 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, initVSCodeRepo } from './agents/generateAgents'; +import { ClaudeGenerator, OpencodeGenerator, VSCodeGenerator, AgentGenerator } from './agents/generateAgents'; import type { ConfigCLIOverrides } from './common/ipc'; import type { TraceMode } from '../types/test'; @@ -183,18 +183,20 @@ function addInitAgentsCommand(program: Command) { const command = program.command('init-agents'); command.description('Initialize repository agents'); const option = command.createOption('--loop ', 'Agentic loop provider'); - option.choices(['vscode', 'claude', 'opencode']); + option.choices(['vscode', 'claude', 'opencode', 'generic']); command.addOption(option); command.option('-c, --config ', `Configuration file to find a project to use for seed test`); command.option('--project ', 'Project to use for seed test'); command.action(async opts => { const config = await loadConfigFromFile(opts.config); if (opts.loop === 'opencode') { - await initOpencodeRepo(config, opts.project); + await OpencodeGenerator.init(config, opts.project); } else if (opts.loop === 'vscode') { - await initVSCodeRepo(config, opts.project); + await VSCodeGenerator.init(config, opts.project); } else if (opts.loop === 'claude') { - await initClaudeCodeRepo(config, opts.project); + await ClaudeGenerator.init(config, opts.project); + } else if (opts.loop === 'generic') { + await AgentGenerator.init(config, opts.project); } else { command.help(); return; From c347bb455ce92ffd80301e47d7c7571691809ba1 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 16 Oct 2025 13:00:13 -0700 Subject: [PATCH 077/250] chore(trace-viewer): fix actions header alignment (#37889) --- packages/trace-viewer/src/ui/workbench.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/trace-viewer/src/ui/workbench.css b/packages/trace-viewer/src/ui/workbench.css index 81080c68d..99f1576a4 100644 --- a/packages/trace-viewer/src/ui/workbench.css +++ b/packages/trace-viewer/src/ui/workbench.css @@ -16,7 +16,7 @@ .workbench-run-status { height: 30px; - padding: 4px; + padding: 4px 4px 4px 5px; flex: none; display: flex; flex-direction: row; From 630345dfc220057ddbd63ed484ec232a21360a93 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 16 Oct 2025 13:00:21 -0700 Subject: [PATCH 078/250] chore(ui): fix TreeView tree role (#37888) --- packages/web/src/components/treeView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx index d4f955fb9..60d3886d4 100644 --- a/packages/web/src/components/treeView.tsx +++ b/packages/web/src/components/treeView.tsx @@ -126,9 +126,10 @@ export function TreeView({ setTreeState({ ...treeState }); }, [treeItems, treeState, setTreeState]); - return
        + return
        0 ? 'tree' : undefined} tabIndex={0} onKeyDown={event => { if (selectedItem && event.key === 'Enter') { From e953dce1f4a34ca19c14ea179ad06a9eeb2f8937 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 16 Oct 2025 13:00:54 -0700 Subject: [PATCH 079/250] chore(ui): fix Expandable aria attributes (#37885) --- packages/web/src/components/expandable.tsx | 34 ++++++++++++++-------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/web/src/components/expandable.tsx b/packages/web/src/components/expandable.tsx index c0ac3ed84..e3b581824 100644 --- a/packages/web/src/components/expandable.tsx +++ b/packages/web/src/components/expandable.tsx @@ -25,19 +25,29 @@ export const Expandable: React.FunctionComponent> = ({ title, children, setExpanded, expanded, expandOnTitleClick }) => { const id = React.useId(); + + const onClick = React.useCallback(() => setExpanded(!expanded), [expanded, setExpanded]); + + const chevron =
        ; + return
        -
        expandOnTitleClick && setExpanded(!expanded)}> + {expandOnTitleClick ?
        !expandOnTitleClick && setExpanded(!expanded)} /> - {title} -
        - { expanded &&
        {children}
        } + role='button' + aria-expanded={expanded} + aria-controls={id} + className='expandable-title' + onClick={onClick}> + {chevron} + {title} +
        : +
        + {chevron} + {title} +
        } + {expanded &&
        {children}
        }
        ; }; From 6f965ad483edc5a4189fe0eda1f9dd70d026ef87 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 16 Oct 2025 15:05:58 -0700 Subject: [PATCH 080/250] fix(agents): remove workspaceFolder ref from vscode mcp (#37891) --- packages/playwright/src/agents/generateAgents.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/playwright/src/agents/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts index 675457375..0145c29ac 100644 --- a/packages/playwright/src/agents/generateAgents.ts +++ b/packages/playwright/src/agents/generateAgents.ts @@ -294,7 +294,6 @@ export class VSCodeGenerator { type: 'stdio', command: 'npx', args: ['playwright', 'run-test-mcp-server'], - cwd: '${workspaceFolder}', }; await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2), '🔧', 'mcp configuration'); From ed73b2674aabd2177e6e64a244871377f41327c2 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 16 Oct 2025 15:20:34 -0700 Subject: [PATCH 081/250] fix: do not zip live stack traces in ui mode (#37887) --- .../playwright-core/src/server/localUtils.ts | 8 ++--- tests/playwright-test/ui-mode-trace.spec.ts | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/playwright-core/src/server/localUtils.ts b/packages/playwright-core/src/server/localUtils.ts index 5fb9b57d4..b58f2aa85 100644 --- a/packages/playwright-core/src/server/localUtils.ts +++ b/packages/playwright-core/src/server/localUtils.ts @@ -60,12 +60,8 @@ export async function zip(progress: Progress, stackSessions: Map { await page.getByTestId('test-tree').getByText('basic fail').dblclick(); await expect(page.getByRole('tabpanel', { name: 'Actions' })).toContainText('Failed'); }); + +test('should be able to create and dispose APIRequestContext inside Promise.all', async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.test.ts': ` + import { test, request } from '@playwright/test'; + test('create api request contexts', async ({ }) => { + await Promise.all(Array.from({ length: 100 }).map(async () => { + let delay = Math.floor(Math.random() * 501); + await new Promise(res => setTimeout(res, delay)); + + const apiContext = await request.newContext(); + delay = Math.floor(Math.random() * 501); + await new Promise(res => setTimeout(res, delay)); + await apiContext.dispose(); + })); + }); + `, + }); + + await page.getByText('create api request contexts').dblclick(); + + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); + + await page.getByText('Errors', { exact: true }).click(); + await expect(page.locator('.tab-errors')).toHaveText('No errors'); + + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); + await expect( + listItem, + 'action list' + ).toHaveText([ + /Before Hooks[\d.]+m?s/, + ...Array.from({ length: 100 }).map(() => /Create request context[\d.]+m?s/), + /After Hooks[\d.]+m?s/, + ]); +}); From eed1f19104886e76c1bb1cb99dff67d88a252eaa Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 16 Oct 2025 16:59:48 -0700 Subject: [PATCH 082/250] chore: allow local-network-access permission in chromium (#37871) --- docs/src/api/class-browsercontext.md | 3 +- packages/playwright-client/types/types.d.ts | 3 +- .../src/server/chromium/crBrowser.ts | 1 + packages/playwright-core/types/types.d.ts | 3 +- tests/library/permissions.spec.ts | 48 +++++++++++++++++++ 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index c49bef583..3e7529c48 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -948,6 +948,8 @@ Here are some permissions that may be supported by some browsers: * `'clipboard-write'` * `'geolocation'` * `'gyroscope'` +* `'local-fonts'` +* `'local-network-access'` * `'magnetometer'` * `'microphone'` * `'midi-sysex'` (system-exclusive midi) @@ -955,7 +957,6 @@ Here are some permissions that may be supported by some browsers: * `'notifications'` * `'payment-handler'` * `'storage-access'` -* `'local-fonts'` ### option: BrowserContext.grantPermissions.origin * since: v1.8 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 28c4b14a9..df5bfa0bc 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -8991,6 +8991,8 @@ export interface BrowserContext { * - `'clipboard-write'` * - `'geolocation'` * - `'gyroscope'` + * - `'local-fonts'` + * - `'local-network-access'` * - `'magnetometer'` * - `'microphone'` * - `'midi-sysex'` (system-exclusive midi) @@ -8998,7 +9000,6 @@ export interface BrowserContext { * - `'notifications'` * - `'payment-handler'` * - `'storage-access'` - * - `'local-fonts'` * @param options */ grantPermissions(permissions: ReadonlyArray, options?: { diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index ef6f16e7c..a81e3d88b 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -435,6 +435,7 @@ export class CRBrowserContext extends BrowserContext { ['midi-sysex', 'midiSysex'], ['storage-access', 'storageAccess'], ['local-fonts', 'localFonts'], + ['local-network-access', 'localNetworkAccess'], ]); const filtered = permissions.map(permission => { const protocolPermission = webPermissionToProtocol.get(permission); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 28c4b14a9..df5bfa0bc 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8991,6 +8991,8 @@ export interface BrowserContext { * - `'clipboard-write'` * - `'geolocation'` * - `'gyroscope'` + * - `'local-fonts'` + * - `'local-network-access'` * - `'magnetometer'` * - `'microphone'` * - `'midi-sysex'` (system-exclusive midi) @@ -8998,7 +9000,6 @@ export interface BrowserContext { * - `'notifications'` * - `'payment-handler'` * - `'storage-access'` - * - `'local-fonts'` * @param options */ grantPermissions(permissions: ReadonlyArray, options?: { diff --git a/tests/library/permissions.spec.ts b/tests/library/permissions.spec.ts index b723aee81..f753adc79 100644 --- a/tests/library/permissions.spec.ts +++ b/tests/library/permissions.spec.ts @@ -253,3 +253,51 @@ it.describe(() => { expect(await page.evaluate(async () => (await (window as any).queryLocalFonts()).length > 0)).toBe(true); }); }); + +it('local network request is allowed from public origin', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37861' } +}, async ({ page, context, server, browserName }) => { + it.fail(browserName === 'webkit'); + if (browserName === 'chromium') + await context.grantPermissions(['local-network-access']); + const serverRequests = []; + server.setRoute('/cors', (req, res) => { + serverRequests.push(`${req.method} ${req.url}`); + if (req.method === 'OPTIONS') { + res.writeHead(204, { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS', + 'Access-Control-Allow-Headers': '*', + }); + res.end(); + return; + } + res.writeHead(200, { 'Content-type': 'text/plain', 'Access-Control-Allow-Origin': '*' }); + res.end('Hello there!'); + }); + const clientRequests = []; + // Has to be a public origin. + await page.goto('https://demo.playwright.dev/todomvc/'); + page.on('request', request => { + clientRequests.push(`${request.method()} ${request.url()}`); + }); + const response = await page.evaluate(async url => { + const response = await fetch(url, { + method: 'POST', + body: '', + headers: { + 'Content-Type': 'application/json', + 'X-Custom-Header': 'test-value' + } + }); + return await response.text(); + }, server.CROSS_PROCESS_PREFIX + '/cors').catch(e => e.message); + expect(response).toBe('Hello there!'); + expect(serverRequests).toEqual([ + 'OPTIONS /cors', + 'POST /cors', + ]); + expect(clientRequests).toEqual([ + `POST ${server.CROSS_PROCESS_PREFIX}/cors`, + ]); +}); From d8b75e55a9aed60cf6a5f2ef735921c0592bda29 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 12:12:20 -0700 Subject: [PATCH 083/250] feat(webkit): roll to r2221 (#37873) --- packages/playwright-core/browsers.json | 2 +- .../src/server/webkit/protocol.d.ts | 48 ------------------- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 2aebbd23c..c628dae3e 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -39,7 +39,7 @@ }, { "name": "webkit", - "revision": "2215", + "revision": "2221", "installByDefault": true, "revisionOverrides": { "debian11-x64": "2105", diff --git a/packages/playwright-core/src/server/webkit/protocol.d.ts b/packages/playwright-core/src/server/webkit/protocol.d.ts index 3b82ed2f8..52b739300 100644 --- a/packages/playwright-core/src/server/webkit/protocol.d.ts +++ b/packages/playwright-core/src/server/webkit/protocol.d.ts @@ -6689,50 +6689,6 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the */ frameId: Network.FrameId; } - /** - * Fired when frame has started loading. - */ - export type frameStartedLoadingPayload = { - /** - * Id of the frame that has started loading. - */ - frameId: Network.FrameId; - } - /** - * Fired when frame has stopped loading. - */ - export type frameStoppedLoadingPayload = { - /** - * Id of the frame that has stopped loading. - */ - frameId: Network.FrameId; - } - /** - * Fired when frame schedules a potential navigation. - */ - export type frameScheduledNavigationPayload = { - /** - * Id of the frame that has scheduled a navigation. - */ - frameId: Network.FrameId; - /** - * Delay (in seconds) until the navigation is scheduled to begin. The navigation is not guaranteed to start. - */ - delay: number; - /** - * Whether the naviation will happen in the same frame. - */ - targetIsCurrentFrame: boolean; - } - /** - * Fired when frame no longer has a scheduled navigation. - */ - export type frameClearedScheduledNavigationPayload = { - /** - * Id of the frame that has cleared its scheduled navigation. - */ - frameId: Network.FrameId; - } /** * Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation. */ @@ -9207,10 +9163,6 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the "Page.frameNavigated": Page.frameNavigatedPayload; "Page.frameAttached": Page.frameAttachedPayload; "Page.frameDetached": Page.frameDetachedPayload; - "Page.frameStartedLoading": Page.frameStartedLoadingPayload; - "Page.frameStoppedLoading": Page.frameStoppedLoadingPayload; - "Page.frameScheduledNavigation": Page.frameScheduledNavigationPayload; - "Page.frameClearedScheduledNavigation": Page.frameClearedScheduledNavigationPayload; "Page.navigatedWithinDocument": Page.navigatedWithinDocumentPayload; "Page.defaultUserPreferencesDidChange": Page.defaultUserPreferencesDidChangePayload; "Page.willCheckNavigationPolicy": Page.willCheckNavigationPolicyPayload; From 1ca9f667fe66cdb4faba807c81437a4baed46500 Mon Sep 17 00:00:00 2001 From: Catalin Lupuleti <105351510+lupuletic@users.noreply.github.com> Date: Fri, 17 Oct 2025 20:41:05 +0100 Subject: [PATCH 084/250] feat(mcp): add headers capability (#37583) --- .../playwright/src/mcp/browser/context.ts | 17 +++ packages/playwright/src/mcp/browser/tools.ts | 2 + .../src/mcp/browser/tools/headers.ts | 49 ++++++++ packages/playwright/src/mcp/config.d.ts | 4 +- packages/playwright/src/mcp/program.ts | 2 +- tests/mcp/headers.spec.ts | 110 ++++++++++++++++++ 6 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 packages/playwright/src/mcp/browser/tools/headers.ts create mode 100644 tests/mcp/headers.spec.ts diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts index 657194f8e..180ce53cb 100644 --- a/packages/playwright/src/mcp/browser/context.ts +++ b/packages/playwright/src/mcp/browser/context.ts @@ -52,6 +52,7 @@ export class Context { private _tabs: Tab[] = []; private _currentTab: Tab | undefined; private _clientInfo: ClientInfo; + private _extraHTTPHeaders: Record | undefined; private static _allContexts: Set = new Set(); private _closeBrowserContextPromise: Promise | undefined; @@ -220,6 +221,20 @@ export class Context { return browserContext; } + async setExtraHTTPHeaders(headers: Record) { + if (!Object.keys(headers).length) + throw new Error('Please provide at least one header to set.'); + + for (const name of Object.keys(headers)) { + if (!name.trim()) + throw new Error('Header names must be non-empty strings.'); + } + + this._extraHTTPHeaders = { ...headers }; + const { browserContext } = await this._ensureBrowserContext(); + await browserContext.setExtraHTTPHeaders(this._extraHTTPHeaders); + } + private _ensureBrowserContext() { if (!this._browserContextPromise) { this._browserContextPromise = this._setupBrowserContext(); @@ -240,6 +255,8 @@ export class Context { const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName); const { browserContext } = result; await this._setupRequestInterception(browserContext); + if (this._extraHTTPHeaders) + await browserContext.setExtraHTTPHeaders(this._extraHTTPHeaders); if (this.sessionLog) await InputRecorder.create(this, browserContext); for (const page of browserContext.pages()) diff --git a/packages/playwright/src/mcp/browser/tools.ts b/packages/playwright/src/mcp/browser/tools.ts index 3ebbe7591..98ec122d3 100644 --- a/packages/playwright/src/mcp/browser/tools.ts +++ b/packages/playwright/src/mcp/browser/tools.ts @@ -26,6 +26,7 @@ import mouse from './tools/mouse'; import navigate from './tools/navigate'; import network from './tools/network'; import pdf from './tools/pdf'; +import headers from './tools/headers'; import snapshot from './tools/snapshot'; import screenshot from './tools/screenshot'; import tabs from './tools/tabs'; @@ -47,6 +48,7 @@ export const browserTools: Tool[] = [ ...keyboard, ...navigate, ...network, + ...headers, ...mouse, ...pdf, ...screenshot, diff --git a/packages/playwright/src/mcp/browser/tools/headers.ts b/packages/playwright/src/mcp/browser/tools/headers.ts new file mode 100644 index 000000000..578eab1f4 --- /dev/null +++ b/packages/playwright/src/mcp/browser/tools/headers.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 { defineTool } from './tool'; + +const setHeaders = defineTool({ + capability: 'headers', + + schema: { + name: 'browser_set_headers', + title: 'Set extra HTTP headers', + description: 'Persistently set custom HTTP headers on the active browser context.', + inputSchema: z.object({ + headers: z.record(z.string(), z.string()).describe('Header names mapped to the values that should be sent with every request.'), + }), + type: 'action', + }, + + handle: async (context, params, response) => { + try { + await context.setExtraHTTPHeaders(params.headers); + } catch (error) { + response.addError((error as Error).message); + return; + } + + const count = Object.keys(params.headers).length; + response.addResult(`Configured ${count} ${count === 1 ? 'header' : 'headers'} for this session.`); + response.addCode(`await context.setExtraHTTPHeaders(${JSON.stringify(params.headers, null, 2)});`); + }, +}); + +export default [ + setHeaders, +]; diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index 7a43731f3..ba0f0977b 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' | 'testing' | 'tracing'; +export type ToolCapability = 'core' | 'core-tabs' | 'core-install' | 'vision' | 'pdf' | 'testing' | 'tracing' | 'headers'; export type Config = { /** @@ -99,6 +99,7 @@ export type Config = { * - 'core': Core browser automation features. * - 'pdf': PDF generation and manipulation. * - 'vision': Coordinate-based interactions. + * - 'headers': Manage persistent custom HTTP headers. */ capabilities?: ToolCapability[]; @@ -171,4 +172,3 @@ export type Config = { */ imageResponses?: 'allow' | 'omit'; }; - diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index 8f979cc3d..54460437b 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -33,7 +33,7 @@ export function decorateCommand(command: Command, version: string) { .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('--caps ', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf, headers.', 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.') diff --git a/tests/mcp/headers.spec.ts b/tests/mcp/headers.spec.ts new file mode 100644 index 000000000..839e8d2b5 --- /dev/null +++ b/tests/mcp/headers.spec.ts @@ -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 { test, expect } from './fixtures'; + +test('headers tool requires capability', async ({ client, startClient }) => { + const { tools } = await client.listTools(); + expect(tools.map(tool => tool.name)).not.toContain('browser_set_headers'); + + const { client: headersClient } = await startClient({ args: ['--caps=headers'] }); + const headersToolList = await headersClient.listTools(); + expect(headersToolList.tools.map(tool => tool.name)).toContain('browser_set_headers'); +}); + +test('browser_set_headers rejects empty input', async ({ startClient }) => { + const { client } = await startClient({ args: ['--caps=headers'] }); + + const response = await client.callTool({ + name: 'browser_set_headers', + arguments: { headers: {} }, + }); + + expect(response).toHaveResponse({ + isError: true, + result: 'Please provide at least one header to set.', + }); +}); + +test('browser_set_headers rejects header names without characters', async ({ startClient }) => { + const { client } = await startClient({ args: ['--caps=headers'] }); + + const response = await client.callTool({ + name: 'browser_set_headers', + arguments: { headers: { ' ': 'value' } }, + }); + + expect(response).toHaveResponse({ + isError: true, + result: 'Header names must be non-empty strings.', + }); +}); + +test('browser_set_headers persists headers across navigations', async ({ startClient, server }) => { + server.setContent('/first', 'First', 'text/html'); + server.setContent('/second', 'Second', 'text/html'); + + const { client } = await startClient({ args: ['--caps=headers'] }); + + expect(await client.callTool({ + name: 'browser_set_headers', + arguments: { + headers: { 'X-Tenant-ID': 'tenant-123' }, + }, + })).toHaveResponse({ + result: 'Configured 1 header for this session.', + }); + + const firstRequestPromise = server.waitForRequest('/first'); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: `${server.PREFIX}/first` }, + }); + const firstRequest = await firstRequestPromise; + expect(firstRequest.headers['x-tenant-id']).toBe('tenant-123'); + + const secondRequestPromise = server.waitForRequest('/second'); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: `${server.PREFIX}/second` }, + }); + const secondRequest = await secondRequestPromise; + expect(secondRequest.headers['x-tenant-id']).toBe('tenant-123'); +}); + +test('browser_set_headers sends headers with requests', async ({ startClient, server }) => { + server.setContent('/page', 'Page', 'text/html'); + + const { client } = await startClient({ args: ['--caps=headers'] }); + + expect(await client.callTool({ + name: 'browser_set_headers', + arguments: { + headers: { 'X-Custom-Header': 'custom-value' }, + }, + })).toHaveResponse({ + result: 'Configured 1 header for this session.', + }); + + const requestPromise = server.waitForRequest('/page'); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: `${server.PREFIX}/page` }, + }); + + const request = await requestPromise; + expect(request.headers['x-custom-header']).toBe('custom-value'); +}); From a8bc0a01bb35560075fc7ff49714a675498ff5ee Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 17 Oct 2025 12:42:25 -0700 Subject: [PATCH 085/250] docs: release notes for langs (#37762) --- docs/src/release-notes-csharp.md | 21 +++++++++++++++++++++ docs/src/release-notes-java.md | 21 +++++++++++++++++++++ docs/src/release-notes-python.md | 21 +++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index f954e68cf..099ccd0d7 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -4,6 +4,27 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.56 + +### New APIs + +- New methods [`method: Page.consoleMessages`] and [`method: Page.pageErrors`] for retrieving the most recent console messages from the page +- New method [`method: Page.requests`] for retrieving the most recent network requests from the page + +### Breaking Changes + +- Event [`event: BrowserContext.backgroundPage`] has been deprecated and will not be emitted. Method [`method: BrowserContext.backgroundPages`] will return an empty list + +### Miscellaneous + +- Aria snapshots render and compare `input` `placeholder` + +### Browser Versions + +- Chromium 141.0.7390.37 +- Mozilla Firefox 142.0.1 +- WebKit 26.0 + ## Version 1.55 ### Codegen diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index ed8d74746..b8f877419 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -4,6 +4,27 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.56 + +### New APIs + +- New methods [`method: Page.consoleMessages`] and [`method: Page.pageErrors`] for retrieving the most recent console messages from the page +- New method [`method: Page.requests`] for retrieving the most recent network requests from the page + +### Breaking Changes + +- Event [`event: BrowserContext.backgroundPage`] has been deprecated and will not be emitted. Method [`method: BrowserContext.backgroundPages`] will return an empty list + +### Miscellaneous + +- Aria snapshots render and compare `input` `placeholder` + +### Browser Versions + +- Chromium 141.0.7390.37 +- Mozilla Firefox 142.0.1 +- WebKit 26.0 + ## Version 1.55 ### Codegen diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md index 0ecd8eb89..5c5359b35 100644 --- a/docs/src/release-notes-python.md +++ b/docs/src/release-notes-python.md @@ -4,6 +4,27 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.56 + +### New APIs + +- New methods [`method: Page.consoleMessages`] and [`method: Page.pageErrors`] for retrieving the most recent console messages from the page +- New method [`method: Page.requests`] for retrieving the most recent network requests from the page + +### Breaking Changes + +- Event [`event: BrowserContext.backgroundPage`] has been deprecated and will not be emitted. Method [`method: BrowserContext.backgroundPages`] will return an empty list + +### Miscellaneous + +- Aria snapshots render and compare `input` `placeholder` + +### Browser Versions + +- Chromium 141.0.7390.37 +- Mozilla Firefox 142.0.1 +- WebKit 26.0 + ## Version 1.55 ### Codegen From 94037eb4b997e003ef70e63f9fb3bcf4764c39a5 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 17 Oct 2025 14:11:42 -0700 Subject: [PATCH 086/250] chore: update mcp test expectations after #37583 (#37904) --- tests/mcp/generator.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/mcp/generator.spec.ts b/tests/mcp/generator.spec.ts index d0feace8e..7c4b7141e 100644 --- a/tests/mcp/generator.spec.ts +++ b/tests/mcp/generator.spec.ts @@ -41,6 +41,7 @@ test('generator tools intent', async ({ startClient }) => { 'browser_type', 'browser_navigate', 'browser_navigate_back', + 'browser_set_headers', 'browser_mouse_move_xy', 'browser_mouse_click_xy', 'browser_mouse_drag_xy', From 7498e6abb0b998691b292087a70293413839a7c5 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 17 Oct 2025 16:16:20 -0700 Subject: [PATCH 087/250] chore: generate loop-specific prompts (#37911) --- ....md => playwright-test-generator.agent.md} | 0 ...ler.md => playwright-test-healer.agent.md} | 0 ...er.md => playwright-test-planner.agent.md} | 4 + .../.claude/prompts/test-coverage.prompt.md | 31 + .../.claude/prompts/test-generate.prompt.md | 8 + .../.claude/prompts/test-heal.prompt.md | 6 + .../.claude/prompts/test-plan.prompt.md | 9 + .../\360\237\216\255 planner.chatmode.md" | 4 + .../.github/prompts/test-coverage.prompt.md | 31 + .../.github/prompts/test-generate.prompt.md | 8 + .../.github/prompts/test-heal.prompt.md | 6 + .../.github/prompts/test-plan.prompt.md | 9 + examples/todomvc/.vscode/mcp.json | 3 +- examples/todomvc/prompts/test-coverage.md | 29 - examples/todomvc/specs/basic-operations.md | 468 ------------ .../todomvc/specs/basic-operations.plan.md | 723 ++++++++++++++++++ .../add-multiple-todos.spec.ts | 34 + .../add-single-valid-todo.spec.ts | 31 + .../add-todo-with-long-text.spec.ts | 17 + .../add-todo-with-special-characters.spec.ts | 20 + .../reject-empty-todo.spec.ts | 21 + .../reject-whitespace-only-todo.spec.ts | 31 + .../complete-multiple-todos.spec.ts | 38 + .../complete-single-todo.spec.ts | 27 + .../mark-all-as-complete.spec.ts | 27 + .../completing-todos/uncomplete-todo.spec.ts | 24 + .../unmark-all-as-complete.spec.ts | 37 + .../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 -- .../cancel-edit-with-escape.spec.ts | 24 + .../delete-todo-by-clearing-text.spec.ts | 24 + .../editing-todos/edit-completed-todo.spec.ts | 28 + .../edit-todo-successfully.spec.ts | 24 + examples/todomvc/tests/integration.spec.ts | 427 ----------- .../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 -- .../playwright/src/agents/generateAgents.ts | 119 +-- .../{generator.md => generator.agent.md} | 0 .../src/agents/{healer.md => healer.agent.md} | 0 .../agents/{planner.md => planner.agent.md} | 4 + .../src/agents/test-coverage.prompt.md | 31 + .../src/agents/test-generate.prompt.md | 8 + .../playwright/src/agents/test-heal.prompt.md | 6 + .../playwright/src/agents/test-plan.prompt.md | 9 + tests/mcp/init-agents.spec.ts | 45 +- 55 files changed, 1406 insertions(+), 1563 deletions(-) rename examples/todomvc/.claude/agents/{playwright-test-generator.md => playwright-test-generator.agent.md} (100%) rename examples/todomvc/.claude/agents/{playwright-test-healer.md => playwright-test-healer.agent.md} (100%) rename examples/todomvc/.claude/agents/{playwright-test-planner.md => playwright-test-planner.agent.md} (97%) create mode 100644 examples/todomvc/.claude/prompts/test-coverage.prompt.md create mode 100644 examples/todomvc/.claude/prompts/test-generate.prompt.md create mode 100644 examples/todomvc/.claude/prompts/test-heal.prompt.md create mode 100644 examples/todomvc/.claude/prompts/test-plan.prompt.md rename "examples/todomvc/.github/chatmodes/ \360\237\216\255 planner.chatmode.md" => "examples/todomvc/.github/chatmodes/\360\237\216\255 planner.chatmode.md" (97%) create mode 100644 examples/todomvc/.github/prompts/test-coverage.prompt.md create mode 100644 examples/todomvc/.github/prompts/test-generate.prompt.md create mode 100644 examples/todomvc/.github/prompts/test-heal.prompt.md create mode 100644 examples/todomvc/.github/prompts/test-plan.prompt.md delete mode 100644 examples/todomvc/prompts/test-coverage.md delete mode 100644 examples/todomvc/specs/basic-operations.md create mode 100644 examples/todomvc/specs/basic-operations.plan.md create mode 100644 examples/todomvc/tests/adding-new-todos/add-multiple-todos.spec.ts create mode 100644 examples/todomvc/tests/adding-new-todos/add-single-valid-todo.spec.ts create mode 100644 examples/todomvc/tests/adding-new-todos/add-todo-with-long-text.spec.ts create mode 100644 examples/todomvc/tests/adding-new-todos/add-todo-with-special-characters.spec.ts create mode 100644 examples/todomvc/tests/adding-new-todos/reject-empty-todo.spec.ts create mode 100644 examples/todomvc/tests/adding-new-todos/reject-whitespace-only-todo.spec.ts create mode 100644 examples/todomvc/tests/completing-todos/complete-multiple-todos.spec.ts create mode 100644 examples/todomvc/tests/completing-todos/complete-single-todo.spec.ts create mode 100644 examples/todomvc/tests/completing-todos/mark-all-as-complete.spec.ts create mode 100644 examples/todomvc/tests/completing-todos/uncomplete-todo.spec.ts create mode 100644 examples/todomvc/tests/completing-todos/unmark-all-as-complete.spec.ts delete mode 100644 examples/todomvc/tests/create/add-empty-todo.spec.ts delete mode 100644 examples/todomvc/tests/create/add-multiple-todos.spec.ts delete mode 100644 examples/todomvc/tests/create/add-todo-with-only-whitespace.spec.ts delete mode 100644 examples/todomvc/tests/create/add-todo-with-special-characters.spec.ts delete mode 100644 examples/todomvc/tests/create/add-valid-todo.spec.ts delete mode 100644 examples/todomvc/tests/edit/cancel-edit-with-escape.spec.ts delete mode 100644 examples/todomvc/tests/edit/edit-multiple-todos.spec.ts delete mode 100644 examples/todomvc/tests/edit/edit-todo-text.spec.ts delete mode 100644 examples/todomvc/tests/edit/edit-todo-to-empty-text.spec.ts create mode 100644 examples/todomvc/tests/editing-todos/cancel-edit-with-escape.spec.ts create mode 100644 examples/todomvc/tests/editing-todos/delete-todo-by-clearing-text.spec.ts create mode 100644 examples/todomvc/tests/editing-todos/edit-completed-todo.spec.ts create mode 100644 examples/todomvc/tests/editing-todos/edit-todo-successfully.spec.ts delete mode 100644 examples/todomvc/tests/integration.spec.ts delete mode 100644 examples/todomvc/tests/toggle/mark-all-todos-complete.spec.ts delete mode 100644 examples/todomvc/tests/toggle/mark-multiple-todos-complete.spec.ts delete mode 100644 examples/todomvc/tests/toggle/mark-single-todo-complete.spec.ts delete mode 100644 examples/todomvc/tests/toggle/toggle-all-todos-incomplete.spec.ts delete mode 100644 examples/todomvc/tests/toggle/toggle-todo-incomplete.spec.ts rename packages/playwright/src/agents/{generator.md => generator.agent.md} (100%) rename packages/playwright/src/agents/{healer.md => healer.agent.md} (100%) rename packages/playwright/src/agents/{planner.md => planner.agent.md} (97%) create mode 100644 packages/playwright/src/agents/test-coverage.prompt.md create mode 100644 packages/playwright/src/agents/test-generate.prompt.md create mode 100644 packages/playwright/src/agents/test-heal.prompt.md create mode 100644 packages/playwright/src/agents/test-plan.prompt.md diff --git a/examples/todomvc/.claude/agents/playwright-test-generator.md b/examples/todomvc/.claude/agents/playwright-test-generator.agent.md similarity index 100% rename from examples/todomvc/.claude/agents/playwright-test-generator.md rename to examples/todomvc/.claude/agents/playwright-test-generator.agent.md diff --git a/examples/todomvc/.claude/agents/playwright-test-healer.md b/examples/todomvc/.claude/agents/playwright-test-healer.agent.md similarity index 100% rename from examples/todomvc/.claude/agents/playwright-test-healer.md rename to examples/todomvc/.claude/agents/playwright-test-healer.agent.md diff --git a/examples/todomvc/.claude/agents/playwright-test-planner.md b/examples/todomvc/.claude/agents/playwright-test-planner.agent.md similarity index 97% rename from examples/todomvc/.claude/agents/playwright-test-planner.md rename to examples/todomvc/.claude/agents/playwright-test-planner.agent.md index afca58af3..51ca3f36f 100644 --- a/examples/todomvc/.claude/agents/playwright-test-planner.md +++ b/examples/todomvc/.claude/agents/playwright-test-planner.agent.md @@ -45,6 +45,7 @@ You will: - Executive summary of the tested page/application - Individual scenarios as separate sections - Each scenario formatted with numbered steps + - Each test case with proposed file name for implementation - Clear expected results for verification @@ -69,6 +70,9 @@ application features: **Seed:** `tests/seed.spec.ts` #### 1.1 Add Valid Todo + +**File** `tests/adding-new-todos/add-valid-todo.spec.ts` + **Steps:** 1. Click in the "What needs to be done?" input field 2. Type "Buy groceries" diff --git a/examples/todomvc/.claude/prompts/test-coverage.prompt.md b/examples/todomvc/.claude/prompts/test-coverage.prompt.md new file mode 100644 index 000000000..87146948e --- /dev/null +++ b/examples/todomvc/.claude/prompts/test-coverage.prompt.md @@ -0,0 +1,31 @@ +--- +mode: default +description: Produce test coverage +--- + +Parameters: +- Task: the task to perform +- Seed file (optional): the seed file to use, defaults to `tests/seed.spec.ts` +- Test plan file (optional): the test plan file to write, under `specs/` folder. + +1. Call #playwright-test-planner subagent with prompt: + + + + + + + +2. For each test case from the test plan file (1.1, 1.2, ...), one after another, not in parallel, call #playwright-test-generator subagent with prompt: + + + + + + + + + +3. Call #playwright-test-healer subagent with prompt: + +Run all tests and fix the failing ones one after another. diff --git a/examples/todomvc/.claude/prompts/test-generate.prompt.md b/examples/todomvc/.claude/prompts/test-generate.prompt.md new file mode 100644 index 000000000..d50dcd350 --- /dev/null +++ b/examples/todomvc/.claude/prompts/test-generate.prompt.md @@ -0,0 +1,8 @@ +--- +mode: playwright-test-generator +description: Generate test plan +--- + +Generate tests for the test plan's bullet 1.1 Add item to card. + +Test plan: `specs/coverage.plan.md` diff --git a/examples/todomvc/.claude/prompts/test-heal.prompt.md b/examples/todomvc/.claude/prompts/test-heal.prompt.md new file mode 100644 index 000000000..c4b526deb --- /dev/null +++ b/examples/todomvc/.claude/prompts/test-heal.prompt.md @@ -0,0 +1,6 @@ +--- +mode: playwright-test-healer +description: Fix tests +--- + +Run all my tests and fix the failing ones. diff --git a/examples/todomvc/.claude/prompts/test-plan.prompt.md b/examples/todomvc/.claude/prompts/test-plan.prompt.md new file mode 100644 index 000000000..7f0129ca4 --- /dev/null +++ b/examples/todomvc/.claude/prompts/test-plan.prompt.md @@ -0,0 +1,9 @@ +--- +mode: playwright-test-planner +description: Create test plan +--- + +Create test plan for "add to cart" functionality of my app. + +- Seed file: `tests/seed.spec.ts` +- Test plan: `specs/coverage.plan.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" similarity index 97% rename from "examples/todomvc/.github/chatmodes/ \360\237\216\255 planner.chatmode.md" rename to "examples/todomvc/.github/chatmodes/\360\237\216\255 planner.chatmode.md" index ed3dc5e01..6566e7992 100644 --- "a/examples/todomvc/.github/chatmodes/ \360\237\216\255 planner.chatmode.md" +++ "b/examples/todomvc/.github/chatmodes/\360\237\216\255 planner.chatmode.md" @@ -42,6 +42,7 @@ You will: - Executive summary of the tested page/application - Individual scenarios as separate sections - Each scenario formatted with numbered steps + - Each test case with proposed file name for implementation - Clear expected results for verification @@ -66,6 +67,9 @@ application features: **Seed:** `tests/seed.spec.ts` #### 1.1 Add Valid Todo + +**File** `tests/adding-new-todos/add-valid-todo.spec.ts` + **Steps:** 1. Click in the "What needs to be done?" input field 2. Type "Buy groceries" diff --git a/examples/todomvc/.github/prompts/test-coverage.prompt.md b/examples/todomvc/.github/prompts/test-coverage.prompt.md new file mode 100644 index 000000000..4cc767543 --- /dev/null +++ b/examples/todomvc/.github/prompts/test-coverage.prompt.md @@ -0,0 +1,31 @@ +--- +mode: agent +description: Produce test coverage +--- + +Parameters: +- Task: the task to perform +- Seed file (optional): the seed file to use, defaults to `tests/seed.spec.ts` +- Test plan file (optional): the test plan file to write, under `specs/` folder. + +1. Call #🎭 planner subagent with prompt: + + + + + + + +2. For each test case from the test plan file (1.1, 1.2, ...), one after another, not in parallel, call #🎭 generator subagent with prompt: + + + + + + + + + +3. Call #🎭 healer subagent with prompt: + +Run all tests and fix the failing ones one after another. diff --git a/examples/todomvc/.github/prompts/test-generate.prompt.md b/examples/todomvc/.github/prompts/test-generate.prompt.md new file mode 100644 index 000000000..1de2430f2 --- /dev/null +++ b/examples/todomvc/.github/prompts/test-generate.prompt.md @@ -0,0 +1,8 @@ +--- +mode: 🎭 generator +description: Generate test plan +--- + +Generate tests for the test plan's bullet 1.1 Add item to card. + +Test plan: `specs/coverage.plan.md` diff --git a/examples/todomvc/.github/prompts/test-heal.prompt.md b/examples/todomvc/.github/prompts/test-heal.prompt.md new file mode 100644 index 000000000..6f4f720e6 --- /dev/null +++ b/examples/todomvc/.github/prompts/test-heal.prompt.md @@ -0,0 +1,6 @@ +--- +mode: 🎭 healer +description: Fix tests +--- + +Run all my tests and fix the failing ones. diff --git a/examples/todomvc/.github/prompts/test-plan.prompt.md b/examples/todomvc/.github/prompts/test-plan.prompt.md new file mode 100644 index 000000000..babb1c5f9 --- /dev/null +++ b/examples/todomvc/.github/prompts/test-plan.prompt.md @@ -0,0 +1,9 @@ +--- +mode: 🎭 planner +description: Create test plan +--- + +Create test plan for "add to cart" functionality of my app. + +- Seed file: `tests/seed.spec.ts` +- Test plan: `specs/coverage.plan.md` diff --git a/examples/todomvc/.vscode/mcp.json b/examples/todomvc/.vscode/mcp.json index df015d2ff..f0c78a8aa 100644 --- a/examples/todomvc/.vscode/mcp.json +++ b/examples/todomvc/.vscode/mcp.json @@ -6,8 +6,7 @@ "args": [ "playwright", "run-test-mcp-server" - ], - "cwd": "${workspaceFolder}" + ] } }, "inputs": [] diff --git a/examples/todomvc/prompts/test-coverage.md b/examples/todomvc/prompts/test-coverage.md deleted file mode 100644 index 09d36ebae..000000000 --- a/examples/todomvc/prompts/test-coverage.md +++ /dev/null @@ -1,29 +0,0 @@ - -# Produce test coverage - -Parameters: -- Task: the task to perform -- Seed file (optional): the seed file to use, defaults to tests/seed.spec.ts -- Test plan file (optional): the test plan file to write, under specs/ folder. - -1. Call #planner subagent with prompt: - - - - - - - -2. For each test case from the test plan file (1.1, 1.2, ...), Call #generator subagent with prompt: - - - - - - - - - -3. Call #healer subagent with prompt: - -Run all tests and fix the failing ones one after another. diff --git a/examples/todomvc/specs/basic-operations.md b/examples/todomvc/specs/basic-operations.md deleted file mode 100644 index 0adc624bd..000000000 --- a/examples/todomvc/specs/basic-operations.md +++ /dev/null @@ -1,468 +0,0 @@ -# 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 - -**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 Add Multiple Todos - -**Steps:** -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 -- 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. 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 -- Counter shows "1 item left" -- No encoding or display issues with special characters - -#### 1.4 Add Empty Todo (Negative Test) - -**Steps:** -1. Click in input field but don't type anything -2. 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. Type only spaces " " in input field -2. 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. Add todo "Buy groceries" -2. 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. 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 -- 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. Add todo "Buy groceries" -2. Click checkbox to mark complete -3. 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. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -2. 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. 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 -- 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. 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 -- Text changes to "Buy organic groceries" -- Todo exits edit mode -- Counter remains "1 item left" - -#### 3.2 Cancel Edit with Escape - -**Steps:** -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 -- 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. 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 -- Counter decrements appropriately -- List becomes empty if this was the only todo - -#### 3.4 Edit Multiple Todos - -**Steps:** -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 -- Counter remains "2 items left" -- Both todos maintain their completion state - -### 4. Deleting Todos - -#### 4.1 Delete Single Todo - -**Steps:** -1. Add todo "Buy groceries" -2. Hover over the todo item -3. 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. 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 -- Counter shows "1 item left" -- List controls remain visible - -#### 4.3 Delete Completed Todo - -**Steps:** -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 -- Only "Walk the dog" remains -- Counter shows "1 item left" -- "Clear completed" button disappears - -#### 4.4 Clear All Completed Todos - -**Steps:** -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 -- Only "Walk the dog" remains -- Counter shows "1 item left" -- "Clear completed" button disappears - -### 5. Filtering Todos - -#### 5.1 Filter by All - -**Steps:** -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) -- URL shows "#/" -- "All" filter appears active/highlighted -- Counter shows "2 items left" - -#### 5.2 Filter by Active - -**Steps:** -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") -- 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. 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") -- 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. 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 -- 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. 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. 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. 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" -- 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. Add todo "Buy groceries" -2. 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. Add todos: "Buy groceries", "Walk the dog", "Call dentist" -2. 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. 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 -- 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. 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) -- 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. Type a very long todo text (200+ characters) -2. 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. 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 -- 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` -1. Navigate directly to `{base_url}#/completed` -2. 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. 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 -- 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. diff --git a/examples/todomvc/specs/basic-operations.plan.md b/examples/todomvc/specs/basic-operations.plan.md new file mode 100644 index 000000000..192dde995 --- /dev/null +++ b/examples/todomvc/specs/basic-operations.plan.md @@ -0,0 +1,723 @@ +# TodoMVC Application - Basic Operations Test Plan + +## Application Overview + +The TodoMVC application is a React-based todo list manager accessible at https://demo.playwright.dev/todomvc. The application provides comprehensive task management functionality with the following features: + +- **Task Creation**: Add new todos via input field +- **Task Completion**: Mark individual todos as complete/incomplete via checkboxes +- **Task Editing**: Double-click to edit todo text inline +- **Task Deletion**: Remove individual todos via delete button +- **Bulk Operations**: Mark all todos as complete/incomplete and clear all completed todos +- **Filtering**: View todos by All, Active, or Completed status with URL routing support +- **Counter Display**: Real-time count of active (incomplete) todos +- **Input Validation**: Prevents empty or whitespace-only todos + +## Test Scenarios + +### 1. Adding New Todos + +**Seed:** `tests/seed.spec.ts` + +#### 1.1 Add Single Valid Todo + +**File:** `tests/adding-new-todos/add-single-valid-todo.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +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 an unchecked checkbox +- Todo text displays as "Buy groceries" +- Counter shows "1 item left" +- Input field is cleared and ready for next entry +- "Mark all as complete" checkbox becomes visible + +#### 1.2 Add Multiple Todos + +**File:** `tests/adding-new-todos/add-multiple-todos.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add first todo: "Buy groceries" (type and press Enter) +3. Add second todo: "Walk the dog" (type and press Enter) +4. Add third todo: "Read a book" (type and press Enter) + +**Expected Results:** +- All three todos appear in the list in order of creation +- Each todo has an unchecked checkbox +- Counter shows "3 items left" (plural) +- Input field is cleared after each addition + +#### 1.3 Reject Empty Todo + +**File:** `tests/adding-new-todos/reject-empty-todo.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Click in the "What needs to be done?" input field +3. Press Enter without typing any text + +**Expected Results:** +- No todo is added to the list +- Todo list remains empty +- Counter is not displayed +- Input field remains focused + +#### 1.4 Reject Whitespace-Only Todo + +**File:** `tests/adding-new-todos/reject-whitespace-only-todo.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Click in the "What needs to be done?" input field +3. Type only spaces (e.g., " ") +4. Press Enter + +**Expected Results:** +- No todo is added to the list +- Todo list remains empty +- Counter is not displayed +- Input field is cleared + +#### 1.5 Add Todo with Special Characters + +**File:** `tests/adding-new-todos/add-todo-with-special-characters.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Type "Test with special chars: @#$%^&*()" +3. Press Enter + +**Expected Results:** +- Todo is successfully added +- Special characters are displayed correctly +- Counter shows "1 item left" + +#### 1.6 Add Todo with Long Text + +**File:** `tests/adding-new-todos/add-todo-with-long-text.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Type a very long text (e.g., "This is a very long todo item to test the character limit and see how the application handles extremely long text inputs that might break the layout or cause other issues") +3. Press Enter + +**Expected Results:** +- Todo is successfully added +- Long text is displayed (may wrap or truncate depending on design) +- Counter shows "1 item left" +- Layout remains intact + +### 2. Completing Todos + +**Seed:** `tests/seed.spec.ts` + +#### 2.1 Complete Single Todo + +**File:** `tests/completing-todos/complete-single-todo.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Click the checkbox next to "Buy groceries" + +**Expected Results:** +- Checkbox becomes checked +- Todo text may show visual indication of completion (strikethrough or style change) +- Counter shows "0 items left" +- "Clear completed" button appears +- Delete button becomes visible on hover + +#### 2.2 Complete Multiple Todos + +**File:** `tests/completing-todos/complete-multiple-todos.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add three todos: "Buy groceries", "Walk the dog", "Read a book" +3. Click the checkbox next to "Buy groceries" +4. Click the checkbox next to "Read a book" + +**Expected Results:** +- Both selected todos show as completed +- Counter shows "1 item left" (only "Walk the dog" remaining) +- "Clear completed" button appears +- One todo remains active + +#### 2.3 Uncomplete Todo + +**File:** `tests/completing-todos/uncomplete-todo.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Click the checkbox to complete it +4. Click the checkbox again to uncomplete it + +**Expected Results:** +- Checkbox becomes unchecked +- Todo returns to active state +- Counter shows "1 item left" +- "Clear completed" button disappears + +#### 2.4 Mark All as Complete + +**File:** `tests/completing-todos/mark-all-as-complete.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add three todos: "Buy groceries", "Walk the dog", "Read a book" +3. Click the "Mark all as complete" checkbox (chevron icon) + +**Expected Results:** +- All todos show as completed +- All individual checkboxes are checked +- "Mark all as complete" checkbox is checked +- Counter shows "0 items left" +- "Clear completed" button appears + +#### 2.5 Unmark All as Complete + +**File:** `tests/completing-todos/unmark-all-as-complete.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add three todos: "Buy groceries", "Walk the dog", "Read a book" +3. Click the "Mark all as complete" checkbox to complete all +4. Click the "Mark all as complete" checkbox again + +**Expected Results:** +- All todos return to active state +- All individual checkboxes are unchecked +- "Mark all as complete" checkbox is unchecked +- Counter shows "3 items left" +- "Clear completed" button disappears + +### 3. Editing Todos + +**Seed:** `tests/seed.spec.ts` + +#### 3.1 Edit Todo Successfully + +**File:** `tests/editing-todos/edit-todo-successfully.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Double-click on the todo text "Buy groceries" +4. Clear the existing text +5. Type "Buy groceries and milk" +6. Press Enter + +**Expected Results:** +- Todo enters edit mode (input field appears) +- Original text is pre-populated in the edit field +- After pressing Enter, todo text updates to "Buy groceries and milk" +- Todo exits edit mode +- Todo remains in the same state (active/completed) + +#### 3.2 Cancel Edit with Escape + +**File:** `tests/editing-todos/cancel-edit-with-escape.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Double-click on the todo text +4. Type "Changed text" +5. Press Escape key + +**Expected Results:** +- Todo exits edit mode +- Original text "Buy groceries" is preserved +- Changes are discarded +- Todo remains in the same state + +#### 3.3 Delete Todo by Clearing Text + +**File:** `tests/editing-todos/delete-todo-by-clearing-text.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Double-click on the todo text +4. Clear all text (delete all characters) +5. Press Enter + +**Expected Results:** +- Todo is removed from the list +- Counter decrements appropriately +- If no todos remain, counter and controls disappear + +#### 3.4 Edit Completed Todo + +**File:** `tests/editing-todos/edit-completed-todo.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Click the checkbox to complete it +4. Double-click on the todo text +5. Type "Buy groceries and milk" +6. Press Enter + +**Expected Results:** +- Todo enters edit mode +- Todo text is successfully updated +- Todo remains in completed state after editing +- Checkbox remains checked + +### 4. Deleting Todos + +**Seed:** `tests/seed.spec.ts` + +#### 4.1 Delete Single Active Todo + +**File:** `tests/deleting-todos/delete-single-active-todo.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Hover over the todo to reveal the delete button +4. Click the delete button (×) + +**Expected Results:** +- Todo is immediately removed from the list +- Counter decrements to "0 items left" or disappears +- Todo list controls disappear if no todos remain + +#### 4.2 Delete Single Completed Todo + +**File:** `tests/deleting-todos/delete-single-completed-todo.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Click the checkbox to complete it +4. Hover over the todo to reveal the delete button +5. Click the delete button (×) + +**Expected Results:** +- Todo is immediately removed from the list +- "Clear completed" button disappears +- Todo list controls disappear if no todos remain + +#### 4.3 Delete Multiple Todos Individually + +**File:** `tests/deleting-todos/delete-multiple-todos-individually.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add three todos: "Buy groceries", "Walk the dog", "Read a book" +3. Delete "Walk the dog" by clicking its delete button +4. Delete "Buy groceries" by clicking its delete button + +**Expected Results:** +- After first deletion, counter shows "2 items left" +- After second deletion, counter shows "1 item left" +- Only "Read a book" remains in the list + +#### 4.4 Clear All Completed Todos + +**File:** `tests/deleting-todos/clear-all-completed-todos.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add three todos: "Buy groceries", "Walk the dog", "Read a book" +3. Complete "Buy groceries" and "Walk the dog" by clicking their checkboxes +4. Click the "Clear completed" button + +**Expected Results:** +- Both completed todos are removed from the list +- Only "Read a book" (active) remains +- Counter shows "1 item left" +- "Clear completed" button disappears + +#### 4.5 Clear Completed When All Are Completed + +**File:** `tests/deleting-todos/clear-completed-when-all-are-completed.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add three todos: "Buy groceries", "Walk the dog", "Read a book" +3. Click "Mark all as complete" checkbox +4. Click "Clear completed" button + +**Expected Results:** +- All todos are removed from the list +- Todo list becomes empty +- Counter and controls disappear +- Only the input field remains visible + +### 5. Filtering Todos + +**Seed:** `tests/seed.spec.ts` + +#### 5.1 View All Todos (Default) + +**File:** `tests/filtering-todos/view-all-todos-default.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add three todos: "Buy groceries", "Walk the dog", "Read a book" +3. Complete "Walk the dog" by clicking its checkbox +4. Verify the "All" filter is selected by default + +**Expected Results:** +- All three todos are visible (both active and completed) +- "All" link appears selected/highlighted +- URL shows "/#/" or "/#" +- Counter shows "2 items left" + +#### 5.2 Filter Active Todos + +**File:** `tests/filtering-todos/filter-active-todos.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add three todos: "Buy groceries", "Walk the dog", "Read a book" +3. Complete "Walk the dog" by clicking its checkbox +4. Click the "Active" filter link + +**Expected Results:** +- Only active todos are visible ("Buy groceries" and "Read a book") +- Completed todo "Walk the dog" is hidden +- "Active" link appears selected/highlighted +- URL changes to "/#/active" +- Counter shows "2 items left" + +#### 5.3 Filter Completed Todos + +**File:** `tests/filtering-todos/filter-completed-todos.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add three todos: "Buy groceries", "Walk the dog", "Read a book" +3. Complete "Walk the dog" and "Buy groceries" by clicking their checkboxes +4. Click the "Completed" filter link + +**Expected Results:** +- Only completed todos are visible ("Walk the dog" and "Buy groceries") +- Active todo "Read a book" is hidden +- "Completed" link appears selected/highlighted +- URL changes to "/#/completed" +- Counter still shows "1 item left" (total active count) +- "Clear completed" button is visible + +#### 5.4 Switch Between Filters + +**File:** `tests/filtering-todos/switch-between-filters.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add three todos: "Buy groceries", "Walk the dog", "Read a book" +3. Complete "Walk the dog" +4. Click "Active" filter +5. Click "Completed" filter +6. Click "All" filter + +**Expected Results:** +- Each filter shows appropriate todos +- Filter selection updates correctly +- URL updates with each filter change +- Counter remains consistent across filter changes + +#### 5.5 Add Todo While Filtered + +**File:** `tests/filtering-todos/add-todo-while-filtered.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Complete it by clicking its checkbox +4. Click "Active" filter (should show no todos) +5. Add a new todo: "Walk the dog" + +**Expected Results:** +- New todo appears in the list (as it's active) +- Counter updates to "1 item left" +- Todo is visible because it matches the active filter + +#### 5.6 Complete Todo While on Active Filter + +**File:** `tests/filtering-todos/complete-todo-while-on-active-filter.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add two todos: "Buy groceries" and "Walk the dog" +3. Click "Active" filter +4. Complete "Buy groceries" by clicking its checkbox + +**Expected Results:** +- "Buy groceries" disappears from the active view +- Only "Walk the dog" remains visible +- Counter updates to "1 item left" +- Completed todo is not deleted, just filtered out + +#### 5.7 Delete Todo While Filtered + +**File:** `tests/filtering-todos/delete-todo-while-filtered.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add two todos: "Buy groceries" and "Walk the dog" +3. Complete "Buy groceries" +4. Click "Completed" filter +5. Delete "Buy groceries" using the delete button + +**Expected Results:** +- "Buy groceries" is removed from the list +- Completed filter shows no todos +- Counter shows "1 item left" (for the active todo) +- "Clear completed" button disappears + +### 6. Counter Display + +**Seed:** `tests/seed.spec.ts` + +#### 6.1 Counter Shows Correct Singular Form + +**File:** `tests/counter-display/counter-shows-singular-form.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a single todo: "Buy groceries" + +**Expected Results:** +- Counter displays "1 item left" (singular "item") + +#### 6.2 Counter Shows Correct Plural Form + +**File:** `tests/counter-display/counter-shows-plural-form.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add two todos: "Buy groceries" and "Walk the dog" + +**Expected Results:** +- Counter displays "2 items left" (plural "items") + +#### 6.3 Counter Updates When Completing Todo + +**File:** `tests/counter-display/counter-updates-when-completing.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add three todos: "Buy groceries", "Walk the dog", "Read a book" +3. Complete "Walk the dog" +4. Complete "Buy groceries" + +**Expected Results:** +- Initially shows "3 items left" +- After first completion shows "2 items left" +- After second completion shows "1 item left" + +#### 6.4 Counter Shows Zero When All Completed + +**File:** `tests/counter-display/counter-shows-zero-when-all-completed.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add two todos: "Buy groceries" and "Walk the dog" +3. Click "Mark all as complete" checkbox + +**Expected Results:** +- Counter displays "0 items left" +- Counter remains visible even at zero + +### 7. UI Controls Visibility + +**Seed:** `tests/seed.spec.ts` + +#### 7.1 Controls Hidden When No Todos + +**File:** `tests/ui-controls-visibility/controls-hidden-when-no-todos.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Verify the initial state with no todos + +**Expected Results:** +- "Mark all as complete" checkbox is not visible +- Counter is not displayed +- Filter links are not displayed +- Only the input field and header are visible + +#### 7.2 Controls Appear When First Todo Added + +**File:** `tests/ui-controls-visibility/controls-appear-when-first-todo-added.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" + +**Expected Results:** +- "Mark all as complete" checkbox becomes visible +- Counter appears showing "1 item left" +- Filter links (All/Active/Completed) appear +- Footer with controls is displayed + +#### 7.3 Controls Disappear When Last Todo Removed + +**File:** `tests/ui-controls-visibility/controls-disappear-when-last-todo-removed.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Delete it using the delete button + +**Expected Results:** +- All controls disappear +- View returns to initial empty state +- Only input field remains visible + +#### 7.4 Clear Completed Button Visibility + +**File:** `tests/ui-controls-visibility/clear-completed-button-visibility.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add two todos: "Buy groceries" and "Walk the dog" +3. Complete "Buy groceries" +4. Complete "Walk the dog" +5. Uncomplete "Walk the dog" + +**Expected Results:** +- "Clear completed" button appears after first completion +- Button remains visible while at least one todo is completed +- Button disappears when no todos are completed + +### 8. Edge Cases and Error Handling + +**Seed:** `tests/seed.spec.ts` + +#### 8.1 Rapidly Add Multiple Todos + +**File:** `tests/edge-cases/rapidly-add-multiple-todos.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Quickly add 10 todos by typing and pressing Enter rapidly + +**Expected Results:** +- All 10 todos are successfully added +- Counter shows "10 items left" +- Todos appear in the order they were added +- No todos are lost or duplicated + +#### 8.2 Rapidly Toggle Todo Completion + +**File:** `tests/edge-cases/rapidly-toggle-todo-completion.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Rapidly click the checkbox multiple times (5-10 clicks) + +**Expected Results:** +- Todo state toggles correctly with each click +- Final state is predictable (checked or unchecked) +- Counter updates correctly +- No UI glitches occur + +#### 8.3 Edit During Filter View + +**File:** `tests/edge-cases/edit-during-filter-view.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Click "Active" filter +4. Double-click to edit the todo +5. Change text to "Buy groceries and milk" +6. Press Enter + +**Expected Results:** +- Todo successfully enters edit mode +- Edit is saved correctly +- Todo remains visible in Active filter +- No filter state is lost + +#### 8.4 Navigate Directly to Filtered URL + +**File:** `tests/edge-cases/navigate-directly-to-filtered-url.spec.ts` + +**Steps:** +1. Navigate directly to "https://demo.playwright.dev/todomvc/#/active" +2. Add a todo: "Buy groceries" +3. Complete it + +**Expected Results:** +- Application loads with Active filter pre-selected +- New active todo is visible +- When completed, todo disappears from view +- Filter state is maintained + +#### 8.5 Multiple Browser Tabs (Session Isolation) + +**File:** `tests/edge-cases/multiple-browser-tabs-session-isolation.spec.ts` + +**Steps:** +1. Open TodoMVC in first tab +2. Add a todo: "Buy groceries" in first tab +3. Open TodoMVC in second tab +4. Verify todo list in second tab + +**Expected Results:** +- Second tab either shows the same todo (if using persistence) or starts empty (if session-based) +- Each tab operates independently without conflicts +- No errors occur from multiple instances + +#### 8.6 Hover States Work Correctly + +**File:** `tests/edge-cases/hover-states-work-correctly.spec.ts` + +**Steps:** +1. Navigate to the TodoMVC application +2. Add a todo: "Buy groceries" +3. Hover over the todo item +4. Move mouse away + +**Expected Results:** +- Delete button (×) appears on hover +- Delete button disappears when not hovering +- Hover state does not interfere with editing or clicking + +## Testing Notes + +### Assumptions +- All tests assume a fresh/blank application state at the start (provided by seed file) +- Tests are designed to be independent and can run in any order +- No persistence testing is included (refresh behavior not covered) + +### Browser Compatibility +- Tests should be run across all major browsers (Chromium, Firefox, WebKit) +- UI controls may have slight visual differences across browsers + +### Performance Considerations +- Application should handle at least 100 todos without performance degradation +- Filtering should be instantaneous even with many todos +- No memory leaks should occur with repeated operations + +### Accessibility Considerations +- All interactive elements should be keyboard accessible +- Screen readers should announce todo state changes +- Focus management should be logical during editing + +## Test Coverage Summary + +This test plan covers: +- **47 individual test scenarios** across 8 major functional areas +- Happy path scenarios for all core features +- Edge cases and boundary conditions +- Input validation and error prevention +- UI state management and visibility +- Filter functionality and URL routing +- Counter accuracy and formatting +- Bulk operations and individual actions + +Each test is independent, clearly documented, and designed for automation using Playwright. diff --git a/examples/todomvc/tests/adding-new-todos/add-multiple-todos.spec.ts b/examples/todomvc/tests/adding-new-todos/add-multiple-todos.spec.ts new file mode 100644 index 000000000..234cdb43f --- /dev/null +++ b/examples/todomvc/tests/adding-new-todos/add-multiple-todos.spec.ts @@ -0,0 +1,34 @@ +// spec: Adding New Todos - should add multiple todos +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Adding New Todos', () => { + test('should add multiple todos', async ({ page }) => { + // Add first todo: "Buy groceries" (type and press Enter) + 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'); + + // Add second todo: "Walk the dog" (type and 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'); + + // Add third todo: "Read a book" (type and press Enter) + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Read a book'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); + + // Verify all three todos appear in the list in order of creation + await expect(page.locator('body')).toMatchAriaSnapshot(` +- list: + - listitem: "Buy groceries" + - listitem: "Walk the dog" + - listitem: "Read a book" +`); + + // Verify counter shows "3 items left" (plural) + await expect(page.getByText('3 items left')).toBeVisible(); + + // Verify input field is cleared after each addition + await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toHaveValue(''); + }); +}); diff --git a/examples/todomvc/tests/adding-new-todos/add-single-valid-todo.spec.ts b/examples/todomvc/tests/adding-new-todos/add-single-valid-todo.spec.ts new file mode 100644 index 000000000..5ba7c100a --- /dev/null +++ b/examples/todomvc/tests/adding-new-todos/add-single-valid-todo.spec.ts @@ -0,0 +1,31 @@ +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Adding New Todos', () => { + test('should add single valid todo', async ({ page }) => { + // Click in the "What needs to be done?" input field + await page.getByRole('textbox', { name: 'What needs to be done?' }).click(); + + // Type "Buy groceries" + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Buy groceries'); + + // Press Enter key + await page.keyboard.press('Enter'); + + // Todo appears in the list with an unchecked checkbox + await expect(page.getByTestId('todo-item')).toBeVisible(); + + // Todo text displays as "Buy groceries" + await expect(page.getByText('Buy groceries')).toBeVisible(); + + // Counter shows "1 item left" + await expect(page.getByText('1 item left')).toBeVisible(); + + // Input field is cleared and ready for next entry + await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toHaveValue(''); + + // "Mark all as complete" checkbox becomes visible + await expect(page.getByRole('checkbox', { name: '❯Mark all as complete' })).toBeVisible(); + }); +}); diff --git a/examples/todomvc/tests/adding-new-todos/add-todo-with-long-text.spec.ts b/examples/todomvc/tests/adding-new-todos/add-todo-with-long-text.spec.ts new file mode 100644 index 000000000..b0297040f --- /dev/null +++ b/examples/todomvc/tests/adding-new-todos/add-todo-with-long-text.spec.ts @@ -0,0 +1,17 @@ +import { test, expect } from '../fixtures'; + +test.describe('Adding New Todos', () => { + test('should add todo with long text', async ({ page }) => { + // Type a very long text (e.g., "This is a very long todo item to test the character limit and see how the application handles extremely long text inputs that might break the layout or cause other issues") + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('This is a very long todo item to test the character limit and see how the application handles extremely long text inputs that might break the layout or cause other issues'); + + // Press Enter + await page.keyboard.press('Enter'); + + // Todo is successfully added - Long text is displayed (may wrap or truncate depending on design) + await expect(page.getByText('This is a very long todo item to test the character limit and see how the application handles extremely long text inputs that might break the layout or cause other issues')).toBeVisible(); + + // Counter shows "1 item left" + await expect(page.getByText('1 item left')).toBeVisible(); + }); +}); diff --git a/examples/todomvc/tests/adding-new-todos/add-todo-with-special-characters.spec.ts b/examples/todomvc/tests/adding-new-todos/add-todo-with-special-characters.spec.ts new file mode 100644 index 000000000..0850735af --- /dev/null +++ b/examples/todomvc/tests/adding-new-todos/add-todo-with-special-characters.spec.ts @@ -0,0 +1,20 @@ +// spec: Adding New Todos - should add todo with special characters +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Adding New Todos', () => { + test('should add todo with special characters', async ({ page }) => { + // Type "Test with special chars: @#$%^&*()" + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Test with special chars: @#$%^&*()'); + + // Press Enter + await page.keyboard.press('Enter'); + + // Verify todo is successfully added and special characters are displayed correctly + await expect(page.getByText('Test with special chars: @#$%^&*()')).toBeVisible(); + + // Verify counter shows "1 item left" + await expect(page.getByText('1 item left')).toBeVisible(); + }); +}); diff --git a/examples/todomvc/tests/adding-new-todos/reject-empty-todo.spec.ts b/examples/todomvc/tests/adding-new-todos/reject-empty-todo.spec.ts new file mode 100644 index 000000000..74edbeb2b --- /dev/null +++ b/examples/todomvc/tests/adding-new-todos/reject-empty-todo.spec.ts @@ -0,0 +1,21 @@ +// spec: Adding New Todos +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Adding New Todos', () => { + test('should reject empty todo', async ({ page }) => { + // Click in the "What needs to be done?" input field + await page.getByRole('textbox', { name: 'What needs to be done?' }).click(); + + // Press Enter without typing any text + await page.keyboard.press('Enter'); + + // Verify no todo is added to the list and counter is not displayed + await expect(page.locator('.todo-list')).not.toBeVisible(); + await expect(page.locator('.todo-count')).not.toBeVisible(); + + // Verify input field remains focused + await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeFocused(); + }); +}); diff --git a/examples/todomvc/tests/adding-new-todos/reject-whitespace-only-todo.spec.ts b/examples/todomvc/tests/adding-new-todos/reject-whitespace-only-todo.spec.ts new file mode 100644 index 000000000..3267557aa --- /dev/null +++ b/examples/todomvc/tests/adding-new-todos/reject-whitespace-only-todo.spec.ts @@ -0,0 +1,31 @@ +// spec: Adding New Todos +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Adding New Todos', () => { + test('should reject whitespace-only todo', async ({ page }) => { + // 1. Navigate to the TodoMVC application + // (handled by seed) + + // 2. Click in the "What needs to be done?" input field + await page.getByRole('textbox', { name: 'What needs to be done?' }).click(); + + // 3. Type only spaces (e.g., " ") + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill(' '); + + // 4. Press Enter + await page.keyboard.press('Enter'); + + // Expected Results: + // - No todo is added to the list + // - Todo list remains empty + await expect(page.getByRole('list')).not.toBeVisible(); + + // - Counter is not displayed + await expect(page.getByText(/\d+ items? left/)).not.toBeVisible(); + + // - Input field retains the whitespace (application doesn't clear it) + await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toHaveValue(' '); + }); +}); diff --git a/examples/todomvc/tests/completing-todos/complete-multiple-todos.spec.ts b/examples/todomvc/tests/completing-todos/complete-multiple-todos.spec.ts new file mode 100644 index 000000000..d3dd381e8 --- /dev/null +++ b/examples/todomvc/tests/completing-todos/complete-multiple-todos.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from '../fixtures'; + +test.describe('Completing Todos', () => { + test('should complete multiple todos', async ({ page }) => { + // Add first todo: "Buy groceries" + 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'); + + // Add second todo: "Walk the dog" + 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'); + + // Add third todo: "Read a book" + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Read a book'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); + + // Click the checkbox next to "Buy groceries" + await page.getByRole('listitem').filter({ hasText: 'Buy groceries' }).getByLabel('Toggle Todo').click(); + + // Click the checkbox next to "Read a book" + await page.getByRole('listitem').filter({ hasText: 'Read a book' }).getByLabel('Toggle Todo').click(); + + // Verify "Buy groceries" is completed + await expect(page.getByRole('listitem').filter({ hasText: 'Buy groceries' }).getByLabel('Toggle Todo')).toBeChecked(); + + // Verify "Read a book" is completed + await expect(page.getByRole('listitem').filter({ hasText: 'Read a book' }).getByLabel('Toggle Todo')).toBeChecked(); + + // Verify counter shows "1 item left" + await expect(page.getByText('1 item left')).toBeVisible(); + + // Verify "Clear completed" button appears + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + + // Verify "Walk the dog" remains active (unchecked) + await expect(page.getByRole('listitem').filter({ hasText: 'Walk the dog' }).getByLabel('Toggle Todo')).not.toBeChecked(); + }); +}); diff --git a/examples/todomvc/tests/completing-todos/complete-single-todo.spec.ts b/examples/todomvc/tests/completing-todos/complete-single-todo.spec.ts new file mode 100644 index 000000000..dee8cb196 --- /dev/null +++ b/examples/todomvc/tests/completing-todos/complete-single-todo.spec.ts @@ -0,0 +1,27 @@ +// spec: Completing Todos - should complete single todo +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Completing Todos', () => { + test('should complete single todo', async ({ page }) => { + // Add a todo: "Buy groceries" + 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'); + + // Click the checkbox next to "Buy groceries" + await page.getByRole('checkbox', { name: 'Toggle Todo' }).click(); + + // Verify checkbox becomes checked + await expect(page.getByRole('checkbox', { name: 'Toggle Todo' })).toBeChecked(); + + // Verify counter shows "0 items left" + await expect(page.getByText('0 items left')).toBeVisible(); + + // Verify "Clear completed" button appears + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + + // Verify delete button becomes visible + await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible(); + }); +}); diff --git a/examples/todomvc/tests/completing-todos/mark-all-as-complete.spec.ts b/examples/todomvc/tests/completing-todos/mark-all-as-complete.spec.ts new file mode 100644 index 000000000..363d7c2d8 --- /dev/null +++ b/examples/todomvc/tests/completing-todos/mark-all-as-complete.spec.ts @@ -0,0 +1,27 @@ +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Completing Todos', () => { + test('should mark all as complete', async ({ page }) => { + // Add three todos: "Buy groceries", "Walk the dog", "Read a book" + 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'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Read a book'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); + + // Click the "Mark all as complete" checkbox (chevron icon) + await page.getByRole('checkbox', { name: '❯Mark all as complete' }).click(); + + // Verify "Mark all as complete" checkbox is checked + await expect(page.getByRole('checkbox', { name: '❯Mark all as complete' })).toBeChecked(); + + // Verify counter shows "0 items left" + await expect(page.getByText('0')).toBeVisible(); + + // Verify "Clear completed" button appears + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); +}); diff --git a/examples/todomvc/tests/completing-todos/uncomplete-todo.spec.ts b/examples/todomvc/tests/completing-todos/uncomplete-todo.spec.ts new file mode 100644 index 000000000..9b26a22cd --- /dev/null +++ b/examples/todomvc/tests/completing-todos/uncomplete-todo.spec.ts @@ -0,0 +1,24 @@ +// spec: Completing Todos - should uncomplete todo +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Completing Todos', () => { + test('should uncomplete todo', async ({ page }) => { + // Add a todo: "Buy groceries" + 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'); + + // Click the checkbox to complete it + await page.getByRole('checkbox', { name: 'Toggle Todo' }).click(); + + // Click the checkbox again to uncomplete it + await page.getByRole('checkbox', { name: 'Toggle Todo' }).click(); + + // Verify checkbox becomes unchecked + await expect(page.getByRole('checkbox', { name: 'Toggle Todo' })).not.toBeChecked(); + + // Verify counter shows "1 item left" + await expect(page.getByText('1 item left')).toBeVisible(); + }); +}); diff --git a/examples/todomvc/tests/completing-todos/unmark-all-as-complete.spec.ts b/examples/todomvc/tests/completing-todos/unmark-all-as-complete.spec.ts new file mode 100644 index 000000000..afff06aeb --- /dev/null +++ b/examples/todomvc/tests/completing-todos/unmark-all-as-complete.spec.ts @@ -0,0 +1,37 @@ +// spec: Completing Todos - should unmark all as complete +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Completing Todos', () => { + test('should unmark all as complete', async ({ page }) => { + // Add first todo: "Buy groceries" + 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'); + + // Add second todo: "Walk the dog" + 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'); + + // Add third todo: "Read a book" + await page.getByRole('textbox', { name: 'What needs to be done?' }).fill('Read a book'); + await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter'); + + // Click the "Mark all as complete" checkbox to complete all + await page.getByRole('checkbox', { name: '❯Mark all as complete' }).click(); + + // Click the "Mark all as complete" checkbox again + await page.getByRole('checkbox', { name: '❯Mark all as complete' }).click(); + + // Verify "Mark all as complete" checkbox is unchecked + await expect(page.getByRole('checkbox', { name: '❯Mark all as complete' })).not.toBeChecked(); + + // Verify all individual checkboxes are unchecked + await expect(page.getByRole('listitem').filter({ hasText: 'Buy groceries' }).getByLabel('Toggle Todo')).not.toBeChecked(); + await expect(page.getByRole('listitem').filter({ hasText: 'Walk the dog' }).getByLabel('Toggle Todo')).not.toBeChecked(); + await expect(page.getByRole('listitem').filter({ hasText: 'Read a book' }).getByLabel('Toggle Todo')).not.toBeChecked(); + + // Verify counter shows "3 items left" + await expect(page.getByText('3 items left')).toBeVisible(); + }); +}); diff --git a/examples/todomvc/tests/create/add-empty-todo.spec.ts b/examples/todomvc/tests/create/add-empty-todo.spec.ts deleted file mode 100644 index 1ed79f985..000000000 --- a/examples/todomvc/tests/create/add-empty-todo.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -// 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 deleted file mode 100644 index a698ca554..000000000 --- a/examples/todomvc/tests/create/add-multiple-todos.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 6a7b70d89..000000000 --- a/examples/todomvc/tests/create/add-todo-with-only-whitespace.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -// 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 deleted file mode 100644 index 3b6bcaf42..000000000 --- a/examples/todomvc/tests/create/add-todo-with-special-characters.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index 0353dba82..000000000 --- a/examples/todomvc/tests/create/add-valid-todo.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index 9fc5ad164..000000000 --- a/examples/todomvc/tests/edit/cancel-edit-with-escape.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -// 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 deleted file mode 100644 index 643921ed7..000000000 --- a/examples/todomvc/tests/edit/edit-multiple-todos.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -// 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 deleted file mode 100644 index f0629ba74..000000000 --- a/examples/todomvc/tests/edit/edit-todo-text.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -// 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 deleted file mode 100644 index 961e3e392..000000000 --- a/examples/todomvc/tests/edit/edit-todo-to-empty-text.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -// 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/editing-todos/cancel-edit-with-escape.spec.ts b/examples/todomvc/tests/editing-todos/cancel-edit-with-escape.spec.ts new file mode 100644 index 000000000..9a7d57f07 --- /dev/null +++ b/examples/todomvc/tests/editing-todos/cancel-edit-with-escape.spec.ts @@ -0,0 +1,24 @@ +// spec: Editing Todos - Cancel edit with escape +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Editing Todos', () => { + test('should cancel edit with escape', async ({ page }) => { + // Add a todo: "Buy groceries" + 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'); + + // Double-click on the todo text + await page.getByTestId('todo-title').dblclick(); + + // Type "Changed text" + await page.getByRole('textbox', { name: 'Edit' }).fill('Changed text'); + + // Press Escape key + await page.keyboard.press('Escape'); + + // Verify original text "Buy groceries" is preserved + await expect(page.getByText('Buy groceries')).toBeVisible(); + }); +}); diff --git a/examples/todomvc/tests/editing-todos/delete-todo-by-clearing-text.spec.ts b/examples/todomvc/tests/editing-todos/delete-todo-by-clearing-text.spec.ts new file mode 100644 index 000000000..312002502 --- /dev/null +++ b/examples/todomvc/tests/editing-todos/delete-todo-by-clearing-text.spec.ts @@ -0,0 +1,24 @@ +// spec: Editing Todos - should delete todo by clearing text +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Editing Todos', () => { + test('should delete todo by clearing text', async ({ page }) => { + // Add a todo: "Buy groceries" + 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'); + + // Double-click on the todo text + await page.getByTestId('todo-title').dblclick(); + + // Clear all text (delete all characters) + await page.getByRole('textbox', { name: 'Edit' }).fill(''); + + // Press Enter + await page.keyboard.press('Enter'); + + // Verify the input field is still visible + await expect(page.getByRole('textbox', { name: 'What needs to be done?' })).toBeVisible(); + }); +}); diff --git a/examples/todomvc/tests/editing-todos/edit-completed-todo.spec.ts b/examples/todomvc/tests/editing-todos/edit-completed-todo.spec.ts new file mode 100644 index 000000000..20d79b95d --- /dev/null +++ b/examples/todomvc/tests/editing-todos/edit-completed-todo.spec.ts @@ -0,0 +1,28 @@ +// spec: Editing Todos - Edit Completed Todo +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Editing Todos', () => { + test('should edit completed todo', async ({ page }) => { + // Add a todo: "Buy groceries" + 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'); + + // Click the checkbox to complete it + await page.getByRole('checkbox', { name: 'Toggle Todo' }).click(); + + // Double-click on the todo text + await page.getByTestId('todo-title').dblclick(); + + // Type "Buy groceries and milk" and press Enter + await page.getByRole('textbox', { name: 'Edit' }).fill('Buy groceries and milk'); + await page.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Verify todo text is successfully updated + await expect(page.getByText('Buy groceries and milk')).toBeVisible(); + + // Verify checkbox remains checked (todo remains in completed state) + await expect(page.getByRole('checkbox', { name: 'Toggle Todo' })).toBeChecked(); + }); +}); diff --git a/examples/todomvc/tests/editing-todos/edit-todo-successfully.spec.ts b/examples/todomvc/tests/editing-todos/edit-todo-successfully.spec.ts new file mode 100644 index 000000000..a9e9fcbfe --- /dev/null +++ b/examples/todomvc/tests/editing-todos/edit-todo-successfully.spec.ts @@ -0,0 +1,24 @@ +// spec: Editing Todos - should edit todo successfully +// seed: tests/seed.spec.ts + +import { test, expect } from '../fixtures'; + +test.describe('Editing Todos', () => { + test('should edit todo successfully', async ({ page }) => { + // Add a todo: "Buy groceries" + 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'); + + // Double-click on the todo text "Buy groceries" + await page.getByTestId('todo-title').dblclick(); + + // Clear the existing text and type "Buy groceries and milk" + await page.getByRole('textbox', { name: 'Edit' }).fill('Buy groceries and milk'); + + // Press Enter to save the edited todo + await page.keyboard.press('Enter'); + + // Verify that todo text updates to "Buy groceries and milk" + await expect(page.getByText('Buy groceries and milk')).toBeVisible(); + }); +}); diff --git a/examples/todomvc/tests/integration.spec.ts b/examples/todomvc/tests/integration.spec.ts deleted file mode 100644 index f41b0a2d5..000000000 --- a/examples/todomvc/tests/integration.spec.ts +++ /dev/null @@ -1,427 +0,0 @@ -/* eslint-disable notice/notice */ - -import { test, expect } from './fixtures'; -import type { Page } from '@playwright/test'; - -test.describe.configure({ mode: 'parallel' }); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment' -]; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0] - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1], - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(page.getByTestId('todo-count')).toHaveText('3 items left'); - await expect(page.getByTestId('todo-count')).toContainText('3'); - await expect(page.getByTestId('todo-count')).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should show #main and #footer when items added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(page.locator('.main')).toBeVisible(); - await expect(page.locator('.footer')).toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodo.getByRole('checkbox').uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2] - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - await expect(page.getByTestId('todo-count')).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(page.getByTestId('todo-count')).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(0).getByRole('checkbox').check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - await page.locator('.todo-list li .toggle').nth(1).check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(2); - await expect(page.getByTestId('todo-item')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - await page.locator('.todo-list li .toggle').nth(1).check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(page.getByTestId('todo-item')).toHaveCount(1); - await page.goBack(); - await expect(page.getByTestId('todo-item')).toHaveCount(2); - await page.goBack(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.locator('.todo-list li .toggle').nth(1).check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.locator('.todo-list li .toggle').nth(1).check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - await page.getByRole('link', { name: 'Active' }).click(); - // Page change - active items. - await expect(page.getByRole('link', { name: 'Active' })).toHaveClass('selected'); - await page.getByRole('link', { name: 'Completed' }).click(); - // Page change - completed items. - await expect(page.getByRole('link', { name: 'Completed' })).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} diff --git a/examples/todomvc/tests/toggle/mark-all-todos-complete.spec.ts b/examples/todomvc/tests/toggle/mark-all-todos-complete.spec.ts deleted file mode 100644 index 3bd89cb4d..000000000 --- a/examples/todomvc/tests/toggle/mark-all-todos-complete.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index 4a6ed1a2d..000000000 --- a/examples/todomvc/tests/toggle/mark-multiple-todos-complete.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -// 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 deleted file mode 100644 index b76a25f41..000000000 --- a/examples/todomvc/tests/toggle/mark-single-todo-complete.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index 11ec10d22..000000000 --- a/examples/todomvc/tests/toggle/toggle-all-todos-incomplete.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -// 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 deleted file mode 100644 index 4f775fed3..000000000 --- a/examples/todomvc/tests/toggle/toggle-todo-incomplete.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -// 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/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts index 0145c29ac..ad9cd951d 100644 --- a/packages/playwright/src/agents/generateAgents.ts +++ b/packages/playwright/src/agents/generateAgents.ts @@ -43,7 +43,7 @@ interface Agent { class AgentParser { static async loadAgents(): Promise { const files = await fs.promises.readdir(__dirname); - return Promise.all(files.filter(file => file.endsWith('.md')).map(file => AgentParser.parseFile(path.join(__dirname, file)))); + return Promise.all(files.filter(file => file.endsWith('.agent.md')).map(file => AgentParser.parseFile(path.join(__dirname, file)))); } static async parseFile(filePath: string): Promise { @@ -109,13 +109,15 @@ class AgentParser { export class ClaudeGenerator { static async init(config: FullConfigInternal, projectName: string) { - await initRepo(config, projectName); + await initRepo(config, projectName, { + promptsFolder: '.claude/prompts', + }); const agents = await AgentParser.loadAgents(); await fs.promises.mkdir('.claude/agents', { recursive: true }); for (const agent of agents) - await writeFile(`.claude/agents/${agent.header.name}.md`, ClaudeGenerator.agentSpec(agent), '🤖', 'agent definition'); + await writeFile(`.claude/agents/${agent.header.name}.agent.md`, ClaudeGenerator.agentSpec(agent), '🤖', 'agent definition'); await writeFile('.mcp.json', JSON.stringify({ mcpServers: { @@ -160,16 +162,18 @@ export class ClaudeGenerator { export class OpencodeGenerator { static async init(config: FullConfigInternal, projectName: string) { - await initRepo(config, projectName); + await initRepo(config, projectName, { + agentDefault: 'Build', + promptsFolder: '.opencode/prompts' + }); const agents = await AgentParser.loadAgents(); - await fs.promises.mkdir('.opencode/prompts', { recursive: true }); for (const agent of agents) { const prompt = [agent.instructions]; prompt.push(''); prompt.push(...agent.examples.map(example => `${example}`)); - await writeFile(`.opencode/prompts/${agent.header.name}.md`, prompt.join('\n'), '🤖', 'agent definition'); + await writeFile(`.opencode/prompts/${agent.header.name}.agent.md`, prompt.join('\n'), '🤖', 'agent definition'); } await writeFile('opencode.json', OpencodeGenerator.configuration(agents), '🔧', 'opencode configuration'); @@ -206,7 +210,7 @@ export class OpencodeGenerator { result['agent'][agent.header.name] = { description: agent.header.description, mode: 'subagent', - prompt: `{file:.opencode/prompts/${agent.header.name}.md}`, + prompt: `{file:.opencode/prompts/${agent.header.name}.agent.md}`, tools, }; for (const tool of agent.header.tools) @@ -230,7 +234,9 @@ export class AgentGenerator { return; } - await initRepo(config, projectName); + await initRepo(config, projectName, { + promptsFolder: path.join(agentsFolder, 'prompts') + }); const agents = await AgentParser.loadAgents(); @@ -262,15 +268,23 @@ export class AgentGenerator { export class VSCodeGenerator { static async init(config: FullConfigInternal, projectName: string) { - await initRepo(config, projectName); + await initRepo(config, projectName, { + agentDefault: 'agent', + agentHealer: '🎭 healer', + agentGenerator: '🎭 generator', + agentPlanner: '🎭 planner', + promptsFolder: '.github/prompts' + }); const agents = await AgentParser.loadAgents(); const nameMap = new Map([ - ['playwright-test-planner', ' 🎭 planner'], + ['playwright-test-planner', '🎭 planner'], ['playwright-test-generator', '🎭 generator'], ['playwright-test-healer', '🎭 healer'], ]); + await deleteFile(`.github/chatmodes/ 🎭 planner.chatmode.md`, 'old planner chatmode'); + await fs.promises.mkdir('.github/chatmodes', { recursive: true }); for (const agent of agents) await writeFile(`.github/chatmodes/${nameMap.get(agent.header.name)}.chatmode.md`, VSCodeGenerator.agentSpec(agent), '🤖', 'chatmode definition'); @@ -349,7 +363,27 @@ async function writeFile(filePath: string, content: string, icon: string, descri await fs.promises.writeFile(filePath, content, 'utf-8'); } -async function initRepo(config: FullConfigInternal, projectName: string) { +async function deleteFile(filePath: string, description: string) { + try { + if (!fs.existsSync(filePath)) + return; + } catch { + return; + } + + console.log(`- ✂️ ${path.relative(process.cwd(), filePath)} ${colors.dim('- ' + description)}`); + await fs.promises.unlink(filePath); +} + +type RepoParams = { + promptsFolder: string; + agentDefault?: string; + agentHealer?: string; + agentGenerator?: string; + agentPlanner?: string; +}; + +async function initRepo(config: FullConfigInternal, projectName: string, options: RepoParams) { const project = seedProject(config, projectName); console.log(`🎭 Using project "${project.project.name}" as a primary project`); @@ -361,56 +395,37 @@ This is a directory for test plans. `, '📝', 'directory for test plans'); } - if (!fs.existsSync('prompts')) { - await fs.promises.mkdir('prompts'); - await writeFile(path.join('prompts', 'README.md'), `# Prompts - -This is a directory for useful prompts. -`, '📝', 'useful prompts'); - } - let seedFile = await findSeedFile(project); if (!seedFile) { seedFile = defaultSeedFile(project); await writeFile(seedFile, seedFileContent, '🌱', 'default environment seed file'); } - const coveragePromptFile = path.join('prompts', 'test-coverage.md'); - if (!fs.existsSync(coveragePromptFile)) - await writeFile(coveragePromptFile, coveragePrompt(seedFile), '📝', 'test coverage prompt'); + await fs.promises.mkdir(options.promptsFolder, { recursive: true }); + + for (const promptFile of await fs.promises.readdir(__dirname)) { + if (!promptFile.endsWith('.prompt.md')) + continue; + const content = await loadPrompt(promptFile, { ...options, seedFile: path.relative(process.cwd(), seedFile) }); + await writeFile(path.join(options.promptsFolder, promptFile), content, '📝', 'prompt template'); + } } function initRepoDone() { console.log('✅ Done.'); } -const coveragePrompt = (seedFile: string) => ` -# Produce test coverage - -Parameters: -- Task: the task to perform -- Seed file (optional): the seed file to use, defaults to ${path.relative(process.cwd(), seedFile)} -- Test plan file (optional): the test plan file to write, under specs/ folder. - -1. Call #playwright-test-planner subagent with prompt: - - - - - - - -2. For each test case from the test plan file (1.1, 1.2, ...), Call #playwright-test-generator subagent with prompt: - - - - - - - - - -3. Call #playwright-test-healer subagent with prompt: - -Run all tests and fix the failing ones one after another. -`; +async function loadPrompt(file: string, params: Record) { + const templateParams = { + agentDefault: params.agentDefault ?? 'default', + agentHealer: params.agentHealer ?? 'playwright-test-healer', + agentGenerator: params.agentGenerator ?? 'playwright-test-generator', + agentPlanner: params.agentPlanner ?? 'playwright-test-planner', + seedFile: params.seedFile, + }; + + const content = await fs.promises.readFile(path.join(__dirname, file), 'utf-8'); + return Object.entries(templateParams).reduce((acc, [key, value]) => { + return acc.replace(new RegExp(`\\\${${key}}`, 'g'), value); + }, content); +} diff --git a/packages/playwright/src/agents/generator.md b/packages/playwright/src/agents/generator.agent.md similarity index 100% rename from packages/playwright/src/agents/generator.md rename to packages/playwright/src/agents/generator.agent.md diff --git a/packages/playwright/src/agents/healer.md b/packages/playwright/src/agents/healer.agent.md similarity index 100% rename from packages/playwright/src/agents/healer.md rename to packages/playwright/src/agents/healer.agent.md diff --git a/packages/playwright/src/agents/planner.md b/packages/playwright/src/agents/planner.agent.md similarity index 97% rename from packages/playwright/src/agents/planner.md rename to packages/playwright/src/agents/planner.agent.md index ae58a7bfe..cbc350ec7 100644 --- a/packages/playwright/src/agents/planner.md +++ b/packages/playwright/src/agents/planner.agent.md @@ -66,6 +66,7 @@ You will: - Executive summary of the tested page/application - Individual scenarios as separate sections - Each scenario formatted with numbered steps + - Each test case with proposed file name for implementation - Clear expected results for verification @@ -90,6 +91,9 @@ application features: **Seed:** `tests/seed.spec.ts` #### 1.1 Add Valid Todo + +**File** `tests/adding-new-todos/add-valid-todo.spec.ts` + **Steps:** 1. Click in the "What needs to be done?" input field 2. Type "Buy groceries" diff --git a/packages/playwright/src/agents/test-coverage.prompt.md b/packages/playwright/src/agents/test-coverage.prompt.md new file mode 100644 index 000000000..12923b36a --- /dev/null +++ b/packages/playwright/src/agents/test-coverage.prompt.md @@ -0,0 +1,31 @@ +--- +mode: ${agentDefault} +description: Produce test coverage +--- + +Parameters: +- Task: the task to perform +- Seed file (optional): the seed file to use, defaults to `${seedFile}` +- Test plan file (optional): the test plan file to write, under `specs/` folder. + +1. Call #${agentPlanner} subagent with prompt: + + + + + + + +2. For each test case from the test plan file (1.1, 1.2, ...), one after another, not in parallel, call #${agentGenerator} subagent with prompt: + + + + + + + + + +3. Call #${agentHealer} subagent with prompt: + +Run all tests and fix the failing ones one after another. diff --git a/packages/playwright/src/agents/test-generate.prompt.md b/packages/playwright/src/agents/test-generate.prompt.md new file mode 100644 index 000000000..ef4b5cb45 --- /dev/null +++ b/packages/playwright/src/agents/test-generate.prompt.md @@ -0,0 +1,8 @@ +--- +mode: ${agentGenerator} +description: Generate test plan +--- + +Generate tests for the test plan's bullet 1.1 Add item to card. + +Test plan: `specs/coverage.plan.md` diff --git a/packages/playwright/src/agents/test-heal.prompt.md b/packages/playwright/src/agents/test-heal.prompt.md new file mode 100644 index 000000000..51a7d3b48 --- /dev/null +++ b/packages/playwright/src/agents/test-heal.prompt.md @@ -0,0 +1,6 @@ +--- +mode: ${agentHealer} +description: Fix tests +--- + +Run all my tests and fix the failing ones. diff --git a/packages/playwright/src/agents/test-plan.prompt.md b/packages/playwright/src/agents/test-plan.prompt.md new file mode 100644 index 000000000..b85285904 --- /dev/null +++ b/packages/playwright/src/agents/test-plan.prompt.md @@ -0,0 +1,9 @@ +--- +mode: ${agentPlanner} +description: Create test plan +--- + +Create test plan for "add to cart" functionality of my app. + +- Seed file: `${seedFile}` +- Test plan: `specs/coverage.plan.md` diff --git a/tests/mcp/init-agents.spec.ts b/tests/mcp/init-agents.spec.ts index cedb2bd91..e2d09e3a9 100644 --- a/tests/mcp/init-agents.spec.ts +++ b/tests/mcp/init-agents.spec.ts @@ -73,6 +73,11 @@ test('create seed file with --config', async ({ }) => { expect(fs.existsSync(path.join(baseDir, 'custom', 'bar', 'e2e', 'seed.spec.ts'))).toBe(true); }); +async function ensureFiles(baseDir: string, files: string[]) { + for (const file of files) + expect(fs.existsSync(path.join(baseDir, file))).toBe(true); +} + test('init claude agents', async ({ }) => { const baseDir = await writeFiles({ 'playwright.config.ts': ` @@ -84,9 +89,15 @@ test('init claude agents', async ({ }) => { cwd: baseDir, args: ['--loop', 'claude'], }); - expect(fs.existsSync(path.join(baseDir, '.claude', 'agents', 'playwright-test-planner.md'))).toBe(true); - expect(fs.existsSync(path.join(baseDir, '.claude', 'agents', 'playwright-test-generator.md'))).toBe(true); - expect(fs.existsSync(path.join(baseDir, '.claude', 'agents', 'playwright-test-healer.md'))).toBe(true); + await ensureFiles(baseDir, [ + '.claude/agents/playwright-test-planner.agent.md', + '.claude/agents/playwright-test-generator.agent.md', + '.claude/agents/playwright-test-healer.agent.md', + '.claude/prompts/test-coverage.prompt.md', + '.claude/prompts/test-plan.prompt.md', + '.claude/prompts/test-generate.prompt.md', + '.claude/prompts/test-heal.prompt.md', + ]); }); test('init vscode agents', async ({ }) => { @@ -100,10 +111,16 @@ test('init vscode agents', async ({ }) => { cwd: baseDir, args: ['--loop', 'vscode'], }); - expect(fs.existsSync(path.join(baseDir, '.github', 'chatmodes', '🎭 generator.chatmode.md'))).toBe(true); - expect(fs.existsSync(path.join(baseDir, '.github', 'chatmodes', ' 🎭 planner.chatmode.md'))).toBe(true); - expect(fs.existsSync(path.join(baseDir, '.github', 'chatmodes', '🎭 healer.chatmode.md'))).toBe(true); - expect(fs.existsSync(path.join(baseDir, '.vscode', 'mcp.json'))).toBe(true); + + await ensureFiles(baseDir, [ + '.github/chatmodes/🎭 generator.chatmode.md', + '.github/chatmodes/🎭 planner.chatmode.md', + '.github/chatmodes/🎭 healer.chatmode.md', + '.github/prompts/test-plan.prompt.md', + '.github/prompts/test-generate.prompt.md', + '.github/prompts/test-heal.prompt.md', + '.vscode/mcp.json', + ]); }); test('init opencode agents', async ({ }) => { @@ -117,8 +134,14 @@ test('init opencode agents', async ({ }) => { cwd: baseDir, args: ['--loop', 'opencode'], }); - expect(fs.existsSync(path.join(baseDir, '.opencode', 'prompts', 'playwright-test-planner.md'))).toBe(true); - expect(fs.existsSync(path.join(baseDir, '.opencode', 'prompts', 'playwright-test-generator.md'))).toBe(true); - expect(fs.existsSync(path.join(baseDir, '.opencode', 'prompts', 'playwright-test-healer.md'))).toBe(true); - expect(fs.existsSync(path.join(baseDir, 'opencode.json'))).toBe(true); + + await ensureFiles(baseDir, [ + '.opencode/prompts/playwright-test-planner.agent.md', + '.opencode/prompts/playwright-test-generator.agent.md', + '.opencode/prompts/playwright-test-healer.agent.md', + '.opencode/prompts/test-plan.prompt.md', + '.opencode/prompts/test-generate.prompt.md', + '.opencode/prompts/test-heal.prompt.md', + 'opencode.json', + ]); }); From 6c335d643198d312350b457a020757db505be612 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 17 Oct 2025 17:46:04 -0700 Subject: [PATCH 088/250] test: allow adding cookies with >400 days expiration (#37913) --- tests/library/browsercontext-cookies.spec.ts | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/library/browsercontext-cookies.spec.ts b/tests/library/browsercontext-cookies.spec.ts index 4a0573346..f9c3a4536 100644 --- a/tests/library/browsercontext-cookies.spec.ts +++ b/tests/library/browsercontext-cookies.spec.ts @@ -72,6 +72,42 @@ it('should get a non-session cookie', async ({ context, page, server, defaultSam expect(cookies[0].expires).toBeGreaterThan((Date.now() + FOUR_HUNDRED_DAYS - FIVE_MINUTES) / 1000); }); +it('should allow adding cookies with >400 days expiration', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37903' } +}, async ({ context, server, browserName, channel }) => { + it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'Firefox fails to add cookies with >400 days expiration'); + + // Browsers start to cap cookies with 400 days max expires value. + // See https://github.com/httpwg/http-extensions/pull/1732 + // Chromium patch: https://chromium.googlesource.com/chromium/src/+/aaa5d2b55478eac2ee642653dcd77a50ac3faff6 + const expire = Date.now() / 1000 + 401 * 24 * 3600; + await context.addCookies([ + { + name: 'username', + value: 'John Doe', + domain: server.HOSTNAME, + path: '/', + expires: expire, + httpOnly: false, + secure: false, + sameSite: 'Lax', + } + ]); + + const cookies = await context.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0]).toEqual({ + name: 'username', + value: 'John Doe', + domain: server.HOSTNAME, + path: '/', + expires: expect.anything(), + httpOnly: false, + secure: false, + sameSite: 'Lax', + }); +}); + it('should properly report httpOnly cookie', async ({ context, page, server }) => { server.setRoute('/empty.html', (req, res) => { res.setHeader('Set-Cookie', 'name=value;HttpOnly; Path=/'); From 9cc54a9bc247a8c3814b5efa357903d3f3c50681 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 20 Oct 2025 09:02:07 +0100 Subject: [PATCH 089/250] chore: roll to chromium-tip-of-tree 1379, update for builds being CfT (#37883) --- packages/playwright-core/browsers.json | 8 +- .../src/server/registry/index.ts | 84 ++++++++++++------- .../src/server/utils/hostPlatform.ts | 14 ++++ 3 files changed, 74 insertions(+), 32 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index c628dae3e..558923e30 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,15 +15,15 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1371", + "revision": "1379", "installByDefault": false, - "browserVersion": "142.0.7430.0" + "browserVersion": "143.0.7476.0" }, { "name": "chromium-tip-of-tree-headless-shell", - "revision": "1371", + "revision": "1379", "installByDefault": false, - "browserVersion": "142.0.7430.0" + "browserVersion": "143.0.7476.0" }, { "name": "firefox", diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 3fb031c01..a591b9351 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -26,7 +26,7 @@ import { installDependenciesLinux, installDependenciesWindows, validateDependenc import { calculateSha1, getAsBooleanFromENV, getFromENV, getPackageManagerExecCommand } from '../../utils'; import { wrapInASCIIBox } from '../utils/ascii'; import { debugLogger } from '../utils/debugLogger'; -import { hostPlatform, isOfficiallySupportedPlatform } from '../utils/hostPlatform'; +import { shortPlatform, hostPlatform, isOfficiallySupportedPlatform } from '../utils/hostPlatform'; import { fetchData, NET_DEFAULT_TIMEOUT } from '../utils/network'; import { spawnAsync } from '../utils/spawnAsync'; import { getEmbedderName } from '../utils/userAgent'; @@ -60,34 +60,68 @@ if (process.env.PW_TEST_CDN_THAT_SHOULD_WORK) { const EXECUTABLE_PATHS = { 'chromium': { - 'linux': ['chrome-linux', 'chrome'], - 'mac': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'], - 'win': ['chrome-win', 'chrome.exe'], + '': undefined, + 'linux-x64': ['chrome-linux', 'chrome'], + 'linux-arm64': ['chrome-linux', 'chrome'], + 'mac-x64': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'], + 'mac-arm64': ['chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'], + 'win-x64': ['chrome-win', 'chrome.exe'], }, 'chromium-headless-shell': { - 'linux': ['chrome-linux', 'headless_shell'], - 'mac': ['chrome-mac', 'headless_shell'], - 'win': ['chrome-win', 'headless_shell.exe'], + '': undefined, + 'linux-x64': ['chrome-linux', 'headless_shell'], + 'linux-arm64': ['chrome-linux', 'headless_shell'], + 'mac-x64': ['chrome-mac', 'headless_shell'], + 'mac-arm64': ['chrome-mac', 'headless_shell'], + 'win-x64': ['chrome-win', 'headless_shell.exe'], + }, + 'chromium-tip-of-tree': { + '': undefined, + 'linux-x64': ['chrome-linux64', 'chrome'], + 'linux-arm64': ['chrome-linux', 'chrome'], // non-cft build + 'mac-x64': ['chrome-mac-x64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'], + 'mac-arm64': ['chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'], + 'win-x64': ['chrome-win64', 'chrome.exe'], + }, + 'chromium-tip-of-tree-headless-shell': { + '': undefined, + 'linux-x64': ['chrome-headless-shell-linux64', 'chrome-headless-shell'], + 'linux-arm64': ['chrome-linux', 'chrome'], // non-cft build + 'mac-x64': ['chrome-headless-shell-mac-x64', 'chrome-headless-shell'], + 'mac-arm64': ['chrome-headless-shell-mac-arm64', 'chrome-headless-shell'], + 'win-x64': ['chrome-headless-shell-win64', 'chrome-headless-shell.exe'], }, 'firefox': { - 'linux': ['firefox', 'firefox'], - 'mac': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], - 'win': ['firefox', 'firefox.exe'], + '': undefined, + 'linux-x64': ['firefox', 'firefox'], + 'linux-arm64': ['firefox', 'firefox'], + 'mac-x64': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], + 'mac-arm64': ['firefox', 'Nightly.app', 'Contents', 'MacOS', 'firefox'], + 'win-x64': ['firefox', 'firefox.exe'], }, 'webkit': { - 'linux': ['pw_run.sh'], - 'mac': ['pw_run.sh'], - 'win': ['Playwright.exe'], + '': undefined, + 'linux-x64': ['pw_run.sh'], + 'linux-arm64': ['pw_run.sh'], + 'mac-x64': ['pw_run.sh'], + 'mac-arm64': ['pw_run.sh'], + 'win-x64': ['Playwright.exe'], }, 'ffmpeg': { - 'linux': ['ffmpeg-linux'], - 'mac': ['ffmpeg-mac'], - 'win': ['ffmpeg-win64.exe'], + '': undefined, + 'linux-x64': ['ffmpeg-linux'], + 'linux-arm64': ['ffmpeg-linux'], + 'mac-x64': ['ffmpeg-mac'], + 'mac-arm64': ['ffmpeg-mac'], + 'win-x64': ['ffmpeg-win64.exe'], }, 'winldd': { - 'linux': undefined, - 'mac': undefined, - 'win': ['PrintDeps.exe'], + '': undefined, + 'linux-x64': undefined, + 'linux-arm64': undefined, + 'mac-x64': undefined, + 'mac-arm64': undefined, + 'win-x64': ['PrintDeps.exe'], }, }; @@ -529,13 +563,7 @@ export class Registry { constructor(browsersJSON: BrowsersJSON) { const descriptors = readDescriptors(browsersJSON); const findExecutablePath = (dir: string, name: keyof typeof EXECUTABLE_PATHS) => { - let tokens = undefined; - if (process.platform === 'linux') - tokens = EXECUTABLE_PATHS[name]['linux']; - else if (process.platform === 'darwin') - tokens = EXECUTABLE_PATHS[name]['mac']; - else if (process.platform === 'win32') - tokens = EXECUTABLE_PATHS[name]['win']; + const tokens = EXECUTABLE_PATHS[name][shortPlatform]; return tokens ? path.join(dir, ...tokens) : undefined; }; const executablePathOrDie = (name: string, e: string | undefined, installByDefault: boolean, sdkLanguage: string) => { @@ -604,7 +632,7 @@ export class Registry { }); const chromiumTipOfTreeHeadlessShell = descriptors.find(d => d.name === 'chromium-tip-of-tree-headless-shell')!; - const chromiumTipOfTreeHeadlessShellExecutable = findExecutablePath(chromiumTipOfTreeHeadlessShell.dir, 'chromium-headless-shell'); + const chromiumTipOfTreeHeadlessShellExecutable = findExecutablePath(chromiumTipOfTreeHeadlessShell.dir, 'chromium-tip-of-tree-headless-shell'); this._executables.push({ type: 'channel', name: 'chromium-tip-of-tree-headless-shell', @@ -622,7 +650,7 @@ export class Registry { }); const chromiumTipOfTree = descriptors.find(d => d.name === 'chromium-tip-of-tree')!; - const chromiumTipOfTreeExecutable = findExecutablePath(chromiumTipOfTree.dir, 'chromium'); + const chromiumTipOfTreeExecutable = findExecutablePath(chromiumTipOfTree.dir, 'chromium-tip-of-tree'); this._executables.push({ type: 'tool', name: 'chromium-tip-of-tree', diff --git a/packages/playwright-core/src/server/utils/hostPlatform.ts b/packages/playwright-core/src/server/utils/hostPlatform.ts index 43bbff7e3..ef01a7518 100644 --- a/packages/playwright-core/src/server/utils/hostPlatform.ts +++ b/packages/playwright-core/src/server/utils/hostPlatform.ts @@ -119,3 +119,17 @@ function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupporte } export const { hostPlatform, isOfficiallySupportedPlatform } = calculatePlatform(); + +export type ShortPlatform = 'mac-x64' | 'mac-arm64' | 'linux-x64' | 'linux-arm64' | 'win-x64' | ''; + +function toShortPlatform(hostPlatform: HostPlatform): ShortPlatform { + if (hostPlatform === '') + return ''; + if (hostPlatform === 'win64') + return 'win-x64'; + if (hostPlatform.startsWith('mac')) + return hostPlatform.endsWith('arm64') ? 'mac-arm64' : 'mac-x64'; + return hostPlatform.endsWith('arm64') ? 'linux-arm64' : 'linux-x64'; +} + +export const shortPlatform = toShortPlatform(hostPlatform); From fd84e37a66e48c9c2ef80f7158ee678ee7dc6a1f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:09:12 +0200 Subject: [PATCH 090/250] fix(junit): Replace spread operator with for-of loop to prevent RangeError with large console logs (#37767) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Skn0tt <14912729+Skn0tt@users.noreply.github.com> Co-authored-by: Simon Knott --- packages/playwright/src/reporters/junit.ts | 6 ++++-- tests/playwright-test/reporter-junit.spec.ts | 22 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/playwright/src/reporters/junit.ts b/packages/playwright/src/reporters/junit.ts index a5304da77..96f428861 100644 --- a/packages/playwright/src/reporters/junit.ts +++ b/packages/playwright/src/reporters/junit.ts @@ -193,8 +193,10 @@ class JUnitReporter implements ReporterV2 { const systemOut: string[] = []; const systemErr: string[] = []; for (const result of test.results) { - systemOut.push(...result.stdout.map(item => item.toString())); - systemErr.push(...result.stderr.map(item => item.toString())); + for (const item of result.stdout) + systemOut.push(item.toString()); + for (const item of result.stderr) + systemErr.push(item.toString()); for (const attachment of result.attachments) { if (!attachment.path) continue; diff --git a/tests/playwright-test/reporter-junit.spec.ts b/tests/playwright-test/reporter-junit.spec.ts index 994748490..838f35e84 100644 --- a/tests/playwright-test/reporter-junit.spec.ts +++ b/tests/playwright-test/reporter-junit.spec.ts @@ -126,6 +126,28 @@ for (const useIntermediateMergeReport of [false, true] as const) { expect(result.exitCode).toBe(1); }); + test('should handle large number of console logs', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37719' } }, async ({ runInlineTest }, testInfo) => { + test.slow(); + // need to go via disk, otherwise our test harness would print 500k lines of stdout to the Github Actions UI that can't handle it. + const reportFile = testInfo.outputPath('report.xml'); + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('one', async ({}) => { + test.slow(); + for (let i = 0; i < 500000; i++) { + console.log('log line ' + i); + } + }); + `, + }, { reporter: 'junit' }, { PLAYWRIGHT_JUNIT_OUTPUT_FILE: reportFile }); + expect(result.exitCode).toBe(0); + const report = await fs.promises.readFile(reportFile, 'utf8'); + const testcase = parseXML(report)['testsuites']['testsuite'][0]['testcase'][0]; + expect(testcase['system-out']).toHaveLength(1); + expect(testcase['system-out'][0]).toContain('log line 99999'); + }); + test('should render stdout without ansi escapes', async ({ runInlineTest }) => { const result = await runInlineTest({ 'playwright.config.ts': ` From 299158e601775bded8e05b6bcf83bdd69c392d95 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 08:31:21 -0700 Subject: [PATCH 091/250] feat(firefox): roll to r1496 (#37918) --- 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 558923e30..1c073422b 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,7 +27,7 @@ }, { "name": "firefox", - "revision": "1495", + "revision": "1496", "installByDefault": true, "browserVersion": "142.0.1" }, From a6056db5ff95eb6cfcec4be4d8b71bd4019f9449 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 08:32:19 -0700 Subject: [PATCH 092/250] feat(firefox-beta): roll to r1491 (#37917) --- 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 1c073422b..e02544657 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -33,7 +33,7 @@ }, { "name": "firefox-beta", - "revision": "1490", + "revision": "1491", "installByDefault": false, "browserVersion": "143.0b10" }, From 3c4194e700ce9f7f176d968d0d3dd0819e7ae2d5 Mon Sep 17 00:00:00 2001 From: Andrea Trogolo Date: Mon, 20 Oct 2025 17:35:44 +0200 Subject: [PATCH 093/250] chore(docs): Fix typo in codegen.md (#37925) --- docs/src/codegen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/codegen.md b/docs/src/codegen.md index 7f2358bda..8cafd449c 100644 --- a/docs/src/codegen.md +++ b/docs/src/codegen.md @@ -59,7 +59,7 @@ In the test file in VS Code you will see your new generated actions added to you ### Generating locators You can generate locators with the test generator. -- Click on the **Pick locator** button form the testing sidebar and then hover over elements in the browser window to see the [locator](./locators.md) highlighted underneath each element. +- Click on the **Pick locator** button from the testing sidebar and then hover over elements in the browser window to see the [locator](./locators.md) highlighted underneath each element. - Click the element you require and it will now show up in the **Pick locator** box in VS Code. - Press Enter on your keyboard to copy the locator into the clipboard and then paste anywhere in your code. Or press 'escape' if you want to cancel. From 9959a4d4c23865e4bccd7381aa9f4207bf8a8add Mon Sep 17 00:00:00 2001 From: Chris <57954026+cpAdm@users.noreply.github.com> Date: Mon, 20 Oct 2025 20:06:39 +0200 Subject: [PATCH 094/250] docs: Add note about swallowing of custom reporter errors (#37915) --- docs/src/test-reporter-api/class-reporter.md | 5 +++++ packages/playwright/types/testReporter.d.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/docs/src/test-reporter-api/class-reporter.md b/docs/src/test-reporter-api/class-reporter.md index 44a148ba3..cd6cf1e64 100644 --- a/docs/src/test-reporter-api/class-reporter.md +++ b/docs/src/test-reporter-api/class-reporter.md @@ -87,6 +87,11 @@ and [`method: Reporter.onError`] is called when something went wrong outside of If your custom reporter does not print anything to the terminal, implement [`method: Reporter.printsToStdio`] and return `false`. This way, Playwright will use one of the standard terminal reporters in addition to your custom reporter to enhance user experience. +**Reporter errors** + +Playwright will swallow any errors thrown in your custom reporter methods. If you need to detect or fail on reporter +errors, you must wrap and handle them yourself. + **Merged report API notes** When merging multiple [`blob`](../test-reporters#blob-reporter) reports via [`merge-reports`](../test-sharding#merge-reports-cli) CLI diff --git a/packages/playwright/types/testReporter.d.ts b/packages/playwright/types/testReporter.d.ts index 3cadb0580..df4a40ba0 100644 --- a/packages/playwright/types/testReporter.d.ts +++ b/packages/playwright/types/testReporter.d.ts @@ -127,6 +127,11 @@ export interface FullResult { * `false`. This way, Playwright will use one of the standard terminal reporters in addition to your custom reporter * to enhance user experience. * + * **Reporter errors** + * + * Playwright will swallow any errors thrown in your custom reporter methods. If you need to detect or fail on + * reporter errors, you must wrap and handle them yourself. + * * **Merged report API notes** * * When merging multiple [`blob`](https://playwright.dev/docs/test-reporters#blob-reporter) reports via From 9b2fbecf9751bd2e3b3a8bde146255d39aaeb412 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 20 Oct 2025 12:29:08 -0700 Subject: [PATCH 095/250] test: unskip cookie test for firefox (#37926) --- tests/library/browsercontext-cookies.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/library/browsercontext-cookies.spec.ts b/tests/library/browsercontext-cookies.spec.ts index f9c3a4536..b7e0fcfa8 100644 --- a/tests/library/browsercontext-cookies.spec.ts +++ b/tests/library/browsercontext-cookies.spec.ts @@ -74,9 +74,7 @@ it('should get a non-session cookie', async ({ context, page, server, defaultSam it('should allow adding cookies with >400 days expiration', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37903' } -}, async ({ context, server, browserName, channel }) => { - it.fixme(browserName === 'firefox' && !channel?.startsWith('moz-firefox'), 'Firefox fails to add cookies with >400 days expiration'); - +}, async ({ context, server }) => { // Browsers start to cap cookies with 400 days max expires value. // See https://github.com/httpwg/http-extensions/pull/1732 // Chromium patch: https://chromium.googlesource.com/chromium/src/+/aaa5d2b55478eac2ee642653dcd77a50ac3faff6 From b47ae6c118fe5271422109af69de8bb33776ce4b Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 20 Oct 2025 12:42:08 -0700 Subject: [PATCH 096/250] docs: missing box: true in step (#37928) --- docs/src/test-api/class-test.md | 2 +- packages/playwright/types/test.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 977c9d83e..990c49d87 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -1652,7 +1652,7 @@ function step(target: Function, context: ClassMethodDecoratorContext) { const name = this.constructor.name + '.' + (context.name as string); return test.step(name, async () => { return await target.call(this, ...args); - }); + }, { box: true }); }; } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 91c168cd7..f27851d42 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -6180,7 +6180,7 @@ export interface TestType { * const name = this.constructor.name + '.' + (context.name as string); * return test.step(name, async () => { * return await target.call(this, ...args); - * }); + * }, { box: true }); * }; * } * @@ -6339,7 +6339,7 @@ export interface TestType { * const name = this.constructor.name + '.' + (context.name as string); * return test.step(name, async () => { * return await target.call(this, ...args); - * }); + * }, { box: true }); * }; * } * From fbdb7571c630249569e6b563b816d06834699904 Mon Sep 17 00:00:00 2001 From: Chris <57954026+cpAdm@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:47:04 +0200 Subject: [PATCH 097/250] feat(trace): filter network on multiple types (#37910) --- .../trace-viewer/src/ui/networkFilters.tsx | 26 +++++++++--- packages/trace-viewer/src/ui/networkTab.tsx | 8 ++-- tests/library/trace-viewer.spec.ts | 33 +++++++++++++++ .../ui-mode-test-network-tab.spec.ts | 41 +++++++++++++++++++ 4 files changed, 98 insertions(+), 10 deletions(-) diff --git a/packages/trace-viewer/src/ui/networkFilters.tsx b/packages/trace-viewer/src/ui/networkFilters.tsx index a88332e7c..5ce9d57aa 100644 --- a/packages/trace-viewer/src/ui/networkFilters.tsx +++ b/packages/trace-viewer/src/ui/networkFilters.tsx @@ -16,15 +16,15 @@ import './networkFilters.css'; -const resourceTypes = ['All', 'Fetch', 'HTML', 'JS', 'CSS', 'Font', 'Image'] as const; +const resourceTypes = ['Fetch', 'HTML', 'JS', 'CSS', 'Font', 'Image'] as const; export type ResourceType = typeof resourceTypes[number]; export type FilterState = { searchValue: string; - resourceType: ResourceType; + resourceTypes: Set; }; -export const defaultFilterState: FilterState = { searchValue: '', resourceType: 'All' }; +export const defaultFilterState: FilterState = { searchValue: '', resourceTypes: new Set() }; export const NetworkFilters = ({ filterState, onFilterStateChange }: { filterState: FilterState, @@ -41,12 +41,28 @@ export const NetworkFilters = ({ filterState, onFilterStateChange }: { />
        +
        onFilterStateChange({ ...filterState, resourceTypes: new Set() })} + className={`network-filters-resource-type ${filterState.resourceTypes.size === 0 ? 'selected' : ''}`} + > + All +
        + {resourceTypes.map(resourceType => (
        onFilterStateChange({ ...filterState, resourceType })} - className={`network-filters-resource-type ${filterState.resourceType === resourceType ? 'selected' : ''}`} + onClick={event => { + let newType; + if (event.ctrlKey || event.metaKey) + newType = filterState.resourceTypes.symmetricDifference(new Set([resourceType])); + else + newType = new Set([resourceType]); + + onFilterStateChange({ ...filterState, resourceTypes: newType }); + }} + className={`network-filters-resource-type ${filterState.resourceTypes.has(resourceType) ? 'selected' : ''}`} > {resourceType}
        diff --git a/packages/trace-viewer/src/ui/networkTab.tsx b/packages/trace-viewer/src/ui/networkTab.tsx index 270368c99..f7342dfcc 100644 --- a/packages/trace-viewer/src/ui/networkTab.tsx +++ b/packages/trace-viewer/src/ui/networkTab.tsx @@ -370,7 +370,6 @@ function comparator(sortBy: ColumnName) { } const resourceTypePredicates: Record boolean> = { - 'All': () => true, 'Fetch': contentType => contentType === 'application/json', 'HTML': contentType => contentType === 'text/html', 'CSS': contentType => contentType === 'text/css', @@ -379,10 +378,9 @@ const resourceTypePredicates: Record bool 'Image': contentType => contentType.includes('image'), }; -function filterEntry({ searchValue, resourceType }: FilterState) { +function filterEntry({ searchValue, resourceTypes }: FilterState) { return (entry: RenderedEntry) => { - const typePredicate = resourceTypePredicates[resourceType]; - - return typePredicate(entry.contentType) && entry.name.url.toLowerCase().includes(searchValue.toLowerCase()); + const isRightType = resourceTypes.size === 0 || Array.from(resourceTypes).some(type => resourceTypePredicates[type](entry.contentType)); + return isRightType && entry.name.url.toLowerCase().includes(searchValue.toLowerCase()); }; } diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 477590138..f5525a580 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -406,6 +406,39 @@ test('should filter network requests by resource type', async ({ page, runAndTra await expect(traceViewer.networkRequests.getByText('font.woff2')).toBeVisible(); }); +test('should filter network requests by multiple resource types', async ({ page, runAndTrace, server }) => { + const traceViewer = await runAndTrace(async () => { + server.setRoute('/api/endpoint', (_, res) => res.setHeader('Content-Type', 'application/json').end()); + await page.goto(`${server.PREFIX}/network-tab/network.html`); + await page.evaluate(() => (window as any).donePromise); + }); + await traceViewer.selectAction('Navigate'); + await traceViewer.showNetworkTab(); + + const { networkRequests } = traceViewer; + + await traceViewer.page.getByText('JS', { exact: true }).click(); + await expect(networkRequests).toHaveCount(1); + await expect(networkRequests.getByText('script.js')).toBeVisible(); + + await traceViewer.page.getByText('CSS', { exact: true }).click({ modifiers: ['ControlOrMeta'] }); + await expect(networkRequests.getByText('script.js')).toBeVisible(); + await expect(networkRequests.getByText('style.css')).toBeVisible(); + await expect(networkRequests).toHaveCount(2); + + await traceViewer.page.getByText('Image', { exact: true }).click({ modifiers: ['ControlOrMeta'] }); + await expect(networkRequests.getByText('image.png')).toBeVisible(); + await expect(networkRequests).toHaveCount(3); + + await traceViewer.page.getByText('CSS', { exact: true }).click({ modifiers: ['ControlOrMeta'] }); + await expect(networkRequests).toHaveCount(2); + await expect(networkRequests.getByText('script.js')).toBeVisible(); + await expect(networkRequests.getByText('image.png')).toBeVisible(); + + await traceViewer.page.getByText('All', { exact: true }).click(); + await expect(networkRequests).toHaveCount(9); +}); + test('should show font preview', async ({ page, runAndTrace, server }) => { const traceViewer = await runAndTrace(async () => { await page.goto(`${server.PREFIX}/network-tab/network.html`); diff --git a/tests/playwright-test/ui-mode-test-network-tab.spec.ts b/tests/playwright-test/ui-mode-test-network-tab.spec.ts index 14004105e..8ca37db17 100644 --- a/tests/playwright-test/ui-mode-test-network-tab.spec.ts +++ b/tests/playwright-test/ui-mode-test-network-tab.spec.ts @@ -59,6 +59,47 @@ test('should filter network requests by resource type', async ({ runUITest, serv await expect(networkItems.getByText('font.woff2')).toBeVisible(); }); +test('should filter network requests by multiple resource types', async ({ runUITest, server }) => { + server.setRoute('/api/endpoint', (_, res) => res.setHeader('Content-Type', 'application/json').end()); + + const { page } = await runUITest({ + 'network-tab.test.ts': ` + import { test, expect } from '@playwright/test'; + test('network tab test', async ({ page }) => { + await page.goto('${server.PREFIX}/network-tab/network.html'); + await page.evaluate(() => (window as any).donePromise); + }); + `, + }); + + await page.getByText('network tab test').dblclick(); + await page.getByText('Network', { exact: true }).click(); + + const networkItems = page.getByRole('list', { name: 'Network requests' }).getByRole('listitem'); + await expect(networkItems).toHaveCount(9); + + await page.getByText('JS', { exact: true }).click(); + await expect(networkItems).toHaveCount(1); + await expect(networkItems.getByText('script.js')).toBeVisible(); + + await page.getByText('CSS', { exact: true }).click({ modifiers: ['ControlOrMeta'] }); + await expect(networkItems.getByText('script.js')).toBeVisible(); + await expect(networkItems.getByText('style.css')).toBeVisible(); + await expect(networkItems).toHaveCount(2); + + await page.getByText('Image', { exact: true }).click({ modifiers: ['ControlOrMeta'] }); + await expect(networkItems.getByText('image.png')).toBeVisible(); + await expect(networkItems).toHaveCount(3); + + await page.getByText('CSS', { exact: true }).click({ modifiers: ['ControlOrMeta'] }); + await expect(networkItems).toHaveCount(2); + await expect(networkItems.getByText('script.js')).toBeVisible(); + await expect(networkItems.getByText('image.png')).toBeVisible(); + + await page.getByText('All', { exact: true }).click(); + await expect(networkItems).toHaveCount(9); +}); + test('should filter network requests by url', async ({ runUITest, server }) => { const { page } = await runUITest({ 'network-tab.test.ts': ` From 246c51c4134567edda9a1da9f0bf9445151bcbaa Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 20 Oct 2025 12:50:17 -0700 Subject: [PATCH 098/250] fix(trace-viewer): make Call tab Time rendering consistent, with copy buttons (#37902) --- packages/trace-viewer/src/ui/callTab.css | 1 + packages/trace-viewer/src/ui/callTab.tsx | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/trace-viewer/src/ui/callTab.css b/packages/trace-viewer/src/ui/callTab.css index f57f3f152..e3907f4b3 100644 --- a/packages/trace-viewer/src/ui/callTab.css +++ b/packages/trace-viewer/src/ui/callTab.css @@ -89,6 +89,7 @@ a.call-value:hover { .call-value.datetime, .call-value.string, +.call-value.literal, .call-value.locator { color: var(--vscode-charts-orange); } diff --git a/packages/trace-viewer/src/ui/callTab.tsx b/packages/trace-viewer/src/ui/callTab.tsx index 94f1938eb..c3a6d34e5 100644 --- a/packages/trace-viewer/src/ui/callTab.tsx +++ b/packages/trace-viewer/src/ui/callTab.tsx @@ -44,8 +44,8 @@ export const CallTab: React.FunctionComponent<{
        {action.title}
        Time
        - - + {renderProperty({ name: 'start', type: 'literal', text: startTime })} + {renderProperty({ name: 'duration', type: 'literal', text: renderDuration(action) })} { !!paramKeys.length && <>
        Parameters
        @@ -64,11 +64,9 @@ export const CallTab: React.FunctionComponent<{ ); }; -const DateTimeCallLine: React.FC<{ name: string, value: string }> = ({ name, value }) =>
        {name}{value}
        ; - type Property = { name: string; - type: 'string' | 'number' | 'object' | 'locator' | 'handle' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'function'; + type: 'literal' | 'string' | 'number' | 'object' | 'locator' | 'handle' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'function'; text: string; }; @@ -88,7 +86,7 @@ function renderProperty(property: Property) { return (
        {property.name}:{text} - { ['string', 'number', 'object', 'locator'].includes(property.type) && + { ['literal', 'string', 'number', 'object', 'locator'].includes(property.type) && }
        From 791ae34734a2d607a57a9efb981b99726905ebae Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 20 Oct 2025 12:50:38 -0700 Subject: [PATCH 099/250] fix(trace-viewer): restore displaying selected call title in call tab (#37898) --- packages/trace-viewer/src/ui/callTab.tsx | 5 ++++- tests/library/trace-viewer.spec.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/trace-viewer/src/ui/callTab.tsx b/packages/trace-viewer/src/ui/callTab.tsx index c3a6d34e5..5accfc860 100644 --- a/packages/trace-viewer/src/ui/callTab.tsx +++ b/packages/trace-viewer/src/ui/callTab.tsx @@ -24,6 +24,7 @@ import { asLocator } from '@isomorphic/locatorGenerators'; import type { Language } from '@isomorphic/locatorGenerators'; import { PlaceholderPanel } from './placeholderPanel'; import type { ActionTraceEventInContext } from './modelUtil'; +import { renderTitleForCall } from './actionList'; export const CallTab: React.FunctionComponent<{ action: ActionTraceEventInContext | undefined, @@ -40,9 +41,11 @@ export const CallTab: React.FunctionComponent<{ const startTimeMillis = action.startTime - startTimeOffset; const startTime = msToString(startTimeMillis); + const { title } = renderTitleForCall(action); + return (
        -
        {action.title}
        +
        {title}
        Time
        {renderProperty({ name: 'start', type: 'literal', text: startTime })} {renderProperty({ name: 'duration', type: 'literal', text: renderDuration(action) })} diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index f5525a580..22ce9d535 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -303,7 +303,7 @@ test('should show params and return value', async ({ showTraceViewer }) => { const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('Evaluate'); await expect(traceViewer.callLines).toHaveText([ - '', + /Evaluate/, /start:[\d\.]+m?s/, /duration:[\d]+ms/, /expression:"\({↵ a↵ }\) => {↵ console\.log\(\'Info\'\);↵ console\.warn\(\'Warning\'\);↵ console/, @@ -329,7 +329,7 @@ test('should show null as a param', async ({ showTraceViewer, browserName }) => const traceViewer = await showTraceViewer(traceFile); await traceViewer.selectAction('Evaluate', 1); await expect(traceViewer.callLines).toHaveText([ - '', + /Evaluate/, /start:[\d\.]+m?s/, /duration:[\d]+ms/, 'expression:"() => 1 + 1"', From d2ffd0d3ebd522c3faa2ce8f43c0a6971bf7ebd9 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 20 Oct 2025 12:54:19 -0700 Subject: [PATCH 100/250] fix(ui): collapse multiline log entries (#37854) --- .../src/utils/isomorphic/protocolFormatter.ts | 4 ++++ tests/library/inspector/pause.spec.ts | 14 ++++++++++++ tests/playwright-test/ui-mode-trace.spec.ts | 22 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/packages/playwright-core/src/utils/isomorphic/protocolFormatter.ts b/packages/playwright-core/src/utils/isomorphic/protocolFormatter.ts index 9f8547d70..89447dff9 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolFormatter.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolFormatter.ts @@ -17,6 +17,10 @@ import { methodMetainfo } from './protocolMetainfo'; export function formatProtocolParam(params: Record | undefined, alternatives: string): string | undefined { + return _formatProtocolParam(params, alternatives)?.replaceAll('\n', '\\n'); +} + +function _formatProtocolParam(params: Record | undefined, alternatives: string): string | undefined { if (!params) return undefined; diff --git a/tests/library/inspector/pause.spec.ts b/tests/library/inspector/pause.spec.ts index 1d202e5b8..e34fb7005 100644 --- a/tests/library/inspector/pause.spec.ts +++ b/tests/library/inspector/pause.spec.ts @@ -48,6 +48,20 @@ it('should not reset timeouts', async ({ page, recorderPageGetter, closeRecorder expect(error.message).toContain('page.goto: Timeout 1000ms exceeded.'); }); +it('should collapse log entries to a single line', async ({ page, recorderPageGetter }) => { + const scriptPromise = (async () => { + // @ts-ignore + await page.pause({ __testHookKeepTestTimeout: true }); + await page.keyboard.type(`Hello +world`); + })(); + + const recorderPage = await recorderPageGetter(); + await recorderPage.click('[title="Resume (F8)"]'); + await expect(recorderPage.locator('.call-log-call').nth(1)).toContainText('Type "Hello\\nworld"'); + await scriptPromise; +}); + it.describe('pause', () => { it.skip(({ mode }) => mode !== 'default'); diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index 08fbb46a1..53c204fbf 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -480,6 +480,28 @@ test('should show custom fixture titles in actions tree', async ({ runUITest }) ]); }); +test('should collapse log entries to a single line', async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + + test('multiline test', async ({ page }) => { + await page.keyboard.type(\`line1 +line2\`); + }); + `, + }); + + await page.getByText('multiline test').dblclick(); + const listItem = page.getByTestId('actions-tree').getByRole('treeitem'); + await expect(listItem, 'action list').toHaveText([ + /Before Hooks[\d.]+m?s/, + /Type "line1\\nline2"[\d.]+m?s/, + /After Hooks[\d.]+m?s/, + ]); +}); + + test('should hide boxed fixtures and contents, reveal upon show all actions setting', async ({ runUITest }) => { const { page } = await runUITest({ 'a.test.ts': ` From 9a74559d074adf07624b05c1794675a003f47875 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:58:50 -0700 Subject: [PATCH 101/250] feat(webkit): roll to r2222 (#37914) --- packages/playwright-core/browsers.json | 2 +- tests/library/modernizr.spec.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index e02544657..c6e75cbed 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -39,7 +39,7 @@ }, { "name": "webkit", - "revision": "2221", + "revision": "2222", "installByDefault": true, "revisionOverrides": { "debian11-x64": "2105", diff --git a/tests/library/modernizr.spec.ts b/tests/library/modernizr.spec.ts index e8f6847ee..a7f868dca 100644 --- a/tests/library/modernizr.spec.ts +++ b/tests/library/modernizr.spec.ts @@ -111,6 +111,7 @@ it('Mobile Safari', async ({ playwright, browser, browserName, platform, httpsSe expected.overflowscrolling = false; expected.mediasource = true; expected.scrolltooptions = false; + expected.hairline = false; delete expected.webglextensions; delete actual.webglextensions; From b4eef242648ebdbdcb5515549ab323813a4268b3 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 20 Oct 2025 13:11:32 -0700 Subject: [PATCH 102/250] fix(trace-viewer): properly size copy button to prevent bounce (#37900) --- packages/trace-viewer/src/ui/callTab.css | 6 ++++++ packages/trace-viewer/src/ui/copyToClipboard.tsx | 1 + 2 files changed, 7 insertions(+) diff --git a/packages/trace-viewer/src/ui/callTab.css b/packages/trace-viewer/src/ui/callTab.css index e3907f4b3..cb3ba613a 100644 --- a/packages/trace-viewer/src/ui/callTab.css +++ b/packages/trace-viewer/src/ui/callTab.css @@ -69,6 +69,12 @@ margin-bottom: -2px; } +.call-line .toolbar-button.check { + margin-left: 5px; + margin-top: -2px; + margin-bottom: -2px; +} + .call-value { margin-left: 2px; text-overflow: ellipsis; diff --git a/packages/trace-viewer/src/ui/copyToClipboard.tsx b/packages/trace-viewer/src/ui/copyToClipboard.tsx index 8f3f8cb44..6b78c2e24 100644 --- a/packages/trace-viewer/src/ui/copyToClipboard.tsx +++ b/packages/trace-viewer/src/ui/copyToClipboard.tsx @@ -18,6 +18,7 @@ import * as React from 'react'; import { ToolbarButton } from '@web/components/toolbarButton'; import './copyToClipboard.css'; +// TODO: This should be deduplicated with packages/html-reporter/src/copyToClipboard.tsx export const CopyToClipboard: React.FunctionComponent<{ value: string | (() => Promise), description?: string, From 9aa55397693acfd8c66ea85e59bdeb4067f4637e Mon Sep 17 00:00:00 2001 From: Chris <57954026+cpAdm@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:08:40 +0200 Subject: [PATCH 103/250] feat(trace): Pin copy address button to the right in the address bar (#37802) --- packages/trace-viewer/src/ui/browserFrame.css | 14 +++++--------- packages/trace-viewer/src/ui/browserFrame.tsx | 2 +- tests/library/trace-viewer.spec.ts | 13 +++++-------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/trace-viewer/src/ui/browserFrame.css b/packages/trace-viewer/src/ui/browserFrame.css index f72af1f60..ed157fcb5 100644 --- a/packages/trace-viewer/src/ui/browserFrame.css +++ b/packages/trace-viewer/src/ui/browserFrame.css @@ -29,20 +29,16 @@ font: 400 16px Arial,sans-serif; margin: 0 16px 0 8px; padding: 5px 15px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; display: flex; align-items: center; height: 28px; } -.browser-frame-address-bar > button { - visibility: hidden; -} - -.browser-frame-address-bar:hover > button { - visibility: visible; +.browser-frame-address { + flex: auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .browser-frame-menu-bar { diff --git a/packages/trace-viewer/src/ui/browserFrame.tsx b/packages/trace-viewer/src/ui/browserFrame.tsx index f689b52cf..4e17abe9a 100644 --- a/packages/trace-viewer/src/ui/browserFrame.tsx +++ b/packages/trace-viewer/src/ui/browserFrame.tsx @@ -31,7 +31,7 @@ export const BrowserFrame: React.FunctionComponent<{ className='browser-frame-address-bar' title={url || 'about:blank'} > - {url || 'about:blank'} + {url || 'about:blank'} {url && ( )} diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 22ce9d535..59ce8823b 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -506,20 +506,17 @@ test('should have network request overrides 2', async ({ page, server, runAndTra await expect.soft(traceViewer.networkRequests).toContainText([/script.jsGET200application\/javascript.*continued/]); }); -test('should show snapshot URL', async ({ page, runAndTrace, server }) => { +test('should show snapshot URL and copy button', async ({ page, runAndTrace, server }) => { const traceViewer = await runAndTrace(async () => { await page.goto(server.EMPTY_PAGE); await page.evaluate('2+2'); }); await traceViewer.snapshotFrame('Evaluate'); - const browserFrameAddressBarLocator = traceViewer.page.locator('.browser-frame-address-bar'); - await expect(browserFrameAddressBarLocator).toHaveText(server.EMPTY_PAGE); - const copySelectorLocator = browserFrameAddressBarLocator.getByRole('button', { name: 'Copy' }); - await expect(copySelectorLocator).toBeHidden(); - await browserFrameAddressBarLocator.hover(); - await expect(copySelectorLocator).toBeVisible(); + const addressBar = traceViewer.page.locator('.browser-frame-address-bar'); + await expect(addressBar).toHaveText(server.EMPTY_PAGE); + await traceViewer.page.context().grantPermissions(['clipboard-read', 'clipboard-write']); - await copySelectorLocator.click(); + await addressBar.getByRole('button', { name: 'Copy' }).click(); expect(await traceViewer.page.evaluate(() => navigator.clipboard.readText())).toBe(server.EMPTY_PAGE); }); From 63472eecff11aa47bfa33981fbb4b2fdd636a160 Mon Sep 17 00:00:00 2001 From: Chris <57954026+cpAdm@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:08:59 +0200 Subject: [PATCH 104/250] feat(html-report): Keep query when clicking project filter and right click (#37916) --- packages/html-reporter/src/filter.ts | 3 ++- packages/html-reporter/src/headerView.tsx | 5 ++--- packages/html-reporter/src/labels.tsx | 3 +-- packages/html-reporter/src/links.tsx | 8 ++++---- packages/html-reporter/src/reportView.tsx | 6 +++--- packages/html-reporter/src/testCaseView.spec.tsx | 8 ++++---- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index 7e54c98d4..c08350feb 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -213,7 +213,8 @@ function cacheSearchValues(test: TestCaseSummary & { [searchValuesSymbol]?: Sear // Extract quoted groups of search params, or tokens separated by whitespace const SEARCH_PARAM_GROUP_REGEX = /("[^"]*"|"[^"]*$|\S+)/g; -export function filterWithQuery(existingQuery: string, token: string, append: boolean): string { +export function filterWithQuery(searchParams: URLSearchParams, token: string, append: boolean): string { + const existingQuery = searchParams.get('q') ?? ''; const tokens = [...existingQuery.matchAll(SEARCH_PARAM_GROUP_REGEX)].map(m => { const rawValue = m[0]; return rawValue.startsWith('"') && rawValue.endsWith('"') && rawValue.length > 1 ? rawValue.slice(1, rawValue.length - 1) : rawValue; diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx index 471818876..9e39df53b 100644 --- a/packages/html-reporter/src/headerView.tsx +++ b/packages/html-reporter/src/headerView.tsx @@ -102,11 +102,10 @@ const NavLink: React.FC<{ count: number, }> = ({ token, count }) => { const searchParams = React.useContext(SearchParamsContext); - const q = searchParams.get('q')?.toString() || ''; const queryToken = `s:${token}`; - const clickUrl = filterWithQuery(q, queryToken, false); - const ctrlClickUrl = filterWithQuery(q, queryToken, true); + const clickUrl = filterWithQuery(searchParams, queryToken, false); + const ctrlClickUrl = filterWithQuery(searchParams, queryToken, true); const label = token.charAt(0).toUpperCase() + token.slice(1); diff --git a/packages/html-reporter/src/labels.tsx b/packages/html-reporter/src/labels.tsx index 253518eaa..bbac8d7cb 100644 --- a/packages/html-reporter/src/labels.tsx +++ b/packages/html-reporter/src/labels.tsx @@ -60,8 +60,7 @@ const LabelsClickView: React.FC<{ const onClickHandle = React.useCallback((e: React.MouseEvent, label: string) => { e.preventDefault(); - const q = searchParams.get('q')?.toString() || ''; - navigate(filterWithQuery(q, label, e.metaKey || e.ctrlKey)); + navigate(filterWithQuery(searchParams, label, e.metaKey || e.ctrlKey)); }, [searchParams]); return <> diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index 6e1451ade..e9cbaa1b7 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -24,6 +24,7 @@ import { clsx, useFlash } from '@web/uiUtils'; import { trace } from './icons'; import { Expandable } from './expandable'; import { Label } from './labels'; +import { filterWithQuery } from './filter'; export function navigate(href: string | URL) { window.history.pushState({}, '', href); @@ -61,10 +62,9 @@ export const LinkBadge: React.FunctionComponent = export const ProjectLink: React.FunctionComponent<{ projectNames: string[], projectName: string, -}> = ({ projectNames, projectName }) => { - const encoded = encodeURIComponent(projectName); - const value = projectName === encoded ? projectName : `"${encoded.replace(/%22/g, '%5C%22')}"`; - return +}> = ({ projectNames, projectName }) => { + const searchParams = React.useContext(SearchParamsContext); + return
        } From 1377d26c9013e03a5068f54c67f86a178d635f0d Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 21 Oct 2025 18:13:35 +0200 Subject: [PATCH 110/250] chore: drop HMR leftovers (#37950) --- packages/html-reporter/src/index.tsx | 25 +------------ packages/trace-viewer/.gitignore | 2 -- packages/trace-viewer/vite.config.ts | 2 +- packages/trace-viewer/vite.sw.config.ts | 4 +-- utils/build/build.js | 47 +------------------------ 5 files changed, 5 insertions(+), 75 deletions(-) diff --git a/packages/html-reporter/src/index.tsx b/packages/html-reporter/src/index.tsx index df1f9b2ab..f7263ea86 100644 --- a/packages/html-reporter/src/index.tsx +++ b/packages/html-reporter/src/index.tsx @@ -55,35 +55,12 @@ window.onload = () => { ReactDOM.createRoot(document.querySelector('#root')!).render(); }; -const kPlaywrightReportStorageForHMR = 'playwrightReportStorageForHMR'; - class ZipReport implements LoadedReport { private _entries = new Map(); private _json!: HTMLReport; async load() { - const zipURI = await new Promise(resolve => { - const element = document.getElementById('playwrightReportBase64'); - if (!!element?.textContent) - return resolve(element.textContent); - if (window.opener) { - const listener = (event: MessageEvent) => { - if (event.source === window.opener) { - localStorage.setItem(kPlaywrightReportStorageForHMR, event.data); - resolve(event.data); - window.removeEventListener('message', listener); - } - }; - window.addEventListener('message', listener); - window.opener.postMessage('ready', '*'); - } else { - const oldReport = localStorage.getItem(kPlaywrightReportStorageForHMR); - if (oldReport) - return resolve(oldReport); - alert('couldnt find report, something with HMR is broken'); - } - }); - + const zipURI = document.getElementById('playwrightReportBase64')!.textContent; const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader(zipURI), { useWebWorkers: false }); for (const entry of await zipReader.getEntries()) this._entries.set(entry.filename, entry); diff --git a/packages/trace-viewer/.gitignore b/packages/trace-viewer/.gitignore index 9c0fa8b11..a547bf36d 100644 --- a/packages/trace-viewer/.gitignore +++ b/packages/trace-viewer/.gitignore @@ -22,5 +22,3 @@ dist-ssr *.njsproj *.sln *.sw? - -public/sw.bundle.js diff --git a/packages/trace-viewer/vite.config.ts b/packages/trace-viewer/vite.config.ts index 071cfee62..ed150fb6b 100644 --- a/packages/trace-viewer/vite.config.ts +++ b/packages/trace-viewer/vite.config.ts @@ -43,7 +43,7 @@ export default defineConfig({ }, build: { outDir: path.resolve(__dirname, '../playwright-core/lib/vite/traceViewer'), - emptyOutDir: true, + emptyOutDir: false, rollupOptions: { input: { index: path.resolve(__dirname, 'index.html'), diff --git a/packages/trace-viewer/vite.sw.config.ts b/packages/trace-viewer/vite.sw.config.ts index 1db959364..6f55ec04c 100644 --- a/packages/trace-viewer/vite.sw.config.ts +++ b/packages/trace-viewer/vite.sw.config.ts @@ -39,8 +39,8 @@ export default defineConfig({ }, publicDir: false, build: { - // outputs into the public dir, where the build of vite.config.ts will pick it up - outDir: path.resolve(__dirname, 'public'), + outDir: path.resolve(__dirname, '../playwright-core/lib/vite/traceViewer'), + // Output dir is shared with vite.config.ts, clearing it here is racy. emptyOutDir: false, rollupOptions: { input: { diff --git a/utils/build/build.js b/utils/build/build.js index bdd7b1545..3ec5981c9 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -532,29 +532,9 @@ steps.push(new ProgramStep({ ], shell: true, cwd: path.join(__dirname, '..', '..', 'packages', 'trace-viewer'), - concurrent: watchMode, // feeds into trace-viewer's `public` directory, so it needs to be finished before trace-viewer build starts + concurrent: true, })); -if (watchMode) { - // the build above outputs into `packages/trace-viewer/public`, where the `vite build` for `packages/trace-viewer` is supposed to pick it up. - // there's a bug in `vite build --watch` though where the public dir is only copied over initially, but its not watched. - // to work around this, we run a second watch build of the service worker into the final output. - // bug: https://github.com/vitejs/vite/issues/18655 - steps.push(new ProgramStep({ - command: 'npx', - args: [ - 'vite', '--config', 'vite.sw.config.ts', - 'build', '--watch', '--minify=false', - '--outDir', path.join(__dirname, '..', '..', 'packages', 'playwright-core', 'lib', 'vite', 'traceViewer'), - '--emptyOutDir=false', - '--clearScreen=false', - ], - shell: true, - cwd: path.join(__dirname, '..', '..', 'packages', 'trace-viewer'), - concurrent: true - })); -} - // Build/watch web packages. for (const webPackage of ['html-reporter', 'recorder', 'trace-viewer']) { steps.push(new ProgramStep({ @@ -572,31 +552,6 @@ for (const webPackage of ['html-reporter', 'recorder', 'trace-viewer']) { })); } -// web packages dev server -if (watchMode) { - steps.push(new ProgramStep({ - command: 'npx', - args: ['vite', '--port', '44223', '--base', '/trace/', '--clearScreen=false'], - shell: true, - cwd: path.join(__dirname, '..', '..', 'packages', 'trace-viewer'), - concurrent: true, - })); - steps.push(new ProgramStep({ - command: 'npx', - args: ['vite', '--port', '44224', '--clearScreen=false'], - shell: true, - cwd: path.join(__dirname, '..', '..', 'packages', 'html-reporter'), - concurrent: true, - })); - steps.push(new ProgramStep({ - command: 'npx', - args: ['vite', '--port', '44225', '--clearScreen=false'], - shell: true, - cwd: path.join(__dirname, '..', '..', 'packages', 'recorder'), - concurrent: true, - })); -} - // Generate injected. onChanges.push({ inputs: [ From f1eeaf54387c0d5775bdbf6cbe403df867e6685d Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 21 Oct 2025 09:49:25 -0700 Subject: [PATCH 111/250] tests: update some webkit test expectations (#37953) --- tests/library/browsercontext-cookies.spec.ts | 4 ++-- tests/library/modernizr.spec.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/library/browsercontext-cookies.spec.ts b/tests/library/browsercontext-cookies.spec.ts index b7e0fcfa8..f2ec8ea7d 100644 --- a/tests/library/browsercontext-cookies.spec.ts +++ b/tests/library/browsercontext-cookies.spec.ts @@ -74,7 +74,7 @@ it('should get a non-session cookie', async ({ context, page, server, defaultSam it('should allow adding cookies with >400 days expiration', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37903' } -}, async ({ context, server }) => { +}, async ({ context, server, browserName, isWindows, channel }) => { // Browsers start to cap cookies with 400 days max expires value. // See https://github.com/httpwg/http-extensions/pull/1732 // Chromium patch: https://chromium.googlesource.com/chromium/src/+/aaa5d2b55478eac2ee642653dcd77a50ac3faff6 @@ -88,7 +88,7 @@ it('should allow adding cookies with >400 days expiration', { expires: expire, httpOnly: false, secure: false, - sameSite: 'Lax', + sameSite: (browserName === 'webkit' && isWindows && channel !== 'webkit-wsl') ? 'None' : 'Lax', } ]); diff --git a/tests/library/modernizr.spec.ts b/tests/library/modernizr.spec.ts index a7f868dca..8b86e9c42 100644 --- a/tests/library/modernizr.spec.ts +++ b/tests/library/modernizr.spec.ts @@ -95,6 +95,7 @@ it('Mobile Safari', async ({ playwright, browser, browserName, platform, httpsSe it.skip(browserName !== 'webkit'); it.skip(browserName === 'webkit' && platform === 'darwin' && os.arch() === 'x64', 'Modernizr uses WebGL which is not available on Intel macOS - https://bugs.webkit.org/show_bug.cgi?id=278277'); it.skip(browserName === 'webkit' && hostPlatform.startsWith('ubuntu20.04'), 'Ubuntu 20.04 is frozen'); + it.skip(browserName === 'webkit' && hostPlatform.startsWith('debian11'), 'Debian 11 is frozen'); const iPhone = playwright.devices['iPhone 12']; const context = await browser.newContext({ ...iPhone, From 10fc9340ce400312d0d3d84bb8aea0e7a57702d6 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 11:08:15 -0700 Subject: [PATCH 112/250] chore(driver): roll driver to recent Node.js LTS version (#37942) --- 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 aadf190ed..204cb79cb 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.20.0" # autogenerated via ./update-playwright-driver-version.mjs +NODE_VERSION="22.21.0" # autogenerated via ./update-playwright-driver-version.mjs cd "$(dirname "$0")" PACKAGE_VERSION=$(node -p "require('../../package.json').version") From 21e7681a582b3b00f64fc63ee9889ff63ee65730 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 21 Oct 2025 13:26:28 -0700 Subject: [PATCH 113/250] test: unflake recently added toHaveText test (#37954) --- tests/page/expect-to-have-text.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/page/expect-to-have-text.spec.ts b/tests/page/expect-to-have-text.spec.ts index 818dc77ff..081e9a2ec 100644 --- a/tests/page/expect-to-have-text.spec.ts +++ b/tests/page/expect-to-have-text.spec.ts @@ -169,6 +169,7 @@ test.describe('toHaveText with text', () => { }); test('do not show "element(s) not found" when the real failure is a string mismatch', async ({ page }) => { + test.skip(process.env.PW_CLOCK === 'frozen', 'The element is attached after 1 second.'); await page.setContent(`
        Initial
        - - diff --git a/packages/html-reporter/playwright/index.ts b/packages/html-reporter/playwright/index.ts deleted file mode 100644 index 5137631e6..000000000 --- a/packages/html-reporter/playwright/index.ts +++ /dev/null @@ -1,17 +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 '../src/theme.css'; diff --git a/packages/html-reporter/src/chip.spec.tsx b/packages/html-reporter/src/chip.spec.tsx deleted file mode 100644 index 8c88fee07..000000000 --- a/packages/html-reporter/src/chip.spec.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 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 { expect, test } from '@playwright/experimental-ct-react'; -import { AutoChip, Chip as LocalChip } from './chip'; - -test.use({ viewport: { width: 500, height: 500 } }); - -test('expand collapse', async ({ mount }) => { - const component = await mount( - Chip body - ); - await expect(component.getByText('Chip body')).toBeVisible(); - await component.getByText('Title').click(); - await expect(component.getByText('Chip body')).not.toBeVisible(); - await component.getByText('Title').click(); - await expect(component.getByText('Chip body')).toBeVisible(); -}); - -test('render long title', async ({ mount }) => { - const title = 'Extremely long title. '.repeat(10); - const component = await mount( - Chip body - ); - await expect(component).toContainText('Extremely long title.'); - await expect(component.getByText('Extremely long title.')).toHaveAttribute('title', title); -}); - -test('setExpanded is called', async ({ mount }) => { - const expandedValues: boolean[] = []; - const component = await mount( expandedValues.push(expanded)}> - ); - - await component.getByText('Title').click(); - expect(expandedValues).toEqual([true]); -}); - -test('setExpanded should work', async ({ mount }) => { - const component = await mount( - Body - ); - await component.getByText('Title').click(); - await expect(component).toMatchAriaSnapshot(` - - button "Title" [expanded] - - region: Body - `); -}); diff --git a/packages/html-reporter/src/headerView.spec.tsx b/packages/html-reporter/src/headerView.spec.tsx deleted file mode 100644 index 85e78758b..000000000 --- a/packages/html-reporter/src/headerView.spec.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 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/experimental-ct-react'; -import { GlobalFilterView } from './headerView'; -import { SearchParamsProvider } from './links'; - -test.use({ viewport: { width: 720, height: 200 } }); - -test('should render counters', async ({ mount }) => { - const component = await mount( - { }} /> - ); - await expect(component.locator('a', { hasText: 'All' }).locator('.counter')).toHaveText('90'); - await expect(component.locator('a', { hasText: 'Passed' }).locator('.counter')).toHaveText('42'); - await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31'); - await expect(component.locator('a', { hasText: 'Flaky' }).locator('.counter')).toHaveText('17'); - await expect(component.locator('a', { hasText: 'Skipped' }).locator('.counter')).toHaveText('10'); - await expect(component).toMatchAriaSnapshot(` - - navigation: - - link "All90" - - link "Passed42" - - link "Failed31" - - link "Flaky17" - - link "Skipped10" - `); -}); - -test('should toggle filters', async ({ page, mount }) => { - const filters: string[] = []; - const component = await mount( - filters.push(filterText)} - /> - ); - await component.locator('a', { hasText: 'All' }).click(); - await component.locator('a', { hasText: 'Passed' }).click(); - await expect(page).toHaveURL(/#\?q=s:passed/); - await component.locator('a', { hasText: 'Failed' }).click(); - await expect(page).toHaveURL(/#\?q=s:failed/); - await component.locator('a', { hasText: 'Flaky' }).click(); - await expect(page).toHaveURL(/#\?q=s:flaky/); - await component.locator('a', { hasText: 'Skipped' }).click(); - await expect(page).toHaveURL(/#\?q=s:skipped/); - await component.getByRole('textbox').fill('annot:annotation type=annotation description'); - expect(filters).toEqual(['', '', 's:passed ', 's:failed ', 's:flaky ', 's:skipped ', 'annot:annotation type=annotation description']); -}); diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx deleted file mode 100644 index fd26ca21a..000000000 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ /dev/null @@ -1,257 +0,0 @@ -/** - * Copyright (c) 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/experimental-ct-react'; -import { TestCaseView } from './testCaseView'; -import type { TestCase, TestCaseSummary, TestResult } from './types'; - -test.use({ viewport: { width: 800, height: 600 } }); - -const result: TestResult = { - retry: 0, - startTime: new Date(0).toUTCString(), - duration: 100, - errors: [], - steps: [{ - title: 'Outer step', - startTime: new Date(100).toUTCString(), - duration: 10, - location: { file: 'test.spec.ts', line: 62, column: 0 }, - count: 1, - steps: [{ - title: 'Inner step', - startTime: new Date(200).toUTCString(), - duration: 10, - location: { file: 'test.spec.ts', line: 82, column: 0 }, - steps: [], - attachments: [], - count: 1, - }], - attachments: [], - }], - annotations: [ - { type: 'annotation', description: 'Annotation text' }, - { type: 'annotation', description: 'Another annotation text' }, - { type: '_annotation', description: 'Hidden annotation' }, - ], - attachments: [], - status: 'passed', -}; - -const testCase: TestCase = { - testId: 'testid', - title: 'My test', - path: [], - projectName: 'chromium', - location: { file: 'test.spec.ts', line: 42, column: 0 }, - annotations: result.annotations, - tags: [], - outcome: 'expected', - duration: 200, - ok: true, - results: [result] -}; - -test('should render test case', async ({ mount }) => { - const component = await mount(); - await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible(); - await expect(component.getByText('Hidden annotation')).toBeHidden(); - await component.getByText('Annotations').click(); - await expect(component.getByText('Annotation text')).not.toBeVisible(); - await expect(component.getByText('Outer step')).toBeVisible(); - await expect(component.getByText('Inner step')).not.toBeVisible(); - await component.getByText('Outer step').click(); - await expect(component.getByText('Inner step')).toBeVisible(); - await expect(component.getByText('test.spec.ts:42')).toBeVisible(); - await expect(component.getByText('My test')).toBeVisible(); -}); - -test('should render copy buttons for annotations', async ({ mount, page, context }) => { - await context.grantPermissions(['clipboard-read', 'clipboard-write']); - - const component = await mount(); - await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible(); - await component.getByText('Annotation text', { exact: false }).first().hover(); - await expect(component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first()).toBeVisible(); - await component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first().click(); - const handle = await page.evaluateHandle(() => navigator.clipboard.readText()); - const clipboardContent = await handle.jsonValue(); - expect(clipboardContent).toBe('Annotation text'); -}); - -const annotationLinkRenderingTestCase: TestCase = { - testId: 'testid', - title: 'My test', - path: [], - projectName: 'chromium', - location: { file: 'test.spec.ts', line: 42, column: 0 }, - annotations: [], - tags: [], - outcome: 'expected', - duration: 10, - ok: true, - results: [{ - ...result, - annotations: [ - { type: 'more info', description: 'read https://playwright.dev/docs/intro and https://playwright.dev/docs/api/class-playwright' }, - { type: 'related issues', description: 'https://github.com/microsoft/playwright/issues/23180, https://github.com/microsoft/playwright/issues/23181' }, - ] - }] -}; - -test('should correctly render links in annotations', async ({ mount }) => { - const component = await mount(); - - const firstLink = await component.getByText('https://playwright.dev/docs/intro').first(); - await expect(firstLink).toBeVisible(); - await expect(firstLink).toHaveAttribute('href', 'https://playwright.dev/docs/intro'); - - const secondLink = await component.getByText('https://playwright.dev/docs/api/class-playwright').first(); - await expect(secondLink).toBeVisible(); - await expect(secondLink).toHaveAttribute('href', 'https://playwright.dev/docs/api/class-playwright'); - - const thirdLink = await component.getByText('https://github.com/microsoft/playwright/issues/23180').first(); - await expect(thirdLink).toBeVisible(); - await expect(thirdLink).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/23180'); - - const fourthLink = await component.getByText('https://github.com/microsoft/playwright/issues/23181').first(); - await expect(fourthLink).toBeVisible(); - await expect(fourthLink).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/23181'); -}); - -const resultWithAttachment: TestResult = { - retry: 0, - startTime: new Date(0).toUTCString(), - duration: 100, - errors: [], - steps: [{ - title: 'Outer step', - startTime: new Date(100).toUTCString(), - duration: 10, - location: { file: 'test.spec.ts', line: 62, column: 0 }, - count: 1, - steps: [], - attachments: [1], - }], - attachments: [{ - name: 'first attachment', - body: 'The body with https://playwright.dev/docs/intro link and https://github.com/microsoft/playwright/issues/31284.', - contentType: 'text/plain' - }, { - name: 'attachment with inline link https://github.com/microsoft/playwright/issues/31284', - contentType: 'text/plain' - }], - annotations: [], - status: 'passed', -}; - -const attachmentLinkRenderingTestCase: TestCase = { - testId: 'testid', - title: 'My test', - path: ['group'], - projectName: 'chromium', - location: { file: 'test.spec.ts', line: 42, column: 0 }, - tags: [], - outcome: 'expected', - duration: 10, - ok: true, - annotations: [], - results: [resultWithAttachment] -}; - -const testCaseSummary: TestCaseSummary = { - testId: 'nextTestId', - title: 'next test', - path: [], - projectName: 'chromium', - location: { file: 'test.spec.ts', line: 42, column: 0 }, - tags: [], - outcome: 'expected', - duration: 10, - ok: true, - annotations: [], - results: [resultWithAttachment] -}; - - -test('should correctly render links in attachments', async ({ mount }) => { - const component = await mount(); - await component.getByText('first attachment').click(); - const body = await component.getByText('The body with https://playwright.dev/docs/intro link'); - await expect(body).toBeVisible(); - await expect(body.locator('a').filter({ hasText: 'playwright.dev' })).toHaveAttribute('href', 'https://playwright.dev/docs/intro'); - await expect(body.locator('a').filter({ hasText: 'github.com' })).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284'); - await expect(component).toMatchAriaSnapshot(` - - link "https://playwright.dev/docs/intro" - - link "https://github.com/microsoft/playwright/issues/31284" - `); -}); - -test('should correctly render links in attachment name', async ({ mount }) => { - const component = await mount(); - const link = component.getByText('attachment with inline link').locator('a'); - await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284'); - await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284'); - await expect(component).toMatchAriaSnapshot(` - - link /https:\\/\\/github\\.com\\/microsoft\\/playwright\\/issues\\/\\d+/ - `); -}); - -test('should correctly render prev and next', async ({ mount }) => { - const component = await mount(); - await expect(component).toMatchAriaSnapshot(` - - text: group - - link "« previous" - - link "next »" - - text: "My test test.spec.ts:42 10ms chromium" - `); -}); - - -const testCaseWithTwoAttempts: TestCase = { - ...testCase, - results: [ - { - ...result, - errors: [{ message: 'Error message' }], - status: 'failed', - duration: 50, - }, - { - ...result, - duration: 150, - status: 'passed', - }, - ], -}; - -test('total duration is selected run duration', async ({ mount, page }) => { - const component = await mount(); - await expect(component).toMatchAriaSnapshot(` - - text: "My test test.spec.ts:42 200ms chromium" - - tablist: - - tab "Run 50ms" - - 'tab "Retry #1 150ms"' - `); - await page.getByRole('tab', { name: 'Run' }).click(); - await expect(component).toMatchAriaSnapshot(` - - text: "My test test.spec.ts:42 200ms chromium" - `); - await page.getByRole('tab', { name: 'Retry' }).click(); - await expect(component).toMatchAriaSnapshot(` - - text: "My test test.spec.ts:42 200ms chromium" - `); -}); diff --git a/packages/web/playwright/index.html b/packages/web/playwright/index.html deleted file mode 100644 index 2d7858da5..000000000 --- a/packages/web/playwright/index.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - Tests - - -
        - - - diff --git a/packages/web/playwright/index.ts b/packages/web/playwright/index.ts deleted file mode 100644 index cb53a5548..000000000 --- a/packages/web/playwright/index.ts +++ /dev/null @@ -1,20 +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 '../src/common.css'; -import '../src/theme'; -import '../src/third_party/vscode/codicon.css'; -import '../src/third_party/vscode/colors.css'; diff --git a/packages/web/src/components/codeMirrorWrapper.spec.tsx b/packages/web/src/components/codeMirrorWrapper.spec.tsx deleted file mode 100644 index 81e8f81d3..000000000 --- a/packages/web/src/components/codeMirrorWrapper.spec.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) 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 { expect, test } from '@playwright/experimental-ct-react'; -import { CodeMirrorWrapper } from './codeMirrorWrapper'; - -test.use({ viewport: { width: 500, height: 500 } }); - -const javascriptSnippet = `import { test, expect } from '@playwright/test'; - -test('basic test', async ({ page }) => { - await page.goto('https://playwright.dev/'); - const title = page.locator('.navbar__inner .navbar__title'); - await expect(title).toHaveText('Playwright'); -}); -`; - -const pythonSnippet = `import asyncio -from playwright.async_api import async_playwright - -async def main(): - async with async_playwright() as p: - # Works across chromium, firefox and webkit - browser = await p.chromium.launch(headless=False) - -asyncio.run(main()) -`; - -const javaSnippet = `import com.microsoft.playwright.*; - -public class Example { - public static void main(String[] args) { - try (Playwright playwright = Playwright.create()) { - BrowserType chromium = playwright.chromium(); - Browser browser = chromium.launch(new BrowserType.LaunchOptions().setHeadless(false)); - } - } -} -`; - -const csharpSnippet = ` -using Microsoft.Playwright; -using System.Threading.Tasks; - -class Program -{ - public static async Task Main() - { - using var playwright = await Playwright.CreateAsync(); - await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions - { - Headless = false - }); - } -} -`; - -test('highlight JavaScript', async ({ mount }) => { - const component = await mount(); - await expect(component.locator('text="async"').first()).toHaveClass('cm-keyword'); -}); - -test('highlight Python', async ({ mount }) => { - const component = await mount(); - await expect(component.locator('text="async"').first()).toHaveClass('cm-keyword'); -}); - -test('highlight Java', async ({ mount }) => { - const component = await mount(); - await expect(component.locator('text="public"').first()).toHaveClass('cm-keyword'); -}); - -test('highlight C#', async ({ mount }) => { - const component = await mount(); - await expect(component.locator('text="public"').first()).toHaveClass('cm-keyword'); -}); - -test('highlight lines', async ({ mount }) => { - const component = await mount(); - await expect(component.locator('.source-line-running')).toContainText('goto'); - await expect(component.locator('.source-line-paused')).toContainText('title'); - await expect(component.locator('.source-line-error')).toContainText('expect'); -}); diff --git a/packages/web/src/components/expandable.spec.tsx b/packages/web/src/components/expandable.spec.tsx deleted file mode 100644 index 9536004be..000000000 --- a/packages/web/src/components/expandable.spec.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 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 { expect, test } from '@playwright/experimental-ct-react'; -import { Expandable } from './expandable'; - -test.use({ viewport: { width: 500, height: 500 } }); - -test('should render collapsed', async ({ mount }) => { - const component = await mount( {}} title='Title'>Details text); - await expect(component.locator('text=Title')).toBeVisible(); - await expect(component.locator('text=Details')).toBeHidden(); - await expect(component.locator('.codicon-chevron-right')).toBeVisible(); -}); - -test('should render expanded', async ({ mount }) => { - const component = await mount( {}} title='Title'>Details text); - await expect(component.locator('text=Title')).toBeVisible(); - await expect(component.locator('text=Details')).toBeVisible(); - await expect(component.locator('.codicon-chevron-down')).toBeVisible(); -}); - -test('click should expand', async ({ mount }) => { - let expanded = false; - const component = await mount( expanded = e} title='Title'>Details text); - await component.locator('.codicon-chevron-right').click(); - expect(expanded).toBeTruthy(); -}); diff --git a/packages/web/src/components/splitView.spec.tsx b/packages/web/src/components/splitView.spec.tsx deleted file mode 100644 index ca1152091..000000000 --- a/packages/web/src/components/splitView.spec.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) 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 { expect, test } from '@playwright/experimental-ct-react'; -import { SplitView } from './splitView'; - -test.use({ viewport: { width: 500, height: 500 } }); - -test('should render', async ({ mount }) => { - const component = await mount( - main
        } - sidebar={} - />); - const mainBox = await component.locator('#main').boundingBox(); - const sidebarBox = await component.locator('#sidebar').boundingBox(); - expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 400 }); - expect.soft(sidebarBox).toEqual({ x: 0, y: 401, width: 500, height: 99 }); -}); - -test('should render sidebar first', async ({ mount }) => { - const component = await mount( - main
        } - sidebar={} - />); - const mainBox = await component.locator('#main').boundingBox(); - const sidebarBox = await component.locator('#sidebar').boundingBox(); - expect.soft(mainBox).toEqual({ x: 0, y: 100, width: 500, height: 400 }); - expect.soft(sidebarBox).toEqual({ x: 0, y: 0, width: 500, height: 99 }); -}); - -test('should render horizontal split', async ({ mount }) => { - const component = await mount( - main
        } - sidebar={} - />); - const mainBox = await component.locator('#main').boundingBox(); - const sidebarBox = await component.locator('#sidebar').boundingBox(); - expect.soft(mainBox).toEqual({ x: 100, y: 0, width: 400, height: 500 }); - expect.soft(sidebarBox).toEqual({ x: 0, y: 0, width: 99, height: 500 }); -}); - -test('should hide sidebar', async ({ mount }) => { - const component = await mount( - main
        } - sidebar={} - />); - const mainBox = await component.locator('#main').boundingBox(); - expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 500 }); -}); - -test('drag resize', async ({ page, mount }) => { - const component = await mount( - main
        } - sidebar={} - />); - await page.mouse.move(25, 400); - await page.mouse.down(); - await page.mouse.move(25, 100); - await page.mouse.up(); - const mainBox = await component.locator('#main').boundingBox(); - const sidebarBox = await component.locator('#sidebar').boundingBox(); - expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 100 }); - expect.soft(sidebarBox).toEqual({ x: 0, y: 101, width: 500, height: 399 }); -}); diff --git a/packages/web/src/shared/imageDiffView.spec.tsx b/packages/web/src/shared/imageDiffView.spec.tsx deleted file mode 100644 index 4fefaa108..000000000 --- a/packages/web/src/shared/imageDiffView.spec.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 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/experimental-ct-react'; -import type { ImageDiff } from './imageDiffView'; -import { ImageDiffView } from './imageDiffView'; - -test.use({ viewport: { width: 1000, height: 800 } }); - -const expectedPng = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAACMElEQVRYw+1XT0tCQRD/9Qci0Cw7mp1C6BMYnt5niMhPEEFCh07evNk54XnuGkhFehA/QxHkqYMEFWXpscMTipri7fqeu+vbfY+EoBkQ3Zn5zTo7MzsL/NNfoClkUUQNN3jCJ/ETfavRSpYkkSmFQzz8wMr4gaSp8OBJ2HCU4Iwd0kqGgd9GPxCccZ+0jWgWVW1wxlWy0qR51I3hv7lOllq7b4SC/+aGzr+QBadjEKgAykvzJGXwr/Lj4JfRk5hUSLKIa00HPUJRki0xeMWSWxVXmi5sddXKymqTyxdwquXAUVV3WREeLx3gTcNFWQY/jXtB8QIzgt4qTvAR4OCe0ATKCmrnmFMEM0Pp2BvrIisaFUdUjgKKZgYWSjjDLR5J+x13lATHuHSti6JBzQP+gq2QHXjfRaiJojbPgYqbmGFow0VpiyIW0/VIF9QKLzeBWA2MHmwCu8QJQV++Ps/joHQQH4HpuO0uobUeVztgIcr4Vnf4we9orWfUIWKHbEVyYKkPmaVpIVKICuo0ZYXWjHTITXWhsVYxkIDpUoKsla1i2Oz2QjvYG9fshu36GbFQ8DGyHNOuvRdOKZSDUtCFM7wyHeSM4XN8e7bOpd9F2gg+TRYal753bGkbuEjzMg0YW/yDV1czUDm+e43Byz86OnRwsYDMKXlmkYbeAOwffrtU/nGpXpwkXfPhVza+D9AiMAtrtOMYfVr0q8Wr1nh8n8ADZCJPqAk8AifyjP2n36cvkA6/Wln9MokAAAAASUVORK5CYII='; -const actualPng = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAACcElEQVRYw+2XQU9TQRDHfxAiYoFE8IaHhhjjN3hw4+SJk0npGU+iCQYv1kQjIQInj6+foh4amhjvEi8EI3yAIk24lBgoBkmqqYfuLvt23763r8QmJszc3sz+/29nZ3Zm4Vr+BxkgoESFPY7o0OGIPSqUCBi4OvgUmzToOLTBJlO9g08QcuEEl3pByEQv8Ascp4JLPWYhG/gQ5QjAGVWWmSXPTUa5xxxLfDJ2V2bIF36ELW3hASuMxfqNsiSOvatbjPj9fU0tabPOjUTvHG/4pfxrPrvQg7PqteNA20c5zbkYiWubgmZbZJcTzvjKGneMZP6m1hST4CdpGvmhU0zzXX0/5VFk5V21iyaTboJQwa7STqH4Y1AE6ixCd9XKxHsHFFIpTo1AvVal56juDeFQZxi8KNaMjJJh2oiDH+RQmFfUtzSKXQPjifh+yGBcsnWNrUhZJVO0DIxxFeTAJigJU9X4nkRxYqF8FL4lm6AiTMuW5bMzaXcs36fCs2IT7AvTjHNvNsVjy3dO+O3bBLLE8jF9oemsblPuq3KzRB7PrZhl8/z2pBhTteAkyMUunI+0HzdFzk0gwzDtvKde8SU2o3TJu0MkD3k28bYtpFDMuA/ZnaZZKJ6707SkuhJXoKi6Cy1QDT7XM8U4LfdVEXfZSXnAMz6wTZ1zzqmzrVWGTvEi6bK7vK4bWqO/zUtF7FJJMcxB0nWtN5y3omje89Nr8OpSrKc1HL1lBjzUGosPxWWTDX2a/o8M4FFNbPrm2NKLFrMMXtk1dfCKjo5ZteY3AEeHX3/1HH7jxne/4HiP7314gPTlCdWHR2BfnrHX8u/lL/ENCdIFFeD3AAAAAElFTkSuQmCC'; - -const actualAttachment = { name: 'screenshot-actual.png', path: actualPng, contentType: 'image/png', }; -const expectedAttachment = { name: 'screenshot-expected.png', path: expectedPng, contentType: 'image/png', }; -const diffAttachment = { name: 'screenshot-diff.png', path: expectedPng, contentType: 'image/png', }; - -const imageDiff: ImageDiff = { - name: 'log in', - actual: { attachment: actualAttachment }, - expected: { attachment: expectedAttachment, title: 'Expected' }, - diff: { attachment: diffAttachment }, -}; - -test('should render links', async ({ mount }) => { - const component = await mount(); - await expect(component.locator('a')).toHaveText([ - 'screenshot-diff.png', - 'screenshot-actual.png', - 'screenshot-expected.png', - ]); -}); - -test('should show diff by default', async ({ mount }) => { - const component = await mount(); - - const image = component.locator('img'); - const box = await image.boundingBox(); - expect(box).toEqual(expect.objectContaining({ width: 48, height: 48 })); -}); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 9ceae5868..ce856b443 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -481,6 +481,22 @@ test('should show font preview', async ({ page, runAndTrace, server }) => { await expect(traceViewer.page.locator('.network-request-details-tab')).toContainText('ABCDEF'); }); +test('should syntax highlight body', async ({ page, runAndTrace, server }) => { + const traceViewer = await runAndTrace(async () => { + await page.goto(`${server.PREFIX}/network-tab/network.html`); + await page.evaluate(() => (window as any).donePromise); + }); + await traceViewer.selectAction('Navigate'); + await traceViewer.showNetworkTab(); + + await traceViewer.page.getByText('HTML', { exact: true }).click(); + await expect(traceViewer.networkRequests).toHaveCount(1); + await traceViewer.networkRequests.getByText('network.html').click(); + await traceViewer.page.getByTestId('network-request-details').getByTitle('Body').click(); + const keyword = traceViewer.page.locator('.network-request-details-tab').getByText('const').first(); + await expect(keyword).toHaveClass('cm-keyword'); +}); + test('should filter network requests by url', async ({ page, runAndTrace, server }) => { const traceViewer = await runAndTrace(async () => { await page.goto(`${server.PREFIX}/network-tab/network.html`); From 68161ba65b82088f7110277a381cf51d887b05e5 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 11 Nov 2025 23:29:56 +0000 Subject: [PATCH 202/250] fix(selector generator): heuristic for icon fonts (#38182) --- packages/injected/src/selectorGenerator.ts | 3 ++- tests/library/selector-generator.spec.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/injected/src/selectorGenerator.ts b/packages/injected/src/selectorGenerator.ts index 202472aa4..73b6e4d3a 100644 --- a/packages/injected/src/selectorGenerator.ts +++ b/packages/injected/src/selectorGenerator.ts @@ -343,7 +343,8 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i const ariaRole = getAriaRole(element); if (ariaRole && !['none', 'presentation'].includes(ariaRole)) { const ariaName = getElementAccessibleName(element, false); - if (ariaName) { + // \p{Co} means "Private Use" characters - these are often used for icon fonts and make for bad locators. + if (ariaName && !ariaName.match(/^\p{Co}+$/u)) { const roleToken = { engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScoreExact }; candidates.push([roleToken]); for (const alternative of suitableTextAlternatives(ariaName)) diff --git a/tests/library/selector-generator.spec.ts b/tests/library/selector-generator.spec.ts index a78fb15a5..37305fc5c 100644 --- a/tests/library/selector-generator.spec.ts +++ b/tests/library/selector-generator.spec.ts @@ -661,4 +661,23 @@ it.describe('selector generator', () => { `internal:label=\"Toggle Todo\"i >> nth=0`, ]); }); + + it('should not use icon fonts aria name', async ({ page }) => { + await page.setContent(` + +
        + + +
        + `); + expect.soft(await generate(page, 'button:first-child')).toBe('internal:role=button >> nth=0'); + expect.soft(await generate(page, 'button:last-child')).toBe('internal:role=button >> nth=1'); + }); }); From 129ac60f466d9c5908db720281d779deccd69e2a Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 11 Nov 2025 16:10:02 -0800 Subject: [PATCH 203/250] chore: run headlessly in copilot cli (#38188) --- .../agents/playwright-test-generator.agent.md | 9 +++ .../agents/playwright-test-healer.agent.md | 9 +++ .../agents/playwright-test-planner.agent.md | 58 ++++--------------- .../playwright/src/agents/generateAgents.ts | 42 +++++++------- .../playwright/src/mcp/test/testContext.ts | 8 ++- packages/playwright/src/mcp/test/testTools.ts | 2 +- 6 files changed, 58 insertions(+), 70 deletions(-) diff --git a/examples/todomvc/.github/agents/playwright-test-generator.agent.md b/examples/todomvc/.github/agents/playwright-test-generator.agent.md index e52ddbf61..b3722976b 100644 --- a/examples/todomvc/.github/agents/playwright-test-generator.agent.md +++ b/examples/todomvc/.github/agents/playwright-test-generator.agent.md @@ -31,6 +31,15 @@ tools: - playwright-test/generator_setup_page - playwright-test/generator_write_test model: Claude Sonnet 4 +mcp-servers: + playwright-test: + type: stdio + command: npx + args: + - playwright + - run-test-mcp-server + tools: + - "*" --- You are a Playwright Test Generator, an expert in browser automation and end-to-end testing. diff --git a/examples/todomvc/.github/agents/playwright-test-healer.agent.md b/examples/todomvc/.github/agents/playwright-test-healer.agent.md index a77d07b45..2e044868b 100644 --- a/examples/todomvc/.github/agents/playwright-test-healer.agent.md +++ b/examples/todomvc/.github/agents/playwright-test-healer.agent.md @@ -13,6 +13,15 @@ tools: - playwright-test/test_list - playwright-test/test_run model: Claude Sonnet 4 +mcp-servers: + playwright-test: + type: stdio + command: npx + args: + - playwright + - run-test-mcp-server + tools: + - "*" --- You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and diff --git a/examples/todomvc/.github/agents/playwright-test-planner.agent.md b/examples/todomvc/.github/agents/playwright-test-planner.agent.md index 8c51e58c8..6ac9a5f1f 100644 --- a/examples/todomvc/.github/agents/playwright-test-planner.agent.md +++ b/examples/todomvc/.github/agents/playwright-test-planner.agent.md @@ -4,7 +4,6 @@ description: Use this agent when you need to create comprehensive test plan for a web application or website tools: - search - - edit - playwright-test/browser_click - playwright-test/browser_close - playwright-test/browser_console_messages @@ -23,7 +22,17 @@ tools: - playwright-test/browser_type - playwright-test/browser_wait_for - playwright-test/planner_setup_page + - playwright-test/planner_save_plan model: Claude Sonnet 4 +mcp-servers: + playwright-test: + type: stdio + command: npx + args: + - playwright + - run-test-mcp-server + tools: + - "*" --- You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test @@ -61,52 +70,7 @@ You will: 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 - - Each test case with proposed file name for implementation - - 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 - -**File** `tests/adding-new-todos/add-valid-todo.spec.ts` - -**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 -... - + Submit your test plan using `planner_save_plan` tool. **Quality Standards**: - Write steps that are specific enough for any tester to follow diff --git a/packages/playwright/src/agents/generateAgents.ts b/packages/playwright/src/agents/generateAgents.ts index 5184c6a9c..5a5dff14a 100644 --- a/packages/playwright/src/agents/generateAgents.ts +++ b/packages/playwright/src/agents/generateAgents.ts @@ -243,26 +243,13 @@ export class CopilotGenerator { await deleteFile(`.github/chatmodes/ 🎭 planner.chatmode.md`, 'legacy planner chatmode'); await deleteFile(`.github/chatmodes/🎭 generator.chatmode.md`, 'legacy generator chatmode'); await deleteFile(`.github/chatmodes/🎭 healer.chatmode.md`, 'legacy healer chatmode'); + await deleteFile(`.github/agents/ 🎭 planner.agent.md`, 'legacy planner agent'); + await deleteFile(`.github/agents/🎭 generator.agent.md`, 'legacy generator agent'); + await deleteFile(`.github/agents/🎭 healer.agent.md`, 'legacy healer agent'); await VSCodeGenerator.appendToMCPJson(); - const cwdFolder = path.basename(process.cwd()); - const mcpConfig = { - 'mcpServers': { - 'playwright-test': { - 'type': 'stdio', - 'command': 'npx', - 'args': [ - `--prefix=/home/runner/work/${cwdFolder}/${cwdFolder}`, - 'playwright', - 'run-test-mcp-server', - '--headless', - `--config=/home/runner/work/${cwdFolder}/${cwdFolder}` - ], - 'tools': ['*'] - } - } - }; + const mcpConfig = { mcpServers: CopilotGenerator.mcpServers }; if (!fs.existsSync('.github/copilot-setup-steps.yml')) { const yaml = fs.readFileSync(path.join(__dirname, 'copilot-setup-steps.yml'), 'utf-8'); @@ -283,10 +270,11 @@ export class CopilotGenerator { const examples = agent.examples.length ? ` Examples: ${agent.examples.map(example => `${example}`).join('')}` : ''; const lines: string[] = []; const header = { - name: agent.header.name, - description: agent.header.description + examples, - tools: agent.header.tools, - model: 'Claude Sonnet 4', + 'name': agent.header.name, + 'description': agent.header.description + examples, + 'tools': agent.header.tools, + 'model': 'Claude Sonnet 4', + 'mcp-servers': CopilotGenerator.mcpServers, }; lines.push(`---`); lines.push(yaml.stringify(header) + `---`); @@ -295,6 +283,18 @@ export class CopilotGenerator { lines.push(''); return lines.join('\n'); } + + static mcpServers = { + 'playwright-test': { + 'type': 'stdio', + 'command': 'npx', + 'args': [ + 'playwright', + 'run-test-mcp-server' + ], + 'tools': ['*'] + }, + }; } export class VSCodeGenerator { diff --git a/packages/playwright/src/mcp/test/testContext.ts b/packages/playwright/src/mcp/test/testContext.ts index e27b1fa50..d556d806a 100644 --- a/packages/playwright/src/mcp/test/testContext.ts +++ b/packages/playwright/src/mcp/test/testContext.ts @@ -15,6 +15,7 @@ */ import fs from 'fs'; +import os from 'os'; import path from 'path'; import { noColors, escapeRegExp } from 'playwright-core/lib/utils'; @@ -75,6 +76,7 @@ export class TestContext { private _pushClient: MDBPushClientCallback; private _testRunner: TestRunner | undefined; readonly options?: { muteConsole?: boolean, headless?: boolean }; + readonly computedHeaded: boolean; configLocation!: ConfigLocation; rootPath!: string; generatorJournal: GeneratorJournal | undefined; @@ -82,6 +84,10 @@ export class TestContext { constructor(pushClient: MDBPushClientCallback, options?: { muteConsole?: boolean, headless?: boolean }) { this._pushClient = pushClient; this.options = options; + if (options?.headless !== undefined) + this.computedHeaded = !options.headless; + else + this.computedHeaded = !process.env.CI && !(os.platform() === 'linux' && !process.env.DISPLAY); } initialize(rootPath: string | undefined, configLocation: ConfigLocation) { @@ -159,7 +165,7 @@ export class TestContext { async runSeedTest(seedFile: string, projectName: string, progress: ProgressCallback) { await this.runWithGlobalSetup(async (testRunner, reporter) => { const result = await testRunner.runTests(reporter, { - headed: !this.options?.headless, + headed: this.computedHeaded, locations: ['/' + escapeRegExp(seedFile) + '/'], projects: [projectName], timeout: 0, diff --git a/packages/playwright/src/mcp/test/testTools.ts b/packages/playwright/src/mcp/test/testTools.ts index 096d6bca9..5087a3ba1 100644 --- a/packages/playwright/src/mcp/test/testTools.ts +++ b/packages/playwright/src/mcp/test/testTools.ts @@ -80,7 +80,7 @@ export const debugTest = defineTestTool({ handle: async (context, params, progress) => { await context.runWithGlobalSetup(async (testRunner, reporter) => { await testRunner.runTests(reporter, { - headed: !context.options?.headless, + headed: context.computedHeaded, testIds: [params.test.id], // For automatic recovery timeout: 0, From 3485accdfee51047ab602a81fe3c83658e556d41 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 12 Nov 2025 06:29:14 -0800 Subject: [PATCH 204/250] chore(docs): fix asymmetric matcher expressions (#38177) --- docs/src/test-assertions-js.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/src/test-assertions-js.md b/docs/src/test-assertions-js.md index 59f30b3bb..c809dce78 100644 --- a/docs/src/test-assertions-js.md +++ b/docs/src/test-assertions-js.md @@ -91,14 +91,21 @@ Prefer [auto-retrying](#auto-retrying-assertions) assertions whenever possible. | [`method: GenericAssertions.toMatchObject`] | Object contains specified properties | | [`method: GenericAssertions.toStrictEqual`] | Value is similar, including property types | | [`method: GenericAssertions.toThrow`] | Function throws an error | -| [`method: GenericAssertions.any`] | Matches any instance of a class/primitive | -| [`method: GenericAssertions.anything`] | Matches anything | -| [`method: GenericAssertions.arrayContaining`] | Array contains specific elements | -| [`method: GenericAssertions.arrayOf`] | Array contains elements of specific type | -| [`method: GenericAssertions.closeTo`] | Number is approximately equal | -| [`method: GenericAssertions.objectContaining`] | Object contains specific properties | -| [`method: GenericAssertions.stringContaining`] | String contains a substring | -| [`method: GenericAssertions.stringMatching`] | String matches a regular expression | + +## Asymmetric matchers + +These expressions can be nested in other assertions to allow more relaxed matching against a given condition. + +| Matcher | Description | +| :- | :- | +| [expect.any()](./api/class-genericassertions.md#generic-assertions-any) | Matches any instance of a class/primitive | +| [expect.anything()](./api/class-genericassertions.md#generic-assertions-anything) | Matches anything | +| [expect.arrayContaining()](./api/class-genericassertions.md#generic-assertions-array-containing) | Array contains specific elements | +| [expect.arrayOf()](./api/class-genericassertions.md#generic-assertions-array-of) | Array contains elements of specific type | +| [expect.closeTo()](./api/class-genericassertions.md#generic-assertions-close-to) | Number is approximately equal | +| [expect.objectContaining()](./api/class-genericassertions.md#generic-assertions-object-containing) | Object contains specific properties | +| [expect.stringContaining()](./api/class-genericassertions.md#generic-assertions-string-containing) | String contains a substring | +| [expect.stringMatching()](./api/class-genericassertions.md#generic-assertions-string-matching) | String matches a regular expression | ## Negating matchers From cba582647b1cabe67534a782f3d4342da6b7daea Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 12 Nov 2025 07:11:22 -0800 Subject: [PATCH 205/250] feat(locator): extend mouse movement steps to dragTo (#38184) --- docs/src/api/class-frame.md | 3 ++ docs/src/api/class-locator.md | 3 ++ docs/src/api/class-page.md | 3 ++ docs/src/api/params.md | 5 ++ packages/playwright-client/types/types.d.ts | 18 +++++++ .../playwright-core/src/protocol/validator.ts | 1 + packages/playwright-core/src/server/frames.ts | 2 +- packages/playwright-core/src/server/types.ts | 1 + packages/playwright-core/types/types.d.ts | 18 +++++++ packages/protocol/src/channels.d.ts | 2 + packages/protocol/src/protocol.yml | 1 + tests/page/page-drag.spec.ts | 54 ++++++++++++++++++- 12 files changed, 109 insertions(+), 2 deletions(-) diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index e286c63bf..f9c366e42 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -482,6 +482,9 @@ Optional event-specific initialization properties. ### option: Frame.dragAndDrop.targetPosition = %%-input-target-position-%% * since: v1.14 +### option: Frame.dragAndDrop.steps = %%-input-drag-steps-%% +* since: v1.57 + ## async method: Frame.evalOnSelector * since: v1.9 * discouraged: This method does not wait for the element to pass the actionability diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index a1f64a75b..042c08bb9 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -875,6 +875,9 @@ Locator of the element to drag to. ### option: Locator.dragTo.targetPosition = %%-input-target-position-%% * since: v1.18 +### option: Locator.dragTo.steps = %%-input-drag-steps-%% +* since: v1.57 + ## async method: Locator.elementHandle * since: v1.14 * discouraged: Always prefer using [Locator]s and web assertions over [ElementHandle]s because latter are inherently racy. diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index ad4a89bda..717cbf06c 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -1102,6 +1102,9 @@ await Page.DragAndDropAsync("#source", "#target", new() ### option: Page.dragAndDrop.targetPosition = %%-input-target-position-%% * since: v1.14 +### option: Page.dragAndDrop.steps = %%-input-drag-steps-%% +* since: v1.57 + ## async method: Page.emulateMedia * since: v1.8 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index e0141bec4..37f6665a9 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -108,6 +108,11 @@ element. Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. +## input-drag-steps +- `steps` <[int]> + +Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` of the drag. When set to 1, emits a single `mousemove` event at the destination location. + ## input-modifiers - `modifiers` <[Array]<[KeyboardModifier]<"Alt"|"Control"|"ControlOrMeta"|"Meta"|"Shift">>> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index d5383a95c..b2d81ca90 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -2496,6 +2496,12 @@ export interface Page { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + * of the drag. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -6400,6 +6406,12 @@ export interface Frame { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + * of the drag. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -13075,6 +13087,12 @@ export interface Locator { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + * of the drag. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * Drops on the target element at this point relative to the top-left corner of the element's padding box. If not * specified, some visible point of the element is used. diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index c3b46f560..7c0622a3f 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1582,6 +1582,7 @@ scheme.FrameDragAndDropParams = tObject({ sourcePosition: tOptional(tType('Point')), targetPosition: tOptional(tType('Point')), strict: tOptional(tBoolean), + steps: tOptional(tInt), }); scheme.FrameDragAndDropResult = tOptional(tObject({})); scheme.FrameDblclickParams = tObject({ diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index b89faf28f..da1de2170 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1168,7 +1168,7 @@ export class Frame extends SdkObject { // Note: do not perform locator handlers checkpoint to avoid moving the mouse in the middle of a drag operation. dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, options.strict, false /* performActionPreChecks */, async handle => { return handle._retryPointerAction(progress, 'move and up', false, async point => { - await this._page.mouse.move(progress, point.x, point.y); + await this._page.mouse.move(progress, point.x, point.y, { steps: options.steps }); await this._page.mouse.up(progress); }, { ...options, diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index 089ef21fd..b189b92db 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -113,6 +113,7 @@ export type PointerActionOptions = { export type DragActionOptions = { sourcePosition?: Point; targetPosition?: Point; + steps?: number; }; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d5383a95c..b2d81ca90 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2496,6 +2496,12 @@ export interface Page { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + * of the drag. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -6400,6 +6406,12 @@ export interface Frame { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + * of the drag. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one * element, the call throws an exception. @@ -13075,6 +13087,12 @@ export interface Locator { y: number; }; + /** + * Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + * of the drag. When set to 1, emits a single `mousemove` event at the destination location. + */ + steps?: number; + /** * Drops on the target element at this point relative to the top-left corner of the element's padding box. If not * specified, some visible point of the element is used. diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 66f4443ce..96946db47 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -2789,6 +2789,7 @@ export type FrameDragAndDropParams = { sourcePosition?: Point, targetPosition?: Point, strict?: boolean, + steps?: number, }; export type FrameDragAndDropOptions = { force?: boolean, @@ -2796,6 +2797,7 @@ export type FrameDragAndDropOptions = { sourcePosition?: Point, targetPosition?: Point, strict?: boolean, + steps?: number, }; export type FrameDragAndDropResult = void; export type FrameDblclickParams = { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 9e018052b..786090a08 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2224,6 +2224,7 @@ Frame: sourcePosition: Point? targetPosition: Point? strict: boolean? + steps: int? flags: slowMo: true snapshot: true diff --git a/tests/page/page-drag.spec.ts b/tests/page/page-drag.spec.ts index e7e3d1783..43e0f335b 100644 --- a/tests/page/page-drag.spec.ts +++ b/tests/page/page-drag.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { ElementHandle, Route } from 'playwright-core'; +import type { ElementHandle, Page, Route } from 'playwright-core'; import { test as it, expect } from './pageTest'; import { attachFrame } from '../config/utils'; @@ -293,6 +293,58 @@ it.describe('Drag and drop', () => { expect(await page.$eval('#target', target => target.contains(document.querySelector('#source')))).toBe(true); // could not find source in target }); + [{ + title: 'dragAndDrop', + drag: (page: Page, steps?: number) => page.dragAndDrop('#red', '#blue', { steps }), + }, { + title: 'dragTo', + drag: (page: Page, steps?: number) => page.locator('#red').dragTo(page.locator('#blue'), { steps }), + }].forEach(({ title, drag }) => { + it(`should ${title} with tweened mouse movement`, async ({ page }) => { + await page.setContent(` + +
        +
        + + `); + const eventsHandle = await page.evaluateHandle(() => { + const events = []; + document.addEventListener('mousedown', event => { + events.push({ + type: 'mousedown', + x: event.pageX, + y: event.pageY, + }); + }); + document.addEventListener('mouseup', event => { + events.push({ + type: 'mouseup', + x: event.pageX, + y: event.pageY, + }); + }); + document.addEventListener('mousemove', event => { + events.push({ + type: 'mousemove', + x: event.pageX, + y: event.pageY, + }); + }); + return events; + }); + await drag(page, 4); + await expect.poll(() => eventsHandle.jsonValue()).toEqual([ + { type: 'mousemove', x: 50, y: 50 }, + { type: 'mousedown', x: 50, y: 50 }, + { type: 'mousemove', x: 75, y: 75 }, + { type: 'mousemove', x: 100, y: 100 }, + { type: 'mousemove', x: 125, y: 125 }, + { type: 'mousemove', x: 150, y: 150 }, + { type: 'mouseup', x: 150, y: 150 }, + ]); + }); + }); + it('should allow specifying the position', async ({ page, server }) => { await page.setContent(`
        From 81711dba8f6edb1be7aab736af1bf673a5725821 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 12 Nov 2025 16:52:20 +0100 Subject: [PATCH 206/250] test: drop "large number of logs" test (#38195) --- tests/playwright-test/reporter-junit.spec.ts | 22 -------------------- 1 file changed, 22 deletions(-) diff --git a/tests/playwright-test/reporter-junit.spec.ts b/tests/playwright-test/reporter-junit.spec.ts index 838f35e84..994748490 100644 --- a/tests/playwright-test/reporter-junit.spec.ts +++ b/tests/playwright-test/reporter-junit.spec.ts @@ -126,28 +126,6 @@ for (const useIntermediateMergeReport of [false, true] as const) { expect(result.exitCode).toBe(1); }); - test('should handle large number of console logs', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37719' } }, async ({ runInlineTest }, testInfo) => { - test.slow(); - // need to go via disk, otherwise our test harness would print 500k lines of stdout to the Github Actions UI that can't handle it. - const reportFile = testInfo.outputPath('report.xml'); - const result = await runInlineTest({ - 'a.test.ts': ` - import { test, expect } from '@playwright/test'; - test('one', async ({}) => { - test.slow(); - for (let i = 0; i < 500000; i++) { - console.log('log line ' + i); - } - }); - `, - }, { reporter: 'junit' }, { PLAYWRIGHT_JUNIT_OUTPUT_FILE: reportFile }); - expect(result.exitCode).toBe(0); - const report = await fs.promises.readFile(reportFile, 'utf8'); - const testcase = parseXML(report)['testsuites']['testsuite'][0]['testcase'][0]; - expect(testcase['system-out']).toHaveLength(1); - expect(testcase['system-out'][0]).toContain('log line 99999'); - }); - test('should render stdout without ansi escapes', async ({ runInlineTest }) => { const result = await runInlineTest({ 'playwright.config.ts': ` From fef825179f4e986495eb592d087e3b02153b9707 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Wed, 12 Nov 2025 19:57:16 +0100 Subject: [PATCH 207/250] chore(bidi): fix finding the opener for popups (#38199) --- .../src/server/bidi/bidiBrowser.ts | 16 ++++++++++------ tests/page/page-event-popup.spec.ts | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/playwright-core/src/server/bidi/bidiBrowser.ts b/packages/playwright-core/src/server/bidi/bidiBrowser.ts index d9cdc83e0..7ac5e105a 100644 --- a/packages/playwright-core/src/server/bidi/bidiBrowser.ts +++ b/packages/playwright-core/src/server/bidi/bidiBrowser.ts @@ -134,16 +134,13 @@ export class BidiBrowser extends Browser { private _onBrowsingContextCreated(event: bidi.BrowsingContext.Info) { if (event.parent) { const parentFrameId = event.parent; - for (const page of this._bidiPages.values()) { - const parentFrame = page._page.frameManager.frame(parentFrameId); - if (!parentFrame) - continue; + const page = this._findPageForFrame(parentFrameId); + if (page) { page._session.addFrameBrowsingContext(event.context); page._page.frameManager.frameAttached(event.context, parentFrameId); const frame = page._page.frameManager.frame(event.context); if (frame) frame._url = event.url; - return; } return; } @@ -153,7 +150,7 @@ export class BidiBrowser extends Browser { if (!context) return; const session = this._connection.createMainFrameBrowsingContextSession(event.context); - const opener = event.originalOpener && this._bidiPages.get(event.originalOpener); + const opener = event.originalOpener && this._findPageForFrame(event.originalOpener); const page = new BidiPage(context, session, opener || null); page._page.mainFrame()._url = event.url; this._bidiPages.set(event.context, page); @@ -185,6 +182,13 @@ export class BidiBrowser extends Browser { return; } } + + private _findPageForFrame(frameId: string) { + for (const page of this._bidiPages.values()) { + if (page._page.frameManager.frame(frameId)) + return page; + } + } } export class BidiBrowserContext extends BrowserContext { diff --git a/tests/page/page-event-popup.spec.ts b/tests/page/page-event-popup.spec.ts index 152136acd..9c9ba51ac 100644 --- a/tests/page/page-event-popup.spec.ts +++ b/tests/page/page-event-popup.spec.ts @@ -148,7 +148,7 @@ it('should work with clicking target=_blank and rel=noopener', async ({ page, se it('should report popup opened from iframes', async ({ page, server, browserName }) => { await page.goto(server.PREFIX + '/frames/two-frames.html'); - const frame = page.frame('uno'); + const frame = page.frames()[1]; expect(frame).toBeTruthy(); const [popup] = await Promise.all([ page.waitForEvent('popup'), From 60e6df499c27a9d9cf0b05ac2440f39139894a15 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Wed, 12 Nov 2025 19:59:36 +0100 Subject: [PATCH 208/250] chore(bidi): don't set Accept-Language header for a locale override (#38196) --- packages/playwright-core/src/server/bidi/bidiBrowser.ts | 6 ++---- packages/playwright-core/src/server/bidi/bidiPage.ts | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/playwright-core/src/server/bidi/bidiBrowser.ts b/packages/playwright-core/src/server/bidi/bidiBrowser.ts index 7ac5e105a..b9d3ce349 100644 --- a/packages/playwright-core/src/server/bidi/bidiBrowser.ts +++ b/packages/playwright-core/src/server/bidi/bidiBrowser.ts @@ -230,7 +230,7 @@ export class BidiBrowserContext extends BrowserContext { userContexts: [this._userContextId()], })); } - if (this._options.extraHTTPHeaders || this._options.locale) + if (this._options.extraHTTPHeaders) promises.push(this.doUpdateExtraHTTPHeaders()); await Promise.all(promises); } @@ -330,9 +330,7 @@ export class BidiBrowserContext extends BrowserContext { } async doUpdateExtraHTTPHeaders(): Promise { - let allHeaders = this._options.extraHTTPHeaders || []; - if (this._options.locale) - allHeaders = network.mergeHeaders([allHeaders, network.singleHeader('Accept-Language', this._options.locale)]); + const allHeaders = this._options.extraHTTPHeaders || []; await this._browser._browserSession.send('network.setExtraHeaders', { headers: allHeaders.map(({ name, value }) => ({ name, value: { type: 'string', value } })), userContexts: [this._userContextId()], diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index e2015ac12..9041469d5 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -298,11 +298,9 @@ export class BidiPage implements PageDelegate { } async updateExtraHTTPHeaders(): Promise { - const locale = this._browserContext._options.locale; const allHeaders = network.mergeHeaders([ this._browserContext._options.extraHTTPHeaders, this._page.extraHTTPHeaders(), - locale ? network.singleHeader('Accept-Language', locale) : undefined, ]); await this._session.send('network.setExtraHeaders', { headers: allHeaders.map(({ name, value }) => ({ name, value: { type: 'string', value } })), From 684f5501d1c3fe52068b0273452fa966f2705a54 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 12 Nov 2025 11:15:40 -0800 Subject: [PATCH 209/250] chore: allow capturing port in webServer stdio (#38181) --- docs/src/test-api/class-testconfig.md | 6 +- .../playwright/src/plugins/webServerPlugin.ts | 56 ++++---- packages/playwright/types/test.d.ts | 12 +- tests/playwright-test/web-server.spec.ts | 131 ++++++++---------- 4 files changed, 97 insertions(+), 108 deletions(-) diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index d321cf662..b5f78ac8a 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -704,9 +704,9 @@ export default defineConfig({ - `reuseExistingServer` ?<[boolean]> If true, it will re-use an existing server on the `port` or `url` when available. If no server is running on that `port` or `url`, it will run the command to start a new server. If `false`, it will throw if an existing process is listening on the `port` or `url`. This should be commonly set to `!process.env.CI` to allow the local dev server when running tests locally. - `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. - `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. - - `wait` ?<[Object]> Consider command started only when given output has been produced or a time in milliseconds has passed. - - `stdout` ?<[RegExp]> - - `stderr` ?<[RegExp]> + - `wait` ?<[Object]> Consider command started only when given output has been produced. + - `stdout` ?<[RegExp]> Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the environment, for example /Listening on port (?\\d+)/ will store the port number in `process.env['MY_SERVER_PORT']`. + - `stderr` ?<[RegExp]> Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the environment, for example /Listening on port (?\\d+)/ will store the port number in `process.env['MY_SERVER_PORT']`. - `time` ?<[int]> - `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. - `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified. diff --git a/packages/playwright/src/plugins/webServerPlugin.ts b/packages/playwright/src/plugins/webServerPlugin.ts index c99ee3726..97f7ff04c 100644 --- a/packages/playwright/src/plugins/webServerPlugin.ts +++ b/packages/playwright/src/plugins/webServerPlugin.ts @@ -25,11 +25,10 @@ import type { FullConfig } from '../../types/testReporter'; import type { FullConfigInternal } from '../common/config'; import type { ReporterV2 } from '../reporters/reporterV2'; - export type WebServerPluginOptions = { command: string; url?: string; - wait?: { stdout?: RegExp, stderr?: RegExp, time?: number }; + wait?: { stdout?: RegExp, stderr?: RegExp }; ignoreHTTPSErrors?: boolean; timeout?: number; gracefulShutdown?: { signal: 'SIGINT' | 'SIGTERM', timeout?: number }; @@ -143,43 +142,44 @@ export class WebServerPlugin implements TestRunnerPlugin { if (this._options.wait?.stdout || this._options.wait?.stderr) this._waitForStdioPromise = new ManualPromise(); - let stdoutWaitCollector = this._options.wait?.stdout ? '' : undefined; - let stderrWaitCollector = this._options.wait?.stderr ? '' : undefined; - - const resolveStdioPromise = () => { - stderrWaitCollector = undefined; - stdoutWaitCollector = undefined; - this._waitForStdioPromise?.resolve(); + const stdioWaitCollectors = { + stdout: this._options.wait?.stdout ? '' : undefined, + stderr: this._options.wait?.stderr ? '' : undefined, }; - launchedProcess.stderr!.on('data', data => { - if (stderrWaitCollector !== undefined) { - stderrWaitCollector += data.toString(); - if (this._options.wait?.stderr?.test(stderrWaitCollector)) - resolveStdioPromise(); - } + launchedProcess.stdout!.on('data', data => { + if (debugWebServer.enabled || this._options.stdout === 'pipe') + this._reporter!.onStdOut?.(prefixOutputLines(data.toString(), this._options.name)); + }); + launchedProcess.stderr!.on('data', data => { if (debugWebServer.enabled || (this._options.stderr === 'pipe' || !this._options.stderr)) this._reporter!.onStdErr?.(prefixOutputLines(data.toString(), this._options.name)); }); - launchedProcess.stdout!.on('data', data => { - if (stdoutWaitCollector !== undefined) { - stdoutWaitCollector += data.toString(); - if (this._options.wait?.stdout?.test(stdoutWaitCollector)) - resolveStdioPromise(); - } + const resolveStdioPromise = () => { + stdioWaitCollectors.stdout = undefined; + stdioWaitCollectors.stderr = undefined; + this._waitForStdioPromise?.resolve(); + }; - if (debugWebServer.enabled || this._options.stdout === 'pipe') - this._reporter!.onStdOut?.(prefixOutputLines(data.toString(), this._options.name)); - }); + for (const stdio of ['stdout', 'stderr'] as const) { + launchedProcess[stdio]!.on('data', data => { + if (!this._options.wait?.[stdio] || stdioWaitCollectors[stdio] === undefined) + return; + stdioWaitCollectors[stdio] += data.toString(); + this._options.wait[stdio].lastIndex = 0; + const result = this._options.wait[stdio].exec(stdioWaitCollectors[stdio]); + if (result) { + for (const [key, value] of Object.entries(result.groups || {})) + process.env[key.toUpperCase()] = value; + resolveStdioPromise(); + } + }); + } } private async _waitForProcess() { - // options.time is immune to the timeout. - if (this._options.wait?.time) - await new Promise(resolve => setTimeout(resolve, this._options.wait!.time)); - if (!this._isAvailableCallback && !this._waitForStdioPromise) { this._processExitedPromise.catch(() => {}); return; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index b5babaa8d..ec6a14c09 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -10193,11 +10193,21 @@ interface TestConfigWebServer { stdout?: "pipe"|"ignore"; /** - * Consider command started only when given output has been produced or a time in milliseconds has passed. + * Consider command started only when given output has been produced. */ wait?: { + /** + * Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the + * environment, for example /Listening on port (?\\d+)/ will store the port number in + * `process.env['MY_SERVER_PORT']`. + */ stdout?: RegExp; + /** + * Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the + * environment, for example /Listening on port (?\\d+)/ will store the port number in + * `process.env['MY_SERVER_PORT']`. + */ stderr?: RegExp; time?: number; diff --git a/tests/playwright-test/web-server.spec.ts b/tests/playwright-test/web-server.spec.ts index 3cb120e14..c8285d96a 100644 --- a/tests/playwright-test/web-server.spec.ts +++ b/tests/playwright-test/web-server.spec.ts @@ -947,80 +947,59 @@ test('should throw helpful error when command is empty', async ({ runInlineTest expect(result.output).toContain('config.webServer.command cannot be empty'); }); -test('should wait for stdout', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'test.spec.ts': ` - import { test, expect } from '@playwright/test'; - test('pass', async ({}) => {}); - `, - 'server.js': ` - setTimeout(() => { console.log('server started'); }, 1000); - setTimeout(() => {}, 100000); - `, - 'playwright.config.ts': ` - module.exports = { - webServer: [ - { - command: 'node server.js', - stdout: 'pipe', - wait: { stdout: /started/ }, - } - ], - }; - `, - }, undefined); - expect(result.exitCode).toBe(0); - expect(result.output).toContain('server started'); -}); - -test('should wait for stderr', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'test.spec.ts': ` - import { test, expect } from '@playwright/test'; - test('pass', async ({}) => {}); - `, - 'server.js': ` - setTimeout(() => { console.error('server started'); }, 1000); - setTimeout(() => {}, 100000); - `, - 'playwright.config.ts': ` - module.exports = { - webServer: [{ - command: 'node server.js', - stdout: 'pipe', - wait: { stderr: /started/ }, - }], - }; - `, - }, undefined); - expect(result.exitCode).toBe(0); - expect(result.output).toContain('server started'); -}); +for (const stdio of ['stdout', 'stderr']) { + test(`should wait for ${stdio}`, async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'test.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({}) => {}); + `, + 'server.js': ` + setTimeout(() => { console.${stdio === 'stdout' ? 'log' : 'error'}('server started'); }, 1000); + setTimeout(() => {}, 100000); + `, + 'playwright.config.ts': ` + module.exports = { + webServer: [ + { + command: 'node server.js', + stdout: 'pipe', + wait: { ${stdio}: /started/ }, + } + ], + }; + `, + }, undefined); + expect(result.exitCode).toBe(0); + expect(result.output).toContain('server started'); + }); -test('should wait for time', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'test.spec.ts': ` - import { test, expect } from '@playwright/test'; - test('pass', async ({}) => { - console.log('TEST: ' + Date.now()); - }); - `, - 'server.js': ` - console.log('SETUP: ' + Date.now()); - setTimeout(() => {}, 100000); - `, - 'playwright.config.ts': ` - module.exports = { - webServer: [{ - command: 'node server.js', - stdout: 'pipe', - wait: { time: 2000 }, - }], - }; - `, - }, undefined); - const [, setupTime] = /SETUP: (\d+)/.exec(result.output)!; - const [, testTime] = /TEST: (\d+)/.exec(result.output)!; - expect(+testTime - +setupTime).toBeGreaterThan(2000); - expect(result.exitCode).toBe(0); -}); + test(`should wait for ${stdio} w/group`, async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'test.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({}) => { + console.log('My server port is ' + process.env['MY_SERVER_PORT']); + }); + `, + 'server.js': ` + setTimeout(() => { console.${stdio === 'stdout' ? 'log' : 'error'}('Listening on port 123'); }, 1000); + setTimeout(() => {}, 100000); + `, + 'playwright.config.ts': ` + module.exports = { + webServer: [ + { + command: 'node server.js', + stdout: 'pipe', + wait: { ${stdio}: /Listening on port (?\\d+)/ }, + } + ], + }; + `, + }, undefined); + expect(result.exitCode).toBe(0); + expect(result.output).toContain('Listening on port 123'); + expect(result.output).toContain('My server port is 123'); + }); +} From 7c5ffb6716bb16c3511f1a2c2c95851f7878f194 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 12 Nov 2025 13:09:52 -0800 Subject: [PATCH 210/250] chore(mcp): remove --allow-origins param (#38202) --- packages/playwright/src/mcp/browser/config.ts | 23 ---- .../playwright/src/mcp/browser/context.ts | 27 ---- packages/playwright/src/mcp/config.d.ts | 12 -- packages/playwright/src/mcp/program.ts | 4 +- tests/mcp/request-blocking.spec.ts | 127 ------------------ 5 files changed, 1 insertion(+), 192 deletions(-) delete mode 100644 tests/mcp/request-blocking.spec.ts diff --git a/packages/playwright/src/mcp/browser/config.ts b/packages/playwright/src/mcp/browser/config.ts index 551c2b774..128801847 100644 --- a/packages/playwright/src/mcp/browser/config.ts +++ b/packages/playwright/src/mcp/browser/config.ts @@ -31,8 +31,6 @@ type ViewportSize = { width: number; height: number }; export type CLIOptions = { allowedHosts?: string[]; - allowedOrigins?: string[]; - blockedOrigins?: string[]; blockServiceWorkers?: boolean; browser?: string; caps?: string[]; @@ -80,10 +78,6 @@ export const defaultConfig: FullConfig = { viewport: null, }, }, - network: { - allowedOrigins: undefined, - blockedOrigins: undefined, - }, server: {}, saveTrace: false, timeouts: { @@ -100,7 +94,6 @@ export type FullConfig = Config & { launchOptions: NonNullable; contextOptions: NonNullable; }, - network: NonNullable, saveTrace: boolean; server: NonNullable, timeouts: { @@ -228,10 +221,6 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { allowedHosts: cliOptions.allowedHosts, }, capabilities: cliOptions.caps as ToolCapability[], - network: { - allowedOrigins: cliOptions.allowedOrigins, - blockedOrigins: cliOptions.blockedOrigins, - }, saveSession: cliOptions.saveSession, saveTrace: cliOptions.saveTrace, saveVideo: cliOptions.saveVideo, @@ -252,8 +241,6 @@ 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); options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER); options.caps = commaSeparatedList(process.env.PLAYWRIGHT_MCP_CAPS); @@ -371,10 +358,6 @@ function mergeConfig(base: FullConfig, overrides: Config): FullConfig { ...pickDefined(base), ...pickDefined(overrides), browser, - network: { - ...pickDefined(base.network), - ...pickDefined(overrides.network), - }, server: { ...pickDefined(base.server), ...pickDefined(overrides.server), @@ -386,12 +369,6 @@ function mergeConfig(base: FullConfig, overrides: Config): FullConfig { } 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; diff --git a/packages/playwright/src/mcp/browser/context.ts b/packages/playwright/src/mcp/browser/context.ts index f3d9208d0..96645717c 100644 --- a/packages/playwright/src/mcp/browser/context.ts +++ b/packages/playwright/src/mcp/browser/context.ts @@ -202,20 +202,6 @@ export class Context { 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(originOrHostGlob(origin), route => route.continue()); - } - - if (this.config.network?.blockedOrigins?.length) { - for (const origin of this.config.network.blockedOrigins) - await context.route(originOrHostGlob(origin), route => route.abort('blockedbyclient')); - } - } - async ensureBrowserContext(): Promise { const { browserContext } = await this._ensureBrowserContext(); return browserContext; @@ -240,7 +226,6 @@ export class Context { selectors.setTestIdAttribute(this.config.testIdAttribute); 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()) @@ -267,18 +252,6 @@ export class Context { } } -function originOrHostGlob(originOrHost: string) { - try { - const url = new URL(originOrHost); - // localhost:1234 will parse as protocol 'localhost:' and 'null' origin. - if (url.origin !== 'null') - return `${url.origin}/**`; - } catch { - } - // Support for legacy host-only mode. - return `*://${originOrHost}/**`; -} - export class InputRecorder { private _context: Context; private _browserContext: playwright.BrowserContext; diff --git a/packages/playwright/src/mcp/config.d.ts b/packages/playwright/src/mcp/config.d.ts index 93d0bc03a..e56ed672b 100644 --- a/packages/playwright/src/mcp/config.d.ts +++ b/packages/playwright/src/mcp/config.d.ts @@ -142,18 +142,6 @@ export type Config = { */ 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[]; - }; - /** * Specify the attribute to use for test ids, defaults to "data-testid". */ diff --git a/packages/playwright/src/mcp/program.ts b/packages/playwright/src/mcp/program.ts index 4a8a7ecf1..ee128cdb5 100644 --- a/packages/playwright/src/mcp/program.ts +++ b/packages/playwright/src/mcp/program.ts @@ -22,7 +22,7 @@ import { colors, ProgramOption } from 'playwright-core/lib/utilsBundle'; import { registry } from 'playwright-core/lib/server'; import * as mcpServer from './sdk/server'; -import { commaSeparatedList, dotenvFileLoader, headerParser, numberParser, resolutionParser, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; +import { commaSeparatedList, dotenvFileLoader, headerParser, numberParser, resolutionParser, resolveCLIConfig } from './browser/config'; import { setupExitWatchdog } from './browser/watchdog'; import { contextFactory } from './browser/browserContextFactory'; import { ProxyBackend } from './sdk/proxyBackend'; @@ -35,8 +35,6 @@ import type { MCPProvider } from './sdk/proxyBackend'; export function decorateCommand(command: Command, version: string) { 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. Pass \'*\' to disable the host check.', 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.') .option('--caps ', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList) diff --git a/tests/mcp/request-blocking.spec.ts b/tests/mcp/request-blocking.spec.ts deleted file mode 100644 index a2ca31879..000000000 --- a/tests/mcp/request-blocking.spec.ts +++ /dev/null @@ -1,127 +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 { 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 (hostname)', 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('blocked works (origin)', async ({ startClient }) => { - const { client } = await startClient({ - args: ['--blocked-origins', 'https://microsoft.com;https://example.com;https://playwright.dev'] - }); - const result = await fetchPage(client, 'https://example.com/'); - expect(result).toMatch(BLOCK_MESSAGE); -}); - -test('allowed works (hostname)', 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('allowed works (origin)', async ({ server, startClient }) => { - server.setContent('/ppp', 'content:PPP', 'text/html'); - const { client } = await startClient({ - args: ['--allowed-origins', `https://microsoft.com;${new URL(server.PREFIX).origin};https://playwright.dev`] - }); - const result = await fetchPage(client, server.PREFIX + '/ppp'); - expect(result).toContain('content:PPP'); -}); - -test('blocked takes precedence (hostname)', 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('blocked takes precedence (origin)', async ({ startClient }) => { - const { client } = await startClient({ - args: [ - '--blocked-origins', 'https://example.com', - '--allowed-origins', 'https://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 (hostname)', 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('allowed without blocked blocks all non-explicitly specified origins (origin)', async ({ startClient }) => { - const { client } = await startClient({ - args: ['--allowed-origins', 'https://playwright.dev'], - }); - const result = await fetchPage(client, 'https://example.com/'); - expect(result).toMatch(BLOCK_MESSAGE); -}); - -test('blocked without allowed allows non-explicitly specified origins (hostname)', 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'); -}); - -test('blocked without allowed allows non-explicitly specified origins (origin)', async ({ server, startClient }) => { - server.setContent('/ppp', 'content:PPP', 'text/html'); - const { client } = await startClient({ - args: ['--blocked-origins', 'https://example.com'], - }); - const result = await fetchPage(client, server.PREFIX + '/ppp'); - expect(result).toContain('content:PPP'); -}); From 34a09f37c7f83538fc5799c76f52f29180964938 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 13 Nov 2025 00:51:32 +0000 Subject: [PATCH 211/250] chore: remove mdb, replace by `sendMessage` ipc into test worker (#38170) --- packages/playwright/src/common/ipc.ts | 12 +- packages/playwright/src/index.ts | 6 +- .../src/mcp/browser/browserServerBackend.ts | 2 +- packages/playwright/src/mcp/sdk/exports.ts | 1 - packages/playwright/src/mcp/sdk/mdb.ts | 143 ------- .../playwright/src/mcp/sdk/proxyBackend.ts | 4 +- packages/playwright/src/mcp/sdk/server.ts | 5 +- .../playwright/src/mcp/test/browserBackend.ts | 83 ++-- .../playwright/src/mcp/test/testBackend.ts | 76 +++- .../playwright/src/mcp/test/testContext.ts | 35 +- packages/playwright/src/program.ts | 16 +- packages/playwright/src/runner/dispatcher.ts | 28 +- .../playwright/src/runner/failureTracker.ts | 7 +- packages/playwright/src/runner/testRunner.ts | 3 +- packages/playwright/src/runner/testServer.ts | 2 +- packages/playwright/src/runner/workerHost.ts | 6 +- packages/playwright/src/worker/testInfo.ts | 6 +- packages/playwright/src/worker/workerMain.ts | 13 +- tests/mcp/mdb.spec.ts | 361 ------------------ 19 files changed, 213 insertions(+), 596 deletions(-) delete mode 100644 packages/playwright/src/mcp/sdk/mdb.ts delete mode 100644 tests/mcp/mdb.spec.ts diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts index 0ddcf9ff1..4d9ca7f84 100644 --- a/packages/playwright/src/common/ipc.ts +++ b/packages/playwright/src/common/ipc.ts @@ -87,8 +87,18 @@ export type AttachmentPayload = { export type TestInfoErrorImpl = TestInfoError; export type TestPausedPayload = { + testId: string; errors: TestInfoErrorImpl[]; - extraData: any; +}; + +export type CustomMessageRequestPayload = { + testId: string; + request: any; +}; + +export type CustomMessageResponsePayload = { + response: any; + error?: TestInfoErrorImpl; }; export type TestEndPayload = { diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 82144035a..f398caaf3 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 { runBrowserBackendOnTestPause } from './mcp/test/browserBackend'; +import { createCustomMessageHandler } from './mcp/test/browserBackend'; import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; import type { ContextReuseMode } from './common/config'; @@ -417,14 +417,14 @@ const playwrightFixtures: Fixtures = ({ attachConnectedHeaderIfNeeded(testInfo, browserImpl); if (!_reuseContext) { const { context, close } = await _contextFactory(); - (testInfo as TestInfoImpl)._onDidPauseTestCallback = () => runBrowserBackendOnTestPause(testInfo, context); + (testInfo as TestInfoImpl)._onCustomMessageCallback = createCustomMessageHandler(testInfo, context); await use(context); await close(); return; } const context = await browserImpl._wrapApiCall(() => browserImpl._newContextForReuse(), { internal: true }); - (testInfo as TestInfoImpl)._onDidPauseTestCallback = () => runBrowserBackendOnTestPause(testInfo, context); + (testInfo as TestInfoImpl)._onCustomMessageCallback = createCustomMessageHandler(testInfo, context); 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 }); diff --git a/packages/playwright/src/mcp/browser/browserServerBackend.ts b/packages/playwright/src/mcp/browser/browserServerBackend.ts index e9ec5c094..9387f86f8 100644 --- a/packages/playwright/src/mcp/browser/browserServerBackend.ts +++ b/packages/playwright/src/mcp/browser/browserServerBackend.ts @@ -40,7 +40,7 @@ export class BrowserServerBackend implements ServerBackend { this._tools = filteredTools(config); } - async initialize(server: mcpServer.Server, clientInfo: mcpServer.ClientInfo): Promise { + async initialize(clientInfo: mcpServer.ClientInfo): Promise { this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, clientInfo) : undefined; this._context = new Context({ config: this._config, diff --git a/packages/playwright/src/mcp/sdk/exports.ts b/packages/playwright/src/mcp/sdk/exports.ts index 92c0c176b..b30e94b9b 100644 --- a/packages/playwright/src/mcp/sdk/exports.ts +++ b/packages/playwright/src/mcp/sdk/exports.ts @@ -19,4 +19,3 @@ export * from './proxyBackend'; export * from './server'; export * from './tool'; export * from './http'; -export * from './mdb'; diff --git a/packages/playwright/src/mcp/sdk/mdb.ts b/packages/playwright/src/mcp/sdk/mdb.ts deleted file mode 100644 index 06f4263ff..000000000 --- a/packages/playwright/src/mcp/sdk/mdb.ts +++ /dev/null @@ -1,143 +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 { debug } from 'playwright-core/lib/utilsBundle'; -import { ManualPromise } from 'playwright-core/lib/utils'; - -import * as mcpBundle from './bundle'; -import * as mcpServer from './server'; -import * as mcpHttp from './http'; - -import type { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; -import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; - -const mdbDebug = debug('pw:mcp:mdb'); -const errorsDebug = debug('pw:mcp:errors'); - -export type MDBPushClientCallback = (mcpUrl: string, introMessage?: string) => Promise; -export type MDBServerBackendFactory = Omit & { - create: (pushClient: MDBPushClientCallback) => mcpServer.ServerBackend; -}; - -export class MDBBackend implements mcpServer.ServerBackend { - private _onPauseClient: { client: Client, tools: mcpServer.Tool[], transport: StreamableHTTPClientTransport } | undefined; - private _interruptPromise: ManualPromise | undefined; - private _mainBackend: mcpServer.ServerBackend; - private _clientInfo: mcpServer.ClientInfo | undefined; - private _progress: mcpServer.CallToolResult['content'] = []; - private _progressCallback: mcpServer.ProgressCallback; - - constructor(mainBackendFactory: MDBServerBackendFactory) { - this._mainBackend = mainBackendFactory.create(this._createOnPauseClient.bind(this)); - this._progressCallback = (params: mcpServer.ProgressParams) => { - if (params.message) - this._progress.push({ type: 'text', text: params.message }); - }; - } - - async initialize(server: mcpServer.Server, clientInfo: mcpServer.ClientInfo): Promise { - if (!this._clientInfo) { - this._clientInfo = clientInfo; - await this._mainBackend.initialize?.(server, clientInfo); - } - } - - async listTools(): Promise { - return await this._mainBackend.listTools(); - } - - async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise { - if (this._onPauseClient?.tools.find(tool => tool.name === name)) { - const result = await this._onPauseClient.client.callTool({ - name, - arguments: args, - }) as mcpServer.CallToolResult; - await this._mainBackend.afterCallTool?.(name, args, result); - return result; - } - - await this._onPauseClient?.transport.terminateSession().catch(errorsDebug); - await this._onPauseClient?.client.close().catch(errorsDebug); - this._onPauseClient = undefined; - - const resultPromise = new ManualPromise(); - const interruptPromise = new ManualPromise(); - this._interruptPromise = interruptPromise; - - this._mainBackend.callTool(name, args, this._progressCallback).then(result => { - resultPromise.resolve(result as mcpServer.CallToolResult); - }).catch(e => { - resultPromise.resolve({ content: [{ type: 'text', text: String(e) }], isError: true }); - }); - - const result = await Promise.race([interruptPromise, resultPromise]); - if (interruptPromise.isDone()) - mdbDebug('client call intercepted', result); - else - mdbDebug('client call result', result); - result.content.unshift(...this._progress); - this._progress.length = 0; - return result; - } - - private async _createOnPauseClient(mcpUrl: string, introMessage?: string) { - if (this._onPauseClient) - await this._onPauseClient.client.close().catch(errorsDebug); - - this._onPauseClient = await this._createClient(mcpUrl); - - this._interruptPromise?.resolve({ - content: [{ - type: 'text', - text: introMessage || '', - }], - }); - this._interruptPromise = undefined; - } - - private async _createClient(url: string): Promise<{ client: Client, tools: mcpServer.Tool[], transport: StreamableHTTPClientTransport }> { - const client = new mcpBundle.Client({ name: 'Interrupting client', version: '0.0.0' }, { capabilities: { roots: {} } }); - client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] })); - client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({})); - client.setNotificationHandler(mcpBundle.ProgressNotificationSchema, notification => { - if (notification.method === 'notifications/progress') { - const { message } = notification.params; - if (message) - this._progress.push({ type: 'text', text: message }); - } - }); - const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(url)); - await client.connect(transport); - const { tools } = await client.listTools(); - return { client, tools, transport }; - } -} - -// TODO: add all options from mcpHttp.startHttpServer. -export async function runMainBackend(backendFactory: MDBServerBackendFactory, options?: { port?: number }): Promise { - const mdbBackend = new MDBBackend(backendFactory); - const factory: mcpServer.ServerBackendFactory = { - ...backendFactory, - create: () => mdbBackend - }; - - if (options?.port !== undefined) { - const httpServer = await mcpHttp.startHttpServer(options); - return await mcpHttp.installHttpTransport(httpServer, factory, true); - } - - await mcpServer.connect(factory, new mcpBundle.StdioServerTransport(), false); -} diff --git a/packages/playwright/src/mcp/sdk/proxyBackend.ts b/packages/playwright/src/mcp/sdk/proxyBackend.ts index c55e5f97d..ba8fc790f 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, ClientInfo, Server } from './server'; +import type { ServerBackend, ClientInfo } 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'; @@ -43,7 +43,7 @@ export class ProxyBackend implements ServerBackend { this._contextSwitchTool = this._defineContextSwitchTool(); } - async initialize(server: Server, clientInfo: ClientInfo): Promise { + async initialize(clientInfo: ClientInfo): Promise { this._clientInfo = clientInfo; } diff --git a/packages/playwright/src/mcp/sdk/server.ts b/packages/playwright/src/mcp/sdk/server.ts index 1488f2990..d6a60e3e1 100644 --- a/packages/playwright/src/mcp/sdk/server.ts +++ b/packages/playwright/src/mcp/sdk/server.ts @@ -42,9 +42,8 @@ export type ProgressParams = { message?: string, progress?: number, total?: numb export type ProgressCallback = (params: ProgressParams) => void; export interface ServerBackend { - initialize?(server: Server, clientInfo: ClientInfo): Promise; + initialize?(clientInfo: ClientInfo): Promise; listTools(): Promise; - afterCallTool?(name: string, args: CallToolRequest['params']['arguments'], result: CallToolResult): Promise; callTool(name: string, args: CallToolRequest['params']['arguments'], progress: ProgressCallback): Promise; serverClosed?(server: Server): void; } @@ -135,7 +134,7 @@ const initializeServer = async (server: Server, backend: ServerBackend, runHeart timestamp: Date.now(), }; - await backend.initialize?.(server, clientInfo); + await backend.initialize?.(clientInfo); if (runHeartbeat) startHeartbeat(server); }; diff --git a/packages/playwright/src/mcp/test/browserBackend.ts b/packages/playwright/src/mcp/test/browserBackend.ts index f60a9844a..a362acda6 100644 --- a/packages/playwright/src/mcp/test/browserBackend.ts +++ b/packages/playwright/src/mcp/test/browserBackend.ts @@ -15,23 +15,75 @@ */ import * as mcp from '../sdk/exports'; -import { defaultConfig, FullConfig } from '../browser/config'; +import { defaultConfig } from '../browser/config'; import { BrowserServerBackend } from '../browser/browserServerBackend'; import { Tab } from '../browser/tab'; +import { stripAnsiEscapes } from '../../util'; import type * as playwright from '../../../index'; import type { Page } from '../../../../playwright-core/src/client/page'; import type { BrowserContextFactory } from '../browser/browserContextFactory'; import type { TestInfo } from '../../../test'; -export type TestPausedExtraData = { - mcpUrl: string; - contextState: string; +export type BrowserMCPRequest = { + initialize?: { clientInfo: mcp.ClientInfo }, + listTools?: {}, + callTool?: { name: string, arguments: mcp.CallToolRequest['params']['arguments'] }, + close?: {}, }; -export async function runBrowserBackendOnTestPause(testInfo: TestInfo, context: playwright.BrowserContext) { +export type BrowserMCPResponse = { + initialize?: { pausedMessage: string }, + listTools?: mcp.Tool[], + callTool?: mcp.CallToolResult, + close?: {}, +}; + +export function createCustomMessageHandler(testInfo: TestInfo, context: playwright.BrowserContext) { + let backend: BrowserServerBackend | undefined; + return async (data: BrowserMCPRequest): Promise => { + if (data.initialize) { + if (backend) + throw new Error('MCP backend is already initialized'); + backend = new BrowserServerBackend({ ...defaultConfig, capabilities: ['testing'] }, identityFactory(context)); + await backend.initialize(data.initialize.clientInfo); + const pausedMessage = await generatePausedMessage(testInfo, context); + return { initialize: { pausedMessage } }; + } + + if (data.listTools) { + if (!backend) + throw new Error('MCP backend is not initialized'); + return { listTools: await backend.listTools() }; + } + + if (data.callTool) { + if (!backend) + throw new Error('MCP backend is not initialized'); + return { callTool: await backend.callTool(data.callTool.name, data.callTool.arguments) }; + } + + if (data.close) { + backend?.serverClosed(); + backend = undefined; + return { close: {} }; + } + + throw new Error('Unknown MCP request'); + }; +} + +async function generatePausedMessage(testInfo: TestInfo, context: playwright.BrowserContext) { const lines: string[] = []; + if (testInfo.errors.length) { + lines.push(`### Paused on error:`); + for (const error of testInfo.errors) + lines.push(stripAnsiEscapes(error.message || '')); + } else { + lines.push(`### 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'; @@ -57,24 +109,11 @@ export async function runBrowserBackendOnTestPause(testInfo: TestInfo, context: ); } - const config: FullConfig = { - ...defaultConfig, - capabilities: ['testing'], - }; + lines.push(''); + if (testInfo.errors.length) + lines.push(`### Task`, `Try recovering from the error prior to continuing`); - const factory: mcp.ServerBackendFactory = { - name: 'Playwright', - nameInConfig: 'playwright', - version: '0.0.0', - create: () => new BrowserServerBackend(config, identityFactory(context)) - }; - const httpServer = await mcp.startHttpServer({ port: 0 }); - const mcpUrl = await mcp.installHttpTransport(httpServer, factory, true); - const dispose = async () => { - await new Promise(cb => httpServer.close(cb)); - }; - const extraData = { mcpUrl, contextState: lines.join('\n') } as TestPausedExtraData; - return { extraData, dispose }; + return lines.join('\n'); } function identityFactory(browserContext: playwright.BrowserContext): BrowserContextFactory { diff --git a/packages/playwright/src/mcp/test/testBackend.ts b/packages/playwright/src/mcp/test/testBackend.ts index 60dbd8357..0f87aa5ba 100644 --- a/packages/playwright/src/mcp/test/testBackend.ts +++ b/packages/playwright/src/mcp/test/testBackend.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import { debug } from 'playwright-core/lib/utilsBundle'; +import { ManualPromise } from 'playwright-core/lib/utils'; + import * as mcp from '../sdk/exports'; import { TestContext } from './testContext'; import * as testTools from './testTools.js'; @@ -24,6 +27,9 @@ import { resolveConfigLocation } from '../../common/configLoader'; import { parseResponse } from '../browser/response'; import type { TestTool } from './testTool'; +import type { BrowserMCPRequest, BrowserMCPResponse } from './browserBackend'; + +const errorsDebug = debug('pw:mcp:errors'); export class TestServerBackend implements mcp.ServerBackend { readonly name = 'Playwright'; @@ -41,13 +47,40 @@ export class TestServerBackend implements mcp.ServerBackend { ]; private _context: TestContext; private _configOption: string | undefined; + private _clientInfo: mcp.ClientInfo | undefined; + private _onPauseClient: { sendMessage: (request: BrowserMCPRequest) => Promise, tools: mcp.Tool[] } | undefined; + private _interruptPromise: ManualPromise | undefined; + private _progress: mcp.CallToolResult['content'] = []; + private _progressCallback: mcp.ProgressCallback; - constructor(configOption: string | undefined, pushClient: mcp.MDBPushClientCallback, options?: { muteConsole?: boolean, headless?: boolean }) { - this._context = new TestContext(pushClient, options); + constructor(configOption: string | undefined, options?: { muteConsole?: boolean, headless?: boolean }) { + this._context = new TestContext(this._pushClient.bind(this), options); this._configOption = configOption; + this._progressCallback = (params: mcp.ProgressParams) => { + if (params.message) + this._progress.push({ type: 'text', text: params.message }); + }; + } + + private async _pushClient(sendMessage: (request: BrowserMCPRequest) => Promise) { + try { + const initializeResponse = await sendMessage({ initialize: { clientInfo: this._clientInfo! } }); + const listToolsResponse = await sendMessage({ listTools: {} }); + const tools = listToolsResponse.listTools!; + this._onPauseClient = { sendMessage, tools }; + this._interruptPromise?.resolve({ + content: [{ + type: 'text', + text: initializeResponse.initialize!.pausedMessage, + }], + }); + this._interruptPromise = undefined; + } catch { + } } - async initialize(server: mcp.Server, clientInfo: mcp.ClientInfo): Promise { + async initialize(clientInfo: mcp.ClientInfo): Promise { + this._clientInfo = clientInfo; const rootPath = mcp.firstRootPath(clientInfo); if (this._configOption) { @@ -70,20 +103,41 @@ export class TestServerBackend implements mcp.ServerBackend { ]; } - async afterCallTool(name: string, args: mcp.CallToolRequest['params']['arguments'], result: mcp.CallToolResult) { - if (!browserTools.find(tool => tool.schema.name === name)) - return; - const response = parseResponse(result); - if (response && !response.isError && response.code && typeof args?.['intent'] === 'string') - this._context.generatorJournal?.logStep(args['intent'], response.code); + async callTool(name: string, args: mcp.CallToolRequest['params']['arguments']): Promise { + if (this._onPauseClient?.tools.find(tool => tool.name === name)) { + const callToolRespone = await this._onPauseClient.sendMessage({ callTool: { name, arguments: args } }); + const result = callToolRespone.callTool!; + const response = parseResponse(result); + if (response && !response.isError && response.code && typeof args?.['intent'] === 'string') + this._context.generatorJournal?.logStep(args['intent'], response.code); + return result; + } + + await this._onPauseClient?.sendMessage({ close: {} }).catch(errorsDebug); + this._onPauseClient = undefined; + + const resultPromise = new ManualPromise(); + const interruptPromise = new ManualPromise(); + this._interruptPromise = interruptPromise; + + this._callTestTool(name, args).then(result => { + resultPromise.resolve(result); + }).catch(e => { + resultPromise.resolve({ content: [{ type: 'text', text: String(e) }], isError: true }); + }); + + const result = await Promise.race([interruptPromise, resultPromise]); + result.content.unshift(...this._progress); + this._progress.length = 0; + return result; } - async callTool(name: string, args: mcp.CallToolRequest['params']['arguments'], progress: mcp.ProgressCallback): Promise { + private async _callTestTool(name: string, args: mcp.CallToolRequest['params']['arguments']): Promise { 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, progress); + return await tool.handle(this._context!, parsedArguments, this._progressCallback); } serverClosed() { diff --git a/packages/playwright/src/mcp/test/testContext.ts b/packages/playwright/src/mcp/test/testContext.ts index d556d806a..63d5aa031 100644 --- a/packages/playwright/src/mcp/test/testContext.ts +++ b/packages/playwright/src/mcp/test/testContext.ts @@ -23,13 +23,12 @@ import { noColors, escapeRegExp } from 'playwright-core/lib/utils'; import { terminalScreen } from '../../reporters/base'; import ListReporter from '../../reporters/list'; import { StringWriteStream } from './streams'; -import { stripAnsiEscapes, fileExistsAsync } from '../../util'; +import { fileExistsAsync } from '../../util'; import { TestRunner, TestRunnerEvent } from '../../runner/testRunner'; import { ensureSeedFile, seedProject } from './seed'; import type { ConfigLocation } from '../../common/config'; -import type { ProgressCallback, MDBPushClientCallback } from '../sdk/exports'; -import type { TestPausedExtraData } from './browserBackend'; +import type { ProgressCallback } from '../sdk/exports'; export type SeedFile = { file: string; @@ -72,8 +71,10 @@ ${step.code} } } +type PushClientCallback = (sendMessage: (request: any) => Promise) => Promise; + export class TestContext { - private _pushClient: MDBPushClientCallback; + private _pushClient: PushClientCallback; private _testRunner: TestRunner | undefined; readonly options?: { muteConsole?: boolean, headless?: boolean }; readonly computedHeaded: boolean; @@ -81,7 +82,7 @@ export class TestContext { rootPath!: string; generatorJournal: GeneratorJournal | undefined; - constructor(pushClient: MDBPushClientCallback, options?: { muteConsole?: boolean, headless?: boolean }) { + constructor(pushClient: PushClientCallback, options?: { muteConsole?: boolean, headless?: boolean }) { this._pushClient = pushClient; this.options = options; if (options?.headless !== undefined) @@ -104,25 +105,13 @@ export class TestContext { await this._testRunner.stopTests(); const testRunner = new TestRunner(this.configLocation!, {}); await testRunner.initialize({}); - this._testRunner = testRunner; - testRunner.on(TestRunnerEvent.TestFilesChanged, testFiles => { - this._testRunner?.emit(TestRunnerEvent.TestFilesChanged, testFiles); - }); testRunner.on(TestRunnerEvent.TestPaused, params => { - const extraData = params.extraData as TestPausedExtraData; - const introMessage: string[] = []; - if (params.errors.length) { - introMessage.push(`### Paused on error:`); - for (const error of params.errors) - introMessage.push(stripAnsiEscapes(error.message || '')); - } else { - introMessage.push(`### Paused at end of test. ready for interaction`); - } - introMessage.push(extraData.contextState); - introMessage.push(''); - if (params.errors.length) - introMessage.push(`### Task`, `Try recovering from the error prior to continuing`); - void this._pushClient(extraData.mcpUrl, introMessage.join('\n')); + void this._pushClient(async (request: any) => { + const response = await params.sendMessage({ request }); + if (response.error) + throw new Error(response.error.message); + return response.response; + }); }); this._testRunner = testRunner; return testRunner; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 5e58a1026..922ea766e 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -33,7 +33,7 @@ import * as testServer from './runner/testServer'; import { runWatchModeLoop } from './runner/watchMode'; import { runAllTestsWithConfig, TestRunner } from './runner/testRunner'; import { createErrorCollectingReporter } from './runner/reporters'; -import { MDBServerBackendFactory, runMainBackend } from './mcp/sdk/exports'; +import * as mcp from './mcp/sdk/exports'; import { TestServerBackend } from './mcp/test/testBackend'; import { decorateCommand } from './mcp/program'; import { setupExitWatchdog } from './mcp/browser/watchdog'; @@ -162,20 +162,14 @@ function addTestMCPServerCommand(program: Command) { command.option('--port ', 'port to listen on for SSE transport.'); command.action(async options => { setupExitWatchdog(); - const backendFactory: MDBServerBackendFactory = { + const factory: mcp.ServerBackendFactory = { name: 'Playwright Test Runner', nameInConfig: 'playwright-test-runner', version: packageJSON.version, - create: pushClient => new TestServerBackend(options.config, pushClient, { muteConsole: options.port === undefined, headless: options.headless }), + create: () => new TestServerBackend(options.config, { muteConsole: options.port === undefined, headless: options.headless }), }; - const mdbUrl = await runMainBackend( - backendFactory, - { - port: options.port === undefined ? undefined : +options.port - }, - ); - if (mdbUrl) - console.error('MCP Listening on: ', mdbUrl); + // TODO: add all options from mcp.startHttpServer. + await mcp.start(factory, { port: options.port === undefined ? undefined : +options.port, host: options.host }); }); } diff --git a/packages/playwright/src/runner/dispatcher.ts b/packages/playwright/src/runner/dispatcher.ts index 89d15427b..d2e122fa5 100644 --- a/packages/playwright/src/runner/dispatcher.ts +++ b/packages/playwright/src/runner/dispatcher.ts @@ -21,6 +21,7 @@ import { addSuggestedRebaseline } from './rebase'; import { WorkerHost } from './workerHost'; import { serializeConfig } from '../common/ipc'; import { addLocationAndSnippetToError } from '../reporters/internalReporter'; +import { serializeError } from '../util'; import type { FailureTracker } from './failureTracker'; import type { ProcessExitData } from './processHost'; @@ -577,16 +578,33 @@ class JobDispatcher { eventsHelper.addEventListener(worker, 'stepBegin', this._onStepBegin.bind(this)), eventsHelper.addEventListener(worker, 'stepEnd', this._onStepEnd.bind(this)), eventsHelper.addEventListener(worker, 'attach', this._onAttach.bind(this)), - eventsHelper.addEventListener(worker, 'testPaused', (params: TestPausedPayload) => { - for (const error of params.errors) - addLocationAndSnippetToError(this._config.config, error); - this._failureTracker.onTestPaused?.(params); - }), + eventsHelper.addEventListener(worker, 'testPaused', this._onTestPaused.bind(this, worker)), eventsHelper.addEventListener(worker, 'done', this._onDone.bind(this)), eventsHelper.addEventListener(worker, 'exit', this.onExit.bind(this)), ]; } + private _onTestPaused(worker: WorkerHost, params: TestPausedPayload) { + const sendMessage = async (message: { request: any }) => { + try { + if (this.jobResult.isDone()) + throw new Error('Test has already stopped'); + const response = await worker.sendCustomMessage({ testId: params.testId, request: message.request }); + if (response.error) + addLocationAndSnippetToError(this._config.config, response.error); + return response; + } catch (e) { + const error = serializeError(e); + addLocationAndSnippetToError(this._config.config, error); + return { response: undefined, error }; + } + }; + + for (const error of params.errors) + addLocationAndSnippetToError(this._config.config, error); + this._failureTracker.onTestPaused?.({ ...params, sendMessage }); + } + skipWholeJob(): boolean { // If all the tests in a group are skipped, we report them immediately // without sending anything to a worker. This avoids creating unnecessary worker processes. diff --git a/packages/playwright/src/runner/failureTracker.ts b/packages/playwright/src/runner/failureTracker.ts index e1b518d2b..6e6ef85b9 100644 --- a/packages/playwright/src/runner/failureTracker.ts +++ b/packages/playwright/src/runner/failureTracker.ts @@ -18,6 +18,11 @@ import type { TestResult, TestError } from '../../types/testReporter'; import type { FullConfigInternal, FullProjectInternal } from '../common/config'; import type { Suite, TestCase } from '../common/test'; +export type TestPausedParams = { + errors: TestError[]; + sendMessage: (params: { request: any }) => Promise<{ response: any, error?: TestError }>; +}; + export class FailureTracker { private _config: FullConfigInternal; private _failureCount = 0; @@ -26,7 +31,7 @@ export class FailureTracker { private _topLevelProjects: FullProjectInternal[] = []; private _pauseOnError: boolean; private _pauseAtEnd: boolean; - onTestPaused?: (params: { errors: TestError[], extraData: any }) => void; + onTestPaused?: (params: TestPausedParams) => void; constructor(config: FullConfigInternal, options?: { pauseOnError?: boolean, pauseAtEnd?: boolean }) { this._config = config; diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index 3b3c1450e..8f5dce044 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -39,6 +39,7 @@ import type { ConfigLocation, FullConfigInternal } from '../common/config'; import type { ConfigCLIOverrides } from '../common/ipc'; import type { TestRunnerPluginRegistration } from '../plugins'; import type { AnyReporter } from '../reporters/reporterV2'; +import type { TestPausedParams } from './failureTracker'; export const TestRunnerEvent = { TestFilesChanged: 'testFilesChanged', @@ -47,7 +48,7 @@ export const TestRunnerEvent = { export type TestRunnerEventMap = { [TestRunnerEvent.TestFilesChanged]: [testFiles: string[]]; - [TestRunnerEvent.TestPaused]: [params: { errors: reporterTypes.TestError[], extraData: any }]; + [TestRunnerEvent.TestPaused]: [params: TestPausedParams]; }; export type ListTestsParams = { diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index b613b0656..c1fd0693a 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -108,7 +108,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.TestPaused, params => this._dispatchEvent('testPaused', params)); + this._testRunner.on(TestRunnerEvent.TestPaused, params => this._dispatchEvent('testPaused', { errors: params.errors })); } private async _wireReporter(messageSink: (message: any) => void) { diff --git a/packages/playwright/src/runner/workerHost.ts b/packages/playwright/src/runner/workerHost.ts index 979f56f3a..19ebe0c2b 100644 --- a/packages/playwright/src/runner/workerHost.ts +++ b/packages/playwright/src/runner/workerHost.ts @@ -24,7 +24,7 @@ import { stdioChunkToParams } from '../common/ipc'; import { artifactsFolderName } from '../isomorphic/folders'; import type { TestGroup } from './testGroups'; -import type { RunPayload, SerializedConfig, WorkerInitParams } from '../common/ipc'; +import type { CustomMessageRequestPayload, CustomMessageResponsePayload, RunPayload, SerializedConfig, WorkerInitParams } from '../common/ipc'; let lastWorkerIndex = 0; @@ -90,6 +90,10 @@ export class WorkerHost extends ProcessHost { this.sendMessageNoReply({ method: 'runTestGroup', params: runPayload }); } + async sendCustomMessage(payload: CustomMessageRequestPayload) { + return await this.sendMessage({ method: 'customMessage', params: payload }) as CustomMessageResponsePayload; + } + hash() { return this._hash; } diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index d9f979510..a3eb8f800 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -85,7 +85,7 @@ export class TestInfoImpl implements TestInfo { private readonly _steps: TestStepInternal[] = []; private readonly _stepMap = new Map(); _onDidFinishTestFunctionCallback?: () => Promise; - _onDidPauseTestCallback?: () => Promise<{ extraData: any, dispose: () => Promise }>; + _onCustomMessageCallback?: (data: any) => Promise; _hasNonRetriableError = false; _hasUnhandledError = false; _allowSkips = false; @@ -463,10 +463,8 @@ export class TestInfoImpl implements TestInfo { async _didFinishTestFunction() { const shouldPause = (this._workerParams.pauseAtEnd && !this._isFailure()) || (this._workerParams.pauseOnError && this._isFailure()); if (shouldPause) { - const customHandler = await this._onDidPauseTestCallback?.(); - this._onTestPaused({ errors: this._isFailure() ? this.errors : [], extraData: customHandler?.extraData }); + this._onTestPaused({ testId: this.testId, errors: this._isFailure() ? this.errors : [] }); await this._interruptedPromise; - await customHandler?.dispose(); } await this._onDidFinishTestFunctionCallback?.(); } diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index cb5a33cdc..f1a96cbdd 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, RunPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc'; +import type { CustomMessageRequestPayload, CustomMessageResponsePayload, DonePayload, RunPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc'; import type { Suite, TestCase } from '../common/test'; import type { TestAnnotation } from '../../types/test'; @@ -267,6 +267,17 @@ export class WorkerMain extends ProcessRunner { } } + async customMessage(payload: CustomMessageRequestPayload): Promise { + try { + if (this._currentTest?.testId !== payload.testId) + throw new Error('Test has already stopped'); + const response = await this._currentTest._onCustomMessageCallback?.(payload.request); + return { response }; + } catch (error) { + return { response: {}, error: testInfoError(error) }; + } + } + 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), diff --git a/tests/mcp/mdb.spec.ts b/tests/mcp/mdb.spec.ts deleted file mode 100644 index f8a408a8b..000000000 --- a/tests/mcp/mdb.spec.ts +++ /dev/null @@ -1,361 +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 { 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 * as mcp from '../../packages/playwright/lib/mcp/sdk/exports'; -import * as mcpBundle from '../../packages/playwright/lib/mcp/sdk/bundle'; -import { test, expect } from './fixtures'; - -import type http from 'http'; - -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_restart', - description: 'Restart the process', - inputSchema: expect.any(Object), - }, { - name: 'gdb_bt', - description: 'Print backtrace', - inputSchema: expect.any(Object), - }, { - name: 'gdb_echo', - description: 'Echo a message', - 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!, roots: ' }]); - - 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' }]); - - // Call the new inner tool. - const btResult = await mdbClient.client.callTool({ - name: 'gdb_bt', - arguments: {}, - }); - expect(btResult.content).toEqual([{ type: 'text', text: 'Backtrace' }]); - - await mdbClient.close(); -}); - -test('outer and inner roots available', async () => { - const { mdbUrl } = await startMDBAndCLI(); - const mdbClient = await createMDBClient(mdbUrl, [{ name: 'test', uri: 'file://tmp/' }]); - - expect(await mdbClient.client.callTool({ - name: 'cli_echo', - arguments: { - message: 'Hello, cli!', - }, - })).toEqual({ - content: [{ - type: 'text', - text: 'Echo: Hello, cli!, roots: test=file://tmp/', - }] - }); - - await mdbClient.client.callTool({ - name: 'cli_pause_in_gdb', - arguments: {}, - }); - - expect(await mdbClient.client.callTool({ - name: 'gdb_echo', - arguments: { - message: 'Hello, bt!', - }, - })).toEqual({ - content: [{ - type: 'text', - text: 'Echo: Hello, bt!, roots: test=file://tmp/', - }] - }); - - await mdbClient.close(); -}); - -test('should reset', async () => { - const { mdbUrl, log } = 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' }]); - - // Call the new inner tool. - const btResult = await mdbClient.client.callTool({ - name: 'gdb_bt', - arguments: {}, - }); - expect(btResult.content).toEqual([{ type: 'text', text: 'Backtrace' }]); - - await mdbClient.client.callTool({ - name: 'cli_echo', - arguments: {}, - }); - - await expect.poll(() => log).toEqual([ - 'CLI: initialize', - 'CLI: callTool cli_pause_in_gdb', - 'GDB: listTools', - 'GDB: initialize', - 'GDB: callTool gdb_bt', - 'CLI: afterCallTool gdb_bt', - 'GDB: serverClosed', - 'CLI: callTool cli_echo', - ]); - - const restartResult = await mdbClient.client.callTool({ - name: 'cli_restart', - arguments: {}, - }); - expect(restartResult.content).toEqual([{ type: 'text', text: 'Restarted' }]); - - const pauseResult = await mdbClient.client.callTool({ - name: 'cli_pause_in_gdb', - arguments: {}, - }); - expect(pauseResult.content).toEqual([{ type: 'text', text: 'Paused on exception' }]); - - const btResult2 = await mdbClient.client.callTool({ - name: 'gdb_bt', - arguments: {}, - }); - expect(btResult2.content).toEqual([{ type: 'text', text: 'Backtrace' }]); - - await expect.poll(() => log).toEqual([ - 'CLI: initialize', - 'CLI: callTool cli_pause_in_gdb', - 'GDB: listTools', - 'GDB: initialize', - 'GDB: callTool gdb_bt', - 'CLI: afterCallTool gdb_bt', - 'GDB: serverClosed', - 'CLI: callTool cli_echo', - 'CLI: callTool cli_restart', - 'CLI: callTool cli_pause_in_gdb', - 'GDB: listTools', - 'GDB: initialize', - 'GDB: callTool gdb_bt', - 'CLI: afterCallTool gdb_bt', - ]); - - await mdbClient.close(); -}); - -test('mdb has unguessable url', async () => { - let firstPathname: string | undefined; - let secondPathname: string | undefined; - { - const { mdbUrl } = await startMDBAndCLI(); - firstPathname = new URL(mdbUrl).pathname; - const mdbClient = await createMDBClient(mdbUrl); - await mdbClient.close(); - } - { - const { mdbUrl } = await startMDBAndCLI(); - secondPathname = new URL(mdbUrl).pathname; - const mdbClient = await createMDBClient(mdbUrl); - await mdbClient.close(); - } - expect(firstPathname.length).toBe(37); - expect(secondPathname.length).toBe(37); - expect(firstPathname).not.toBe(secondPathname); -}); - -async function startMDBAndCLI(): Promise<{ mdbUrl: string, log: string[] }> { - const mdbUrlBox = { mdbUrl: undefined as string | undefined }; - const log: string[] = []; - const cliBackendFactory = { - name: 'CLI', - nameInConfig: 'cli', - version: '0.0.0', - create: pushClient => new CLIBackend(log, pushClient) - }; - - const mdbUrl = (await mcp.runMainBackend(cliBackendFactory, { port: 0 }))!; - mdbUrlBox.mdbUrl = mdbUrl; - return { mdbUrl, log }; -} - -async function createMDBClient(mdbUrl: string, roots: any[] | undefined = undefined): Promise<{ client: Client, close: () => Promise }> { - 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)); - await client.connect(transport); - return { - client, - close: async () => { - await transport.terminateSession(); - await client.close(); - } - }; -} - -class CLIBackend { - private _roots: any[] | undefined; - private _log: string[] = []; - private _pushClient: (url: string, message: string) => Promise; - private _gdbServer: http.Server | undefined; - - constructor(log: string[], pushClient: (url: string, message: string) => Promise) { - this._log = log; - this._pushClient = pushClient; - } - - async initialize(server, clientInfo) { - this._log.push('CLI: initialize'); - this._roots = clientInfo.roots; - } - - async listTools() { - this._log.push('CLI: 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_restart', - description: 'Restart the process', - inputSchema: zodToJsonSchema(z.object({})) as any, - }, { - name: 'gdb_bt', - description: 'Print backtrace', - inputSchema: zodToJsonSchema(z.object({})) as any, - }, { - name: 'gdb_echo', - description: 'Echo a message', - inputSchema: zodToJsonSchema(z.object({ message: z.string() })) as any, - }]; - } - - async afterCallTool(name: string, args: any) { - this._log.push(`CLI: afterCallTool ${name}`); - } - - async callTool(name: string, args: any) { - this._log.push(`CLI: callTool ${name}`); - if (name === 'cli_echo') - return { content: [{ type: 'text', text: `Echo: ${args?.message as string}, roots: ${stringifyRoots(this._roots)}` }] }; - if (name === 'cli_pause_in_gdb') { - const factory = { - name: 'gdb', - nameInConfig: 'gdb', - version: '0.0.0', - create: () => new GDBBackend(this._log), - }; - this._gdbServer = await mcp.startHttpServer({ port: 0 }); - const mcpUrl = await mcp.installHttpTransport(this._gdbServer, factory, true); - await this._pushClient(mcpUrl, 'Paused on exception'); - return { content: [{ type: 'text', text: 'Done' }] }; - } - if (name === 'cli_restart') { - this._gdbServer.close(); - this._gdbServer = undefined; - return { content: [{ type: 'text', text: 'Restarted' }] }; - } - throw new Error(`Unknown tool: ${name}`); - } - - serverClosed() { - this._log.push('CLI: serverClosed'); - } -} - -class GDBBackend { - private _roots: any[] | undefined; - private _log: string[] = []; - - constructor(log: string[]) { - this._log = log; - } - - async initialize(server, clientVersion) { - this._log.push('GDB: initialize'); - this._roots = clientVersion.roots; - } - - async listTools() { - this._log.push('GDB: listTools'); - return [{ - name: 'gdb_bt', - description: 'Print backtrace', - inputSchema: zodToJsonSchema(z.object({})) as any, - }, { - name: 'gdb_echo', - description: 'Echo a message', - inputSchema: zodToJsonSchema(z.object({ message: z.string() })) as any, - }]; - } - - async callTool(name: string, args: any) { - this._log.push(`GDB: callTool ${name}`); - if (name === 'gdb_echo') - return { content: [{ type: 'text', text: `Echo: ${args?.message as string}, roots: ${stringifyRoots(this._roots)}` }] }; - if (name === 'gdb_bt') - return { content: [{ type: 'text', text: 'Backtrace' }] }; - throw new Error(`Unknown tool: ${name}`); - } - - serverClosed() { - this._log.push('GDB: serverClosed'); - } -} - -function stringifyRoots(roots: any[]) { - return roots.map(root => `${root.name}=${root.uri}`).join(','); -} From b3c49321135ee609fcf3623470e82fc08c6ee440 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:05:54 +0100 Subject: [PATCH 212/250] chore: roll driver/Dockerfile to recent Node.js LTS version (#38206) 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 43d2d0e2a..44b331333 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="24.11.0" # autogenerated via ./update-playwright-node.mjs +NODE_VERSION="24.11.1" # autogenerated via ./update-playwright-node.mjs cd "$(dirname "$0")" PACKAGE_VERSION=$(node -p "require('../../package.json').version") From 3089df7944029f8a09b4f3dd72de8a585cad148f Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 13 Nov 2025 14:28:26 +0000 Subject: [PATCH 213/250] feat: worker.on('console') event for web workers (#38201) --- docs/src/api/class-worker.md | 61 +++++++++++++++++++ packages/playwright-client/types/types.d.ts | 46 ++++++++++++++ .../src/client/browserContext.ts | 5 +- packages/playwright-core/src/client/events.ts | 1 + packages/playwright-core/src/client/worker.ts | 22 +++++++ .../playwright-core/src/protocol/validator.ts | 10 ++- .../src/server/bidi/bidiPage.ts | 2 +- .../src/server/chromium/crPage.ts | 6 +- .../playwright-core/src/server/console.ts | 10 ++- .../dispatchers/browserContextDispatcher.ts | 4 +- .../src/server/dispatchers/pageDispatcher.ts | 10 +++ .../src/server/electron/electron.ts | 2 +- .../src/server/firefox/ffPage.ts | 4 +- packages/playwright-core/src/server/page.ts | 4 +- .../src/server/webkit/wkPage.ts | 2 +- .../src/server/webkit/wkWorkers.ts | 2 +- .../src/utils/isomorphic/protocolMetainfo.ts | 2 + packages/playwright-core/types/types.d.ts | 46 ++++++++++++++ packages/protocol/src/channels.d.ts | 14 ++++- packages/protocol/src/protocol.yml | 14 ++++- tests/page/workers.spec.ts | 20 ++++++ 21 files changed, 266 insertions(+), 21 deletions(-) diff --git a/docs/src/api/class-worker.md b/docs/src/api/class-worker.md index fe69a8424..fa7beeda1 100644 --- a/docs/src/api/class-worker.md +++ b/docs/src/api/class-worker.md @@ -58,6 +58,12 @@ 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.57 +- argument: <[ConsoleMessage]> + +Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or `console.dir`. Console is not supported for Service Workers. + ## async method: Worker.evaluate * since: v1.8 - returns: <[Serializable]> @@ -123,3 +129,58 @@ Performs action and waits for the Worker to close. ### param: Worker.waitForClose.callback = %%-java-wait-for-event-callback-%% * since: v1.9 + +## async method: Worker.waitForEvent +* since: v1.57 +* langs: js, python + - alias-python: expect_event +- returns: <[any]> + +Waits for event to fire and passes its value into the predicate function. +Returns when the predicate returns truthy value. +Will throw an error if the page is closed before the event is fired. +Returns the event data value. + +**Usage** + +```js +// Start waiting for download before clicking. Note no await. +const consolePromise = worker.waitForEvent('console'); +await worker.evaluate('console.log(42)'); +const consoleMessage = await consolePromise; +``` + +```python async +async with worker.expect_event("console") as event_info: + await worker.evaluate("console.log(42)") +message = await event_info.value +``` + +```python sync +with worker.expect_event("console") as event_info: + worker.evaluate("console.log(42)") +message = event_info.value +``` + +## async method: Worker.waitForEvent +* since: v1.57 +* langs: python +- returns: <[EventContextManager]> + +### param: Worker.waitForEvent.event = %%-wait-for-event-event-%% +* since: v1.57 + +### param: Worker.waitForEvent.optionsOrPredicate +* since: v1.57 +* langs: js +- `optionsOrPredicate` ?<[function]|[Object]> + - `predicate` <[function]> Receives the event data and resolves to truthy value when the waiting should resolve. + - `timeout` ?<[float]> Maximum time to wait for in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout` option in the config, or by using the [`method: BrowserContext.setDefaultTimeout`] or [`method: Page.setDefaultTimeout`] methods. + +Either a predicate that receives an event or an options object. Optional. + +### option: Worker.waitForEvent.predicate = %%-wait-for-event-predicate-%% +* since: v1.57 + +### option: Worker.waitForEvent.timeout = %%-wait-for-event-timeout-%% +* since: v1.57 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index b2d81ca90..2e5274cd5 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -10330,34 +10330,80 @@ export interface Worker { */ on(event: 'close', listener: (worker: Worker) => any): this; + /** + * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or + * `console.dir`. Console is not supported for Service Workers. + */ + 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; + /** + * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or + * `console.dir`. Console is not supported for Service Workers. + */ + 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; + /** + * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or + * `console.dir`. Console is not supported for Service Workers. + */ + prependListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; + url(): string; + + /** + * Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is + * terminated. + */ + waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (worker: Worker) => boolean | Promise, timeout?: number } | ((worker: Worker) => boolean | Promise)): Promise; + + /** + * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or + * `console.dir`. Console is not supported for Service Workers. + */ + waitForEvent(event: 'console', optionsOrPredicate?: { predicate?: (consoleMessage: ConsoleMessage) => boolean | Promise, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean | Promise)): Promise; + } /** diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 9f97260f9..c32538746 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -109,10 +109,9 @@ export class BrowserContext extends ChannelOwner }); this._channel.on('console', event => { const consoleMessage = new ConsoleMessage(this._platform, event, Page.fromNullable(event.page)); + Worker.fromNullable(event.worker)?.emit(Events.Worker.Console, consoleMessage); + consoleMessage.page()?.emit(Events.Page.Console, consoleMessage); this.emit(Events.BrowserContext.Console, consoleMessage); - const page = consoleMessage.page(); - if (page) - page.emit(Events.Page.Console, consoleMessage); }); this._channel.on('pageError', ({ error, page }) => { const pageObject = Page.from(page); diff --git a/packages/playwright-core/src/client/events.ts b/packages/playwright-core/src/client/events.ts index 41544b8c3..cede73640 100644 --- a/packages/playwright-core/src/client/events.ts +++ b/packages/playwright-core/src/client/events.ts @@ -87,6 +87,7 @@ 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 5a5a5fe24..fdca07b52 100644 --- a/packages/playwright-core/src/client/worker.ts +++ b/packages/playwright-core/src/client/worker.ts @@ -19,12 +19,15 @@ import { TargetClosedError } from './errors'; import { Events } from './events'; import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle'; import { LongStandingScope } from '../utils/isomorphic/manualPromise'; +import { TimeoutSettings } from './timeoutSettings'; +import { Waiter } from './waiter'; import type { BrowserContext } from './browserContext'; import type { Page } from './page'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type * as channels from '@protocol/channels'; +import type { WaitForEventOptions } from './types'; export class Worker extends ChannelOwner implements api.Worker { @@ -32,6 +35,10 @@ export class Worker extends ChannelOwner implements api. _context: BrowserContext | undefined; // Set for service workers. readonly _closedScope = new LongStandingScope(); + static fromNullable(worker: channels.WorkerChannel | undefined): Worker | null { + return worker ? Worker.from(worker) : null; + } + static from(worker: channels.WorkerChannel): Worker { return (worker as any)._object; } @@ -63,4 +70,19 @@ export class Worker extends ChannelOwner implements api. const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return JSHandle.from(result.handle) as any as structs.SmartHandle; } + + async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise { + return await this._wrapApiCall(async () => { + const timeoutSettings = this._page?._timeoutSettings ?? this._context?._timeoutSettings ?? new TimeoutSettings(this._platform); + const timeout = timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); + const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; + const waiter = Waiter.createForEvent(this, event); + waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`); + if (event !== Events.Worker.Close) + waiter.rejectOnEvent(this, Events.Worker.Close, () => new TargetClosedError()); + const result = await waiter.waitForEvent(this, event, predicate as any); + waiter.dispose(); + return result; + }); + } } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 7c0622a3f..81bb78cdb 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -810,12 +810,14 @@ scheme.EventTargetWaitForEventInfoParams = tObject({ }); scheme.BrowserContextWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.PageWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); +scheme.WorkerWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.WebSocketWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.ElectronApplicationWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.AndroidDeviceWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams'); scheme.EventTargetWaitForEventInfoResult = tOptional(tObject({})); scheme.BrowserContextWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.PageWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); +scheme.WorkerWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.WebSocketWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.ElectronApplicationWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); scheme.AndroidDeviceWaitForEventInfoResult = tType('EventTargetWaitForEventInfoResult'); @@ -894,7 +896,8 @@ scheme.BrowserContextConsoleEvent = tObject({ lineNumber: tInt, columnNumber: tInt, }), - page: tChannel(['Page']), + page: tOptional(tChannel(['Page'])), + worker: tOptional(tChannel(['Worker'])), }); scheme.BrowserContextCloseEvent = tOptional(tObject({})); scheme.BrowserContextDialogEvent = tObject({ @@ -1914,6 +1917,11 @@ 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/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index 9041469d5..d46623448 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -286,7 +286,7 @@ export class BidiPage implements PageDelegate { const callFrame = params.stackTrace?.callFrames[0]; const location = callFrame ?? { url: '', lineNumber: 1, columnNumber: 1 }; - this._page.addConsoleMessage(entry.method, entry.args.map(arg => createHandle(context, arg)), location, params.text || undefined); + this._page.addConsoleMessage(null, entry.method, entry.args.map(arg => createHandle(context, arg)), location, params.text || undefined); } async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise { diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 716ce51c8..e29da4115 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -759,7 +759,7 @@ class FrameSession { session.on('Target.detachedFromTarget', event => this._onDetachedFromTarget(event)); session.on('Runtime.consoleAPICalled', event => { const args = event.args.map(o => createHandle(worker.existingExecutionContext!, o)); - this._page.addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace)); + this._page.addConsoleMessage(worker, event.type, args, toConsoleMessageLocation(event.stackTrace)); }); session.on('Runtime.exceptionThrown', exception => this._page.addPageError(exceptionToError(exception.exceptionDetails))); } @@ -822,7 +822,7 @@ class FrameSession { if (!context) return; const values = event.args.map(arg => createHandle(context, arg)); - this._page.addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace)); + this._page.addConsoleMessage(null, event.type, values, toConsoleMessageLocation(event.stackTrace)); } async _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) { @@ -869,7 +869,7 @@ class FrameSession { lineNumber: lineNumber || 0, columnNumber: 0, }; - this._page.addConsoleMessage(level, [], location, text); + this._page.addConsoleMessage(null, level, [], location, text); } } diff --git a/packages/playwright-core/src/server/console.ts b/packages/playwright-core/src/server/console.ts index 383b49fc8..cc0c60317 100644 --- a/packages/playwright-core/src/server/console.ts +++ b/packages/playwright-core/src/server/console.ts @@ -15,7 +15,7 @@ */ import type * as js from './javascript'; -import type { Page } from './page'; +import type { Page, Worker } from './page'; import type { ConsoleMessageLocation } from './types'; export class ConsoleMessage { @@ -24,9 +24,11 @@ export class ConsoleMessage { private _args: js.JSHandle[]; private _location: ConsoleMessageLocation; private _page: Page | null; + private _worker: Worker | null; - constructor(page: Page | null, type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) { + constructor(page: Page | null, worker: Worker | null, type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) { this._page = page; + this._worker = worker; this._type = type; this._text = text; this._args = args; @@ -37,6 +39,10 @@ export class ConsoleMessage { return this._page; } + worker() { + return this._worker; + } + type(): string { return this._type; } diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index b2bf606a1..49e9560c0 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -120,10 +120,12 @@ export class BrowserContextDispatcher extends Dispatcher { const page = message.page()!; - if (this._shouldDispatchEvent(page, 'console')) { + const workerDispatcher = WorkerDispatcher.fromNullable(this, message.worker()); + if (this._shouldDispatchEvent(page, 'console') || workerDispatcher?._subscriptions.has('console')) { const pageDispatcher = PageDispatcher.from(this, page); this._dispatchEvent('console', { page: pageDispatcher, + worker: workerDispatcher, ...pageDispatcher.serializeConsoleMessage(message), }); } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 407520166..1d460c647 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -414,6 +414,9 @@ export class PageDispatcher extends Dispatcher implements channels.WorkerChannel { _type_Worker = true; + _type_EventTarget = true; + + readonly _subscriptions = new Set(); static fromNullable(scope: PageDispatcher | BrowserContextDispatcher, worker: Worker | null): WorkerDispatcher | undefined { if (!worker) @@ -436,6 +439,13 @@ export class WorkerDispatcher extends Dispatcher { return { handle: JSHandleDispatcher.fromJSHandle(this, await progress.race(this._object.evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg)))) }; } + + async updateSubscription(params: channels.WorkerUpdateSubscriptionParams, progress: Progress): Promise { + if (params.enabled) + this._subscriptions.add(params.event); + else + this._subscriptions.delete(params.event); + } } export class BindingCallDispatcher extends Dispatcher implements channels.BindingCallChannel { diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index 8a341743b..aab06be4c 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -114,7 +114,7 @@ export class ElectronApplication extends SdkObject { if (!this._nodeExecutionContext) return; const args = event.args.map(arg => createHandle(this._nodeExecutionContext!, arg)); - const message = new ConsoleMessage(null, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace)); + const message = new ConsoleMessage(null, null, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace)); this.emit(ElectronApplication.Events.Console, message); } diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 3f5fbc2b2..c06611be9 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -231,7 +231,7 @@ export class FFPage implements PageDelegate { if (!context) return; // Juggler reports 'warn' for some internal messages generated by the browser. - this._page.addConsoleMessage(type === 'warn' ? 'warning' : type, args.map(arg => createHandle(context, arg)), location); + this._page.addConsoleMessage(null, type === 'warn' ? 'warning' : type, args.map(arg => createHandle(context, arg)), location); } _onDialogOpened(params: Protocol.Page.dialogOpenedPayload) { @@ -284,7 +284,7 @@ export class FFPage implements PageDelegate { workerSession.on('Runtime.console', event => { const { type, args, location } = event; const context = worker.existingExecutionContext!; - this._page.addConsoleMessage(type, args.map(arg => createHandle(context, arg)), location); + this._page.addConsoleMessage(worker, type, args.map(arg => createHandle(context, arg)), location); }); // Note: we receive worker exceptions directly from the page. } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index ae67a847d..d96615114 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -361,8 +361,8 @@ export class Page extends SdkObject { await PageBinding.dispatch(this, payload, context); } - addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) { - const message = new ConsoleMessage(this, type, text, args, location); + addConsoleMessage(worker: Worker | null, type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) { + const message = new ConsoleMessage(this, worker, type, text, args, location); const intercepted = this.frameManager.interceptConsoleMessage(message); if (intercepted) { args.forEach(arg => arg.dispose()); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 7e4350b99..cd96f8d54 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -582,7 +582,7 @@ export class WKPage implements PageDelegate { location } = this._lastConsoleMessage; for (let i = count; i < event.count; ++i) - this._page.addConsoleMessage(derivedType, handles, location, handles.length ? undefined : text); + this._page.addConsoleMessage(null, derivedType, handles, location, handles.length ? undefined : text); this._lastConsoleMessage.count = event.count; } } diff --git a/packages/playwright-core/src/server/webkit/wkWorkers.ts b/packages/playwright-core/src/server/webkit/wkWorkers.ts index 71f06e2c9..6381b3d9d 100644 --- a/packages/playwright-core/src/server/webkit/wkWorkers.ts +++ b/packages/playwright-core/src/server/webkit/wkWorkers.ts @@ -103,6 +103,6 @@ export class WKWorkers { lineNumber: (lineNumber || 1) - 1, columnNumber: (columnNumber || 1) - 1 }; - this._page.addConsoleMessage(derivedType, handles, location, handles.length ? undefined : text); + this._page.addConsoleMessage(worker, derivedType, handles, location, handles.length ? undefined : text); } } diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index 37b1d3282..3b85ae2ee 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -62,6 +62,7 @@ export const methodMetainfo = new Map any): this; + /** + * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or + * `console.dir`. Console is not supported for Service Workers. + */ + 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; + /** + * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or + * `console.dir`. Console is not supported for Service Workers. + */ + 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; + /** + * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or + * `console.dir`. Console is not supported for Service Workers. + */ + prependListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => any): this; + url(): string; + + /** + * Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is + * terminated. + */ + waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (worker: Worker) => boolean | Promise, timeout?: number } | ((worker: Worker) => boolean | Promise)): Promise; + + /** + * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or + * `console.dir`. Console is not supported for Service Workers. + */ + waitForEvent(event: 'console', optionsOrPredicate?: { predicate?: (consoleMessage: ConsoleMessage) => boolean | Promise, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean | Promise)): Promise; + } /** diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 96946db47..ce94be4aa 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1622,7 +1622,8 @@ export type BrowserContextConsoleEvent = { lineNumber: number, columnNumber: number, }, - page: PageChannel, + page?: PageChannel, + worker?: WorkerChannel, }; export type BrowserContextCloseEvent = {}; export type BrowserContextDialogEvent = { @@ -3277,10 +3278,11 @@ export type WorkerInitializer = { export interface WorkerEventTarget { on(event: 'close', callback: (params: WorkerCloseEvent) => void): this; } -export interface WorkerChannel extends WorkerEventTarget, Channel { +export interface WorkerChannel extends WorkerEventTarget, EventTargetChannel { _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 WorkerEvaluateExpressionParams = { @@ -3305,6 +3307,14 @@ 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; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 786090a08..dab1ea1cd 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1442,7 +1442,8 @@ BrowserContext: console: parameters: $mixin: ConsoleMessage - page: Page + page: Page? + worker: Worker? close: @@ -2781,6 +2782,8 @@ Frame: Worker: type: interface + extends: EventTarget + initializer: url: string @@ -2804,6 +2807,15 @@ Worker: returns: handle: JSHandle + updateSubscription: + internal: true + parameters: + event: + type: enum + literals: + - console + enabled: boolean + events: close: diff --git a/tests/page/workers.spec.ts b/tests/page/workers.spec.ts index 7db80de3b..6035e8524 100644 --- a/tests/page/workers.spec.ts +++ b/tests/page/workers.spec.ts @@ -90,6 +90,26 @@ it('should evaluate', async function({ page }) { expect(await worker.evaluate('1+1')).toBe(2); }); +it('should report console event on the worker', async function({ page }) { + const [worker] = await Promise.all([ + page.waitForEvent('worker'), + page.evaluate(() => { + (window as any).worker = new Worker(URL.createObjectURL(new Blob(['42'], { type: 'application/javascript' }))); + }), + ]); + const [message1, message2, message3] = await Promise.all([ + worker.waitForEvent('console'), + page.waitForEvent('console'), + page.context().waitForEvent('console'), + worker.evaluate(() => { + console.log('hello from worker'); + }), + ]); + expect(message1.text()).toBe('hello from worker'); + expect(message1).toBe(message2); + expect(message1).toBe(message3); +}); + it('should report errors', async function({ page }) { const errorPromise = new Promise(x => page.on('pageerror', x)); await page.evaluate(() => new Worker(URL.createObjectURL(new Blob([` From 35157394d8a1d96f59133d4c6859131602526ae9 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 13 Nov 2025 16:51:37 +0100 Subject: [PATCH 214/250] docs: fix docs roll (#38208) --- docs/src/test-api/class-testconfig.md | 4 ++-- packages/playwright/types/test.d.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index b5f78ac8a..18b741630 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -705,8 +705,8 @@ export default defineConfig({ - `stderr` ?<["pipe"|"ignore"]> Whether to pipe the stderr of the command to the process stderr or ignore it. Defaults to `"pipe"`. - `stdout` ?<["pipe"|"ignore"]> If `"pipe"`, it will pipe the stdout of the command to the process stdout. If `"ignore"`, it will ignore the stdout of the command. Default to `"ignore"`. - `wait` ?<[Object]> Consider command started only when given output has been produced. - - `stdout` ?<[RegExp]> Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the environment, for example /Listening on port (?\\d+)/ will store the port number in `process.env['MY_SERVER_PORT']`. - - `stderr` ?<[RegExp]> Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the environment, for example /Listening on port (?\\d+)/ will store the port number in `process.env['MY_SERVER_PORT']`. + - `stdout` ?<[RegExp]> Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the environment, for example `/Listening on port (?\\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`. + - `stderr` ?<[RegExp]> Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the environment, for example `/Listening on port (?\\d+)/` will store the port number in `process.env['MY_SERVER_PORT']`. - `time` ?<[int]> - `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000. - `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified. diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index ec6a14c09..86134b274 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -10198,14 +10198,14 @@ interface TestConfigWebServer { wait?: { /** * Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the - * environment, for example /Listening on port (?\\d+)/ will store the port number in + * environment, for example `/Listening on port (?\\d+)/` will store the port number in * `process.env['MY_SERVER_PORT']`. */ stdout?: RegExp; /** * Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the - * environment, for example /Listening on port (?\\d+)/ will store the port number in + * environment, for example `/Listening on port (?\\d+)/` will store the port number in * `process.env['MY_SERVER_PORT']`. */ stderr?: RegExp; From aab804e1f52c53333afdd2d13a3544fd9fbdfca3 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 13 Nov 2025 08:44:45 -0800 Subject: [PATCH 215/250] fix(reporter): properly record result annotations when all tests are skipped (#38210) --- packages/html-reporter/src/testCaseView.tsx | 1 + packages/playwright/src/runner/dispatcher.ts | 2 + tests/playwright-test/reporter.spec.ts | 55 ++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index 75a8e1c74..22e5c146f 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -68,6 +68,7 @@ export const TestCaseView: React.FC<{
        {msToString(test.duration)}
        + {/* If there are no results, display test annotations. Otherwise test annotations will be displayed alongside runtime annotations in individual result pane */} {test.results.length === 0 && visibleTestAnnotations.length !== 0 && {visibleTestAnnotations.map((annotation, index) => )} } diff --git a/packages/playwright/src/runner/dispatcher.ts b/packages/playwright/src/runner/dispatcher.ts index d2e122fa5..358f639ac 100644 --- a/packages/playwright/src/runner/dispatcher.ts +++ b/packages/playwright/src/runner/dispatcher.ts @@ -619,6 +619,8 @@ class JobDispatcher { const result = test._appendTestResult(); this._reporter.onTestBegin?.(test, result); result.status = 'skipped'; + // This must mirror _onTestEnd() above + result.annotations = [...test.annotations]; this._reportTestEnd(test, result); } return true; diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index 358368a24..b7e8dc963 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -831,3 +831,58 @@ test('attachments are reported in onStepEnd', { annotation: { type: 'issue', des '[hook] After Hooks: 2 attachments in result', ]); }); + +test('should have static annotations on result when all tests are skipped', async ({ runInlineTest }) => { + class TestReporter implements Reporter { + onTestEnd(test: TestCase, result: TestResult): void { + for (const annotation of result.annotations) + console.log(`%%annotation: ${annotation.type} ${annotation.description || ''}`); + } + } + + const result = await runInlineTest({ + 'reporter.ts': `module.exports = ${TestReporter.toString()}`, + 'playwright.config.ts': `module.exports = { reporter: './reporter' };`, + 'a.spec.ts': ` + import { test } from '@playwright/test'; + test.skip('test', { + annotation: [{ type: 'foo', description: 'desc' }, { type: 'bar' }], + }, async () => {}); + `, + }, { 'reporter': '', 'workers': 1 }); + + expect(result.outputLines).toEqual([ + 'annotation: foo desc', + 'annotation: bar', + 'annotation: skip', + ]); + + const resultMany = await runInlineTest({ + 'reporter.ts': `module.exports = ${TestReporter.toString()}`, + 'playwright.config.ts': `module.exports = { reporter: './reporter' };`, + 'a.spec.ts': ` + import { test } from '@playwright/test'; + test.skip('test', { + annotation: [{ type: 'foo', description: 'desc' }, { type: 'bar' }], + }, async () => {}); + + test.skip('test2', { + annotation: [{ type: 'foobar', description: 'another' }], + }, async () => {}); + + test.skip('test3', { + annotation: [{ type: 'warning', description: 'one more' }], + }, async () => {}); + `, + }, { 'reporter': '', 'workers': 1 }); + + expect(resultMany.outputLines).toEqual([ + 'annotation: foo desc', + 'annotation: bar', + 'annotation: skip', + 'annotation: foobar another', + 'annotation: skip', + 'annotation: warning one more', + 'annotation: skip', + ]); +}); From 468570ffe9c89f2a7536c6cb8836019805273623 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 13 Nov 2025 21:56:27 +0000 Subject: [PATCH 216/250] feat: console messages in service worker (#38214) --- docs/src/api/class-consolemessage.md | 6 ++ docs/src/api/class-worker.md | 2 +- packages/playwright-client/types/types.d.ts | 19 +++--- .../src/client/browserContext.ts | 26 +++++++- .../src/client/consoleMessage.ts | 11 +++- .../playwright-core/src/client/electron.ts | 2 +- packages/playwright-core/src/client/page.ts | 2 +- packages/playwright-core/src/client/worker.ts | 3 + .../src/server/chromium/crServiceWorker.ts | 12 +++- .../dispatchers/browserContextDispatcher.ts | 25 ++++++-- .../src/server/dispatchers/pageDispatcher.ts | 19 +----- packages/playwright-core/types/types.d.ts | 19 +++--- tests/library/chromium/chromium.spec.ts | 60 +++++++++++++++++++ tests/page/workers.spec.ts | 16 +++++ 14 files changed, 175 insertions(+), 47 deletions(-) diff --git a/docs/src/api/class-consolemessage.md b/docs/src/api/class-consolemessage.md index f856dffe5..b712bca26 100644 --- a/docs/src/api/class-consolemessage.md +++ b/docs/src/api/class-consolemessage.md @@ -150,3 +150,9 @@ The text of the console message. One of the following values: `'log'`, `'debug'`, `'info'`, `'error'`, `'warning'`, `'dir'`, `'dirxml'`, `'table'`, `'trace'`, `'clear'`, `'startGroup'`, `'startGroupCollapsed'`, `'endGroup'`, `'assert'`, `'profile'`, `'profileEnd'`, `'count'`, `'timeEnd'`. + +## method: ConsoleMessage.worker +* since: v1.57 +- returns: <[null]|[Worker]> + +The web worker or service worker that produced this console message, if any. Note that console messages from web workers also have non-null [`method: ConsoleMessage.page`]. diff --git a/docs/src/api/class-worker.md b/docs/src/api/class-worker.md index fa7beeda1..bd41abc1c 100644 --- a/docs/src/api/class-worker.md +++ b/docs/src/api/class-worker.md @@ -62,7 +62,7 @@ Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs * since: v1.57 - argument: <[ConsoleMessage]> -Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or `console.dir`. Console is not supported for 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 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 2e5274cd5..55fb65559 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -10331,8 +10331,7 @@ export interface Worker { on(event: 'close', listener: (worker: Worker) => any): this; /** - * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or - * `console.dir`. Console is not supported for 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; @@ -10353,8 +10352,7 @@ export interface Worker { addListener(event: 'close', listener: (worker: Worker) => any): this; /** - * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or - * `console.dir`. Console is not supported for 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; @@ -10385,8 +10383,7 @@ export interface Worker { prependListener(event: 'close', listener: (worker: Worker) => any): this; /** - * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or - * `console.dir`. Console is not supported for 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; @@ -10399,8 +10396,7 @@ export interface Worker { waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (worker: Worker) => boolean | Promise, timeout?: number } | ((worker: Worker) => boolean | Promise)): Promise; /** - * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or - * `console.dir`. Console is not supported for Service Workers. + * Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. */ waitForEvent(event: 'console', optionsOrPredicate?: { predicate?: (consoleMessage: ConsoleMessage) => boolean | Promise, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean | Promise)): Promise; @@ -18886,6 +18882,13 @@ export interface ConsoleMessage { text(): string; type(): "log"|"debug"|"info"|"error"|"warning"|"dir"|"dirxml"|"table"|"trace"|"clear"|"startGroup"|"startGroupCollapsed"|"endGroup"|"assert"|"profile"|"profileEnd"|"count"|"timeEnd"; + + /** + * The web worker or service worker that produced this console message, if any. Note that console messages from web + * workers also have non-null + * [consoleMessage.page()](https://playwright.dev/docs/api/class-consolemessage#console-message-page). + */ + worker(): null|Worker; } /** diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index c32538746..05178d2e7 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -108,9 +108,18 @@ export class BrowserContext extends ChannelOwner this.emit(Events.BrowserContext.ServiceWorker, serviceWorker); }); this._channel.on('console', event => { - const consoleMessage = new ConsoleMessage(this._platform, event, Page.fromNullable(event.page)); - Worker.fromNullable(event.worker)?.emit(Events.Worker.Console, consoleMessage); - consoleMessage.page()?.emit(Events.Page.Console, consoleMessage); + const worker = Worker.fromNullable(event.worker); + const page = Page.fromNullable(event.page); + const consoleMessage = new ConsoleMessage(this._platform, event, page, worker); + worker?.emit(Events.Worker.Console, consoleMessage); + page?.emit(Events.Page.Console, consoleMessage); + if (worker && this._serviceWorkers.has(worker)) { + const scope = this._serviceWorkerScope(worker); + for (const page of this._pages) { + if (scope && page.url().startsWith(scope)) + page.emit(Events.Page.Console, consoleMessage); + } + } this.emit(Events.BrowserContext.Console, consoleMessage); }); this._channel.on('pageError', ({ error, page }) => { @@ -253,6 +262,17 @@ export class BrowserContext extends ChannelOwner await bindingCall.call(func); } + private _serviceWorkerScope(serviceWorker: Worker) { + try { + let url = new URL('.', serviceWorker.url()).href; + if (!url.endsWith('/')) + url += '/'; + return url; + } catch { + return null; + } + } + setDefaultNavigationTimeout(timeout: number | undefined) { this._timeoutSettings.setDefaultNavigationTimeout(timeout); } diff --git a/packages/playwright-core/src/client/consoleMessage.ts b/packages/playwright-core/src/client/consoleMessage.ts index b7b5a9720..1db351f0c 100644 --- a/packages/playwright-core/src/client/consoleMessage.ts +++ b/packages/playwright-core/src/client/consoleMessage.ts @@ -15,26 +15,33 @@ */ import { JSHandle } from './jsHandle'; -import { Page } from './page'; import type * as api from '../../types/types'; import type { Platform } from './platform'; import type * as channels from '@protocol/channels'; +import type { Page } from './page'; +import type { Worker } from './worker'; type ConsoleMessageLocation = channels.BrowserContextConsoleEvent['location']; export class ConsoleMessage implements api.ConsoleMessage { private _page: Page | null; + private _worker: Worker | null; private _event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent; - constructor(platform: Platform, event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent, page: Page | null) { + constructor(platform: Platform, event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent, page: Page | null, worker: Worker | null) { this._page = page; + this._worker = worker; this._event = event; if (platform.inspectCustom) (this as any)[platform.inspectCustom] = () => this._inspect(); } + worker() { + return this._worker; + } + page() { return this._page; } diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index efef83d84..ed6505e24 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, null))); + this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(this._platform, event, null, 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 7c9c68ff9..62891e54b 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -668,7 +668,7 @@ export class Page extends ChannelOwner implements api.Page async consoleMessages(): Promise { const { messages } = await this._channel.consoleMessages(); - return messages.map(message => new ConsoleMessage(this._platform, message, this)); + return messages.map(message => new ConsoleMessage(this._platform, message, this, null)); } async pageErrors(): Promise { diff --git a/packages/playwright-core/src/client/worker.ts b/packages/playwright-core/src/client/worker.ts index fdca07b52..c9d9af5b8 100644 --- a/packages/playwright-core/src/client/worker.ts +++ b/packages/playwright-core/src/client/worker.ts @@ -45,6 +45,9 @@ export class Worker extends ChannelOwner implements api. constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.WorkerInitializer) { super(parent, type, guid, initializer); + this._setEventToSubscriptionMapping(new Map([ + [Events.Worker.Console, 'console'], + ])); this._channel.on('close', () => { if (this._page) this._page._workers.delete(this); diff --git a/packages/playwright-core/src/server/chromium/crServiceWorker.ts b/packages/playwright-core/src/server/chromium/crServiceWorker.ts index 3ef163e43..cb3df4a68 100644 --- a/packages/playwright-core/src/server/chromium/crServiceWorker.ts +++ b/packages/playwright-core/src/server/chromium/crServiceWorker.ts @@ -14,10 +14,12 @@ * limitations under the License. */ import { Worker } from '../page'; -import { CRExecutionContext } from './crExecutionContext'; +import { createHandle, CRExecutionContext } from './crExecutionContext'; import { CRNetworkManager } from './crNetworkManager'; import { BrowserContext } from '../browserContext'; import * as network from '../network'; +import { ConsoleMessage } from '../console'; +import { toConsoleMessageLocation } from './crProtocolHelper'; import type { CRBrowserContext } from './crBrowser'; import type { CRSession } from './crConnection'; @@ -49,6 +51,14 @@ export class CRServiceWorker extends Worker { this._networkManager.addSession(session, undefined, true /* isMain */).catch(() => {}); } + session.on('Runtime.consoleAPICalled', event => { + if (!this.existingExecutionContext) + return; + const args = event.args.map(o => createHandle(this.existingExecutionContext!, o)); + const message = new ConsoleMessage(null, this, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace)); + this.browserContext.emit(BrowserContext.Events.Console, message); + }); + session.send('Runtime.enable', {}).catch(e => { }); session.send('Runtime.runIfWaitingForDebugger').catch(e => { }); session.on('Inspector.targetReloadedAfterCrash', () => { diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 49e9560c0..2563e3577 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -34,6 +34,8 @@ import { createGuid } from '../utils/crypto'; import { urlMatches } from '../../utils/isomorphic/urlMatch'; import { Recorder } from '../recorder'; import { RecorderApp } from '../recorder/recorderApp'; +import { ElementHandleDispatcher } from './elementHandlerDispatcher'; +import { JSHandleDispatcher } from './jsHandleDispatcher'; import type { Artifact } from '../artifact'; import type { ConsoleMessage } from '../console'; @@ -119,14 +121,13 @@ export class BrowserContextDispatcher extends Dispatcher { - const page = message.page()!; + const pageDispatcher = PageDispatcher.fromNullable(this, message.page()); const workerDispatcher = WorkerDispatcher.fromNullable(this, message.worker()); - if (this._shouldDispatchEvent(page, 'console') || workerDispatcher?._subscriptions.has('console')) { - const pageDispatcher = PageDispatcher.from(this, page); + if (this._shouldDispatchEvent(message.page(), 'console') || workerDispatcher?._subscriptions.has('console')) { this._dispatchEvent('console', { page: pageDispatcher, worker: workerDispatcher, - ...pageDispatcher.serializeConsoleMessage(message), + ...this.serializeConsoleMessage(message, workerDispatcher || pageDispatcher!), }); } }); @@ -198,7 +199,7 @@ export class BrowserContextDispatcher extends Dispatcher(page) : undefined; @@ -207,6 +208,20 @@ export class BrowserContextDispatcher extends Dispatcher { + const elementHandle = a.asElement(); + if (elementHandle) + return ElementHandleDispatcher.from(FrameDispatcher.from(this, elementHandle._frame), elementHandle); + return JSHandleDispatcher.fromJSHandle(jsScope, a); + }), + location: message.location(), + }; + } + async createTempFiles(params: channels.BrowserContextCreateTempFilesParams, progress: Progress): Promise { const dir = this._context._browser.options.artifactsDir; const tmpDir = path.join(dir, 'upload-' + createGuid()); diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 1d460c647..eb64dbe95 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -40,7 +40,6 @@ 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; @@ -61,7 +60,7 @@ export class PageDispatcher extends Dispatcher(page); @@ -126,20 +125,6 @@ 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, @@ -292,7 +277,7 @@ export class PageDispatcher extends Dispatcher this.serializeConsoleMessage(message)) }; + return { messages: this._page.consoleMessages().map(message => this.parentScope().serializeConsoleMessage(message, this)) }; } async pageErrors(params: channels.PagePageErrorsParams, progress: Progress): Promise { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 2e5274cd5..55fb65559 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -10331,8 +10331,7 @@ export interface Worker { on(event: 'close', listener: (worker: Worker) => any): this; /** - * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or - * `console.dir`. Console is not supported for 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; @@ -10353,8 +10352,7 @@ export interface Worker { addListener(event: 'close', listener: (worker: Worker) => any): this; /** - * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or - * `console.dir`. Console is not supported for 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; @@ -10385,8 +10383,7 @@ export interface Worker { prependListener(event: 'close', listener: (worker: Worker) => any): this; /** - * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or - * `console.dir`. Console is not supported for 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; @@ -10399,8 +10396,7 @@ export interface Worker { waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (worker: Worker) => boolean | Promise, timeout?: number } | ((worker: Worker) => boolean | Promise)): Promise; /** - * Emitted when JavaScript within the Web Worker calls one of console API methods, e.g. `console.log` or - * `console.dir`. Console is not supported for Service Workers. + * Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. */ waitForEvent(event: 'console', optionsOrPredicate?: { predicate?: (consoleMessage: ConsoleMessage) => boolean | Promise, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean | Promise)): Promise; @@ -18886,6 +18882,13 @@ export interface ConsoleMessage { text(): string; type(): "log"|"debug"|"info"|"error"|"warning"|"dir"|"dirxml"|"table"|"trace"|"clear"|"startGroup"|"startGroupCollapsed"|"endGroup"|"assert"|"profile"|"profileEnd"|"count"|"timeEnd"; + + /** + * The web worker or service worker that produced this console message, if any. Note that console messages from web + * workers also have non-null + * [consoleMessage.page()](https://playwright.dev/docs/api/class-consolemessage#console-message-page). + */ + worker(): null|Worker; } /** diff --git a/tests/library/chromium/chromium.spec.ts b/tests/library/chromium/chromium.spec.ts index 5c55850bb..19dd5d708 100644 --- a/tests/library/chromium/chromium.spec.ts +++ b/tests/library/chromium/chromium.spec.ts @@ -684,3 +684,63 @@ test('PLAYWRIGHT_DISABLE_SERVICE_WORKER_NETWORK', async ({ mode, context, page, delete process.env.PLAYWRIGHT_DISABLE_SERVICE_WORKER_NETWORK; }); + +test('should emit console messages from service worker', async ({ page, context, server }) => { + const emptyPage = await context.newPage(); + await emptyPage.goto(server.EMPTY_PAGE); + const emptyPageMessages = []; + emptyPage.on('console', message => emptyPageMessages.push(message)); + + const [worker] = await Promise.all([ + context.waitForEvent('serviceworker'), + page.goto(server.PREFIX + '/serviceworkers/empty/sw.html') + ]); + + const [message, pageMessage] = await Promise.all([ + worker.waitForEvent('console'), + page.waitForEvent('console'), + worker.evaluate(() => console.log('hello from service worker', { + i: 'am', + am: 1, + complex: { + yup: true, + }, + })), + ]); + + expect(message).toBe(pageMessage); + expect(emptyPageMessages).toHaveLength(0); + + // Should be able to access the message after closing the page. + await page.close(); + + expect(message.text()).toContain('hello from service worker'); + expect(message.type()).toBe('log'); + const args = message.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 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 worker.waitForEvent('console'); + expect(consoleMessage.text()).toBe('Hello from the first line of sw.js'); + expect(consoleMessage.type()).toBe('log'); +}); diff --git a/tests/page/workers.spec.ts b/tests/page/workers.spec.ts index 6035e8524..b2f086af8 100644 --- a/tests/page/workers.spec.ts +++ b/tests/page/workers.spec.ts @@ -110,6 +110,22 @@ it('should report console event on the worker', async function({ page }) { expect(message1).toBe(message3); }); +it('should report console event on the worker when not listening on page or context', async function({ page }) { + const [worker] = await Promise.all([ + page.waitForEvent('worker'), + page.evaluate(() => { + (window as any).worker = new Worker(URL.createObjectURL(new Blob(['42'], { type: 'application/javascript' }))); + }), + ]); + const [message] = await Promise.all([ + worker.waitForEvent('console'), + worker.evaluate(() => { + console.log('hello from worker'); + }), + ]); + expect(message.text()).toBe('hello from worker'); +}); + it('should report errors', async function({ page }) { const errorPromise = new Promise(x => page.on('pageerror', x)); await page.evaluate(() => new Worker(URL.createObjectURL(new Blob([` From 5525566db193e249c18fe195709f7432cbae40ca Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Nov 2025 14:22:31 -0800 Subject: [PATCH 217/250] chore: remove unused indexes in image scale (#38215) --- .../src/server/utils/imageUtils.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/playwright-core/src/server/utils/imageUtils.ts b/packages/playwright-core/src/server/utils/imageUtils.ts index 180185111..b8d1ab948 100644 --- a/packages/playwright-core/src/server/utils/imageUtils.ts +++ b/packages/playwright-core/src/server/utils/imageUtils.ts @@ -42,10 +42,17 @@ export function padImageToSize(image: ImageData, size: { width: number, height: export function scaleImageToSize(image: ImageData, size: { width: number; height: number }): ImageData { const { data: src, width: w1, height: h1 } = image; - const w2 = size.width | 0, h2 = size.height | 0; + const w2 = Math.max(1, Math.floor(size.width)); + const h2 = Math.max(1, Math.floor(size.height)); + if (w1 === w2 && h1 === h2) return image; + if (w1 <= 0 || h1 <= 0) + throw new Error('Invalid input image'); + if (size.width <= 0 || size.height <= 0 || !isFinite(size.width) || !isFinite(size.height)) + throw new Error('Invalid output dimensions'); + const clamp = (v: number, lo: number, hi: number) => (v < lo ? lo : v > hi ? hi : v); // Catmull–Rom weights @@ -61,7 +68,6 @@ export function scaleImageToSize(image: ImageData, size: { width: number; height const dstRowStride = w2 * 4; // Precompute X: indices, weights, and byte offsets (idx*4) - const xIdx = new Int32Array(w2 * 4); const xOff = new Int32Array(w2 * 4); // byte offsets = xIdx*4 const xW = new Float32Array(w2 * 4); const wx = new Float32Array(4); @@ -76,13 +82,11 @@ export function scaleImageToSize(image: ImageData, size: { width: number; height const i1 = clamp(sxi + 0, 0, w1 - 1); const i2 = clamp(sxi + 1, 0, w1 - 1); const i3 = clamp(sxi + 2, 0, w1 - 1); - xIdx[b + 0] = i0; xIdx[b + 1] = i1; xIdx[b + 2] = i2; xIdx[b + 3] = i3; xOff[b + 0] = i0 << 2; xOff[b + 1] = i1 << 2; xOff[b + 2] = i2 << 2; xOff[b + 3] = i3 << 2; xW[b + 0] = wx[0]; xW[b + 1] = wx[1]; xW[b + 2] = wx[2]; xW[b + 3] = wx[3]; } // Precompute Y: indices, weights, and row-base byte offsets (y*rowStride) - const yIdx = new Int32Array(h2 * 4); const yRow = new Int32Array(h2 * 4); // row base in bytes const yW = new Float32Array(h2 * 4); const wy = new Float32Array(4); @@ -97,7 +101,6 @@ export function scaleImageToSize(image: ImageData, size: { width: number; height const j1 = clamp(syi + 0, 0, h1 - 1); const j2 = clamp(syi + 1, 0, h1 - 1); const j3 = clamp(syi + 2, 0, h1 - 1); - yIdx[b + 0] = j0; yIdx[b + 1] = j1; yIdx[b + 2] = j2; yIdx[b + 3] = j3; yRow[b + 0] = j0 * srcRowStride; yRow[b + 1] = j1 * srcRowStride; yRow[b + 2] = j2 * srcRowStride; @@ -131,5 +134,5 @@ export function scaleImageToSize(image: ImageData, size: { width: number; height } } - return { data: Buffer.from(dst), width: w2, height: h2 }; + return { data: Buffer.from(dst.buffer), width: w2, height: h2 }; } From 1abcdef015dc54b3614c685f2528888d18b42122 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 13 Nov 2025 22:53:46 +0000 Subject: [PATCH 218/250] fix(chromium): disable sync to avoid crash in three dots menu (#38216) --- .../playwright-core/src/server/chromium/chromiumSwitches.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts index ec4a332b9..ca98372dd 100644 --- a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts +++ b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts @@ -42,6 +42,8 @@ const disabledFeatures = (assistantMode?: boolean) => [ 'AutoDeElevate', // See https://github.com/microsoft/playwright/issues/37714 'RenderDocument', + // Prevents downloading optimization hints on startup. + 'OptimizationHints', assistantMode ? 'AutomationControlled' : '', ].filter(Boolean); @@ -85,4 +87,8 @@ export const chromiumSwitches = (assistantMode?: boolean, channel?: string) => [ // This disables Chrome for Testing infobar that is visible in the persistent context. // The switch is ignored everywhere else, including Chromium/Chrome/Edge. '--disable-infobars', + // Less annoying popups. + '--disable-search-engine-choice-screen', + // Prevents the "three dots" menu crash in IdentityManager::HasPrimaryAccount for ephemeral contexts. + '--disable-sync', ].filter(Boolean); From d18d7d9890ffa53525900c570ed09d93338d8ef2 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 14 Nov 2025 01:10:43 +0000 Subject: [PATCH 219/250] chore(test mcp): simplify pause handling in test backend (#38205) --- packages/playwright/src/mcp/sdk/tool.ts | 11 +- packages/playwright/src/mcp/test/DEPS.list | 1 + .../playwright/src/mcp/test/generatorTools.ts | 6 +- .../playwright/src/mcp/test/plannerTools.ts | 6 +- packages/playwright/src/mcp/test/streams.ts | 16 +- .../playwright/src/mcp/test/testBackend.ts | 116 +++------- .../playwright/src/mcp/test/testContext.ts | 201 ++++++++++++------ packages/playwright/src/mcp/test/testTool.ts | 3 +- packages/playwright/src/mcp/test/testTools.ts | 53 ++--- packages/playwright/src/runner/testRunner.ts | 2 +- tests/mcp/planner.spec.ts | 1 + tests/mcp/test-debug.spec.ts | 9 +- 12 files changed, 215 insertions(+), 210 deletions(-) diff --git a/packages/playwright/src/mcp/sdk/tool.ts b/packages/playwright/src/mcp/sdk/tool.ts index 6441ff787..0f8ccaa0c 100644 --- a/packages/playwright/src/mcp/sdk/tool.ts +++ b/packages/playwright/src/mcp/sdk/tool.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z as zod, zodToJsonSchema } from '../sdk/bundle'; +import { zodToJsonSchema } from '../sdk/bundle'; import type { z } from 'zod'; import type * as mcpServer from './server'; @@ -27,17 +27,12 @@ export type ToolSchema = { type: 'input' | 'assertion' | 'action' | 'readOnly'; }; -const typesWithIntent = ['action', 'assertion', 'input']; - -export function toMcpTool(tool: ToolSchema, options?: { addIntent?: boolean }): mcpServer.Tool { - const inputSchema = options?.addIntent && typesWithIntent.includes(tool.type) ? tool.inputSchema.extend({ - intent: zod.string().describe('The intent of the call, for example the test step description plan idea') - }) : tool.inputSchema; +export function toMcpTool(tool: ToolSchema): mcpServer.Tool { const readOnly = tool.type === 'readOnly' || tool.type === 'assertion'; return { name: tool.name, description: tool.description, - inputSchema: zodToJsonSchema(inputSchema, { strictUnions: true }) as mcpServer.Tool['inputSchema'], + inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as mcpServer.Tool['inputSchema'], annotations: { title: tool.title, readOnlyHint: readOnly, diff --git a/packages/playwright/src/mcp/test/DEPS.list b/packages/playwright/src/mcp/test/DEPS.list index 23b556217..385682de8 100644 --- a/packages/playwright/src/mcp/test/DEPS.list +++ b/packages/playwright/src/mcp/test/DEPS.list @@ -5,6 +5,7 @@ ../browser/response.ts ../browser/tab.ts ../browser/tools/ +../log.ts ../../reporters ../../runner ../../transform diff --git a/packages/playwright/src/mcp/test/generatorTools.ts b/packages/playwright/src/mcp/test/generatorTools.ts index fbb6be740..57eec9f47 100644 --- a/packages/playwright/src/mcp/test/generatorTools.ts +++ b/packages/playwright/src/mcp/test/generatorTools.ts @@ -34,11 +34,11 @@ export const setupPage = defineTestTool({ type: 'readOnly', }, - handle: async (context, params, progress) => { + handle: async (context, params) => { const seed = await context.getOrCreateSeedFile(params.seedFile, params.project); context.generatorJournal = new GeneratorJournal(context.rootPath, params.plan, seed); - await context.runSeedTest(seed.file, seed.projectName, progress); - return { content: [] }; + const { output, status } = await context.runSeedTest(seed.file, seed.projectName); + return { content: [{ type: 'text', text: output }], isError: status !== 'paused' }; }, }); diff --git a/packages/playwright/src/mcp/test/plannerTools.ts b/packages/playwright/src/mcp/test/plannerTools.ts index e405adc9a..45fa39fd5 100644 --- a/packages/playwright/src/mcp/test/plannerTools.ts +++ b/packages/playwright/src/mcp/test/plannerTools.ts @@ -32,10 +32,10 @@ export const setupPage = defineTestTool({ type: 'readOnly', }, - handle: async (context, params, progress) => { + handle: async (context, params) => { const seed = await context.getOrCreateSeedFile(params.seedFile, params.project); - await context.runSeedTest(seed.file, seed.projectName, progress); - return { content: [] }; + const { output, status } = await context.runSeedTest(seed.file, seed.projectName); + return { content: [{ type: 'text', text: output }], isError: status !== 'paused' }; }, }); diff --git a/packages/playwright/src/mcp/test/streams.ts b/packages/playwright/src/mcp/test/streams.ts index 451341101..4b8f3cae6 100644 --- a/packages/playwright/src/mcp/test/streams.ts +++ b/packages/playwright/src/mcp/test/streams.ts @@ -18,22 +18,22 @@ import { Writable } from 'stream'; import { stripAnsiEscapes } from '../../util'; -import type { ProgressCallback } from '../sdk/server'; - export class StringWriteStream extends Writable { - private _progress: ProgressCallback; + private _output: string[]; private _prefix: string; - constructor(progress: ProgressCallback, stdio: 'stdout' | 'stderr') { + constructor(output: string[], stdio: 'stdout' | 'stderr') { super(); - this._progress = progress; + this._output = output; this._prefix = stdio === 'stdout' ? '' : '[err] '; } override _write(chunk: any, encoding: any, callback: any) { - const text = stripAnsiEscapes(chunk.toString()); - // Progress wraps these as individual messages. - this._progress({ message: `${this._prefix}${text.endsWith('\n') ? text.slice(0, -1) : text}` }); + let text = stripAnsiEscapes(chunk.toString()); + if (text.endsWith('\n')) + text = text.slice(0, -1); + if (text) + this._output.push(this._prefix + text); callback(); } } diff --git a/packages/playwright/src/mcp/test/testBackend.ts b/packages/playwright/src/mcp/test/testBackend.ts index 0f87aa5ba..e49c03220 100644 --- a/packages/playwright/src/mcp/test/testBackend.ts +++ b/packages/playwright/src/mcp/test/testBackend.ts @@ -14,22 +14,16 @@ * limitations under the License. */ -import { debug } from 'playwright-core/lib/utilsBundle'; -import { ManualPromise } from 'playwright-core/lib/utils'; - import * as mcp from '../sdk/exports'; import { TestContext } from './testContext'; import * as testTools from './testTools.js'; import * as generatorTools from './generatorTools.js'; import * as plannerTools from './plannerTools.js'; import { browserTools } from '../browser/tools'; -import { resolveConfigLocation } from '../../common/configLoader'; -import { parseResponse } from '../browser/response'; +import { z as zod } from '../sdk/bundle'; import type { TestTool } from './testTool'; -import type { BrowserMCPRequest, BrowserMCPResponse } from './browserBackend'; - -const errorsDebug = debug('pw:mcp:errors'); +import type { Tool as BrowserTool } from '../browser/tools/tool'; export class TestServerBackend implements mcp.ServerBackend { readonly name = 'Playwright'; @@ -44,103 +38,55 @@ export class TestServerBackend implements mcp.ServerBackend { testTools.listTests, testTools.runTests, testTools.debugTest, + ...browserTools.map(tool => wrapBrowserTool(tool)), ]; - private _context: TestContext; + private _options: { muteConsole?: boolean, headless?: boolean }; + private _context: TestContext | undefined; private _configOption: string | undefined; - private _clientInfo: mcp.ClientInfo | undefined; - private _onPauseClient: { sendMessage: (request: BrowserMCPRequest) => Promise, tools: mcp.Tool[] } | undefined; - private _interruptPromise: ManualPromise | undefined; - private _progress: mcp.CallToolResult['content'] = []; - private _progressCallback: mcp.ProgressCallback; constructor(configOption: string | undefined, options?: { muteConsole?: boolean, headless?: boolean }) { - this._context = new TestContext(this._pushClient.bind(this), options); + this._options = options || {}; this._configOption = configOption; - this._progressCallback = (params: mcp.ProgressParams) => { - if (params.message) - this._progress.push({ type: 'text', text: params.message }); - }; - } - - private async _pushClient(sendMessage: (request: BrowserMCPRequest) => Promise) { - try { - const initializeResponse = await sendMessage({ initialize: { clientInfo: this._clientInfo! } }); - const listToolsResponse = await sendMessage({ listTools: {} }); - const tools = listToolsResponse.listTools!; - this._onPauseClient = { sendMessage, tools }; - this._interruptPromise?.resolve({ - content: [{ - type: 'text', - text: initializeResponse.initialize!.pausedMessage, - }], - }); - this._interruptPromise = undefined; - } catch { - } } async initialize(clientInfo: mcp.ClientInfo): Promise { - this._clientInfo = clientInfo; - const rootPath = mcp.firstRootPath(clientInfo); - - if (this._configOption) { - this._context.initialize(rootPath, resolveConfigLocation(this._configOption)); - return; - } - - if (rootPath) { - this._context.initialize(rootPath, resolveConfigLocation(rootPath)); - return; - } - - this._context.initialize(rootPath, resolveConfigLocation(undefined)); + this._context = new TestContext(clientInfo, this._configOption, this._options); } async listTools(): Promise { - return [ - ...this._tools.map(tool => mcp.toMcpTool(tool.schema)), - ...browserTools.map(tool => mcp.toMcpTool(tool.schema, { addIntent: true })), - ]; + return this._tools.map(tool => mcp.toMcpTool(tool.schema)); } async callTool(name: string, args: mcp.CallToolRequest['params']['arguments']): Promise { - if (this._onPauseClient?.tools.find(tool => tool.name === name)) { - const callToolRespone = await this._onPauseClient.sendMessage({ callTool: { name, arguments: args } }); - const result = callToolRespone.callTool!; - const response = parseResponse(result); - if (response && !response.isError && response.code && typeof args?.['intent'] === 'string') - this._context.generatorJournal?.logStep(args['intent'], response.code); - return result; - } - - await this._onPauseClient?.sendMessage({ close: {} }).catch(errorsDebug); - this._onPauseClient = undefined; - - const resultPromise = new ManualPromise(); - const interruptPromise = new ManualPromise(); - this._interruptPromise = interruptPromise; - - this._callTestTool(name, args).then(result => { - resultPromise.resolve(result); - }).catch(e => { - resultPromise.resolve({ content: [{ type: 'text', text: String(e) }], isError: true }); - }); - - const result = await Promise.race([interruptPromise, resultPromise]); - result.content.unshift(...this._progress); - this._progress.length = 0; - return result; - } - - private async _callTestTool(name: string, args: mcp.CallToolRequest['params']['arguments']): Promise { 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, this._progressCallback); + try { + return await tool.handle(this._context!, tool.schema.inputSchema.parse(args || {})); + } catch (e) { + return { content: [{ type: 'text', text: String(e) }], isError: true }; + } } serverClosed() { void this._context!.close(); } } + +const typesWithIntent = ['action', 'assertion', 'input']; + +function wrapBrowserTool(tool: BrowserTool): TestTool { + const inputSchema = typesWithIntent.includes(tool.schema.type) ? (tool.schema.inputSchema as any).extend({ + intent: zod.string().describe('The intent of the call, for example the test step description plan idea') + }) : tool.schema.inputSchema; + return { + schema: { + ...tool.schema, + inputSchema, + }, + handle: async (context: TestContext, params: any) => { + const response = await context.sendMessageToPausedTest({ callTool: { name: tool.schema.name, arguments: params } }); + return response.callTool!; + }, + }; +} diff --git a/packages/playwright/src/mcp/test/testContext.ts b/packages/playwright/src/mcp/test/testContext.ts index 63d5aa031..dc17cd213 100644 --- a/packages/playwright/src/mcp/test/testContext.ts +++ b/packages/playwright/src/mcp/test/testContext.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import { noColors, escapeRegExp } from 'playwright-core/lib/utils'; +import { noColors, escapeRegExp, ManualPromise } from 'playwright-core/lib/utils'; import { terminalScreen } from '../../reporters/base'; import ListReporter from '../../reporters/list'; @@ -26,9 +26,16 @@ import { StringWriteStream } from './streams'; import { fileExistsAsync } from '../../util'; import { TestRunner, TestRunnerEvent } from '../../runner/testRunner'; import { ensureSeedFile, seedProject } from './seed'; +import { firstRootPath } from '../sdk/exports'; +import { resolveConfigLocation } from '../../common/configLoader'; +import { parseResponse } from '../browser/response'; +import { logUnhandledError } from '../log'; +import type { TerminalScreen } from '../../reporters/base'; +import type { FullResultStatus, RunTestsParams } from '../../runner/testRunner'; import type { ConfigLocation } from '../../common/config'; -import type { ProgressCallback } from '../sdk/exports'; +import type { ClientInfo } from '../sdk/exports'; +import type { BrowserMCPRequest, BrowserMCPResponse } from './browserBackend'; export type SeedFile = { file: string; @@ -71,55 +78,77 @@ ${step.code} } } -type PushClientCallback = (sendMessage: (request: any) => Promise) => Promise; +type TestRunnerAndScreen = { + testRunner: TestRunner; + screen: TerminalScreen; + claimStdio: () => void; + releaseStdio: () => void; + output: string[]; + waitForTestPaused: () => Promise; + sendMessageToPausedTest?: (params: { request: BrowserMCPRequest }) => Promise<{ response: BrowserMCPResponse, error?: any }>; +}; export class TestContext { - private _pushClient: PushClientCallback; - private _testRunner: TestRunner | undefined; - readonly options?: { muteConsole?: boolean, headless?: boolean }; + private _clientInfo: ClientInfo; + private _testRunnerAndScreen: TestRunnerAndScreen | undefined; readonly computedHeaded: boolean; - configLocation!: ConfigLocation; - rootPath!: string; + private readonly _configLocation: ConfigLocation; + readonly rootPath: string; generatorJournal: GeneratorJournal | undefined; - constructor(pushClient: PushClientCallback, options?: { muteConsole?: boolean, headless?: boolean }) { - this._pushClient = pushClient; - this.options = options; + constructor(clientInfo: ClientInfo, configPath: string | undefined, options?: { muteConsole?: boolean, headless?: boolean }) { + this._clientInfo = clientInfo; + + const rootPath = firstRootPath(clientInfo); + this._configLocation = resolveConfigLocation(configPath || rootPath); + this.rootPath = rootPath || this._configLocation.configDir; + if (options?.headless !== undefined) this.computedHeaded = !options.headless; else this.computedHeaded = !process.env.CI && !(os.platform() === 'linux' && !process.env.DISPLAY); } - initialize(rootPath: string | undefined, configLocation: ConfigLocation) { - this.configLocation = configLocation; - this.rootPath = rootPath || configLocation.configDir; + existingTestRunner(): TestRunner | undefined { + return this._testRunnerAndScreen?.testRunner; } - existingTestRunner(): TestRunner | undefined { - return this._testRunner; + private async _cleanupTestRunner() { + if (!this._testRunnerAndScreen) + return; + await this._testRunnerAndScreen.testRunner.stopTests(); + this._testRunnerAndScreen.claimStdio(); + try { + await this._testRunnerAndScreen.testRunner.runGlobalTeardown(); + } finally { + this._testRunnerAndScreen.releaseStdio(); + this._testRunnerAndScreen = undefined; + } } - async createTestRunner(): Promise { - if (this._testRunner) - await this._testRunner.stopTests(); - const testRunner = new TestRunner(this.configLocation!, {}); + async createTestRunner() { + await this._cleanupTestRunner(); + + const testRunner = new TestRunner(this._configLocation, {}); await testRunner.initialize({}); + const testPaused = new ManualPromise(); + const testRunnerAndScreen: TestRunnerAndScreen = { + ...createScreen(), + testRunner, + waitForTestPaused: () => testPaused, + }; + this._testRunnerAndScreen = testRunnerAndScreen; + testRunner.on(TestRunnerEvent.TestPaused, params => { - void this._pushClient(async (request: any) => { - const response = await params.sendMessage({ request }); - if (response.error) - throw new Error(response.error.message); - return response.response; - }); + testRunnerAndScreen.sendMessageToPausedTest = params.sendMessage; + testPaused.resolve(); }); - this._testRunner = testRunner; - return testRunner; + return testRunnerAndScreen; } async getOrCreateSeedFile(seedFile: string | undefined, projectName: string | undefined) { - const configDir = this.configLocation.configDir; - const testRunner = await this.createTestRunner(); + const configDir = this._configLocation.configDir; + const { testRunner } = await this.createTestRunner(); const config = await testRunner.loadConfig(); const project = seedProject(config, projectName); @@ -151,63 +180,101 @@ export class TestContext { }; } - async runSeedTest(seedFile: string, projectName: string, progress: ProgressCallback) { - await this.runWithGlobalSetup(async (testRunner, reporter) => { - const result = await testRunner.runTests(reporter, { - headed: this.computedHeaded, - locations: ['/' + escapeRegExp(seedFile) + '/'], - projects: [projectName], - timeout: 0, - 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) - throw new Error('seed test not found.'); - - if (result.status !== 'passed') - throw new Error('Errors while running the seed test.'); - }, progress); + async runSeedTest(seedFile: string, projectName: string): Promise<{ output: string, status: FullResultStatus | 'paused' }> { + const result = await this.runTestsWithGlobalSetupAndPossiblePause({ + headed: this.computedHeaded, + locations: ['/' + escapeRegExp(seedFile) + '/'], + projects: [projectName], + timeout: 0, + workers: 1, + pauseAtEnd: true, + disableConfigReporters: true, + failOnLoadErrors: true, + }); + if (result.status === 'passed') + result.output += '\nError: seed test not found.'; + else if (result.status !== 'paused') + result.output += '\nError while running the seed test.'; + return result; } - async runWithGlobalSetup( - callback: (testRunner: TestRunner, reporter: ListReporter) => Promise, - progress: ProgressCallback): Promise { - const { screen, claimStdio, releaseStdio } = createScreen(progress); - const configDir = this.configLocation.configDir; - const testRunner = await this.createTestRunner(); + async runTestsWithGlobalSetupAndPossiblePause(params: RunTestsParams): Promise<{ output: string, status: FullResultStatus | 'paused' }> { + const configDir = this._configLocation.configDir; + const testRunnerAndScreen = await this.createTestRunner(); + const { testRunner, screen, claimStdio, releaseStdio } = testRunnerAndScreen; claimStdio(); try { const setupReporter = new ListReporter({ configDir, screen, includeTestId: true }); const { status } = await testRunner.runGlobalSetup([setupReporter]); if (status !== 'passed') - throw new Error('Failed to run global setup'); + return { output: testRunnerAndScreen.output.join('\n'), status }; } finally { releaseStdio(); } - try { - const reporter = new ListReporter({ configDir, screen, includeTestId: true }); - return await callback(testRunner, reporter); - } finally { + let status: FullResultStatus | 'paused' = 'passed'; + + const cleanup = async () => { claimStdio(); - await testRunner.runGlobalTeardown().finally(() => { + try { + const result = await testRunner.runGlobalTeardown(); + if (status === 'passed') + status = result.status; + } finally { releaseStdio(); - }); + } + }; + + try { + const reporter = new ListReporter({ configDir, screen, includeTestId: true }); + status = await Promise.race([ + testRunner.runTests(reporter, params).then(result => result.status), + testRunnerAndScreen.waitForTestPaused().then(() => 'paused' as const), + ]); + + if (status === 'paused') { + const response = await testRunnerAndScreen.sendMessageToPausedTest!({ request: { initialize: { clientInfo: this._clientInfo } } }); + if (response.error) + throw new Error(response.error.message); + testRunnerAndScreen.output.push(response.response.initialize!.pausedMessage); + return { output: testRunnerAndScreen.output.join('\n'), status }; + } + } catch (e) { + status = 'failed'; + testRunnerAndScreen.output.push(String(e)); + await cleanup(); + return { output: testRunnerAndScreen.output.join('\n'), status }; } + + await cleanup(); + return { output: testRunnerAndScreen.output.join('\n'), status }; } async close() { + await this._cleanupTestRunner().catch(logUnhandledError); + } + + async sendMessageToPausedTest(request: BrowserMCPRequest): Promise { + const sendMessage = this._testRunnerAndScreen?.sendMessageToPausedTest; + if (!sendMessage) + throw new Error('Must setup test before interacting with the page'); + const result = await sendMessage({ request }); + if (result.error) + throw new Error(result.error.message); + if (typeof request?.callTool?.arguments?.['intent'] === 'string') { + const response = parseResponse(result.response.callTool!); + if (response && !response.isError && response.code) + this.generatorJournal?.logStep(request.callTool.arguments['intent'], response.code); + } + return result.response; } } -export function createScreen(progress: ProgressCallback) { - const stdout = new StringWriteStream(progress, 'stdout'); - const stderr = new StringWriteStream(progress, 'stderr'); +export function createScreen() { + const output: string[] = []; + const stdout = new StringWriteStream(output, 'stdout'); + const stderr = new StringWriteStream(output, 'stderr'); const screen = { ...terminalScreen, @@ -238,7 +305,7 @@ export function createScreen(progress: ProgressCallback) { }; /* eslint-enable no-restricted-properties */ - return { screen, claimStdio, releaseStdio }; + return { screen, claimStdio, releaseStdio, output }; } const bestPracticesMarkdown = ` diff --git a/packages/playwright/src/mcp/test/testTool.ts b/packages/playwright/src/mcp/test/testTool.ts index b1dbbe32a..05a3e5212 100644 --- a/packages/playwright/src/mcp/test/testTool.ts +++ b/packages/playwright/src/mcp/test/testTool.ts @@ -18,11 +18,10 @@ 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'; -import type { ProgressCallback } from '../sdk/server.js'; export type TestTool = { schema: ToolSchema; - handle: (context: TestContext, params: z.output, progress: ProgressCallback) => Promise; + handle: (context: TestContext, params: z.output) => Promise; }; export function defineTestTool(tool: TestTool): TestTool { diff --git a/packages/playwright/src/mcp/test/testTools.ts b/packages/playwright/src/mcp/test/testTools.ts index 5087a3ba1..2c57ecc81 100644 --- a/packages/playwright/src/mcp/test/testTools.ts +++ b/packages/playwright/src/mcp/test/testTools.ts @@ -17,7 +17,6 @@ import { z } from '../sdk/bundle'; import ListModeReporter from '../../reporters/listModeReporter'; import { defineTestTool } from './testTool'; -import { createScreen } from './testContext'; export const listTests = defineTestTool({ schema: { @@ -28,13 +27,11 @@ export const listTests = defineTestTool({ type: 'readOnly', }, - handle: async (context, _, progress) => { - const { screen } = createScreen(progress); + handle: async context => { + const { testRunner, screen, output } = await context.createTestRunner(); const reporter = new ListModeReporter({ screen, includeTestId: true }); - const testRunner = await context.createTestRunner(); await testRunner.listTests(reporter, {}); - - return { content: [] }; + return { content: output.map(text => ({ type: 'text', text })) }; }, }); @@ -50,16 +47,13 @@ export const runTests = defineTestTool({ type: 'readOnly', }, - handle: async (context, params, progress) => { - await context.runWithGlobalSetup(async (testRunner, reporter) => { - await testRunner.runTests(reporter, { - locations: params.locations, - projects: params.projects, - disableConfigReporters: true, - }); - }, progress); - - return { content: [] }; + handle: async (context, params) => { + const { output } = await context.runTestsWithGlobalSetupAndPossiblePause({ + locations: params.locations, + projects: params.projects, + disableConfigReporters: true, + }); + return { content: [{ type: 'text', text: output }] }; }, }); @@ -77,20 +71,17 @@ export const debugTest = defineTestTool({ type: 'readOnly', }, - handle: async (context, params, progress) => { - await context.runWithGlobalSetup(async (testRunner, reporter) => { - await testRunner.runTests(reporter, { - headed: context.computedHeaded, - testIds: [params.test.id], - // For automatic recovery - timeout: 0, - workers: 1, - pauseOnError: true, - disableConfigReporters: true, - actionTimeout: 5000, - }); - }, progress); - - return { content: [] }; + handle: async (context, params) => { + const { output, status } = await context.runTestsWithGlobalSetupAndPossiblePause({ + headed: context.computedHeaded, + testIds: [params.test.id], + // For automatic recovery + timeout: 0, + workers: 1, + pauseOnError: true, + disableConfigReporters: true, + actionTimeout: 5000, + }); + return { content: [{ type: 'text', text: output }], isError: status !== 'paused' && status !== 'passed' }; }, }); diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index 8f5dce044..426c90d19 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -82,7 +82,7 @@ export type RunTestsParams = { failOnLoadErrors?: boolean; }; -type FullResultStatus = reporterTypes.FullResult['status']; +export type FullResultStatus = reporterTypes.FullResult['status']; export class TestRunner extends EventEmitter { readonly configLocation: ConfigLocation; diff --git a/tests/mcp/planner.spec.ts b/tests/mcp/planner.spec.ts index 1917660b5..30fbe7428 100644 --- a/tests/mcp/planner.spec.ts +++ b/tests/mcp/planner.spec.ts @@ -58,6 +58,7 @@ test('planner_setup_page', async ({ startClient }) => { arguments: { element: 'Submit button', ref: 'e2', + intent: 'Click on the "Submit" button', }, })).toHaveResponse({ code: `await page.getByRole('button', { name: 'Submit' }).click();`, diff --git a/tests/mcp/test-debug.spec.ts b/tests/mcp/test-debug.spec.ts index b0f1a8758..b060380e1 100644 --- a/tests/mcp/test-debug.spec.ts +++ b/tests/mcp/test-debug.spec.ts @@ -223,6 +223,7 @@ test('test_debug / evaluate', async ({ startClient }) => { name: 'browser_evaluate', arguments: { function: '() => 21+21', + intent: 'Calculate 21+21', }, })).toHaveResponse({ result: `42`, @@ -241,6 +242,7 @@ test('test_debug / evaluate x 2', async ({ startClient }) => { name: 'browser_evaluate', arguments: { function: '() => 21+21', + intent: 'Calculate 21+21', }, })).toHaveResponse({ result: `42`, @@ -254,13 +256,15 @@ test('test_debug / evaluate x 2', async ({ startClient }) => { })).toEqual({ content: [ { type: 'text', text: expect.stringContaining(`Paused on error`) }, - ] + ], + isError: false, }); expect(await client.callTool({ name: 'browser_evaluate', arguments: { function: '() => 21+23', + intent: 'Calculate 21+23', }, })).toHaveResponse({ result: `44`, @@ -281,6 +285,7 @@ test('test_debug / evaluate (with element)', async ({ startClient }) => { function: 'element => element.textContent', element: 'button', ref: 'e2', + intent: 'Get button text', }, })).toHaveResponse({ result: `"Submit"`, @@ -427,7 +432,7 @@ Error: expect(locator).toBeVisible() failed`)); expect(await client.callTool({ name: 'browser_navigate', - arguments: { url: server.HELLO_WORLD }, + arguments: { url: server.HELLO_WORLD, intent: 'Go to hello world' }, })).toHaveResponse({ pageState: expect.stringContaining(`- Page URL: ${server.HELLO_WORLD}\n- Page Title: Title2`), }); From 2db5effacd2efe5accf728189b5466f175c426db Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 14 Nov 2025 05:14:14 +0000 Subject: [PATCH 220/250] chore: pass "live" flag explicitly instead of through env var (#38217) --- packages/playwright-core/src/client/tracing.ts | 10 ++++++---- packages/playwright-core/src/protocol/validator.ts | 1 + packages/playwright-core/src/server/localUtils.ts | 5 +++-- packages/playwright/src/runner/testRunner.ts | 4 ---- packages/protocol/src/channels.d.ts | 2 ++ packages/protocol/src/protocol.yml | 1 + tests/mcp/tracing.spec.ts | 2 ++ 7 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/playwright-core/src/client/tracing.ts b/packages/playwright-core/src/client/tracing.ts index 536d1673b..a3c7bd53a 100644 --- a/packages/playwright-core/src/client/tracing.ts +++ b/packages/playwright-core/src/client/tracing.ts @@ -22,6 +22,7 @@ import type * as channels from '@protocol/channels'; export class Tracing extends ChannelOwner implements api.Tracing { private _includeSources = false; + private _isLive = false; _tracesDir: string | undefined; private _stacksId: string | undefined; private _isTracing = false; @@ -37,6 +38,7 @@ export class Tracing extends ChannelOwner implements ap async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) { await this._wrapApiCall(async () => { this._includeSources = !!options.sources; + this._isLive = !!options._live; await this._channel.tracingStart({ name: options.name, snapshots: options.snapshots, @@ -44,14 +46,14 @@ export class Tracing extends ChannelOwner implements ap live: options._live, }); const { traceName } = await this._channel.tracingStartChunk({ name: options.name, title: options.title }); - await this._startCollectingStacks(traceName); + await this._startCollectingStacks(traceName, this._isLive); }); } async startChunk(options: { name?: string, title?: string } = {}) { await this._wrapApiCall(async () => { const { traceName } = await this._channel.tracingStartChunk(options); - await this._startCollectingStacks(traceName); + await this._startCollectingStacks(traceName, this._isLive); }); } @@ -63,12 +65,12 @@ export class Tracing extends ChannelOwner implements ap await this._channel.tracingGroupEnd(); } - private async _startCollectingStacks(traceName: string) { + private async _startCollectingStacks(traceName: string, live: boolean) { if (!this._isTracing) { this._isTracing = true; this._connection.setIsTracing(true); } - const result = await this._connection.localUtils()?.tracingStarted({ tracesDir: this._tracesDir, traceName }); + const result = await this._connection.localUtils()?.tracingStarted({ tracesDir: this._tracesDir, traceName, live }); this._stacksId = result?.stacksId; } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 81bb78cdb..c2f860582 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -317,6 +317,7 @@ scheme.LocalUtilsConnectResult = tObject({ scheme.LocalUtilsTracingStartedParams = tObject({ tracesDir: tOptional(tString), traceName: tString, + live: tOptional(tBoolean), }); scheme.LocalUtilsTracingStartedResult = tObject({ stacksId: tString, diff --git a/packages/playwright-core/src/server/localUtils.ts b/packages/playwright-core/src/server/localUtils.ts index b58f2aa85..685a0f64f 100644 --- a/packages/playwright-core/src/server/localUtils.ts +++ b/packages/playwright-core/src/server/localUtils.ts @@ -38,6 +38,7 @@ export type StackSession = { writer: Promise; tmpDir: string | undefined; callStacks: channels.ClientSideCallMetadata[]; + live?: boolean; }; export async function zip(progress: Progress, stackSessions: Map, params: channels.LocalUtilsZipParams): Promise { @@ -194,7 +195,7 @@ export async function tracingStarted(progress: Progress, stackSessions: Map, params: channels.LocalUtilsAddStackToTracingNoReplyParams) { for (const session of stackSessions.values()) { session.callStacks.push(params.callData); - if (process.env.PW_LIVE_TRACE_STACKS) { + if (session.live) { session.writer = session.writer.then(() => { const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(session.callStacks))); return fs.promises.writeFile(session.file, buffer); diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index 426c90d19..596ac10a5 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -324,10 +324,6 @@ export class TestRunner extends EventEmitter { ...(params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {}), ...(params.workers ? { workers: params.workers } : {}), }; - if (params.trace === 'on') - process.env.PW_LIVE_TRACE_STACKS = '1'; - else - process.env.PW_LIVE_TRACE_STACKS = undefined; const config = await this._loadConfigOrReportError(new InternalReporter([userReporter]), overrides); if (!config) diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index ce94be4aa..d001a36af 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -533,9 +533,11 @@ export type LocalUtilsConnectResult = { export type LocalUtilsTracingStartedParams = { tracesDir?: string, traceName: string, + live?: boolean, }; export type LocalUtilsTracingStartedOptions = { tracesDir?: string, + live?: boolean, }; export type LocalUtilsTracingStartedResult = { stacksId: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index dab1ea1cd..1629cf5fe 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -709,6 +709,7 @@ LocalUtils: parameters: tracesDir: string? traceName: string + live: boolean? returns: stacksId: string diff --git a/tests/mcp/tracing.spec.ts b/tests/mcp/tracing.spec.ts index c790aa926..420534c92 100644 --- a/tests/mcp/tracing.spec.ts +++ b/tests/mcp/tracing.spec.ts @@ -64,6 +64,7 @@ test('check that trace is saved with browser_start_tracing', async ({ startClien expect(files).toEqual([ 'resources', expect.stringMatching(/trace-\d+\.network/), + expect.stringMatching(/trace-\d+\.stacks/), expect.stringMatching(/trace-\d+\.trace/), ]); }); @@ -102,6 +103,7 @@ test('check that trace is saved with browser_start_tracing (no output dir)', asy expect(files).toEqual([ 'resources', expect.stringMatching(/trace-\d+\.network/), + expect.stringMatching(/trace-\d+\.stacks/), expect.stringMatching(/trace-\d+\.trace/), ]); }); From 3af9725200cdd82a4a82931bafc97116c89e7c23 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 14 Nov 2025 12:54:34 +0100 Subject: [PATCH 221/250] chore(ui): Speedboard UI improvements (#38194) --- packages/html-reporter/src/chip.css | 1 - packages/html-reporter/src/chip.tsx | 3 +- packages/html-reporter/src/common.css | 4 + packages/html-reporter/src/filter.ts | 11 +- packages/html-reporter/src/headerView.tsx | 9 +- packages/html-reporter/src/icons.tsx | 4 + packages/html-reporter/src/labels.tsx | 14 +- packages/html-reporter/src/links.css | 9 ++ packages/html-reporter/src/links.tsx | 7 +- packages/html-reporter/src/reportView.tsx | 33 ++++- packages/html-reporter/src/speedboard.tsx | 22 ++- packages/html-reporter/src/testCaseView.tsx | 5 +- packages/html-reporter/src/testFileView.tsx | 19 +-- packages/html-reporter/src/testResultView.tsx | 5 +- tests/playwright-test/reporter-html.spec.ts | 135 +++++++++++++++--- 15 files changed, 213 insertions(+), 68 deletions(-) diff --git a/packages/html-reporter/src/chip.css b/packages/html-reporter/src/chip.css index b061b1c8c..1960b7d02 100644 --- a/packages/html-reporter/src/chip.css +++ b/packages/html-reporter/src/chip.css @@ -58,7 +58,6 @@ .chip-footer { border-top: 1px solid var(--color-border-default); - padding: 8px; } @media only screen and (max-width: 600px) { diff --git a/packages/html-reporter/src/chip.tsx b/packages/html-reporter/src/chip.tsx index df79a7181..93e79abd4 100644 --- a/packages/html-reporter/src/chip.tsx +++ b/packages/html-reporter/src/chip.tsx @@ -40,8 +40,7 @@ export const Chip: React.FC<{ className={clsx('chip-header', setExpanded && ' expanded-' + expanded)} onClick={() => setExpanded?.(!expanded)} title={typeof header === 'string' ? header : undefined}> - {setExpanded && !!expanded && icons.downArrow()} - {setExpanded && !expanded && icons.rightArrow()} + {setExpanded ? (expanded ? : ) : } {header}
        {(!setExpanded || expanded) &&
        diff --git a/packages/html-reporter/src/common.css b/packages/html-reporter/src/common.css index 2cdb41bd3..c67df07e6 100644 --- a/packages/html-reporter/src/common.css +++ b/packages/html-reporter/src/common.css @@ -215,6 +215,10 @@ article, aside, details, figcaption, figure, footer, header, main, menu, nav, se background-color: var(--color-canvas-subtle); } +.subnav-item[aria-selected="true"] { + background: var(--color-control-transparent-bg-hover); +} + .subnav-item:first-child { border-top-left-radius: 6px; border-bottom-left-radius: 6px; diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index c08350feb..36da55546 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -214,13 +214,16 @@ function cacheSearchValues(test: TestCaseSummary & { [searchValuesSymbol]?: Sear const SEARCH_PARAM_GROUP_REGEX = /("[^"]*"|"[^"]*$|\S+)/g; export function filterWithQuery(searchParams: URLSearchParams, token: string, append: boolean): string { + const result = new URLSearchParams(searchParams); const existingQuery = searchParams.get('q') ?? ''; const tokens = [...existingQuery.matchAll(SEARCH_PARAM_GROUP_REGEX)].map(m => { const rawValue = m[0]; return rawValue.startsWith('"') && rawValue.endsWith('"') && rawValue.length > 1 ? rawValue.slice(1, rawValue.length - 1) : rawValue; }); - if (append) - return '#?q=' + joinTokens(!tokens.includes(token) ? [...tokens, token] : tokens.filter(t => t !== token)); + if (append) { + result.set('q', joinTokens(!tokens.includes(token) ? [...tokens, token] : tokens.filter(t => t !== token))); + return '#?' + result; + } // if metaKey or ctrlKey is not pressed, replace existing token with new token let prefix: 's:' | 'p:' | '@'; @@ -233,7 +236,9 @@ export function filterWithQuery(searchParams: URLSearchParams, token: string, ap const newTokens = tokens.filter(t => !t.startsWith(prefix)); newTokens.push(token); - return '#?q=' + joinTokens(newTokens); + + result.set('q', joinTokens(newTokens)); + return '#?' + result; } function joinTokens(tokens: string[]): string { diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx index d8c5f7165..dacfd19b8 100644 --- a/packages/html-reporter/src/headerView.tsx +++ b/packages/html-reporter/src/headerView.tsx @@ -89,6 +89,8 @@ export const GlobalFilterView: React.FC<{ const StatsNavView: React.FC<{ stats: Stats }> = ({ stats }) => { + const searchParams = React.useContext(SearchParamsContext); + return