import React from "react"; import "@testing-library/jest-dom"; import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { api } from "./api"; import { I18nProvider } from "./i18n/I18nProvider"; import OperationsPage from "./views/OperationsPage"; import { translations } from "./i18n/translations"; jest.mock("./api", () => ({ api: { get: jest.fn(), post: jest.fn(), delete: jest.fn(), interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }, }, getApiErrorMessage: (_error: unknown, fallback?: string) => fallback || "Request failed.", })); const mockedApi = api as jest.Mocked; function renderPage() { return render(); } const operation = { id: "11111111-1111-1111-1111-111111111111", taskType: "strategy_snapshot", status: "running", createdAtUtc: "2026-08-02T12:00:00Z", progressStage: "Analysing role", progressPercent: 40, canCancel: true, canRetry: false, }; const notification = { id: "22222222-2222-2222-2222-222222222222", operationId: operation.id, kind: "operation_failed", title: "Operation failed", message: "A background operation failed. Review it for details.", createdAtUtc: "2026-08-02T12:05:00Z", readAtUtc: null, }; let serverOperations: any[]; let serverNotifications: any[]; function deferred() { let resolve!: (value: T) => void; let reject!: (cause: unknown) => void; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; } async function language(value: "en" | "nb") { await act(async () => window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: value }))); } function response(url: string) { return { data: url.startsWith("/operations") ? serverOperations : serverNotifications } as any; } function activity() { return screen.getByRole("region", { name: "Recent activity" }); } beforeEach(() => { jest.clearAllMocks(); window.localStorage.clear(); document.documentElement.lang = "en"; serverOperations = [{ ...operation }]; serverNotifications = [{ ...notification }]; mockedApi.get.mockReset().mockImplementation((url: string) => Promise.resolve(response(url))); mockedApi.post.mockReset().mockImplementation((url: string) => { if (url.startsWith("/operations")) { serverOperations = serverOperations.map(item => ({ ...item, status: url.endsWith("/cancel") ? "cancelled" : "queued", canCancel: url.endsWith("/retry"), canRetry: url.endsWith("/cancel") })); return Promise.resolve({ data: serverOperations[0] } as any); } serverNotifications = serverNotifications.map(item => ({ ...item, readAtUtc: "2026-08-02T12:06:00Z" })); return Promise.resolve({ data: undefined } as any); }); mockedApi.delete.mockReset().mockImplementation(() => { serverNotifications = []; return Promise.resolve({ data: undefined } as any); }); }); afterEach(() => { jest.useRealTimers(); jest.restoreAllMocks(); }); test("shows persistent operation and notification states with accessible actions", async () => { renderPage(); expect(await screen.findByText("strategy_snapshot")).toBeInTheDocument(); const task = screen.getByRole("article", { name: operation.taskType }); expect(task).toHaveAccessibleName(operation.taskType); expect(task).toHaveAttribute("data-operation-id", operation.id); expect(screen.getByText("Analysing role")).toBeInTheDocument(); expect(screen.getByRole("progressbar", { name: "strategy_snapshot progress" })).toHaveAttribute("aria-valuenow", "40"); expect(screen.getByText("Operation failed")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Cancel" })); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith(`/operations/${operation.id}/cancel`)); const markRead = screen.getByRole("button", { name: "Mark read" }); await waitFor(() => expect(markRead).toBeEnabled()); fireEvent.click(markRead); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith(`/notifications/${notification.id}/read`)); const dismiss = screen.getByRole("button", { name: "Dismiss" }); await waitFor(() => expect(dismiss).toBeEnabled()); fireEvent.click(dismiss); await waitFor(() => expect(mockedApi.delete).toHaveBeenCalledWith(`/notifications/${notification.id}`)); }); test("shows honest empty and failure states", async () => { mockedApi.get.mockResolvedValueOnce({ data: [] } as any).mockResolvedValueOnce({ data: [] } as any); const { unmount } = renderPage(); expect(await screen.findByText("No notifications.")).toBeInTheDocument(); expect(screen.getByText("No background operations yet.")).toBeInTheDocument(); unmount(); mockedApi.get.mockRejectedValue(new Error("offline")); renderPage(); expect(await screen.findByRole("alert")).toHaveTextContent("Operations could not be loaded."); expect(screen.queryByText("No notifications.")).not.toBeInTheDocument(); expect(screen.queryByText("No background operations yet.")).not.toBeInTheDocument(); }); test("localizes operation controls and empty states in Norwegian Bokmål", async () => { window.localStorage.setItem("uiLanguage", "nb"); mockedApi.get.mockResolvedValue({ data: [] } as any); renderPage(); expect(await screen.findByText("Ingen varsler.")).toBeInTheDocument(); expect(screen.getByText("Ingen bakgrunnsoperasjoner ennå.")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Oppdater" })).toBeInTheDocument(); }); test("[P7-08 operations] initial failure offers single-flight GET Retry without false empty or writes", async () => { mockedApi.get.mockRejectedValue(new Error("offline")); renderPage(); expect(await screen.findByRole("alert")).toHaveTextContent("Operations could not be loaded."); expect((mockedApi.get.mock.calls[0][1]?.signal as AbortSignal).aborted).toBe(true); expect(screen.queryByText("No notifications.")).not.toBeInTheDocument(); expect(screen.queryByText("No background operations yet.")).not.toBeInTheDocument(); const pending = deferred(); mockedApi.get.mockImplementation(() => pending.promise); const retry = screen.getByRole("button", { name: "Retry" }); act(() => { retry.focus(); fireEvent.click(retry); fireEvent.click(retry); }); expect(mockedApi.get).toHaveBeenCalledTimes(4); expect(mockedApi.post).not.toHaveBeenCalled(); expect(mockedApi.delete).not.toHaveBeenCalled(); await act(async () => pending.resolve({ data: [] })); expect(await screen.findByText("No notifications.")).toBeVisible(); expect(screen.getByRole("heading", { name: "Recent activity", level: 2 })).toHaveFocus(); expect(activity()).toHaveAttribute("aria-busy", "false"); expect(mockedApi.get).toHaveBeenCalledWith("/operations?limit=50", expect.objectContaining({ signal: expect.any(AbortSignal) })); expect(mockedApi.get).toHaveBeenCalledWith("/notifications?limit=50", expect.objectContaining({ signal: expect.any(AbortSignal) })); }); test("[P7-08 operations] cached refresh failure preserves known rows and does not fabricate empty results", async () => { renderPage(); expect(await screen.findByText(operation.taskType)).toBeVisible(); mockedApi.get.mockRejectedValue(new Error("offline")); fireEvent.click(screen.getByRole("button", { name: "Refresh" })); expect(await screen.findByRole("alert")).toHaveTextContent("Operations could not be loaded."); expect(screen.getByText(operation.taskType)).toBeVisible(); expect(screen.getByText(notification.title)).toBeVisible(); expect(screen.queryByText("No notifications.")).not.toBeInTheDocument(); expect(screen.queryByText("No background operations yet.")).not.toBeInTheDocument(); expect(mockedApi.post).not.toHaveBeenCalled(); expect(mockedApi.delete).not.toHaveBeenCalled(); }); test.each(["Cancel", "Retry", "Mark read", "Dismiss"])("[P7-08 operations] same-render duplicate %s reserves exactly one mutation and blocks competing work", async label => { if (label === "Retry") serverOperations = [{ ...operation, status: "cancelled", canCancel: false, canRetry: true }]; const pending = deferred(); mockedApi.post.mockImplementation(() => pending.promise); mockedApi.delete.mockImplementation(() => pending.promise); renderPage(); const button = await screen.findByRole("button", { name: label }); act(() => { fireEvent.click(button); fireEvent.click(button); fireEvent.click(screen.getByRole("button", { name: label === "Dismiss" ? "Mark read" : "Dismiss" })); }); expect(mockedApi.post.mock.calls.length + mockedApi.delete.mock.calls.length).toBe(1); expect(screen.getByRole("button", { name: "Refresh" })).toBeDisabled(); await act(async () => pending.resolve({ data: label === "Cancel" || label === "Retry" ? { ...serverOperations[0], canCancel: false, canRetry: false } : undefined })); await waitFor(() => expect(screen.getByRole("button", { name: "Refresh" })).toBeEnabled()); }); test("[P7-08 operations] language and ordinary rerenders reformat cached data without refetch or polling restart", async () => { const intervals = jest.spyOn(window, "setInterval"); const cleared = jest.spyOn(window, "clearInterval"); serverOperations = [{ ...operation, taskType: "strategy.snapshot", status: "waiting_for_retry" }]; const { rerender } = renderPage(); expect(await screen.findByText("Strategy snapshot")).toBeVisible(); expect(screen.getByText("Waiting to retry")).toBeVisible(); expect(mockedApi.get).toHaveBeenCalledTimes(2); const starts = intervals.mock.calls.length; const pollId = intervals.mock.results[intervals.mock.calls.findIndex(([, delay]) => delay === 15000)].value; await language("nb"); expect(screen.getByText("Strategioversikt")).toBeVisible(); expect(screen.getByText("Venter på nytt forsøk")).toBeVisible(); expect(screen.getByText(`Opprettet ${new Date(operation.createdAtUtc).toLocaleString("nb-NO")}`)).toBeVisible(); rerender(); expect(mockedApi.get).toHaveBeenCalledTimes(2); expect(intervals).toHaveBeenCalledTimes(starts); expect(cleared).not.toHaveBeenCalledWith(pollId); }); test("[P7-08 operations] 15-second polling is single-flight and resumes after the held pair completes", async () => { jest.useFakeTimers(); renderPage(); await act(async () => {}); expect(mockedApi.get).toHaveBeenCalledTimes(2); const pending = deferred(); mockedApi.get.mockImplementation(() => pending.promise); await act(async () => jest.advanceTimersByTime(15000)); expect(mockedApi.get).toHaveBeenCalledTimes(4); await act(async () => jest.advanceTimersByTime(30000)); expect(mockedApi.get).toHaveBeenCalledTimes(4); await act(async () => pending.resolve({ data: [] })); mockedApi.get.mockImplementation((url: string) => Promise.resolve(response(url))); await act(async () => jest.advanceTimersByTime(15000)); expect(mockedApi.get).toHaveBeenCalledTimes(6); expect(mockedApi.post).not.toHaveBeenCalled(); }); test("[P7-08 operations] mutation invalidates a held old poll before adopting the returned same-ID DTO", async () => { jest.useFakeTimers(); renderPage(); await act(async () => {}); const oldOperations = deferred(); const oldNotices = deferred(); mockedApi.get.mockImplementationOnce(() => oldOperations.promise).mockImplementationOnce(() => oldNotices.promise); await act(async () => jest.advanceTimersByTime(15000)); const signal = mockedApi.get.mock.calls[2][1]?.signal as AbortSignal; expect(signal).toBeInstanceOf(AbortSignal); const post = deferred(); const refresh = deferred(); mockedApi.post.mockImplementationOnce(() => post.promise); mockedApi.get.mockImplementation(() => refresh.promise); fireEvent.click(screen.getByRole("button", { name: "Cancel" })); expect(signal.aborted).toBe(true); const cancelled = { ...operation, status: "cancelled", canCancel: false, canRetry: true }; await act(async () => post.resolve({ data: cancelled })); expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument(); expect(screen.getByRole("article", { name: operation.taskType })).toHaveTextContent("Cancelled"); await act(async () => { oldOperations.resolve({ data: [{ ...operation, progressStage: "Stale old poll" }] }); oldNotices.resolve({ data: [notification] }); }); expect(screen.queryByText("Stale old poll")).not.toBeInTheDocument(); await act(async () => refresh.resolve({ data: [] })); expect(mockedApi.post).toHaveBeenCalledTimes(1); }); test("[P7-08 operations] committed cancel plus failed refresh announces truth and GET Retry cannot replay or unlock stale writes", async () => { const changed = jest.fn(); window.addEventListener("notifications-changed", changed); try { renderPage(); const cancel = await screen.findByRole("button", { name: "Cancel" }); const cancelled = { ...operation, status: "cancelled", canCancel: false, canRetry: true }; mockedApi.post.mockResolvedValueOnce({ data: cancelled } as any); mockedApi.get.mockRejectedValue(new Error("read offline")); act(() => cancel.focus()); fireEvent.click(cancel); const failure = await screen.findByRole("alert"); expect(failure).toHaveTextContent("The action completed, but recent activity could not be refreshed. Refresh before taking another action."); const item = screen.getByRole("article", { name: operation.taskType }); expect(within(item).getByRole("button", { name: "Retry" })).toBeDisabled(); expect(item).toHaveTextContent("Cancelled"); expect(changed).toHaveBeenCalledTimes(1); expect(screen.getByRole("heading", { level: 2, name: "Recent activity" })).toHaveFocus(); serverOperations = [cancelled]; mockedApi.get.mockImplementation((url: string) => Promise.resolve(response(url))); fireEvent.click(within(failure).getByRole("button", { name: "Retry" })); await waitFor(() => expect(screen.queryByRole("alert")).not.toBeInTheDocument()); expect(mockedApi.post).toHaveBeenCalledTimes(1); expect(mockedApi.delete).not.toHaveBeenCalled(); expect(changed).toHaveBeenCalledTimes(1); expect(within(item).getByRole("button", { name: "Retry" })).toBeEnabled(); } finally { window.removeEventListener("notifications-changed", changed); } }); test.each(["Mark read", "Dismiss"])("[P7-08 operations] committed204 %s updates notices locally even when refresh fails without completing a task", async label => { const changed = jest.fn(); window.addEventListener("notifications-changed", changed); try { renderPage(); const action = await screen.findByRole("button", { name: label }); mockedApi.get.mockRejectedValue(new Error("offline")); fireEvent.click(action); expect(await screen.findByRole("alert")).toHaveTextContent("The action completed, but recent activity could not be refreshed."); expect(screen.queryByRole("button", { name: "Mark read" })).not.toBeInTheDocument(); if (label === "Dismiss") expect(screen.queryByText(notification.title)).not.toBeInTheDocument(); else expect(screen.getByText(notification.title)).toBeVisible(); expect(screen.getByRole("article", { name: operation.taskType })).toHaveTextContent("Running"); expect(screen.getByRole("button", { name: "Cancel" })).toBeDisabled(); expect(mockedApi.post.mock.calls.filter(([url]) => String(url).startsWith("/operations"))).toEqual([]); expect(changed).toHaveBeenCalledTimes(1); } finally { window.removeEventListener("notifications-changed", changed); } }); test("[P7-08 operations] failed POST preserves eligibility and current-locale recovery without false commit or new GET", async () => { const pending = deferred(); mockedApi.post.mockImplementationOnce(() => pending.promise); const changed = jest.fn(); window.addEventListener("notifications-changed", changed); try { renderPage(); const cancel = await screen.findByRole("button", { name: "Cancel" }); act(() => cancel.focus()); fireEvent.click(cancel); await language("nb"); expect(mockedApi.get).toHaveBeenCalledTimes(2); await act(async () => pending.reject(new Error("offline"))); expect(await screen.findByRole("alert")).toHaveTextContent(translations.nb.operationsActionFailed); expect(screen.getByRole("button", { name: translations.nb.cancel })).toBeEnabled(); expect(screen.getByRole("button", { name: translations.nb.cancel })).toHaveFocus(); expect(changed).not.toHaveBeenCalled(); expect(mockedApi.get).toHaveBeenCalledTimes(2); await language("en"); expect(screen.getByRole("alert")).toHaveTextContent(translations.en.operationsActionFailed); expect(mockedApi.get).toHaveBeenCalledTimes(2); } finally { window.removeEventListener("notifications-changed", changed); } }); test("[P7-08 operations] server action admission overrides displayed status and unknown metadata remains honest", async () => { serverOperations = [{ ...operation, taskType: "Unknown_Task_Type", status: "succeeded", progressStage: "arbitrary-stage", failureCategory: "unmapped_category", canCancel: true, canRetry: false }, { ...operation, id: "foreign-looking-local-fixture", taskType: "cv.process", status: "running", canCancel: false, canRetry: true }, { ...operation, id: "unmapped-prototype-code", taskType: "toString", status: "constructor", canCancel: false, canRetry: false }]; renderPage(); expect(await screen.findByText("Unknown_Task_Type")).toBeVisible(); const first = screen.getByRole("article", { name: "Unknown_Task_Type" }); expect(first).toHaveAttribute("data-operation-id", operation.id); expect(within(first).getByRole("button", { name: "Cancel" })).toBeEnabled(); expect(within(first).queryByRole("button", { name: "Retry" })).not.toBeInTheDocument(); expect(first).toHaveTextContent("arbitrary-stage"); expect(first).toHaveTextContent("unmapped category"); const second = screen.getByRole("article", { name: "CV processing" }); expect(second).toHaveAttribute("data-operation-id", "foreign-looking-local-fixture"); expect(within(second).queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument(); expect(within(second).getByRole("button", { name: "Retry" })).toBeEnabled(); const unknown = screen.getByRole("article", { name: "toString" }); expect(unknown).toHaveAttribute("data-operation-id", "unmapped-prototype-code"); expect(unknown).toHaveTextContent("constructor"); expect(within(unknown).queryAllByRole("button")).toHaveLength(0); }); test("[P7-08 operations] unmount aborts a pending read and late completion cannot fetch or publish", async () => { const pending = deferred(); mockedApi.get.mockImplementation(() => pending.promise); const { unmount } = renderPage(); const signal = mockedApi.get.mock.calls[0][1]?.signal as AbortSignal; expect(signal).toBeInstanceOf(AbortSignal); unmount(); expect(signal.aborted).toBe(true); await act(async () => pending.resolve({ data: [operation] })); expect(mockedApi.get).toHaveBeenCalledTimes(2); expect(mockedApi.post).not.toHaveBeenCalled(); }); test("[P7-08 operations] unmount during a write prevents its late follow-up GET, event and focus effects", async () => { const changed = jest.fn(); window.addEventListener("notifications-changed", changed); const pending = deferred(); mockedApi.post.mockImplementationOnce(() => pending.promise); try { const { unmount } = renderPage(); fireEvent.click(await screen.findByRole("button", { name: "Cancel" })); unmount(); await act(async () => pending.resolve({ data: { ...operation, status: "cancelled" } })); expect(mockedApi.get).toHaveBeenCalledTimes(2); expect(changed).not.toHaveBeenCalled(); } finally { window.removeEventListener("notifications-changed", changed); } }); test("[P7-08 operations] removing the initiating control restores meaningful focus while deliberate focus elsewhere stays put", async () => { const pending = deferred(); mockedApi.post.mockImplementationOnce(() => pending.promise); renderPage(); const mark = await screen.findByRole("button", { name: "Mark read" }); act(() => mark.focus()); fireEvent.click(mark); serverNotifications = [{ ...notification, readAtUtc: "2026-08-02T12:06:00Z" }]; await act(async () => pending.resolve({ data: undefined })); expect(screen.queryByRole("button", { name: "Mark read" })).not.toBeInTheDocument(); expect(screen.getByRole("heading", { level: 2, name: "Recent activity" })).toHaveFocus(); const next = deferred(); mockedApi.delete.mockImplementationOnce(() => next.promise); const dismiss = screen.getByRole("button", { name: "Dismiss" }); act(() => dismiss.focus()); fireEvent.click(dismiss); const external = document.createElement("button"); external.textContent = "Deliberate external target"; document.body.appendChild(external); act(() => external.focus()); serverNotifications = []; await act(async () => next.resolve({ data: undefined })); expect(external).toHaveFocus(); external.remove(); }); test("[P7-08 operations] accepted background polls preserve surviving and deliberate focus and recover removed focused controls", async () => { jest.useFakeTimers(); renderPage(); await act(async () => {}); const cancel = screen.getByRole("button", { name: "Cancel" }); act(() => cancel.focus()); serverOperations = [{ ...operation, progressStage: "Fresh progress with the same action" }]; await act(async () => jest.advanceTimersByTime(15000)); expect(cancel).toHaveFocus(); expect(cancel.isConnected).toBe(true); const external = document.createElement("button"); external.textContent = "Deliberate target outside recent activity"; document.body.appendChild(external); try { act(() => external.focus()); serverOperations = [{ ...operation, status: "cancelled", canCancel: false, canRetry: true }]; await act(async () => jest.advanceTimersByTime(15000)); expect(external).toHaveFocus(); expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument(); serverOperations = [{ ...operation }]; await act(async () => jest.advanceTimersByTime(15000)); expect(external).toHaveFocus(); act(() => screen.getByRole("button", { name: "Cancel" }).focus()); serverOperations = [{ ...operation, status: "cancelled", canCancel: false, canRetry: true }]; await act(async () => jest.advanceTimersByTime(15000)); expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument(); expect(screen.getByRole("heading", { level: 2, name: "Recent activity" })).toHaveFocus(); act(() => screen.getByRole("button", { name: "Mark read" }).focus()); serverNotifications = []; await act(async () => jest.advanceTimersByTime(15000)); expect(screen.queryByRole("button", { name: "Mark read" })).not.toBeInTheDocument(); expect(screen.getByRole("heading", { level: 2, name: "Recent activity" })).toHaveFocus(); expect(mockedApi.get).toHaveBeenCalledTimes(12); expect(mockedApi.post).not.toHaveBeenCalled(); expect(mockedApi.delete).not.toHaveBeenCalled(); } finally { external.remove(); } });