fix(cli): contrast gate judges only readable content; examples pass check

Three filters keep the escalated contrast gate honest, each surfaced by
running check across the registry examples:
- data-layout-ignore (the layout audit's existing decorative opt-out)
  now also excludes set-dressing text from the contrast audit — one
  vocabulary for 'not copy a viewer must read'.
- Text that has (nearly) left the canvas is skipped: sampling a clamped
  off-canvas box reads border pixels and produced the classic false
  white-on-white (a cursor exiting the frame).
- Contrast failures follow the same persistence rule as layout findings:
  observed at a single sample of a multi-sample sweep demotes to
  warning; held failures gate.

Example fixes the sweep exposed: motion-blur's 21 deliberately-dim rail
labels are marked decorative (its vivid labels pass on their own);
nyt-graph's subtitle and source-note adopt the gate's suggested
compliant gray; decision-tree's declared duration drops 15s to 10s —
its content ends at ~9.5s and every render shipped a blank white tail.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-10 14:08:31 -04:00
parent cea3458016
commit 4b03866c02
10 changed files with 224 additions and 62 deletions
+14 -18
View File
@@ -51,6 +51,18 @@ afterEach(() => {
Reflect.deleteProperty(window, "__contrastAuditRestoreIfPending");
});
function installSessionMock(page: ReturnType<typeof fakePage>): void {
const browser = Object.assign(Object.create(null), {
close: vi.fn(async () => undefined),
});
vi.mocked(openSettledCompositionPage).mockImplementation(
async (_html: string, _url: string, options: OpenSettledCompositionPageOptions) => {
await options.beforeNavigate?.(page);
return { page, browser, renderReadyTimedOut: false };
},
);
}
it("carries raw browser geometry through the page driver and pipeline", async () => {
vi.spyOn(Date, "now")
.mockReturnValueOnce(100)
@@ -68,15 +80,7 @@ it("carries raw browser geometry through the page driver and pipeline", async ()
Object.defineProperty(window, "innerHeight", { configurable: true, value: 360 });
installRects();
const page = fakePage();
const browser = Object.assign(Object.create(null), {
close: vi.fn(async () => undefined),
});
vi.mocked(openSettledCompositionPage).mockImplementation(
async (_html: string, _url: string, options: OpenSettledCompositionPageOptions) => {
await options.beforeNavigate?.(page);
return { page, browser, renderReadyTimedOut: false };
},
);
installSessionMock(page);
const result = await runBrowserCheck(
PROJECT,
@@ -184,15 +188,7 @@ it("round-trips the browser script's raw contrast candidates back into finish",
}
});
page.screenshot = vi.fn(async () => "c3R1Yg==");
const browser = Object.assign(Object.create(null), {
close: vi.fn(async () => undefined),
});
vi.mocked(openSettledCompositionPage).mockImplementation(
async (_html: string, _url: string, options: OpenSettledCompositionPageOptions) => {
await options.beforeNavigate?.(page);
return { page, browser, renderReadyTimedOut: false };
},
);
installSessionMock(page);
await runBrowserCheck(
PROJECT,