import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { fireEvent, render, screen, waitFor } 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 TenantUsersPage from "./TenantUsersPage"; const exportUsersCSVMock = vi.hoisted(() => vi.fn()); const updateUserMock = vi.hoisted(() => vi.fn()); const bulkUpdateUsersMock = vi.hoisted(() => vi.fn()); const fetchUsersMock = vi.hoisted(() => vi.fn()); vi.mock("../../../lib/i18n", () => createI18nMock()); vi.mock("../../../lib/adminApi", () => ({ fetchTenant: vi.fn(async () => ({ id: "tenant-team-id", name: "기술기획팀", slug: "tech-planning", })), fetchUsers: fetchUsersMock, bulkUpdateUsers: bulkUpdateUsersMock, exportUsersCSV: exportUsersCSVMock, updateUser: updateUserMock, })); function renderTenantUsersPage() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); const result = render( } /> , ); return { ...result, queryClient }; } describe("TenantUsersPage export", () => { beforeEach(() => { exportUsersCSVMock.mockReset(); updateUserMock.mockReset(); bulkUpdateUsersMock.mockReset(); fetchUsersMock.mockReset(); fetchUsersMock.mockResolvedValue({ items: [ { id: "user-1", name: "Alice", email: "alice@example.com", role: "user", status: "active", }, ], total: 1, }); exportUsersCSVMock.mockResolvedValue({ blob: new Blob(["email,name\nalice@example.com,Alice\n"], { type: "text/csv", }), filename: "users_export_20260609.csv", }); updateUserMock.mockResolvedValue({}); vi.spyOn(window.URL, "createObjectURL").mockReturnValue( "blob:tenant-users-export", ); vi.spyOn(window.URL, "revokeObjectURL").mockImplementation(() => {}); bulkUpdateUsersMock.mockResolvedValue({ results: [] }); }); it("exports only the currently opened tenant users by tenant slug", async () => { renderTenantUsersPage(); await screen.findByText("Alice"); fireEvent.click(screen.getByTestId("tenant-users-export-menu-item")); await waitFor(() => { expect(exportUsersCSVMock).toHaveBeenCalledWith( "", "tech-planning", false, ); }); }); it("queues searched users and adds all queued users to the tenant at once", async () => { fetchUsersMock .mockResolvedValueOnce({ items: [], total: 0 }) .mockResolvedValueOnce({ items: [ { id: "user-2", name: "Bob", email: "bob@example.com", role: "user", status: "active", }, { id: "user-3", name: "Carol", email: "carol@example.com", role: "user", status: "active", }, ], total: 2, }) .mockResolvedValue({ items: [], total: 0 }); updateUserMock.mockResolvedValue({}); renderTenantUsersPage(); const addButton = await screen.findByTestId( "tenant-member-add-existing-btn", ); await waitFor(() => expect(addButton).not.toBeDisabled()); fireEvent.click(addButton); fireEvent.change(screen.getByTestId("tenant-member-search-input"), { target: { value: "bo" }, }); fireEvent.click(await screen.findByText("Bob")); fireEvent.click(await screen.findByText("Carol")); expect(screen.getByTestId("tenant-member-add-queue")).toHaveTextContent( "Bob", ); expect(screen.getByTestId("tenant-member-add-queue")).toHaveTextContent( "Carol", ); fireEvent.click(screen.getByTestId("tenant-member-add-submit-btn")); await waitFor(() => { expect(bulkUpdateUsersMock).toHaveBeenCalledWith({ userIds: ["user-2", "user-3"], tenantSlug: "tech-planning", isAddTenant: true, }); }); expect(updateUserMock).not.toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ isAddTenant: true }), ); }); it("queues orgfront multi picker users and adds them with one bulk request", async () => { fetchUsersMock .mockResolvedValueOnce({ items: [ { id: "existing-user", name: "Existing", email: "existing@example.com", role: "user", status: "active", }, ], total: 1, }) .mockResolvedValue({ items: [], total: 0 }); renderTenantUsersPage(); const addButton = await screen.findByTestId( "tenant-member-add-existing-btn", ); await waitFor(() => expect(addButton).not.toBeDisabled()); fireEvent.click(addButton); const picker = await screen.findByTitle("조직도에서 구성원 선택"); expect(decodeURIComponent(picker.getAttribute("src") ?? "")).toContain( "/embed/picker?mode=multiple&select=user", ); fireEvent( window, new MessageEvent("message", { data: { type: "orgfront:picker:confirm", payload: { mode: "multiple", selections: [ { type: "tenant", id: "team-1", name: "플랫폼팀" }, { type: "user", id: "picked-user-1", name: "Picked One", email: "picked1@example.com", }, { type: "user", id: "picked-user-2", name: "Picked Two", }, { type: "user", id: "existing-user", name: "Existing", email: "existing@example.com", }, ], }, }, }), ); expect(screen.getByTestId("tenant-member-add-queue")).toHaveTextContent( "Picked One", ); expect(screen.getByTestId("tenant-member-add-queue")).toHaveTextContent( "Picked Two", ); expect(screen.getByTestId("tenant-member-add-queue")).not.toHaveTextContent( "Existing", ); fireEvent.click(screen.getByTestId("tenant-member-add-submit-btn")); await waitFor(() => { expect(bulkUpdateUsersMock).toHaveBeenCalledWith({ userIds: ["picked-user-1", "picked-user-2"], tenantSlug: "tech-planning", isAddTenant: true, }); }); }); it("removes a member from the tenant and invalidates the user detail cache", async () => { const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true); const { queryClient } = renderTenantUsersPage(); queryClient.setQueryData(["user", "user-1"], { id: "user-1", name: "Alice", }); await screen.findByText("Alice"); fireEvent.click(screen.getByTestId("tenant-member-remove-user-1")); await waitFor(() => { expect(updateUserMock).toHaveBeenCalledWith("user-1", { tenantSlug: "tech-planning", isRemoveTenant: true, }); }); expect(queryClient.getQueryState(["user", "user-1"])?.isInvalidated).toBe( true, ); confirmSpy.mockRestore(); }); });