Files
ApiCore/ApiCore-Frontend/src/components/hive/hive-focus-bar.spec.tsx
T
Nick SzaunigandClaude Opus 5.5 6e13236d49 feat: holographic 3D hive scene and Futtertasche (Beute L3, first cut)
- 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]>
2026-10-16 09:12:03 +02:00

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();
});
});