import React from "react"; import "@testing-library/jest-dom"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import ApplicationChecklist from "./components/ApplicationChecklist"; import { api } from "./api"; jest.mock("./api", () => ({ api: { get: jest.fn(), post: jest.fn(), patch: jest.fn(), put: jest.fn(), delete: jest.fn(), interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }, }, getApiErrorMessage: (_e: any, fallback?: string) => fallback || "Request failed.", })); const mockedApi = api as jest.Mocked; const item = (over: Partial = {}) => ({ id: 1, systemKey: "prepare-cv", title: "Prepare a CV for this role", description: "Attach a CV variant tailored to this application.", category: "preparation", status: "pending", section: "cv", sortOrder: 0, isSystemGenerated: true, isAutoCompleted: false, completedAt: null, ...over, }); const checklist = (items: any[]) => ({ items, progress: { total: items.filter((i) => i.status !== "dismissed").length, completed: items.filter((i) => i.status === "done").length, dismissed: items.filter((i) => i.status === "dismissed").length, percent: 0, }, }); beforeEach(() => { jest.clearAllMocks(); mockedApi.get.mockResolvedValue({ data: checklist([ item(), item({ id: 2, systemKey: "create-cover-letter", title: "Create a cover letter", status: "done", isAutoCompleted: true }), item({ id: 3, systemKey: null, title: "Ask Sara for a referral", category: "custom", isSystemGenerated: false, description: null }), ]), } as any); }); test("renders the checklist grouped by category with progress", async () => { render(); expect(await screen.findByText("Prepare a CV for this role")).toBeInTheDocument(); expect(screen.getByText("Before applying")).toBeInTheDocument(); expect(screen.getByText("Your own tasks")).toBeInTheDocument(); expect(screen.getByText("1 of 3 done")).toBeInTheDocument(); // Items already satisfied by an existing readiness signal are marked as detected, not hand-ticked. expect(screen.getByText("Detected")).toBeInTheDocument(); }); test("completing an item patches its status", async () => { mockedApi.patch.mockResolvedValue({ data: item({ status: "done" }) } as any); render(); fireEvent.click(await screen.findByRole("checkbox", { name: "Prepare a CV for this role" })); await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith("/jobapplications/7/checklist/1", { status: "done" })); }); test("un-ticking a completed item sends it back to pending", async () => { mockedApi.patch.mockResolvedValue({ data: item({ id: 2, status: "pending" }) } as any); render(); fireEvent.click(await screen.findByRole("checkbox", { name: "Create a cover letter" })); await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith("/jobapplications/7/checklist/2", { status: "pending" })); }); test("adding a custom task posts the title", async () => { mockedApi.post.mockResolvedValue({ data: item({ id: 9, systemKey: null, isSystemGenerated: false }) } as any); render(); fireEvent.change(await screen.findByLabelText(/Add your own task/i), { target: { value: "Email the hiring manager" } }); fireEvent.click(screen.getByRole("button", { name: "Add" })); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith( "/jobapplications/7/checklist", { title: "Email the hiring manager", description: undefined, category: undefined }, )); }); test("reordering sends the new id order", async () => { mockedApi.put.mockResolvedValue({ data: checklist([]) } as any); render(); fireEvent.click(await screen.findByRole("button", { name: "Move down: Prepare a CV for this role" })); await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith("/jobapplications/7/checklist/order", [2, 1, 3])); }); test("removing an item calls delete", async () => { mockedApi.delete.mockResolvedValue({ data: undefined } as any); render(); fireEvent.click(await screen.findByRole("button", { name: "Remove: Ask Sara for a referral" })); await waitFor(() => expect(mockedApi.delete).toHaveBeenCalledWith("/jobapplications/7/checklist/3")); });