Files
ApiCore/ApiCore-Frontend/src/lib/hive/layout.spec.ts
T
Nick SzaunigandClaude Opus 5.5 c608f5d405 feat: hive card with SVG, outline, edit sheets and catalog settings (Beute L2)
- lib/hive: frontend port of the hive domain (ops, validator, facts,
  layout, follow-ups, describe texts, reason texts) running the backend
  fixtures, API client
- hive card on the colony page (same height as Stammdaten): SVG view,
  accessible outline tree, frame/part sheets with optimistic save,
  conflict sheet, history with past states, init from template or
  another colony, inspection draft mode
- follow-up toasts prefilling feeding and treatment forms (treatment
  end only with an open treatment)
- settings page "Beutensysteme" (hive-systems:manage)
- NumberStepper decimal steps, reactive motion level hook, holo tokens
- spec: owner design decision 2026-10-16 (glass hologram, focus mode)

The SVG view is the fallback; the 3D scene follows in L3.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 09:12:03 +02:00

70 lines
2.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { hiveLayout } from "./layout";
import { inlineCatalog, part, templateState } from "./test-utils";
describe("hiveLayout", () => {
const catalog = inlineCatalog();
it("computes the template height with the roof overlapping by 35 mm", () => {
const layout = hiveLayout(templateState(), catalog);
expect(layout.bounds.h).toBeCloseTo(4.31, 9);
expect(layout.bounds.w).toBeCloseTo(5.25, 9);
expect(layout.bounds.d).toBeCloseTo(4.3, 9);
});
it("stacks parts on +y from 0 with centres and sizes in units of 100 mm", () => {
const [floor, box, cover] = hiveLayout(templateState(), catalog).parts;
expect(floor.center).toEqual([0, 0.6, 0]);
expect(floor.size).toEqual([5.25, 1.2, 4.3]);
expect(floor.stackIndex).toBe(0);
expect(box.center[1]).toBeCloseTo(1.2 + 1.15, 9);
expect(cover.center[1]).toBeCloseTo(3.5 + 0.005, 9);
expect(box.stackIndex).toBe(1);
});
it("lets the roof overlap the part below while using its own height", () => {
const layout = hiveLayout(templateState(), catalog);
const roof = layout.parts[layout.parts.length - 1];
const below = layout.parts[layout.parts.length - 2];
const belowTop = below.center[1] + below.size[1] / 2;
const roofBottom = roof.center[1] - roof.size[1] / 2;
expect(belowTop - roofBottom).toBeCloseTo(0.35, 9);
expect(roof.size[1]).toBeCloseTo(0.6, 9);
});
it("places 10 frames evenly across the inner width of the box", () => {
const layout = hiveLayout(templateState(), catalog);
expect(layout.frames).toHaveLength(10);
const inner = 5.25 - 0.4;
const xs = layout.frames.map((f) => f.center[0]);
expect(xs[0]).toBeCloseTo(-inner / 2 + inner / 20, 9);
expect(xs[9]).toBeCloseTo(inner / 2 - inner / 20, 9);
const pitch = inner / 10;
xs.slice(1).forEach((x, i) => expect(x - xs[i]).toBeCloseTo(pitch, 9));
layout.frames.forEach((f, i) => {
expect(f.boxId).toBe("b");
expect(f.slot).toBe(i + 1);
expect(f.size[0]).toBeCloseTo(0.25, 9);
expect(f.size[1]).toBeCloseTo(2.2, 9);
});
});
it("puts the floor insert 5 mm above the floor bottom and the evaporator on the box bottom", () => {
const state = templateState();
state.stack[0].inserts = [part("fi", "floor-insert")];
state.stack[1].inserts = [part("ev", "evaporator")];
const layout = hiveLayout(state, catalog);
const insert = layout.inserts.find((i) => i.id === "fi")!;
const evaporator = layout.inserts.find((i) => i.id === "ev")!;
expect(insert.center[1]).toBeCloseTo(0.05, 9);
expect(insert.kind).toBe("FLOOR_INSERT");
expect(evaporator.center[1]).toBeCloseTo(1.2 + 0.6, 9);
expect(evaporator.stackIndex).toBe(1);
});
it("returns an empty layout for an empty stack", () => {
const layout = hiveLayout({ schemaVersion: 1, stack: [] }, catalog);
expect(layout).toEqual({ parts: [], inserts: [], frames: [], bounds: { w: 0, h: 0, d: 0 } });
});
});