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 { waitForScreenshotPage } from "#scripts/screenshots/readiness.ts"; import { countRgbPixels, expectLayersRecombine, expectOnlyLayerColor, launchScreenshotBrowser, withPage, } from "./screenshots-browser-helpers.ts"; const maxChannel = async ( png: Uint8Array, channel: number, ): Promise => { const { data, info } = await sharp(png) .ensureAlpha() .raw() .toBuffer({ resolveWithObject: true }); return data.reduce( (highest, value, index) => index % info.channels === channel ? Math.max(highest, value) : highest, 0, ); }; const captureAgainstBrowser = async (page: Page) => { const browserPng = new Uint8Array( await page.screenshot({ caret: "hide", type: "png" }), ); const layered = await capturePreparedLayers(page); expect(await sharp(layered.png).ensureAlpha().raw().toBuffer()).toEqual( await sharp(browserPng).ensureAlpha().raw().toBuffer(), ); return layered.layers; }; const expectShadowButtonRecombines = async ( page: Page, label: string, ): Promise => { await page.locator("#host").evaluate((host) => { const root = host.attachShadow({ mode: "open" }); root.innerHTML = ''; }); await expectLayersRecombine(page, label); }; describe("screenshot layer compositing browser contracts", () => { let browser: Browser; beforeAll(async () => { browser = await launchScreenshotBrowser(); }); afterAll(async () => { await browser.close(); }); test("recombines whole-paint groups inside nested shadow roots", async () => { await withPage(browser, '
', async (page) => { await page.locator("#host").evaluate((host) => { const outer = host.attachShadow({ mode: "open" }); outer.innerHTML = '
'; const nested = outer.querySelector("#nested"); if (!nested) throw new Error("Missing nested shadow host."); const inner = nested.attachShadow({ mode: "open" }); inner.innerHTML = ``; }); const { layers } = await capturePreparedLayers(page); await expectOnlyLayerColor(layers, [255, 0, 0], "background"); }); }); test("recombines a whole-paint shadow host", async () => { await withPage( browser, '
', (page) => expectShadowButtonRecombines(page, "shadow host opacity group"), ); }); test("recombines a shadow host inside a whole-paint ancestor", async () => { await withPage( browser, '
', (page) => expectShadowButtonRecombines( page, "shadow host ancestor opacity group", ), ); }); test("recombines generated opacity groups", async () => { await withPage( browser, `
Words
`, (page) => expectLayersRecombine(page, "generated opacity group"), ); }); test("recombines mix-blend-mode groups", async () => { await withPage( browser, `
Words
`, (page) => expectLayersRecombine(page, "blend group"), ); }); test("recombines backdrop-filter groups", async () => { await withPage( browser, ``, (page) => expectLayersRecombine(page, "backdrop-filter group"), ); }); test("recombines masked paint groups", async () => { await withPage( browser, `
Words
`, (page) => expectLayersRecombine(page, "masked paint group"), ); }); test("separates generated content owned by the html root", async () => { await withPage( browser, ``, async (page) => { const { layers } = await capturePreparedLayers(page); await expectOnlyLayerColor(layers, [255, 0, 0], "background"); expect(await countRgbPixels(layers.controls, [0, 0, 255])).toBe(0); expect(await countRgbPixels(layers.background, [0, 0, 255])).toBe(0); expect(await countRgbPixels(layers.text, [0, 0, 255])).toBeGreaterThan( 0, ); }, ); }); test("uses one animation state for normal and layered captures", async () => { await withPage( browser, ``, async (page) => { const { layers, png } = await capturePreparedLayers(page); const pixelAt = (image: Uint8Array) => sharp(image) .extract({ height: 1, left: 20, top: 20, width: 1 }) .removeAlpha() .raw() .toBuffer(); expect(await pixelAt(layers.controls)).toEqual(await pixelAt(png)); }, ); }); test("keeps transitioned control chrome in one layer", async () => { await withPage( browser, ``, async (page) => { const { layers } = await capturePreparedLayers(page); await expectOnlyLayerColor(layers, [255, 0, 0], "controls"); }, ); }); test("keeps modal stacking in browser paint order", async () => { await withPage( browser, `Over`, async (page) => { await page .locator("dialog") .evaluate((dialog) => (dialog as HTMLDialogElement).showModal()); const layers = await captureAgainstBrowser(page); expect(await maxChannel(layers.controls, 3)).toBe(0); expect(await maxChannel(layers.text, 3)).toBe(0); }, ); }); test("keeps clipped paint together", async () => { await withPage( browser, `
Words
`, async (page) => { const layers = await captureAgainstBrowser(page); await expectOnlyLayerColor(layers, [255, 0, 0], "background"); await expectOnlyLayerColor(layers, [0, 0, 255], "background"); }, ); }); test("keeps generated images with their owner", async () => { const icon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40' height='40' fill='red' fill-opacity='.5'/%3E%3C/svg%3E"; await withPage( browser, ``, async (page) => { const layers = await captureAgainstBrowser(page); expect(await countRgbPixels(layers.controls, [255, 127, 127])).toBe(0); expect(await countRgbPixels(layers.text, [255, 127, 127])).toBe(0); }, ); }); test("marks paint once for the complete layer sequence", async () => { await withPage( browser, '
Words
', async (page) => { await page.evaluate(() => { const records: MutationRecord[] = []; new MutationObserver((changes) => records.push(...changes)).observe( document, { attributeFilter: ["data-screenshot-whole-paint"], attributes: true, subtree: true, }, ); Reflect.set(globalThis, "paintMarkRecords", records); }); await capturePreparedLayers(page); await page.evaluate(() => Promise.resolve()); expect( await page.evaluate( () => (Reflect.get(globalThis, "paintMarkRecords") as MutationRecord[]) .length, ), ).toBe(2); }, ); }); test("waits for pending page requests before capture", async () => { await withPage( browser, 'Waiting', async (page) => { const requestStarted = Promise.withResolvers(); const releaseResponse = Promise.withResolvers(); await page.route("https://screenshots.test/value", async (route) => { requestStarted.resolve(); await releaseResponse.promise; await route.fulfill({ body: "Ready" }); }); await page.evaluate(() => { void (async () => { const response = await fetch("/value"); const value = await response.text(); const output = document.querySelector("output"); if (!output) throw new Error("Missing test output."); output.textContent = value; })(); }); await requestStarted.promise; // The held request proves network idle, not a coincidental paint wait, // controls readiness. let ready = false; const readiness = (async () => { await waitForScreenshotPage(page); ready = true; })(); await page.evaluate( () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)), ), ); expect(ready).toBe(false); releaseResponse.resolve(); await readiness; expect(await page.locator("#result").textContent()).toBe("Ready"); }, ); }); test("keeps background-clipped lettering in the text layer", async () => { await withPage( browser, `

Words

`, async (page) => { const { layers } = await capturePreparedLayers(page); expect(await countRgbPixels(layers.background, [255, 0, 0])).toBe(0); expect(await countRgbPixels(layers.text, [255, 0, 0])).toBeGreaterThan( 0, ); }, ); }); test("keeps timers still for the complete layer sequence", async () => { await withPage( browser, ``, async (page) => { const { layers } = await capturePreparedLayers(page); expect(await maxChannel(layers.background, 0)).toBe( await maxChannel(layers.controls, 1), ); const beforeAdvance = await page.evaluate(() => Reflect.get(globalThis, "ticks"), ); await page.waitForFunction( (previous) => Reflect.get(globalThis, "ticks") !== previous, beforeAdvance, ); expect( await page.evaluate(() => Reflect.get(globalThis, "ticks")), ).not.toBe(beforeAdvance); }, ); }); });