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
+57 -17
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, kundeWaehlen } from "./helfer";
/**
* Personen und Abteilungen durch die Oberflaeche (#011).
@@ -9,7 +10,10 @@ import { prisma } from "@/lib/db";
* wirklich in der Datenbank steht: durch Neuladen. Ein Screenshot sagt darueber
* nichts (siehe CLAUDE.md, „Oberflaeche pruefen“).
*
* Voraussetzung: ein **gebauter** Stand laeuft (`pnpm build && pnpm start`).
* Voraussetzungen prueft `tests/e2e/global-setup.ts` vor dem ersten Ablauf: ein
* **gebauter** Stand antwortet, der Seed-Bestand steht, und der Server liest
* dieselbe Datenbank wie dieser Lauf. Alles, was die Ablaeufe hier darueber
* hinaus brauchen, legen sie selbst an.
*
* Aufgeraeumt wird am Ende ueber die Datenbank und nicht ueber die Oberflaeche:
* die Anwendung kennt bewusst keinen Loeschweg fuer Personen und Abteilungen
@@ -21,20 +25,14 @@ import { prisma } from "@/lib/db";
const STEMPEL = Date.now();
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"]')]);
}
/** Nachnamen dieses Durchgangs. Beide Praefixe gehoeren dieser Datei allein. */
const PRAEFIX_HAND = "ZZPruef";
const PRAEFIX_QUELLE = "ZZQuelle";
test.describe("Personen", () => {
test("legt an, findet über die Suche und deaktiviert", async ({ page }) => {
test.slow();
const nachname = `ZZPruef${STEMPEL}`;
const nachname = `${PRAEFIX_HAND}${STEMPEL}`;
await anmelden(page);
await page.goto("/mitarbeiter");
@@ -43,6 +41,7 @@ test.describe("Personen", () => {
await page.getByLabel("Vorname").fill("Testa");
await page.getByLabel("Nachname").fill(nachname);
await page.getByLabel("Mail-Adresse (optional)").fill(`testa.${STEMPEL}@firma.de`);
await kundeWaehlen(page);
await page.getByRole("button", { name: "Person anlegen" }).click();
await expect(page.getByText(`„Testa ${nachname}“ wurde angelegt.`)).toBeVisible();
@@ -65,12 +64,51 @@ test.describe("Personen", () => {
});
test("sperrt die Stammdaten einer Person aus dem Verzeichnis", async ({ page }) => {
// Vorausgesetzt wird die Beispielperson aus `pnpm beispiel:stammdaten`.
/*
* Die Person legt dieser Ablauf selbst an.
*
* Vorher hing der Test an „Katrin Winter“ aus `pnpm beispiel:stammdaten` und
* schaltete sich mit `test.skip` ab, wenn sie fehlte — gegen eine frisch
* eingerichtete Installation also immer. Ein Test, der sich stillschweigend
* selbst abschaltet, ist keiner (#018).
*
* Gesperrt wird an der **Herkunft**, nicht an der Quelle: `ABGLEICH` allein
* entscheidet. Die Beispielquelle aus dem Seed kommt trotzdem dran, damit die
* Zeile aussieht wie eine echte — sie gibt es nur im Modus INTERN, deshalb
* bleibt `quelleId` ohne sie leer, statt den Ablauf rot zu faerben.
*/
test.slow();
const nachname = `${PRAEFIX_QUELLE}${STEMPEL}`;
const quelle = await prisma.verzeichnisquelle.findFirst({
where: { name: "Firmen-AD (Beispiel)" },
select: { id: true },
});
const kunde = await prisma.kunde.findFirstOrThrow({
where: { istEigeneOrganisation: true },
select: { id: true },
});
await prisma.mitarbeiter.create({
data: {
kundeId: kunde.id,
quelleId: quelle?.id ?? null,
externeId: quelle ? `e2e-${STEMPEL}` : null,
vorname: "Katrin",
nachname,
anzeigename: `Katrin ${nachname}`,
mail: `k.${nachname.toLowerCase()}@firma.example`,
upn: `k.${nachname.toLowerCase()}@firma.example`,
sam: `k${nachname.toLowerCase()}`,
abteilung: "Buchhaltung",
titel: "Sachbearbeiterin",
herkunft: "ABGLEICH",
aktiv: true,
},
});
await anmelden(page);
await page.goto("/mitarbeiter?suche=kwinter");
const treffer = page.getByRole("link", { name: /Katrin Winter/ });
test.skip((await treffer.count()) === 0, "Beispiel-Stammdaten fehlen (pnpm beispiel:stammdaten).");
await page.goto(`/mitarbeiter?suche=${nachname}`);
const treffer = page.getByRole("link", { name: new RegExp(nachname) });
await expect(treffer).toHaveCount(1);
await treffer.click();
await page.waitForURL(/\/mitarbeiter\/[^/]+$/);
@@ -94,6 +132,7 @@ test.describe("Abteilungen", () => {
await formular.getByLabel("Name").fill(name);
await formular.getByLabel("Ziel-OU (optional)").fill("OU=Test,DC=firma,DC=de");
await formular.getByLabel("Standardgruppen, mit Komma getrennt").fill("G-Test, G-Alle");
await kundeWaehlen(page);
await formular.getByRole("button", { name: "Abteilung anlegen" }).click();
await expect(page.getByText(`„${name}“ wurde angelegt.`)).toBeVisible();
@@ -105,6 +144,7 @@ test.describe("Abteilungen", () => {
// Denselben Namen ein zweites Mal – mit lesbarer Meldung, nicht mit einem
// Datenbankfehler.
await formular.getByLabel("Name").fill(name);
await kundeWaehlen(page);
await formular.getByRole("button", { name: "Abteilung anlegen" }).click();
await expect(page.getByText(/gibt es schon/)).toBeVisible();
@@ -116,7 +156,7 @@ test.describe("Abteilungen", () => {
test.afterAll(async () => {
const personen = await prisma.mitarbeiter.findMany({
where: { nachname: { startsWith: "ZZPruef" } },
where: { OR: [{ nachname: { startsWith: PRAEFIX_HAND } }, { nachname: { startsWith: PRAEFIX_QUELLE } }] },
select: { id: true },
});
const abteilungen = await prisma.abteilung.findMany({