Files
Lumotia/scripts/capture-v0.2-screenshots.mjs
Jake 7f933f3ca2 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>
2026-05-15 09:13:13 +01:00

125 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);
});