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(); });