import { expect, test, type Page } from "@playwright/test"; import "dotenv/config"; import { prisma } from "@/lib/db"; /** * 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“). * * Voraussetzung: ein **gebauter** Stand laeuft (`pnpm build && pnpm start`). * * 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(); 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"]')]); } test.describe("Personen", () => { test("legt an, findet über die Suche und deaktiviert", async ({ page }) => { test.slow(); const nachname = `ZZPruef${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 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 }) => { // Vorausgesetzt wird die Beispielperson aus `pnpm beispiel:stammdaten`. test.slow(); 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 treffer.click(); await page.waitForURL(/\/mitarbeiter\/[^/]+$/); await expect(page.getByText(/kommt aus dem Verzeichnis/)).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 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 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: { nachname: { startsWith: "ZZPruef" } }, 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(); });