Files
jobtrackingapp/job-tracker-ui/src/application-intelligence.test.tsx
T
2026-10-07 01:58:31 +02:00

207 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from "react";
import "@testing-library/jest-dom";
import { fireEvent, render as rtlRender, screen, waitFor } from "@testing-library/react";
import {
ApplicationAnalysis, ApplicationMatch, ApplicationTimeline,
} from "./components/ApplicationIntelligence";
import { api } from "./api";
import { I18nProvider } from "./i18n/I18nProvider";
const render = (ui: React.ReactElement) => rtlRender(<I18nProvider>{ui}</I18nProvider>);
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<typeof api>;
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,
hasSelectedCv: true,
selectedCvName: "Backend CV",
jobLanguage: "en",
cvLanguage: "en",
languageMismatch: false,
usedTranslatedJobDescription: false,
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(<ApplicationTimeline jobId={7} />);
expect(await screen.findByText("Milestones")).toBeInTheDocument();
expect(screen.getByText(new Date(`${timeline.days[0].date}T12:00:00`).toLocaleDateString("en", { dateStyle: "long" }))).toBeInTheDocument();
expect(screen.getByRole("list")).toHaveTextContent("AI suggestions refreshed");
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(<ApplicationTimeline jobId={7} />);
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(<ApplicationTimeline jobId={7} />);
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(<ApplicationTimeline jobId={7} />);
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(<ApplicationAnalysis jobId={7} />);
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(<ApplicationAnalysis jobId={7} />);
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);
render(<ApplicationAnalysis jobId={7} />);
expect(screen.getByRole("progressbar", { name: "Loading..." })).toBeInTheDocument();
});
// ---------- Match ----------
test("match renders the score, evidence and suggestions", async () => {
mockedApi.get.mockResolvedValue({ data: match } as any);
render(<ApplicationMatch jobId={7} />);
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(<ApplicationMatch jobId={7} />);
expect(await screen.findByText(/linked CV cannot be matched/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(<ApplicationMatch jobId={7} />);
expect(await screen.findByText(/too short to score reliably/i)).toBeInTheDocument();
});
test("match explains when a saved translation is used for a cross-language comparison", async () => {
mockedApi.get.mockResolvedValue({
data: {
...match,
jobLanguage: "nb",
cvLanguage: "en",
languageMismatch: true,
usedTranslatedJobDescription: true,
},
} as any);
render(<ApplicationMatch jobId={7} />);
expect(await screen.findByText(/uses the saved English translation/i)).toBeInTheDocument();
});