From 68f07c1b253e2bfd390b9cb9226619be11dcd4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 13 Jul 2026 02:15:52 +0000 Subject: [PATCH] fix(cli): skip contrast on transparent backdrops --- .../src/commands/contrast-audit.browser.js | 10 ++++++- .../src/commands/layout-audit.browser.test.ts | 30 +++++++++++++++++++ packages/cli/src/commands/validate.ts | 6 +++- packages/cli/src/utils/checkBrowser.ts | 6 +++- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/contrast-audit.browser.js b/packages/cli/src/commands/contrast-audit.browser.js index 0aa7efe505..d3215ddf07 100644 --- a/packages/cli/src/commands/contrast-audit.browser.js +++ b/packages/cli/src/commands/contrast-audit.browser.js @@ -361,17 +361,25 @@ window.__contrastAuditFinish = async function (imgBase64, time, candidates) { var stepY = Math.max(1, Math.floor((y1 - y0) / 6)); var rr = [], gg = [], - bb = []; + bb = [], + aa = []; for (var y = y0; y <= y1; y += stepY) { for (var x = x0; x <= x1; x += stepX) { var idx = (y * w + x) * 4; rr.push(px[idx]); gg.push(px[idx + 1]); bb.push(px[idx + 2]); + aa.push(px[idx + 3]); } } if (rr.length === 0) continue; + // A transparent sampled backdrop is supplied by the downstream editor, + // not this composition. WCAG contrast cannot be inferred until that live + // video/image is known, so do not invent an opaque browser default and + // hard-fail an otherwise valid alpha overlay. + if (median(aa) < 255) continue; + var bgR = median(rr), bgG = median(gg), bgB = median(bb); diff --git a/packages/cli/src/commands/layout-audit.browser.test.ts b/packages/cli/src/commands/layout-audit.browser.test.ts index 821bad8c14..2c47f6df14 100644 --- a/packages/cli/src/commands/layout-audit.browser.test.ts +++ b/packages/cli/src/commands/layout-audit.browser.test.ts @@ -605,6 +605,36 @@ describe("contrast-audit.browser background sampling", () => { expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ selector: "#label", wcagAA: true, bg: "rgb(10,10,10)" }); }); + + it("skips text whose sampled backdrop remains transparent", async () => { + document.body.innerHTML = ` +
+ Live +
+ `; + + vi.spyOn(window, "getComputedStyle").mockImplementation( + () => + ({ + display: "block", + visibility: "visible", + opacity: "1", + color: "rgb(255, 255, 255)", + fontSize: "20px", + fontWeight: "700", + clipPath: "none", + }) as unknown as CSSStyleDeclaration, + ); + vi.spyOn(document.getElementById("label")!, "getBoundingClientRect").mockReturnValue( + rect({ left: 50, top: 50, width: 100, height: 30 }), + ); + (document as unknown as { elementFromPoint: () => Element | null }).elementFromPoint = () => + null; + + installContrastScript(new Uint8ClampedArray(640 * 360 * 4)); + + expect(await runContrastAudit()).toEqual([]); + }); }); // Both blocks overlap heavily; only the exemption on block A should suppress diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index 9624615717..2610aa725e 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -276,7 +276,11 @@ async function runContrastAudit(page: import("puppeteer-core").Page): Promise typeof (window as unknown as Record).__contrastAuditFinish === "function" diff --git a/packages/cli/src/utils/checkBrowser.ts b/packages/cli/src/utils/checkBrowser.ts index d889e41b73..8dd7a47fa6 100644 --- a/packages/cli/src/utils/checkBrowser.ts +++ b/packages/cli/src/utils/checkBrowser.ts @@ -510,7 +510,11 @@ async function collectContrast( // This screenshot is the one contrast math is sampled from below — it must // stay untouched by the annotation overlay (finishContrast reads real // painted pixels), so annotation only ever happens on a SECOND shot. - const measurementShot = await page.screenshot({ encoding: "base64", type: "png" }); + const measurementShot = await page.screenshot({ + encoding: "base64", + type: "png", + omitBackground: true, + }); if (typeof measurementShot !== "string") throw new Error("Contrast screenshot was not base64"); const raw = await finishContrast( page,