feat(film): playwright capture pipeline (dark-mode, role-aware)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user