#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { chromium, devices } from 'playwright'; const baseUrl = process.env.BASE_URL ?? 'http://fotospiel-app.test'; const eventSlug = process.env.EVENT_SLUG ?? 'demo-starter-wedding'; const notificationFallback = process.env.NOTIFICATION_ID ?? 'demo-notification'; const helpFallback = process.env.HELP_SLUG ?? 'demo-help'; const outputDir = process.env.OUTPUT_DIR ?? path.join(process.cwd(), 'test-results', 'mobile-pwa-screenshots', timestamp()); const skipParam = 'consent=skip'; const publicRoutes = [ '/event-admin', '/event-admin/login', '/event-admin/mobile/login', '/event-admin/help', '/event-admin/forgot-password', '/event-admin/reset-password/demo-token', '/event-admin/start', '/event-admin/logout', '/event-admin/auth/callback', ]; const authedRoutes = [ '/event-admin/dashboard', '/event-admin/live', '/event-admin/events', '/event-admin/events/new', `/event-admin/events/${eventSlug}`, `/event-admin/events/${eventSlug}/recap`, `/event-admin/events/${eventSlug}/edit`, `/event-admin/events/${eventSlug}/photos`, `/event-admin/events/${eventSlug}/members`, `/event-admin/events/${eventSlug}/tasks`, `/event-admin/events/${eventSlug}/invites`, `/event-admin/events/${eventSlug}/branding`, `/event-admin/events/${eventSlug}/photobooth`, `/event-admin/events/${eventSlug}/guest-notifications`, `/event-admin/events/${eventSlug}/toolkit`, '/event-admin/mobile/events', `/event-admin/mobile/events/${eventSlug}`, `/event-admin/mobile/events/${eventSlug}/branding`, '/event-admin/mobile/events/new', `/event-admin/mobile/events/${eventSlug}/edit`, `/event-admin/mobile/events/${eventSlug}/qr`, `/event-admin/mobile/events/${eventSlug}/qr/customize`, `/event-admin/mobile/events/${eventSlug}/control-room`, `/event-admin/mobile/events/${eventSlug}/photos`, `/event-admin/mobile/events/${eventSlug}/live-show`, `/event-admin/mobile/events/${eventSlug}/live-show/settings`, `/event-admin/mobile/events/${eventSlug}/recap`, `/event-admin/mobile/events/${eventSlug}/analytics`, `/event-admin/mobile/events/${eventSlug}/members`, `/event-admin/mobile/events/${eventSlug}/tasks`, `/event-admin/mobile/events/${eventSlug}/photobooth`, `/event-admin/mobile/events/${eventSlug}/guest-notifications`, '/event-admin/mobile/notifications', '/event-admin/mobile/profile', '/event-admin/mobile/profile/account', '/event-admin/mobile/help', '/event-admin/mobile/billing', '/event-admin/mobile/billing/shop', '/event-admin/mobile/settings', '/event-admin/mobile/exports', '/event-admin/mobile/dashboard', '/event-admin/mobile/tasks', '/event-admin/mobile/uploads', ]; const welcomeRoutes = [ '/event-admin/mobile/welcome', '/event-admin/mobile/welcome/packages', '/event-admin/mobile/welcome/summary', '/event-admin/mobile/welcome/event', ]; const device = devices['iPhone 12']; function timestamp() { const now = new Date(); const pad = (value) => String(value).padStart(2, '0'); return [ now.getFullYear(), pad(now.getMonth() + 1), pad(now.getDate()), '-', pad(now.getHours()), pad(now.getMinutes()), pad(now.getSeconds()), ].join(''); } function withSkip(url) { if (url.includes(skipParam)) { return url; } return url.includes('?') ? `${url}&${skipParam}` : `${url}?${skipParam}`; } function slugify(input) { return input .replace(/^https?:\/\//, '') .replace(/\?.*$/, '') .replace(/\s+/g, '-') .replace(/[^a-zA-Z0-9-_./]/g, '') .replace(/[\\/]+/g, '_') .replace(/_{2,}/g, '_') .replace(/^_+|_+$/g, ''); } async function ensureDir(dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } async function waitForDemoAuth(page) { await page.waitForFunction(() => Boolean(window.fotospielDemoAuth?.loginAs), null, { timeout: 10000, }); } async function loginAs(page, tenantKey) { await page.goto(withSkip(`${baseUrl}/event-admin/login`), { waitUntil: 'load' }); await waitForDemoAuth(page); await page.evaluate(async (key) => { await window.fotospielDemoAuth?.loginAs(key); }, tenantKey); await page.waitForLoadState('networkidle'); } async function captureRoute(page, route, tag, log) { const url = withSkip(`${baseUrl}${route}`); try { await page.goto(url, { waitUntil: 'load' }); await page.waitForTimeout(700); const file = path.join(outputDir, `${tag}__${slugify(route)}.png`); await page.screenshot({ path: file, fullPage: true }); log.push({ url, file, status: 'ok' }); } catch (error) { log.push({ url, status: 'error', error: String(error) }); } } async function captureDerivedRoute(page, baseRoute, matchPrefix, fallback, tag, log) { const baseUrlWithSkip = withSkip(`${baseUrl}${baseRoute}`); await page.goto(baseUrlWithSkip, { waitUntil: 'load' }); await page.waitForTimeout(700); const href = await page.evaluate((prefix) => { const anchors = Array.from(document.querySelectorAll('a[href]')); const match = anchors.map((anchor) => anchor.getAttribute('href') || '').find((value) => value.includes(prefix)); return match || ''; }, matchPrefix); const route = href && href.startsWith('/') ? href : fallback; await captureRoute(page, route, tag, log); } async function main() { await ensureDir(outputDir); const log = []; const browser = await chromium.launch(); const context = await browser.newContext({ ...device, locale: 'de-DE' }); await context.addInitScript(() => { try { window.localStorage.setItem('fotospiel.consent.skip', '1'); window.localStorage.setItem('fotospiel-dev-switcher-collapsed', '1'); } catch (error) { // ignore storage errors } }); const page = await context.newPage(); for (const route of publicRoutes) { await captureRoute(page, route, 'public', log); } await loginAs(page, process.env.TENANT_KEY ?? 'cust-starter-wedding'); for (const route of authedRoutes) { await captureRoute(page, route, 'authed', log); } await captureDerivedRoute( page, '/event-admin/mobile/notifications', '/event-admin/mobile/notifications/', `/event-admin/mobile/notifications/${notificationFallback}`, 'authed', log, ); await captureDerivedRoute( page, '/event-admin/mobile/help', '/event-admin/mobile/help/', `/event-admin/mobile/help/${helpFallback}`, 'authed', log, ); await context.close(); const emptyContext = await browser.newContext({ ...device, locale: 'de-DE' }); await emptyContext.addInitScript(() => { try { window.localStorage.setItem('fotospiel.consent.skip', '1'); window.localStorage.setItem('fotospiel-dev-switcher-collapsed', '1'); } catch (error) { // ignore storage errors } }); const emptyPage = await emptyContext.newPage(); await loginAs(emptyPage, process.env.EMPTY_TENANT_KEY ?? 'cust-standard-empty'); for (const route of welcomeRoutes) { await captureRoute(emptyPage, route, 'welcome', log); } await emptyContext.close(); await browser.close(); fs.writeFileSync(path.join(outputDir, 'run-log.json'), JSON.stringify(log, null, 2)); const okCount = log.filter((entry) => entry.status === 'ok').length; console.log(`Saved ${okCount} screenshots to ${outputDir}`); } main().catch((error) => { console.error('Screenshot capture failed', error); process.exit(1); });