fix(ui): gate dashboard layout on ui config load so deep links work under SERVER_ROOT_PATH (#30312)

* fix(ui): gate dashboard layout on ui config load so deep links work under SERVER_ROOT_PATH

* test(ui): create ui config deferred per test so the pending state stays repeatable
This commit is contained in:
ryan-crabbe-berri 2026-06-12 15:35:48 -07:00 committed by GitHub
parent 40301820e7
commit 76b4c4b111
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 83 additions and 1 deletions

View file

@ -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: () => <div data-testid="navbar" />,
}));
vi.mock("@/app/(dashboard)/components/SidebarProvider", () => ({
default: () => <div data-testid="sidebar" />,
}));
vi.mock("@/components/DebugWarningBanner", () => ({
DebugWarningBanner: () => null,
}));
vi.mock("@/contexts/ThemeContext", () => ({
ThemeProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
vi.mock("@/components/common_components/LoadingScreen", () => ({
default: () => <div data-testid="loading-screen" />,
}));
type Deferred = { promise: Promise<void>; resolve: () => void };
const createDeferred = (): Deferred => {
let resolve!: () => void;
const promise = new Promise<void>((r) => {
resolve = r;
});
return { promise, resolve };
};
let pendingUiConfig: Deferred;
vi.mock("@/components/networking", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/components/networking")>();
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(
<AuthProvider>
<Layout>
<div data-testid="page-content" />
</Layout>
</AuthProvider>,
);
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();
});
});

View file

@ -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 <LoadingScreen />;
}
return (
<ThemeProvider accessToken={accessToken}>
{isInvitationFlow ? children : <DashboardShell>{children}</DashboardShell>}