import { render as rtlRender, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { buildUnitTree, PositionTree } from "@/components/orgchart/PositionTree"; import type { OrgEmployee, OrgUnitNode, OrgVacancy } from "@/components/orgchart/types"; import { ToastProvider } from "@/components/ui/Toast"; vi.mock("@/actions/org", () => ({ createOrgUnit: vi.fn(async () => ({ success: true })), deleteOrgUnit: vi.fn(async () => ({ success: true })), })); // Die Angaben zu einer Einheit laden ihre Planstellen nach; das Aktionsmodul // zieht über lib/db das Paket `server-only` nach und ist im Test ein // Importfehler. Geprüft wird hier, was die Ansicht anbietet, nicht was die // Datenbank liefert. vi.mock("@/actions/positions", () => ({ ladePlanstellenDerEinheit: vi.fn(async () => ({ planstellen: [], kostenstellen: [], heute: "2026-09-29" })), updatePosition: vi.fn(async () => ({ success: true })), setPositionCostCenter: vi.fn(async () => ({ success: true })), })); vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh: vi.fn(), push: vi.fn() }) })); function render(ui: React.ReactElement) { return rtlRender({ui}); } // Der Knopf „Einheit darunter anlegen" hängt allein daran, dass ein Knoten // eine `unitId` trägt. Verschwindet die still — etwa weil jemand den Baum // umbaut —, bleibt die Oberfläche fehlerfrei und die Handlung ist weg. const UNITS: OrgUnitNode[] = [ { id: "u1", org_number: "OE-0001", name: "Gesellschaft", parent_id: null, unit_type: "Gesellschaft" }, { id: "u2", org_number: "OE-0010", name: "Produktion", parent_id: "u1", unit_type: "Bereich" }, ]; const EMPLOYEES: OrgEmployee[] = [ { id: "p1", personnel_number: 1, first_name: "Anna", last_name: "Huber", job_title: "Maschinenbedienung", manager_id: null, formal_manager_id: null, absent: false, absence_type: null, org_unit_id: "u2", is_chief: false, position_id: "s1", position_number: "S-0001", }, ]; const VACANCIES: OrgVacancy[] = [ { position_id: "s2", position_number: "S-0002", job_title: "Schlosser:in", org_unit_id: "u2", is_chief: false }, ]; describe("buildUnitTree", () => { const baum = buildUnitTree(UNITS, EMPLOYEES, VACANCIES); it("trägt die Einheit an jedem Einheitenknoten", () => { expect(baum[0].unitId).toBe("u1"); expect(baum[0].children[0].unitId).toBe("u2"); }); it("trägt sie an nichts anderem", () => { // Eine Person, eine Tätigkeitsgruppe oder eine Vakanz ist keine Einheit — // ein Plus daran würde etwas anlegen, das woanders hinge. const bereich = baum[0].children[0]; for (const kind of bereich.children.filter((k) => k.kind !== "role")) { expect(kind.unitId, `„${kind.label}" darf keine Einheit tragen`).toBeUndefined(); for (const enkel of kind.children) expect(enkel.unitId).toBeUndefined(); } }); }); describe("PositionTree", () => { it("bietet je Einheit einen Knopf zum Anlegen an", async () => { render(); // Die Wurzel ist aufgeklappt; ihr Kind ist damit sichtbar. expect(await screen.findByRole("button", { name: /Einheit unter .*Gesellschaft anlegen/ })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /Einheit unter .*Produktion anlegen/ })).toBeInTheDocument(); }); it("bietet ihn nicht an einer Person an", () => { render(); expect(screen.queryByRole("button", { name: /Einheit unter Huber/ })).not.toBeInTheDocument(); }); it("bietet den Papierkorb nur an, wo nichts darunter hängt", () => { // „Produktion" trägt eine Person und eine Vakanz, die Gesellschaft trägt // „Produktion" — entfernbar ist keine von beiden. Ein Knopf, der beim // Klick eine Absage erteilt, ist schlechter als kein Knopf. render(); expect(screen.queryByRole("button", { name: /Einheit .* entfernen/ })).not.toBeInTheDocument(); }); it("bietet ihn an einer leeren Einheit an", () => { const leer: OrgUnitNode = { id: "u3", org_number: "OE-0411", name: "Neu", parent_id: "u1", unit_type: "Bereich" }; render(); expect(screen.getByRole("button", { name: /Einheit .*OE-0411.* entfernen/ })).toBeInTheDocument(); }); it("öffnet beim Klick auf eine Einheit deren Angaben", async () => { // Der Klick auf die Karte war lange ohne Wirkung — die naheliegendste // Geste in dieser Ansicht, und sie antwortete nicht. render(); await userEvent.click(screen.getByRole("button", { name: /Angaben zu .*Produktion/ })); const panel = await screen.findByRole("dialog"); expect(within(panel).getByText("OE-0010 · Bereich")).toBeInTheDocument(); // Eine Person, keine Leitung, eine offene Planstelle — genau der Bestand // aus den Fixtures. expect(within(panel).getByText("vakant")).toBeInTheDocument(); }); });