feat(stammdaten): Mitarbeiter, Abteilungen und Geraete (#25)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #25.
This commit is contained in:
2026-09-25 10:52:44 +00:00
parent e3d80e5982
commit f85bc87324
81 changed files with 6784 additions and 59 deletions
+133
View File
@@ -0,0 +1,133 @@
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();
});