// @vitest-environment jsdom // // DESIGN.md B7: the nav rail's row. Icon-only, its name is the module's full name (with an unread // count, which was only ever drawn); with the labels on, the name is the short label the user SEES // (WCAG 2.5.3), the full one staying in the tooltip. "You are here" is aria-current plus the tint. import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { Activity, Bell, Shield, Users } from "lucide-react"; import { NavRailAction, NavRailGroup, NavRailItem } from "./NavRail"; vi.mock("../../lib/hints", () => ({ hintsEnabled: () => true, subscribeHints: () => () => {} })); afterEach(cleanup); describe("NavRailItem", () => { it("icon-only: the full name, the count in it, and the current page marked", () => { render( {}} />, ); const row = screen.getByRole("link", { name: "Notifications (3)" }); expect(row.getAttribute("aria-current")).toBe("page"); expect(row.className).toContain("bg-accent/15"); expect(row.textContent).not.toContain("Alerts"); }); it("labelled: the name is the visible short label", () => { render( {}} />, ); const row = screen.getByRole("link", { name: "Alerts" }); expect(row.getAttribute("aria-current")).toBeNull(); }); // Audit: labelled, the name was the row's text, the corner badge first, so 20 unread was heard // as "9+Alerts" where the icon-only row says "Notifications (20)". it("labelled with a count: the visible label, then the exact count", () => { render( {}} />, ); expect(screen.getByRole("link", { name: "Alerts (20)" })).toBeTruthy(); }); // Owner's call: a middle-click on a rail row opens the page in a new tab. The row is a real link // to the page, so the browser's own gestures work; only a plain left click navigates in-app. it("is a link to its page, navigating in-app only on a plain left click", () => { const onClick = vi.fn(); render( , ); const row = screen.getByRole("link", { name: "Notifications" }); expect(row.getAttribute("href")).toBe("/?page=notifications"); // Whether the row left the click to the browser, read after the app's own handler ran; then the // click is stopped here, as jsdom cannot follow a link. const leftToBrowser: boolean[] = []; const record = (e: Event) => { leftToBrowser.push(!e.defaultPrevented); e.preventDefault(); }; window.addEventListener("click", record); try { fireEvent.click(row, { button: 0 }); expect(onClick).toHaveBeenCalledTimes(1); for (const init of [ { button: 1 }, { button: 0, ctrlKey: true }, { button: 0, metaKey: true }, ]) fireEvent.click(row, init); } finally { window.removeEventListener("click", record); } expect(leftToBrowser).toEqual([false, true, true, true]); expect(onClick).toHaveBeenCalledTimes(1); }); }); // F32: the rail's footer (language, About, Sign out) drew 32 and 36 squares with 16 and 20px glyphs // beside the 36-tall full-width rows, so its spacing and glyphs broke the column. An action is a row too. describe("NavRailAction", () => { it("is a named rail row, not a toolbar icon button", () => { render( {}} />); const row = screen.getByRole("button", { name: "About" }); expect(row.className).toContain("h-9"); expect(row.className).toContain("w-full"); expect(row.querySelector("svg")?.getAttribute("class")).toContain("size-4.5"); }); it("the nav rail holds no toolbar icon button", () => { const src = readFileSync(resolve(__dirname, "../../shell/NavSidebar.tsx"), "utf8"); expect(src).not.toMatch(/]/); }); }); // The owner's pick: the five admin rows sat below the fold at 133% zoom behind a fade alone, so // they are one "Admin" row opening a panel of them. The panel's pages stay links, as the rail's are. describe("NavRailGroup", () => { const pages = (active: string | null, onSelect = vi.fn()) => [ { id: "scheduler", icon: Activity, label: "Scheduler" }, { id: "users", icon: Users, label: "Users" }, ].map((p) => ({ ...p, href: `/?page=${p.id}`, active: p.id === active, onSelect })); it("is one row, current while any of its pages is, opening a panel of links", () => { render( , ); const row = screen.getByRole("button", { name: "Administration" }); expect(row.getAttribute("aria-current")).toBe("page"); expect(screen.queryByRole("link")).toBeNull(); fireEvent.click(row); // jsdom never positions the panel, so it stays visibility:hidden and nameless to the role query. const panel = screen.getByRole("dialog", { hidden: true }); expect(panel.getAttribute("aria-label")).toBe("Administration"); const links = Array.from(panel.querySelectorAll("a")); expect(links.map((a) => [a.textContent, a.getAttribute("href")])).toEqual([ ["Scheduler", "/?page=scheduler"], ["Users", "/?page=users"], ]); expect(links.map((a) => a.getAttribute("aria-current"))).toEqual([null, "page"]); }); it("is not current while none of its pages is", () => { render( , ); const row = screen.getByRole("button", { name: "Admin" }); expect(row.getAttribute("aria-current")).toBeNull(); expect(row.className).not.toContain("bg-accent/15"); }); it("a plain click goes to the page and closes the panel; a middle or Ctrl click is the browser's", () => { const onSelect = vi.fn(); render( , ); fireEvent.click(screen.getByRole("button", { name: "Administration" })); const link = () => screen.getByRole("dialog", { hidden: true }).querySelector("a")!; const leftToBrowser: boolean[] = []; const record = (e: Event) => { leftToBrowser.push(!e.defaultPrevented); e.preventDefault(); }; window.addEventListener("click", record); try { fireEvent.click(link(), { button: 1 }); fireEvent.click(link(), { button: 0, ctrlKey: true }); expect(onSelect).not.toHaveBeenCalled(); fireEvent.click(link(), { button: 0 }); } finally { window.removeEventListener("click", record); } expect(leftToBrowser).toEqual([true, true, false]); expect(onSelect).toHaveBeenCalledTimes(1); expect(screen.queryByRole("dialog", { hidden: true })).toBeNull(); }); });