import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import AppLayout from "./AppLayout"; const authState = { isAuthenticated: true, isLoading: false, activeNavigator: undefined as string | undefined, error: null as Error | null, user: { access_token: "access-token", expires_at: Math.floor(Date.now() / 1000) + 120, profile: { sub: "user-1", name: "Dev Admin", email: "dev@example.com", role: "super_admin", }, }, signinSilent: vi.fn(), removeUser: vi.fn(), }; vi.mock("react-oidc-context", () => ({ useAuth: () => authState, })); vi.mock("../../features/auth/authApi", () => ({ fetchMe: vi.fn(async () => ({ id: "user-1", name: "Fetched Dev Admin", email: "fetched@example.com", role: "super_admin", })), })); vi.mock("../../lib/i18n", () => ({ t: (key: string, fallback?: string, vars?: Record) => { let text = fallback ?? key; for (const [name, value] of Object.entries(vars ?? {})) { text = text.replaceAll(`{{${name}}}`, String(value)); } return text; }, })); const roots: Root[] = []; beforeEach(() => { authState.isAuthenticated = true; authState.isLoading = false; authState.activeNavigator = undefined; authState.error = null; authState.user.expires_at = Math.floor(Date.now() / 1000) + 120; authState.signinSilent.mockReset(); authState.signinSilent.mockResolvedValue(undefined); authState.removeUser.mockReset(); window.localStorage.clear(); vi.spyOn(window, "confirm").mockReturnValue(true); }); afterEach(() => { for (const root of roots.splice(0)) { act(() => { root.unmount(); }); } vi.restoreAllMocks(); }); async function renderLayout(initialEntry = "/clients") { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); roots.push(root); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); await act(async () => { root.render( }> Client outlet} /> Profile outlet} /> , ); }); await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); return container; } describe("devfront AppLayout", () => { it("renders shell navigation, profile summary, and outlet content", async () => { const container = await renderLayout(); expect(container.textContent).toContain("Developer Console"); expect(container.textContent).toContain("Clients"); expect(container.textContent).toContain("Client outlet"); expect(container.textContent).toContain("Fetched Dev Admin"); expect(document.documentElement.classList.contains("light")).toBe(true); }); it("toggles profile menu, navigates to profile, toggles theme, and logs out", async () => { const container = await renderLayout(); const themeButton = container.querySelector( 'button[aria-label="Toggle theme"]', ) as HTMLButtonElement; await act(async () => { themeButton.click(); }); expect(document.documentElement.classList.contains("dark")).toBe(true); const profileButton = container.querySelector( 'button[aria-label="Open account menu"]', ) as HTMLButtonElement; await act(async () => { profileButton.click(); }); expect(container.textContent).toContain("My Profile"); const profileMenuItem = Array.from( container.querySelectorAll('button[role="menuitem"]'), ).find((button) => button.textContent?.includes("My Profile")); await act(async () => { (profileMenuItem as HTMLButtonElement).click(); }); expect(container.textContent).toContain("Profile outlet"); const logoutButton = Array.from(container.querySelectorAll("button")).find( (button) => button.textContent?.includes("Logout"), ); await act(async () => { (logoutButton as HTMLButtonElement).click(); }); expect(window.confirm).toHaveBeenCalled(); expect(authState.removeUser).toHaveBeenCalled(); }); it("attempts silent renewal after user action when the session is expiring", async () => { authState.user.expires_at = Math.floor(Date.now() / 1000) + 60; await renderLayout(); await act(async () => { window.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab" })); }); expect(authState.signinSilent).toHaveBeenCalled(); }); });