diff --git a/packages/cli/src/utils/checkBrowser.test.ts b/packages/cli/src/utils/checkBrowser.test.ts index 3a2a3f0d5..d7a138bf6 100644 --- a/packages/cli/src/utils/checkBrowser.test.ts +++ b/packages/cli/src/utils/checkBrowser.test.ts @@ -213,6 +213,61 @@ it("round-trips the browser script's raw contrast candidates back into finish", } }); +it("captures check overview snapshots after contrast restores hidden text", async () => { + vi.spyOn(Date, "now").mockReturnValue(100); + mountCanvasFixture(`
Readable copy
`); + const root = document.querySelector("[data-composition-id]"); + const headline = document.querySelector("#headline"); + if (!root || !headline) throw new Error("Contrast fixture failed to mount"); + vi.spyOn(root, "getBoundingClientRect").mockReturnValue(new DOMRect(0, 0, 640, 360)); + vi.spyOn(headline, "getBoundingClientRect").mockReturnValue(new DOMRect(50, 50, 300, 40)); + vi.spyOn(window, "getComputedStyle").mockImplementation( + () => + ({ + display: "block", + visibility: "visible", + opacity: "1", + color: "rgb(255,255,255)", + fill: "", + clipPath: "none", + fontSize: "32px", + fontWeight: "700", + }) as unknown as CSSStyleDeclaration, + ); + + const page = fakePage(); + const injectScript = page.addScriptTag; + page.addScriptTag = vi.fn(async (arg: { content: string }) => { + await injectScript(arg); + const w = window as unknown as { + __contrastAuditFinish?: (...args: unknown[]) => Promise; + __contrastAuditRestoreIfPending?: () => void; + }; + if (w.__contrastAuditFinish) { + w.__contrastAuditFinish = async () => { + w.__contrastAuditRestoreIfPending?.(); + return []; + }; + } + }); + page.screenshot = vi.fn(async () => + headline.style.getPropertyValue("color") === "transparent" + ? "text-hidden-base64" + : "text-visible-base64", + ); + installSessionMock(page); + + const result = await runBrowserCheck( + PROJECT, + { ...DEFAULT_CHECK_OPTIONS, samples: 1, contrast: true, snapshots: true }, + { kind: "none" }, + runAuditGrid, + ); + + expect(page.screenshot).toHaveBeenCalledTimes(2); + expect(result.screenshots[0]?.pngBase64).toBe("text-visible-base64"); +}); + it("carries validate's clip-duration audit into the runtime findings", async () => { vi.spyOn(Date, "now").mockReturnValue(100); mountCanvasFixture(); diff --git a/packages/cli/src/utils/checkBrowser.ts b/packages/cli/src/utils/checkBrowser.ts index 87923e026..3f2abdc6b 100644 --- a/packages/cli/src/utils/checkBrowser.ts +++ b/packages/cli/src/utils/checkBrowser.ts @@ -520,6 +520,12 @@ async function collectContrast( ); const finished = raw.flatMap(parseFinishedContrast); const entries = joinContrastEntries(finished, prepared); + // __contrastAuditFinish restores the text paint hidden by prepare. The + // measurement screenshot intentionally has glyphs removed so contrast + // can sample the pixels behind them; never persist that image as visual + // QA evidence. Capture the restored page for the overview instead. + const overviewShot = await page.screenshot({ encoding: "base64", type: "png" }); + if (typeof overviewShot !== "string") throw new Error("Overview screenshot was not base64"); // Contrast failures are only known once measurement above completes, so // they're appended to the layout-derived annotations passed in by the // pipeline rather than being requested up front. @@ -527,7 +533,7 @@ async function collectContrast( ...layoutAnnotations, ...contrastFailureAnnotations(entries, layoutAnnotations.length), ]; - const pngBase64 = await captureOverviewShot(page, annotations, measurementShot); + const pngBase64 = await captureOverviewShot(page, annotations, overviewShot); return { entries, pngBase64 }; } finally { await page