Files
alpenwerk-hr/tests/components/PositionTree.test.tsx
Andrei Laas ab50c21b8a
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m56s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m7s
Klick auf eine Einheit zeigt ihre Angaben
Die naheliegendste Geste in dieser Ansicht war ohne Wirkung: eine Karte im
Organigramm liess sich anklicken und antwortete nicht. Jetzt oeffnet sie eine
Leiste mit dem, was an der Einheit steht -- Orgnummer, Art, die Kette nach
oben, Gueltigkeit, Leitung, Belegschaft, unbesetzte Planstellen, die direkt
untergeordneten Einheiten und die Zahl der Personen im ganzen Teilbaum. Dazu
dieselben zwei Handlungen wie auf der Karte, nur beschriftet statt als Symbol.

Alles daraus stammt aus den Daten, die die Struktursicht ohnehin geladen hat.
Kein zusaetzlicher Gang zur Datenbank und damit auch keine zweite Wahrheit --
was die Leiste zeigt, ist dasselbe, woraus der Baum daneben gezeichnet ist.

Die Liste klickt sich genauso: beide Ansichten kommen aus einem Baum, und eine
Handlung, die es nur in einer von beiden gibt, findet man in der anderen nie.

org_units liefert dafuer zusaetzlich valid_from und valid_to, aber nur an die
Struktursicht. Der Druck kommt ohne sie aus; auf OrgUnitNode stehen sie deshalb
als optionale Felder und nicht als zwei weitere Spalten, die ueberall
mitgeschleppt werden.
2026-09-28 21:27:36 +02:00

108 lines
4.7 KiB
TypeScript

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 })),
}));
vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh: vi.fn(), push: vi.fn() }) }));
function render(ui: React.ReactElement) {
return rtlRender(<ToastProvider>{ui}</ToastProvider>);
}
// 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(<PositionTree employees={EMPLOYEES} units={UNITS} vacancies={VACANCIES} />);
// 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(<PositionTree employees={EMPLOYEES} units={UNITS} vacancies={VACANCIES} />);
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(<PositionTree employees={EMPLOYEES} units={UNITS} vacancies={VACANCIES} />);
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(<PositionTree employees={EMPLOYEES} units={[...UNITS, leer]} vacancies={VACANCIES} />);
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(<PositionTree employees={EMPLOYEES} units={UNITS} vacancies={VACANCIES} />);
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();
});
});