multica/scripts/screenshot-pr-cards.mjs

92 lines
3.7 KiB
JavaScript

// Standalone screenshot capture for the 6 PR-card demo issues. Logs in as
// dev@localhost via the local /auth send-code → verify-code flow, then opens
// each /dev/issues/DEV-N and saves a clipped PNG focused on the right sidebar.
//
// Run: pnpm exec node scripts/screenshot-pr-cards.mjs
// Output: ./.screenshots/pr-card-DEV-{2..7}.png
import { chromium } from "@playwright/test";
import pg from "pg";
import { mkdirSync } from "node:fs";
const FRONTEND = process.env.FRONTEND_ORIGIN || "http://localhost:13101";
const API = process.env.NEXT_PUBLIC_API_URL || "http://localhost:18181";
const DB = process.env.DATABASE_URL || "postgres://multica:multica@localhost:5432/multica_multica_101?sslmode=disable";
const EMAIL = "dev@localhost";
const SLUG = "dev";
const ISSUES = [2, 3, 4, 5, 6, 7];
async function loginAndGetToken() {
const client = new pg.Client(DB);
await client.connect();
try {
await client.query("DELETE FROM verification_code WHERE email = $1", [EMAIL]);
const sendRes = await fetch(`${API}/auth/send-code`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: EMAIL }),
});
if (!sendRes.ok) throw new Error(`send-code: ${sendRes.status}`);
const row = await client.query(
"SELECT code FROM verification_code WHERE email=$1 AND used=FALSE AND expires_at>now() ORDER BY created_at DESC LIMIT 1",
[EMAIL],
);
if (row.rows.length === 0) throw new Error("no verification code");
const verifyRes = await fetch(`${API}/auth/verify-code`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: EMAIL, code: row.rows[0].code }),
});
if (!verifyRes.ok) throw new Error(`verify-code: ${verifyRes.status}`);
const data = await verifyRes.json();
return data.token;
} finally {
await client.end();
}
}
async function main() {
mkdirSync(".screenshots", { recursive: true });
const token = await loginAndGetToken();
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1600, height: 1200 } });
const page = await ctx.newPage();
// Set token before navigation so the renderer hits an authenticated state.
await page.goto(`${FRONTEND}/login`);
await page.evaluate((t) => localStorage.setItem("multica_token", t), token);
for (const n of ISSUES) {
const url = `${FRONTEND}/${SLUG}/issues/DEV-${n}`;
console.log("→", url);
await page.goto(url, { waitUntil: "domcontentloaded" });
// Wait for the PR section header to appear so the card has rendered.
const prSection = page.locator("text=Pull requests").first();
await prSection.waitFor({ timeout: 15000 }).catch(() => undefined);
// Close the floating chat widget — it overlays the right sidebar and
// hides exactly the area we want to capture. The minimize button has
// aria-label or just an icon; brute-force any chat close/minimize.
const closeChat = page.locator('[aria-label="Minimize" i], [aria-label="Close" i], button[title="Minimize" i]').first();
if (await closeChat.count()) {
await closeChat.click({ trial: false }).catch(() => undefined);
}
// Fallback: click on the chat header to collapse it.
await page.locator("text=New chat").first().click({ timeout: 1000 }).catch(() => undefined);
await page.waitForTimeout(400);
// Scroll the PR section into view.
await prSection.scrollIntoViewIfNeeded().catch(() => undefined);
await page.waitForTimeout(300);
const out = `.screenshots/pr-card-DEV-${n}.png`;
await page.screenshot({ path: out, fullPage: false });
console.log(" saved", out);
}
await browser.close();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});