Botanica/scripts/verify-m2.cjs

216 lines
7.5 KiB
JavaScript

const fs = require("node:fs"),
path = require("node:path");
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || "playwright");
const directory = "artifacts/m2";
fs.mkdirSync(directory, { recursive: true });
(async () => {
const browser = await chromium.launch({
headless: true,
...(process.env.BROWSER_EXECUTABLE
? { executablePath: process.env.BROWSER_EXECUTABLE }
: {}),
});
const page = await browser.newPage({
viewport: { width: 1440, height: 1100 },
reducedMotion: "reduce",
acceptDownloads: true,
});
const report = { routes: [], exports: [], errors: [] };
page.on("pageerror", (e) => report.errors.push(e.message));
async function checkWidth(route, width) {
await page.setViewportSize({ width, height: 1100 });
await page.goto("http://localhost:8155" + route);
await page
.getByRole("status")
.filter({ hasText: "Laboratory online" })
.waitFor();
const size = await page.evaluate(() => ({
width: innerWidth,
scrollWidth: document.documentElement.scrollWidth,
}));
if (size.width !== size.scrollWidth)
throw Error("Horizontal overflow " + route + " " + width);
report.routes.push({ route, ...size });
}
await checkWidth("/lab", 1440);
await page.getByLabel("Golden veins · manual preview").check();
const before = await page.locator(".trait-summary").innerText();
await page.getByLabel("Visual seed", { exact: true }).fill("9876");
if ((await page.locator(".trait-summary").innerText()) !== before)
throw Error("Seed changed phenotype");
await page.getByLabel("Visual seed", { exact: true }).fill("8155");
await page.getByLabel("Golden veins · manual preview").uncheck();
await page.screenshot({
path: directory + "/lab-desktop.png",
fullPage: true,
});
for (const background of ["warm", "transparent"]) {
await page
.getByLabel("Preview & export background")
.selectOption(background);
for (const format of ["svg", "png"]) {
const [download] = await Promise.all([
page.waitForEvent("download"),
page
.getByRole("button", {
name: "Export " + format.toUpperCase(),
exact: true,
})
.click(),
]);
if (await download.failure()) throw Error("Download failed");
await download.saveAs(directory + "/export-" + background + "." + format);
}
const svg = fs.readFileSync(
directory + "/export-" + background + ".svg",
"utf8",
);
if (
!svg.includes('viewBox="0 0 512 512"') ||
/<script|foreignObject|<image|href=/.test(svg)
)
throw Error("Unsafe SVG export");
const png = fs
.readFileSync(directory + "/export-" + background + ".png")
.toString("base64");
const comparison = await page.evaluate(
async ({ svg, png }) => {
async function raster(url) {
const image = new Image();
image.src = url;
await image.decode();
const canvas = document.createElement("canvas");
canvas.width = 512;
canvas.height = 512;
const c = canvas.getContext("2d");
c.drawImage(image, 0, 0, 512, 512);
return {
width: image.naturalWidth,
height: image.naturalHeight,
data: c.getImageData(0, 0, 512, 512).data,
};
}
const a = await raster("data:image/png;base64," + png),
b = await raster(
"data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg),
);
const preview = document.querySelector(
".specimen>.plant-preview svg",
).outerHTML;
const c = await raster(
"data:image/svg+xml;charset=utf-8," + encodeURIComponent(preview),
);
let pngDifference = 0,
previewDifference = 0;
for (let i = 0; i < a.data.length; i++) {
pngDifference += Math.abs(a.data[i] - b.data[i]);
previewDifference += Math.abs(a.data[i] - c.data[i]);
}
return {
width: a.width,
height: a.height,
corner: Array.from(a.data.slice(0, 4)),
pngToSvgMeanDifference: pngDifference / a.data.length,
pngToPreviewMeanDifference: previewDifference / a.data.length,
};
},
{ svg, png },
);
if (
comparison.width !== 512 ||
comparison.height !== 512 ||
comparison.pngToSvgMeanDifference > 0.1 ||
comparison.pngToPreviewMeanDifference > 0.1
)
throw Error("Export mismatch " + JSON.stringify(comparison));
if (
background === "transparent"
? comparison.corner[3] !== 0
: comparison.corner.join(",") !== "246,243,232,255"
)
throw Error("Wrong background alpha/color");
report.exports.push({ background, ...comparison });
}
await page.getByLabel("Preview & export background").selectOption("warm");
await page
.locator(".specimen>.plant-preview")
.screenshot({ path: directory + "/hero-512.png" });
await page
.locator(".specimen-bottom .plant-preview")
.screenshot({ path: directory + "/icon-64.png" });
await checkWidth("/gallery", 1440);
if ((await page.locator(".plant-card").count()) !== 12)
throw Error("Expected 12 studies");
await page.screenshot({
path: directory + "/atlas-desktop.png",
fullPage: true,
});
await page.evaluate(() => {
window.__qa = {
gaps: [],
tasks: [],
start: performance.now(),
last: performance.now(),
};
new PerformanceObserver((list) =>
window.__qa.tasks.push(...list.getEntries().map((e) => e.duration)),
).observe({ type: "longtask", buffered: false });
const frame = (now) => {
window.__qa.gaps.push(now - window.__qa.last);
window.__qa.last = now;
if (now - window.__qa.start < 3000) requestAnimationFrame(frame);
};
requestAnimationFrame(frame);
});
await page
.getByRole("button", { name: "Show 100-seed study", exact: true })
.click();
await page.waitForFunction(
() => document.querySelectorAll(".plant-card").length === 100,
);
await page.evaluate(
() =>
new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))),
);
report.performance = await page.evaluate(() => ({
cards: document.querySelectorAll(".plant-card").length,
elapsedMs: performance.now() - window.__qa.start,
maxFrameGapMs: Math.max(...window.__qa.gaps),
longTasksMs: window.__qa.tasks,
uniqueIds: new Set(
Array.from(document.querySelectorAll("[id]")).map((e) => e.id),
).size,
ids: document.querySelectorAll("[id]").length,
}));
if (report.performance.ids !== report.performance.uniqueIds)
throw Error("SVG instance ID collision");
await page.screenshot({ path: directory + "/smoke-desktop.png" });
await page
.getByRole("button", { name: "Show 12 studies", exact: true })
.click();
await page.waitForFunction(
() => document.querySelectorAll(".plant-card").length === 12,
);
for (const route of ["/lab", "/gallery", "/breed", "/about"]) {
await checkWidth(route, 360);
if (["/lab", "/gallery"].includes(route))
await page.screenshot({
path:
directory +
"/" +
(route === "/lab" ? "lab" : "atlas") +
"-mobile.png",
fullPage: true,
});
}
if (report.errors.length) throw Error(report.errors.join("\n"));
fs.writeFileSync(
directory + "/browser-checks.json",
JSON.stringify(report, null, 2) + "\n",
);
console.log(JSON.stringify(report, null, 2));
await browser.close();
})().catch((error) => {
console.error(error);
process.exit(1);
});