jacquardSnapshot

← snapshot

4258 bytes
/**
 * Verifies every lab tile actually renders something.
 *
 * The gallery only lets on-screen tiles hold a GPU context (browsers cap how
 * many may live at once), so each tile is scrolled into view, given a moment
 * to compile and draw, and then sampled for non-transparent pixels. A tile
 * that stays empty means its shader failed or its scene never drew.
 *
 * Screenshots are taken one viewport at a time for the same reason: a single
 * full-page capture would show empty stages for everything off screen.
 */

import { mkdir, rm } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { chromium } from "@playwright/test";

const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(HERE, "lab");
const BASE = process.env.TOUR_BASE ?? "http://localhost:3000";

async function main() {
  await rm(OUT, { recursive: true, force: true });
  await mkdir(OUT, { recursive: true });

  const browser = await chromium.launch();
  const page = await browser.newPage({
    viewport: { width: 1440, height: 1000 },
    deviceScaleFactor: 1,
    colorScheme: "dark",
  });

  const errors = [];
  page.on("console", (m) => {
    const t = m.text();
    if (m.type() === "error" || t.includes("[lab:") || t.includes("Too many active WebGL"))
      errors.push(t);
  });
  page.on("pageerror", (e) => errors.push(`pageerror: ${e.message}`));

  await page.goto(`${BASE}/lab`, { waitUntil: "networkidle" });
  await page.waitForSelector(".jac-lab-tile", { timeout: 30000 });
  await page.waitForTimeout(2500);

  const count = await page.locator(".jac-lab-tile").count();
  console.log(`tiles: ${count}`);

  const sample = (i) =>
    page.evaluate((idx) => {
      const tile = document.querySelectorAll(".jac-lab-tile")[idx];
      const label = tile.querySelector(".jac-lab-label")?.textContent ?? "?";
      const engine = tile.querySelector(".jac-lab-engine")?.textContent ?? "?";
      const canvas = tile.querySelector("canvas");
      let ink = null;
      if (canvas) {
        const c = document.createElement("canvas");
        c.width = 64;
        c.height = 64;
        const ctx = c.getContext("2d");
        try {
          ctx.drawImage(canvas, 0, 0, 64, 64);
          const d = ctx.getImageData(0, 0, 64, 64).data;
          let lit = 0;
          for (let i = 0; i < d.length; i += 4) {
            if (d[i + 3] > 12 && d[i] + d[i + 1] + d[i + 2] > 40) lit++;
          }
          ink = +((lit / (64 * 64)) * 100).toFixed(1);
        } catch {
          ink = -1;
        }
      }
      return { label, engine: engine.split("·")[0].trim(), hasCanvas: !!canvas, ink };
    }, i);

  const rows = [];
  for (let i = 0; i < count; i++) {
    await page.locator(".jac-lab-tile").nth(i).scrollIntoViewIfNeeded();
    await page.waitForTimeout(900);
    rows.push(await sample(i));
  }

  let blank = 0;
  for (const t of rows) {
    // A CSS tile has no canvas by design; anything else without one never
    // got a context, which is a failure, not an exemption.
    const isCss = t.engine.toUpperCase().startsWith("CSS");
    const bad = !isCss && (t.ink === null || t.ink <= 0.5);
    if (bad) blank++;
    const flag = isCss ? "css " : bad ? "BLANK" : "  ok";
    console.log(
      `  ${flag}  ${String(t.label).padEnd(20)} ${String(t.engine).padEnd(10)} ink=${t.ink ?? "n/a"}%`,
    );
  }

  // Contact sheets. Anchored on tiles rather than a pixel offset: this layout
  // scrolls an inner pane, so window.scrollTo would sit still and lie.
  const anchors = [];
  for (let i = 0; i < count; i += 4) anchors.push(i);
  let sheet = 0;
  for (const i of anchors) {
    await page.locator(".jac-lab-tile").nth(i).scrollIntoViewIfNeeded();
    await page.waitForTimeout(1400);
    sheet += 1;
    await page.screenshot({ path: path.join(OUT, `sheet-${sheet}.png`) });
  }
  console.log(`\ncontact sheets: ${sheet} (tour/lab/sheet-N.png)`);

  if (errors.length) {
    console.log("\nconsole errors:");
    for (const e of [...new Set(errors)].slice(0, 12)) console.log(`  ${e.slice(0, 160)}`);
  }
  console.log(`\nblank tiles: ${blank}`);
  await browser.close();
  process.exit(blank > 0 ? 1 : 0);
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});