Files
DOSSIER-STANDALONE/src/panels/layoutModel.test.ts
T
karim 35299307d6 Akkumulierten grünen Arbeitsstand landen (Basis für Weiterarbeit)
Bündelt den über mehrere Sessions gewachsenen, uncommitteten Stand in
einem Basis-Commit, damit Folge-Features isoliert darauf aufsetzen.
Verifikation: tsc --noEmit sauber, vitest 600/600 grün.

Enthalten (Details in PENDENZEN.md -Liste / HANDOVER.md):
- truck-Integration: Profil-Extrusion + Verjüngung + Boolean-CSG (csgrs),
  Crate src-tauri/trucksolid, Werkzeug `extrude`, ExtrudedSolid-Modell.
- kernel2d-Port nach Rust/WASM (Phasen 1–5, Diff-Harness).
- render3d 3D-Live-Schnitt = 2D-Schnitt: geschichteter Bodenaufbau,
  Prioritäts-Verschneidung (section_boolean.rs), einstellbare
  Schichttrennlinien, per-Hatch-Strichstärke, relativeToWall-Orientierung.
- Interop-Export IFC4/STL/OBJ (Loch-Ausschnitt wallMeshCut), Schnellexport.
- Projektdatei .obp + OS-Lock (lock.rs, LockConflictDialog).
- Layout-Blätter (Modell/Editor/Panel/PDF), Ausschnitte, Override-Engine,
  Tragwerk-Stützen (Column), BIM-Tree-Panel.
- Bauteil-Typsystem (Tür/Fenster/Treppe-Typen), Betontreppe mit schräger
  Laufplatte, Text-/Textbox-Werkzeug, Mess-Werkzeug, 2D/3D-Griffe für
  Öffnungen/Treppen, Snap-Symbol-Restyle.
2026-07-09 00:57:29 +02:00

