Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/src/commands/contrast-audit.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,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) {
Expand Down
25 changes: 25 additions & 0 deletions packages/cli/src/commands/contrast-occlusion.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});
});
88 changes: 88 additions & 0 deletions packages/cli/src/commands/contrast-occlusion.chromium.test.ts
Original file line number Diff line number Diff line change
@@ -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
? '<div data-hf-inner-root style="position:absolute;inset:0"></div>'
: "";
await page.setContent(`<body style="margin:0">
<div data-composition-id="main" style="position:relative;width:640px;height:360px;background:white">
<div style="position:absolute;inset:0;${options.ancestor ? clip : ""}">
<div id="headline" data-layout-allow-occlusion style="position:absolute;left:60px;top:100px;width:500px;height:80px;font:60px/80px Arial;color:#eee;${options.ancestor ? "" : clip}">Visible copy</div>
</div>
<div data-composition-id="overlay" style="position:absolute;inset:0;background:${options.painted ? "white" : "transparent"}">${overlay}</div>
</div>
</body>`);
const image = await page.screenshot();
await page.addScriptTag({ content: script });
const selectors = await page.evaluate(() =>
window.__contrastAuditPrepare().map((entry) => entry.selector),
);
return { image, selectors };
} finally {
await page.close();
}
}

it("audits an unchanged visible frame when a clip-path is added", async () => {
const normal = await audit("none");
const clipped = await audit("inset(0)");
expect(clipped.image).toEqual(normal.image);
expect(normal.selectors).toContain("#headline");
expect(clipped.selectors).toContain("#headline");
});

it("audits partially revealed text beneath a transparent composition host", async () => {
const partial = await audit("inset(0 50% 0 0)");
const hidden = await audit("inset(0 100% 0 0)");
expect(partial.image).not.toEqual(hidden.image);
expect(partial.selectors).toContain("#headline");
expect(hidden.selectors).not.toContain("#headline");
});

it("walks through nested transparent roots for an ancestor clip-path", async () => {
const clipped = await audit("inset(0)", { ancestor: true, nested: true });
expect(clipped.selectors).toContain("#headline");
});

it.each([{ ancestor: false }, { ancestor: true }])(
"keeps fully clipped text out of contrast candidates with ancestor=$ancestor",
async (options) => {
const hidden = await audit("inset(0 100% 0 0)", options);
expect(hidden.selectors).not.toContain("#headline");
},
);

it("keeps text behind a painted composition host out of contrast candidates", async () => {
const covered = await audit("inset(0)", { painted: true });
expect(covered.selectors).not.toContain("#headline");
});
});
Loading