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
+41 -21
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, SEED_VORLAGE } from "./helfer";
/**
* Das Archiv am gebauten Stand (#015).
@@ -9,20 +10,18 @@ import { prisma } from "@/lib/db";
* Mandantengrenze hinein. Hier geht es um den Weg, den ein Mensch nimmt: Wort
* eintippen, Treffer sehen, **erkennen warum**, in den Lauf springen. Ein
* Treffer ohne sichtbaren Grund ist fuer den Suchenden wertlos.
*
* Voraussetzungen prueft `tests/e2e/global-setup.ts`, aufgeraeumt wird am Ende
* ueber die Datenbank: fuer einen Lauf kennt die Anwendung keinen Loeschweg, und
* das ist richtig so (#018).
*/
const STEMPEL = Date.now();
const NOTIZ = `Restposten ${STEMPEL} aus dem Lager`;
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"]')]);
}
/** Fortlaufend, damit zwei Laeufe desselben Durchgangs sich nicht verwechseln. */
let laufende = 0;
type Testlauf = { readonly id: string; readonly titel: string; readonly suchwort: string };
/**
* Ein archivierter Lauf mit einer Notiz, die es sonst nirgends gibt.
@@ -31,19 +30,31 @@ async function anmelden(seite: Page) {
* geprueft werden, nicht der Weg dorthin — den prueft `tests/e2e/laeufe.spec.ts`
* vom Anlegen bis zum Archivieren. Die Zeitstempel gehoeren trotzdem dazu, drei
* CHECK-Bedingungen in der Datenbank verlangen sie.
*
* Vorlage **am Namen**, Kunde ueber `istEigeneOrganisation`: ein
* `findFirstOrThrow` ohne Bedingung nahm die Version, die die Sortierung gerade
* nach vorn stellte, und im Modus DIENSTLEISTUNG einen beliebigen Demo-Kunden.
* Dass es `SEED_VORLAGE` veroeffentlicht gibt, prueft der Global-Setup.
*/
async function archivierterLauf(): Promise<{ id: string; titel: string }> {
async function archivierterLauf(): Promise<Testlauf> {
laufende += 1;
const marke = `${STEMPEL}-${laufende}`;
const suchwort = `Restposten ${marke}`;
const version = await prisma.vorlagenVersion.findFirstOrThrow({
where: { status: "VEROEFFENTLICHT" },
orderBy: { veroeffentlichtAm: "desc" },
where: { status: "VEROEFFENTLICHT", vorlage: { name: SEED_VORLAGE } },
orderBy: { version: "desc" },
select: { id: true, abschnitte: { select: { schritte: { select: { id: true } } } } },
});
const kunde = await prisma.kunde.findFirstOrThrow({ select: { id: true } });
const kunde = await prisma.kunde.findFirstOrThrow({
where: { istEigeneOrganisation: true },
select: { id: true },
});
const schrittId = version.abschnitte.flatMap((abschnitt) => abschnitt.schritte)[0]?.id;
if (!schrittId) throw new Error("Die Vorlage hat keinen Schritt.");
if (!schrittId) throw new Error(`Die Vorlage „${SEED_VORLAGE}“ hat keinen Schritt.`);
const jetzt = new Date();
const titel = `ZZ E2E Archiv ${STEMPEL}`;
const titel = `ZZ E2E Archiv ${marke}`;
const lauf = await prisma.lauf.create({
data: {
vorlagenVersionId: version.id,
@@ -60,10 +71,10 @@ async function archivierterLauf(): Promise<{ id: string; titel: string }> {
laufId: lauf.id,
vorlagenSchrittId: schrittId,
zustand: "NICHT_ZUTREFFEND",
notiz: NOTIZ,
notiz: `${suchwort} aus dem Lager`,
},
});
return { id: lauf.id, titel };
return { id: lauf.id, titel, suchwort };
}
test.describe("Archiv", () => {
@@ -77,7 +88,7 @@ test.describe("Archiv", () => {
}
await page.getByRole("link", { name: "Archiv" }).first().click();
await page.waitForURL("**/archiv");
await page.getByLabel("Suchbegriff").fill(`Restposten ${STEMPEL}`);
await page.getByLabel("Suchbegriff").fill(lauf.suchwort);
await page.getByRole("button", { name: "Suchen" }).click();
const treffer = page.getByRole("listitem").filter({ hasText: lauf.titel });
@@ -102,8 +113,16 @@ test.describe("Archiv", () => {
test("trägt am Handy ohne horizontales Scrollen", async ({ page, isMobile }) => {
test.skip(!isMobile, "Nur im Projekt „handy“.");
/*
* Der Ablauf bringt seinen Treffer mit, statt auf einen zu hoffen: gegen
* eine frische Datenbank fand „Restposten“ nichts, und gemessen wurde dann
* die Breite einer Seite ohne Treffer (#018).
*/
const lauf = await archivierterLauf();
await anmelden(page);
await page.goto("/archiv?suche=Restposten");
await page.goto(`/archiv?suche=${encodeURIComponent(lauf.suchwort)}`);
await expect(page.getByRole("listitem").filter({ hasText: lauf.titel })).toHaveCount(1);
const masse = await page.evaluate(() => ({
scroll: document.documentElement.scrollWidth,
sichtbar: document.documentElement.clientWidth,
@@ -114,6 +133,7 @@ test.describe("Archiv", () => {
test.afterAll(async () => {
const laeufe = await prisma.lauf.findMany({
// Der Praefix gehoert dieser Datei allein – geraeumt wird nur, was sie anlegt.
where: { titel: { startsWith: "ZZ E2E Archiv" } },
select: { id: true },
});