125 lines
8.2 KiB
TypeScript
125 lines
8.2 KiB
TypeScript
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" });
|
||
});
|