Files
jobtrackingapp/job-tracker-ui/src/job-table-density.test.tsx
T

128 lines
8.0 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 } 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 ? <div role="dialog" aria-label="Edit job"><button onClick={onClose}>Close editor</button></div> : null);
const mockedApi = api as jest.Mocked<typeof api>;
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<JobTableColumns>({ status: true, dateApplied: true, daysSince: true, jobUrl: true });
return <><output aria-label="Current route">{location.pathname}{location.search}</output><JobTable mode={mode} refreshToken={0} pageSize={pageSize} onPageSizeChange={setPageSize} columns={columns} onColumnsChange={setColumns} /></>;
}
function show(mode: "jobs" | "trash" = "jobs", query = "") {
return render(<MemoryRouter initialEntries={[`/${mode}${query}`]}><I18nProvider><ToastProvider><ConfirmProvider><PromptProvider><Consumer mode={mode} /></PromptProvider></ConfirmProvider></ToastProvider></I18nProvider></MemoryRouter>);
}
const calls = () => mockedApi.get.mock.calls.filter(([url]) => url === "/jobapplications");
const lastParams = () => calls().at(-1)?.[1]?.params as Record<string, unknown>;
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();
});