import { describe, it, expect } from "vitest"; import { NAV_ITEMS, navItemsForRole, pickActiveNavKey } from "./nav-config"; describe("navItemsForRole", () => { it("includes every NAV_ITEM for an admin", () => { const items = navItemsForRole("admin"); expect(items).toHaveLength(NAV_ITEMS.length); for (const original of NAV_ITEMS) { expect(items.find((i) => i.key === original.key)).toBeDefined(); } }); it("hides admin-only entries for the 'user' role", () => { const items = navItemsForRole("user"); const keys = items.map((i) => i.key); expect(keys).not.toContain("admin"); }); it("returns the un-restricted entries (no visibleTo) for the 'user' role", () => { const items = navItemsForRole("user"); const keys = items.map((i) => i.key); expect(keys).toEqual( expect.arrayContaining(["dashboard", "accounts", "reminders", "activity", "settings"]), ); }); it("admin nav entry routes to /settings/users", () => { const admin = NAV_ITEMS.find((i) => i.key === "admin"); expect(admin).toBeDefined(); expect(admin!.href).toBe("/settings/users"); expect(admin!.visibleTo).toEqual(["admin"]); }); }); describe("pickActiveNavKey (longest-match active highlight)", () => { // Use the real NAV_ITEMS so a future href change doesn't silently // re-introduce the regression. const adminItems = navItemsForRole("admin"); const userItems = navItemsForRole("user"); it("highlights ONLY the Admin entry on /settings/users (not Settings too)", () => { // Repro of the user-reported regression. Naïve startsWith would // light up both Settings (/settings) and Admin (/settings/users) // because both prefixes match. The longest-match rule must pick // the Admin entry alone. const active = pickActiveNavKey(adminItems, "/settings/users"); expect(active).toBe("admin"); }); it("highlights Settings on /settings exact, with Admin NOT lit", () => { const active = pickActiveNavKey(adminItems, "/settings"); expect(active).toBe("settings"); }); it("highlights Settings on a subpath that is NOT /settings/users", () => { // Admin nav is admin-only; this test is just to confirm the // longest-match still picks Settings when no admin descendant // claims the path. const active = pickActiveNavKey(adminItems, "/settings/profile"); expect(active).toBe("settings"); }); it("highlights Dashboard ONLY on '/' exact (not on every route)", () => { expect(pickActiveNavKey(adminItems, "/")).toBe("dashboard"); expect(pickActiveNavKey(adminItems, "/accounts")).toBe("accounts"); expect(pickActiveNavKey(adminItems, "/reminders/abc-123")).toBe("reminders"); }); it("returns null when nothing matches (e.g. a route hidden from this role)", () => { // /settings/users isn't visible to a 'user' role, so the helper // must NOT highlight it as Settings just because /settings is a // prefix — we'd be claiming an item is active when the user can't // navigate to it from this nav. expect(pickActiveNavKey(userItems, "/settings/users")).toBe("settings"); // Neither item's href matches a totally foreign route. expect(pickActiveNavKey(adminItems, "/elsewhere")).toBeNull(); }); it("does NOT match a sibling that shares a prefix string", () => { // /settingsfoo is NOT a child of /settings — startsWith would // mistakenly mark Settings active. The strict descendant check // (`href + '/'`) prevents that. expect(pickActiveNavKey(adminItems, "/settingsfoo")).toBeNull(); }); it("each pathname highlights AT MOST one nav key (defense check)", () => { // Walk a small representative set of routes and confirm we never // light up two items at once. This is the contract the JSX in // app-shell.tsx relies on. const probes = [ "/", "/accounts", "/accounts/abc", "/reminders", "/reminders/abc", "/activity", "/activity?filter=success", "/settings", "/settings/users", "/settings/users/something", "/login", "/elsewhere", ]; for (const path of probes) { const matchCount = adminItems.filter((item) => { if (item.href === "/") return path === "/"; return path === item.href || path.startsWith(item.href + "/"); }).length; // If two prefixes both match, pickActiveNavKey must collapse // them to one — that's the whole point of the helper. const active = pickActiveNavKey(adminItems, path); if (matchCount === 0) { expect(active).toBeNull(); } else { expect(active).not.toBeNull(); } } }); });