Files
jobtrackingapp/job-tracker-ui/src/application-checklist.test.tsx
T

147 lines
5.7 KiB
TypeScript

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";
import { I18nProvider } from "./i18n/I18nProvider";
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<typeof api>;
const renderChecklist = () => render(<I18nProvider><ApplicationChecklist jobId={7} /></I18nProvider>);
const item = (over: Partial<any> = {}) => ({
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();
window.localStorage.setItem("uiLanguage", "en");
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("localizes system checklist content without changing user-authored tasks", async () => {
window.localStorage.setItem("uiLanguage", "nb");
renderChecklist();
expect(await screen.findByText("Forbered en CV for denne stillingen")).toBeInTheDocument();
expect(screen.getByText("Dine egne oppgaver")).toBeInTheDocument();
expect(screen.getByText("Ask Sara for a referral")).toBeInTheDocument();
});
test("renders the checklist grouped by category with progress", async () => {
renderChecklist();
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);
renderChecklist();
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);
renderChecklist();
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);
renderChecklist();
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);
renderChecklist();
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("move and remove tooltips describe their wrappers without replacing item-specific button names", async () => {
mockedApi.patch.mockReturnValueOnce(new Promise(() => {}));
renderChecklist();
const down = await screen.findByRole("button", { name: "Move down: Prepare a CV for this role" });
for (const label of ["Move up: Prepare a CV for this role", "Move down: Prepare a CV for this role", "Remove: Prepare a CV for this role"]) {
expect(screen.getByRole("button", { name: label }).parentElement).not.toHaveAttribute("aria-label");
}
fireEvent.click(screen.getByRole("checkbox", { name: "Prepare a CV for this role" }));
expect(down).toBeDisabled();
fireEvent.mouseOver(down.parentElement!);
expect(await screen.findByRole("tooltip")).toHaveTextContent("Move down");
expect(mockedApi.put).not.toHaveBeenCalled();
});
test("removing an item calls delete", async () => {
mockedApi.delete.mockResolvedValue({ data: undefined } as any);
renderChecklist();
fireEvent.click(await screen.findByRole("button", { name: "Remove: Ask Sara for a referral" }));
await waitFor(() => expect(mockedApi.delete).toHaveBeenCalledWith("/jobapplications/7/checklist/3"));
});