From 592e9432617ecee6e382c330a387a33bd9e398bd Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 25 Jun 2026 08:18:24 -0700 Subject: [PATCH 01/10] fix(mcp): never include data: URL payloads in snapshot or network output (#41450) --- packages/injected/src/ariaSnapshot.ts | 4 ++-- packages/isomorphic/stringUtils.ts | 11 +++++++++++ packages/playwright-core/src/tools/backend/network.ts | 5 +++-- tests/page/page-aria-snapshot-ai.spec.ts | 8 ++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index d99727d47640f..c72d3c64918f8 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -15,7 +15,7 @@ */ import * as aria from '@isomorphic/ariaSnapshot'; -import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace } from '@isomorphic/stringUtils'; +import { escapeRegExp, longestCommonSubstring, normalizeWhiteSpace, truncateDataUrl } from '@isomorphic/stringUtils'; import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from '@isomorphic/yaml'; import { computeBox, getElementComputedStyle, isElementVisible } from './domUtils'; @@ -184,7 +184,7 @@ export function generateAriaTree(rootElement: Element, publicOptions: AriaTreeOp if (ariaNode.role === 'link' && element.hasAttribute('href')) { const href = element.getAttribute('href')!; - ariaNode.props['url'] = href; + ariaNode.props['url'] = truncateDataUrl(href); } if (ariaNode.role === 'textbox' && element.hasAttribute('placeholder') && element.getAttribute('placeholder') !== ariaNode.name) { diff --git a/packages/isomorphic/stringUtils.ts b/packages/isomorphic/stringUtils.ts index c4a3476818962..8629b3813db03 100644 --- a/packages/isomorphic/stringUtils.ts +++ b/packages/isomorphic/stringUtils.ts @@ -136,6 +136,17 @@ export function trimStringWithEllipsis(input: string, cap: number): string { return trimString(input, cap, '\u2026'); } +export function truncateDataUrl(url: string): string { + // Data URLs can carry megabytes of base64 payload, which is never useful in + // human/AI-facing output. Keep the media type prefix for context, drop the data. + if (!url.startsWith('data:')) + return url; + const comma = url.indexOf(','); + if (comma === -1) + return url; + return url.slice(0, comma + 1) + '\u2026'; +} + export function escapeRegExp(s: string) { // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string diff --git a/packages/playwright-core/src/tools/backend/network.ts b/packages/playwright-core/src/tools/backend/network.ts index 9f33b1540c3c5..ded33eb582207 100644 --- a/packages/playwright-core/src/tools/backend/network.ts +++ b/packages/playwright-core/src/tools/backend/network.ts @@ -20,6 +20,7 @@ import * as z from 'zod'; import { getExtensionForMimeType, isTextualMimeType } from '@isomorphic/mimeType'; import { isRegexString } from '@isomorphic/rtti'; +import { truncateDataUrl } from '@isomorphic/stringUtils'; import { defineTool, defineTabTool } from './tool'; @@ -128,7 +129,7 @@ export function isFetch(request: playwright.Request): boolean { export function renderRequestLine(request: playwright.Request): string { const response = request.existingResponse(); - let line = `[${request.method().toUpperCase()}] ${request.url()}`; + let line = `[${request.method().toUpperCase()}] ${truncateDataUrl(request.url())}`; if (response) line += ` => [${response.status()}] ${response.statusText()}`; else if (request.failure()) @@ -140,7 +141,7 @@ function renderRequestDetails(index: number, request: playwright.Request, skillM const httpResponse = request.existingResponse(); const responseHeaders = httpResponse?.headers(); const lines: string[] = []; - lines.push(`#${index} [${request.method().toUpperCase()}] ${request.url()}`); + lines.push(`#${index} [${request.method().toUpperCase()}] ${truncateDataUrl(request.url())}`); lines.push(''); lines.push(' General'); diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 927a3dad967a5..69612aae55da2 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -318,6 +318,14 @@ it('should not nest cursor pointer hints', async ({ page }) => { `); }); +it('should truncate data url in link', async ({ page }) => { + const base64 = Buffer.from('

hello

').toString('base64'); + await page.setContent(`a link`); + const snapshot = await snapshotForAI(page); + expect(snapshot).toContain('/url: data:text/html;base64,…'); + expect(snapshot).not.toContain(base64); +}); + it('should gracefully fallback when child frame cant be captured', async ({ page, server }) => { await page.setContent(`

Test

From 1120121d1c0df50463c165abf06ad3907f366fee Mon Sep 17 00:00:00 2001 From: Alex Tumanov Date: Thu, 25 Jun 2026 10:20:02 -0500 Subject: [PATCH 02/10] test: cover MCP CDP header env config (#41452) --- tests/mcp/config-resolve.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/mcp/config-resolve.spec.ts b/tests/mcp/config-resolve.spec.ts index f4cc2c35f26e7..d961bfec20465 100644 --- a/tests/mcp/config-resolve.spec.ts +++ b/tests/mcp/config-resolve.spec.ts @@ -248,6 +248,21 @@ test.describe('merge order', () => { const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv); expect(config.browser.cdpHeaders).toEqual({ Authorization: 'Bearer token-from-file' }); }); + + test('env browser.cdpHeaders overrides config file and preserves colons in values', async ({}, testInfo) => { + const configFile = testInfo.outputPath('config.json'); + const fileConfig: Config = { + browser: { + cdpEndpoint: 'ws://example.invalid', + cdpHeaders: { Authorization: 'Bearer token-from-file' }, + }, + }; + await fs.promises.writeFile(configFile, JSON.stringify(fileConfig)); + const config = await resolveCLIConfigForMCP({ config: configFile }, { + PLAYWRIGHT_MCP_CDP_HEADERS: 'X-Forwarded-Proto: value:with:colons', + }); + expect(config.browser.cdpHeaders).toEqual({ 'X-Forwarded-Proto': 'value:with:colons' }); + }); }); // --------------------------------------------------------------------------- From 9dc3af1b2a88b11dd36f96cd967aad94137a6f31 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:30:41 -0700 Subject: [PATCH 03/10] chore: roll driver/Dockerfile to recent Node.js LTS version (#41461) 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 652b85248b694..da2c8ff30de34 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.17.0" # autogenerated via ./update-playwright-node.mjs +NODE_VERSION="24.18.0" # autogenerated via ./update-playwright-node.mjs cd "$(dirname "$0")" PACKAGE_VERSION=$(node -p "require('../../package.json').version") From 23985e75199e83c8223ec1c765b94d48bff1118f Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 25 Jun 2026 16:35:50 +0100 Subject: [PATCH 04/10] test: add test for allHeaders() on the worker script (#41459) --- tests/page/workers.spec.ts | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/page/workers.spec.ts b/tests/page/workers.spec.ts index f7f1b27360649..3da5821b57186 100644 --- a/tests/page/workers.spec.ts +++ b/tests/page/workers.spec.ts @@ -365,3 +365,58 @@ it('should support offline', async ({ page, server, browserName }) => { await page.context().setOffline(false); await expect.poll(() => worker.evaluate(() => navigator.onLine)).toBe(true); }); + +it('should resolve worker script allHeaders in main frame', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39948' }, +}, async function({ page, server, browserName }) { + const [request] = await Promise.all([ + page.waitForEvent('requestfinished', request => request.url() === server.PREFIX + '/worker/worker.js'), + page.goto(server.PREFIX + '/worker/worker.html'), + ]); + const response = await request.response(); + const requestHeaders = await request.allHeaders(); + expect(requestHeaders['host']).toBeTruthy(); + const responseHeaders = await response.allHeaders(); + expect(responseHeaders['content-type']).toBeTruthy(); +}); + +it('should resolve worker script allHeaders in iframe', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39948' }, +}, async function({ page, server, browserName }) { + it.fixme(browserName === 'chromium', 'https://github.com/microsoft/playwright/issues/39948'); + + const [request] = await Promise.all([ + page.waitForEvent('requestfinished', request => request.url() === server.PREFIX + '/worker/worker.js'), + attachFrame(page, 'frame1', server.PREFIX + '/worker/worker.html'), + ]); + const response = await request.response(); + const requestHeaders = await request.allHeaders(); + expect(requestHeaders['host']).toBeTruthy(); + const responseHeaders = await response.allHeaders(); + expect(responseHeaders['content-type']).toBeTruthy(); +}); + +it('should resolve worker script allHeaders in nested worker inside iframe', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39948' }, +}, async function({ page, server, browserName }) { + it.fixme(browserName === 'webkit', 'cannot evaluate in nested worker'); + it.fixme(browserName === 'firefox', 'nested worker script request is not reported at all'); + it.fixme(browserName === 'chromium', 'https://github.com/microsoft/playwright/issues/39948'); + + const [worker] = await Promise.all([ + page.waitForEvent('worker'), + attachFrame(page, 'frame1', server.PREFIX + '/worker/worker.html'), + ]); + + const url = server.PREFIX + '/worker/worker.js'; + const [request] = await Promise.all([ + page.waitForEvent('requestfinished', request => request.url() === url), + worker.evaluate(url => { + (self as any).w = new Worker(url); + }, url), + ]); + + const response = await request.response(); + const headers = await response.allHeaders(); + expect(headers['content-type']).toBeTruthy(); +}); From 0217f036eec61b85f056ac20352d0cd82c67ed35 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 25 Jun 2026 08:44:29 -0700 Subject: [PATCH 05/10] fix(aria): re-number main frame refs on cross-document navigation (#41453) Signed-off-by: Pavel Feldman Co-authored-by: Dmitry Gozman --- packages/playwright-core/src/server/frames.ts | 8 ++++- tests/page/page-aria-snapshot-ai.spec.ts | 35 +++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index dc8e86ef84607..ca307ec5d82df 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -235,6 +235,7 @@ export class FrameManager { const frame = this._frames.get(frameId)!; this.removeChildFramesRecursively(frame); this._clearWebSockets(frame); + const previousUrl = frame._url; frame._url = url; frame._name = name; @@ -269,6 +270,11 @@ export class FrameManager { if (!initial) { frame.apiLog(` navigated to "${url}"`); this._page.frameNavigatedToNewDocument(frame); + // Re-number the main frame when it navigates away from a real document, so that aria + // refs (fe) minted against the previous document do not accidentally resolve + // to elements in the new one. + if (frame === this._mainFrame && previousUrl && previousUrl !== 'about:blank') + frame.seq = this._allocateFrameSeq(); } // Restore pending if any - see comments above about keepPending. frame._setPendingDocument(keepPending); @@ -501,7 +507,7 @@ export class Frame extends SdkObject { static Events = FrameEvent; _id: string; - readonly seq: number; + seq: number; _firedLifecycleEvents = new Set(); private _firedNetworkIdleSelf = false; _currentDocument: DocumentInfo; diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 69612aae55da2..e77b8786ea1a7 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -142,6 +142,36 @@ it('should stitch all frame snapshots', async ({ page, server }) => { } }); +it('should re-number refs across navigations but not same-document navigations', async ({ page, server }) => { + server.setRoute('/one.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(''); + }); + server.setRoute('/two.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(''); + }); + + // The first committed document keeps the base seq, so the main frame has no prefix. + await page.goto(server.PREFIX + '/one.html'); + const oneRef = (await snapshotForAI(page)).match(/button "One" \[ref=(e\d+)\]/)![1]; + await expect(page.locator(`aria-ref=${oneRef}`)).toHaveText('One'); + + // Cross-document navigation re-numbers the main frame, so its refs gain a frame prefix. + await page.goto(server.PREFIX + '/two.html'); + const twoRef = (await snapshotForAI(page)).match(/button "Two" \[ref=(f\d+e\d+)\]/)![1]; + await expect(page.locator(`aria-ref=${twoRef}`)).toHaveText('Two'); + + // The stale ref from the previous document must not resolve against the new one. + const error = await page.locator(`aria-ref=${oneRef}`).normalize().catch(e => e); + expect(error.message).toContain(`No element matching aria-ref=${oneRef}`); + + // Same-document navigation keeps refs intact. + await page.evaluate(() => history.pushState({}, '', '/pushed.html')); + expect(await snapshotForAI(page)).toContain(`button "Two" [ref=${twoRef}]`); + await expect(page.locator(`aria-ref=${twoRef}`)).toHaveText('Two'); +}); + it('should persist iframe references', async ({ page }) => { await page.setContent(`
    @@ -345,9 +375,8 @@ it('should auto-wait for navigation', async ({ page, server }) => { page.evaluate(() => window.location.reload()), snapshotForAI(page) ]); - expect(snapshot).toContainYaml(` - - generic [ref=e2]: Hi, I'm frame - `); + // The snapshot races the reload, which may re-number the main frame, so accept any ref. + expect(snapshot).toMatch(/- generic \[ref=(?:f\d+)?e\d+\]: Hi, I'm frame/); }); it('should auto-wait for blocking CSS', async ({ page, server }) => { From 966ed43b57e2360dbe401cf82a301825d8838546 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 25 Jun 2026 16:46:44 +0100 Subject: [PATCH 06/10] chore(chromium): reenable RenderDocument again (#41303) --- docs/src/api/class-coverage.md | 3 ++- packages/playwright-client/types/types.d.ts | 5 ++++- .../src/server/chromium/chromiumSwitches.ts | 2 -- packages/playwright-core/types/types.d.ts | 5 ++++- tests/library/chromium/js-coverage.spec.ts | 8 -------- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/docs/src/api/class-coverage.md b/docs/src/api/class-coverage.md index 8eae75e24df73..8a5fb7af6824d 100644 --- a/docs/src/api/class-coverage.md +++ b/docs/src/api/class-coverage.md @@ -54,9 +54,10 @@ will have `__playwright_evaluation_script__` as their URL. ### option: Coverage.startJSCoverage.resetOnNavigation * since: v1.11 +* discouraged: Settings this to `false` may still reset on navigations. - `resetOnNavigation` <[boolean]> -Whether to reset coverage on every navigation. Defaults to `true`. +Whether to reset coverage on every navigation. Defaults to `true`. Note that passing `false` does not guarantee that coverage persists through navigations, due to browser architecture limitations. ### option: Coverage.startJSCoverage.reportAnonymousScripts * since: v1.11 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index b2150842591d0..0e37993306575 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -19024,7 +19024,10 @@ export interface Coverage { reportAnonymousScripts?: boolean; /** - * Whether to reset coverage on every navigation. Defaults to `true`. + * **NOTE** Settings this to `false` may still reset on navigations. + * + * Whether to reset coverage on every navigation. Defaults to `true`. Note that passing `false` does not guarantee + * that coverage persists through navigations, due to browser architecture limitations. */ resetOnNavigation?: boolean; }): Promise; diff --git a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts index b63cadd1ff68b..aade807fc3e65 100644 --- a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts +++ b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts @@ -40,8 +40,6 @@ const disabledFeatures = [ 'Translate', // See https://issues.chromium.org/u/1/issues/435410220 'AutoDeElevate', - // See https://github.com/microsoft/playwright/issues/37714 - 'RenderDocument', // Prevents downloading optimization hints on startup. 'OptimizationHints', // Disables forced sign-in in Edge. diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index b2150842591d0..0e37993306575 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -19024,7 +19024,10 @@ export interface Coverage { reportAnonymousScripts?: boolean; /** - * Whether to reset coverage on every navigation. Defaults to `true`. + * **NOTE** Settings this to `false` may still reset on navigations. + * + * Whether to reset coverage on every navigation. Defaults to `true`. Note that passing `false` does not guarantee + * that coverage persists through navigations, due to browser architecture limitations. */ resetOnNavigation?: boolean; }): Promise; diff --git a/tests/library/chromium/js-coverage.spec.ts b/tests/library/chromium/js-coverage.spec.ts index 18f21b70fd9d5..7acc69386f985 100644 --- a/tests/library/chromium/js-coverage.spec.ts +++ b/tests/library/chromium/js-coverage.spec.ts @@ -63,14 +63,6 @@ it('should report multiple scripts', async function({ page, server }) { expect(coverage[1].url).toContain('/jscoverage/script2.js'); }); -it('should report scripts across navigations when disabled', async function({ page, server }) { - await page.coverage.startJSCoverage({ resetOnNavigation: false }); - await page.goto(server.PREFIX + '/jscoverage/multiple.html'); - await page.goto(server.EMPTY_PAGE); - const coverage = await page.coverage.stopJSCoverage(); - expect(coverage.length).toBe(2); -}); - it('should NOT report scripts across navigations when enabled', async function({ page, server }) { await page.coverage.startJSCoverage(); // Enabled by default. await page.goto(server.PREFIX + '/jscoverage/multiple.html'); From 74e517201451902aac9b90d1116131a0644fbac8 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 25 Jun 2026 17:26:55 +0100 Subject: [PATCH 07/10] Revert "devops: publish stable release on tag push instead of release event (#41425)" (#41460) --- .github/workflows/publish_release.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index bfd4d6d5fa9d2..07eb51e35d67c 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -7,10 +7,8 @@ on: push: branches: - release-* - tags: - # TODO: revert this to "published release" once github.ref is set there. - # See https://github.com/actions/runner/issues/2788 as well. - - 'v1.61.1' + release: + types: [published] jobs: publish-npm-and-driver: @@ -49,7 +47,7 @@ jobs: node utils/build/update_canary_version.js --beta --commit-timestamp utils/publish_all_packages.sh --beta - name: "publish release to NPM" - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'release' && github.event.action == 'published' run: utils/publish_all_packages.sh --release - name: Azure Login @@ -60,7 +58,7 @@ jobs: subscription-id: ${{ secrets.AZURE_PW_CDN_SUBSCRIPTION_ID }} - name: build & publish driver env: - AZ_UPLOAD_FOLDER: ${{ startsWith(github.ref, 'refs/tags/v') && 'driver' || 'driver/next' }} + AZ_UPLOAD_FOLDER: ${{ github.event_name == 'release' && 'driver' || 'driver/next' }} run: | utils/build/build-playwright-driver.sh utils/build/upload-playwright-driver.sh @@ -91,7 +89,7 @@ jobs: env: GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }} - name: Deploy Stable - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'release' && github.event.action == 'published' run: bash utils/build/deploy-trace-viewer.sh --stable env: GH_SERVICE_ACCOUNT_TOKEN: ${{ steps.app-token.outputs.token }} From bb2dae0059af32d550e0bfff036afdbeb32c2937 Mon Sep 17 00:00:00 2001 From: Omer Murat Aydin <109145643+aydinomer00@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:41:20 +0300 Subject: [PATCH 08/10] fix(selectors): accept regexp v flag in locator options (#41458) --- packages/isomorphic/selectorParser.ts | 2 +- tests/page/selectors-get-by.spec.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/isomorphic/selectorParser.ts b/packages/isomorphic/selectorParser.ts index f37d1ef4c31d8..c5c322cb7a361 100644 --- a/packages/isomorphic/selectorParser.ts +++ b/packages/isomorphic/selectorParser.ts @@ -324,7 +324,7 @@ export function parseAttributeSelector(selector: string, allowUnquotedStrings: b syntaxError('parsing regular expression'); let flags = ''; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions - while (!EOL && next().match(/[dgimsuy]/)) + while (!EOL && next().match(/[dgimsuvy]/)) flags += eat1(); try { return new RegExp(source, flags); diff --git a/tests/page/selectors-get-by.spec.ts b/tests/page/selectors-get-by.spec.ts index 692de3cc7fc29..de76065eca6c0 100644 --- a/tests/page/selectors-get-by.spec.ts +++ b/tests/page/selectors-get-by.spec.ts @@ -286,6 +286,14 @@ it('getByRole escaping', async ({ page }) => { ]); }); +it('getByRole should accept regexp with v flag', async ({ page }) => { + // Regression test for https://github.com/microsoft/playwright/issues/41457 + await page.setContent(``); + await expect(page.getByRole('button', { name: /Click me/v })).toHaveCount(1); + await expect(page.getByRole('button', { name: /click me/iv })).toHaveCount(1); + await expect(page.getByRole('button', { name: /Missing/v })).toHaveCount(0, { timeout: 1000 }); +}); + it('getByRole with description', async ({ page }) => { await page.setContent(`
    Alert 1
    From cc01132ba504cb5d336f6ccd9539f3b6337cb240 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 25 Jun 2026 10:31:36 -0700 Subject: [PATCH 09/10] fix(reporters): do not truncate piped output on exit (#41454) --- packages/playwright/src/reporters/base.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index 5a0702087085d..e1d0737af599b 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -86,8 +86,9 @@ class StripAnsiStream extends Writable { this._target = target; } - override _write(chunk: any, encoding: any, callback: any) { - this._target.write(stripAnsiEscapes(chunk.toString()), callback); + override write(chunk: any, encodingOrCallback?: any, callback?: any): boolean { + const cb = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback; + return this._target.write(stripAnsiEscapes(chunk.toString()), cb); } } From 224ed087809eca8db731cd1cb4c63d71d1efad3b Mon Sep 17 00:00:00 2001 From: Aditya Kumar Gupta Date: Thu, 25 Jun 2026 23:01:54 +0530 Subject: [PATCH 10/10] chore(html-reporter): use button elements for tabs (#41440) --- packages/html-reporter/src/tabbedPane.css | 9 +++++++++ packages/html-reporter/src/tabbedPane.tsx | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/html-reporter/src/tabbedPane.css b/packages/html-reporter/src/tabbedPane.css index 042ab9f7608d5..d063719c3912b 100644 --- a/packages/html-reporter/src/tabbedPane.css +++ b/packages/html-reporter/src/tabbedPane.css @@ -48,11 +48,20 @@ align-items: center; justify-content: center; user-select: none; + background: none; + border: none; border-bottom: 2px solid transparent; + color: inherit; + font: inherit; outline: none; height: 100%; } +.tabbed-pane-tab-element:focus-visible { + outline: 1px solid var(--color-accent-fg); + outline-offset: -1px; +} + .tabbed-pane-tab-label { max-width: 250px; white-space: pre; diff --git a/packages/html-reporter/src/tabbedPane.tsx b/packages/html-reporter/src/tabbedPane.tsx index a1e533348b873..55d8f51955f08 100644 --- a/packages/html-reporter/src/tabbedPane.tsx +++ b/packages/html-reporter/src/tabbedPane.tsx @@ -37,14 +37,14 @@ export const TabbedPane: React.FunctionComponent<{
    { tabs.map(tab => ( -
    setSelectedTab(tab.id)} id={`${idPrefix}-${tab.id}`} key={tab.id} role='tab' aria-selected={selectedTab === tab.id}>
    {tab.title}
    -
    + )) }