diff --git a/packages/cli/src/commands/contrast-audit.browser.js b/packages/cli/src/commands/contrast-audit.browser.js index a9b780acb8..854b0b6458 100644 --- a/packages/cli/src/commands/contrast-audit.browser.js +++ b/packages/cli/src/commands/contrast-audit.browser.js @@ -286,7 +286,7 @@ window.__contrastAuditPrepare = function () { function isClippedAway(el, rect) { if (typeof document.elementFromPoint !== "function") return false; if (!hasClipPath(el)) return false; - return !paintsAnyProbePoint(el, rect); + return !paintsAnyProbePoint(el, rect, true); } function isIntentionallyOccluded(el, rect) { diff --git a/packages/cli/src/commands/contrast-occlusion.browser.test.ts b/packages/cli/src/commands/contrast-occlusion.browser.test.ts index 9bb2ec0141..521c95abe6 100644 --- a/packages/cli/src/commands/contrast-occlusion.browser.test.ts +++ b/packages/cli/src/commands/contrast-occlusion.browser.test.ts @@ -206,4 +206,29 @@ describe("contrast audit beneath composition boxes", () => { installStack(["overlay", "root"]); expect(prepare()).toEqual([]); }); + + it.each(["inset(0)", "inset(0 50% 0 0)"])( + "audits visible text with clip-path %s beneath a transparent host", + (clipPath) => { + element("headline").style.clipPath = clipPath; + expect(prepare()).toContainEqual(expect.objectContaining({ selector: "#headline" })); + }, + ); + + it("audits text clipped by an ancestor beneath transparent inner roots", () => { + element("headline").removeAttribute("data-layout-allow-occlusion"); + element("root").style.clipPath = "inset(0)"; + const inner = document.createElement("div"); + inner.id = "inner"; + inner.dataset.hfInnerRoot = "true"; + element("overlay").appendChild(inner); + installStack(["inner", "overlay", "headline", "root"]); + expect(prepare()).toContainEqual(expect.objectContaining({ selector: "#headline" })); + }); + + it("keeps clipped text behind a painted host out of contrast candidates", () => { + element("headline").style.clipPath = "inset(0)"; + element("overlay").style.backgroundColor = "white"; + expect(prepare()).toEqual([]); + }); }); diff --git a/packages/cli/src/commands/contrast-occlusion.chromium.test.ts b/packages/cli/src/commands/contrast-occlusion.chromium.test.ts new file mode 100644 index 0000000000..6c78ab67e7 --- /dev/null +++ b/packages/cli/src/commands/contrast-occlusion.chromium.test.ts @@ -0,0 +1,88 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import puppeteer, { type Browser } from "puppeteer-core"; + +declare global { + interface Window { + __contrastAuditPrepare(): { selector: string }[]; + } +} + +const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH; +const script = readFileSync(new URL("./contrast-audit.browser.js", import.meta.url), "utf8"); + +describe.runIf(executablePath)("clipped-text contrast candidates in Chromium", () => { + let browser: Browser; + + beforeAll(async () => { + browser = await puppeteer.launch({ executablePath, args: ["--no-sandbox"] }); + }); + + afterAll(async () => { + await browser?.close(); + }); + + async function audit( + clipPath: string, + options: { ancestor?: boolean; nested?: boolean; painted?: boolean } = {}, + ) { + const page = await browser.newPage(); + try { + await page.setViewport({ width: 640, height: 360, deviceScaleFactor: 1 }); + const clip = `clip-path:${clipPath}`; + const overlay = options.nested + ? '
' + : ""; + await page.setContent(` +