- 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]>
143 lines
5.9 KiB
TypeScript
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([]);
|
|
});
|
|
});
|