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("active"); return ( { setValue(next); onValueChange?.(next); }} activeCount={1} historyCount={0} toolbar={} > {{ active:

Liste aktiv

, history:

Liste Historie

}}
); } 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(); 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( {}}> {{ active: null, history: null }} , ); 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(); 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(); 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( {}} toolbar={} toolbarAlign="end" > {{ active: null, history: null }} , ); 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( {}} heading={

Lagerbehälter

} > {{ active: null, history: null }}
, ); 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(); 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) => / { 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([]); }); });