#!/usr/bin/env node /** * Bildschirmfotos der Anwendung erzeugen. * * node scripts/screenshots.mjs [pfad ...] * * Meldet sich mit dem Entwicklungszugang an (SEED_DEV_TOTP_SECRET aus der * .env) und fotografiert die angegebenen Seiten - jeweils am Schreibtisch * und am Handy, hell und dunkel. Die Bilder landen unter ./screenshots. * * Nur fuer die Entwicklung gedacht: das Skript setzt voraus, dass der * zweite Faktor mit einem bekannten Geheimnis vorbelegt ist. */ import { chromium } from '@playwright/test' import { TOTP, Secret } from 'otpauth' import { mkdir, readFile } from 'node:fs/promises' import path from 'node:path' const BASIS = process.env.SCREENSHOT_BASIS ?? 'http://localhost:3000' const AUSGABE = 'screenshots' async function envLesen() { const roh = await readFile('.env', 'utf8') const werte = {} for (const zeile of roh.split(/\r?\n/)) { const treffer = zeile.match(/^([A-Z0-9_]+)=(.*)$/) if (treffer) werte[treffer[1]] = treffer[2].replace(/^"|"$/g, '') } return werte } function code(secret, kennung) { return new TOTP({ issuer: 'Betreuungs-Cockpit', label: kennung, algorithm: 'SHA1', digits: 6, period: 30, secret: Secret.fromBase32(secret), }).generate() } const GERAETE = [ { name: 'schreibtisch', viewport: { width: 1440, height: 960 }, mobil: false }, { name: 'handy', viewport: { width: 390, height: 844 }, mobil: true }, ] async function main() { const env = await envLesen() const email = env.SEED_ADMIN_EMAIL const passwort = env.SEED_ADMIN_PASSWORT const secret = env.SEED_DEV_TOTP_SECRET if (!email || !passwort || !secret) { console.error( 'In der .env fehlen SEED_ADMIN_EMAIL, SEED_ADMIN_PASSWORT oder SEED_DEV_TOTP_SECRET.', ) process.exit(1) } // Git Bash unter Windows wandelt "/heute" in einen Windows-Pfad um. // Deshalb wird hier nur der letzte Abschnitt genommen und ein Slash // vorangestellt - "heute" und "/heute" fuehren beide zum Ziel. const pfade = process.argv.slice(2).map((roh) => { // Aus "C:/Program Files/Git/heute" wird wieder "/heute". const abschnitte = roh.split(/[\\/]+/).filter(Boolean) const gefunden = abschnitte.findIndex((teil) => teil.toLowerCase() === 'git') const rest = gefunden >= 0 ? abschnitte.slice(gefunden + 1) : abschnitte return `/${rest.join('/')}` }) if (pfade.length === 0) { console.error('Bitte mindestens einen Pfad angeben, z.B. /heute /fristen') process.exit(1) } await mkdir(AUSGABE, { recursive: true }) const browser = await chromium.launch() for (const geraet of GERAETE) { for (const thema of ['hell', 'dunkel']) { const kontext = await browser.newContext({ viewport: geraet.viewport, isMobile: geraet.mobil, hasTouch: geraet.mobil, deviceScaleFactor: 2, locale: 'de-DE', timezoneId: 'Europe/Berlin', colorScheme: thema === 'dunkel' ? 'dark' : 'light', }) // Das Farbschema merkt sich die Anwendung im localStorage. await kontext.addInitScript((wahl) => { try { localStorage.setItem('bc-thema', wahl) } catch { /* egal */ } }, thema) const seite = await kontext.newPage() // --- Anmelden ------------------------------------------------------ await seite.goto(`${BASIS}/anmelden`, { waitUntil: 'networkidle' }) if (pfade.includes('/anmelden')) { await seite.screenshot({ path: path.join(AUSGABE, `anmelden-${geraet.name}-${thema}.png`), fullPage: true, }) } await seite.fill('input[name="email"]', email) await seite.fill('input[name="passwort"]', passwort) await seite.fill('input[name="code"]', code(secret, email)) await Promise.all([ seite.waitForURL((url) => !url.pathname.startsWith('/anmelden'), { timeout: 30_000 }), seite.click('button[type="submit"]'), ]) // --- Seiten fotografieren ------------------------------------------- for (const pfad of pfade) { if (pfad === '/anmelden') continue const name = pfad.replace(/^\//, '').replace(/\//g, '-') || 'start' try { await seite.goto(`${BASIS}${pfad}`, { waitUntil: 'networkidle', timeout: 45_000 }) // Kurz warten, damit Schriften und Übergänge sitzen. await seite.waitForTimeout(600) await seite.screenshot({ path: path.join(AUSGABE, `${name}-${geraet.name}-${thema}.png`), fullPage: true, }) console.log(` ${name}-${geraet.name}-${thema}.png`) } catch (fehler) { console.warn(` ${pfad} (${geraet.name}/${thema}) fehlgeschlagen: ${fehler.message}`) } } await kontext.close() } } await browser.close() console.log(`\nFertig. Bilder liegen in ./${AUSGABE}`) } main().catch((fehler) => { console.error(fehler) process.exit(1) })