Files
ApiCore/ApiCore-Frontend/src/components/active-history-tabs.spec.tsx
T
Nick.RosenloecherandClaude Opus 5.5 427969d8b9
Backend Image / build-and-push (push) Successful in 15m27s
Frontend Image / build-and-push (push) Successful in 23m9s
feat(t5): Wächter vollständig, Hinweis Sonnenmodus, Nacharbeiten (T5a A10)
- Baukasten-Wächter: jede Regel prüft eigene Beispiele (muss fangen, muss
  ignorieren), veraltete Ausnahmen und Ordner-Ausschlüsse schlagen fehl;
  neue Regeln für text-primary ohne -ink, dark:, Schrift unter 13 px
  (Tailwind, SVG, CSS, style, *_SIZE-Konstanten) und Verlaufs-Hintergründe.
  Aufgeteilt in baukasten-guard, touch-target-guard und einen Scan-Helfer.
- Hinweis „Neu: Sonnenmodus für draußen“ auf dem Dashboard, einmal je Gerät,
  „Ausprobieren“ schaltet live um; Fokus geht danach zur Überschrift.
- Varroa-Diagramm: Beschriftungen als HTML mit fester Größe statt SVG-Text.
- Medikamente zeigen die Tabs auch leer; Zahlen in Tabs nicht beim Laden
  oder Fehler; inaktive Tabs mit Hover für die Maus; TabPicker (Auswahl
  unter 640 px) für Automatisierung und Statistik; Rollen-Dialoge breit
  genug für die Rechte-Matrix.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-19 13:47:59 +02:00

143 lines
5.9 KiB
TypeScript

import { useState } from "react";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { ActiveHistoryTabs, countOf, type EntityLifecycleTab } from "./active-history-tabs";
function Harness({ onValueChange }: { onValueChange?: (value: EntityLifecycleTab) => void }) {
const [value, setValue] = useState<EntityLifecycleTab>("active");
return (
<ActiveHistoryTabs
value={value}
onValueChange={(next) => {
setValue(next);
onValueChange?.(next);
}}
activeCount={1}
historyCount={0}
toolbar={<button type="button">Filter</button>}
>
{{ active: <p>Liste aktiv</p>, history: <p>Liste Historie</p> }}
</ActiveHistoryTabs>
);
}
describe("ActiveHistoryTabs (spec 10.4)", () => {
it("ActiveHistoryTabs shows the counts and renders the matching panel", async () => {
const user = userEvent.setup();
const onValueChange = vi.fn();
render(<Harness onValueChange={onValueChange} />);
const active = screen.getByRole("tab", { name: "Aktiv (1)" });
const history = screen.getByRole("tab", { name: "Historie (0)" });
expect(active).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tabpanel", { name: "Aktiv (1)" })).toHaveTextContent("Liste aktiv");
expect(screen.queryByText("Liste Historie")).not.toBeInTheDocument();
await user.click(history);
expect(onValueChange).toHaveBeenLastCalledWith("history");
expect(history).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tabpanel", { name: "Historie (0)" })).toHaveTextContent("Liste Historie");
expect(screen.queryByText("Liste aktiv")).not.toBeInTheDocument();
await user.keyboard("{ArrowLeft}");
expect(onValueChange).toHaveBeenLastCalledWith("active");
expect(screen.getByRole("tabpanel")).toHaveTextContent("Liste aktiv");
});
it("leaves the counts out while they are unknown", () => {
render(
<ActiveHistoryTabs value="active" onValueChange={() => {}}>
{{ active: null, history: null }}
</ActiveHistoryTabs>,
);
expect(screen.getByRole("tab", { name: "Aktiv" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Historie" })).toBeInTheDocument();
});
it("puts the toolbar next to the tabs, outside the panels", () => {
render(<Harness />);
const filter = screen.getByRole("button", { name: "Filter" });
expect(within(screen.getByRole("tabpanel")).queryByRole("button", { name: "Filter" })).toBeNull();
expect(screen.getByRole("tablist").parentElement).toContainElement(filter);
});
it("arrow keys switch at once here, because both panels filter data that is already loaded", async () => {
const user = userEvent.setup();
render(<Harness />);
await user.click(screen.getByRole("tab", { name: "Aktiv (1)" }));
await user.keyboard("{ArrowRight}");
expect(screen.getByRole("tab", { name: "Historie (0)" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tabpanel")).toHaveTextContent("Liste Historie");
});
it("toolbarAlign end pushes the toolbar to the far right of the row", () => {
render(
<ActiveHistoryTabs
value="active"
onValueChange={() => {}}
toolbar={<button type="button">Neue Rolle</button>}
toolbarAlign="end"
>
{{ active: null, history: null }}
</ActiveHistoryTabs>,
);
const wrapper = screen.getByRole("button", { name: "Neue Rolle" }).parentElement as HTMLElement;
expect(wrapper.className.split(/\s+/)).toContain("ml-auto");
expect(screen.getByRole("tablist").parentElement).toContainElement(wrapper);
});
it("a heading opens the row before the tabs, the tabs sit on the right", () => {
render(
<ActiveHistoryTabs
value="active"
onValueChange={() => {}}
heading={<h2>Lagerbehälter</h2>}
>
{{ active: null, history: null }}
</ActiveHistoryTabs>,
);
const heading = screen.getByRole("heading", { name: "Lagerbehälter" });
const list = screen.getByRole("tablist");
expect(heading.compareDocumentPosition(list) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(list.parentElement).toContainElement(heading);
expect(list.className.split(/\s+/)).toContain("ml-auto");
});
it("the chips are 44 px high on touch and compact only with a fine pointer", () => {
render(<Harness />);
for (const tab of screen.getAllByRole("tab")) {
const classes = tab.className.split(/\s+/);
expect(classes).toContain("h-11");
expect(classes).toContain("pointer-fine:h-9");
}
});
it("countOf counts only loaded data: no number while loading or after a failed first load", () => {
expect(countOf(undefined, [])).toBeUndefined();
expect(countOf([], [])).toBe(0);
expect(countOf([1, 2, 3], [1, 3])).toBe(2);
});
it("every list passes its counts through countOf, so a failed load never says (0)", () => {
const src = join(__dirname, "..");
const files = (function walk(dir: string): string[] {
return readdirSync(dir).flatMap((name) => {
const path = join(dir, name);
if (statSync(path).isDirectory()) return walk(path);
return /\.tsx$/.test(name) && !/\.spec\./.test(name) ? [path] : [];
});
})(src);
const users = files.filter((file) => /<ActiveHistoryTabs\b/.test(readFileSync(file, "utf8")));
expect(users.length).toBeGreaterThanOrEqual(10);
const wrong = users.filter((file) => {
const text = readFileSync(file, "utf8");
const counts = [...text.matchAll(/(?:active|history)Count=\{([^}]*)\}/g)].map((match) => match[1]);
return counts.length !== 2 || counts.some((expression) => !/^countOf\(/.test(expression.trim()));
});
expect(wrong).toEqual([]);
});
});