Skip to content
Draft
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: 6 additions & 4 deletions packages/cli/src/commands/layout-audit.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -656,8 +656,9 @@
code: "content_overlap",
severity: "warning",
time,
selector: selectorFor(a.element),
containerSelector: selectorFor(b.element),
// Unique both ends: the collapse key names this pair by its selectors alone, so aliased siblings must not share one.
selector: uniqueSelectorFor(a.element),
containerSelector: uniqueSelectorFor(b.element),
text: textContentFor(a.element),
message: "Two text blocks overlap and may render unreadable.",
rect: a.rect,
Expand Down Expand Up @@ -1017,8 +1018,9 @@
code: "text_occluded",
severity: "error",
time,
selector: selectorFor(element),
containerSelector: selectorFor(occluder),
// Unique both ends: the collapse key names this pair by its selectors alone, so aliased siblings must not share one.
selector: uniqueSelectorFor(element),
containerSelector: uniqueSelectorFor(occluder),
text,
message: "Text is hidden beneath an opaque element.",
rect: textRect,
Expand Down
63 changes: 63 additions & 0 deletions packages/cli/src/utils/layoutAudit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,69 @@ describe("persistence-tiered severity (#U10)", () => {
expect(collapsed[0]).toMatchObject({ severity: "warning", occurrences: 2 });
});

it("promotes a content_overlap whose text changes every sample (count-up over a label)", () => {
// The colliding pair is named by both selectors, so per-sample text must not split one held collision into transient groups.
const collapsed = collapseStaticLayoutIssues(
[
{
...issue("content_overlap", "warning"),
time: 4.0,
containerSelector: ".num",
text: "$1,204",
},
{
...issue("content_overlap", "warning"),
time: 4.5,
containerSelector: ".num",
text: "$8,930",
},
],
73,
);

expect(collapsed).toHaveLength(1);
expect(collapsed[0]).toMatchObject({ severity: "error", occurrences: 2 });
});

it("keeps two content_overlap pairs on different containers in separate groups", () => {
const collapsed = collapseStaticLayoutIssues(
[
{ ...issue("content_overlap", "warning"), time: 4.0, containerSelector: ".num" },
{ ...issue("content_overlap", "warning"), time: 4.5, containerSelector: ".pct" },
],
73,
);

expect(collapsed).toHaveLength(2);
});

it("does not bridge two separate transients on one pair into a held collision", () => {
// Both blips sit under the 500ms floor; spanning them would fabricate a 4.1s collision that never happened.
const blip = { ...issue("content_overlap", "warning"), containerSelector: ".label" };
const collapsed = collapseStaticLayoutIssues(
[
{ ...blip, time: 1.0 },
{ ...blip, time: 1.125 },
],
73,
);

expect(collapsed).toHaveLength(1);
expect(collapsed[0]).toMatchObject({ severity: "warning", occurrences: 2 });
});

it("still separates two distinct text_box_overflow findings that differ only by text", () => {
const collapsed = collapseStaticLayoutIssues(
[
{ ...issue("text_box_overflow", "warning"), time: 4.0, text: "first" },
{ ...issue("text_box_overflow", "warning"), time: 4.5, text: "second" },
],
73,
);

expect(collapsed).toHaveLength(2);
});

it("promotes content_overlap whose two occurrences span exactly 500ms (at the floor)", () => {
const collapsed = collapseStaticLayoutIssues(
[
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/utils/layoutAudit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export function formatLayoutIssue(issue: LayoutIssue): string {
return issue.fixHint ? `${line}\n Fix: ${issue.fixHint}` : line;
}

// Keeps raw `text` where `staticIssueKey` drops it: this key pins an exact time, so text still separates concurrent findings rather than spanning samples.
export function dedupeLayoutIssues(issues: LayoutIssue[]): LayoutIssue[] {
const seen = new Set<string>();
const result: LayoutIssue[] = [];
Expand Down Expand Up @@ -204,6 +205,15 @@ const PERSISTENCE_TIERED_CODES: ReadonlySet<LayoutIssueCode> = new Set([
"connector_detached",
]);

// Codes whose collapse key is the selector pair alone. Both builders uniquely
// select their two ends, so the pair IS the identity — and keeping per-sample
// text would split one held finding into transient groups whenever the subject's
// text animates (count-up, typewriter, rotating word).
const TEXT_AGNOSTIC_KEY_CODES: ReadonlySet<LayoutIssueCode> = new Set([
"content_overlap",
"text_occluded",
]);

export function collapseStaticLayoutIssues(
issues: LayoutIssue[],
totalSampleCount?: number,
Expand Down Expand Up @@ -342,7 +352,7 @@ function staticIssueKey(issue: LayoutIssue): string {
issue.severity,
issue.selector,
issue.containerSelector ?? "",
issue.text ?? "",
TEXT_AGNOSTIC_KEY_CODES.has(issue.code) ? "" : (issue.text ?? ""),
issue.overflow ? formatOverflow(issue.overflow) : "",
framePositionKey(issue),
].join("|");
Expand Down
Loading