v0.2 UI capture: scripts/capture-v0.2-screenshots.mjs

Spins up `npm run dev:frontend` (Vite without Tauri), drives Playwright
Chromium at 1440x900, and writes a PNG per UI surface to
/home/jake/lumotia-v0.2-screenshots/.

Surfaces captured (16 total):

  01 Dictation (default)
  02 Files
  03 Tasks
  04 History
  05 Settings
  06 Dictation × dark/light × cave/energy/reset (6 surface sets)
  07 /float — frame-less task panel
  08 /viewer — transcript viewer
  09 /preview — Wayland-hardened transcription overlay
  10 /design-system-v2 — internal primitives showcase

The design-system-v2 route is gated by VITE_LUMOTIA_DESIGN_SYSTEM_V2=1
per Phase 5; the screenshot run picks this up via a local-only
`.env.local` (now gitignored). shootRoute() waits on a Lumotia
text node before screenshotting so the 11-primitive showcase has time
to hydrate.

Browser-preview-only console noise ("transformCallback") is expected:
some Tauri-only code paths still call invoke() during initial render
even with hasTauriRuntime() gates upstream. No effect on the
captured surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 09:13:13 +01:00
parent f03f8a01b0
commit 7f933f3ca2
2 changed files with 128 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env node
// v0.2 frontend-overhaul UI capture.
//
// Spins up `npm run dev:frontend` with the design-system-v2 env flag,
// drives Playwright Chromium at 1440x900, walks every reachable UI
// surface, and writes a PNG per surface to /home/jake/lumotia-v0.2-screenshots/.
//
// The dev server runs without Tauri, so any Tauri-only surface (record
// flow, model download, model loading state) renders the graceful
// browser-preview fallback rather than the production state. This is
// intentional and matches what the Phase 1 e2e smoke baseline captures.
//
// Usage:
// node scripts/capture-v0.2-screenshots.mjs
import { chromium } from "playwright";
import { spawn } from "node:child_process";
import { mkdir, rm } from "node:fs/promises";
import { existsSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";
const OUT_DIR = "/home/jake/lumotia-v0.2-screenshots";
const BASE = "http://localhost:1420";
const VIEWPORT = { width: 1440, height: 900 };
async function waitForUrl(url, timeoutMs = 60_000) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
try {
const res = await fetch(url, { method: "GET" });
if (res.ok) return;
} catch { /* not up yet */ }
await sleep(500);
}
throw new Error(`dev server did not come up on ${url} within ${timeoutMs}ms`);
}
async function startDevServer() {
console.log("Starting Vite dev server …");
const env = { ...process.env, VITE_LUMOTIA_DESIGN_SYSTEM_V2: "1" };
const proc = spawn("npm", ["run", "dev:frontend"], { env, stdio: "ignore" });
await waitForUrl(BASE);
console.log("Vite up at", BASE);
return proc;
}
async function shoot(page, slug, label, action) {
console.log(` ${slug}: ${label}`);
await page.goto(BASE + "/");
await page.locator("main").waitFor({ state: "visible", timeout: 20_000 }).catch(() => {});
if (action) {
try { await action(page); } catch (err) {
console.warn(` action failed for ${slug}:`, err.message);
}
}
await page.waitForTimeout(700);
await page.screenshot({ path: `${OUT_DIR}/${slug}.png` });
}
async function shootRoute(page, route, slug, label) {
console.log(` ${slug}: ${label} (${route})`);
await page.goto(BASE + route);
// Wait for at least one Lumotia text node to appear; the design-system-v2
// route in particular hydrates a lot of primitives and needs longer than
// the previous flat 1.2 s sleep.
await page.waitForSelector("text=Lumotia", { timeout: 10_000 }).catch(() => {});
await page.waitForTimeout(2500);
await page.screenshot({ path: `${OUT_DIR}/${slug}.png` });
}
async function main() {
if (existsSync(OUT_DIR)) await rm(OUT_DIR, { recursive: true, force: true });
await mkdir(OUT_DIR, { recursive: true });
const dev = await startDevServer();
let proc = dev;
let browser;
try {
browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: VIEWPORT });
const page = await ctx.newPage();
page.on("pageerror", (e) => console.warn(" pageerror:", e.message));
// Five sidebar-driven pages — click via aria-label.
await shoot(page, "01-dictation", "Dictation (default)", null);
await shoot(page, "02-files", "Files", (p) => p.click("[aria-label='Files']"));
await shoot(page, "03-tasks", "Tasks", (p) => p.click("[aria-label='Tasks']"));
await shoot(page, "04-history", "History", (p) => p.click("[aria-label='History']"));
await shoot(page, "05-settings", "Settings", (p) => p.click("[aria-label='Settings']"));
// Theme + zone variants of the default Dictation page so reviewers
// can sanity-check the warm-brutalist tokens hold up across all six
// (3 zones × 2 themes) surface sets per docs/release §15 mitigation.
for (const theme of ["dark", "light"]) {
for (const zone of ["cave", "energy", "reset"]) {
await shoot(page, `06-dictation-${theme}-${zone}`, `Dictation ${theme}/${zone}`, async (p) => {
await p.evaluate(({ t, z }) => {
document.documentElement.dataset.theme = t;
document.documentElement.dataset.zone = z;
}, { t: theme, z: zone });
});
}
}
// Standalone windows.
await shootRoute(page, "/float", "07-float", "Float — task panel");
await shootRoute(page, "/viewer", "08-viewer", "Viewer — transcript");
await shootRoute(page, "/preview", "09-preview", "Preview — transcription overlay");
// Internal preview surface (gated by VITE_LUMOTIA_DESIGN_SYSTEM_V2=1).
await shootRoute(page, "/design-system-v2", "10-design-system-v2", "Design system v0.2 preview");
console.log("");
console.log(`Screenshots written to: ${OUT_DIR}`);
} finally {
if (browser) await browser.close();
proc?.kill();
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});