Kuma/tests/rooms/floor-plan.test.ts
2026-10-02 00:41:54 +02:00

122 lines
3.7 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
bookableSpots,
findFreePosition,
itemFootprint,
nextLabel,
parseFloorPlan,
placeInRoom,
spotDisplayName,
type FloorPlan,
type FloorPlanItem,
} from "@/src/modules/rooms/lib/floor-plan";
let idCounter = 0;
const item = (overrides: Partial<FloorPlanItem> = {}): FloorPlanItem => ({
id: `item-${++idCounter}`,
type: "reformer",
x: 1,
y: 2,
rotation: 0,
label: "1",
...overrides,
});
const plan = (items: FloorPlanItem[]): FloorPlan => ({ width: 8, depth: 6, items });
describe("parseFloorPlan", () => {
it("liefert null für Räume ohne oder mit kaputtem Grundriss", () => {
expect(parseFloorPlan(null)).toBeNull();
expect(parseFloorPlan({})).toBeNull();
expect(parseFloorPlan({ width: 8, depth: 6, items: [{ id: "x", type: "sofa" }] })).toBeNull();
});
it("lehnt doppelte Platznummern innerhalb eines Gerätetyps ab", () => {
const duplicate = plan([item({ label: "1" }), item({ label: "1" })]);
expect(parseFloorPlan(duplicate)).toBeNull();
});
it("erlaubt dieselbe Nummer bei verschiedenen Gerätetypen und Deko ohne Nummer", () => {
const mixed = plan([
item({ type: "reformer", label: "1" }),
item({ type: "mat", label: "1" }),
item({ type: "mirror", label: "" }),
]);
expect(parseFloorPlan(mixed)?.items).toHaveLength(3);
});
it("verlangt bei buchbaren Geräten eine Nummer", () => {
expect(parseFloorPlan(plan([item({ label: "" })]))).toBeNull();
});
});
describe("bookableSpots", () => {
it("sortiert nach Gerätetyp und Nummer und lässt Deko weg", () => {
const spots = bookableSpots(
plan([
item({ type: "mat", label: "1" }),
item({ type: "reformer", label: "10" }),
item({ type: "trainer", label: "" }),
item({ type: "reformer", label: "2" }),
])
);
expect(spots.map(spotDisplayName)).toEqual(["Reformer 2", "Reformer 10", "Matte 1"]);
});
it("liefert für keinen Grundriss eine leere Liste", () => {
expect(bookableSpots(null)).toEqual([]);
});
});
describe("nextLabel", () => {
it("füllt Lücken in der Nummerierung pro Gerätetyp", () => {
const items = [
item({ label: "1" }),
item({ label: "2" }),
item({ label: "4" }),
item({ type: "mat", label: "3" }),
];
expect(nextLabel(items, "reformer")).toBe("3");
expect(nextLabel(items, "mat")).toBe("1");
});
it("vergibt an Deko keine Nummer", () => {
expect(nextLabel([], "door")).toBe("");
});
});
describe("placeInRoom", () => {
it("hält ein Gerät vollständig innerhalb der Wände", () => {
// Reformer ungedreht: 0,7 m breit, 2,4 m lang
expect(placeInRoom(item(), { x: -3, y: 100 }, plan([]))).toEqual({ x: 0.35, y: 4.8 });
});
it("berücksichtigt die Drehung beim Einpassen", () => {
const turned = item({ rotation: 90 });
expect(itemFootprint(turned)).toEqual({ w: 2.4, h: 0.7 });
expect(placeInRoom(turned, { x: 0, y: 0 }, plan([]))).toEqual({ x: 1.2, y: 0.35 });
});
it("rastet auf 10 cm ein, ohne Gleitkomma-Reste", () => {
const pos = placeInRoom(item(), { x: 2.34, y: 3.26 }, plan([]));
expect(pos).toEqual({ x: 2.3, y: 3.3 });
});
});
describe("findFreePosition", () => {
it("reiht neue Geräte nebeneinander auf, statt sie zu stapeln", () => {
const room = plan([]);
for (let i = 0; i < 6; i++) {
const pos = findFreePosition(room, { type: "reformer", rotation: 0 });
room.items.push(item({ ...pos, label: String(i + 1) }));
}
for (const a of room.items) {
for (const b of room.items) {
if (a === b) continue;
const overlapX = Math.abs(a.x - b.x) < 0.7;
const overlapY = Math.abs(a.y - b.y) < 2.4;
expect(overlapX && overlapY).toBe(false);
}
}
});
});