Files
alpenwerk-hr/tests/components/PrintChart.test.tsx
Maximilian Stubhan f14f1cb8df Keep the org chart inside the page it is printed on
Six divisions, each with its departments beside it, ran off the edge of
the sheet. The cause was structural, not cosmetic: every level spread
horizontally, so width multiplied with depth. Six divisions times three
departments is eighteen boxes across a landscape A4 — about two
millimetres each, if they had fitted at all, which they did not. They
overlapped and were clipped at the margin.

Now only one level spreads sideways. The divisions stand in a row and
everything below them hangs lengthwise off a vertical line, so width is
the number of divisions and nothing else. Depth costs height instead,
and on a landscape page height is what there is to spare.

What still overhangs is scaled down as a whole. The sheet in the preview
now carries the print area's exact dimensions rather than growing with
its contents, so the fit is measured against the real page: what you see
is what the printer gets. If a sheet has to shrink below 55% to fit, it
says so and points at A3, instead of quietly producing something nobody
can read.

With names switched on, each department gets its own sheet — a whole
division with every name was never going to be legible on one page — and
long name lists set in two columns so the box grows sideways rather than
pushing the scale down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 12:40:31 +02:00

175 lines
6.7 KiB
TypeScript

import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { PrintChart } from "@/components/orgchart/PrintChart";
import type { OrgEmployee, OrgUnitNode, OrgVacancy } from "@/components/orgchart/types";
import { buildPrintModel } from "@/lib/orgchart-print";
// Der Druck ist die einzige Ansicht, deren Ergebnis niemand mehr korrigieren
// kann: was auf dem Papier fehlt, fällt erst im Besprechungsraum auf. Deshalb
// wird hier die Auswahl geprüft — die Tiefe und das Ab-/Anwählen von
// Bereichen entscheiden, was überhaupt gezeichnet wird.
function unit(id: string, name: string, type: OrgUnitNode["unit_type"], parent: string | null, nr: string): OrgUnitNode {
return { id, org_number: nr, name, parent_id: parent, unit_type: type };
}
const UNITS: OrgUnitNode[] = [
unit("g", "Alpenwerk GmbH", "Gesellschaft", null, "1000"),
unit("b1", "Produktion", "Bereich", "g", "1100"),
unit("a1", "Fertigung", "Abteilung", "b1", "1110"),
unit("t1", "Schicht A", "Team", "a1", "1111"),
unit("b2", "Vertrieb", "Bereich", "g", "1200"),
unit("a2", "Innendienst", "Abteilung", "b2", "1210"),
];
function person(id: string, last: string, unitId: string, chief = false): OrgEmployee {
return {
id,
personnel_number: Number(id),
first_name: "Test",
last_name: last,
job_title: chief ? "Leitung" : "Sachbearbeitung",
manager_id: null,
formal_manager_id: null,
absent: false,
absence_type: null,
org_unit_id: unitId,
is_chief: chief,
position_id: `p${id}`,
position_number: `S${id}`,
};
}
const EMPLOYEES: OrgEmployee[] = [
person("1", "Wiesinger", "g", true),
person("2", "Steinbacher", "b1", true),
person("3", "Aigner", "t1"),
person("4", "Berger", "b2", true),
person("5", "Cerny", "a2"),
];
const VACANCIES: OrgVacancy[] = [
{ position_id: "p9", position_number: "S9", job_title: "Disposition", org_unit_id: "a1", is_chief: false },
];
function renderChart() {
const model = buildPrintModel(UNITS, EMPLOYEES, VACANCIES);
return render(<PrintChart model={model} asOf="2026-08-11" today="2026-08-11" />);
}
/** Die Blätter der Vorschau — je Blatt eine Druckseite. */
function blaetter(): HTMLElement[] {
return Array.from(document.querySelectorAll<HTMLElement>(".blatt"));
}
async function zurVorschau(user: ReturnType<typeof userEvent.setup>) {
await user.click(screen.getByRole("button", { name: /Vorschau/ }));
}
describe("PrintChart", () => {
it("fragt zuerst, was gedruckt wird, und zeigt vorher kein Blatt", () => {
renderChart();
expect(screen.getByText("Was soll aufs Papier?")).toBeInTheDocument();
expect(blaetter()).toHaveLength(0);
});
it("wählt alle Bereiche vor und druckt je Bereich eine Seite plus Übersicht", async () => {
const user = userEvent.setup();
renderChart();
expect(screen.getByRole("checkbox", { name: /Produktion/ })).toBeChecked();
expect(screen.getByRole("checkbox", { name: /Vertrieb/ })).toBeChecked();
await zurVorschau(user);
expect(blaetter()).toHaveLength(3);
});
it("lässt einen abgewählten Bereich ganz weg — auch aus der Übersicht", async () => {
const user = userEvent.setup();
renderChart();
await user.click(screen.getByRole("checkbox", { name: /Vertrieb/ }));
await zurVorschau(user);
const seiten = blaetter();
expect(seiten).toHaveLength(2);
expect(within(seiten[0]).queryByText("Vertrieb")).not.toBeInTheDocument();
expect(within(seiten[0]).getByText("Produktion")).toBeInTheDocument();
});
it("„Keine“ lässt nur die Übersicht übrig", async () => {
const user = userEvent.setup();
renderChart();
await user.click(screen.getByRole("button", { name: "Keine" }));
await zurVorschau(user);
expect(blaetter()).toHaveLength(1);
});
it("bricht bei „Bereiche“ nach der ersten Ebene ab", async () => {
const user = userEvent.setup();
renderChart();
await user.click(screen.getByRole("button", { name: /Bereiche/ }));
await zurVorschau(user);
const seiten = blaetter();
// Nur die Übersicht, und darin keine Abteilung.
expect(seiten).toHaveLength(1);
expect(within(seiten[0]).getByText("Produktion")).toBeInTheDocument();
expect(within(seiten[0]).queryByText("Fertigung")).not.toBeInTheDocument();
});
it("zeichnet hierarchisch: die Abteilung hängt unter ihrem Bereich", async () => {
const user = userEvent.setup();
renderChart();
await zurVorschau(user);
// Die Abteilungen hängen in einer Reihe unter dem Bereich …
const seite = blaetter()[1];
const reihe = seite.querySelector<HTMLElement>("ul.reihe")!;
const fertigung = within(reihe).getByText("Fertigung");
// … und das Team längs unter seiner Abteilung.
const abteilungsKnoten = fertigung.closest("li")!;
const stapel = abteilungsKnoten.querySelector<HTMLElement>("ul.stapel")!;
expect(within(stapel).getByText("Schicht A")).toBeInTheDocument();
});
it("breitet nur eine Ebene waagrecht aus — der Rest hängt längs", async () => {
const user = userEvent.setup();
renderChart();
await zurVorschau(user);
// Der Grund: zwei waagrechte Ebenen multiplizieren sich in der Breite und
// laufen über den Blattrand. Je Blatt darf es genau eine Reihe geben.
for (const seite of blaetter()) {
expect(seite.querySelectorAll("ul.reihe")).toHaveLength(1);
}
expect(blaetter()[1].querySelectorAll("ul.stapel").length).toBeGreaterThan(0);
});
it("nennt Namen erst bei „mit Personen“", async () => {
const user = userEvent.setup();
renderChart();
await zurVorschau(user);
// Führungskräfte stehen immer im Kasten, gewöhnliche Mitarbeitende nicht.
expect(screen.getAllByText("Test Steinbacher").length).toBeGreaterThan(0);
expect(screen.queryByText("Test Aigner")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /mit Personen/ }));
expect(screen.getByText("Test Aigner")).toBeInTheDocument();
expect(screen.getByText(/offen · Disposition/)).toBeInTheDocument();
// Mit Namen bekommt jede Abteilung ihr eigenes Blatt — ein ganzer Bereich
// mit allen Namen wäre auf einem Blatt nicht mehr lesbar.
expect(blaetter()).toHaveLength(3);
expect(screen.getByText("Produktion · Fertigung")).toBeInTheDocument();
});
it("meldet unbesetzte Leitungen statt sie zu verschweigen", async () => {
const user = userEvent.setup();
renderChart();
await zurVorschau(user);
// Auf der Produktionsseite: Fertigung und Schicht A stehen ohne Leitung da.
const seite = blaetter()[1];
expect(within(seite).getAllByText("Leitung unbesetzt")).toHaveLength(2);
expect(within(seite).getByText("Test Steinbacher")).toBeInTheDocument();
});
});