83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
import React from "react";
|
|
import "@testing-library/jest-dom";
|
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
|
|
import { api } from "./api";
|
|
import OperationsPage from "./views/OperationsPage";
|
|
|
|
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 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,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockedApi.get.mockImplementation((url: string) => Promise.resolve({
|
|
data: url.startsWith("/operations") ? [operation] : [notification],
|
|
} as any));
|
|
mockedApi.post.mockResolvedValue({ data: {} } as any);
|
|
mockedApi.delete.mockResolvedValue({ data: {} } as any);
|
|
});
|
|
|
|
test("shows persistent operation and notification states with accessible actions", async () => {
|
|
render(<OperationsPage />);
|
|
|
|
expect(await screen.findByText("strategy_snapshot")).toBeInTheDocument();
|
|
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 } = render(<OperationsPage />);
|
|
expect(await screen.findByText("No notifications.")).toBeInTheDocument();
|
|
expect(screen.getByText("No background operations yet.")).toBeInTheDocument();
|
|
unmount();
|
|
|
|
mockedApi.get.mockRejectedValue(new Error("offline"));
|
|
render(<OperationsPage />);
|
|
expect(await screen.findByRole("alert")).toHaveTextContent("Operations could not be loaded.");
|
|
});
|