Skip to content

Commit 7b28267

Browse files
xuanruliclaude
andcommitted
fix(check): key content_overlap collapse by pair, not text
`staticIssueKey` put `issue.text` in the collapse key, but `overlapIssue` already names a collision by both of its selectors. When the leading element's text changes per sample — a count-up, typewriter, or rotating word — every dense-pass sample formed its own group with `occurrences: 1`, so `applyPersistenceTier` demoted all of them to `info` and a genuinely held collision never reached the `error` tier that gates a run. Empirically: two projects with identical geometry and an identical 5.9s overlap, differing only in DOM order, verdicted differently — label-first gave `error`/`ok=false`, count-up-first gave 48x `info`/`ok=true`. After this change both give `error`/`ok=false` with `occurrences: 48`. Seven other overlap fixtures (crossfades, static, masked reel, 600ms transient) are unchanged. Codes that identify themselves by text keep it; only `content_overlap`, which carries both selectors, drops it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 8c50770 commit 7b28267

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

packages/cli/src/utils/layoutAudit.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,42 @@ describe("persistence-tiered severity (#U10)", () => {
255255
expect(collapsed[0]).toMatchObject({ severity: "warning", occurrences: 2 });
256256
});
257257

258+
it("promotes a content_overlap whose text changes every sample (count-up over a label)", () => {
259+
// The colliding pair is named by both selectors, so per-sample text must not split one held collision into transient groups.
260+
const collapsed = collapseStaticLayoutIssues(
261+
[
262+
{
263+
...issue("content_overlap", "warning"),
264+
time: 4.0,
265+
containerSelector: ".num",
266+
text: "$1,204",
267+
},
268+
{
269+
...issue("content_overlap", "warning"),
270+
time: 4.5,
271+
containerSelector: ".num",
272+
text: "$8,930",
273+
},
274+
],
275+
73,
276+
);
277+
278+
expect(collapsed).toHaveLength(1);
279+
expect(collapsed[0]).toMatchObject({ severity: "error", occurrences: 2 });
280+
});
281+
282+
it("still separates two distinct text_box_overflow findings that differ only by text", () => {
283+
const collapsed = collapseStaticLayoutIssues(
284+
[
285+
{ ...issue("text_box_overflow", "warning"), time: 4.0, text: "first" },
286+
{ ...issue("text_box_overflow", "warning"), time: 4.5, text: "second" },
287+
],
288+
73,
289+
);
290+
291+
expect(collapsed).toHaveLength(2);
292+
});
293+
258294
it("promotes content_overlap whose two occurrences span exactly 500ms (at the floor)", () => {
259295
const collapsed = collapseStaticLayoutIssues(
260296
[

packages/cli/src/utils/layoutAudit.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,12 +342,18 @@ function staticIssueKey(issue: LayoutIssue): string {
342342
issue.severity,
343343
issue.selector,
344344
issue.containerSelector ?? "",
345-
issue.text ?? "",
345+
collapseTextKey(issue),
346346
issue.overflow ? formatOverflow(issue.overflow) : "",
347347
framePositionKey(issue),
348348
].join("|");
349349
}
350350

351+
/** Text identity for the collapse key, omitted where the code identifies itself by geometry instead. */
352+
function collapseTextKey(issue: LayoutIssue): string {
353+
// content_overlap names its pair by both selectors, so animated text would split one held collision into per-sample groups.
354+
return issue.code === "content_overlap" ? "" : (issue.text ?? "");
355+
}
356+
351357
function framePositionKey(issue: LayoutIssue): string {
352358
// connector_detached shares it: id-less paths collapse to one selector, so distinct lines need geometry in the key.
353359
return issue.code === "frame_out_of_frame" || issue.code === "connector_detached"

0 commit comments

Comments
 (0)