import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; import { act, useState, type ReactNode } from "react"; import { createRoot, type Root } from "react-dom/client"; import { LineTabs, LineTabsLink, LineTabsList, LineTabsNav, LineTabsTrigger } from "./line-tabs"; beforeAll(() => { GlobalRegistrator.register(); ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; }); afterAll(() => { GlobalRegistrator.unregister(); }); async function render(node: ReactNode): Promise<{ container: HTMLElement; root: Root }> { const container = document.createElement("div"); document.body.append(container); const root = createRoot(container); await act(async () => root.render(node)); return { container, root }; } describe("LineTabs", () => { test("a count with a label is read as words, not as a bare number", async () => { const { container, root } = await render( Add}> Library Review , ); try { const review = [...container.querySelectorAll('[role="tab"]')].find((tab) => tab.textContent?.startsWith("Review"), )!; const badge = review.querySelector('[data-slot="line-tab-count"]')!; expect(badge.getAttribute("aria-hidden")).toBe("true"); expect(badge.className).toContain("text-status-waiting"); expect(review.textContent).toContain(", 3 waiting"); // The trailing action sits outside the tablist, on the same rule. const bar = container.querySelector('[data-slot="line-tabs-bar"]')!; expect(bar.querySelector('[role="tablist"] button[type="button"]:not([role])')).toBeNull(); expect(bar.querySelector('[data-slot="line-tabs-trailing"] button')?.textContent).toBe("Add"); } finally { await act(async () => root.unmount()); container.remove(); } }); test("the pill variant styles every trigger as a pill", async () => { const { container, root } = await render( All Skills , ); try { const tabs = [...container.querySelectorAll('[role="tab"]')]; expect(tabs).toHaveLength(2); for (const tab of tabs) expect(tab.className).toContain("rounded-full"); expect(tabs[0]!.getAttribute("data-state")).toBe("active"); } finally { await act(async () => root.unmount()); container.remove(); } }); test("link tabs mark the current page and accept a router link as the child", async () => { const { container, root } = await render( General Access , ); try { const links = [...container.querySelectorAll("a")]; expect(links).toHaveLength(2); expect(links[0]!.getAttribute("aria-current")).toBe("page"); expect(links[0]!.getAttribute("data-state")).toBe("active"); expect(links[1]!.hasAttribute("data-router-link")).toBe(true); expect(links[1]!.getAttribute("aria-current")).toBeNull(); expect(links[1]!.className).toContain("h-11"); expect(links[1]!.textContent).toBe("Access2"); expect(container.querySelectorAll('[role="listitem"]')).toHaveLength(2); } finally { await act(async () => root.unmount()); container.remove(); } }); test("scrolls a newly active tab into view without scrolling the page", async () => { const tabs = ["overview", "setup", "checks", "versions", "changes"]; let setValue: (value: string) => void = () => {}; function Harness() { const [value, set] = useState("overview"); setValue = set; return ( {tabs.map((tab) => ( {tab} ))} ); } const { container, root } = await render(); try { const list = container.querySelector('[role="tablist"]')!; // A 100px viewport over five 70px tabs, 80px apart. Object.defineProperty(list, "clientWidth", { configurable: true, value: 100 }); Object.defineProperty(list, "scrollWidth", { configurable: true, value: 390 }); list.getBoundingClientRect = () => new DOMRect(0, 0, 100, 44); container.querySelectorAll('[role="tab"]').forEach((tab, index) => { tab.getBoundingClientRect = () => new DOMRect(index * 80 - list.scrollLeft, 0, 70, 44); }); expect(list.scrollLeft).toBe(0); await act(async () => setValue("versions")); await act(async () => new Promise((resolve) => setTimeout(resolve, 0))); // Right edge of "versions" (310) - viewport (100) + 24px of breathing room. expect(list.scrollLeft).toBe(234); } finally { await act(async () => root.unmount()); container.remove(); } }); });