diff --git a/marketing/product-film/scripts/capture.mjs b/marketing/product-film/scripts/capture.mjs new file mode 100644 index 0000000..2c3a114 --- /dev/null +++ b/marketing/product-film/scripts/capture.mjs @@ -0,0 +1,82 @@ +// Drives the running CRM (npm run dev) and screenshots each manifest screen in dark mode. +// Prereq: in the CRM repo root run `npm run dev` (serves http://localhost:5173). +import { chromium } from "playwright"; +import { readFile, mkdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(__dirname, ".."); + +// tab label + identifier per role (password is always "password") +const ROLE_LOGIN = { + owner: { tab: "Owner", id: "justin" }, + agentCody: { tab: "Employee", id: "LUP-1040" }, + subCarlos: { tab: "Sub-Con", id: "carlos" }, +}; + +async function login(page, baseUrl, role) { + const creds = ROLE_LOGIN[role]; + if (!creds) throw new Error(`unknown role ${role}`); + await page.goto(`${baseUrl}/login`, { waitUntil: "networkidle" }); + // The role tab appears before the demo chip of the same name in the DOM, so .first() targets the tab. + await page.getByRole("button", { name: creds.tab, exact: true }).first().click(); + await page.fill("#identifier", creds.id); + await page.fill("#password", "password"); + await page.getByRole("button", { name: "Sign In" }).click(); + // Auth lives only in React state (no localStorage), so we must leave the + // /login route via client-side routing. Wait until we have left it. + await page.waitForFunction(() => !location.pathname.startsWith("/login")); + await page.waitForLoadState("networkidle"); +} + +// Auth state is in-memory only, so a full page reload (page.goto) would drop the +// session and bounce us to /login. Navigate inside the SPA via the History API +// and a popstate event, which React Router (BrowserRouter) listens to. +async function spaNavigate(page, route) { + await page.evaluate((r) => { + window.history.pushState({}, "", r); + window.dispatchEvent(new PopStateEvent("popstate")); + }, route); + await page.waitForFunction((r) => location.pathname === r, route); +} + +async function main() { + const manifest = JSON.parse(await readFile(join(ROOT, "src/captures.manifest.json"), "utf8")); + const outDir = join(ROOT, "public/captures"); + await mkdir(outDir, { recursive: true }); + + const browser = await chromium.launch(); + const context = await browser.newContext({ + viewport: { width: manifest.viewport.width, height: manifest.viewport.height }, + deviceScaleFactor: manifest.viewport.deviceScaleFactor, + }); + // Force dark theme before any app code runs. + await context.addInitScript((theme) => { + try { localStorage.setItem("theme", theme); } catch (e) {} + }, manifest.theme); + + const page = await context.newPage(); + + // Group by role so we log in once per role, in manifest order. + let currentRole = null; + for (const cap of manifest.captures) { + if (cap.role !== currentRole) { + await login(page, manifest.baseUrl, cap.role); + currentRole = cap.role; + } + await spaNavigate(page, cap.route); + await page.waitForTimeout(cap.waitMs ?? 1500); + const path = join(outDir, `${cap.id}.png`); + await page.screenshot({ path }); // viewport-only → exact 16:9 + console.log(`captured ${cap.id} -> ${path}`); + } + + await browser.close(); + console.log(`\nDone. ${manifest.captures.length} captures written to public/captures/`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});