Files
jobtrackingapp/job-tracker-ui/src/jobs-view-integration.test.tsx
T
2026-09-29 23:01:05 +02:00

125 lines
8.2 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, { useState } from "react";
import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { MemoryRouter, useLocation, useNavigate } from "react-router-dom";
import { api } from "./api";
import JobTable from "./components/JobTable";
import { ConfirmProvider } from "./confirm";
import { PromptProvider } from "./prompt";
import { ToastProvider } from "./toast";
import { I18nProvider } from "./i18n/I18nProvider";
import { translations } from "./i18n/translations";
import { getUserScopedStorageKey } from "./auth";
const t = translations.en;
jest.mock("./components/EditJobDialog", () => () => null);
const mockedApi = api as jest.Mocked<typeof api>;
const jobs = Array.from({ length: 27 }, (_, i) => ({ id: i + 1, jobTitle: i === 0 ? "Different" : `Engineer ${i}`, companyId: 1, company: { id: 1, name: "Nordlys" }, status: i === 1 ? "Custom review" : "Saved", location: "Oslo", isDeleted: false }));
function Consumer({ mode = "jobs" }: { mode?: "jobs" | "trash" }) {
const location = useLocation();
const navigate = useNavigate();
const [refreshToken, setRefreshToken] = useState(0);
return <><output aria-label="Route">{location.pathname}{location.search}</output>
<button onClick={() => navigate(-1)}>History back</button><button onClick={() => navigate(1)}>History forward</button>
<button onClick={() => setRefreshToken(value => value + 1)}>Job created</button>
<JobTable refreshToken={refreshToken} pageSize={15} onPageSizeChange={jest.fn()} columns={{ status: true, dateApplied: true, daysSince: true, jobUrl: true }} onColumnsChange={jest.fn()} mode={mode} /></>;
}
function show(query = "", mode: "jobs" | "trash" = "jobs") {
return render(<MemoryRouter initialEntries={[`/${mode}${query}`]}><I18nProvider><ToastProvider><ConfirmProvider><PromptProvider><Consumer mode={mode} /></PromptProvider></ConfirmProvider></ToastProvider></I18nProvider></MemoryRouter>);
}
const reads = (path: string) => mockedApi.get.mock.calls.filter(([url]) => url === path);
const ids = (selector: string, attribute: string) => Array.from(document.querySelectorAll(selector), el => Number(el.getAttribute(attribute))).sort((a, b) => a - b);
beforeEach(() => {
localStorage.clear(); jest.clearAllMocks();
mockedApi.get.mockImplementation((url, config) => {
const p = (config?.params ?? {}) as { q?: string; status?: string; page?: number };
const matching = jobs.filter(job => (!p.q || job.jobTitle.includes(p.q)) && (!p.status || p.status === "All" || job.status === p.status));
return Promise.resolve({ data: url === "/companies" ? [jobs[0].company] : url === "/jobapplications/board" ? matching : url === "/jobapplications" ? { items: matching.slice(((p.page ?? 1) - 1) * 15, (p.page ?? 1) * 15), total: matching.length, page: p.page, pageSize: 15 } : [] } as any);
});
});
test("Table/Board switches preserve query, complete membership, page, density and history without hidden reads", async () => {
show("?q=Engineer&page=2&sortBy=jobTitle&sortDir=asc");
await screen.findByText("16–26 of 26");
expect(reads("/jobapplications/board")).toHaveLength(0);
const pageIds = ids("[data-job-row-id]", "data-job-row-id");
fireEvent.click(screen.getAllByRole("checkbox", { name: /Select Engineer/ })[0]);
fireEvent.mouseDown(screen.getByRole("combobox", { name: t.listDensity }));
fireEvent.click(await screen.findByRole("option", { name: "Compact" }));
const count = reads("/jobapplications").length;
const board = screen.getByRole("link", { name: "Board" });
board.focus(); fireEvent.click(board);
await waitFor(() => expect(ids("[data-kanban-card-id]", "data-kanban-card-id")).toEqual(jobs.slice(1).map(j => j.id)));
expect(board).toHaveFocus();
expect(board).toHaveAttribute("aria-current", "page");
expect(reads("/jobapplications")).toHaveLength(count);
expect(reads("/jobapplications/board")).toHaveLength(1);
expect(screen.queryByRole("combobox", { name: t.listDensity })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("link", { name: "Table" }));
await screen.findByText("16–26 of 26");
expect(ids("[data-job-row-id]", "data-job-row-id")).toEqual(pageIds);
expect(screen.getByRole("combobox", { name: t.listDensity })).toHaveTextContent("Compact");
expect(screen.getAllByRole("checkbox", { name: /Select Engineer/ }).every(el => !(el as HTMLInputElement).checked)).toBe(true);
expect(reads("/jobapplications/board")).toHaveLength(1);
fireEvent.click(screen.getByText("History back"));
await waitFor(() => expect(document.querySelectorAll("[data-kanban-card-id]")).toHaveLength(26));
expect(screen.getByLabelText("Route")).toHaveTextContent("view=board");
fireEvent.click(screen.getByText("History forward"));
await screen.findByText("16–26 of 26");
expect(screen.getByLabelText("Route")).toHaveTextContent("view=table");
});
test.each(["?q=Engineer&status=Custom+review", "?q=absent"])("direct Board and both switch directions retain exact result membership: %s", async query => {
show(`${query}&view=board`);
const expected = query.includes("absent") ? [] : [2];
await screen.findByText(`${expected.length} matching jobs`);
expect(ids("[data-kanban-card-id]", "data-kanban-card-id")).toEqual(expected);
expect(reads("/jobapplications")).toHaveLength(0);
fireEvent.click(screen.getByRole("link", { name: "Table" }));
await waitFor(() => expect(reads("/jobapplications")).toHaveLength(1));
await waitFor(() => expect(ids("[data-job-row-id]", "data-job-row-id")).toEqual(expected));
fireEvent.click(screen.getByRole("link", { name: "Board" }));
await screen.findByText(`${expected.length} matching jobs`);
expect(ids("[data-kanban-card-id]", "data-kanban-card-id")).toEqual(expected);
expect(reads("/jobapplications/board").every(([, config]) => (config?.params as { q?: string }).q === (expected.length ? "Engineer" : "absent"))).toBe(true);
});
test("Board uses the shared filter controls and saved query owner without fetching Table", async () => {
show("?view=board");
await screen.findByText("27 matching jobs");
fireEvent.change(screen.getByRole("textbox", { name: t.jobTableSearch }), { target: { value: "Engineer" } });
await screen.findByText("26 matching jobs");
fireEvent.mouseDown(screen.getByRole("combobox", { name: "Status" }));
fireEvent.click(await screen.findByRole("option", { name: "Offer" }));
await screen.findByText("0 matching jobs");
expect(reads("/jobapplications")).toHaveLength(0);
expect(screen.getByLabelText("Route")).toHaveTextContent("view=board&q=Engineer&status=Offer");
});
test("Trash stays a Table even when supplied a Board view parameter", async () => {
show("?view=board", "trash");
await screen.findByText("1–15 of 27");
expect(screen.queryByRole("group", { name: "Jobs view" })).not.toBeInTheDocument();
expect(reads("/jobapplications/board")).toHaveLength(0);
expect(reads("/jobapplications")[0][1]?.params).toMatchObject({ deletedOnly: true });
expect(within(screen.getByRole("table")).getAllByRole("row")).toHaveLength(16);
});
test("saved filters applied in Board reset the Table page and refresh only the active population", async () => {
localStorage.setItem(getUserScopedStorageKey("jt_saved_views_v1"), JSON.stringify([{ id: "review", name: "Review shortlist", params: { q: "Engineer", status: "Custom review" }, createdAt: "2026-09-29" }]));
show("?view=board&page=2");
await screen.findByText("27 matching jobs");
fireEvent.click(screen.getByRole("button", { name: t.savedViewsTooltip }));
fireEvent.click(await screen.findByText("Review shortlist"));
await screen.findByText("1 matching jobs");
expect(screen.getByLabelText("Route")).not.toHaveTextContent("page=");
expect(ids("[data-kanban-card-id]", "data-kanban-card-id")).toEqual([2]);
const before = reads("/jobapplications/board").length;
fireEvent.click(screen.getByRole("button", { name: "Job created" }));
await waitFor(() => expect(reads("/jobapplications/board")).toHaveLength(before + 1));
expect(reads("/jobapplications")).toHaveLength(0);
fireEvent.click(screen.getByRole("link", { name: "Table" }));
await waitFor(() => expect(ids("[data-job-row-id]", "data-job-row-id")).toEqual([2]));
expect(reads("/jobapplications")[0][1]?.params).toMatchObject({ page: 1, q: "Engineer", status: "Custom review" });
});