diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx new file mode 100644 index 00000000000..af68d9f87e9 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import { AuthProvider } from "@/contexts/AuthContext"; +import Layout from "./layout"; + +vi.mock("next/navigation", () => ({ + useRouter: vi.fn(() => ({ push: vi.fn(), replace: vi.fn() })), + useSearchParams: vi.fn(() => new URLSearchParams()), + usePathname: vi.fn(() => "/ui/guardrails"), +})); + +vi.mock("@/components/navbar", () => ({ + default: () =>
, +})); + +vi.mock("@/app/(dashboard)/components/SidebarProvider", () => ({ + default: () =>
, +})); + +vi.mock("@/components/DebugWarningBanner", () => ({ + DebugWarningBanner: () => null, +})); + +vi.mock("@/contexts/ThemeContext", () => ({ + ThemeProvider: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +vi.mock("@/components/common_components/LoadingScreen", () => ({ + default: () =>
, +})); + +type Deferred = { promise: Promise; resolve: () => void }; + +const createDeferred = (): Deferred => { + let resolve!: () => void; + const promise = new Promise((r) => { + resolve = r; + }); + return { promise, resolve }; +}; + +let pendingUiConfig: Deferred; + +vi.mock("@/components/networking", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getUiConfig: vi.fn(() => pendingUiConfig.promise), + setGlobalLitellmHeaderName: vi.fn(), + }; +}); + +describe("(dashboard) Layout", () => { + beforeEach(() => { + vi.clearAllMocks(); + pendingUiConfig = createDeferred(); + }); + + it("does not mount route content until getUiConfig has resolved", async () => { + render( + + +
+ + , + ); + + await waitFor(() => expect(screen.getByTestId("loading-screen")).toBeTruthy()); + expect(screen.queryByTestId("page-content")).toBeNull(); + expect(screen.queryByTestId("navbar")).toBeNull(); + + pendingUiConfig.resolve(); + + await waitFor(() => expect(screen.getByTestId("page-content")).toBeTruthy()); + expect(screen.getByTestId("navbar")).toBeTruthy(); + expect(screen.queryByTestId("loading-screen")).toBeNull(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx index df5b2ab4511..b32bed44a87 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx @@ -45,9 +45,13 @@ function DashboardShell({ children }: { children: React.ReactNode }) { function LayoutContent({ children }: { children: React.ReactNode }) { const searchParams = useSearchParams(); - const { accessToken } = useAuth(); + const { accessToken, authLoading } = useAuth(); const isInvitationFlow = Boolean(searchParams.get("invitation_id")); + if (authLoading) { + return ; + } + return ( {isInvitationFlow ? children : {children}}