Stop the print preview measuring itself into a freeze

Opening the org chart PDF preview locked up the browser tab. The
measurement that fits each sheet to the page fed itself: the effect
listed onFaktor in its dependencies, and onFaktor was an arrow function
created fresh on every render, so the effect re-ran after every render.
It measured, reported the scale, and the report called setState with a
newly built object every time — new object, so React saw a change,
re-rendered, and the effect ran again. Measure, render, measure, until
React gave up with "Maximum update depth exceeded".

Two changes, and the mutation test says either one closes the loop on
its own: the callback now lives in a ref so the effect depends only on
the sheet identity, and the reducer returns the previous state unchanged
when the scale has not moved. Both are worth keeping — the ref stops the
effect from re-running, the guard stops pointless renders.

This shipped broken, and the reason it shipped is in the test file now.
Every element in jsdom is zero pixels, so the measurement bailed out on
its first line and the feedback never started; nine tests covering the
selection, the page count and the hierarchy all passed against a
component that froze on contact with a real browser. The new test gives
the elements a size, and fails with the exact error a user hits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 17:44:20 +02:00
parent 578ce696f0
commit 00df973824
2 changed files with 60 additions and 8 deletions

View File

@@ -1,6 +1,6 @@
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { afterAll, beforeAll, 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";
@@ -67,6 +67,42 @@ async function zurVorschau(user: ReturnType<typeof userEvent.setup>) {
await user.click(screen.getByRole("button", { name: /Vorschau/ }));
}
// Die Vorschau misst sich selbst, um den Massstab zu bestimmen. Das ist eine
// Rückkopplung: messen → Zustand ändern → neu rendern → messen. Bricht sie
// nicht ab, friert der Browser-Tab ein, sobald jemand die Vorschau öffnet.
//
// In jsdom hat jedes Element die Grösse 0, deshalb steigt die Messung dort
// sonst sofort aus und die Schleife entsteht gar nicht erst — genau deshalb
// ist der Fehler durch alle bisherigen Tests gekommen. Hier bekommen die
// Elemente Masse, damit die Rückkopplung wirklich läuft.
describe("PrintChart — Messung", () => {
const masse = { clientWidth: 900, clientHeight: 600 };
const gesichert: Record<string, PropertyDescriptor | undefined> = {};
beforeAll(() => {
for (const [name, wert] of Object.entries(masse)) {
gesichert[name] = Object.getOwnPropertyDescriptor(HTMLElement.prototype, name);
Object.defineProperty(HTMLElement.prototype, name, { configurable: true, value: wert });
}
});
afterAll(() => {
for (const name of Object.keys(masse)) {
if (gesichert[name]) Object.defineProperty(HTMLElement.prototype, name, gesichert[name]!);
else delete (HTMLElement.prototype as unknown as Record<string, unknown>)[name];
}
});
it("kommt zur Ruhe, statt sich selbst immer weiter neu zu vermessen", async () => {
const user = userEvent.setup();
renderChart();
// Ein Durchlauf, der nicht abbricht, endet in React mit „Maximum update
// depth exceeded" — der Klick wirft dann.
await user.click(screen.getByRole("button", { name: /Vorschau/ }));
expect(blaetter()).toHaveLength(3);
});
});
describe("PrintChart", () => {
it("fragt zuerst, was gedruckt wird, und zeigt vorher kein Blatt", () => {
renderChart();