Files
Laufzettel/tests/e2e/stammdaten.spec.ts
T
Florian WildeandClaude Opus 5 80d97467da
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
fix: Reviewbefunde aus der Durchsicht von Phase 2 (#036, #027)
Acht Meilensteine lagen ohne fremden Blick auf main. Ein Review mit eigener
Datenbank, Gegenproben und vollem Suitenlauf hat sie gegen die
Akzeptanzkriterien und gegen CLAUDE.md geprueft.

Die Kernregeln halten: fuenf von fuenf Gegenproben gegen Versionierung,
Mandantengrenze, Abschlussregel und Selbstauskunft wurden rot. Alle Befunde
lagen am Rand — und die drei wertvollsten waren Gegenproben, die gruen blieben.

Schwer:

- Der Link in der Mail "Verzeichnis-Abgleich fehlgeschlagen" war ein 404
  (/verwaltung/verzeichnis statt /admin/verzeichnis). Die eine Meldung, die an
  alle Administratoren geht, im Moment eines Ausfalls. Ein Test prueft jetzt
  jeden Meldungspfad gegen die vorhandenen Routen.
- Der Export eines Entwurfs mit Baustein lieferte einen leeren Abschnitt unter
  dem Platzhalter "wird ersetzt" — der Zweck des Formats ist, eine Vorlage in
  eine zweite Installation zu bringen. Bausteine werden jetzt aufgeloest.

Mittel:

- Der Anhang-Handler hatte keinen Test; die ganze Rechtepruefung liess sich
  durch einen hartcodierten ADMIN ersetzen, Suite gruen. Es ist die einzige
  Stelle, die rohe Dateibytes ausliefert. tests/anhang-route.test.ts faehrt ihn
  jetzt echt, acht Pruefungen.
- /api/scim in der Proxy-Allowlist stand in keinem Test. OEFFENTLICH ist
  exportiert, der Test vergleicht beide Richtungen: eine Liste, die nur in einer
  Richtung geprueft wird, laeuft dem Code hinterher.
- Ein offener Ereignisstrom ueberlebte Abmelden, Deaktivieren und
  Sitzungswiderruf. Der Herzschlag prueft die Sitzung jetzt nach. Dass es kein
  Test zeigte, lag an einer Attrappe `sitzung: {}` — eine Attrappe ohne die Form
  der Wirklichkeit prueft etwas anderes als die Wirklichkeit.
- README kannte MITARBEITER nicht und behauptete, unbekannte Benutzer wuerden
  angelegt. Wer auf v0.2.0 hebt und nur das README liest, haette genau diese
  Leute ausgesperrt.
- sicherung.md nannte einen Anhaenge-Pfad, den es nirgends gibt — als
  Rueckfallwert in einem Skript mit rm -rf. Jetzt bricht es ab statt zu raten.
- Attributnamen wurden nur in der Server Action geprueft, waehrend der
  Graph-Leser sich im Kommentar darauf berief.

Dazu #027: die Ende-zu-Ende-Ablaeufe raeumen jetzt zentral auf, vor und nach
dem Lauf, und ein struktureller Test bewacht es. Beim ersten echten Lauf seit
dem 26. September waren sieben Ablaeufe rot, vier Ursachen, keine ein
Produktfehler — unbemerkt, weil pnpm test:e2e in keinem Tor steht. Das ist jetzt
#038 und keine Fussnote.

Ein Befund des Reviews war falsch (der Zustands-Handler habe keinen Test; er hat
acht direkte Aufrufe). Nachgeprueft, bevor etwas geaendert wurde.

1454 Vitest-Tests (vorher 1432), 113 E2E-Ablaeufe gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-11 01:32:09 +02:00

184 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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).
*
* Geprueft wird, was nur mit angehaengtem React gelingt — und danach, ob es
* wirklich in der Datenbank steht: durch Neuladen. Ein Screenshot sagt darueber
* nichts (siehe CLAUDE.md, „Oberflaeche pruefen“).
*
* 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
* (deaktivieren und archivieren sind das Gegenteil von Aufraeumen). Ohne diesen
* Nachlauf saehe jede Datenbank, gegen die diese Tests laufen, nach ein paar
* Durchlaeufen aus wie ein Messie-Keller — in der Entwicklungsdatenbank faellt
* das sofort auf den Screenshots auf.
*/
const STEMPEL = Date.now();
/** 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 = `${PRAEFIX_HAND}${STEMPEL}`;
await anmelden(page);
await page.goto("/mitarbeiter");
// `summary` ist in Chromium kein `button`, sondern ein Aufklapp-Element.
await page.locator("summary", { hasText: "Person von Hand anlegen" }).click();
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();
// Über die Suche finden – das ist der Weg, den ein Techniker geht.
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\/[^/]+$/);
// Deaktivieren – der einzige Weg hinaus, geloescht wird nichts.
await page.getByRole("button", { name: "deaktivieren" }).click();
await expect(page.getByText("deaktiviert")).toBeVisible();
// Und damit aus der Standardliste verschwinden, aber nicht aus der Welt.
await page.goto(`/mitarbeiter?suche=${nachname}`);
await expect(page.getByRole("link", { name: new RegExp(nachname) })).toHaveCount(0);
await page.goto(`/mitarbeiter?suche=${nachname}&deaktivierte=1`);
await expect(page.getByRole("link", { name: new RegExp(nachname) })).toHaveCount(1);
});
test("sperrt die Stammdaten einer Person aus dem Verzeichnis", async ({ page }) => {
/*
* 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=${nachname}`);
const treffer = page.getByRole("link", { name: new RegExp(nachname) });
await expect(treffer).toHaveCount(1);
await treffer.click();
await page.waitForURL(/\/mitarbeiter\/[^/]+$/);
/*
* **Der ganze Satz, nicht nur der Anfang.** „kommt aus dem Verzeichnis“
* allein traf seit #020 zwei Stellen: dieselbe Auskunft steht auch am
* Vorgesetzten-Feld. Playwright wies das als „strict mode violation“ ab, und
* der Ablauf war seitdem rot — unbemerkt, weil `pnpm test:e2e` nicht im Tor
* vor dem Commit steht (Reviewbefund zu #029).
*
* Hier zählt die Auskunft an den **Stammdaten**: dass sie dort gepflegt
* werden und eine Änderung hier der nächste Abgleich zurücksetzen würde.
*/
await expect(page.getByText(/kommt aus dem Verzeichnis\. Die Stammdaten werden dort/u)).toBeVisible();
await expect(page.getByLabel("Vorname")).toHaveAttribute("readonly", "");
await expect(page.getByRole("button", { name: "Stammdaten speichern" })).toHaveCount(0);
// Deaktivieren muss trotzdem gehen: das Offboarding wartet nicht auf den
// naechsten Abgleich.
await expect(page.getByRole("button", { name: "deaktivieren" })).toBeVisible();
});
});
test.describe("Abteilungen", () => {
test("legt eine Abteilung mit Ziel-OU und Gruppen an", async ({ page }) => {
test.slow();
const name = `ZZ Abteilung ${STEMPEL}`;
await anmelden(page);
await page.goto("/admin/abteilungen");
const formular = page.locator("section", { hasText: "Abteilung anlegen" });
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();
await page.reload();
const eintrag = page.locator("li", { hasText: name }).first();
await expect(eintrag).toContainText("OU=Test,DC=firma,DC=de");
await expect(eintrag).toContainText("G-Test, G-Alle");
// 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();
// Aufräumen: archivieren, denn geloescht wird nichts.
await eintrag.getByRole("button", { name: "archivieren" }).click();
await expect(eintrag.getByRole("button", { name: "wieder freigeben" })).toBeVisible();
});
});
test.afterAll(async () => {
const personen = await prisma.mitarbeiter.findMany({
where: { OR: [{ nachname: { startsWith: PRAEFIX_HAND } }, { nachname: { startsWith: PRAEFIX_QUELLE } }] },
select: { id: true },
});
const abteilungen = await prisma.abteilung.findMany({
where: { name: { startsWith: "ZZ Abteilung" } },
select: { id: true },
});
const kennungen = [...personen, ...abteilungen].map((zeile) => zeile.id);
if (kennungen.length > 0) {
await prisma.protokoll.deleteMany({ where: { entitaetId: { in: kennungen } } });
await prisma.mitarbeiter.deleteMany({ where: { id: { in: personen.map((z) => z.id) } } });
await prisma.abteilung.deleteMany({ where: { id: { in: abteilungen.map((z) => z.id) } } });
}
await prisma.$disconnect();
});