655 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Tests der reinen Layout-Logik (DOSSIER A3): Blatt-Geometrie, Fabriken,
// immutables Viewport-CRUD, Master-Vererbung der Titelblock-Werte,
// Viewport→Ausschnitt-Auflösung + Massstab, Sichtbarkeits-Ableitung.
import { describe, expect, it } from "vitest";
import type {
Layout,
LayoutFolder,
MasterLayout,
Project,
ViewSnapshot,
} from "../model/types";
import {
addAnnotation,
addViewport,
buildLayoutTree,
buildMasterTree,
collectFolderLayouts,
createAnnotation,
createLayout,
createLayoutFolder,
createMasterLayout,
createViewport,
deleteFolder,
deleteMasterFolder,
effectiveSheetSizeMm,
findMaster,
findSnapshot,
folderPdfPages,
isDescendant,
moveFolderToFolder,
moveLayoutToFolder,
PAPER_FORMATS,
patchAnnotation,
patchViewport,
removeAnnotation,
removeViewport,
resolveTitleBlock,
resolveViewportScale,
sheetSizeMm,
visibleCodesFromSnapshot,
} from "./layoutModel";
/** Minimaler Ausschnitt (nur die im Layout-System genutzten Felder tragen Werte). */
function snap(id: string, patch?: Partial<ViewSnapshot>): ViewSnapshot {
return {
id,
name: `Snap ${id}`,
viewType: "grundriss",
view3d: "top",
scaleDenominator: 100,
detail: "mittel",
activeLevelId: "floor-0",
layerVisibility: { A: true, B: false, C: true },
drawingVisibility: {},
...patch,
};
}
/** Minimales Projekt-Gerüst mit optionalen Layout-Feldern. */
function project(patch?: Partial<Project>): Project {
return {
id: "p1",
name: "Villa Test",
lineStyles: [],
hatches: [],
components: [],
wallTypes: [],
drawingLevels: [],
layers: [],
walls: [],
doors: [],
drawings2d: [],
...patch,
};
}
describe("sheetSizeMm", () => {
it("liefert A4/A3 in Hoch- und Querformat", () => {
expect(sheetSizeMm("a4", "portrait")).toEqual({ w: 210, h: 297 });
expect(sheetSizeMm("a4", "landscape")).toEqual({ w: 297, h: 210 });
expect(sheetSizeMm("a3", "portrait")).toEqual({ w: 297, h: 420 });
expect(sheetSizeMm("a3", "landscape")).toEqual({ w: 420, h: 297 });
});
});
describe("Fabriken", () => {
it("createLayout: leer, ohne Master", () => {
const l = createLayout("l1", "Blatt 1", "a3", "landscape");
expect(l).toEqual({
id: "l1",
name: "Blatt 1",
paper: "a3",
orientation: "landscape",
viewports: [],
});
expect(l.masterId).toBeUndefined();
});
it("createMasterLayout: leerer Titelblock + Rahmen an", () => {
const m = createMasterLayout("m1", "Master", "a4", "portrait");
expect(m.titleBlock).toEqual({});
expect(m.border).toBe(true);
});
it("createViewport: Defaults + optionaler Massstab", () => {
const v = createViewport("v1", "s1");
expect(v).toMatchObject({ id: "v1", snapshotId: "s1", xMm: 20, yMm: 20, widthMm: 120, heightMm: 90 });
expect(v.scaleDenominator).toBeUndefined();
const v2 = createViewport("v2", "s1", { xMm: 5, scaleDenominator: 50 });
expect(v2.xMm).toBe(5);
expect(v2.scaleDenominator).toBe(50);
});
});
describe("Viewport-CRUD (immutabel)", () => {
const base = addViewport(createLayout("l1", "L", "a4", "portrait"), createViewport("v1", "s1"));
it("addViewport verändert das Original nicht", () => {
const before = createLayout("l1", "L", "a4", "portrait");
const after = addViewport(before, createViewport("v1", "s1"));
expect(before.viewports).toHaveLength(0);
expect(after.viewports).toHaveLength(1);
expect(after).not.toBe(before);
});
it("removeViewport entfernt per Id", () => {
const two = addViewport(base, createViewport("v2", "s2"));
const one = removeViewport(two, "v1");
expect(one.viewports.map((v) => v.id)).toEqual(["v2"]);
});
it("patchViewport ändert nur das Ziel, immutabel", () => {
const patched = patchViewport(base, "v1", { xMm: 42, scaleDenominator: 50 });
expect(patched.viewports[0]).toMatchObject({ xMm: 42, scaleDenominator: 50 });
expect(base.viewports[0].xMm).toBe(20);
// Unbekannte Id → No-op (gleiche Werte, neue Liste).
const noop = patchViewport(base, "nope", { xMm: 1 });
expect(noop.viewports[0].xMm).toBe(20);
});
});
// ── Annotationen (Linie/Rechteck/Text) ───────────────────────────────────────
describe("createAnnotation — Defaults je Art", () => {
it("line: Default-Geometrie ohne Farbe/Strichstärke", () => {
const a = createAnnotation("a1", "line");
expect(a).toEqual({ id: "a1", kind: "line", x1Mm: 20, y1Mm: 20, x2Mm: 80, y2Mm: 20 });
});
it("line: init überschreibt einzelne Felder + optionale Farbe/Strichstärke", () => {
const a = createAnnotation("a1", "line", { x1Mm: 5, color: "#f00", weightMm: 0.5 });
expect(a).toMatchObject({ x1Mm: 5, y1Mm: 20, color: "#f00", weightMm: 0.5 });
});
it("rect: Default-Grösse", () => {
const a = createAnnotation("a2", "rect");
expect(a).toEqual({ id: "a2", kind: "rect", xMm: 20, yMm: 20, widthMm: 40, heightMm: 30 });
});
it("text: Default-Zeilenhöhe 3.5mm, leerer Text", () => {
const a = createAnnotation("a3", "text");
expect(a).toEqual({ id: "a3", kind: "text", xMm: 20, yMm: 20, text: "", heightMm: 3.5 });
});
it("text: init setzt Inhalt + Höhe", () => {
const a = createAnnotation("a3", "text", { text: "EG", heightMm: 5 });
expect(a).toMatchObject({ text: "EG", heightMm: 5 });
});
});
describe("Annotations-CRUD (immutabel)", () => {
const base = createLayout("l1", "L", "a4", "portrait");
it("addAnnotation verändert das Original nicht", () => {
const after = addAnnotation(base, createAnnotation("a1", "line"));
expect(base.annotations).toBeUndefined();
expect(after.annotations).toHaveLength(1);
expect(after).not.toBe(base);
});
it("addAnnotation baut auf einer fehlenden `annotations`-Liste auf (Alt-Layout)", () => {
const legacy: Layout = { id: "old", name: "Alt", paper: "a4", orientation: "portrait", viewports: [] };
const after = addAnnotation(legacy, createAnnotation("a1", "rect"));
expect(after.annotations).toHaveLength(1);
expect(legacy.annotations).toBeUndefined();
});
it("removeAnnotation entfernt per Id", () => {
let l = addAnnotation(base, createAnnotation("a1", "line"));
l = addAnnotation(l, createAnnotation("a2", "rect"));
const removed = removeAnnotation(l, "a1");
expect(removed.annotations!.map((a) => a.id)).toEqual(["a2"]);
});
it("removeAnnotation auf Alt-Layout ohne `annotations` bleibt heil (No-op)", () => {
const legacy: Layout = { id: "old", name: "Alt", paper: "a4", orientation: "portrait", viewports: [] };
const r = removeAnnotation(legacy, "nope");
expect(r.annotations).toEqual([]);
});
it("patchAnnotation ändert nur das Ziel, immutabel", () => {
const l = addAnnotation(base, createAnnotation("a1", "line", { color: "#111" }));
const patched = patchAnnotation(l, "a1", { x1Mm: 42, color: "#0f0" });
expect(patched.annotations![0]).toMatchObject({ x1Mm: 42, color: "#0f0" });
expect(l.annotations![0].color).toBe("#111");
});
it("patchAnnotation: unbekannte Id → No-op (gleiche Werte, neue Liste)", () => {
const l = addAnnotation(base, createAnnotation("a1", "line"));
const noop = patchAnnotation(l, "nope", { x1Mm: 1 });
expect((noop.annotations![0] as { x1Mm: number }).x1Mm).toBe(20);
});
it("patchAnnotation auf einer Text-Annotation ändert den Inhalt", () => {
const l = addAnnotation(base, createAnnotation("t1", "text", { text: "EG" }));
const patched = patchAnnotation(l, "t1", { text: "1. OG" });
expect(patched.annotations![0]).toMatchObject({ text: "1. OG" });
});
});
describe("resolveViewportScale", () => {
const s = snap("s1", { scaleDenominator: 200 });
it("Viewport-Override gewinnt", () => {
expect(resolveViewportScale(createViewport("v", "s1", { scaleDenominator: 50 }), s)).toBe(50);
});
it("sonst Ausschnitt-Massstab", () => {
expect(resolveViewportScale(createViewport("v", "s1"), s)).toBe(200);
});
it("Fallback ohne beides", () => {
expect(resolveViewportScale(createViewport("v", "s1"), undefined, 100)).toBe(100);
});
});
describe("findSnapshot / findMaster", () => {
const p = project({
viewSnapshots: [snap("s1"), snap("s2")],
masterLayouts: [createMasterLayout("m1", "M1", "a4", "portrait")],
});
it("findSnapshot", () => {
expect(findSnapshot(p, "s2")?.id).toBe("s2");
expect(findSnapshot(p, "x")).toBeUndefined();
});
it("findMaster nur bei gesetzter masterId", () => {
const bound: Layout = { ...createLayout("l", "L", "a4", "portrait"), masterId: "m1" };
expect(findMaster(p, bound)?.id).toBe("m1");
expect(findMaster(p, createLayout("l2", "L2", "a4", "portrait"))).toBeUndefined();
// Dangling masterId → undefined.
expect(findMaster(p, { ...bound, masterId: "gone" })).toBeUndefined();
});
});
describe("resolveTitleBlock — Master-Vererbung + Defaults", () => {
const p = project({ name: "Villa Test", viewSnapshots: [snap("s1", { scaleDenominator: 50 })] });
it("ohne Master: Defaults aus Projekt/Layout + Massstab aus erstem Viewport", () => {
let l = createLayout("l1", "EG Grundriss", "a4", "portrait");
l = addViewport(l, createViewport("v1", "s1"));
const tb = resolveTitleBlock(p, l, undefined, "2026-07-07");
expect(tb.projectName).toBe("Villa Test");
expect(tb.sheetName).toBe("EG Grundriss");
expect(tb.scale).toBe("1:50");
expect(tb.date).toBe("2026-07-07");
expect(tb.author).toBe("");
});
it("Master-Werte übersteuern die Defaults", () => {
const master: MasterLayout = {
id: "m1",
name: "M",
paper: "a4",
orientation: "portrait",
titleBlock: { projectName: "Kundenprojekt", author: "GV", scale: "1:100" },
};
const l = createLayout("l1", "EG", "a4", "portrait");
const tb = resolveTitleBlock(p, l, master, "2026-07-07");
expect(tb.projectName).toBe("Kundenprojekt");
expect(tb.author).toBe("GV");
expect(tb.scale).toBe("1:100"); // Master-Wert schlägt den Viewport-Default
expect(tb.sheetName).toBe("EG"); // leer im Master → Layout-Name
});
it("leere/whitespace Master-Felder fallen auf Defaults zurück", () => {
const master: MasterLayout = {
id: "m1",
name: "M",
paper: "a4",
orientation: "portrait",
titleBlock: { projectName: " ", sheetName: "" },
};
const l = createLayout("l1", "OG", "a4", "portrait");
const tb = resolveTitleBlock(p, l, master, "2026-07-07");
expect(tb.projectName).toBe("Villa Test");
expect(tb.sheetName).toBe("OG");
expect(tb.scale).toBe("—"); // kein Viewport → kein Massstab
});
});
describe("visibleCodesFromSnapshot", () => {
it("nur sichtbare Codes landen im Set", () => {
const codes = visibleCodesFromSnapshot(snap("s1"));
expect([...codes].sort()).toEqual(["A", "C"]);
expect(codes.has("B")).toBe(false);
});
});
// ── Freie Grösse (custom übersteuert Papier) ──────────────────────────────────
describe("sheetSizeMm / effectiveSheetSizeMm — freie Grösse", () => {
it("custom übersteuert paper/orientation, wenn beide positiv gesetzt", () => {
expect(sheetSizeMm("a4", "portrait", 500, 300)).toEqual({ w: 500, h: 300 });
});
it("fällt auf paper zurück, wenn custom fehlt oder nicht positiv ist", () => {
expect(sheetSizeMm("a4", "portrait", undefined, 300)).toEqual({ w: 210, h: 297 });
expect(sheetSizeMm("a3", "landscape", 0, 300)).toEqual({ w: 420, h: 297 });
});
it("effectiveSheetSizeMm liest die Felder eines Layouts/Masters", () => {
expect(
effectiveSheetSizeMm({ paper: "a4", orientation: "portrait" }),
).toEqual({ w: 210, h: 297 });
expect(
effectiveSheetSizeMm({
paper: "a4",
orientation: "portrait",
customWidthMm: 250,
customHeightMm: 250,
}),
).toEqual({ w: 250, h: 250 });
});
});
describe("createMasterLayout — freie Grösse", () => {
it("übernimmt custom Breite/Höhe, wenn übergeben", () => {
const m = createMasterLayout("m", "M", "a4", "portrait", {
widthMm: 400,
heightMm: 250,
});
expect(m.customWidthMm).toBe(400);
expect(m.customHeightMm).toBe(250);
});
});
// ── Ordner-Baum (CRUD + Aufbau) ───────────────────────────────────────────────
function lay(id: string, folderId?: string): Layout {
return {
id,
name: id,
paper: "a4",
orientation: "portrait",
viewports: [],
...(folderId ? { folderId } : {}),
};
}
describe("createLayoutFolder", () => {
it("Wurzelordner ohne parentId", () => {
expect(createLayoutFolder("f1", "Ordner")).toEqual({ id: "f1", name: "Ordner" });
});
it("Unterordner mit parentId", () => {
expect(createLayoutFolder("f2", "Sub", "f1")).toEqual({
id: "f2",
name: "Sub",
parentId: "f1",
});
});
it("Master-Ordner trägt kind:\"master\"", () => {
expect(createLayoutFolder("m1", "Master-Ordner", undefined, "master")).toEqual({
id: "m1",
name: "Master-Ordner",
kind: "master",
});
});
it("kind \"layout\"/undefined wird NICHT gesetzt (rückwärtskompatibel)", () => {
expect(createLayoutFolder("l1", "Layout-Ordner", undefined, "layout")).toEqual({
id: "l1",
name: "Layout-Ordner",
});
expect(createLayoutFolder("l2", "Layout-Ordner", "p", undefined)).toEqual({
id: "l2",
name: "Layout-Ordner",
parentId: "p",
});
});
});
describe("buildLayoutTree", () => {
it("gruppiert Layouts in Ordner + Wurzel; Unterordner verschachtelt", () => {
const folders: LayoutFolder[] = [
createLayoutFolder("A", "A"),
createLayoutFolder("B", "B", "A"),
];
const layouts = [lay("root1"), lay("inA", "A"), lay("inB", "B")];
const tree = buildLayoutTree(folders, layouts);
expect(tree.rootLayouts.map((l) => l.id)).toEqual(["root1"]);
expect(tree.rootFolders).toHaveLength(1);
const a = tree.rootFolders[0];
expect(a.folder.id).toBe("A");
expect(a.layouts.map((l) => l.id)).toEqual(["inA"]);
expect(a.subfolders).toHaveLength(1);
expect(a.subfolders[0].folder.id).toBe("B");
expect(a.subfolders[0].layouts.map((l) => l.id)).toEqual(["inB"]);
});
it("verwaiste Referenzen landen auf der Wurzel", () => {
const tree = buildLayoutTree(
[createLayoutFolder("orphan", "O", "missing")],
[lay("ghost", "gone")],
);
// Ordner mit unbekanntem parent → Wurzel; Layout mit unbekanntem folder → Wurzel.
expect(tree.rootFolders.map((f) => f.folder.id)).toEqual(["orphan"]);
expect(tree.rootLayouts.map((l) => l.id)).toEqual(["ghost"]);
});
});
// ── Master-Baum + Layout/Master-Trennung (kind) ──────────────────────────────
function mas(id: string, folderId?: string): MasterLayout {
return {
id,
name: id,
paper: "a4",
orientation: "portrait",
titleBlock: {},
...(folderId ? { folderId } : {}),
};
}
describe("buildMasterTree", () => {
it("gruppiert Master in Master-Ordner + Wurzel; Unterordner verschachtelt", () => {
const folders: LayoutFolder[] = [
createLayoutFolder("A", "A", undefined, "master"),
createLayoutFolder("B", "B", "A", "master"),
];
const masters = [mas("root1"), mas("inA", "A"), mas("inB", "B")];
const tree = buildMasterTree(folders, masters);
expect(tree.rootMasters.map((m) => m.id)).toEqual(["root1"]);
expect(tree.rootFolders).toHaveLength(1);
const a = tree.rootFolders[0];
expect(a.folder.id).toBe("A");
expect(a.masters.map((m) => m.id)).toEqual(["inA"]);
expect(a.subfolders[0].masters.map((m) => m.id)).toEqual(["inB"]);
});
});
describe("Layout-/Master-Ordner bleiben getrennt (kind-Filter)", () => {
it("Master-Ordner erscheint NICHT im Layout-Baum und umgekehrt", () => {
const folders: LayoutFolder[] = [
createLayoutFolder("L", "Layout-Ordner"), // kind fehlt → Layout
createLayoutFolder("M", "Master-Ordner", undefined, "master"),
];
const layouts = [lay("l1", "L"), lay("lm", "M")]; // lm zeigt fälschlich auf Master-Ordner
const masters = [mas("m1", "M"), mas("ml", "L")]; // ml zeigt fälschlich auf Layout-Ordner
// Wie das Panel filtert: Layout-Baum nur kind!=="master", Master-Baum nur kind==="master".
const layoutTree = buildLayoutTree(
folders.filter((f) => f.kind !== "master"),
layouts,
);
const masterTree = buildMasterTree(
folders.filter((f) => f.kind === "master"),
masters,
);
// Layout-Baum kennt nur den Layout-Ordner; das auf M zeigende Layout fällt auf die Wurzel.
expect(layoutTree.rootFolders.map((f) => f.folder.id)).toEqual(["L"]);
expect(layoutTree.rootLayouts.map((l) => l.id)).toEqual(["lm"]);
// Master-Baum kennt nur den Master-Ordner; der auf L zeigende Master fällt auf die Wurzel.
expect(masterTree.rootFolders.map((f) => f.folder.id)).toEqual(["M"]);
expect(masterTree.rootMasters.map((m) => m.id)).toEqual(["ml"]);
expect(masterTree.rootFolders[0].masters.map((m) => m.id)).toEqual(["m1"]);
});
});
describe("deleteMasterFolder", () => {
it("zieht Master + Unterordner auf die Elternebene und erhält kind:\"master\"", () => {
const folders: LayoutFolder[] = [
createLayoutFolder("A", "A", undefined, "master"),
createLayoutFolder("B", "B", "A", "master"),
];
const masters = [mas("a1", "A"), mas("b1", "B")];
const r = deleteMasterFolder("A", folders, masters);
expect(r.folders.map((f) => f.id)).toEqual(["B"]);
// B auf Wurzel hochgezogen, aber weiterhin Master-Ordner.
expect(r.folders[0].parentId).toBeUndefined();
expect(r.folders[0].kind).toBe("master");
expect(r.masters.find((m) => m.id === "a1")!.folderId).toBeUndefined();
expect(r.masters.find((m) => m.id === "b1")!.folderId).toBe("B");
});
});
describe("collectFolderLayouts", () => {
it("liefert direkte Blätter zuerst, dann rekursiv Unterordner (PDF-Reihenfolge)", () => {
const folders: LayoutFolder[] = [
createLayoutFolder("A", "A"),
createLayoutFolder("B", "B", "A"),
];
const layouts = [lay("root1"), lay("a1", "A"), lay("a2", "A"), lay("b1", "B")];
expect(collectFolderLayouts("A", folders, layouts).map((l) => l.id)).toEqual([
"a1",
"a2",
"b1",
]);
expect(collectFolderLayouts("B", folders, layouts).map((l) => l.id)).toEqual([
"b1",
]);
});
it("unbekannter Ordner → leer", () => {
expect(collectFolderLayouts("nope", [], [lay("x")])).toEqual([]);
});
});
describe("deleteFolder", () => {
it("zieht Layouts + Unterordner auf die Elternebene (kein Datenverlust)", () => {
const folders: LayoutFolder[] = [
createLayoutFolder("A", "A"),
createLayoutFolder("B", "B", "A"),
];
const layouts = [lay("a1", "A"), lay("b1", "B")];
// A (Wurzel) löschen → a1 auf Wurzel, B verliert parent → Wurzel.
const r = deleteFolder("A", folders, layouts);
expect(r.folders.map((f) => f.id)).toEqual(["B"]);
expect(r.folders[0].parentId).toBeUndefined();
expect(r.layouts.find((l) => l.id === "a1")!.folderId).toBeUndefined();
expect(r.layouts.find((l) => l.id === "b1")!.folderId).toBe("B");
});
it("Unterordner löschen zieht Kinder zum Grosselter", () => {
const folders: LayoutFolder[] = [
createLayoutFolder("A", "A"),
createLayoutFolder("B", "B", "A"),
];
const layouts = [lay("b1", "B")];
const r = deleteFolder("B", folders, layouts);
expect(r.folders.map((f) => f.id)).toEqual(["A"]);
expect(r.layouts[0].folderId).toBe("A");
});
});
describe("folderPdfPages", () => {
it("plant Seiten in Baum-Reihenfolge mit effektiver Blattgrösse", () => {
const master: MasterLayout = createMasterLayout("m", "M", "a3", "landscape");
const layouts: Layout[] = [
{ ...lay("p1", "F"), masterId: "m" }, // erbt A3 quer vom Master
{ ...lay("p2", "F"), customWidthMm: 500, customHeightMm: 200 }, // freie Grösse
lay("p3", "F"), // A4 hoch (Default)
];
const p = project({
layoutFolders: [createLayoutFolder("F", "F")],
layouts,
masterLayouts: [master],
});
const pages = folderPdfPages(p, "F");
expect(pages.map((x) => x.layout.id)).toEqual(["p1", "p2", "p3"]);
expect(pages[0]).toMatchObject({ widthMm: 420, heightMm: 297 }); // Master A3 quer
expect(pages[1]).toMatchObject({ widthMm: 500, heightMm: 200 }); // custom
expect(pages[2]).toMatchObject({ widthMm: 210, heightMm: 297 }); // A4 hoch
});
it("leerer/unbekannter Ordner → keine Seiten", () => {
expect(folderPdfPages(project(), "nope")).toEqual([]);
});
});
describe("PAGE_MM / Standard-Formate A0A6 + B0B6", () => {
it("PAPER_FORMATS listet 14 Formate (A-Reihe, dann B-Reihe)", () => {
expect(PAPER_FORMATS).toEqual([
"a0", "a1", "a2", "a3", "a4", "a5", "a6",
"b0", "b1", "b2", "b3", "b4", "b5", "b6",
]);
});
it("liefert offizielle ISO-216-Masse (Hochformat, mm)", () => {
expect(sheetSizeMm("a0", "portrait")).toEqual({ w: 841, h: 1189 });
expect(sheetSizeMm("a1", "portrait")).toEqual({ w: 594, h: 841 });
expect(sheetSizeMm("a2", "portrait")).toEqual({ w: 420, h: 594 });
expect(sheetSizeMm("a5", "portrait")).toEqual({ w: 148, h: 210 });
expect(sheetSizeMm("a6", "portrait")).toEqual({ w: 105, h: 148 });
expect(sheetSizeMm("b0", "portrait")).toEqual({ w: 1000, h: 1414 });
expect(sheetSizeMm("b3", "portrait")).toEqual({ w: 353, h: 500 });
expect(sheetSizeMm("b6", "portrait")).toEqual({ w: 125, h: 176 });
});
it("Querformat dreht die Basis (Beispiel A0)", () => {
expect(sheetSizeMm("a0", "landscape")).toEqual({ w: 1189, h: 841 });
});
it("jedes Format hat eine definierte Grösse", () => {
for (const f of PAPER_FORMATS) {
const s = sheetSizeMm(f, "portrait");
expect(s.w).toBeGreaterThan(0);
expect(s.h).toBeGreaterThan(0);
}
});
});
describe("isDescendant — Zyklus-Schutz", () => {
const folders: LayoutFolder[] = [
createLayoutFolder("A", "A"),
createLayoutFolder("B", "B", "A"),
createLayoutFolder("C", "C", "B"),
createLayoutFolder("X", "X"),
];
it("ein Ordner ist sein eigener Nachfahre (self)", () => {
expect(isDescendant(folders, "A", "A")).toBe(true);
});
it("erkennt direkte + tiefe Nachfahren", () => {
expect(isDescendant(folders, "B", "A")).toBe(true);
expect(isDescendant(folders, "C", "A")).toBe(true);
});
it("nicht-verwandte Ordner sind keine Nachfahren", () => {
expect(isDescendant(folders, "X", "A")).toBe(false);
expect(isDescendant(folders, "A", "B")).toBe(false);
});
});
describe("moveLayoutToFolder", () => {
const layouts = [lay("l1", "A"), lay("l2")];
it("setzt folderId auf den Zielordner", () => {
const r = moveLayoutToFolder(layouts, "l2", "A");
expect(r.find((l) => l.id === "l2")!.folderId).toBe("A");
});
it("null → zurück auf Wurzel (folderId entfernt)", () => {
const r = moveLayoutToFolder(layouts, "l1", null);
expect(r.find((l) => l.id === "l1")!.folderId).toBeUndefined();
expect("folderId" in r.find((l) => l.id === "l1")!).toBe(false);
});
it("unbekannte Id → unverändert", () => {
expect(moveLayoutToFolder(layouts, "nope", "A")).toEqual(layouts);
});
});
describe("moveFolderToFolder", () => {
const folders: LayoutFolder[] = [
createLayoutFolder("A", "A"),
createLayoutFolder("B", "B", "A"),
createLayoutFolder("X", "X"),
];
it("setzt parentId auf den Zielordner", () => {
const r = moveFolderToFolder(folders, "X", "A");
expect(r.find((f) => f.id === "X")!.parentId).toBe("A");
});
it("null → zurück auf Wurzel (parentId entfernt)", () => {
const r = moveFolderToFolder(folders, "B", null);
expect(r.find((f) => f.id === "B")!.parentId).toBeUndefined();
});
it("kind bleibt beim Wurzel-Verschieben erhalten (Master-Ordner)", () => {
const withMaster: LayoutFolder[] = [
createLayoutFolder("P", "P", undefined, "master"),
createLayoutFolder("Q", "Q", "P", "master"),
];
const r = moveFolderToFolder(withMaster, "Q", null);
const q = r.find((f) => f.id === "Q")!;
expect(q.parentId).toBeUndefined();
expect(q.kind).toBe("master");
});
it("Zyklus (in eigenen Nachfahren) → No-op, Original zurück", () => {
expect(moveFolderToFolder(folders, "A", "B")).toEqual(folders);
});
it("in sich selbst → No-op", () => {
expect(moveFolderToFolder(folders, "A", "A")).toEqual(folders);
});
});