import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { fireEvent, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createI18nMock } from "../../test/i18nMock"; import UserListPage from "./UserListPage"; const selectRenderCounter = vi.hoisted(() => ({ count: 0 })); const users = Array.from({ length: 200 }, (_, index) => ({ id: `user-${index}`, name: `User ${index}`, email: `user${index}@example.com`, phone: `010-${String(index).padStart(4, "0")}-0000`, role: "user", status: "active", tenantSlug: "hanmac", tenant: { id: "tenant-1", name: "한맥", slug: "hanmac" }, metadata: {}, createdAt: "2026-05-01T00:00:00Z", updatedAt: "2026-05-01T00:00:00Z", })); const fetchUsersMock = vi.hoisted(() => vi.fn()); const searchRenderBudgetMs = process.env.npm_lifecycle_event === "test:coverage" ? 500 : 200; vi.mock("../../lib/i18n", () => createI18nMock()); vi.mock("../../lib/adminApi", () => ({ fetchMe: vi.fn(async () => ({ id: "admin-user", role: "super_admin", name: "Admin", email: "admin@example.com", })), fetchAllTenants: vi.fn(async () => ({ items: [{ id: "tenant-1", name: "한맥", slug: "hanmac" }], total: 1, })), fetchTenant: vi.fn(async () => ({ id: "tenant-1", name: "한맥", slug: "hanmac", config: { userSchema: [] }, })), fetchUsers: fetchUsersMock, bulkCreateUsers: vi.fn(), bulkDeleteUsers: vi.fn(), bulkUpdateUsers: vi.fn(), deleteUser: vi.fn(), exportUsersCSV: vi.fn(), updateUser: vi.fn(), })); vi.mock("../../components/ui/select", () => ({ Select: ({ children }: { children: React.ReactNode }) => (
{children}
), SelectTrigger: ({ children, ...props }: React.ButtonHTMLAttributes) => { selectRenderCounter.count += 1; return ( ); }, SelectValue: () => , SelectContent: ({ children }: { children: React.ReactNode }) => (
{children}
), SelectItem: ({ children, value: _value, }: { children: React.ReactNode; value: string; }) =>
{children}
, })); function renderUserListPage() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); return render( , ); } function createDeferred() { let resolve: (value: T) => void = () => {}; const promise = new Promise((promiseResolve) => { resolve = promiseResolve; }); return { promise, resolve }; } describe("UserListPage search rendering", () => { beforeEach(() => { selectRenderCounter.count = 0; fetchUsersMock.mockReset(); fetchUsersMock.mockImplementation( async (_limit: number, _offset: number, search?: string) => { const normalizedSearch = search?.trim().toLowerCase(); const items = normalizedSearch ? users.filter((user) => `${user.name} ${user.email}` .toLowerCase() .includes(normalizedSearch), ) : users; return { items, total: items.length }; }, ); }); it("does not rerender user table controls while typing a draft search", async () => { renderUserListPage(); await screen.findByText("User 0"); const searchInput = screen.getByPlaceholderText("이름 또는 이메일 검색"); const renderCountBeforeTyping = selectRenderCounter.count; fireEvent.change(searchInput, { target: { value: "u" } }); expect(searchInput).toHaveValue("u"); expect(selectRenderCounter.count).toBe(renderCountBeforeTyping); }); it("keeps rendered row controls below the full 200-user result set", async () => { renderUserListPage(); await screen.findByText("User 0"); expect(screen.getAllByTestId(/^user-status-select-/).length).toBeLessThan( 200, ); }); it("renders compact vertically centered user table headers", async () => { renderUserListPage(); await screen.findByText("User 0"); const nameHeader = screen.getByRole("columnheader", { name: /이름/ }); const content = nameHeader.firstElementChild; expect(nameHeader).toHaveClass("h-9", "py-1", "align-middle", "text-xs"); expect(content).toHaveClass("flex", "h-full", "items-center"); }); it("centers the initial loading message across the user table", async () => { const deferred = createDeferred<{ items: typeof users; total: number }>(); fetchUsersMock.mockReturnValueOnce(deferred.promise); renderUserListPage(); const loadingCell = await screen.findByTestId("user-table-loading-cell"); expect(loadingCell).toHaveClass( "flex", "items-center", "justify-center", "text-center", ); expect(loadingCell).toHaveStyle({ gridColumn: "1 / -1" }); deferred.resolve({ items: users, total: users.length }); }); it("renders a 200-user search result update within 200ms after search submit", async () => { renderUserListPage(); await screen.findByText("User 0"); const searchInput = screen.getByPlaceholderText("이름 또는 이메일 검색"); const startedAt = performance.now(); fireEvent.change(searchInput, { target: { value: "user 19" } }); fireEvent.keyDown(searchInput, { key: "Enter" }); expect(await screen.findByText("User 19")).toBeInTheDocument(); expect(screen.queryByText("User 0")).not.toBeInTheDocument(); expect(performance.now() - startedAt).toBeLessThan(searchRenderBudgetMs); }); it("keeps rendered form fields identifiable for browser autofill diagnostics", async () => { const { container } = renderUserListPage(); await screen.findByText("User 0"); const anonymousFields = Array.from( container.querySelectorAll("input, select, textarea"), ).filter( (field) => !field.getAttribute("id")?.trim() && !field.getAttribute("name")?.trim(), ); expect(anonymousFields).toHaveLength(0); }); });