1
0
forked from baron/baron-sso
Files
baron-sso/devfront/src/features/clients/ClientsPage.test.tsx

367 lines
10 KiB
TypeScript

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRoot, type Root } from "react-dom/client";
import { act } from "react-dom/test-utils";
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import ClientsPage from "./ClientsPage";
const navigateMock = vi.fn();
const fetchClientsMock = vi.fn();
const fetchMeMock = vi.fn();
const fetchDeveloperRequestStatusMock = vi.fn();
const fetchMyTenantsMock = vi.fn();
const requestDeveloperAccessMock = vi.fn();
let authState = {
user: {
access_token: "access-token",
profile: {
role: "super_admin",
tenant_id: "tenant-1",
companyCode: "HANMAC",
name: "Dev Admin",
email: "dev@example.com",
phone: "010-0000-0000",
},
},
};
vi.mock("react-oidc-context", () => ({
useAuth: () => authState,
}));
vi.mock("react-router-dom", async () => {
const actual =
await vi.importActual<typeof import("react-router-dom")>(
"react-router-dom",
);
return {
...actual,
useNavigate: () => navigateMock,
};
});
vi.mock("../../lib/devApi", () => ({
fetchClients: () => fetchClientsMock(),
fetchMe: () => fetchMeMock(),
fetchDeveloperRequestStatus: () => fetchDeveloperRequestStatusMock(),
fetchMyTenants: () => fetchMyTenantsMock(),
requestDeveloperAccess: (...args: unknown[]) =>
requestDeveloperAccessMock(...args),
}));
vi.mock("../../lib/i18n", () => ({
t: (key: string, fallback?: string, vars?: Record<string, unknown>) => {
let text = fallback ?? key;
for (const [name, value] of Object.entries(vars ?? {})) {
text = text.replaceAll(`{{${name}}}`, String(value));
}
return text;
},
}));
const roots: Root[] = [];
afterEach(() => {
for (const root of roots.splice(0)) {
act(() => {
root.unmount();
});
}
vi.clearAllMocks();
document.body.innerHTML = "";
});
beforeEach(() => {
authState = {
user: {
access_token: "access-token",
profile: {
role: "super_admin",
tenant_id: "tenant-1",
companyCode: "HANMAC",
name: "Dev Admin",
email: "dev@example.com",
phone: "010-0000-0000",
},
},
};
fetchClientsMock.mockResolvedValue({
items: [],
limit: 100,
offset: 0,
});
fetchMeMock.mockResolvedValue({
role: "super_admin",
name: "Dev Admin",
email: "dev@example.com",
phone: "010-0000-0000",
});
fetchDeveloperRequestStatusMock.mockResolvedValue({ status: "none" });
fetchMyTenantsMock.mockResolvedValue([
{
id: "tenant-1",
name: "Hanmac",
slug: "hanmac",
type: "COMPANY",
parentId: null,
description: "",
status: "active",
memberCount: 10,
createdAt: "2026-05-01T00:00:00Z",
updatedAt: "2026-05-01T00:00:00Z",
},
]);
});
function makeClients(count: number) {
return Array.from({ length: count }, (_, index) => ({
id: `client-${index + 1}`,
name: `App ${index + 1}`,
type: index % 2 === 0 ? "private" : "pkce",
status: index % 2 === 0 ? "active" : "inactive",
createdAt: `2026-05-${String(index + 1).padStart(2, "0")}T00:00:00Z`,
redirectUris: [],
scopes: [],
metadata: {},
}));
}
async function setInputValue(input: HTMLInputElement, value: string) {
const descriptor = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
);
descriptor?.set?.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 0));
}
async function renderPage() {
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(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<ClientsPage />
</MemoryRouter>
</QueryClientProvider>,
);
});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
return container;
}
async function waitForTextContent(container: HTMLElement, text: string) {
for (let attempt = 0; attempt < 20; attempt += 1) {
if (container.textContent?.includes(text)) {
return;
}
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
}
throw new Error(`Expected container text to include: ${text}`);
}
describe("ClientsPage", () => {
it("expands the list and applies search filters", async () => {
fetchClientsMock.mockResolvedValue({
items: makeClients(6),
limit: 100,
offset: 0,
});
const container = await renderPage();
expect(container.textContent).toContain(
"총 6개의 애플리케이션이 등록되어 있습니다.",
);
expect(container.textContent).toContain("App 6");
expect(container.textContent).toContain("App 2");
expect(container.textContent).not.toContain("App 1");
const moreButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "더보기",
);
expect(moreButton).toBeTruthy();
await act(async () => {
moreButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("App 6");
expect(container.textContent).toContain("접기");
const advancedButton = Array.from(
container.querySelectorAll("button"),
).find((button) => button.textContent === "Advanced Filters");
expect(advancedButton).toBeTruthy();
await act(async () => {
advancedButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const searchInput = Array.from(container.querySelectorAll("input")).find(
(input) =>
input
.getAttribute("placeholder")
?.includes("클라이언트 이름/ID로 검색"),
) as HTMLInputElement | undefined;
if (!searchInput) {
throw new Error("Expected search input to be rendered");
}
await act(async () => {
await setInputValue(searchInput, "missing-client");
});
expect(container.textContent).toContain("조건에 맞는 연동 앱이 없습니다.");
const resetButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "초기화",
);
expect(resetButton).toBeTruthy();
await act(async () => {
resetButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await act(async () => {
await setInputValue(searchInput, "");
});
expect(container.textContent).toContain("App 1");
});
it("navigates to the developer request page from empty states", async () => {
authState = {
user: {
access_token: "access-token",
profile: {
role: "user",
tenant_id: "tenant-1",
companyCode: "HANMAC",
name: "Requester",
email: "requester@example.com",
phone: "010-1234-5678",
},
},
};
fetchClientsMock.mockResolvedValue({
items: [],
limit: 100,
offset: 0,
});
fetchDeveloperRequestStatusMock.mockResolvedValue({ status: "none" });
fetchMeMock.mockResolvedValue({
role: "user",
name: "Requester",
email: "requester@example.com",
phone: "010-1234-5678",
});
const container = await renderPage();
expect(container.textContent).toContain("개발자 등록 신청하기");
const requestButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "개발자 등록 신청하기",
);
expect(requestButton).toBeTruthy();
await act(async () => {
requestButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(navigateMock).toHaveBeenCalledWith("/developer-requests");
});
it("allows a user without tenant context to request developer access", async () => {
authState = {
user: {
access_token: "access-token",
profile: {
role: "user",
companyCode: "HANMAC",
name: "Requester",
email: "requester@example.com",
phone: "010-1234-5678",
},
},
};
fetchMeMock.mockResolvedValue({
role: "user",
name: "Requester",
email: "requester@example.com",
phone: "010-1234-5678",
});
fetchDeveloperRequestStatusMock.mockResolvedValue({ status: "none" });
const container = await renderPage();
await waitForTextContent(container, "개발자 등록 신청하기");
const requestButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "개발자 등록 신청하기",
);
expect(requestButton).toBeTruthy();
await act(async () => {
requestButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(navigateMock).toHaveBeenCalledWith("/developer-requests");
expect(fetchDeveloperRequestStatusMock).toHaveBeenCalled();
});
it("shows the create app button for a super admin without tenant context", async () => {
authState = {
user: {
access_token: "access-token",
profile: {
role: "super_admin",
companyCode: "HANMAC",
name: "Dev Admin",
email: "dev@example.com",
phone: "010-0000-0000",
},
},
};
fetchMeMock.mockResolvedValue({
role: "super_admin",
name: "Dev Admin",
email: "dev@example.com",
phone: "010-0000-0000",
});
const container = await renderPage();
expect(container.textContent).toContain("연동 앱 추가");
const createButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "연동 앱 추가",
);
expect(createButton).toBeTruthy();
await act(async () => {
createButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(navigateMock).toHaveBeenCalledWith("/clients/new");
});
});