// @vitest-environment jsdom import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { MemoryRouter } from "@/lib/router"; import { IconRail } from "./IconRail"; // The real @/lib/router Link wrapper calls useCompany(), which throws // outside a CompanyProvider. The IconRail doesn't depend on the company // context for building URLs (it receives companyPrefix as a prop), so we // stub useCompany() to return a minimal value so Link can render in tests. vi.mock("@/context/CompanyContext", () => ({ useCompany: () => ({ companies: [], selectedCompanyId: null, selectedCompany: null, selectionSource: "bootstrap" as const, loading: false, error: null, setSelectedCompanyId: () => {}, reloadCompanies: async () => {}, createCompany: async () => { throw new Error("not implemented in test stub"); }, }), })); // Tell React this environment uses act() for event flushing. // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; describe("IconRail", () => { let container: HTMLDivElement; let root: ReturnType | null = null; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); root = null; }); afterEach(() => { if (root) { act(() => { root!.unmount(); }); root = null; } if (container.parentNode) { container.remove(); } }); function renderRail(initialPath: string) { root = createRoot(container); act(() => { root!.render( , ); }); return { getNav: () => container.querySelector("nav[aria-label='Primary']") as HTMLElement, getLinks: () => Array.from(container.querySelectorAll("nav[aria-label='Primary'] a")) as HTMLAnchorElement[], getLinkByLabel: (label: string) => container.querySelector(`nav[aria-label='Primary'] a[aria-label='${label}']`) as HTMLAnchorElement | null, }; } it("renders a Primary nav with the four destination links", () => { const { getNav, getLinks } = renderRail("/NEX/assistant"); expect(getNav()).not.toBeNull(); const links = getLinks(); // The rail also renders a "Nexus home" link for the mark at the top; // we filter to just the four destinations below. const labels = links .map((a) => a.getAttribute("aria-label")) .filter((label) => label !== "Nexus home"); expect(labels).toEqual(["Assistant", "Studio", "Projects", "Settings"]); }); it("builds company-prefixed URLs for the first three destinations", () => { const { getLinkByLabel } = renderRail("/NEX/assistant"); expect(getLinkByLabel("Assistant")?.getAttribute("href")).toBe("/NEX/assistant"); expect(getLinkByLabel("Studio")?.getAttribute("href")).toBe("/NEX/content-studio"); expect(getLinkByLabel("Projects")?.getAttribute("href")).toBe("/NEX/projects"); }); it("points Settings at the global /instance/settings/general route", () => { const { getLinkByLabel } = renderRail("/NEX/assistant"); expect(getLinkByLabel("Settings")?.getAttribute("href")).toBe("/instance/settings/general"); }); it("marks the Assistant link as current on /NEX/assistant", () => { const { getLinkByLabel } = renderRail("/NEX/assistant"); expect(getLinkByLabel("Assistant")?.getAttribute("aria-current")).toBe("page"); expect(getLinkByLabel("Projects")?.getAttribute("aria-current")).toBeNull(); }); it("marks the Projects link as current on /NEX/issues", () => { const { getLinkByLabel } = renderRail("/NEX/issues"); expect(getLinkByLabel("Projects")?.getAttribute("aria-current")).toBe("page"); expect(getLinkByLabel("Assistant")?.getAttribute("aria-current")).toBeNull(); }); it("marks the Studio link as current on /NEX/convert (Convert is folded into Studio)", () => { const { getLinkByLabel } = renderRail("/NEX/convert"); expect(getLinkByLabel("Studio")?.getAttribute("aria-current")).toBe("page"); }); it("marks the Settings link as current on /instance/settings/general", () => { const { getLinkByLabel } = renderRail("/instance/settings/general"); expect(getLinkByLabel("Settings")?.getAttribute("aria-current")).toBe("page"); }); });