174 lines
7.6 KiB
TypeScript
174 lines
7.6 KiB
TypeScript
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\/[^/]+$/);
|
||
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 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();
|
||
});
|