feat(check): add data-layout-allow-caption-zone waiver (#2853)

* feat(check): add data-layout-allow-caption-zone waiver

Opt intentional lower-third copy out of caption_zone_collision.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check): address caption-zone waiver review nits

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(skills): document caption-zone waiver on CLI agent path

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(cli): document caption-zone waiver under check, not inspect

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Xuanru Li
2026-07-28 15:56:43 -07:00
committed by GitHub
co-authored by Cursor
parent a996e91e5d
commit 3a7950fd63
8 changed files with 77 additions and 14 deletions
+22
View File
@@ -175,11 +175,13 @@ interface GeometryFixture {
elementRect?: LayoutRect;
time: number;
overflow?: LayoutOverflow;
dataAttributes?: Record<string, string>;
}
function geometryCandidate(fixture: GeometryFixture) {
return {
...anchor(fixture.selector, fixture.time),
...(fixture.dataAttributes ? { dataAttributes: fixture.dataAttributes } : {}),
kind: fixture.kind,
tag: fixture.tag,
text: fixture.text,
@@ -446,6 +448,26 @@ it("flags only text whose center is inside the caption band at the default end s
expect(report.ok).toBe(true);
});
it("skips caption_zone_collision when data-layout-allow-caption-zone is set", async () => {
const collectGeometryCandidates = vi.fn(async (time: number) => [
geometryCandidate({
kind: "text",
tag: "div",
text: "Intentional lower third",
selector: "#lower-third",
rect: fixtureRect(860, 870, 200, 60),
time,
dataAttributes: { "data-layout-allow-caption-zone": "" },
}),
]);
const { report } = await runScenario(
fakeDriver({ getDuration: vi.fn(async () => 10), collectGeometryCandidates }),
{ samples: 1, contrast: false, captionZone: { x0: 0, y0: 0.8, x1: 1, y1: 0.9 } },
);
expect(report.layout.findings).toEqual([]);
});
it("filters caption candidates by the element box while centering the text rect", async () => {
const collectGeometryCandidates = vi.fn(async (time: number) => [
geometryCandidate({
@@ -105,6 +105,10 @@
return !!element.closest("[data-layout-allow-overflow]");
}
function hasAllowCaptionZoneFlag(element) {
return !!element.closest("[data-layout-allow-caption-zone]");
}
function hasTextClipOptOut(element) {
return hasAllowOverflowFlag(element) || element.hasAttribute("data-layout-bleed");
}
@@ -1396,7 +1400,7 @@
}
if (!isVisibleElement(element, 0.05, false)) continue;
const elementRect = toRect(element.getBoundingClientRect());
if (includeText && hasOwnTextCandidate(element, true)) {
if (includeText && hasOwnTextCandidate(element, true) && !hasAllowCaptionZoneFlag(element)) {
const rect = textRectFor(element, true);
if (rect) {
candidates.push(
@@ -381,6 +381,27 @@ it("returns own-text rects and media overflow while excluding caption layers", (
expect(candidates.some((candidate) => candidate.selector === "#caption")).toBe(false);
});
it("excludes text marked data-layout-allow-caption-zone from geometry candidates", () => {
document.body.innerHTML = `
<div id="root" data-composition-id="main" data-width="640" data-height="360">
<p id="copy">Main copy</p>
<p id="lower" data-layout-allow-caption-zone>Lower third</p>
<div data-layout-allow-caption-zone><span id="nested">Nested lower</span></div>
</div>
`;
installGeometry({
root: rect({ left: 0, top: 0, width: 640, height: 360 }),
copy: rect({ left: 100, top: 100, width: 200, height: 40 }),
lower: rect({ left: 100, top: 280, width: 200, height: 40 }),
nested: rect({ left: 100, top: 300, width: 200, height: 40 }),
text: rect({ left: 100, top: 100, width: 200, height: 40 }),
});
installAuditScript();
const candidates = runGeometryCandidates({ text: true, media: false, tolerance: 2 });
expect(candidates.map((candidate) => candidate.selector)).toEqual(["#copy"]);
});
it("scans body-level composition siblings and includes a media boundary root", () => {
document.body.innerHTML = `
<canvas id="boundary" data-composition-id="background" data-width="640" data-height="360"></canvas>