import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { fireEvent, render, screen } from "@testing-library/react"; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createI18nMock } from "../../test/i18nMock"; import AppLayout from "./AppLayout"; const authState = { isAuthenticated: true, isLoading: false, user: { access_token: "access-token", expires_at: Math.floor(Date.now() / 1000) + 120, profile: { sub: "admin-1", name: "Admin User", email: "admin@example.com", }, }, signinSilent: vi.fn(async () => undefined), removeUser: vi.fn(), }; vi.mock("react-oidc-context", () => ({ useAuth: () => authState, })); vi.mock("../../lib/i18n", () => createI18nMock()); vi.mock("../../lib/adminApi", () => ({ fetchMe: vi.fn(async () => ({ id: "admin-1", name: "Fetched Admin", email: "fetched@example.com", role: "super_admin", tenantId: "tenant-1", manageableTenants: [ { id: "tenant-1", name: "GPDTDC", slug: "gpdtdc", type: "COMPANY", }, { id: "tenant-2", name: "기술연구팀", slug: "gpdtdc-rnd", type: "ORGANIZATION", }, ], })), })); function renderLayout(entry = "/users") { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return render( }> Users outlet} /> User detail outlet} /> Tenant outlet} /> Worksmobile outlet} /> Login outlet} /> , ); } describe("admin AppLayout", () => { beforeEach(() => { ( window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean } )._IS_TEST_MODE = true; authState.isAuthenticated = true; authState.isLoading = false; authState.user.expires_at = Math.floor(Date.now() / 1000) + 120; authState.signinSilent.mockClear(); authState.removeUser.mockClear(); window.localStorage.clear(); vi.spyOn(window, "confirm").mockReturnValue(true); }); it("renders admin navigation, fetched profile, and outlet content", async () => { renderLayout(); expect(await screen.findByText("Fetched Admin")).toBeInTheDocument(); expect(screen.getByText("Admin Control")).toBeInTheDocument(); expect(screen.getByText("Users outlet")).toBeInTheDocument(); expect(screen.getByText("Tenants")).toBeInTheDocument(); expect(screen.getByText("Org Chart")).toBeInTheDocument(); expect(screen.getByText("Worksmobile")).toBeInTheDocument(); expect(screen.getByText("Ory SSOT System")).toBeInTheDocument(); expect(screen.getByText("Data Integrity")).toBeInTheDocument(); const navigation = screen.getByRole("navigation"); const navLabels = Array.from(navigation.querySelectorAll("a")).map((link) => link.textContent?.trim(), ); expect(navLabels).toEqual([ "Overview", "Tenants", "Org Chart", "Worksmobile", "Ory SSOT System", "Data Integrity", "Users", "권한 부여", "Auth Guard", "API Keys", "Audit Logs", ]); const worksmobileIcon = screen.getByTestId("worksmobile-nav-icon"); expect(worksmobileIcon.tagName.toLowerCase()).toBe("svg"); expect(worksmobileIcon).toHaveAttribute("fill", "none"); expect(worksmobileIcon.querySelectorAll("path")).toHaveLength(4); expect(worksmobileIcon.querySelector('path[fill="white"]')).toBeNull(); }); it("toggles the sidebar and persists the collapsed state", async () => { renderLayout(); const collapseButton = await screen.findByRole("button", { name: "사이드바 접기", }); fireEvent.click(collapseButton); expect(window.localStorage.getItem("baron_shell_sidebar_collapsed")).toBe( "true", ); expect( screen.getByRole("button", { name: "사이드바 펼치기" }), ).toBeInTheDocument(); }); it("opens profile menu, navigates, toggles theme/session, and logs out", async () => { renderLayout(); const themeButton = await screen.findByRole("button", { name: "테마 전환", }); fireEvent.click(themeButton); expect(document.documentElement.classList.contains("dark")).toBe(true); fireEvent.click(screen.getByRole("button", { name: "계정 메뉴 열기" })); expect(screen.getByText("Manageable Tenants")).toBeInTheDocument(); const sessionSwitch = screen.getByRole("switch"); fireEvent.click(sessionSwitch); expect(window.localStorage.getItem("baron_session_expiry_enabled")).toBe( "false", ); fireEvent.click(screen.getByText("기술연구팀")); expect(await screen.findByText("Tenant outlet")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "계정 메뉴 열기" })); fireEvent.click(screen.getAllByText("내 정보")[0]); expect(await screen.findByText("User detail outlet")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "계정 메뉴 열기" })); fireEvent.click(screen.getAllByText("Logout")[1]); expect(window.confirm).toHaveBeenCalled(); expect(authState.removeUser).toHaveBeenCalled(); }, 10_000); it("attempts silent renewal on user activity when session is near expiry", async () => { authState.user.expires_at = Math.floor(Date.now() / 1000) + 60; renderLayout(); await screen.findByText("Fetched Admin"); fireEvent.keyDown(window, { key: "Tab" }); expect(authState.signinSilent).toHaveBeenCalled(); }); });