Files
fotospiel-app/scripts/capture-mobile-pwa-screenshots.mjs
Codex Agent 049c4f82b9
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Allowing local lemonsqueezy payment skip and emulate success response
2026-02-04 10:39:53 +01:00

241 lines
7.3 KiB
JavaScript

#!/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);
});