fix(cli): check no longer treats a transparent canvas as covering the frame (#6681)

* fix(cli): check no longer treats a transparent canvas as covering the frame

* fix(cli): a canvas with an opaque CSS background still counts as covering
This commit is contained in:
Miguel Ángel
2026-10-28 17:48:26 +00:00
committed by GitHub
parent c569760a7a
commit 203a08dd7c
2 changed files with 60 additions and 0 deletions
@@ -1276,6 +1276,33 @@
// An element hides text beneath it when it paints opaque pixels at near-full
// opacity: raster content (img/video/canvas), a background image, or a solid
// background colour. Low-opacity overlays (grain, scrims) do not occlude.
const canvasProbe = document.createElement("canvas");
canvasProbe.width = 1;
canvasProbe.height = 1;
// The drawn alpha under the point, or null when the canvas cannot be read (then it counts as opaque).
function canvasAlphaAt(element, x, y) {
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0 || !element.width || !element.height) return null;
const px = Math.min(
element.width - 1,
Math.max(0, Math.floor(((x - rect.left) / rect.width) * element.width)),
);
const py = Math.min(
element.height - 1,
Math.max(0, Math.floor(((y - rect.top) / rect.height) * element.height)),
);
try {
const context = canvasProbe.getContext("2d", { willReadFrequently: true });
if (!context) return null;
context.clearRect(0, 0, 1, 1);
context.drawImage(element, px, py, 1, 1, 0, 0, 1, 1);
return context.getImageData(0, 0, 1, 1).data[3] / 255;
} catch {
return null;
}
}
function isOpaqueOccluder(element, x, y) {
const opacity = opacityChain(element);
if (opacity < OCCLUDING_ALPHA) return false;
@@ -1284,6 +1311,11 @@
const alpha = imageAlphaAt(element, x, y);
if (alpha !== null) return alpha * opacity >= OCCLUDING_ALPHA;
}
if (element.tagName === "CANVAS") {
const alpha = canvasAlphaAt(element, x, y);
if (alpha !== null)
return alpha * opacity >= OCCLUDING_ALPHA || hasOpaqueBackground(getComputedStyle(element));
}
const matte = matteFrameFor(element);
if (matte) return matteOccludesAt(matte, x, y);
if (RASTER_TAGS.has(element.tagName)) return true;
@@ -427,6 +427,34 @@ describe.runIf(executablePath)("layout audit in Chromium", () => {
expect(codes.includes("content_overlap")).toBe(overlap);
});
it.each([
{ name: "transparent", fill: "", css: "", covered: false },
{
name: "opaque",
fill: "ctx.fillStyle='#123';ctx.fillRect(0,0,1920,1080);",
css: "",
covered: true,
},
{
name: "glyph-only with an opaque CSS background",
fill: "",
css: "background:#112233;",
covered: true,
},
])(
"judges a full-frame $name canvas over a caption by what it draws",
async ({ fill, css, covered }) => {
const codes = await auditCodes(`
<div data-composition-id="main" data-width="1920" data-height="1080" style="position:relative;width:1920px;height:1080px">
<div style="position:absolute;inset:0;background:#0b0d12"></div>
<p style="position:absolute;left:100px;top:100px;margin:0;font:64px Arial;color:#fff">Caption</p>
<canvas id="c" width="1920" height="1080" style="position:absolute;inset:0;width:1920px;height:1080px;z-index:2;${css}"></canvas>
<script>const ctx=document.getElementById("c").getContext("2d");${fill}ctx.fillStyle="#fff";ctx.font="64px Arial";ctx.fillText("Drawn",900,900);</script>
</div>`);
expect(codes.includes("canvas_covered")).toBe(covered);
},
);
describe("craft sampling", () => {
async function sample(body: string) {
const page = await browser.newPage();