import { createRoot, type Root } from "react-dom/client"; import { act } from "react-dom/test-utils"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { CommonAuditLog } from "../../audit"; import { AuditLogTable } from "./AuditLogTable"; const roots: Root[] = []; afterEach(() => { for (const root of roots.splice(0)) { act(() => { root.unmount(); }); } vi.restoreAllMocks(); document.body.innerHTML = ""; }); function renderTable(props: Parameters[0]) { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); roots.push(root); act(() => { root.render(); }); return { container }; } const logs: CommonAuditLog[] = [ { event_id: "evt-1", timestamp: "2026-05-28T06:07:18.000Z", user_id: "user-1", event_type: "CLIENT_UPDATE", status: "success", ip_address: "127.0.0.1", user_agent: "Vitest", device_id: "device-1", details: JSON.stringify({ request_id: "req-1", method: "POST", path: "/api/v1/clients", latency_ms: 120, tenant_id: "tenant-1", actor_id: "user-1", action: "업데이트", target_id: "client-a", before: { status: "inactive" }, after: { status: "active" }, }), }, ]; describe("AuditLogTable", () => { it("renders loading and empty states", () => { const { container: loadingContainer } = renderTable({ logs: [], t: (key, fallback) => fallback ?? key, loading: true, hasNextPage: false, isFetchingNextPage: false, onLoadMore: vi.fn(), }); expect(loadingContainer.textContent).toContain("Loading audit logs..."); const { container: emptyContainer } = renderTable({ logs: [], t: (key, fallback) => fallback ?? key, loading: false, hasNextPage: false, isFetchingNextPage: false, onLoadMore: vi.fn(), }); expect(emptyContainer.textContent).toContain("No audit logs found."); expect(emptyContainer.textContent).toContain("End of audit feed"); }); it("renders rows, expands details, copies fields, and loads more", async () => { const writeText = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true, }); const onLoadMore = vi.fn(); const { container } = renderTable({ logs, t: (key, fallback, vars) => { let text = fallback ?? key; for (const [name, value] of Object.entries(vars ?? {})) { text = text.replaceAll(`{{${name}}}`, String(value)); } return text; }, loading: false, hasNextPage: true, isFetchingNextPage: false, onLoadMore, }); expect(container.textContent).toContain("user-1"); expect(container.textContent).toContain("업데이트"); expect(container.textContent).toContain("client-a"); expect(container.textContent).toContain("success"); const buttons = Array.from(container.querySelectorAll("button")); const actorCopyButton = buttons.find( (button) => button.getAttribute("aria-label") === "Copy User ID", ); const targetCopyButton = buttons.find( (button) => button.getAttribute("aria-label") === "Copy Client ID", ); const expandButton = buttons.find( (button) => !button.getAttribute("aria-label") && !button.textContent, ); const loadMoreButton = buttons.find( (button) => button.textContent === "Load more", ); expect(actorCopyButton).toBeTruthy(); expect(targetCopyButton).toBeTruthy(); expect(expandButton).toBeTruthy(); expect(loadMoreButton).toBeTruthy(); await act(async () => { actorCopyButton?.dispatchEvent( new MouseEvent("click", { bubbles: true }), ); targetCopyButton?.dispatchEvent( new MouseEvent("click", { bubbles: true }), ); expandButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(writeText).toHaveBeenCalledWith("user-1"); expect(writeText).toHaveBeenCalledWith("client-a"); expect(container.textContent).toContain("Request ID · req-1"); expect(container.textContent).toContain("Actor"); expect(container.textContent).toContain("Result"); await act(async () => { loadMoreButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(onLoadMore).toHaveBeenCalledTimes(1); }); });