Files
Laufzettel/tests/e2e/stammdaten.spec.ts
T
Florian WildeandClaude Opus 5 86d55917f9
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(stammdaten): Mitarbeiter, Abteilungen und Geraete
Die Stammdaten, an denen die Historie haengt. Personen werden deaktiviert,
Geraete und Abteilungen archiviert — geloescht wird nichts, woran ein Lauf
haengen kann.

- Mitarbeiterliste mit Suche ueber Anzeigename, Mail, UPN und sAMAccountName,
  Filter nach Abteilung, standardmaessig nur aktive; die Suche ist ein eigenes
  Bauteil, weil #013 und #014 sie wieder brauchen
- Personenseite mit Stammdaten, Geraeten, Vorgesetztem; was aus dem Verzeichnis
  kommt, ist gesperrt — der naechste Abgleich wuerde es ohnehin zurueckdrehen
- Abteilungen unter Admin: Ziel-OU, Standardgruppen, Mail-Domain, dazu die
  Namen, die bei Personen vorkommen, aber nicht gepflegt sind
- Geraete mit Suche ueber Hostname, Bezeichnung, Seriennummer und Tag-Code;
  CSV-Import mit einem Ergebnis je Zeile
- Neues Recht mitarbeiterBearbeiten; die Seitenleiste fuehrt Personen und
  Geraete unter einem Eintrag, im Modus Dienstleistung anders beschriftet

Der unabhaengige Review fand zwei Blocker. Der schwerere meldete Erfolg: ein
nicht geschlossenes Anfuehrungszeichen verschluckte im CSV-Import alle
Folgezeilen bis zum naechsten `"`, und der Import sagte „3 von 3 Zeilen
angelegt". Ein Wert reicht jetzt nicht mehr ueber die Zeile hinaus — fuer
Hostnamen und Seriennummern ist ein Umbruch praktisch immer ein vergessenes
Anfuehrungszeichen, und so endet der Schaden an seiner Zeile. Der zweite:
ungepflegte Abteilungen wurden ueber alle Kunden hinweg gruppiert.

Dazu sieben weitere Befunde, alle behoben — darunter eine Besitzerauswahl, die
Personen fremder Kunden anbot, und ein Techniker, der ein Geraet endgueltig
loeschen konnte.

610 Tests gruen, 18 E2E-Ablaeufe gegen den gebauten Stand.

Schliesst #011

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 12:51:54 +02:00

134 lines
6.1 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, 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();
});