118 lines
5.1 KiB
TypeScript
118 lines
5.1 KiB
TypeScript
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<typeof api>;
|
|
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<React.ComponentProps<typeof NotificationsPopover>> = {}) {
|
|
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(<I18nProvider><NotificationsPopover {...props} /></I18nProvider>);
|
|
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(<I18nProvider><NotificationsPopover {...props} anchorEl={null} /></I18nProvider>);
|
|
mockedApi.get.mockResolvedValue({ data: [] } as any);
|
|
rerender(<I18nProvider><NotificationsPopover {...props} anchorEl={anchor} /></I18nProvider>);
|
|
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(<I18nProvider><NotificationsPopover {...props} anchorEl={null} /></I18nProvider>);
|
|
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();
|
|
});
|