import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { useCallback, useState } from "react"; import { describe, expect, it } from "vitest"; import { GraphOrgChart } from "@/components/orgchart/GraphOrgChart"; import type { ChartNode } from "@/components/orgchart/types"; // Regression cover for the graphical org chart. The expand control lives // inside a React Flow custom node, and React Flow decides whether that node // receives pointer events at all from the props passed to : // setting elementsSelectable={false} alongside nodesDraggable={false} made it // compute pointer-events:none for the whole card, so neither the toggle nor // the employee link responded. Nothing in the type system catches that, and // no logic test would either — it only shows up when the tree is rendered // and actually clicked. function person(id: string, name: string, children: ChartNode[] = []): ChartNode { return { id, kind: "person", label: name, sublabel: "Testrolle", href: `/employees/${id}`, avatar: { firstName: name.split(" ")[0], lastName: name.split(" ")[1] ?? "X" }, totalReports: children.length, children, }; } const TREE: ChartNode[] = [ person("1", "Johanna Wiesinger", [ person("2", "Markus Steinbacher", [person("4", "Katharina Aigner"), person("5", "Kurt Aigner")]), person("3", "Simon Aigner"), ]), ]; function Harness({ initial = ["1"] }: { initial?: string[] }) { const [expanded, setExpanded] = useState>(new Set(initial)); const onToggle = useCallback((id: string) => { setExpanded((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); const isExpanded = useCallback((id: string) => expanded.has(id), [expanded]); return ; } describe("GraphOrgChart", () => { it("renders the root and its expanded children", async () => { render(); expect(await screen.findByText("Johanna Wiesinger")).toBeInTheDocument(); expect(screen.getByText("Markus Steinbacher")).toBeInTheDocument(); expect(screen.getByText("Simon Aigner")).toBeInTheDocument(); // Grandchildren stay hidden until their parent is expanded. expect(screen.queryByText("Katharina Aigner")).not.toBeInTheDocument(); }); it("expands a collapsed branch when its control is clicked", async () => { render(); await userEvent.click(await screen.findByRole("button", { name: /Markus Steinbacher aufklappen/i })); expect(await screen.findByText("Katharina Aigner")).toBeInTheDocument(); expect(screen.getByText("Kurt Aigner")).toBeInTheDocument(); }); it("collapses again", async () => { render(); expect(await screen.findByText("Katharina Aigner")).toBeInTheDocument(); await userEvent.click(await screen.findByRole("button", { name: /Johanna Wiesinger zuklappen/i })); expect(screen.queryByText("Markus Steinbacher")).not.toBeInTheDocument(); }); it("keeps the node's pointer events enabled", () => { // The direct assertion on the regression: React Flow writes // pointer-events onto the node wrapper as an inline style, and 'none' // there silently disables everything inside the card. const { container } = render(); const nodes = container.querySelectorAll(".react-flow__node"); expect(nodes.length).toBeGreaterThan(0); for (const node of nodes) { expect(node.style.pointerEvents).not.toBe("none"); } }); it("shows the child count on a collapsed node and links to the employee", async () => { render(); expect(await screen.findByRole("button", { name: /Markus Steinbacher aufklappen \(2\)/i })).toHaveTextContent("2"); expect(screen.getByRole("link", { name: /Johanna Wiesinger/i })).toHaveAttribute("href", "/employees/1"); }); it("gives a leaf no expand control", async () => { render(); await screen.findByText("Simon Aigner"); expect(screen.queryByRole("button", { name: /Simon Aigner (auf|zu)klappen/i })).not.toBeInTheDocument(); }); });