import React from "react"; import "@testing-library/jest-dom"; import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { api, getApiErrorMessage } from "./api"; import { I18nProvider } from "./i18n/I18nProvider"; import { ToastProvider } from "./toast"; import AdminAuditPage from "./views/AdminAuditPage"; import { translations } from "./i18n/translations"; const mockToast = jest.fn(); let mockSession = "one"; jest.mock("./sessionBoundary", () => ({ checkSessionCookie: jest.fn(), getSessionContext: () => mockSession, isSessionInvalidated: () => false })); jest.mock("./toast", () => ({ ...jest.requireActual("./toast"), useToast: () => ({ toast: mockToast }) })); jest.mock("./api", () => ({ api: { get: jest.fn(), post: jest.fn() }, getApiErrorMessage: jest.fn((_error, fallback) => fallback), })); const mockedApi = api as jest.Mocked; function renderPage() { return render(); } beforeEach(() => { jest.clearAllMocks(); window.localStorage.clear(); mockSession = "one"; }); const row = { id: 7, type: "Deleted", at: "2026-10-20T12:00:00Z", jobApplicationId: 42, note: "Owner authored text" }; function deferred() { let resolve!: (value: T) => void; const promise = new Promise(done => { resolve = done; }); return { promise, resolve }; } test("[P7-10 audit] retains shared heading hierarchy and a named keyboard scroll region", async () => { mockedApi.get.mockResolvedValue({ data: [row] } as any); renderPage(); await screen.findByRole("button", { name: "Undo" }); expect(screen.getByRole("heading", { name: "Audit log", level: 2 })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: translations.en.adminAuditEvents, level: 3 })).toBeInTheDocument(); const region = screen.getByRole("region", { name: "Audit log" }); expect(region).toHaveAttribute("tabindex", "0"); expect(within(region).getByRole("table")).toHaveAccessibleName("Audit log"); }); test("[P7-10 audit] exact Undo allowlist and deleted-only Restore retain endpoint contracts", async () => { const types = ["StatusChanged", "FollowUpSet", "ResponseUpdated", "Deleted", "Restored", "Created", "NoteAdded"]; mockedApi.get.mockResolvedValue({ data: types.map((type, index) => ({ ...row, type, id: index + 1 })) } as any); renderPage(); expect(await screen.findAllByRole("button", { name: "Undo" })).toHaveLength(5); expect(screen.getAllByRole("button", { name: "Restore" })).toHaveLength(1); }); test("[P7-10 audit] Undo and Restore serialize across rows, retaining focus on pending table", async () => { const held = deferred(); mockedApi.get.mockResolvedValue({ data: [row, { ...row, id: 8 }] } as any); mockedApi.post.mockReturnValue(held.promise); renderPage(); const undo = (await screen.findAllByRole("button", { name: "Undo" }))[0]; act(() => undo.focus()); act(() => { fireEvent.click(undo); fireEvent.click(undo); fireEvent.click(screen.getAllByRole("button", { name: "Restore" })[1]); }); expect(mockedApi.post).toHaveBeenCalledTimes(1); expect(mockedApi.post).toHaveBeenCalledWith("/admin/audit/7/undo", {}); expect(screen.getByRole("region", { name: "Audit log" })).toHaveFocus(); expect(screen.getByRole("region", { name: "Audit log" })).toHaveAttribute("aria-busy", "true"); expect(screen.getAllByRole("button", { name: "Undo" })[1]).toBeDisabled(); await act(async () => held.resolve({ data: { ok: true } })); }); test("[P7-10 audit] a committed Restore with failed refresh recovers through GET only", async () => { mockedApi.get.mockResolvedValueOnce({ data: [row] } as any).mockRejectedValueOnce(new Error("offline")).mockResolvedValue({ data: [] } as any); mockedApi.post.mockResolvedValue({ data: {} } as any); renderPage(); fireEvent.click(await screen.findByRole("button", { name: "Restore" })); expect(await screen.findByText(translations.en.adminAuditLoadFailed)).toBeInTheDocument(); expect(mockedApi.post).toHaveBeenCalledWith("/jobapplications/42/restore", {}); const retry = screen.getByRole("button", { name: "Retry" }); act(() => retry.focus()); fireEvent.click(retry); expect(await screen.findByText("No events.")).toBeInTheDocument(); expect(screen.getByRole("region", { name: "Audit log" })).toHaveFocus(); expect(mockedApi.post).toHaveBeenCalledTimes(1); expect(mockedApi.get).toHaveBeenCalledTimes(3); }); test.each(["unmount", "session"])("[P7-10 audit] late Undo after %s cannot notify or refresh", async boundary => { const held = deferred(); mockedApi.post.mockReturnValue(held.promise); mockedApi.get.mockResolvedValue({ data: [row] } as any); const view = renderPage(); fireEvent.click(await screen.findByRole("button", { name: "Undo" })); if (boundary === "unmount") view.unmount(); else mockSession = "two"; await act(async () => held.resolve({ data: { message: "Undone" } })); expect(mockToast).not.toHaveBeenCalled(); expect(mockedApi.get).toHaveBeenCalledTimes(1); }); test("[P7-10 audit] pending failure uses safe current-language feedback without a locale GET", async () => { const held = deferred(); mockedApi.post.mockReturnValue(held.promise); mockedApi.get.mockResolvedValue({ data: [row] } as any); renderPage(); fireEvent.click(await screen.findByRole("button", { name: "Undo" })); act(() => window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: "nb" }))); const error = { response: { data: { title: "proxy error" } } }; await act(async () => held.resolve(Promise.reject(error))); await waitFor(() => expect(mockToast).toHaveBeenCalledWith(translations.nb.adminAuditUndoFailed, "error")); expect(getApiErrorMessage).toHaveBeenCalledWith(error, translations.nb.adminAuditUndoFailed); expect(mockedApi.get).toHaveBeenCalledTimes(1); }); test("distinguishes an unavailable audit log from an empty audit log and retries", async () => { mockedApi.get.mockRejectedValueOnce(new Error("offline")); renderPage(); expect(screen.getByRole("heading", { name: "Audit log", level: 2 })).toBeInTheDocument(); expect(await screen.findByText("Unable to load audit events")).toBeInTheDocument(); expect(screen.queryByText("No events.")).not.toBeInTheDocument(); mockedApi.get.mockResolvedValueOnce({ data: [] } as any); fireEvent.click(screen.getByRole("button", { name: "Retry" })); expect(await screen.findByText("No events.")).toBeInTheDocument(); }); test("localizes audit actions in Bokmål without translating stored event data", async () => { window.localStorage.setItem("uiLanguage", "nb-NO"); mockedApi.get.mockResolvedValueOnce({ data: [{ id: 7, type: "Deleted", at: "2026-08-29T12:00:00Z", jobApplicationId: 42, companyName: "Example AS", note: "User-authored note", }] } as any); renderPage(); expect(await screen.findByRole("button", { name: "Angre" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Gjenopprett" })).toBeInTheDocument(); expect(screen.getByText("Example AS - Jobb #42")).toBeInTheDocument(); expect(screen.getByText("User-authored note")).toBeInTheDocument(); });