import React from "react"; import "@testing-library/jest-dom"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { api } from "./api"; import NotificationsPopover from "./components/NotificationsPopover"; import { I18nProvider } from "./i18n/I18nProvider"; 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; const notification = { id: "22222222-2222-2222-2222-222222222222", operationId: null, kind: "operation_failed", title: "CV export failed", message: "Review the operation and try again.", linkPath: "/operations", createdAtUtc: "2026-08-15T12:00:00Z", readAtUtc: null, }; function renderPopover(overrides: Partial> = {}) { const anchor = document.createElement("button"); document.body.appendChild(anchor); const props = { anchorEl: anchor, onClose: jest.fn(), onNavigate: jest.fn(), onChanged: jest.fn(), ...overrides, }; const result = render(); return { ...result, props, anchor }; } beforeEach(() => { jest.clearAllMocks(); mockedApi.get.mockResolvedValue({ data: [notification] } as any); mockedApi.post.mockResolvedValue({ data: {} } as any); mockedApi.delete.mockResolvedValue({ data: {} } as any); }); test("shows notifications and updates read and dismissed state in place", async () => { const { props } = renderPopover(); expect(await screen.findByText("CV export failed")).toBeInTheDocument(); expect(mockedApi.get).toHaveBeenCalledWith("/notifications?limit=10", expect.objectContaining({ signal: expect.any(AbortSignal) })); fireEvent.click(screen.getByRole("button", { name: "Mark read" })); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith(`/notifications/${notification.id}/read`)); expect(props.onChanged).toHaveBeenCalledTimes(1); await waitFor(() => expect(screen.queryByRole("button", { name: "Mark read" })).not.toBeInTheDocument()); fireEvent.click(screen.getByRole("button", { name: "Dismiss" })); await waitFor(() => expect(mockedApi.delete).toHaveBeenCalledWith(`/notifications/${notification.id}`)); await waitFor(() => expect(screen.queryByText("CV export failed")).not.toBeInTheDocument()); expect(props.onChanged).toHaveBeenCalledTimes(2); }); test("opens a notification destination instead of routing the bell itself", async () => { const { props } = renderPopover(); fireEvent.click(await screen.findByRole("button", { name: "Open" })); await waitFor(() => expect(props.onNavigate).toHaveBeenCalledWith("/operations")); expect(mockedApi.post).toHaveBeenCalledWith(`/notifications/${notification.id}/read`); expect(props.onClose).toHaveBeenCalledTimes(1); }); test("shows an honest empty state", async () => { mockedApi.get.mockResolvedValue({ data: [] } as any); renderPopover(); expect(await screen.findByText("You're all caught up")).toBeInTheDocument(); }); test("[P5-02 PRES-E09] a closed notification request cannot populate a later account's panel", async () => { let resolveOld!: (value: unknown) => void; mockedApi.get.mockImplementationOnce(() => new Promise(resolve => { resolveOld = resolve; }) as never); const { rerender, props, anchor } = renderPopover(); await waitFor(() => expect(resolveOld).toBeDefined()); rerender(); mockedApi.get.mockResolvedValue({ data: [] } as any); rerender(); await act(async () => resolveOld({ data: [notification] })); expect(await screen.findByText("You're all caught up")).toBeInTheDocument(); expect(screen.queryByText("CV export failed")).not.toBeInTheDocument(); }); test("[P5-02 PRES-E09] a delayed read cannot navigate from a closed earlier-account panel", async () => { let resolveRead!: (value: unknown) => void; mockedApi.post.mockImplementationOnce(() => new Promise(resolve => { resolveRead = resolve; }) as never); const { rerender, props } = renderPopover(); fireEvent.click(await screen.findByRole("button", { name: "Open" })); await waitFor(() => expect(resolveRead).toBeDefined()); rerender(); await act(async () => resolveRead({ data: {} })); expect(props.onNavigate).not.toHaveBeenCalled(); }); test("[P5-02 PRES-S02] failed notification load offers Retry without claiming an empty inbox", async () => { mockedApi.get.mockRejectedValueOnce(new Error("offline")); renderPopover(); expect(await screen.findByRole("alert")).toHaveTextContent("Notifications could not be loaded"); expect(screen.queryByText("You're all caught up")).not.toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Retry" })); expect(await screen.findByText("CV export failed")).toBeInTheDocument(); });