/** * Theme switching visual regression — captures dark + light screenshots of key pages. * * Run from web/ directory after starting backend (port 8000) + dev server (port 5173): * node test/theme-screenshots.mjs * * Output: ../docs/screenshots/__.png */ import { chromium } from '@playwright/test'; import { mkdir } from 'node:fs/promises'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const OUT_DIR = resolve(__dirname, '../../docs/screenshots/v2'); const BASE = 'http://localhost:5173'; const API = 'http://localhost:8000'; const ADMIN = { username: 'admin', password: 'admin123' }; const TEAM_USER = { username: 'screenshot_user', password: 'shotpass123' }; /** Set theme directly via localStorage + html attribute, no UI click needed. */ async function setTheme(page, theme) { await page.evaluate((t) => { localStorage.setItem('airdrama-theme', t); document.documentElement.dataset.theme = t; }, theme); // Reload to ensure ECharts and any once-mounted styles re-init await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForTimeout(400); } /** Programmatic login: POST to API → seed tokens into localStorage → navigate. */ async function login(page, creds) { const res = await page.request.post(`${API}/api/v1/auth/login`, { data: creds }); if (!res.ok()) throw new Error(`login ${creds.username} failed: ${res.status()} ${await res.text()}`); const body = await res.json(); const access = body?.tokens?.access; const refresh = body?.tokens?.refresh; const user = body?.user; if (!access) throw new Error(`login ${creds.username}: no access token in response`); await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' }); await page.evaluate(({ access, refresh, user }) => { localStorage.setItem('access_token', access); if (refresh) localStorage.setItem('refresh_token', refresh); if (user) localStorage.setItem('user', JSON.stringify(user)); }, { access, refresh, user }); } async function shot(page, slug, theme) { const file = resolve(OUT_DIR, `${slug}__${theme}.png`); await page.screenshot({ path: file, fullPage: false }); console.log(` ✓ ${slug}__${theme}.png`); } /** Visit URL, wait for network idle + a settle timeout, then screenshot in both themes. */ async function visitAndCapture(page, slug, url, opts = {}) { for (const theme of ['dark', 'light']) { await setTheme(page, theme); await page.goto(`${BASE}${url}`, { waitUntil: 'domcontentloaded' }).catch(() => {}); await page.waitForTimeout(opts.settle ?? 800); if (opts.afterLoad) await opts.afterLoad(page); await shot(page, slug, theme); } } async function main() { await mkdir(OUT_DIR, { recursive: true }); const browser = await chromium.launch({ headless: true }); const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1, }); const page = await ctx.newPage(); // Mute console errors (API 4xx/5xx in empty DB are noisy but expected) page.on('pageerror', () => {}); page.on('console', () => {}); console.log(`▼ Capturing to ${OUT_DIR}`); // 1. Login page (no auth needed) — use a fresh context so localStorage is clean console.log('\n[1/12] /login'); for (const theme of ['dark', 'light']) { await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' }); await setTheme(page, theme); await page.waitForTimeout(500); await shot(page, '01_login', theme); } // 2. Login as admin → admin pages console.log('\n[2/12] admin login'); await login(page, ADMIN); await visitAndCapture(page, '02_admin_dashboard', '/admin/dashboard', { settle: 1500 }); console.log('[3/12] /admin/dashboard'); await visitAndCapture(page, '03_admin_users', '/admin/users'); console.log('[4/12] /admin/users'); await visitAndCapture(page, '04_admin_records', '/admin/records'); console.log('[5/12] /admin/records'); await visitAndCapture(page, '05_admin_settings', '/admin/settings'); console.log('[6/12] /admin/settings'); await visitAndCapture(page, '06_admin_security', '/admin/security'); console.log('[7/12] /admin/security'); await visitAndCapture(page, '07_admin_logs', '/admin/logs'); console.log('[8/12] /admin/logs'); await visitAndCapture(page, '08_admin_assets', '/admin/assets'); console.log('[9/12] /admin/assets'); // 3. Switch to team_admin user → generation + profile + team pages console.log('\n[10/12] team_user login'); await ctx.clearCookies(); await page.evaluate(() => localStorage.clear()); await login(page, TEAM_USER); await visitAndCapture(page, '09_generation', '/app', { settle: 1200 }); console.log('[10/12] /app'); await visitAndCapture(page, '10_profile', '/profile', { settle: 1200 }); console.log('[11/12] /profile'); await visitAndCapture(page, '11_team_dashboard', '/team/dashboard', { settle: 1500 }); console.log('[12/12] /team/dashboard'); await visitAndCapture(page, '12_team_members', '/team/members'); console.log('[12/12] /team/members'); await browser.close(); console.log('\n✅ done'); } main().catch((err) => { console.error('❌ screenshot run failed:', err); process.exit(1); });