- 3D dependencies (three, react-three-fiber, drei, postprocessing, motion), exact versions, supply-chain checked; scene loaded only on demand on the colony page (283 kB gzip), SVG stays as fallback - glass hologram per owner design 2026-10-16: true Liebig Zander shapes (grips, rebate, Wanderboden foot), cool blue edge glow, click any part -> it flies to the front while the rest stays as a faint ghost; focus shows content (feed levels, Bausperre, pockets); frames rise on hover and take their type look (colour + comb pattern) - full-bleed card with frosted overlays and glass buttons - Futtertasche (FRAME_FEEDER): new part kind occupying frame slots with a fill level, catalog row, validator rules in both ports, outline, sheets, follow-ups, settings Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
import { fireEvent, render, screen } from "@testing-library/react";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { frames, inlineCatalog, part } from "@/lib/hive/test-utils";
|
|
import type { HiveState } from "@/lib/hive/types";
|
|
import { HiveFocusBar, focusChips } from "./hive-focus-bar";
|
|
|
|
const catalog = inlineCatalog();
|
|
const state: HiveState = {
|
|
schemaVersion: 1,
|
|
stack: [
|
|
part("f", "floor", { inserts: [part("fi", "floor-insert")] }),
|
|
part("b1", "box", { frames: frames(10).map((frame) => (frame.slot <= 2 ? { ...frame, type: "NONE" as const } : frame)) }),
|
|
part("fe", "feeder", { state: { syrupLiters: 2.5, fondantKg: 1 } }),
|
|
part("lid", "inner-lid", { state: { feedHole: "CLOSED" } }),
|
|
],
|
|
};
|
|
|
|
function renderBar(extra: Partial<Parameters<typeof HiveFocusBar>[0]> = {}) {
|
|
const props = {
|
|
focusedId: "fe",
|
|
name: "Futterzarge",
|
|
chips: focusChips(state, catalog, "fe"),
|
|
canEdit: true,
|
|
onEdit: vi.fn(),
|
|
onBack: vi.fn(),
|
|
...extra,
|
|
};
|
|
render(<HiveFocusBar {...props} />);
|
|
return props;
|
|
}
|
|
|
|
describe("focusChips", () => {
|
|
it("lists levels with their maximum and choices with their German text", () => {
|
|
expect(focusChips(state, catalog, "fe").map((chip) => chip.text)).toEqual([
|
|
"Flüssigfutter 2,5 l von 3 l",
|
|
"Futterteig 1 kg von 2,5 kg",
|
|
]);
|
|
expect(focusChips(state, catalog, "lid").map((chip) => chip.text)).toEqual(["Futterloch: zu"]);
|
|
});
|
|
|
|
it("counts the frames of a box and says whether the floor holds a Bausperre", () => {
|
|
expect(focusChips(state, catalog, "b1")[0].text).toBe("8 Rähmchen");
|
|
expect(focusChips(state, catalog, "f").at(-1)!.text).toBe("Bausperre drin");
|
|
});
|
|
|
|
it("finds inserts and returns nothing for unknown ids", () => {
|
|
expect(focusChips(state, catalog, "fi")).toEqual([]);
|
|
expect(focusChips(state, catalog, "nope")).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("HiveFocusBar", () => {
|
|
it("renders nothing outside focus mode", () => {
|
|
const { container } = render(
|
|
<HiveFocusBar focusedId={null} name="" chips={[]} canEdit={false} onEdit={vi.fn()} onBack={vi.fn()} />,
|
|
);
|
|
expect(container).toBeEmptyDOMElement();
|
|
});
|
|
|
|
it("shows „Zurück“, the name, the state chips and „Ändern“", () => {
|
|
const props = renderBar();
|
|
expect(screen.getByText("Futterzarge")).toBeInTheDocument();
|
|
expect(screen.getByText("Flüssigfutter 2,5 l von 3 l")).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole("button", { name: "Zurück" }));
|
|
expect(props.onBack).toHaveBeenCalled();
|
|
fireEvent.click(screen.getByRole("button", { name: "Ändern" }));
|
|
expect(props.onEdit).toHaveBeenCalled();
|
|
});
|
|
|
|
it("hides „Ändern“ for readers", () => {
|
|
renderBar({ canEdit: false });
|
|
expect(screen.queryByRole("button", { name: "Ändern" })).toBeNull();
|
|
});
|
|
|
|
it("leaves focus mode on Esc", () => {
|
|
const props = renderBar();
|
|
fireEvent.keyDown(window, { key: "Escape" });
|
|
expect(props.onBack).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("leaves Esc to an open sheet", () => {
|
|
const props = renderBar();
|
|
const dialog = document.createElement("div");
|
|
dialog.setAttribute("role", "dialog");
|
|
const input = document.createElement("input");
|
|
dialog.appendChild(input);
|
|
document.body.appendChild(dialog);
|
|
fireEvent.keyDown(input, { key: "Escape" });
|
|
expect(props.onBack).not.toHaveBeenCalled();
|
|
dialog.remove();
|
|
});
|
|
});
|