From ae3da48f794ac21ec818e15fe3db8cd658f04936 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 21 Nov 2025 09:55:28 +0000 Subject: [PATCH 1/9] cherry-pick(#38124): feat(firefox): roll to latest firefox and firefox-beta (#38296) Co-authored-by: Andrey Lushnikov --- README.md | 4 ++-- packages/playwright-core/browsers.json | 8 ++++---- .../src/server/deviceDescriptorsSource.json | 4 ++-- tests/library/browsercontext-timezone-id.spec.ts | 13 +++++++++---- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d353c3486285f..c3a597409967f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-143.0.7499.4-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-142.0.1-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-143.0.7499.4-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-144.0.2-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -10,7 +10,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | :--- | :---: | :---: | :---: | | Chromium 143.0.7499.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 26.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Firefox 142.0.1 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 144.0.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details. diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 498067e76bb54..a8260fa12ed89 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -27,15 +27,15 @@ }, { "name": "firefox", - "revision": "1496", + "revision": "1497", "installByDefault": true, - "browserVersion": "142.0.1" + "browserVersion": "144.0.2" }, { "name": "firefox-beta", - "revision": "1491", + "revision": "1493", "installByDefault": false, - "browserVersion": "143.0b10" + "browserVersion": "145.0b10" }, { "name": "webkit", diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 8a118c12d5313..fd1150b3d8b3b 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -1702,7 +1702,7 @@ "defaultBrowserType": "chromium" }, "Desktop Firefox HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0.1) Gecko/20100101 Firefox/142.0.1", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0.2) Gecko/20100101 Firefox/144.0.2", "screen": { "width": 1792, "height": 1120 @@ -1762,7 +1762,7 @@ "defaultBrowserType": "chromium" }, "Desktop Firefox": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0.1) Gecko/20100101 Firefox/142.0.1", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0.2) Gecko/20100101 Firefox/144.0.2", "screen": { "width": 1920, "height": 1080 diff --git a/tests/library/browsercontext-timezone-id.spec.ts b/tests/library/browsercontext-timezone-id.spec.ts index 1f6b7c6dced61..4ce69ffcba46a 100644 --- a/tests/library/browsercontext-timezone-id.spec.ts +++ b/tests/library/browsercontext-timezone-id.spec.ts @@ -32,7 +32,7 @@ it('should work @smoke', async ({ browser, browserName }) => { await context.close(); } { - const context = await browser.newContext({ locale: 'en-US', timezoneId: 'America/Buenos_Aires' }); + const context = await browser.newContext({ locale: 'en-US', timezoneId: browserName === 'firefox' ? 'America/Argentina/Buenos_Aires' : 'America/Buenos_Aires' }); const page = await context.newPage(); expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'); await context.close(); @@ -55,10 +55,15 @@ it('should throw for invalid timezone IDs when creating pages', async ({ browser expect(error.message).toContain(`Expected "timezone" to be a valid timezone ID (e.g., "Europe/Berlin") or a valid timezone offset (e.g., "+01:00"), got ${timezoneId}`); } else { let error = null; - const context = await browser.newContext({ timezoneId }); - await context.newPage().catch(e => error = e); + let context = null; + try { + context = await browser.newContext({ timezoneId }); + await context.newPage(); + } catch (e) { + error = e; + } expect(error.message).toContain(`Invalid timezone ID: ${timezoneId}`); - await context.close(); + await context?.close(); } } }); From 414c4f5e0a507ea94c3ff4a5924bbda1a7665d50 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 21 Nov 2025 15:20:04 +0100 Subject: [PATCH 2/9] cherry-pick(#38301): fix(android): dont pass "--disable-sync" (#38301) --- packages/playwright-core/src/server/android/android.ts | 2 +- .../playwright-core/src/server/chromium/chromiumSwitches.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index 077d0569bcb6c..81b3346dab751 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -287,7 +287,7 @@ export class AndroidDevice extends SdkObject { '--disable-fre', '--no-default-browser-check', `--remote-debugging-socket-name=${socketName}`, - ...chromiumSwitches(), + ...chromiumSwitches(undefined, undefined, true), ...this._innerDefaultArgs(options) ]; return chromeArguments; diff --git a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts index ca98372dd56d5..28d6928455ad2 100644 --- a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts +++ b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts @@ -47,7 +47,7 @@ const disabledFeatures = (assistantMode?: boolean) => [ assistantMode ? 'AutomationControlled' : '', ].filter(Boolean); -export const chromiumSwitches = (assistantMode?: boolean, channel?: string) => [ +export const chromiumSwitches = (assistantMode?: boolean, channel?: string, android?: boolean) => [ '--disable-field-trial-config', // https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/README.md '--disable-background-networking', '--disable-background-timer-throttling', @@ -90,5 +90,5 @@ export const chromiumSwitches = (assistantMode?: boolean, channel?: string) => [ // Less annoying popups. '--disable-search-engine-choice-screen', // Prevents the "three dots" menu crash in IdentityManager::HasPrimaryAccount for ephemeral contexts. - '--disable-sync', + android ? '' : '--disable-sync', ].filter(Boolean); From 54818c59f107e73d610c7476b44dec4c7439c3dc Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 25 Nov 2025 11:17:00 +0100 Subject: [PATCH 3/9] chore: mark 1.57.0 (#38320) --- 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 94d7eff8a761f..8cd7b20aa7414 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "playwright-internal", - "version": "1.57.0-next", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "playwright-internal", - "version": "1.57.0-next", + "version": "1.57.0", "license": "Apache-2.0", "workspaces": [ "packages/*" @@ -8135,10 +8135,10 @@ "version": "0.0.0" }, "packages/playwright": { - "version": "1.57.0-next", + "version": "1.57.0", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" }, "bin": { "playwright": "cli.js" @@ -8152,11 +8152,11 @@ }, "packages/playwright-browser-chromium": { "name": "@playwright/browser-chromium", - "version": "1.57.0-next", + "version": "1.57.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" }, "engines": { "node": ">=18" @@ -8164,11 +8164,11 @@ }, "packages/playwright-browser-firefox": { "name": "@playwright/browser-firefox", - "version": "1.57.0-next", + "version": "1.57.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" }, "engines": { "node": ">=18" @@ -8176,22 +8176,22 @@ }, "packages/playwright-browser-webkit": { "name": "@playwright/browser-webkit", - "version": "1.57.0-next", + "version": "1.57.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" }, "engines": { "node": ">=18" } }, "packages/playwright-chromium": { - "version": "1.57.0-next", + "version": "1.57.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" }, "bin": { "playwright": "cli.js" @@ -8205,14 +8205,14 @@ "version": "0.0.0", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" }, "engines": { "node": ">=18" } }, "packages/playwright-core": { - "version": "1.57.0-next", + "version": "1.57.0", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -8223,11 +8223,11 @@ }, "packages/playwright-ct-core": { "name": "@playwright/experimental-ct-core", - "version": "1.57.0-next", + "version": "1.57.0", "license": "Apache-2.0", "dependencies": { - "playwright": "1.57.0-next", - "playwright-core": "1.57.0-next", + "playwright": "1.57.0", + "playwright-core": "1.57.0", "vite": "^6.4.1" }, "engines": { @@ -8236,10 +8236,10 @@ }, "packages/playwright-ct-react": { "name": "@playwright/experimental-ct-react", - "version": "1.57.0-next", + "version": "1.57.0", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.57.0-next", + "@playwright/experimental-ct-core": "1.57.0", "@vitejs/plugin-react": "^4.2.1" }, "bin": { @@ -8251,10 +8251,10 @@ }, "packages/playwright-ct-react17": { "name": "@playwright/experimental-ct-react17", - "version": "1.57.0-next", + "version": "1.57.0", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.57.0-next", + "@playwright/experimental-ct-core": "1.57.0", "@vitejs/plugin-react": "^4.2.1" }, "bin": { @@ -8266,10 +8266,10 @@ }, "packages/playwright-ct-svelte": { "name": "@playwright/experimental-ct-svelte", - "version": "1.57.0-next", + "version": "1.57.0", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.57.0-next", + "@playwright/experimental-ct-core": "1.57.0", "@sveltejs/vite-plugin-svelte": "^5.1.0" }, "bin": { @@ -8284,10 +8284,10 @@ }, "packages/playwright-ct-vue": { "name": "@playwright/experimental-ct-vue", - "version": "1.57.0-next", + "version": "1.57.0", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.57.0-next", + "@playwright/experimental-ct-core": "1.57.0", "@vitejs/plugin-vue": "^5.2.0" }, "bin": { @@ -8298,11 +8298,11 @@ } }, "packages/playwright-firefox": { - "version": "1.57.0-next", + "version": "1.57.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" }, "bin": { "playwright": "cli.js" @@ -8313,10 +8313,10 @@ }, "packages/playwright-test": { "name": "@playwright/test", - "version": "1.57.0-next", + "version": "1.57.0", "license": "Apache-2.0", "dependencies": { - "playwright": "1.57.0-next" + "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" @@ -8326,11 +8326,11 @@ } }, "packages/playwright-webkit": { - "version": "1.57.0-next", + "version": "1.57.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" }, "bin": { "playwright": "cli.js" diff --git a/package.json b/package.json index ef4154d72d70f..d3bc13c15995f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "playwright-internal", "private": true, - "version": "1.57.0-next", + "version": "1.57.0", "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 5223d8f541264..335a233328caf 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.57.0-next", + "version": "1.57.0", "description": "Playwright package that automatically installs Chromium", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" } } diff --git a/packages/playwright-browser-firefox/package.json b/packages/playwright-browser-firefox/package.json index a5d47c473a299..a297c4569a270 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.57.0-next", + "version": "1.57.0", "description": "Playwright package that automatically installs Firefox", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" } } diff --git a/packages/playwright-browser-webkit/package.json b/packages/playwright-browser-webkit/package.json index 3b0dd5d08f46b..b09c1ce26a182 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.57.0-next", + "version": "1.57.0", "description": "Playwright package that automatically installs WebKit", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" } } diff --git a/packages/playwright-chromium/package.json b/packages/playwright-chromium/package.json index 404ece9e1053d..ca2c290528901 100644 --- a/packages/playwright-chromium/package.json +++ b/packages/playwright-chromium/package.json @@ -1,6 +1,6 @@ { "name": "playwright-chromium", - "version": "1.57.0-next", + "version": "1.57.0", "description": "A high-level API to automate Chromium", "repository": { "type": "git", @@ -30,6 +30,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" } } diff --git a/packages/playwright-client/package.json b/packages/playwright-client/package.json index bc76d8a5daf44..914fc9c2b4ea5 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.57.0-next" + "playwright-core": "1.57.0" } } diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json index f4d18a0209cef..18f055d414cdd 100644 --- a/packages/playwright-core/package.json +++ b/packages/playwright-core/package.json @@ -1,6 +1,6 @@ { "name": "playwright-core", - "version": "1.57.0-next", + "version": "1.57.0", "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 2aa743185892d..0b269b3455513 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.57.0-next", + "version": "1.57.0", "description": "Playwright Component Testing Helpers", "repository": { "type": "git", @@ -26,8 +26,8 @@ } }, "dependencies": { - "playwright-core": "1.57.0-next", + "playwright-core": "1.57.0", "vite": "^6.4.1", - "playwright": "1.57.0-next" + "playwright": "1.57.0" } } diff --git a/packages/playwright-ct-react/package.json b/packages/playwright-ct-react/package.json index 6887fb87d7020..d9a1b682b3f73 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.57.0-next", + "version": "1.57.0", "description": "Playwright Component Testing for React", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.57.0-next", + "@playwright/experimental-ct-core": "1.57.0", "@vitejs/plugin-react": "^4.2.1" }, "bin": { diff --git a/packages/playwright-ct-react17/package.json b/packages/playwright-ct-react17/package.json index 01129d68ee6c5..0831475069f81 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.57.0-next", + "version": "1.57.0", "description": "Playwright Component Testing for React", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.57.0-next", + "@playwright/experimental-ct-core": "1.57.0", "@vitejs/plugin-react": "^4.2.1" }, "bin": { diff --git a/packages/playwright-ct-svelte/package.json b/packages/playwright-ct-svelte/package.json index 4ec07ebccc68a..5dd75bb90e199 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.57.0-next", + "version": "1.57.0", "description": "Playwright Component Testing for Svelte", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.57.0-next", + "@playwright/experimental-ct-core": "1.57.0", "@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 9c8847635d478..db7f67c3cbcf0 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.57.0-next", + "version": "1.57.0", "description": "Playwright Component Testing for Vue", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.57.0-next", + "@playwright/experimental-ct-core": "1.57.0", "@vitejs/plugin-vue": "^5.2.0" }, "bin": { diff --git a/packages/playwright-firefox/package.json b/packages/playwright-firefox/package.json index e707c26ae4476..0312aca1f38c3 100644 --- a/packages/playwright-firefox/package.json +++ b/packages/playwright-firefox/package.json @@ -1,6 +1,6 @@ { "name": "playwright-firefox", - "version": "1.57.0-next", + "version": "1.57.0", "description": "A high-level API to automate Firefox", "repository": { "type": "git", @@ -30,6 +30,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" } } diff --git a/packages/playwright-test/package.json b/packages/playwright-test/package.json index 4bfa3ddce7205..bef46d8992bee 100644 --- a/packages/playwright-test/package.json +++ b/packages/playwright-test/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/test", - "version": "1.57.0-next", + "version": "1.57.0", "description": "A high-level API to automate web browsers", "repository": { "type": "git", @@ -30,6 +30,6 @@ }, "scripts": {}, "dependencies": { - "playwright": "1.57.0-next" + "playwright": "1.57.0" } } diff --git a/packages/playwright-webkit/package.json b/packages/playwright-webkit/package.json index b51f02e29ebe4..d7a32d58449b0 100644 --- a/packages/playwright-webkit/package.json +++ b/packages/playwright-webkit/package.json @@ -1,6 +1,6 @@ { "name": "playwright-webkit", - "version": "1.57.0-next", + "version": "1.57.0", "description": "A high-level API to automate WebKit", "repository": { "type": "git", @@ -30,6 +30,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" } } diff --git a/packages/playwright/package.json b/packages/playwright/package.json index 9584ab2b878a8..a81f8b31c258d 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -1,6 +1,6 @@ { "name": "playwright", - "version": "1.57.0-next", + "version": "1.57.0", "description": "A high-level API to automate web browsers", "repository": { "type": "git", @@ -64,7 +64,7 @@ }, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0-next" + "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" From 80581972582c9565e141c5fedd3c5fa10cc0e38b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 25 Nov 2025 11:09:27 +0000 Subject: [PATCH 4/9] cherry-pick(#38328): docs: update 1.57 release notes --- docs/src/release-notes-js.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index 5a2a533f45293..af898272a226b 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -16,14 +16,13 @@ In HTML reporter, there's a new tab we call "Speedboard": It shows you all your executed tests sorted by slowness, and can help you understand where your test suite is taking longer than expected. -Take a look at yours - maybe you'll find some tests that are spending a longer time waiting than they should! +Take a look at yours - maybe you'll find some tests that are spending a longer time waiting than they should! ### Chrome for Testing -Starting with this release, Playwright switches from Chromium, to using [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) builds. -Both headed and headless browsers are subject to this. -Your tests should still be passing after upgrading to Playwright 1.57. -We're expecting no functional changes to come from this switch - the biggest change is the new icon and title in your toolbar: +Starting with this release, Playwright switches from Chromium, to using [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) builds. Both headed and headless browsers are subject to this. Your tests should still be passing after upgrading to Playwright 1.57. + +We're expecting no functional changes to come from this switch. The biggest change is the new icon and title in your toolbar. ![new and old logo](./images/cft-logo-change.png) @@ -60,8 +59,7 @@ test('homepage', async ({ page }) => { }); ``` -This is not just useful for capturing varying ports of dev servers: -You can also use it to wait for readiness of a service that doesn't expose an HTTP readiness check, but instead prints a readiness message to stdout or stderr. +This is not just useful for capturing varying ports of dev servers. You can also use it to wait for readiness of a service that doesn't expose an HTTP readiness check, but instead prints a readiness message to stdout or stderr. ### Breaking Change @@ -70,12 +68,11 @@ After 3 years of being deprecated, we removed `Page#accessibility` from our API. ### New APIs - New property [`property: TestConfig.tag`] adds a tag to all tests in this run. This is useful when using [merge-reports](./test-sharding.md#merging-reports-from-multiple-shards). -- [`event: Worker.console`] event is emitted when JavaScript within the worker calls one of console API methods, e.g. console.log or console.dir. [`method: Worker.waitForEvent`] can be used to wait for it. You can opt out of this using the `PLAYWRIGHT_DISABLE_SERVICE_WORKER_CONSOLE` environment variable. +- [`event: Worker.console`] event is emitted when JavaScript within the worker calls one of console API methods, e.g. console.log or console.dir. [`method: Worker.waitForEvent`] can be used to wait for it. - [`method: Locator.description`] returns locator description previously set with [`method: Locator.describe`], and `Locator.toString()` now uses the description when available. -- New option [`option: Locator.click.steps`] in [`method: Locator.click`] and [`method: Locator.dragTo`] that configures the number of `mousemove` events emitted while moving the mouse pointer to the target element. +- New option [`option: Locator.click.steps`] in [`method: Locator.click`] and [`method: Locator.dragTo`] that configures the number of `mousemove` events emitted while moving the mouse pointer to the target element. - Network requests issued by [Service Workers](./service-workers.md#network-events-and-routing) are now reported and can be routed through the [BrowserContext](./api/class-browsercontext.md), only in Chromium. You can opt out using the `PLAYWRIGHT_DISABLE_SERVICE_WORKER_NETWORK` environment variable. -- New methods [`method: Request.postData`], [`method: Request.postDataBuffer`] and [`method: Request.postDataJSON`]. -- Option [`property: TestConfig.webServer`] added a `wait` field to check readiness based on stdout/stderr. +- Console messages from Service Workers are dispatched through [`event: Worker.console`]. You can opt out of this using the `PLAYWRIGHT_DISABLE_SERVICE_WORKER_CONSOLE` environment variable. ### Browser Versions From b3a059fef6100949a3a0aac1645033ed28c7a96e Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 2 Dec 2025 08:21:47 -0800 Subject: [PATCH 5/9] cherry-pick(#38385): docs: Worker.waitForConsoleMessage in java --- docs/src/api/class-worker.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/src/api/class-worker.md b/docs/src/api/class-worker.md index bd41abc1cdc62..5fc5992affc23 100644 --- a/docs/src/api/class-worker.md +++ b/docs/src/api/class-worker.md @@ -130,6 +130,25 @@ Performs action and waits for the Worker to close. ### param: Worker.waitForClose.callback = %%-java-wait-for-event-callback-%% * since: v1.9 +## async method: Worker.waitForConsoleMessage +* since: v1.57 +* langs: java +- returns: <[ConsoleMessage]> + +Performs action and waits for a console message. + +### option: Worker.waitForConsoleMessage.predicate +* since: v1.57 +- `predicate` <[function]\([ConsoleMessage]\):[boolean]> + +Receives the [ConsoleMessage] object and resolves to true when the waiting should resolve. + +### option: Worker.waitForConsoleMessage.timeout = %%-wait-for-event-timeout-%% +* since: v1.57 + +### param: Worker.waitForConsoleMessage.callback = %%-java-wait-for-event-callback-%% +* since: v1.57 + ## async method: Worker.waitForEvent * since: v1.57 * langs: js, python From 32175dec53536057a8bc8475d66985437de9e861 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 3 Dec 2025 21:03:37 +0000 Subject: [PATCH 6/9] cherry-pick(#38407): docs: 1.57 release notes for languages (#38407) --- docs/src/release-notes-csharp.md | 31 ++++++++++++++++++++++++++++++ docs/src/release-notes-java.md | 31 ++++++++++++++++++++++++++++++ docs/src/release-notes-js.md | 4 ++-- docs/src/release-notes-python.md | 33 ++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 2 deletions(-) diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index 099ccd0d73a99..d41f932f71bdc 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -4,6 +4,37 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.57 + +### Chrome for Testing + +Starting with this release, Playwright switches from Chromium, to using [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) builds. Both headed and headless browsers are subject to this. Your tests should still be passing after upgrading to Playwright 1.57. + +We're expecting no functional changes to come from this switch. The biggest change is the new icon and title in your toolbar. + +![new and old logo](./images/cft-logo-change.png) + +If you still see an unexpected behaviour change, please [file an issue](https://github.com/microsoft/playwright/issues/new). + +On Arm64 Linux, Playwright continues to use Chromium. + +### Breaking Change + +After 3 years of being deprecated, we removed `Page.Accessibility` from our API. Please use other libraries such as [Axe](https://www.deque.com/axe/) if you need to test page accessibility. See our Node.js [guide](https://playwright.dev/docs/accessibility-testing) for integration with Axe. + +### New APIs + +- [`event: Worker.console`] event is emitted when JavaScript within the worker calls one of console API methods, e.g. console.log or console.dir. +- [`method: Locator.description`] returns locator description previously set with [`method: Locator.describe`]. +- New option [`option: Locator.click.steps`] in [`method: Locator.click`] and [`method: Locator.dragTo`] that configures the number of `mousemove` events emitted while moving the mouse pointer to the target element. + +### Browser Versions + +- Chromium 143.0.7499.4 +- Mozilla Firefox 144.0.2 +- WebKit 26.0 + + ## Version 1.56 ### New APIs diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index b8f877419a880..5081bef05fa61 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -4,6 +4,37 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.57 + +### Chrome for Testing + +Starting with this release, Playwright switches from Chromium, to using [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) builds. Both headed and headless browsers are subject to this. Your tests should still be passing after upgrading to Playwright 1.57. + +We're expecting no functional changes to come from this switch. The biggest change is the new icon and title in your toolbar. + +![new and old logo](./images/cft-logo-change.png) + +If you still see an unexpected behaviour change, please [file an issue](https://github.com/microsoft/playwright/issues/new). + +On Arm64 Linux, Playwright continues to use Chromium. + +### Breaking Change + +After 3 years of being deprecated, we removed `page.accessibility()` from our API. Please use other libraries such as [Axe](https://www.deque.com/axe/) if you need to test page accessibility. See our Node.js [guide](https://playwright.dev/docs/accessibility-testing) for integration with Axe. + +### New APIs + +- [`event: Worker.console`] event is emitted when JavaScript within the worker calls one of console API methods, e.g. console.log or console.dir. [`method: Worker.waitForConsoleMessage`] can be used to wait for it. +- [`method: Locator.description`] returns locator description previously set with [`method: Locator.describe`]. +- New option [`option: Locator.click.steps`] in [`method: Locator.click`] and [`method: Locator.dragTo`] that configures the number of `mousemove` events emitted while moving the mouse pointer to the target element. + +### Browser Versions + +- Chromium 143.0.7499.4 +- Mozilla Firefox 144.0.2 +- WebKit 26.0 + + ## Version 1.56 ### New APIs diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index af898272a226b..2ab8bccdd4045 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -63,7 +63,7 @@ This is not just useful for capturing varying ports of dev servers. You can also ### Breaking Change -After 3 years of being deprecated, we removed `Page#accessibility` from our API. Please use other libraries such as [Axe](https://www.deque.com/axe/) if you need to test page accessibility. See our Node.js [guide](https://playwright.dev/docs/accessibility-testing) for integration with Axe. +After 3 years of being deprecated, we removed `page.accessibility` from our API. Please use other libraries such as [Axe](https://www.deque.com/axe/) if you need to test page accessibility. See our Node.js [guide](https://playwright.dev/docs/accessibility-testing) for integration with Axe. ### New APIs @@ -77,7 +77,7 @@ After 3 years of being deprecated, we removed `Page#accessibility` from our API. ### Browser Versions - Chromium 143.0.7499.4 -- Mozilla Firefox 142.0.1 +- Mozilla Firefox 144.0.2 - WebKit 26.0 ## Version 1.56 diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md index 5c5359b352fc9..ae51bad27174b 100644 --- a/docs/src/release-notes-python.md +++ b/docs/src/release-notes-python.md @@ -4,6 +4,39 @@ title: "Release notes" toc_max_heading_level: 2 --- +## Version 1.57 + +### Chrome for Testing + +Starting with this release, Playwright switches from Chromium, to using [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) builds. Both headed and headless browsers are subject to this. Your tests should still be passing after upgrading to Playwright 1.57. + +We're expecting no functional changes to come from this switch. The biggest change is the new icon and title in your toolbar. + +![new and old logo](./images/cft-logo-change.png) + +If you still see an unexpected behaviour change, please [file an issue](https://github.com/microsoft/playwright/issues/new). + +On Arm64 Linux, Playwright continues to use Chromium. + +### Breaking Change + +After 3 years of being deprecated, we removed `page.accessibility` from our API. Please use other libraries such as [Axe](https://www.deque.com/axe/) if you need to test page accessibility. See our Node.js [guide](https://playwright.dev/docs/accessibility-testing) for integration with Axe. + +### New APIs + +- [`event: Worker.console`] event is emitted when JavaScript within the worker calls one of console API methods, e.g. console.log or console.dir. [`method: Worker.waitForEvent`] can be used to wait for it. +- [`method: Locator.description`] returns locator description previously set with [`method: Locator.describe`]. +- New option [`option: Locator.click.steps`] in [`method: Locator.click`] and [`method: Locator.dragTo`] that configures the number of `mousemove` events emitted while moving the mouse pointer to the target element. +- Network requests issued by [Service Workers](./service-workers.md#network-events-and-routing) are now reported and can be routed through the [BrowserContext](./api/class-browsercontext.md), only in Chromium. You can opt out using the `PLAYWRIGHT_DISABLE_SERVICE_WORKER_NETWORK` environment variable. +- Console messages from Service Workers are dispatched through [`event: Worker.console`]. You can opt out of this using the `PLAYWRIGHT_DISABLE_SERVICE_WORKER_CONSOLE` environment variable. + +### Browser Versions + +- Chromium 143.0.7499.4 +- Mozilla Firefox 144.0.2 +- WebKit 26.0 + + ## Version 1.56 ### New APIs From c0bdd1db55c71b0449b9f0a146ce407da8d564a8 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 4 Dec 2025 16:30:31 +0000 Subject: [PATCH 7/9] cherry-pick(#38432): docs: service-workers doc for python (#38432) --- docs/src/api/class-request.md | 2 +- ...ers-js.md => service-workers-js-python.md} | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) rename docs/src/{service-workers-js.md => service-workers-js-python.md} (75%) diff --git a/docs/src/api/class-request.md b/docs/src/api/class-request.md index e13d9de69f32f..8552af4670025 100644 --- a/docs/src/api/class-request.md +++ b/docs/src/api/class-request.md @@ -287,7 +287,7 @@ Returns the matching [Response] object, or `null` if the response was not receiv ## method: Request.serviceWorker * since: v1.24 -* langs: js +* langs: js, python - returns: <[null]|[Worker]> The Service [Worker] that is performing the request. diff --git a/docs/src/service-workers-js.md b/docs/src/service-workers-js-python.md similarity index 75% rename from docs/src/service-workers-js.md rename to docs/src/service-workers-js-python.md index 4db2e32a35284..2721e5800c8a8 100644 --- a/docs/src/service-workers-js.md +++ b/docs/src/service-workers-js-python.md @@ -21,6 +21,7 @@ They can act as a **network proxy** between the page and the external network to 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 +* langs: js 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. @@ -36,6 +37,24 @@ export default defineConfig({ }); ``` +## How to Disable Service Workers +* langs: python + +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 `service_workers` context option to `"block"`. + +```python title="conftest.py" +import pytest + +@pytest.fixture(scope="session") +def browser_context_args(browser_context_args): + return { + **browser_context_args, + "service_workers": "block" + } +``` + ## 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): @@ -46,6 +65,18 @@ await page.goto('/example-with-a-service-worker.html'); const serviceworker = await serviceWorkerPromise; ``` +```python sync +with context.expect_event("serviceworker") as worker_info: + page.goto("/example-with-a-service-worker.html") +service_worker = worker_info.value +``` + +```python async +async with context.expect_event("serviceworker") as worker_info: + await page.goto("/example-with-a-service-worker.html") +service_worker = await worker_info.value +``` + [`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: @@ -61,6 +92,28 @@ await page.evaluate(async () => { }); ``` +```python sync +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); + }); +}""") +``` + +```python async +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: @@ -126,6 +179,28 @@ await context.route('**', async route => { }); ``` +```python sync +def handle_route(route: Route): + if route.request.service_worker: + # NB: accessing route.request.frame here would THROW + route.fulfill(content_type="text/plain", status=200, body="from sw") + else: + route.continue_() + +context.route("**", handle_route) +``` + +```python async +async def handle_route(route: Route): + if route.request.service_worker: + # NB: accessing route.request.frame here would THROW + await route.fulfill(content_type="text/plain", status=200, body="from sw") + else: + await route.continue_() + +await context.route("**", handle_route) +``` + ## Known Limitations Requests for updated Service Worker main script code currently cannot be routed (https://github.com/microsoft/playwright/issues/14711). From a1d73648ba62ecb10e8763b3cd568caf28b48152 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 16 Dec 2025 15:58:56 -0800 Subject: [PATCH 8/9] cherry-pick(#38584): fix(esm): fix source maps in esm loader --- .../playwright/src/transform/esmLoader.ts | 26 ++++++++++--------- .../playwright/src/transform/transform.ts | 26 ++++++++++++------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/packages/playwright/src/transform/esmLoader.ts b/packages/playwright/src/transform/esmLoader.ts index d32537be5eb12..2d8622bac8f40 100644 --- a/packages/playwright/src/transform/esmLoader.ts +++ b/packages/playwright/src/transform/esmLoader.ts @@ -22,9 +22,14 @@ import { PortTransport } from './portTransport'; import { resolveHook, setSingleTSConfig, setTransformConfig, shouldTransform, transformHook } from './transform'; import { fileIsModule } from '../util'; +// Before each import of the ESM module, a preflight request with the .esm.preflight extension is issued. +// When handled, it is resolved similarly to the reqular import, but loading it yields empty content. +const esmPreflightExtension = '.esm.preflight'; + // Node < 18.6: defaultResolve takes 3 arguments. // Node >= 18.6: nextResolve from the chain takes 2 arguments. -async function resolve(specifier: string, context: { parentURL?: string }, defaultResolve: Function) { +async function resolve(originalSpecifier: string, context: { parentURL?: string }, defaultResolve: Function) { + let specifier = originalSpecifier.replace(esmPreflightExtension, ''); if (context.parentURL && context.parentURL.startsWith('file://')) { const filename = url.fileURLToPath(context.parentURL); const resolved = resolveHook(filename, specifier); @@ -37,6 +42,8 @@ async function resolve(specifier: string, context: { parentURL?: string }, defau if (result?.url && result.url.startsWith('file://')) currentFileDepsCollector()?.add(url.fileURLToPath(result.url)); + if (originalSpecifier.endsWith(esmPreflightExtension)) + result.url = result.url + esmPreflightExtension; return result; } @@ -54,7 +61,8 @@ const kSupportedFormats = new Map([ // Node < 18.6: defaultLoad takes 3 arguments. // Node >= 18.6: nextLoad from the chain takes 2 arguments. -async function load(moduleUrl: string, context: { format?: string }, defaultLoad: Function) { +async function load(originalModuleUrl: string, context: { format?: string }, defaultLoad: Function) { + const moduleUrl = originalModuleUrl.replace(esmPreflightExtension, ''); // Bail out for wasm, json, etc. if (!kSupportedFormats.has(context.format)) return defaultLoad(moduleUrl, context, defaultLoad); @@ -71,29 +79,23 @@ async function load(moduleUrl: string, context: { format?: string }, defaultLoad const code = fs.readFileSync(filename, 'utf-8'); const transformed = transformHook(code, filename, moduleUrl); - // Flush the source maps to the main thread, so that errors during import() are source-mapped. - if (transformed.serializedCache) { - if (legacyWaitForSourceMaps) - await transport?.send('pushToCompilationCache', { cache: transformed.serializedCache }); - else - transport?.post('pushToCompilationCache', { cache: transformed.serializedCache }); - } + // Flush the source maps to the main thread, so that errors after import() are source-mapped. + if (transformed.serializedCache) + transport?.post('pushToCompilationCache', { cache: transformed.serializedCache }); // Output format is required, so we determine it manually when unknown. // shortCircuit is required by Node >= 18.6 to designate no more loaders should be called. return { format: kSupportedFormats.get(context.format) || (fileIsModule(filename) ? 'module' : 'commonjs'), - source: transformed.code, + source: originalModuleUrl.endsWith(esmPreflightExtension) ? `void 0;` : transformed.code, shortCircuit: true, }; } let transport: PortTransport | undefined; -let legacyWaitForSourceMaps = false; function initialize(data: { port: MessagePort }) { transport = createTransport(data?.port); - legacyWaitForSourceMaps = !!process.env.PLAYWRIGHT_WAIT_FOR_SOURCE_MAPS; } function createTransport(port: MessagePort) { diff --git a/packages/playwright/src/transform/transform.ts b/packages/playwright/src/transform/transform.ts index 18a99b2b24753..0aa637572b3bf 100644 --- a/packages/playwright/src/transform/transform.ts +++ b/packages/playwright/src/transform/transform.ts @@ -260,16 +260,20 @@ function calculateHash(content: string, filePath: string, isModule: boolean, plu export async function requireOrImport(file: string) { installTransformIfNeeded(); const isModule = fileIsModule(file); - const esmImport = () => eval(`import(${JSON.stringify(url.pathToFileURL(file))})`); if (isModule) { - return await esmImport().finally(async () => { - // Compilation cache, which includes source maps, is populated in a post task. - // When importing a module results in an error, the very next access to `error.stack` - // will need source maps. To make sure source maps have arrived, we insert a task - // that will be processed after compilation cache and guarantee that - // source maps are available, before `error.stack` is accessed. - await new Promise(resolve => setTimeout(resolve, 0)); - }); + const fileName = url.pathToFileURL(file); + const esmImport = () => eval(`import(${JSON.stringify(fileName)})`); + + // For ESM imports, issue a preflight to populate the compilation cache with the + // source maps. This allows inline test() calls to resolve wrapFunctionWithLocation. + await eval(`import(${JSON.stringify(fileName + '.esm.preflight')})`).finally(nextTask); + + // Compilation cache, which includes source maps, is populated in a post task. + // When importing a module results in an error, the very next access to `error.stack` + // will need source maps. To make sure source maps have arrived, we insert a task + // that will be processed after compilation cache and guarantee that + // source maps are available, before `error.stack` is accessed. + return await esmImport().finally(nextTask); } const result = require(file); const depsCollector = currentFileDepsCollector(); @@ -344,3 +348,7 @@ export function wrapFunctionWithLocation(func: (location: Lo function isRelativeSpecifier(specifier: string) { return specifier === '.' || specifier === '..' || specifier.startsWith('./') || specifier.startsWith('../'); } + +async function nextTask() { + return new Promise(resolve => setTimeout(resolve, 0)); +} From 539000b9a72616541ad5995daaee82a691581645 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 16 Dec 2025 14:19:16 -0800 Subject: [PATCH 9/9] cherry-pick(#38566): fix(trace): operate trace uri --- .../src/server/trace/viewer/traceViewer.ts | 32 +++++++----- packages/trace-viewer/src/sw/main.ts | 50 ++++++++----------- packages/trace-viewer/src/sw/traceModel.ts | 1 - .../trace-viewer/src/sw/traceModelBackends.ts | 48 +++++------------- packages/trace-viewer/src/ui/modelUtil.ts | 10 ++-- packages/trace-viewer/src/ui/snapshotTab.tsx | 8 +-- packages/trace-viewer/src/ui/sourceTab.tsx | 2 +- .../trace-viewer/src/ui/uiModeTraceView.tsx | 13 ++--- packages/trace-viewer/src/ui/workbench.tsx | 10 +++- .../ui-mode-test-output.spec.ts | 1 + tests/playwright-test/ui-mode-trace.spec.ts | 35 +++++++++++++ utils/limits.sh | 2 + 12 files changed, 118 insertions(+), 94 deletions(-) create mode 100755 utils/limits.sh diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 7caaffe5cdfe6..0e83a15f387f7 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -55,25 +55,26 @@ export type TraceViewerAppOptions = { const tracesDirMarker = 'traces.dir'; -function validateTraceUrl(traceUrl: string | undefined): string | undefined { - if (!traceUrl) - return traceUrl; +function validateTraceUrl(traceFileOrUrl: string | undefined): string | undefined { + if (!traceFileOrUrl) + return traceFileOrUrl; - if (traceUrl.startsWith('http://') || traceUrl.startsWith('https://')) - return traceUrl; + if (traceFileOrUrl.startsWith('http://') || traceFileOrUrl.startsWith('https://')) + return traceFileOrUrl; + let traceFile = traceFileOrUrl; // If .json is requested, we'll synthesize it. - if (traceUrl.endsWith('.json')) - return traceUrl; + if (traceFile.endsWith('.json')) + return toFilePathUrl(traceFile); try { - const stat = fs.statSync(traceUrl); + const stat = fs.statSync(traceFile); // If the path is a directory, add 'trace.dir' which has a special handler. if (stat.isDirectory()) - return path.join(traceUrl, tracesDirMarker); - return traceUrl; + traceFile = path.join(traceFile, tracesDirMarker); + return toFilePathUrl(traceFile); } catch { - throw new Error(`Trace file ${traceUrl} does not exist!`); + throw new Error(`Trace file ${traceFileOrUrl} does not exist!`); } } @@ -270,13 +271,18 @@ function traceDescriptor(traceDir: string, tracePrefix: string | undefined) { for (const name of fs.readdirSync(traceDir)) { if (!tracePrefix || name.startsWith(tracePrefix)) - result.entries.push({ name, path: path.join(traceDir, name) }); + result.entries.push({ name, path: toFilePathUrl(path.join(traceDir, name)) }); } const resourcesDir = path.join(traceDir, 'resources'); if (fs.existsSync(resourcesDir)) { for (const name of fs.readdirSync(resourcesDir)) - result.entries.push({ name: 'resources/' + name, path: path.join(resourcesDir, name) }); + result.entries.push({ name: 'resources/' + name, path: toFilePathUrl(path.join(resourcesDir, name)) }); } return result; } + + +function toFilePathUrl(filePath: string): string { + return `file?path=${encodeURIComponent(filePath)}`; +} diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 4252972d072af..652c3ccb8d9d5 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -17,7 +17,7 @@ import { Progress, splitProgress } from './progress'; import { SnapshotServer } from './snapshotServer'; import { TraceModel } from './traceModel'; -import { FetchTraceModelBackend, traceFileURL, ZipTraceModelBackend } from './traceModelBackends'; +import { FetchTraceModelBackend, ZipTraceModelBackend } from './traceModelBackends'; import { TraceVersionError } from './traceModernizer'; type Client = { @@ -73,9 +73,9 @@ function simulateRestart() { clientIdToTraceUrls.clear(); } -async function loadTraceOrError(clientId: string, url: URL, isContextRequest: boolean, progress: Progress): Promise<{ loadedTrace?: LoadedTrace, errorResponse?: Response }> { +async function loadTraceOrError(clientId: string, url: URL, progress: Progress): Promise<{ loadedTrace?: LoadedTrace, errorResponse?: Response }> { try { - const loadedTrace = await loadTrace(clientId, url, isContextRequest, progress); + const loadedTrace = await loadTrace(clientId, url, progress); return { loadedTrace }; } catch (error) { return { @@ -87,29 +87,28 @@ async function loadTraceOrError(clientId: string, url: URL, isContextRequest: bo } } -function loadTrace(clientId: string, url: URL, isContextRequest: boolean, progress: Progress): Promise { - const traceUrl = url.searchParams.get('trace')!; - if (!traceUrl) +function loadTrace(clientId: string, url: URL, progress: Progress): Promise { + const traceUri = url.searchParams.get('trace')!; + if (!traceUri) throw new Error('trace parameter is missing'); - clientIdToTraceUrls.set(clientId, traceUrl); - const omitCache = isContextRequest && isLiveTrace(traceUrl); - const loadedTrace = omitCache ? undefined : loadedTraces.get(traceUrl); + clientIdToTraceUrls.set(clientId, traceUri); + const loadedTrace = loadedTraces.get(traceUri); if (loadedTrace) return loadedTrace; - const promise = innerLoadTrace(traceUrl, progress); - loadedTraces.set(traceUrl, promise); + const promise = innerLoadTrace(traceUri, progress); + loadedTraces.set(traceUri, promise); return promise; } -async function innerLoadTrace(traceUrl: string, progress: Progress): Promise { +async function innerLoadTrace(traceUri: string, progress: Progress): Promise { 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 = isLiveTrace(traceUrl) || traceUrl.endsWith('traces.dir') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress); + const backend = isLiveTrace(traceUri) || traceUri.endsWith('traces.dir') ? new FetchTraceModelBackend(traceUri) : new ZipTraceModelBackend(traceUri, fetchProgress); await traceModel.load(backend, unzipProgress); } catch (error: any) { // eslint-disable-next-line no-console @@ -117,8 +116,8 @@ async function innerLoadTrace(traceUrl: string, progress: Progress): Promise traceModel.resourceForSha1(sha1)); return { traceModel, snapshotServer }; @@ -159,7 +158,7 @@ async function doFetch(event: FetchEvent): Promise { // 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); + const { errorResponse, loadedTrace } = await loadTraceOrError(event.resultingClientId!, url, noopProgress); if (errorResponse) return errorResponse; const pageOrFrameId = relativePath.substring('/snapshot/'.length); @@ -178,7 +177,7 @@ async function doFetch(event: FetchEvent): Promise { if (!client) return new Response('Sub-resource without a client', { status: 500 }); - const { snapshotServer } = await loadTrace(client.id, new URL(client.url), false, clientProgress(client)); + const { snapshotServer } = await loadTrace(client.id, new URL(client.url), clientProgress(client)); if (!snapshotServer) return new Response(null, { status: 404 }); @@ -198,8 +197,7 @@ async function doFetch(event: FetchEvent): Promise { 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)); + const { errorResponse, loadedTrace } = await loadTraceOrError(client.id, url, clientProgress(client)); if (errorResponse) return errorResponse; @@ -228,13 +226,7 @@ async function doFetch(event: FetchEvent): Promise { } } - // Pass through to the server for file requests. - if (relativePath?.startsWith('/file/')) { - const path = url.searchParams.get('path')!; - return await fetch(traceFileURL(path)); - } - - // Static content for sub-resource. + // Pass through to the server for file requests and static content. return fetch(event.request); } @@ -276,8 +268,10 @@ function clientProgress(client: Client): Progress { function noopProgress(done: number, total: number): undefined { } -function isLiveTrace(traceUrl: string): boolean { - return traceUrl.endsWith('.json'); +function isLiveTrace(traceUri: string): boolean { + const url = new URL(traceUri, 'http://localhost'); + const path = url.searchParams.get('path'); + return !!path?.endsWith('.json'); } self.addEventListener('fetch', function(event: FetchEvent) { diff --git a/packages/trace-viewer/src/sw/traceModel.ts b/packages/trace-viewer/src/sw/traceModel.ts index e86129c77856d..19c0e25ec3b68 100644 --- a/packages/trace-viewer/src/sw/traceModel.ts +++ b/packages/trace-viewer/src/sw/traceModel.ts @@ -27,7 +27,6 @@ export interface TraceModelBackend { readText(entryName: string): Promise; readBlob(entryName: string): Promise; isLive(): boolean; - traceURL(): string; } export class TraceModel { diff --git a/packages/trace-viewer/src/sw/traceModelBackends.ts b/packages/trace-viewer/src/sw/traceModelBackends.ts index da60e9c1e9811..fe8641e45d1f3 100644 --- a/packages/trace-viewer/src/sw/traceModelBackends.ts +++ b/packages/trace-viewer/src/sw/traceModelBackends.ts @@ -26,14 +26,12 @@ type Progress = (done: number, total: number) => undefined; export class ZipTraceModelBackend implements TraceModelBackend { private _zipReader: zip.ZipReader; private _entriesPromise: Promise>; - private _traceURL: string; - constructor(traceURL: string, progress: Progress) { - this._traceURL = traceURL; + constructor(traceUri: string, progress: Progress) { zipjs.configure({ baseURL: self.location.href } as any); this._zipReader = new zipjs.ZipReader( - new zipjs.HttpReader(this._resolveTraceURL(traceURL), { mode: 'cors', preventHeadRequest: true } as any), + new zipjs.HttpReader(this._resolveTraceURI(traceUri), { mode: 'cors', preventHeadRequest: true } as any), { useWebWorkers: false }); this._entriesPromise = this._zipReader.getEntries({ onprogress: progress }).then(entries => { const map = new Map(); @@ -43,26 +41,16 @@ 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; + private _resolveTraceURI(traceUri: string): string { + if (traceUri.startsWith('https://www.dropbox.com/')) + return 'https://dl.dropboxusercontent.com/' + traceUri.substring('https://www.dropbox.com/'.length); + return traceUri; } isLive() { return false; } - traceURL() { - return this._traceURL; - } - async entryNames(): Promise { const entries = await this._entriesPromise; return [...entries.keys()]; @@ -96,11 +84,9 @@ export class ZipTraceModelBackend implements TraceModelBackend { export class FetchTraceModelBackend implements TraceModelBackend { private _entriesPromise: Promise>; - private _path: string; - constructor(path: string) { - this._path = path; - this._entriesPromise = this._readFile(path).then(async response => { + constructor(traceUri: string) { + this._entriesPromise = this._readFile(traceUri).then(async response => { if (!response) throw new Error('File not found'); const json = await response.json(); @@ -115,10 +101,6 @@ export class FetchTraceModelBackend implements TraceModelBackend { return true; } - traceURL(): string { - return this._path; - } - async entryNames(): Promise { const entries = await this._entriesPromise; return [...entries.keys()]; @@ -141,20 +123,16 @@ export class FetchTraceModelBackend implements TraceModelBackend { private async _readEntry(entryName: string): Promise { const entries = await this._entriesPromise; - const fileName = entries.get(entryName); - if (!fileName) + const fileUri = entries.get(entryName); + if (!fileUri) return; - return this._readFile(fileName); + return this._readFile(fileUri); } - private async _readFile(path: string): Promise { - const response = await fetch(traceFileURL(path)); + private async _readFile(uri: string): Promise { + const response = await fetch(uri); 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/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index 6203992a99b25..c2025248eb5df 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -86,14 +86,14 @@ export class MultiTraceModel { readonly sources: Map; resources: ResourceSnapshot[]; readonly actionCounters: Map; - readonly traceUrl: string; + readonly traceUri: string; - constructor(traceUrl: string, contexts: ContextEntry[]) { + constructor(traceUri: string, contexts: ContextEntry[]) { contexts.forEach(contextEntry => indexModel(contextEntry)); const libraryContext = contexts.find(context => context.origin === 'library'); - this.traceUrl = traceUrl; + this.traceUri = traceUri; this.browserName = libraryContext?.browserName || ''; this.sdkLanguage = libraryContext?.sdkLanguage; this.channel = libraryContext?.channel; @@ -114,7 +114,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, callId: action.callId, traceUrl })) ?? []); + this.attachments = this.actions.flatMap(action => action.attachments?.map(attachment => ({ ...attachment, callId: action.callId, traceUri })) ?? []); this.visibleAttachments = this.attachments.filter(attachment => !attachment.name.startsWith('_')); this.events.sort((a1, a2) => a1.time - a2.time); @@ -132,7 +132,7 @@ export class MultiTraceModel { createRelativeUrl(path: string) { const url = new URL('http://localhost/' + path); - url.searchParams.set('trace', this.traceUrl); + url.searchParams.set('trace', this.traceUri); return url.toString().substring('http://localhost/'.length); } diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 382e459829532..9b76744fb5431 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -57,7 +57,7 @@ export const SnapshotTabsView: React.FunctionComponent<{ }, [action]); const { snapshotInfoUrl, snapshotUrl, popoutUrl } = React.useMemo(() => { const snapshot = snapshots[snapshotTab]; - return model && snapshot ? extendSnapshot(model.traceUrl, snapshot, shouldPopulateCanvasFromScreenshot) : { snapshotInfoUrl: undefined, snapshotUrl: undefined, popoutUrl: undefined }; + return model && snapshot ? extendSnapshot(model.traceUri, 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]); @@ -415,9 +415,9 @@ export function collectSnapshots(action: ActionTraceEvent | undefined): Snapshot const isUnderTest = new URLSearchParams(window.location.search).has('isUnderTest'); -export function extendSnapshot(traceUrl: string, snapshot: Snapshot, shouldPopulateCanvasFromScreenshot: boolean): SnapshotUrls { +export function extendSnapshot(traceUri: string, snapshot: Snapshot, shouldPopulateCanvasFromScreenshot: boolean): SnapshotUrls { const params = new URLSearchParams(); - params.set('trace', traceUrl); + params.set('trace', traceUri); params.set('name', snapshot.snapshotName); if (isUnderTest) params.set('isUnderTest', 'true'); @@ -435,7 +435,7 @@ export function extendSnapshot(traceUrl: string, snapshot: Snapshot, shouldPopul const popoutParams = new URLSearchParams(); popoutParams.set('r', snapshotUrl); - popoutParams.set('trace', traceUrl); + popoutParams.set('trace', traceUri); const popoutUrl = new URL(`snapshot.html?${popoutParams.toString()}`, window.location.href).toString(); return { snapshotInfoUrl, snapshotUrl, popoutUrl }; } diff --git a/packages/trace-viewer/src/ui/sourceTab.tsx b/packages/trace-viewer/src/ui/sourceTab.tsx index 3799096cb1f4f..1d0d1c3947880 100644 --- a/packages/trace-viewer/src/ui/sourceTab.tsx +++ b/packages/trace-viewer/src/ui/sourceTab.tsx @@ -59,7 +59,7 @@ function useSources(stack: StackFrame[] | undefined, selectedFrame: number, sour if (!response || response.status === 404) response = await fetch(`file?path=${encodeURIComponent(file)}`); if (response.status >= 400) - source.content = ``; + source.content = ``; else source.content = await response.text(); } catch { diff --git a/packages/trace-viewer/src/ui/uiModeTraceView.tsx b/packages/trace-viewer/src/ui/uiModeTraceView.tsx index 211639103bd4c..03c5ba5727fe1 100644 --- a/packages/trace-viewer/src/ui/uiModeTraceView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTraceView.tsx @@ -54,7 +54,7 @@ export const TraceView: React.FC<{ // Test finished. const attachment = result && result.duration >= 0 && result.attachments.find(a => a.name === 'trace'); if (attachment && attachment.path) { - loadSingleTraceFile(attachment.path).then(model => setModel({ model, isLive: false })); + loadSingleTraceFile(attachment.path, result.startTime.getTime()).then(model => setModel({ model, isLive: false })); return; } @@ -65,14 +65,14 @@ export const TraceView: React.FC<{ const traceLocation = [ outputDir, - artifactsFolderName(result!.workerIndex), + artifactsFolderName(result.workerIndex), 'traces', `${item.testCase?.id}.json` ].join(pathSeparator); // Start polling running test. pollTimer.current = setTimeout(async () => { try { - const model = await loadSingleTraceFile(traceLocation); + const model = await loadSingleTraceFile(traceLocation, Date.now()); setModel({ model, isLive: true }); } catch { const model = new MultiTraceModel('', []); @@ -110,10 +110,11 @@ const outputDirForTestCase = (testCase: reporterTypes.TestCase): string | undefi return undefined; }; -async function loadSingleTraceFile(url: string): Promise { +async function loadSingleTraceFile(absolutePath: string, timestamp: number): Promise { + const traceUri = `file?path=${encodeURIComponent(absolutePath)}×tamp=${timestamp}`; const params = new URLSearchParams(); - params.set('trace', url); + params.set('trace', traceUri); const response = await fetch(`contexts?${params.toString()}`); const contextEntries = await response.json() as ContextEntry[]; - return new MultiTraceModel(url, contextEntries); + return new MultiTraceModel(traceUri, contextEntries); } diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 36a38d155ac29..6e34d878d4e65 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -63,7 +63,7 @@ export type WorkbenchProps = { }; export const Workbench: React.FunctionComponent = props => { - const partition = props.model?.traceUrl ?? 'default'; + const partition = traceUriToPartition(props.model?.traceUri); return ; @@ -441,3 +441,11 @@ const ActionsFilterButton: React.FC<{ counters?: Map; hiddenActi /> ; }; + +function traceUriToPartition(traceUri: string | undefined): string { + if (!traceUri) + return 'default'; + const url = new URL(traceUri, 'http://localhost'); + url.searchParams.delete('timestamp'); + return url.toString(); +} diff --git a/tests/playwright-test/ui-mode-test-output.spec.ts b/tests/playwright-test/ui-mode-test-output.spec.ts index 75aa19a66d7f9..1d7a03f3fc99b 100644 --- a/tests/playwright-test/ui-mode-test-output.spec.ts +++ b/tests/playwright-test/ui-mode-test-output.spec.ts @@ -84,6 +84,7 @@ test('should show console messages for test', async ({ runUITest }, testInfo) => test('print', async ({ page }) => { await page.evaluate(() => console.log('page message')); console.log('node message'); + await page.waitForTimeout(500); await page.evaluate(() => console.error('page error')); console.error('node error'); console.log('Colors: \x1b[31mRED\x1b[0m \x1b[32mGREEN\x1b[0m'); diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index ac62040b1b945..99fe82871ce14 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -763,3 +763,38 @@ test('should partition action tree state by test', async ({ runUITest }) => { - treeitem /Fixture \"context\"/ `); }); + +test('should update state on subsequent run', async ({ runUITest, writeFiles }) => { + const { page } = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('test1', async ({ page }) => { + await page.evaluate('1+1'); + }); + `, + }); + const actionsTree = page.getByTestId('actions-tree'); + + await page.getByTestId('test-tree').getByText('test1').click(); + await page.keyboard.press('Enter'); + + await expect(actionsTree).toMatchAriaSnapshot(` + - treeitem /Evaluate/ [selected] + `); + + await writeFiles({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('test1', async ({ page }) => { + expect(1).toBe(2); + await page.evaluate('1+1'); + }); + `, + }); + + await page.keyboard.press('Enter'); + + await expect(actionsTree).toMatchAriaSnapshot(` + - treeitem /Expect \"toBe\"/ + `); +}); diff --git a/utils/limits.sh b/utils/limits.sh new file mode 100755 index 0000000000000..71baec19a4b8f --- /dev/null +++ b/utils/limits.sh @@ -0,0 +1,2 @@ +#!/bin/sh +sudo sysctl -w fs.inotify.max_user_instances=1024