/** * Screenshots der Oberfläche für Anleitung und Design-Abnahme (#002). * * pnpm build && pnpm start # erstes Terminal * pnpm exec tsx scripts/screenshots.ts # zweites Terminal * * Umgebung: * SSHKV_URL Basis-URL (Standard http://localhost:3400) * SSHKV_BILDER Zielordner (Standard docs/anleitung/bilder) * SSHKV_SEITEN Kommagetrennte Auswahl der Seitennamen (Standard: alle) * * Prüft nebenbei, dass keine Seite horizontal scrollt (Design-Vorgabe ab 390 px), und * endet dann mit Fehlercode – so taugt das Skript auch als schneller Layout-Test. */ import { mkdir } from "node:fs/promises"; import { join } from "node:path"; import { chromium } from "@playwright/test"; const BASIS = (process.env["SSHKV_URL"] ?? "http://localhost:3400").replace(/\/$/, ""); const ZIEL = process.env["SSHKV_BILDER"] ?? join(process.cwd(), "docs/anleitung/bilder"); /** * `oeffentlich`: ohne Anmeldung aufnehmen. Alle anderen nach Anmeldung ueber den Mock-OIDC (#004). * `folgen`: Seiten mit wechselnder ID (#008) – erst `pfad` laden, dann dem ersten Link folgen, * der zum Selektor passt (z. B. der erste Host der Liste, bei SSHKV_SEED_DEMO=1 ein Demo-Host). */ const SEITEN: { name: string; pfad: string; oeffentlich?: boolean; folgen?: string }[] = [ { name: "anmelden", pfad: "/anmelden", oeffentlich: true }, { name: "uebersicht", pfad: "/" }, { name: "schluessel", pfad: "/schluessel" }, { name: "schluessel-neu", pfad: "/schluessel/neu" }, // Der Demo-Key mit nahem Ablaufdatum zeigt die meisten Badges (SSHKV_SEED_DEMO=1, #012). { name: "schluessel-detail", pfad: "/schluessel?q=wartung", folgen: 'main table a[href^="/schluessel/"]', }, { name: "personen", pfad: "/personen" }, { name: "meine-schluessel", pfad: "/meine-schluessel" }, { name: "hosts", pfad: "/hosts" }, { name: "host-detail", pfad: "/hosts", folgen: 'main table a[href^="/hosts/"]' }, // Matrix Schluessel × Host (#025): die Seite darf auf 390 px nicht seitwaerts scrollen – // nur der Matrix-Container. „belegt=1“, damit das Bild Zellen zeigt statt leerer Zeilen. { name: "zuweisungen", pfad: "/zuweisungen?belegt=1" }, { name: "vorlagen", pfad: "/vorlagen" }, // Die eingebaute „Deployment-Zugang“ zeigt Platzhalter, Laufzeit und Benutzer-Anlegen (#019). { name: "vorlage-bearbeiten", pfad: "/vorlagen", folgen: 'main a[aria-label="Deployment-Zugang bearbeiten"]', }, // Audit-Log (#027): unter md Karten statt Tabelle, die Seite darf nicht seitwaerts scrollen. { name: "protokoll", pfad: "/protokoll" }, // Benachrichtigungen, Zeitplaene, Instanz (#028) – als Admin mit Formular und Testknoepfen. { name: "einstellungen", pfad: "/einstellungen" }, ]; const ANSICHTEN = [ { name: "desktop", width: 1440, height: 900 }, { name: "mobil", width: 390, height: 844 }, ]; async function main() { const auswahl = process.env["SSHKV_SEITEN"]?.split(",").map((s) => s.trim()); const seiten = auswahl ? SEITEN.filter((s) => auswahl.includes(s.name)) : SEITEN; await mkdir(ZIEL, { recursive: true }); const browser = await chromium.launch(); const fehler: string[] = []; try { for (const ansicht of ANSICHTEN) { const kontext = await browser.newContext({ viewport: { width: ansicht.width, height: ansicht.height }, deviceScaleFactor: 1, locale: "de-DE", timezoneId: "Europe/Berlin", colorScheme: "dark", }); const seite = await kontext.newPage(); let angemeldet = false; for (const s of seiten) { if (!s.oeffentlich && !angemeldet) { // Anmeldung ueber den Mock-OIDC aus docker-compose.test.yml, Rolle Admin. await seite.goto(`${BASIS}/anmelden`); await seite .getByRole("link", { name: /anmelden/i }) .first() .click(); await seite.locator('button[data-rolle="admin"]').click(); await seite.getByRole("button", { name: "Anmelden", exact: true }).click(); await seite.waitForURL(`${BASIS}/`); angemeldet = true; } let antwort = await seite.goto(`${BASIS}${s.pfad}`, { waitUntil: "networkidle" }); if (s.folgen && antwort?.ok()) { const links = seite.locator(s.folgen); const ziel = (await links.count()) > 0 ? await links.first().getAttribute("href") : null; if (!ziel) { fehler.push(`${s.pfad}: kein Link für „${s.name}“ (${s.folgen}) – Demo-Daten geseedet?`); continue; } antwort = await seite.goto(`${BASIS}${ziel}`, { waitUntil: "networkidle" }); } if (!antwort?.ok()) { fehler.push(`${s.pfad}: HTTP ${antwort?.status() ?? "keine Antwort"}`); continue; } // Erst nach dem Laden der selbst gehosteten Schriften fotografieren, sonst // stehen Fallback-Schriften im Bild. await seite.evaluate(() => document.fonts.ready); const ueberlauf = await seite.evaluate( () => document.documentElement.scrollWidth - document.documentElement.clientWidth, ); if (ueberlauf > 0) { fehler.push(`${s.pfad} @ ${ansicht.width}px: ${ueberlauf}px horizontaler Überlauf`); } const datei = join(ZIEL, `${s.name}-${ansicht.name}.png`); await seite.screenshot({ path: datei, fullPage: true }); console.log(`✓ ${datei}`); } await kontext.close(); } } finally { await browser.close(); } if (fehler.length > 0) { console.error(`\n${fehler.length} Problem(e):\n- ${fehler.join("\n- ")}`); process.exitCode = 1; } } await main();