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
+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({