From c9f6e7b3bf81dfccbb0480750ea157d62a4ee56c Mon Sep 17 00:00:00 2001 From: aydinomer00 Date: Sat, 27 Jun 2026 17:01:06 +0300 Subject: [PATCH] fix(highlight): prevent page ::backdrop styles from tinting screenshots The glass pane is promoted to the top layer via the popover API, which renders a ::backdrop pseudo-element. That pseudo-element lives in the light DOM and is styled by page stylesheets, so page rules targeting ::backdrop bled onto it and tinted screenshots, including masked screenshots. Neutralize the glass pane backdrop from a document-level stylesheet. Fixes https://github.com/microsoft/playwright/issues/41504 --- packages/injected/src/highlight.ts | 16 ++++++++++++++++ tests/page/page-screenshot.spec.ts | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/packages/injected/src/highlight.ts b/packages/injected/src/highlight.ts index fbd012c4bcd31..b174a05e1c995 100644 --- a/packages/injected/src/highlight.ts +++ b/packages/injected/src/highlight.ts @@ -109,6 +109,22 @@ export class Highlight { this._glassPaneShadow.appendChild(this._actionCursorElement); this._glassPaneShadow.appendChild(this._titleElement); this._glassPaneShadow.appendChild(this._userOverlayContainer); + // The glass pane is promoted to the top layer via the popover API, which renders a + // ::backdrop pseudo-element. Its ::backdrop lives in the light DOM and is therefore + // styled by page stylesheets, so page rules targeting ::backdrop would bleed onto it + // and tint screenshots (including masked screenshots). Neutralize it from a + // document-level stylesheet, since the shadow stylesheet cannot reach the host ::backdrop. + // https://github.com/microsoft/playwright/issues/41504 + const backdropCSS = 'x-pw-glass::backdrop { background: transparent !important; }'; + if (typeof document.adoptedStyleSheets.push === 'function') { + const backdropSheet = new this._injectedScript.window.CSSStyleSheet(); + backdropSheet.replaceSync(backdropCSS); + document.adoptedStyleSheets.push(backdropSheet); + } else { + const backdropStyleElement = document.createElement('style'); + backdropStyleElement.textContent = backdropCSS; + document.documentElement?.appendChild(backdropStyleElement); + } } install() { diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index 4fc5008de9d4c..0a3f9209cd038 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -22,6 +22,7 @@ import type { Route } from 'playwright-core'; import path from 'path'; import fs from 'fs'; import { comparePNGs } from '../config/comparator'; +import { PNG } from 'playwright-core/lib/utilsBundle'; it.describe('page screenshot', () => { it.skip(({ browserName, headless }) => browserName === 'firefox' && !headless, 'Firefox headed produces a different image.'); @@ -574,6 +575,28 @@ it.describe('page screenshot', () => { })).toMatchSnapshot('mask-color-should-work.png'); }); + it('should not be affected by page ::backdrop styles', async ({ page }) => { + // Regression test for https://github.com/microsoft/playwright/issues/41504 + await page.setViewportSize({ width: 200, height: 200 }); + await page.setContent(` + +
+ `); + const screenshot = await page.screenshot({ mask: [page.locator('#target')] }); + const decoded = PNG.sync.read(screenshot); + // A pixel away from the mask must stay white. Before the fix, the page ::backdrop + // rule leaked onto the glass pane's top-layer backdrop and tinted the whole viewport. + const index = (10 * decoded.width + 10) * 4; + expect(decoded.data[index]).toBeGreaterThan(250); + expect(decoded.data[index + 1]).toBeGreaterThan(250); + expect(decoded.data[index + 2]).toBeGreaterThan(250); + }); + it('should hide elements based on attr', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html');