forked from baron/baron-sso
test: raise frontend coverage baselines
This commit is contained in:
145
adminfront/src/components/layout/AppLayout.test.tsx
Normal file
145
adminfront/src/components/layout/AppLayout.test.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={[entry]}>
|
||||
<Routes>
|
||||
<Route path="/" element={<AppLayout />}>
|
||||
<Route path="users" element={<div>Users outlet</div>} />
|
||||
<Route path="users/:id" element={<div>User detail outlet</div>} />
|
||||
<Route
|
||||
path="tenants/:tenantId"
|
||||
element={<div>Tenant outlet</div>}
|
||||
/>
|
||||
<Route path="login" element={<div>Login outlet</div>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
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("Data Integrity")).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();
|
||||
});
|
||||
});
|
||||
@@ -148,11 +148,7 @@ function AppLayout() {
|
||||
const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() =>
|
||||
readShellSessionExpiryEnabled(!isDevelopmentRuntime),
|
||||
);
|
||||
const {
|
||||
data: profile,
|
||||
isLoading: isProfileLoading,
|
||||
error: profileError,
|
||||
} = useQuery({
|
||||
const { data: profile } = useQuery({
|
||||
queryKey: ["me"],
|
||||
queryFn: async () => {
|
||||
debugLog("[AppLayout] Fetching profile...");
|
||||
|
||||
49
adminfront/src/components/ui/avatar.test.tsx
Normal file
49
adminfront/src/components/ui/avatar.test.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
|
||||
|
||||
let container: HTMLDivElement | null = null;
|
||||
|
||||
const render = async (element: React.ReactElement) => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(element);
|
||||
});
|
||||
return root;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
if (container) {
|
||||
container.remove();
|
||||
container = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe("Avatar", () => {
|
||||
it("renders image and fallback with merged classes", async () => {
|
||||
const root = await render(
|
||||
<Avatar className="custom-root" data-testid="avatar">
|
||||
<AvatarImage
|
||||
alt="Admin user"
|
||||
className="custom-image"
|
||||
src="/avatar.png"
|
||||
/>
|
||||
<AvatarFallback className="custom-fallback">AU</AvatarFallback>
|
||||
</Avatar>,
|
||||
);
|
||||
|
||||
const avatar = container?.querySelector("[data-testid='avatar']");
|
||||
const fallback = container?.textContent;
|
||||
|
||||
expect(avatar?.className).toContain("custom-root");
|
||||
expect(fallback).toContain("AU");
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
41
adminfront/src/components/ui/separator.test.tsx
Normal file
41
adminfront/src/components/ui/separator.test.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { Separator } from "./separator";
|
||||
|
||||
let container: HTMLDivElement | null = null;
|
||||
|
||||
const render = async (element: React.ReactElement) => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(element);
|
||||
});
|
||||
return root;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
if (container) {
|
||||
container.remove();
|
||||
container = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe("Separator", () => {
|
||||
it("renders a horizontal separator with custom classes", async () => {
|
||||
const root = await render(
|
||||
<Separator className="custom-separator" data-testid="separator" />,
|
||||
);
|
||||
|
||||
const separator = container?.querySelector("[data-testid='separator']");
|
||||
|
||||
expect(separator?.className).toContain("h-px");
|
||||
expect(separator?.className).toContain("custom-separator");
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user