feat(doku): Anleitung, PDF und Ende-zu-Ende-Abnahme
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s

Neun Kapitel unter docs/anleitung/ fuer Technik und Verwaltung, mit 35
Bildern aus `pnpm docs:screenshots` und einem Fehlerbehebungskapitel aus
echten Meldungen. `pnpm docs:pdf` macht daraus 72 Seiten A4 — hell
gedruckt, mit Deckblatt, Inhaltsverzeichnis und Seitenzahlen, ohne neue
Abhaengigkeit.

Die Ablaeufe pruefen ihre Voraussetzungen, bevor der erste startet:
antwortet ein gebauter Stand, steht der Seed-Bestand, und liest der
Server dieselbe Datenbank wie der Testprozess. Jeder Ablauf legt an, was
er braucht, und raeumt es weg. Vier neue kommen dazu: Hauptablauf,
Onboarding mit Vergleich der Zwischenablage, Rollen und 390 px auf
achtzehn Seiten.

Drei Fehler kamen dabei heraus. Das Projekt „handy" pruefte 412 px statt
der verlangten 390. Ein angemeldeter Benutzer ohne Recht erfuhr nie,
warum er abgewiesen wurde — die Meldung stand auf einer Seite, die ihn
sofort weiterleitet; jetzt gibt es /kein-zugriff. Und der
Verzeichnis-Ablauf schrieb die Attributzuordnung der ersten vorhandenen
Quelle um, in einer echten Installation also der produktiven.

101 Ablaeufe in INTERN und 97 in DIENSTLEISTUNG, beide ohne Fehlschlag.

