diff --git a/packages/cli/src/commands/layout-audit.browser.js b/packages/cli/src/commands/layout-audit.browser.js index e26425dbeb..b1bf1fdc93 100644 --- a/packages/cli/src/commands/layout-audit.browser.js +++ b/packages/cli/src/commands/layout-audit.browser.js @@ -1362,6 +1362,12 @@ return !!ctx && ctx === preserve3dContext(b); } + // A covering element marked to cover text, or inside a marked wrapper that does not also hold the text. + function allowsOcclusionOf(hit, element) { + const allowed = authoredElement(hit).closest("[data-layout-allow-occlusion]"); + return !!allowed && !allowed.contains(element); + } + // The opaque element painted over (x, y), or null when the topmost element // there is related to the text, non-opaque, sharing a 3D context with it, or // part of a transient crossfade overlap. @@ -1377,7 +1383,7 @@ for (const hit of stack) { if (!isForeignElement(element, hit)) return null; // Pair-specific exemptions excuse this hit only; keep walking for deeper occluders. - if (hasIgnoreFlag(hit)) continue; + if (hasIgnoreFlag(hit) || allowsOcclusionOf(hit, element)) continue; if (sharedPreserve3d(element, hit)) continue; if (isCrossSceneTransitionOverlap(element, hit)) continue; const matte = seenMattes && matteFrameFor(hit); diff --git a/packages/cli/src/commands/layout-audit.browser.test.ts b/packages/cli/src/commands/layout-audit.browser.test.ts index 0f7f20a69e..d0099f0d4a 100644 --- a/packages/cli/src/commands/layout-audit.browser.test.ts +++ b/packages/cli/src/commands/layout-audit.browser.test.ts @@ -3262,6 +3262,55 @@ describe("layout-audit.browser occlusion", () => { ); }); + it("lets data-layout-allow-occlusion on the covering element silence text_occluded", () => { + const issues = (overlayAttrs?: string) => + auditOcclusionScene({ + overlayAttrs, + overlayStyle: { backgroundColor: "rgb(10, 10, 10)" }, + topmostId: "overlay", + }); + + expect(issues().some((issue) => issue.code === "text_occluded")).toBe(true); + expect( + issues("data-layout-allow-occlusion").some((issue) => issue.code === "text_occluded"), + ).toBe(false); + }); + + it("honours the flag on a graded video whose topmost element is its canvas stand-in", () => { + document.body.innerHTML = ` +
+
Headline copy
+ + +
+ `; + installOcclusionGeometry({ + styleOverrides: { overlay: { backgroundColor: "rgb(10, 10, 10)" } }, + headlineTextRect: rect({ left: 200, top: 500, width: 600, height: 80 }), + topmostId: "overlay", + }); + installAuditScript(); + expect(runAudit().some((issue) => issue.code === "text_occluded")).toBe(false); + }); + + it("still flags a cover inside a flagged wrapper that also holds the text", () => { + document.body.innerHTML = ` +
+
+
Headline copy
+
+
+
+ `; + installOcclusionGeometry({ + styleOverrides: { overlay: { backgroundColor: "rgb(10, 10, 10)" } }, + headlineTextRect: rect({ left: 200, top: 500, width: 600, height: 80 }), + topmostId: "overlay", + }); + installAuditScript(); + expect(runAudit().some((issue) => issue.code === "text_occluded")).toBe(true); + }); + it("respects the data-layout-allow-occlusion opt-out", () => { const issues = auditOcclusionScene({ headlineAttrs: "data-layout-allow-occlusion",