Files
jobtrackingapp/job-tracker-ui/src/operations-page.test.tsx
T

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.");
});