feat(jobs): redesign table and mobile cards with preserved workflows
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user