import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { HomeIcon, SearchIcon } from "lucide-react"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { MobileNavigationProvider, type MobileNavigationItem } from "../../../contexts/MobileNavigationContext"; import { ViewportProvider } from "../../../contexts/ViewportContext"; import { configureI18n } from "../../../i18n"; import { MobileNavigationBar } from "../MobileNavigationBar"; // usePathname is globally mocked to "/" in vitest.setup.ts. Override it here so // the active-state rule can be exercised against a real route. A spec-level // vi.mock takes precedence over the setup-file mock for this module. const pathnameRef = { current: "/" }; vi.mock("next/navigation", () => ({ usePathname: () => pathnameRef.current, useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn(), back: vi.fn(), forward: vi.fn(), refresh: vi.fn(), }), useSearchParams: () => new URLSearchParams(), useParams: () => ({}), redirect: vi.fn(), notFound: vi.fn(), })); beforeAll(() => { // The package Link resolves its inner component at runtime and throws if i18n // was never configured (src/i18n/config.ts:53-58). configureI18n({ useRouter: () => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn(), forward: vi.fn(), refresh: vi.fn(), prefetch: vi.fn(), }), useTranslations: () => (key: string) => key, usePathname: () => pathnameRef.current, Link: ({ href, children, ...rest }: { href: string; children: React.ReactNode; [key: string]: any }) => ( {children} ), }); }); beforeEach(() => { pathnameRef.current = "/"; // jsdom defaults to 1024; ViewportProvider's effect measures immediately and // would otherwise overwrite the mobile seed with "desktop". Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 375 }); }); function renderBar(items: MobileNavigationItem[], { mobile = true }: { mobile?: boolean } = {}) { if (!mobile) { Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1024 }); } return render( , ); } const searchItem: MobileNavigationItem = { key: "search", label: "Cerca", icon: SearchIcon, onClick: vi.fn() }; const homeItem: MobileNavigationItem = { key: "home", label: "Dashboard", icon: HomeIcon, href: "/" }; describe("MobileNavigationBar", () => { it("renders nothing when the application supplied no items", () => { renderBar([]); expect(screen.queryByTestId("mobile-navigation-bar")).not.toBeInTheDocument(); }); it("renders nothing on desktop even when items exist", () => { renderBar([searchItem], { mobile: false }); expect(screen.queryByTestId("mobile-navigation-bar")).not.toBeInTheDocument(); }); it("renders one slot per item, labelled by its accessible name", () => { renderBar([searchItem, homeItem]); expect(screen.getByTestId("mobile-navigation-bar")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Cerca" })).toBeInTheDocument(); expect(screen.getByRole("link", { name: "Dashboard" })).toBeInTheDocument(); }); it("does not render the label as visible text (icons-only bar)", () => { renderBar([searchItem]); expect(screen.queryByText("Cerca")).not.toBeInTheDocument(); }); it("fires onClick for action slots", async () => { const onClick = vi.fn(); renderBar([{ ...searchItem, onClick }]); await userEvent.click(screen.getByRole("button", { name: "Cerca" })); expect(onClick).toHaveBeenCalledOnce(); }); it("renders the `render` escape hatch instead of an icon", () => { renderBar([ { key: "profile", label: "Impostazioni", render: , href: "/settings" }, ]); expect(screen.getByTestId("avatar-stub")).toBeInTheDocument(); }); // The load-bearing case: a bare startsWith would make href="/" match everything. it("marks only the exactly-matching slot active when one href is '/'", () => { pathnameRef.current = "/conversations"; renderBar([homeItem, { key: "ai", label: "Chiedi all'AI", icon: SearchIcon, href: "/conversations" }]); expect(screen.getByRole("link", { name: "Dashboard" })).not.toHaveAttribute("aria-current"); expect(screen.getByRole("link", { name: "Chiedi all'AI" })).toHaveAttribute("aria-current", "page"); }); it("keeps a section active on its detail route", () => { pathnameRef.current = "/conversations/abc-123"; renderBar([{ key: "ai", label: "Chiedi all'AI", icon: SearchIcon, href: "/conversations" }]); expect(screen.getByRole("link", { name: "Chiedi all'AI" })).toHaveAttribute("aria-current", "page"); }); it("never marks an action slot active", () => { pathnameRef.current = "/"; renderBar([searchItem]); expect(screen.getByRole("button", { name: "Cerca" })).not.toHaveAttribute("aria-current"); }); });