Closes #18

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-26 11:03:02 +02:00
co-authored by Claude Opus 5
parent 9bd198a531
commit 10e272654f
104 changed files with 4527 additions and 299 deletions
+44 -24
View File
@@ -1,6 +1,7 @@
import { expect, test, type Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import "dotenv/config";
import { prisma } from "@/lib/db";
import { anmelden } from "./helfer";
/**
* Uebersicht und Druckansicht am gebauten Stand (#015).
@@ -9,17 +10,22 @@ import { prisma } from "@/lib/db";
* Dringlichkeit, die Druckansicht gehoert auf Papier. Was davon stimmt, zeigt
* erst der Browser — die Lesemodelle dahinter pruefen `tests/uebersicht.test.ts`
* und `tests/druckansicht.test.ts`.
*
* Voraussetzungen prueft `tests/e2e/global-setup.ts`. Jeden Lauf, den diese
* Ablaeufe brauchen, legen sie selbst an und raeumen ihn hinterher weg. Vorher
* holten sie sich mit `findFirstOrThrow` irgendeinen vorhandenen Lauf und
* erwarteten, dass „Zuletzt bearbeitet“ gefuellt ist — gegen eine frische
* Datenbank scheiterte beides, das eine mit „No record was found“, das andere
* mit einem Zeitablauf auf einen Verweis, den es nicht gab (#018).
*/
async function anmelden(seite: Page) {
const email = process.env["SEED_ADMIN_EMAIL"];
const passwort = process.env["SEED_ADMIN_PASSWORD"];
if (!email || !passwort) throw new Error("SEED_ADMIN_EMAIL und SEED_ADMIN_PASSWORD fehlen.");
await seite.goto("/anmelden");
await seite.fill('input[name="email"]', email);
await seite.fill('input[name="passwort"]', passwort);
await Promise.all([seite.waitForURL("**/"), seite.click('button[type="submit"]')]);
}
const STEMPEL = Date.now();
/** Fortlaufend, damit die Laeufe eines Durchgangs sich nicht verwechseln. */
let laufende = 0;
/** Name und Titel tragen diesen Praefix – nur daran raeumt der Nachlauf auf. */
const PRAEFIX = "ZZ E2E Druck";
/**
* Ein Lauf aus einer Vorlage, die `{{generiertes_passwort}}` wirklich enthaelt.
@@ -27,12 +33,16 @@ async function anmelden(seite: Page) {
* Direkt ueber Prisma: geprueft werden soll die **Druckansicht**, nicht der Weg
* zum Lauf — den prueft `tests/e2e/laeufe.spec.ts` vom Anlegen bis zum Archiv.
*/
async function laufMitPasswortschritt(): Promise<string> {
const stempel = Date.now();
const kunde = await prisma.kunde.findFirstOrThrow({ select: { id: true } });
const vorlage = await prisma.vorlage.create({
data: { name: `ZZ E2E Druck ${stempel}`, kategorie: "ZZ Test" },
async function laufMitPasswortschritt(): Promise<{ id: string; titel: string }> {
laufende += 1;
const marke = `${PRAEFIX} ${STEMPEL}-${laufende}`;
// Ueber `istEigeneOrganisation` und nicht ueber den ersten Treffer: im Modus
// DIENSTLEISTUNG waere das ein beliebiger Demo-Kunde.
const kunde = await prisma.kunde.findFirstOrThrow({
where: { istEigeneOrganisation: true },
select: { id: true },
});
const vorlage = await prisma.vorlage.create({ data: { name: marke, kategorie: "ZZ Test" } });
const version = await prisma.vorlagenVersion.create({
data: { vorlageId: vorlage.id, version: 1, status: "ENTWURF" },
});
@@ -53,9 +63,9 @@ async function laufMitPasswortschritt(): Promise<string> {
});
const lauf = await prisma.lauf.create({
data: { vorlagenVersionId: version.id, kundeId: kunde.id, titel: `ZZ E2E Druck ${stempel}` },
data: { vorlagenVersionId: version.id, kundeId: kunde.id, titel: marke },
});
return lauf.id;
return { id: lauf.id, titel: marke };
}
test.describe("Übersicht", () => {
@@ -66,12 +76,19 @@ test.describe("Übersicht", () => {
});
test("führt von einer Zeile in den Lauf", async ({ page }) => {
/*
* Der Ablauf bringt die Zeile mit, auf die er klickt. „Zuletzt bearbeitet“
* sortiert nach `geaendertAm` absteigend, der eben angelegte Lauf steht also
* oben — und weil der Klick auf **seinen** Titel geht, prueft der Test einen
* Weg und nicht die Datenlage.
*/
const lauf = await laufMitPasswortschritt();
await anmelden(page);
const zuletzt = page
.locator("section")
.filter({ has: page.getByRole("heading", { name: "Zuletzt bearbeitet" }) });
await zuletzt.getByRole("link").first().click();
await page.waitForURL(/\/laeufe\/[^/]+$/);
await zuletzt.getByRole("link", { name: new RegExp(lauf.titel) }).click();
await page.waitForURL(`**/laeufe/${lauf.id}`);
});
test("trägt am Handy ohne horizontales Scrollen", async ({ page, isMobile }) => {
@@ -88,8 +105,10 @@ test.describe("Übersicht", () => {
test.describe("Druckansicht", () => {
test("ist hell, ohne Navigation und passt auf A4", async ({ page }) => {
test.skip(test.info().project.name !== "desktop", "Das Seitenformat hängt nicht am Gerät.");
// Der eigene Lauf statt `findFirstOrThrow`: gegen eine frische Datenbank gab
// es keinen, und die Meldung sprach von „No record was found“ (#018).
const lauf = await laufMitPasswortschritt();
await anmelden(page);
const lauf = await prisma.lauf.findFirstOrThrow({ select: { id: true } });
await page.goto(`/drucken/lauf/${lauf.id}`);
// Keine Hülle: weder Seitenleiste noch der Scannen-Knopf gehören aufs Blatt.
@@ -123,9 +142,9 @@ test.describe("Druckansicht", () => {
* dann bestanden, wenn die Druckansicht Passwörter erzeugte (Reviewbefund
* zu #015).
*/
const laufId = await laufMitPasswortschritt();
const lauf = await laufMitPasswortschritt();
await anmelden(page);
await page.goto(`/laeufe/${laufId}`);
await page.goto(`/laeufe/${lauf.id}`);
await page.getByRole("link", { name: "Drucken" }).click();
await page.waitForURL(/\/drucken\/lauf\//);
@@ -146,7 +165,7 @@ test.describe("Druckansicht", () => {
test.afterAll(async () => {
const laeufe = await prisma.lauf.findMany({
where: { titel: { startsWith: "ZZ E2E Druck" } },
where: { titel: { startsWith: PRAEFIX } },
select: { id: true },
});
const kennungen = laeufe.map((lauf) => lauf.id);
@@ -157,6 +176,7 @@ test.afterAll(async () => {
});
await prisma.lauf.deleteMany({ where: { id: { in: kennungen } } });
}
await prisma.vorlage.deleteMany({ where: { name: { startsWith: "ZZ E2E Druck" } } });
// Die Vorlagen zuletzt: an ihnen hingen gerade noch die Laeufe.
await prisma.vorlage.deleteMany({ where: { name: { startsWith: PRAEFIX } } });
await prisma.$disconnect();
});