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
10 changes: 9 additions & 1 deletion packages/cli/src/commands/contrast-audit.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/commands/layout-audit.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<div id="root" data-composition-id="overlay" data-width="640" data-height="360">
<span id="label">Live</span>
</div>
`;

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
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/commands/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,11 @@ async function runContrastAudit(page: import("puppeteer-core").Page): Promise<Co
: [],
)) as ContrastCandidate[];

const screenshot = (await page.screenshot({ encoding: "base64", type: "png" })) as string;
const screenshot = (await page.screenshot({
encoding: "base64",
type: "png",
omitBackground: true,
})) as string;
const entries = await page.evaluate(
(b64: string, time: number, cands: ContrastCandidate[]) =>
typeof (window as unknown as Record<string, unknown>).__contrastAuditFinish === "function"
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/utils/checkBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading