/** * Render the built app and write a PNG you can open and judge. * * The defects this exists for cannot be asserted. "The ocean has no specular * response", "the board ends in a hard diamond edge", "the aircraft are * illegible at board scale" are all true of code that typechecks, passes every * test and meets every performance budget. The only instrument that finds them * is a picture, so this makes taking one cheap. * * node scripts/look.mjs [--url ] [--phone] [--at ] * [--wait ] [--click ] * * Writes /tmp/tera-look/.png. `--at` pins the clock, because the sun's * position is computed from the real one and a shot taken at 03:00 tells you * nothing about how the water reads at noon. * * This box has an AMD card and no monitor; the ANGLE/Vulkan flags below are what * make Chrome render headless here rather than falling back to a blank canvas. */ import { spawn } from "node:child_process"; import { mkdirSync } from "node:fs"; import { chromium } from "playwright"; const args = process.argv.slice(2); const name = args[0] ?? "look"; const flag = (f, d) => { const i = args.indexOf(f); return i === -1 ? d : args[i + 1]; }; const has = (f) => args.includes(f); const OUT = "/tmp/tera-look"; mkdirSync(OUT, { recursive: true }); const PORT = 4700 + Math.floor(Math.random() * 200); const server = spawn("npx", ["vite", "preview", "--port", String(PORT), "--strictPort"], { stdio: "ignore", }); await new Promise((r) => setTimeout(r, 4000)); const phone = has("--phone"); const browser = await chromium.launch({ channel: "chrome", args: [ "--use-gl=angle", "--use-angle=vulkan", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist", ], }); const context = await browser.newContext({ viewport: phone ? { width: 390, height: 844 } : { width: 1600, height: 1000 }, deviceScaleFactor: 2, timezoneId: "America/Los_Angeles", ...(phone ? { isMobile: true, hasTouch: true, userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 " + "(KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", } : {}), }); await context.clock.setFixedTime(new Date(flag("--at", "2026-08-21T20:00:00Z"))); const page = await context.newPage(); const errors = []; page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); }); await page.goto(`http://localhost:${PORT}${flag("--url", "/")}`, { waitUntil: "networkidle", timeout: 60000, }); await page.waitForTimeout(Number(flag("--wait", "10000"))); // The first-run flow covers the scene it is teaching you about. try { await page.getByText(/^Skip$/).first().click({ timeout: 2500 }); await page.waitForTimeout(1200); } catch { /* already dismissed, or not shown */ } const click = flag("--click", ""); if (click !== "") { try { await page.getByText(new RegExp(click, "i")).first().click({ timeout: 5000 }); await page.waitForTimeout(8000); } catch { console.log(`look: could not click ${click}`); } } const path = `${OUT}/${name}.png`; await page.screenshot({ path }); console.log(`look: wrote ${path}`); // Two 404s on a static preview are the zero-config path working: see deploy/STATIC.md. const real = errors.filter((e) => !/404|Failed to load resource/.test(e)); console.log(real.length === 0 ? "look: no console errors" : `look: ERRORS ${JSON.stringify(real)}`); await browser.close(); server.kill();