import React, { useState } from "react"; import "@testing-library/jest-dom"; import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { MemoryRouter, useLocation } from "react-router-dom"; import { api } from "./api"; import JobTable, { type JobTableColumns } 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"; jest.mock("./components/EditJobDialog", () => ({ open, onClose }: { open: boolean; onClose: () => void }) => open ?
: null); const mockedApi = api as jest.Mocked; const job = { id: 42, companyId: 1, company: { id: 1, name: "Østlandet" }, jobTitle: "Platform engineer", status: "Custom review", dateApplied: "2026-08-01", daysSince: 2, location: "Tromsø", jobUrl: "https://example.test/listing", isDeleted: false }; const t = translations.en; function Consumer({ mode }: { mode: "jobs" | "trash" }) { const location = useLocation(); const [pageSize, setPageSize] = useState<15 | 20 | 25>(15); const [columns, setColumns] = useState({ status: true, dateApplied: true, daysSince: true, jobUrl: true }); return <>{location.pathname}{location.search}; } function show(mode: "jobs" | "trash" = "jobs", query = "") { return render(); } const calls = () => mockedApi.get.mock.calls.filter(([url]) => url === "/jobapplications"); const lastParams = () => calls().at(-1)?.[1]?.params as Record; beforeEach(() => { localStorage.clear(); jest.clearAllMocks(); mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url === "/companies" ? [job.company] : url === "/jobapplications" ? { items: [job], total: 31, page: 1, pageSize: 15 } : [] } as any)); }); test("row status/edit/delete controls retain selection and do not open the workspace", async () => { show(); const row = await screen.findByRole("row", { name: /open platform engineer/i }); fireEvent.click(within(row).getByRole("checkbox")); fireEvent.click(within(row).getByRole("button", { name: `${t.jobTableEdit}: ${job.jobTitle}` })); expect(screen.getByRole("dialog", { name: "Edit job" })).toBeVisible(); fireEvent.click(screen.getByText("Close editor")); expect(within(row).getByRole("checkbox")).toBeChecked(); fireEvent.click(within(row).getByRole("button", { name: `${t.jobTableSoftDelete}: ${job.jobTitle}` })); fireEvent.click(within(await screen.findByRole("dialog")).getByRole("button", { name: /cancel/i })); expect(mockedApi.delete).not.toHaveBeenCalled(); fireEvent.click(await within(row).findByRole("button", { name: `${t.jobTableQuickStatus}: ${job.jobTitle}` })); fireEvent.click(await screen.findByRole("menuitem", { name: /Interview/ })); await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith("/jobapplications/42/status", { status: "Interview" })); expect(screen.getByLabelText("Current route")).toHaveTextContent(/^\/jobs$/); }); test("table sort, page-size and optional columns keep their existing independent contracts", async () => { show("jobs", "?q=platform&location=Troms%C3%B8"); await screen.findByRole("row", { name: /open platform engineer/i }); fireEvent.click(screen.getByRole("button", { name: t.jobTableCompany })); await waitFor(() => expect(lastParams()).toMatchObject({ q: "platform", location: "Tromsø", sortBy: "company", sortDir: "asc" })); fireEvent.click(screen.getByRole("button", { name: "Go to next page" })); await waitFor(() => expect(lastParams().page).toBe(2)); fireEvent.mouseDown(screen.getByRole("combobox", { name: /rows per page/i })); fireEvent.click(await screen.findByRole("option", { name: "20" })); await waitFor(() => expect(lastParams()).toMatchObject({ page: 1, pageSize: 20 })); fireEvent.click(screen.getByRole("button", { name: t.jobTableColumns })); fireEvent.click(await screen.findByRole("menuitem", { name: t.settingsColumnJobUrl })); expect(screen.queryByRole("link", { name: t.jobTableLink })).not.toBeInTheDocument(); }); test("Trash select-all and bulk restore use the original IDs without workspace navigation", async () => { show("trash"); await screen.findByText(job.jobTitle); fireEvent.click(screen.getByRole("checkbox", { name: t.jobTableSelectAll })); fireEvent.click(screen.getByRole("button", { name: t.jobTableRestoreSelected })); await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith("/jobapplications/42/restore")); expect(screen.getByLabelText("Current route")).toHaveTextContent(/^\/trash$/); }); async function compact() { fireEvent.mouseDown(screen.getByRole("combobox", { name: t.listDensity })); fireEvent.click(await screen.findByRole("option", { name: t.densityCompact })); } test("density preserves selected rows, query, page, custom status, columns and listing link without refetch", async () => { show("jobs", "?q=platform&page=2&sortBy=company&sortDir=asc"); const row = await screen.findByRole("row", { name: /open platform engineer/i }); const columns = within(row).getAllByRole("cell").length; fireEvent.click(within(row).getByRole("checkbox")); const before = calls().length; expect(screen.getByRole("combobox", { name: t.listDensity })).toHaveTextContent(t.densityComfortable); await compact(); expect(within(row).getByRole("checkbox")).toBeChecked(); expect(within(row).getAllByRole("cell")).toHaveLength(columns); expect(within(row).getByText("Custom review")).toBeVisible(); expect(within(row).getByRole("link", { name: t.jobTableLink })).toHaveAttribute("href", job.jobUrl); expect(screen.getByText("16–30 of 31")).toBeVisible(); expect(screen.getByLabelText("Current route")).toHaveTextContent("/jobs?q=platform&page=2&sortBy=company&sortDir=asc"); expect(calls()).toHaveLength(before); fireEvent.click(screen.getByRole("button", { name: "Offer" })); await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith("/jobapplications/42/status", { status: "Offer" })); }); test("density is local and resets on unmount without writing settings or a saved view", async () => { const view = show(); await screen.findByText(job.jobTitle); const stored = { ...localStorage }; await compact(); expect({ ...localStorage }).toEqual(stored); expect(mockedApi.put).not.toHaveBeenCalled(); view.unmount(); show(); await screen.findByText(job.jobTitle); expect(screen.getByRole("combobox", { name: t.listDensity })).toHaveTextContent(t.densityComfortable); }); test("density survives an empty result and a failed refresh with the existing retry action", async () => { show(); await screen.findByText(job.jobTitle); await compact(); mockedApi.get.mockImplementation((url: string) => url === "/jobapplications" ? Promise.reject({ message: "Synthetic unavailable", response: { status: 503 } }) : Promise.resolve({ data: [] } as any)); fireEvent.click(screen.getByRole("button", { name: t.tableNextPage })); expect(await screen.findByRole("alert")).toHaveTextContent("Synthetic unavailable"); expect(screen.getByRole("combobox", { name: t.listDensity })).toHaveTextContent(t.densityCompact); mockedApi.get.mockResolvedValue({ data: { items: [], total: 0, page: 1, pageSize: 15 } } as any); fireEvent.click(screen.getByRole("button", { name: /retry/i })); await waitFor(() => expect(screen.queryByRole("alert")).not.toBeInTheDocument()); expect(screen.getByRole("combobox", { name: t.listDensity })).toHaveTextContent(t.densityCompact); expect(screen.queryByText(job.jobTitle)).not.toBeInTheDocument(); });