/**
* 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);
});