import React from "react"; import "@testing-library/jest-dom"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { ApplicationFollowUp, ApplicationInterviewPrep } from "./components/InterviewPrep"; 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 board = { groups: [ { category: "company-research", label: "Company research", items: [ { id: 1, category: "company-research", title: "Funding history", content: "Series B in 2025.", source: "user", isPrepared: true, sortOrder: 1, updatedAtUtc: "2026-07-19T10:00:00Z" }, ], }, { category: "behavioural", label: "Behavioural questions", items: [ { id: 2, category: "behavioural", title: "Tell me about a conflict", content: null, source: "ai", isPrepared: false, sortOrder: 2, updatedAtUtc: "2026-07-19T10:00:00Z" }, ], }, ], total: 2, prepared: 1, percent: 50, isInterviewStage: true, aiSuggestionCount: 1, }; const followUp = { followUpAt: "2026-07-26T00:00:00", nextAction: "Chase recruiter", responseReceived: false, openFollowUpTasks: 1 }; function routeGet(overrides: Record = {}) { mockedApi.get.mockImplementation((url: string) => { if (url.endsWith("/follow-up")) return Promise.resolve({ data: overrides.followUp ?? followUp } as any); return Promise.resolve({ data: overrides.board ?? board } as any); }); } beforeEach(() => jest.clearAllMocks()); test("prep renders grouped items with progress and marks AI-sourced ones", async () => { routeGet(); render(); // Each label appears twice: once as the group heading, once as a category option in the add form. expect((await screen.findAllByText("Company research")).length).toBeGreaterThan(0); expect(screen.getAllByText("Behavioural questions").length).toBeGreaterThan(0); expect(screen.getByText("Funding history")).toBeInTheDocument(); expect(screen.getByText("1 of 2 ready")).toBeInTheDocument(); expect(screen.getByText("From AI")).toBeInTheDocument(); }); test("adding a prep item posts the chosen category and title", async () => { routeGet(); mockedApi.post.mockResolvedValue({ data: board.groups[0].items[0] } as any); render(); fireEvent.change(await screen.findByLabelText(/Add a question, topic or note/i), { target: { value: "What does success look like?" }, }); fireEvent.click(screen.getByRole("button", { name: "Add" })); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith( "/jobapplications/7/interview-prep", { category: "company-research", title: "What does success look like?" }, )); }); test("marking an item ready patches it", async () => { routeGet(); mockedApi.patch.mockResolvedValue({ data: board.groups[1].items[0] } as any); render(); fireEvent.click(await screen.findByRole("checkbox", { name: "Ready: Tell me about a conflict" })); await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith( "/jobapplications/7/interview-prep/2", { isPrepared: true })); }); test("an answer is only saved when the user asks", async () => { routeGet(); mockedApi.patch.mockResolvedValue({ data: board.groups[1].items[0] } as any); render(); const boxes = await screen.findAllByPlaceholderText(/Your answer, in your own words/i); fireEvent.change(boxes[1], { target: { value: "My STAR answer" } }); // Typing alone must not persist anything. expect(mockedApi.patch).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole("button", { name: /Save answer/i })); await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith( "/jobapplications/7/interview-prep/2", { content: "My STAR answer" })); }); test("deleting a prep item calls delete", async () => { routeGet(); mockedApi.delete.mockResolvedValue({ data: undefined } as any); render(); fireEvent.click(await screen.findByRole("button", { name: "Delete: Funding history" })); await waitFor(() => expect(mockedApi.delete).toHaveBeenCalledWith("/jobapplications/7/interview-prep/1")); }); test("empty prep shows a useful empty state", async () => { routeGet({ board: { groups: [], total: 0, prepared: 0, percent: 0, isInterviewStage: false, aiSuggestionCount: 0 } }); render(); expect(await screen.findByText(/Nothing prepared yet/i)).toBeInTheDocument(); expect(screen.getByText(/has not reached an interview stage/i)).toBeInTheDocument(); }); test("prep surfaces a load error", async () => { mockedApi.get.mockRejectedValue(new Error("boom")); render(); expect(await screen.findByText(/Could not load interview preparation/i)).toBeInTheDocument(); }); // ---------- follow-up ---------- test("follow-up loads the existing date and open checklist tasks", async () => { routeGet(); render(); expect(await screen.findByDisplayValue("2026-07-26")).toBeInTheDocument(); expect(screen.getByDisplayValue("Chase recruiter")).toBeInTheDocument(); expect(screen.getByText(/1 open follow-up task on the checklist/i)).toBeInTheDocument(); }); test("saving a follow-up sends the date and next action", async () => { routeGet(); mockedApi.put.mockResolvedValue({ data: followUp } as any); render(); fireEvent.change(await screen.findByLabelText(/Follow up on/i), { target: { value: "2026-08-01" } }); fireEvent.click(screen.getByRole("button", { name: "Save" })); await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith( "/jobapplications/7/follow-up", { followUpAt: "2026-08-01", nextAction: "Chase recruiter" }, )); }); test("no follow-up shows an empty state", async () => { routeGet({ followUp: { followUpAt: null, nextAction: null, responseReceived: false, openFollowUpTasks: 0 } }); render(); expect(await screen.findByText(/No follow-up scheduled/i)).toBeInTheDocument(); });