#!/usr/bin/env tsx /** * Erzeugt die Screenshots reproduzierbar neu – fuer die Meilenstein-Belege unter * docs/screenshots// und fuer die Anleitung unter docs/anleitung/bilder/. * * Bilder werden nie von Hand nachbearbeitet. Aendert sich die Oberflaeche, laeuft * dieses Skript erneut. * * pnpm docs:screenshots alle Seiten nach docs/anleitung/bilder/ * pnpm docs:screenshots --ziel m0 nach docs/screenshots/m0/ * pnpm docs:screenshots --nur uebersicht nur diese Seite * * Voraussetzung: die App laeuft (pnpm start oder pnpm dev). Adresse ueber * E2E_APP_URL, Standard http://127.0.0.1:3000. */ import { mkdir } from "node:fs/promises"; import { join } from "node:path"; import { chromium, type Browser } from "@playwright/test"; const APP_URL = process.env["E2E_APP_URL"] ?? "http://127.0.0.1:3000"; const ANSICHTEN = [ { name: "desktop", breite: 1440, hoehe: 900 }, { name: "handy", breite: 390, hoehe: 844 }, ] as const; /** Seiten, die abgebildet werden. Waechst mit jedem Meilenstein. */ const SEITEN: readonly { readonly name: string; readonly pfad: string }[] = [ { name: "uebersicht", pfad: "/" }, ]; function argument(name: string): string | undefined { const index = process.argv.indexOf(`--${name}`); return index >= 0 ? process.argv[index + 1] : undefined; } async function warteAufApp(): Promise { for (let versuch = 1; versuch <= 30; versuch += 1) { try { const antwort = await fetch(APP_URL, { signal: AbortSignal.timeout(2000) }); if (antwort.ok) return; } catch { // noch nicht da } await new Promise((f) => setTimeout(f, 1000)); } throw new Error(`Die App antwortet nicht auf ${APP_URL}. Erst "pnpm start" laufen lassen.`); } /** Erzeugt die PWA-Kacheln aus dem SVG, damit keine Bilddatei von Hand entsteht. */ async function kachelnErzeugen(browser: Browser): Promise { const svg = new URL(`${APP_URL}/icon-kachel.svg`).toString(); for (const kante of [192, 512]) { const seite = await browser.newPage({ viewport: { width: kante, height: kante } }); await seite.goto(svg, { waitUntil: "load" }); await seite.screenshot({ path: join("public", `icon-${kante}.png`), omitBackground: false }); await seite.close(); console.log(` public/icon-${kante}.png`); } } const zielMeilenstein = argument("ziel"); const nur = argument("nur"); const ziel = zielMeilenstein ? join("docs", "screenshots", zielMeilenstein) : join("docs", "anleitung", "bilder"); await warteAufApp(); await mkdir(ziel, { recursive: true }); const browser = await chromium.launch(); try { await kachelnErzeugen(browser); for (const seite of SEITEN.filter((s) => !nur || s.name === nur)) { for (const ansicht of ANSICHTEN) { const kontext = await browser.newContext({ viewport: { width: ansicht.breite, height: ansicht.hoehe }, colorScheme: "dark", locale: "de-DE", timezoneId: "Europe/Berlin", deviceScaleFactor: 2, }); const tab = await kontext.newPage(); await tab.goto(`${APP_URL}${seite.pfad}`, { waitUntil: "networkidle" }); // Schriften muessen geladen sein, sonst wackelt das Bild zwischen Laeufen. await tab.evaluate(() => document.fonts.ready); const datei = join(ziel, `${seite.name}-${ansicht.name}.png`); await tab.screenshot({ path: datei, fullPage: ansicht.name === "desktop" }); // Am Handy darf nie horizontal gescrollt werden – hier gleich mitgeprueft. if (ansicht.name === "handy") { const masse = await tab.evaluate(() => ({ scroll: document.documentElement.scrollWidth, sichtbar: document.documentElement.clientWidth, })); if (masse.scroll > masse.sichtbar) { throw new Error(`${seite.pfad} scrollt am Handy horizontal: ${masse.scroll} > ${masse.sichtbar}`); } } console.log(` ${datei}`); await kontext.close(); } } } finally { await browser.close(); } console.log("Screenshots erzeugt.");