Der E2E-Lauf belegt im gebauten Stand, was die Unit- und DB-Tests nur einzeln zeigen: ein Admin speichert SMTP, die Testnachricht kommt ueber den Worker in Mailpit an, das Passwort kehrt nie in die Seite zurueck, und ein Leser sieht keine Aendern-Knoepfe. Die Screenshots (1440/390 px) halten das Layout der neuen Seite fuer die Anleitung fest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
5.4 KiB
TypeScript
134 lines
5.4 KiB
TypeScript
/**
|
||
* 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"]',
|
||
},
|
||
// 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();
|