From 8e50e8477ffc4c266f6ee33371f00bbe460b245b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 13 Jul 2026 18:22:30 -0400 Subject: [PATCH] fix(cli): skip contrast for intentionally covered text (#2366) --- .../src/commands/contrast-audit.browser.js | 10 +++ .../src/commands/layout-audit.browser.test.ts | 62 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/packages/cli/src/commands/contrast-audit.browser.js b/packages/cli/src/commands/contrast-audit.browser.js index 0aa7efe50..e11c50947 100644 --- a/packages/cli/src/commands/contrast-audit.browser.js +++ b/packages/cli/src/commands/contrast-audit.browser.js @@ -131,6 +131,12 @@ window.__contrastAuditPrepare = function () { return !paintsAnyProbePoint(el, rect); } + function isIntentionallyOccluded(el, rect) { + if (typeof document.elementFromPoint !== "function") return false; + if (!el.closest || !el.closest("[data-layout-allow-occlusion]")) return false; + return !paintsAnyProbePoint(el, rect); + } + var out = []; var restores = []; // Registered BEFORE the walk starts (not after it finishes) and pushed to @@ -200,6 +206,10 @@ window.__contrastAuditPrepare = function () { if (rect.width < 8 || rect.height < 8) continue; if (rect.right <= 0 || rect.bottom <= 0) continue; if (isClippedAway(el, rect)) continue; + // The layout audit's explicit occlusion opt-out means this text is allowed + // to sit behind another scene. Skip contrast only while every probe point + // is actually covered; the same copy is audited normally when visible. + if (isIntentionallyOccluded(el, rect)) continue; // For SVG text, `fill` is the paint that's actually rendered; `color` is // frequently just the inherited/initial value and unrelated to what's on diff --git a/packages/cli/src/commands/layout-audit.browser.test.ts b/packages/cli/src/commands/layout-audit.browser.test.ts index a3ecd4f6b..891a228dd 100644 --- a/packages/cli/src/commands/layout-audit.browser.test.ts +++ b/packages/cli/src/commands/layout-audit.browser.test.ts @@ -635,6 +635,68 @@ describe("contrast-audit.browser clip-path visibility", () => { expect(selectors).not.toContain("#rail-label"); }); + it("excludes intentionally occluded text from contrast reports", async () => { + document.body.innerHTML = ` +
+
Covered copy
+
+
+ `; + + vi.spyOn(window, "getComputedStyle").mockImplementation( + () => + ({ + display: "block", + visibility: "visible", + opacity: "1", + color: "rgb(255, 255, 255)", + fontSize: "32px", + fontWeight: "400", + clipPath: "none", + }) as unknown as CSSStyleDeclaration, + ); + vi.spyOn(document.getElementById("headline")!, "getBoundingClientRect").mockReturnValue( + rect({ left: 100, top: 100, width: 400, height: 40 }), + ); + (document as unknown as { elementFromPoint: () => Element | null }).elementFromPoint = () => + document.getElementById("cover"); + + installContrastScript(); + + expect(await runContrastAudit()).toEqual([]); + }); + + it("still audits visible text that allows occlusion", async () => { + document.body.innerHTML = ` +
+
Visible copy
+
+ `; + + vi.spyOn(window, "getComputedStyle").mockImplementation( + () => + ({ + display: "block", + visibility: "visible", + opacity: "1", + color: "rgb(255, 255, 255)", + fontSize: "32px", + fontWeight: "400", + clipPath: "none", + }) as unknown as CSSStyleDeclaration, + ); + vi.spyOn(document.getElementById("headline")!, "getBoundingClientRect").mockReturnValue( + rect({ left: 100, top: 100, width: 400, height: 40 }), + ); + (document as unknown as { elementFromPoint: () => Element | null }).elementFromPoint = () => + document.getElementById("headline"); + + installContrastScript(); + + const entries = await runContrastAudit(); + expect(entries.map((entry) => entry.selector)).toContain("#headline"); + }); + it("excludes text that has left the canvas from contrast reports", async () => { document.body.innerHTML = `