feat(jobs): redesign table and mobile cards with preserved workflows

This commit is contained in:
cesnimda
2026-09-28 23:30:43 +02:00
parent 0f54007015
commit 7127005e48
20 changed files with 192 additions and 63 deletions
+69 -2
View File
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { act, 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";
@@ -9,6 +9,7 @@ import { PromptProvider } from "./prompt";
import { ToastProvider } from "./toast";
import { I18nProvider } from "./i18n/I18nProvider";
import { translations } from "./i18n/translations";
import { getUserScopedStorageKey, setAuthUserKey } from "./auth";
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);
@@ -35,7 +36,7 @@ beforeEach(() => {
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 });
const row = await screen.findByRole("row", { name: /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();
@@ -60,6 +61,9 @@ test("table sort, page-size and optional columns keep their existing independent
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.mouseDown(screen.getByRole("combobox", { name: /rows per page/i }));
fireEvent.click(await screen.findByRole("option", { name: "25" }));
await waitFor(() => expect(lastParams()).toMatchObject({ page: 1, pageSize: 25 }));
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();
@@ -125,3 +129,66 @@ test("density survives an empty result and a failed refresh with the existing re
expect(screen.getByRole("combobox", { name: t.listDensity })).toHaveTextContent(t.densityCompact);
expect(screen.queryByText(job.jobTitle)).not.toBeInTheDocument();
});
test("Bokmål display localizes standard statuses without changing API values or custom status filters", async () => {
localStorage.setItem("uiLanguage", "nb");
const standardJob = { ...job, status: "Interview", dateApplied: "2026-08-01", deadline: "2026-08-10" };
mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url === "/companies" ? [job.company] : url === "/jobapplications" ? { items: [standardJob], total: 1, page: 1, pageSize: 15 } : [] } as any));
show("jobs", "?status=Custom%20review");
const row = await screen.findByRole("row", { name: /platform engineer/i });
expect(lastParams().status).toBe("Custom review");
expect(within(row).getByText(translations.nb.statusInterview)).toBeVisible();
expect(within(row).getByText(new Date(standardJob.dateApplied).toLocaleDateString("nb-NO"))).toBeVisible();
expect(within(row).getByText(new Date(standardJob.deadline).toLocaleDateString("nb-NO"))).toBeVisible();
expect(screen.getByRole("combobox", { name: translations.nb.jobTableStatus })).toHaveTextContent("Custom review");
fireEvent.click(within(row).getByRole("button", { name: `${translations.nb.jobTableQuickStatus}: ${job.jobTitle}` }));
fireEvent.click(await screen.findByRole("menuitem", { name: translations.nb.jobTableSetStatus.replace("{status}", translations.nb.statusOffer) }));
await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith("/jobapplications/42/status", { status: "Offer" }));
});
test("partial bulk failure refreshes successful rows and retains only failed visible IDs", async () => {
const other = { ...job, id: 43, jobTitle: "Second role" };
mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url === "/companies" ? [job.company] : url === "/jobapplications" ? { items: [job, other], total: 2, page: 1, pageSize: 15 } : [] } as any));
mockedApi.patch.mockImplementation((url: string) => url.includes("/43/") ? Promise.reject(new Error("Synthetic failure")) : Promise.resolve({ status: 204 } as any));
show();
const first = await screen.findByRole("row", { name: /open platform engineer/i });
const second = screen.getByRole("row", { name: /open second role/i });
fireEvent.click(screen.getByRole("checkbox", { name: t.jobTableSelectAll }));
fireEvent.click(screen.getByRole("button", { name: "Offer" }));
await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledTimes(2));
await waitFor(() => expect(within(first).getByRole("checkbox")).not.toBeChecked());
expect(within(second).getByRole("checkbox")).toBeChecked();
expect(calls().length).toBeGreaterThan(1);
});
test("saved view create/apply/delete stays scoped to the signed-in account", async () => {
setAuthUserKey("account-a");
show("jobs", "?q=platform&status=Interview");
await screen.findByRole("row", { name: /platform engineer/i });
fireEvent.click(screen.getByRole("button", { name: t.savedViewsTooltip }));
fireEvent.change(screen.getByRole("textbox", { name: t.savedViewsName }), { target: { value: "Interview search" } });
fireEvent.click(screen.getByRole("button", { name: t.savedViewsSaveCurrent }));
expect(localStorage.getItem(getUserScopedStorageKey("jt_saved_views_v1"))).toContain("Interview search");
fireEvent.click(screen.getByRole("menuitem", { name: /Interview search/ }));
expect(screen.getByLabelText("Current route")).toHaveTextContent("status=Interview");
act(() => setAuthUserKey("account-b"));
fireEvent.click(screen.getByRole("button", { name: t.savedViewsTooltip }));
expect(screen.getByText(t.savedViewsEmpty)).toBeVisible();
act(() => setAuthUserKey("account-a"));
await waitFor(() => expect(screen.queryByText(t.savedViewsEmpty)).not.toBeInTheDocument());
fireEvent.click(screen.getByRole("button", { name: t.savedViewsTooltip }));
expect(screen.getByRole("menuitem", { name: /Interview search/ })).toBeVisible();
fireEvent.click(screen.getByRole("button", { name: `${t.attachmentsDelete}: Interview search` }));
expect(localStorage.getItem(getUserScopedStorageKey("jt_saved_views_v1"))).not.toContain("Interview search");
});
test("changing the result set clears selection before an old row can be bulk-updated", async () => {
show();
const row = await screen.findByRole("row", { name: /platform engineer/i });
fireEvent.click(within(row).getByRole("checkbox"));
expect(screen.getByText(t.jobTableSelected.replace("{count}", "1"))).toBeVisible();
fireEvent.change(screen.getByRole("textbox", { name: t.jobTableSearch }), { target: { value: "different" } });
expect(within(row).getByRole("checkbox")).not.toBeChecked();
expect(screen.queryByRole("button", { name: "Offer" })).not.toBeInTheDocument();
expect(mockedApi.patch).not.toHaveBeenCalled();
});