litellm/ui/litellm-dashboard/src/components/navbar.test.tsx
ryan-crabbe-berri 4def6916da
refactor(ui): consolidate dashboard to one shell in the (dashboard) layout (#30166)
* refactor(ui): consolidate dashboard to one shell in the (dashboard) layout

Moves the legacy ?page= switch page into the (dashboard) route group and
hoists Navbar, sidebar, ThemeProvider, and DebugWarningBanner into the
shared layout with real props, deleting the degraded duplicate shell that
wrapped migrated routes. The active page key now derives from the URL at
render time, so navigating between legacy and migrated pages no longer
remounts the shell.

useProxySettings becomes a React Query hook taking accessToken, shared by
the navbar, the AdminPanel arm, and migrated pages; this replaces the
lifted proxySettings state and the Navbar setProxySettings prop drilling.
The invitation onboarding flow (?invitation_id=) keeps rendering without
chrome via a layout escape hatch. Dead dark mode state and the no-op antd
ConfigProvider are removed.

* fix(ui): include accessToken in useProxySettings query key

The queryFn closes over accessToken, so the key must include it for the
cache to be honest about its inputs. Settings are instance-global today,
which made the omission harmless, but a token change while mounted would
have served the cached entry without refetching.

* test(ui): point CreateKeyPage test at the moved page

The page moved into the (dashboard) route group and no longer renders
the navbar (the layout owns chrome now), so the valid-token test asserts
the default page content (UserDashboard stub) instead.
2026-06-10 18:37:44 -07:00

314 lines
10 KiB
TypeScript

import userEvent from "@testing-library/user-event";
import React, { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders, screen, waitFor } from "../../tests/test-utils";
import Navbar from "./navbar";
// Mock the hooks and utilities
vi.mock("@/components/networking", () => ({
getProxyBaseUrl: vi.fn(() => "http://localhost:4000"),
serverRootPath: "",
}));
vi.mock("@/app/(dashboard)/hooks/useDisableBouncingIcon", () => ({
useDisableBouncingIcon: () => false,
}));
vi.mock("./Navbar/BlogDropdown/BlogDropdown", () => ({
BlogDropdown: () => <div data-testid="blog-dropdown">Blog</div>,
}));
const mockUserDropdownData = vi.hoisted(() => ({
current: () => ({
userId: "test-user",
userEmail: "test@example.com",
userRole: "Admin",
premiumUser: false,
}),
}));
vi.mock("./Navbar/UserDropdown/UserDropdown", async (importOriginal) => {
const React = await import("react");
const { useState } = React;
const { Button } = await import("antd");
const localStorageUtils = await import("@/utils/localStorageUtils");
return {
default: function MockUserDropdown({ onLogout }: { onLogout: () => void }) {
const { userId, userEmail, userRole, premiumUser } = mockUserDropdownData.current();
const [open, setOpen] = useState(false);
return (
<div>
<Button type="text" aria-label="Open account menu" onClick={() => setOpen(!open)}>
Account
</Button>
{open && (
<div data-testid="user-dropdown-content">
<span>{userId}</span>
<span>{userRole}</span>
<span>{userEmail}</span>
{premiumUser && <span>Premium</span>}
<button type="button" onClick={() => onLogout()}>
Logout
</button>
<label>
<input
type="checkbox"
aria-label="Toggle hide new feature indicators"
onChange={(e) => {
if (e.target.checked) {
localStorageUtils.setLocalStorageItem("disableShowNewBadge", "true");
localStorageUtils.emitLocalStorageChange("disableShowNewBadge");
}
}}
/>
Toggle hide new feature indicators
</label>
</div>
)}
</div>
);
},
};
});
vi.mock("@/utils/proxyUtils", () => ({
fetchProxySettings: vi.fn().mockResolvedValue({
PROXY_BASE_URL: "",
PROXY_LOGOUT_URL: "https://example.com/logout",
}),
}));
// Mock CommunityEngagementButtons component
vi.mock("./Navbar/CommunityEngagementButtons/CommunityEngagementButtons", () => ({
CommunityEngagementButtons: () => (
<div data-testid="community-engagement-buttons">
<a href="https://www.litellm.ai/support" target="_blank" rel="noopener noreferrer">
Join Slack
</a>
<a href="https://github.com/BerriAI/litellm" target="_blank" rel="noopener noreferrer">
Star us on GitHub
</a>
</div>
),
}));
// Create mock functions that can be controlled in tests
let mockUseThemeImpl = () => ({ logoUrl: null as string | null });
let mockUseHealthReadinessDetailsImpl = () => ({ data: null as any });
let mockGetLocalStorageItemImpl = (key: string) => null as string | null;
let mockUseAuthorizedImpl = () => ({
userId: "test-user",
userEmail: "test@example.com",
userRole: "Admin",
premiumUser: false,
});
const useHealthReadinessDetailsSpy = vi.hoisted(() => vi.fn());
vi.mock("@/contexts/ThemeContext", () => ({
useTheme: () => mockUseThemeImpl(),
}));
vi.mock("@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails", () => ({
useHealthReadinessDetails: (accessToken: string | null | undefined) => {
useHealthReadinessDetailsSpy(accessToken);
return mockUseHealthReadinessDetailsImpl();
},
}));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => mockUseAuthorizedImpl(),
}));
vi.mock("@/utils/localStorageUtils", () => ({
LOCAL_STORAGE_EVENT: "local-storage-change",
getLocalStorageItem: (key: string) => mockGetLocalStorageItemImpl(key),
setLocalStorageItem: vi.fn(),
removeLocalStorageItem: vi.fn(),
emitLocalStorageChange: vi.fn(),
}));
vi.mock("@/utils/cookieUtils", () => ({
clearTokenCookies: vi.fn(),
}));
// Mock window.location.href for logout testing
Object.defineProperty(window, "location", {
value: { href: "" },
writable: true,
});
describe("Navbar", () => {
const defaultProps = {
accessToken: "test-token",
isPublicPage: false,
};
it("should render without crashing", () => {
renderWithProviders(<Navbar {...defaultProps} />);
expect(screen.getByRole("button", { name: /^notifications$/i })).toBeInTheDocument();
expect(screen.getByText("Docs")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /open account menu/i })).toBeInTheDocument();
});
it("should display user information in dropdown", async () => {
const user = userEvent.setup();
renderWithProviders(<Navbar {...defaultProps} />);
await user.click(screen.getByRole("button", { name: /open account menu/i }));
await waitFor(() => {
expect(screen.getByText("test-user")).toBeInTheDocument();
});
expect(screen.getByText("Admin")).toBeInTheDocument();
expect(screen.getByText("test@example.com")).toBeInTheDocument();
});
it("should show sidebar toggle button when onToggleSidebar is provided", () => {
const mockToggle = vi.fn();
renderWithProviders(<Navbar {...defaultProps} onToggleSidebar={mockToggle} />);
const toggleButton = screen.getByTitle("Collapse sidebar");
expect(toggleButton).toBeInTheDocument();
});
it("should call onToggleSidebar when sidebar button is clicked", async () => {
const mockToggle = vi.fn();
const user = userEvent.setup();
renderWithProviders(<Navbar {...defaultProps} onToggleSidebar={mockToggle} />);
const toggleButton = screen.getByTitle("Collapse sidebar");
await user.click(toggleButton);
expect(mockToggle).toHaveBeenCalledTimes(1);
});
it("should show premium user badge when premiumUser is true", async () => {
const user = userEvent.setup();
const originalCurrent = mockUserDropdownData.current;
mockUserDropdownData.current = () => ({
userId: "test-user",
userEmail: "test@example.com",
userRole: "Admin",
premiumUser: true,
});
renderWithProviders(<Navbar {...defaultProps} />);
await user.click(screen.getByRole("button", { name: /open account menu/i }));
await waitFor(() => {
expect(screen.getByText("Premium")).toBeInTheDocument();
});
// Reset mock
mockUserDropdownData.current = originalCurrent;
});
it("should show version badge when health data contains version", () => {
mockUseHealthReadinessDetailsImpl = () => ({ data: { litellm_version: "1.0.0" } });
renderWithProviders(<Navbar {...defaultProps} />);
expect(screen.getByText("v1.0.0")).toBeInTheDocument();
// Reset mock
mockUseHealthReadinessDetailsImpl = () => ({ data: null });
});
it("should forward accessToken to the readiness hook", () => {
useHealthReadinessDetailsSpy.mockClear();
renderWithProviders(<Navbar {...defaultProps} accessToken="my-token" />);
expect(useHealthReadinessDetailsSpy).toHaveBeenCalledWith("my-token");
});
it("should forward a null accessToken to the readiness hook (disables the hook)", () => {
useHealthReadinessDetailsSpy.mockClear();
renderWithProviders(<Navbar {...defaultProps} accessToken={null} />);
expect(useHealthReadinessDetailsSpy).toHaveBeenCalledWith(null);
});
it("should use custom logo from theme context", () => {
mockUseThemeImpl = () => ({ logoUrl: "https://example.com/custom-logo.png" });
renderWithProviders(<Navbar {...defaultProps} />);
const logoImg = screen.getByAltText("LiteLLM Brand");
expect(logoImg).toHaveAttribute("src", "https://example.com/custom-logo.png");
// Reset mock
mockUseThemeImpl = () => ({ logoUrl: null });
});
it("should hide user dropdown and notifications on public pages", () => {
const publicPageProps = { ...defaultProps, isPublicPage: true };
renderWithProviders(<Navbar {...publicPageProps} />);
expect(screen.queryByRole("button", { name: /open account menu/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^notifications$/i })).not.toBeInTheDocument();
});
it("should handle hide new features toggle", async () => {
const user = userEvent.setup();
// Initially disabled
mockGetLocalStorageItemImpl = (key: string) => {
if (key === "disableShowNewBadge") return "false";
return null;
};
renderWithProviders(<Navbar {...defaultProps} />);
await user.click(screen.getByRole("button", { name: /open account menu/i }));
await waitFor(() => {
expect(screen.getByText("test-user")).toBeInTheDocument();
});
// Find and click the toggle switch
const toggleSwitch = screen.getByLabelText("Toggle hide new feature indicators");
await user.click(toggleSwitch);
// The functions are mocked globally, so we can check if they were called
// by accessing them through the mock registry
const localStorageUtils = vi.mocked(await import("@/utils/localStorageUtils"));
expect(localStorageUtils.setLocalStorageItem).toHaveBeenCalledWith("disableShowNewBadge", "true");
expect(localStorageUtils.emitLocalStorageChange).toHaveBeenCalledWith("disableShowNewBadge");
// Reset mock
mockGetLocalStorageItemImpl = (key: string) => null;
});
it("should handle logout functionality", async () => {
const user = userEvent.setup();
renderWithProviders(<Navbar {...defaultProps} />);
await user.click(screen.getByRole("button", { name: /open account menu/i }));
await waitFor(() => {
expect(screen.getByText("test-user")).toBeInTheDocument();
});
// Click logout
await user.click(screen.getByText("Logout"));
const cookieUtils = vi.mocked(await import("@/utils/cookieUtils"));
expect(cookieUtils.clearTokenCookies).toHaveBeenCalled();
await waitFor(() => {
expect(window.location.href).toBe("https://example.com/logout");
});
});
it("should not render dark mode toggle slider", () => {
renderWithProviders(<Navbar {...defaultProps} />);
// DO NOT RENDER THIS UNTIL ALL COMPONENTS ARE CONFIRMED TO SUPPORT DARK MODE STYLES. IT IS AN ISSUE IF THIS TEST FAILS.
expect(screen.queryByTestId("dark-mode-toggle")).not.toBeInTheDocument();
});
});