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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e3d80e5982
commit
86d55917f9
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user