The Search entry added in 1ee3555 was in the array, in the build, and
typed correctly — and never appeared. The top-level nav block rendered
`dashboardItems[0]` with a hardcoded index, so every entry after the
first was silently dropped. Caught when the operator asked where the
link was.
Rendered as a map so appending an entry just works.
Regression test asserts the whole top-level set (Dashboard + Search)
rather than "Search exists", so the next person to append an entry gets
a passing test instead of an invisible link. Verified it fails when the
map is limited back to a single entry while the three pre-existing
sidebar tests keep passing.
125 lines
4.4 KiB
TypeScript
125 lines
4.4 KiB
TypeScript
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { Sidebar } from "./sidebar";
|
|
|
|
vi.mock("next/link", () => ({
|
|
default: ({ href, children, ...props }: { href: string; children: React.ReactNode }) => (
|
|
<a href={href} {...props}>
|
|
{children}
|
|
</a>
|
|
),
|
|
}));
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
usePathname: () => "/suppliers",
|
|
}));
|
|
|
|
vi.mock("./auth-provider", () => ({
|
|
useAuth: () => ({
|
|
user: { name: "Test User", email: "test@example.com" },
|
|
logout: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
vi.mock("next-themes", () => ({
|
|
useTheme: () => ({
|
|
theme: "light",
|
|
setTheme: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
describe("Sidebar", () => {
|
|
it("does not show a separate supplier map navigation item", () => {
|
|
render(<Sidebar />);
|
|
|
|
// Desktop + mobile nav trees both render — assert at least one.
|
|
expect(screen.getAllByText("Suppliers").length).toBeGreaterThan(0);
|
|
expect(screen.queryByText("Supplier Map")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("shows pricelist under the Stock section", () => {
|
|
render(<Sidebar />);
|
|
|
|
// Scope to the desktop sidebar (the first <aside>). The mobile
|
|
// Sheet renders a duplicate nav tree that's hidden via Tailwind
|
|
// `lg:hidden` but still in the DOM, so unscoped role queries
|
|
// return two of every button.
|
|
const desktop = screen.getAllByRole("complementary")[0];
|
|
// The toggle is a `<button>` with text "Stock" (the nav link
|
|
// is an `<a>` so role=button scopes us to the toggle only,
|
|
// and exact match stops it from matching "Stock Orders" link).
|
|
const stockToggle = within(desktop).getByRole("button", {
|
|
name: "Stock",
|
|
});
|
|
|
|
fireEvent.click(stockToggle);
|
|
|
|
const stockSection = stockToggle.parentElement;
|
|
expect(stockSection).not.toBeNull();
|
|
|
|
// The Stock group should contain Stock, Stock Orders, and Pricelist.
|
|
// Use role=link to scope to the nav items (not the toggle
|
|
// button, which also says "Stock" and would create an
|
|
// ambiguous match).
|
|
const stock = within(stockSection as HTMLElement);
|
|
expect(stock.getByRole("link", { name: "Stock" })).toBeInTheDocument();
|
|
expect(stock.getByRole("link", { name: "Stock Orders" })).toBeInTheDocument();
|
|
expect(stock.getByRole("link", { name: "Pricelist" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows Stock under the Stock section, not Operations", () => {
|
|
render(<Sidebar />);
|
|
|
|
const desktop = screen.getAllByRole("complementary")[0];
|
|
const stockToggle = within(desktop).getByRole("button", {
|
|
name: "Stock",
|
|
});
|
|
const operationsToggle = within(desktop).getByRole("button", {
|
|
name: "Operations",
|
|
});
|
|
|
|
fireEvent.click(stockToggle);
|
|
fireEvent.click(operationsToggle);
|
|
|
|
const stockSection = stockToggle.parentElement;
|
|
const operationsSection = operationsToggle.parentElement;
|
|
|
|
expect(stockSection).not.toBeNull();
|
|
expect(operationsSection).not.toBeNull();
|
|
|
|
const ops = within(operationsSection as HTMLElement);
|
|
// Operations should NOT contain Stock anymore — that's the whole
|
|
// point of the reorg.
|
|
expect(ops.queryByText("Stock", { exact: true })).not.toBeInTheDocument();
|
|
expect(ops.queryByText("Stock Orders")).not.toBeInTheDocument();
|
|
expect(ops.queryByText("Pricelist")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("renders every top-level nav entry, not just the first", () => {
|
|
// Regression guard. The top-level nav rendered `dashboardItems[0]`
|
|
// — a hardcoded index — so any entry added after Dashboard existed
|
|
// in the array and in the build but never appeared in the sidebar.
|
|
// That's how the universal Search link shipped invisible: the
|
|
// operator asked for it, the code was correct, the entry was simply
|
|
// never rendered.
|
|
//
|
|
// Asserts the full top-level set rather than "Search exists" so the
|
|
// next person to append an entry gets a passing test instead of a
|
|
// silently invisible link.
|
|
render(<Sidebar />);
|
|
const desktop = screen.getAllByRole("complementary")[0];
|
|
|
|
for (const label of ["Dashboard", "Search"]) {
|
|
expect(
|
|
within(desktop).queryByText(label, { exact: true }),
|
|
`top-level nav should render "${label}"`,
|
|
).toBeInTheDocument();
|
|
}
|
|
|
|
// And the Search entry must actually point at the route.
|
|
const link = within(desktop).getByText("Search", { exact: true }).closest("a");
|
|
expect(link).not.toBeNull();
|
|
expect(link).toHaveAttribute("href", "/search");
|
|
});
|
|
});
|