import { Buffer } from "node:buffer"; import { expect } from "@std/expect"; import { afterAll, beforeAll, describe, it as test } from "@std/testing/bdd"; import type { Browser, Page } from "playwright"; import sharp from "sharp"; import { capturePreparedLayers } from "#scripts/screenshots/capture.ts"; import { addScreenshotStyle } from "#scripts/screenshots/style.ts"; import { countRgbPixels, launchScreenshotBrowser, layerStyle, withLayer, withPage, } from "./screenshots-browser-helpers.ts"; const expectScreenshotStyle = async ( page: Page, css: string, expectedColor: string, ): Promise => { const removeStyle = await addScreenshotStyle(page, css); try { expect(await layerStyle(page, "text", "p", "color")).toBe(expectedColor); } finally { await removeStyle(); } }; describe("screenshot layer browser contracts", () => { let browser: Browser; beforeAll(async () => { browser = await launchScreenshotBrowser(); }); afterAll(async () => { await browser.close(); }); test("keeps SVG control icons in the controls layer", async () => { await withPage( browser, '', async (page) => { expect(await layerStyle(page, "controls", "use", "fill")).toBe( "rgb(0, 0, 255)", ); expect( await layerStyle(page, "controls", "span", "-webkit-text-fill-color"), ).toBe("rgba(0, 0, 0, 0)"); }, ); }); test("keeps generated control labels out of the controls layer", async () => { await withPage( browser, '', async (page) => { expect( await layerStyle( page, "controls", "button", "-webkit-text-fill-color", "::after", ), ).toBe("rgba(0, 0, 0, 0)"); expect( await layerStyle(page, "text", "button", "color", "::after"), ).toBe("rgb(255, 0, 0)"); }, ); }); test("keeps selection highlights out of the controls layer", async () => { await withPage( browser, ` `, async (page) => { await page .locator("input") .evaluate((input) => (input as HTMLInputElement).select()); expect( await layerStyle( page, "controls", "input", "background-color", "::selection", ), ).toBe("rgba(0, 0, 0, 0)"); expect( await layerStyle( page, "text", "input", "background-color", "::selection", ), ).toBe("rgb(255, 255, 0)"); }, ); }); test("keeps nested generated control labels out of the controls layer", async () => { await withPage( browser, ``, async (page) => { expect( await layerStyle( page, "controls", "button span", "-webkit-text-fill-color", "::after", ), ).toBe("rgba(0, 0, 0, 0)"); }, ); }); test("keeps foreignObject labels in the text layer", async () => { await withPage( browser, '
Label
', async (page) => { expect( await layerStyle(page, "text", "foreignObject", "visibility"), ).toBe("visible"); expect( await layerStyle(page, "text", "foreignObject div", "color"), ).not.toBe("rgba(0, 0, 0, 0)"); }, ); }); test("keeps embedded documents out of the text layer", async () => { await withPage( browser, '', async (page) => { expect(await layerStyle(page, "text", "iframe", "visibility")).toBe( "hidden", ); expect( await layerStyle(page, "background", "iframe", "visibility"), ).toBe("visible"); }, ); }); test("puts disabled btn spans in the controls layer", async () => { await withPage( browser, 'Unavailable', async (page) => { expect(await layerStyle(page, "background", ".btn", "visibility")).toBe( "hidden", ); expect(await layerStyle(page, "controls", ".btn", "visibility")).toBe( "visible", ); }, ); }); test("applies layer masks without bypassing page CSP", async () => { await withPage( browser, `

Words

`, async (page) => { expect(await layerStyle(page, "text", "p", "color")).toBe( "rgb(0, 0, 0)", ); expect( await layerStyle(page, "background", "p", "-webkit-text-fill-color"), ).toBe("rgba(0, 0, 0, 0)"); }, ); }); test("puts label-backed and disclosure widgets in the controls layer", async () => { await withPage( browser, `
More

Details

`, async (page) => { for (const selector of [".order-card", ".row-select", "summary"]) { expect( await layerStyle(page, "background", selector, "visibility"), ).toBe("hidden"); expect( await layerStyle(page, "controls", selector, "visibility"), ).toBe("visible"); } }, ); }); test("puts combobox options in the controls layer", async () => { await withPage( browser, '
Matching attendee
', async (page) => { expect( await layerStyle(page, "background", '[role="option"]', "visibility"), ).toBe("hidden"); expect( await layerStyle(page, "controls", '[role="option"]', "visibility"), ).toBe("visible"); }, ); }); test("preserves native control geometry in the text layer", async () => { await withPage( browser, '', async (page) => { const geometry = () => page.locator("label").evaluate((label) => { const control = label.querySelector("input"); const words = label.querySelector("span"); if (!control || !words) throw new Error("Missing test control."); const controlBox = control.getBoundingClientRect(); const wordsBox = words.getBoundingClientRect(); return { control: [ controlBox.x, controlBox.y, controlBox.width, controlBox.height, ], words: [wordsBox.x, wordsBox.y, wordsBox.width, wordsBox.height], }; }); const normal = await geometry(); expect(await withLayer(page, "text", geometry)).toEqual(normal); }, ); }); test("keeps modal backdrops out of the text layer", async () => { await withPage( browser, "Words", async (page) => { await page .locator("dialog") .evaluate((dialog) => (dialog as HTMLDialogElement).showModal()); expect( await layerStyle( page, "text", "dialog", "background-color", "::backdrop", ), ).toBe("rgba(0, 0, 0, 0)"); expect( await layerStyle( page, "background", "dialog", "background-color", "::backdrop", ), ).toBe("rgb(1, 2, 3)"); }, ); }); test("keeps date picker indicator pixels out of the text layer", async () => { await withPage( browser, ``, async (page) => { const redPixels = async (layer: "controls" | "text") => { const png = await withLayer(page, layer, () => page.locator("input").screenshot({ omitBackground: true }), ); return await countRgbPixels(png, [255, 0, 0]); }; expect(await redPixels("controls")).toBeGreaterThan(0); expect(await redPixels("text")).toBe(0); }, ); }); test("preserves imports in screenshot CSS", async () => { await withPage(browser, "

Imported

", async (page) => { await expectScreenshotStyle( page, '@import url("data:text/css,p%7Bcolor%3Argb(1%2C2%2C3)%7D");', "rgb(1, 2, 3)", ); }); }); test("adds screenshot CSS on a page with a URL fragment", async () => { await withPage(browser, "

Styled

", async (page) => { await page.goto(`${page.url()}#terms`); await expectScreenshotStyle( page, "p { color: rgb(1, 2, 3); }", "rgb(1, 2, 3)", ); }); }); test("layer masks outrank existing important cascade layers", async () => { await withPage( browser, `

Words

`, async (page) => { expect( await layerStyle(page, "background", "p", "-webkit-text-fill-color"), ).toBe("rgba(0, 0, 0, 0)"); expect( await layerStyle(page, "controls", "body", "background-color"), ).toBe("rgba(0, 0, 0, 0)"); }, ); }); test("recombines an opacity group exactly", async () => { await withPage( browser, `
Words
`, async (page) => { const { layers, png: normal } = await capturePreparedLayers(page); const combined = await sharp(layers.background) .composite([ { input: Buffer.from(layers.controls) }, { input: Buffer.from(layers.text) }, ]) .ensureAlpha() .raw() .toBuffer(); expect(combined).toEqual( await sharp(normal).ensureAlpha().raw().toBuffer(), ); }, ); }); });