import React from "react"; import "@testing-library/jest-dom"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { ApplicationAnalysis, ApplicationMatch, ApplicationTimeline, } from "./components/ApplicationIntelligence"; import { api } from "./api"; jest.mock("./api", () => ({ api: { get: 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 timeline = { days: [ { date: "2026-07-19", label: "Today", events: [ { id: 2, type: "StatusChanged", category: "stage", summary: "Moved from Applied to Interview", detail: null, isMilestone: true, at: "2026-07-19T09:00:00Z" }, { id: 3, type: "AiRefreshed", category: "ai", summary: "AI suggestions refreshed", detail: null, isMilestone: false, at: "2026-07-19T08:00:00Z" }, ], }, ], milestones: [ { id: 2, type: "StatusChanged", category: "stage", summary: "Moved from Applied to Interview", detail: null, isMilestone: true, at: "2026-07-19T09:00:00Z" }, ], categories: ["ai", "stage"], totalEvents: 2, }; const analysis = { role: "Senior Backend Developer", company: "Acme", location: "Oslo", employmentType: "Full-time", seniority: "Senior", salary: null, technologies: ["C#", ".NET"], skills: ["Collaboration"], responsibilities: ["Build and operate REST APIs"], keywords: ["C#", ".NET", "Collaboration"], summary: "Senior Backend Developer at Acme · Oslo.", importantRequirements: ["Strong experience with C# and .NET"], interviewTopics: ["C#", ".NET"], missingInformation: ["Salary or compensation range"], hasJobDescription: true, aiSuggestionCount: 0, }; const match = { score: 72, band: "Good", hasEnoughSignal: true, hasCareerProfile: true, matchedSkills: ["C#", "SQL"], missingSkills: ["Kubernetes"], relevantExperience: [{ title: "Backend Developer", subtitle: "Initech · 2021 – present", matched: ["C#"] }], relevantProjects: [], suggestions: ["Solid match. Lead with the matched skills."], aiSuggestionCount: 0, }; beforeEach(() => jest.clearAllMocks()); // ---------- Timeline ---------- test("timeline renders grouped days, milestones and readable summaries", async () => { mockedApi.get.mockResolvedValue({ data: timeline } as any); render(); expect(await screen.findByText("Milestones")).toBeInTheDocument(); expect(screen.getByText("Today")).toBeInTheDocument(); expect(screen.getAllByText("Moved from Applied to Interview").length).toBeGreaterThan(0); expect(screen.getByText("AI suggestions refreshed")).toBeInTheDocument(); }); test("timeline filters by category", async () => { mockedApi.get.mockResolvedValue({ data: timeline } as any); render(); fireEvent.click(await screen.findByRole("button", { name: "AI" })); await waitFor(() => expect(mockedApi.get).toHaveBeenLastCalledWith( "/jobapplications/7/timeline", { params: { category: "ai", milestonesOnly: undefined } }, )); }); test("timeline shows an empty state when nothing has happened", async () => { mockedApi.get.mockResolvedValue({ data: { days: [], milestones: [], categories: [], totalEvents: 0 } } as any); render(); expect(await screen.findByText(/Nothing has happened yet/i)).toBeInTheDocument(); }); test("timeline surfaces an error instead of rendering nothing", async () => { mockedApi.get.mockRejectedValue(new Error("boom")); render(); expect(await screen.findByText(/Could not load this section/i)).toBeInTheDocument(); }); // ---------- Analysis ---------- test("analysis renders the extracted structure", async () => { mockedApi.get.mockResolvedValue({ data: analysis } as any); render(); expect(await screen.findByText("Senior Backend Developer at Acme · Oslo.")).toBeInTheDocument(); expect(screen.getByText("Full-time")).toBeInTheDocument(); expect(screen.getByText("Strong experience with C# and .NET")).toBeInTheDocument(); expect(screen.getByText("Salary or compensation range")).toBeInTheDocument(); }); test("analysis prompts for the advert when there is none", async () => { mockedApi.get.mockResolvedValue({ data: { ...analysis, hasJobDescription: false, technologies: [], responsibilities: [], importantRequirements: [] }, } as any); render(); expect(await screen.findByText(/No advert text saved yet/i)).toBeInTheDocument(); }); test("analysis shows a loading state before the data arrives", () => { mockedApi.get.mockReturnValue(new Promise(() => {}) as any); const { container } = render(); expect(container.querySelectorAll(".MuiSkeleton-root").length).toBeGreaterThan(0); }); // ---------- Match ---------- test("match renders the score, evidence and suggestions", async () => { mockedApi.get.mockResolvedValue({ data: match } as any); render(); expect(await screen.findByText("72%")).toBeInTheDocument(); expect(screen.getByText("Good")).toBeInTheDocument(); expect(screen.getByText("Backend Developer")).toBeInTheDocument(); expect(screen.getByText("Kubernetes")).toBeInTheDocument(); expect(screen.getByText(/Solid match/i)).toBeInTheDocument(); }); test("match asks for a career profile before showing a score", async () => { mockedApi.get.mockResolvedValue({ data: { ...match, score: 0, band: "No profile", hasCareerProfile: false, matchedSkills: [], missingSkills: [], relevantExperience: [], suggestions: ["Build your career profile first."], }, } as any); render(); expect(await screen.findByText(/No career profile yet/i)).toBeInTheDocument(); expect(screen.queryByText("0%")).not.toBeInTheDocument(); }); test("match warns when the advert is too short to score", async () => { mockedApi.get.mockResolvedValue({ data: { ...match, hasEnoughSignal: false } } as any); render(); expect(await screen.findByText(/too short to score reliably/i)).toBeInTheDocument(); });