import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createRoot, type Root } from "react-dom/client"; import { act } from "react-dom/test-utils"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import DeveloperRequestPage from "./DeveloperRequestPage"; const fetchDeveloperRequestsMock = vi.fn(); const fetchMyTenantsMock = vi.fn(); const fetchMeMock = vi.fn(); const requestDeveloperAccessMock = vi.fn(); let 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", }, }, }; vi.mock("react-oidc-context", () => ({ useAuth: () => authState, })); vi.mock("../auth/authApi", () => ({ fetchMe: () => fetchMeMock(), })); vi.mock("../../lib/devApi", () => ({ fetchDeveloperRequests: () => fetchDeveloperRequestsMock(), fetchMyTenants: () => fetchMyTenantsMock(), requestDeveloperAccess: (...args: unknown[]) => requestDeveloperAccessMock(...args), approveDeveloperRequest: vi.fn(), rejectDeveloperRequest: vi.fn(), cancelDeveloperRequestApproval: vi.fn(), })); 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[] = []; 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: "user", tenant_id: "tenant-1", companyCode: "HANMAC", name: "Requester", email: "requester@example.com", phone: "010-1234-5678", }, }, }; fetchDeveloperRequestsMock.mockResolvedValue([]); 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", }, ]); fetchMeMock.mockResolvedValue({ id: "user-1", name: "Requester", email: "requester@example.com", phone: "010-1234-5678", role: "user", }); requestDeveloperAccessMock.mockResolvedValue({ status: "pending" }); }); async function setTextAreaValue(input: HTMLTextAreaElement, value: string) { const descriptor = Object.getOwnPropertyDescriptor( HTMLTextAreaElement.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( , ); }); await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); return container; } describe("DeveloperRequestPage", () => { it("opens the request modal and submits a request", async () => { const container = await renderPage(); expect(container.textContent).toContain("신규 신청하기"); const actionButton = Array.from(container.querySelectorAll("button")).find( (button) => button.textContent?.includes("신규 신청하기"), ); expect(actionButton).toBeTruthy(); await act(async () => { actionButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(container.textContent).toContain("개발자 등록 신청"); const reasonField = container.querySelector( "textarea", ) as HTMLTextAreaElement | null; if (!reasonField) { throw new Error("Expected reason textarea to be rendered"); } await act(async () => { await setTextAreaValue(reasonField, "Need RP access"); }); const submitButton = Array.from(container.querySelectorAll("button")).find( (button) => button.textContent === "신청하기", ); expect(submitButton).toBeTruthy(); await act(async () => { submitButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); await new Promise((resolve) => setTimeout(resolve, 0)); }); expect(requestDeveloperAccessMock).toHaveBeenCalled(); expect(requestDeveloperAccessMock.mock.calls[0]?.[0]).toEqual({ name: "Requester", organization: "Hanmac", reason: "Need RP access", tenantId: "tenant-1", }); }); it("shows a tenant-required notice and hides the request button when tenant is missing", async () => { authState = { user: { access_token: "access-token", profile: { role: "user", companyCode: "HANMAC", name: "Requester", email: "requester@example.com", phone: "010-1234-5678", }, }, }; fetchMeMock.mockResolvedValue({ id: "user-1", name: "Requester", email: "requester@example.com", phone: "010-1234-5678", role: "user", }); const container = await renderPage(); expect(container.textContent).toContain( "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.", ); expect(container.textContent).not.toContain("신규 신청하기"); expect(requestDeveloperAccessMock).not.toHaveBeenCalled(); }); });