diff --git a/src/components/layout/app-sidebar.tsx b/src/components/layout/app-sidebar.tsx index 84aeb606..e897bb89 100644 --- a/src/components/layout/app-sidebar.tsx +++ b/src/components/layout/app-sidebar.tsx @@ -3,7 +3,8 @@ import { Link, useLocation } from "react-router"; import { Box, Drawer } from "@mui/material"; import { BrandLockup, BrandMark } from "@/components/common/brand-lockup"; -import { getActiveNavPath, getSidebarWidth, navGroups, type NavItemDef } from "@/config/menu"; +import { useNavGroups } from "@/components/layout/use-nav-groups"; +import { getActiveNavPath, getSidebarWidth, type NavItemDef } from "@/config/menu"; function getNavItemAppearance(isActive: boolean, isHover: boolean) { if (isActive) { @@ -129,6 +130,7 @@ export function AppSidebar({ collapsed, mobile = false, open = true, onClose }: const width = getSidebarWidth(collapsed); const { pathname } = useLocation(); const activePath = getActiveNavPath(pathname); + const navGroups = useNavGroups(); return ( group.items) .map((item) => item.path) .filter((path) => diff --git a/src/test/components/layout/app-sidebar-nav.test.tsx b/src/test/components/layout/app-sidebar-nav.test.tsx new file mode 100644 index 00000000..5075b777 --- /dev/null +++ b/src/test/components/layout/app-sidebar-nav.test.tsx @@ -0,0 +1,83 @@ +import { screen, within } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { AppSidebar } from "@/components/layout/app-sidebar"; +import { renderWithProviders } from "@/test/test-utils"; + +const canApproveMock = vi.hoisted(() => vi.fn()); + +vi.mock("@/domain/uplifts/use-cases/use-uplifts-can-approve", () => ({ + useUpliftsCanApprove: canApproveMock, +})); + +function renderSidebar(route: string) { + return renderWithProviders(, { + route, + withAuth: false, + }); +} + +function sidebarLinks(): Record { + const nav = document.getElementById("primary-navigation"); + const links = Array.from(within(nav as HTMLElement).getAllByRole("link")); + return Object.fromEntries( + links.map((link) => [link.textContent ?? "", link.getAttribute("href") ?? ""]), + ); +} + +describe("AppSidebar navigation entries", () => { + it("shows Uplift Approvals for a user the backend says can approve uplifts", () => { + canApproveMock.mockReturnValue({ data: true }); + + renderSidebar("/"); + const links = sidebarLinks(); + + expect(links["Uplift Approvals"]).toBe("/uplifts"); + }); + + it("hides Uplift Approvals when the backend says the user cannot approve uplifts", () => { + canApproveMock.mockReturnValue({ data: false }); + + renderSidebar("/"); + const links = sidebarLinks(); + + expect(links["Uplift Approvals"]).toBeUndefined(); + }); + + it("hides Uplift Approvals while the backend answer is still loading", () => { + canApproveMock.mockReturnValue({ data: undefined }); + + renderSidebar("/"); + const links = sidebarLinks(); + + expect(links["Uplift Approvals"]).toBeUndefined(); + }); + + it("offers Services to every user", () => { + canApproveMock.mockReturnValue({ data: false }); + + renderSidebar("/"); + const links = sidebarLinks(); + + expect(links["Services"]).toBe("/settings/services"); + }); + + it("highlights the Uplift Approvals entry on /uplifts", () => { + canApproveMock.mockReturnValue({ data: true }); + + renderSidebar("/uplifts"); + + expect(screen.getByRole("link", { name: "Uplift Approvals" })).toHaveAttribute( + "aria-current", + "page", + ); + }); + + it("highlights the Services entry on /settings/services", () => { + canApproveMock.mockReturnValue({ data: false }); + + renderSidebar("/settings/services"); + + expect(screen.getByRole("link", { name: "Services" })).toHaveAttribute("aria-current", "page"); + }); +}); diff --git a/src/test/config/menu.test.ts b/src/test/config/menu.test.ts index ac75b38c..61184661 100644 --- a/src/test/config/menu.test.ts +++ b/src/test/config/menu.test.ts @@ -1,4 +1,4 @@ -import { getActiveNavPath } from "@/config/menu"; +import { getActiveNavPath, getNavGroups, navGroups } from "@/config/menu"; import { describe, expect, it } from "vitest"; describe("getActiveNavPath", () => { @@ -10,6 +10,9 @@ describe("getActiveNavPath", () => { ["/vendors/insights", "/vendors/insights"], ["/vendors/preferences", "/vendors/preferences"], ["/vendor-pos/42", "/vendor-pos"], + ["/settings/services", "/settings/services"], + ["/uplifts", "/uplifts"], + ["/uplifts/12", "/uplifts"], ])("selects one longest boundary-matching route for %s", (pathname, expectedPath) => { expect(getActiveNavPath(pathname)).toBe(expectedPath); }); @@ -17,4 +20,39 @@ describe("getActiveNavPath", () => { it("does not match partial route segments", () => { expect(getActiveNavPath("/vendor-posts")).toBeUndefined(); }); + + it("does not highlight the Services entry from other settings tabs", () => { + expect(getActiveNavPath("/settings/dropdowns")).toBeUndefined(); + }); +}); + +describe("navGroups", () => { + it("always offers Services in a DATA MANAGEMENT group", () => { + const dataManagement = navGroups.find((group) => group.title === "DATA MANAGEMENT"); + expect(dataManagement?.items.map((item) => item.path)).toContain("/settings/services"); + }); + + it("does not include the approvals group by default", () => { + expect(navGroups.some((group) => group.title === "APPROVALS")).toBe(false); + }); +}); + +describe("getNavGroups", () => { + it("adds the APPROVALS group with Uplift Approvals for uplift reviewers", () => { + const groups = getNavGroups({ canReviewUplifts: true }); + const approvals = groups.find((group) => group.title === "APPROVALS"); + expect(approvals?.items.map((item) => item.path)).toEqual(["/uplifts"]); + expect(approvals?.items[0]?.label).toBe("Uplift Approvals"); + }); + + it("omits the APPROVALS group for users who cannot review uplifts", () => { + const groups = getNavGroups({ canReviewUplifts: false }); + expect(groups.some((group) => group.title === "APPROVALS")).toBe(false); + }); + + it("keeps the approvals group after TEAM", () => { + const groups = getNavGroups({ canReviewUplifts: true }); + const titles = groups.map((group) => group.title); + expect(titles.indexOf("TEAM")).toBeLessThan(titles.indexOf("APPROVALS")); + }); });