Ctrl K
ComunidadGuías…

test/scripts/screenshots-compositing-browser.contract.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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<number> => {
  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<void> => {
  await page.locator("#host").evaluate((host) => {
    const root = host.attachShadow({ mode: "open" });
    root.innerHTML =
      '<button style="background: red; color: blue">Words</button>';
  });
  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, '<div id="host"></div>', async (page) => {
      await page.locator("#host").evaluate((host) => {
        const outer = host.attachShadow({ mode: "open" });
        outer.innerHTML = '<div id="nested"></div>';
        const nested = outer.querySelector("#nested");
        if (!nested) throw new Error("Missing nested shadow host.");
        const inner = nested.attachShadow({ mode: "open" });
        inner.innerHTML = `<style>
          button { background: rgb(200, 0, 0); color: rgb(100, 0, 0); filter: brightness(2); }
        </style><button>Words</button>`;
      });
      const { layers } = await capturePreparedLayers(page);

      await expectOnlyLayerColor(layers, [255, 0, 0], "background");
    });
  });

  test("recombines a whole-paint shadow host", async () => {
    await withPage(
      browser,
      '<div id="host" style="opacity: 0.5"></div>',
      (page) => expectShadowButtonRecombines(page, "shadow host opacity group"),
    );
  });

  test("recombines a shadow host inside a whole-paint ancestor", async () => {
    await withPage(
      browser,
      '<div style="opacity: 0.5"><div id="host"></div></div>',
      (page) =>
        expectShadowButtonRecombines(
          page,
          "shadow host ancestor opacity group",
        ),
    );
  });

  test("recombines generated opacity groups", async () => {
    await withPage(
      browser,
      `<style>
        body { background: white; margin: 0; }
        div::before { background: red; color: blue; content: "Badge"; opacity: 0.5; }
      </style><div>Words</div>`,
      (page) => expectLayersRecombine(page, "generated opacity group"),
    );
  });

  test("recombines mix-blend-mode groups", async () => {
    await withPage(
      browser,
      `<style>
        body { background: rgb(50, 100, 150); margin: 0; }
        div { background: rgb(200, 100, 50); color: white; mix-blend-mode: multiply; }
      </style><div>Words</div>`,
      (page) => expectLayersRecombine(page, "blend group"),
    );
  });

  test("recombines backdrop-filter groups", async () => {
    await withPage(
      browser,
      `<style>
        body { background: linear-gradient(to right, red, blue); margin: 0; }
        button { backdrop-filter: blur(8px); background: rgb(255 255 255 / 0.5); border: 0; height: 80px; width: 160px; }
      </style><button>Words</button>`,
      (page) => expectLayersRecombine(page, "backdrop-filter group"),
    );
  });

  test("recombines masked paint groups", async () => {
    await withPage(
      browser,
      `<style>
        body { background: white; margin: 0; }
        div { background: red; color: blue; font: 80px sans-serif; mask-image: linear-gradient(to right, transparent, black); }
      </style><div>Words</div>`,
      (page) => expectLayersRecombine(page, "masked paint group"),
    );
  });

  test("separates generated content owned by the html root", async () => {
    await withPage(
      browser,
      `<style>
        html::before { background: red; color: blue; content: "Banner"; }
      </style>`,
      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,
      `<style>
        @keyframes pulse { from { background: red; } to { background: blue; } }
        button { animation: pulse 10s infinite alternate; border: 0; height: 80px; width: 160px; }
      </style><button>Words</button>`,
      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,
      `<style>
        button { background: red; border: 0; height: 80px; transition: background 10s; width: 160px; }
      </style><button>Words</button>`,
      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,
      `<style>
        body { background: white; color: blue; }
        dialog { background: white; color: red; }
        dialog::backdrop { background: rgb(0 0 0 / 0.5); }
      </style><button>Under</button><dialog>Over</dialog>`,
      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,
      `<style>
        body { background: white; margin: 0; }
        div { background: red; clip-path: polygon(0 0, 70% 0, 40% 100%, 0 100%); color: blue; font: 80px sans-serif; }
      </style><div>Words</div>`,
      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,
      `<style>
        button::before { content: url("${icon}"); }
      </style><button>Words</button>`,
      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,
      '<div style="opacity: 0.5">Words</div>',
      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,
      '<output id="result">Waiting</output>',
      async (page) => {
        const requestStarted = Promise.withResolvers<void>();
        const releaseResponse = Promise.withResolvers<void>();
        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,
      `<style>
        body { background: white; margin: 0; }
        p { background: rgb(255, 0, 0); background-clip: text; color: transparent; font: 80px sans-serif; margin: 0; }
      </style><p>Words</p>`,
      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,
      `<button aria-label="Changing control" style="border: 0; height: 80px; margin: 0; width: 160px"></button><script>
        globalThis.ticks = 20;
        const update = () => {
          globalThis.ticks = globalThis.ticks === 219 ? 20 : globalThis.ticks + 1;
          const button = document.querySelector("button");
          document.body.style.background = "rgb(" + globalThis.ticks + ", 0, 0)";
          button.style.background = "rgb(0, " + globalThis.ticks + ", 0)";
        };
        update();
        setInterval(update, 1);
      </script>`,
      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);
      },
    );
  });
});