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

This commit was merged in pull request #36.
This commit is contained in:
2026-09-26 09:04:15 +00:00
parent 9bd198a531
commit fbbb634e56
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();
});