53 lines
3.7 KiB
TypeScript
53 lines
3.7 KiB
TypeScript
import React from "react";
|
|
import "@testing-library/jest-dom";
|
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
|
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
|
|
import { api } from "./api";
|
|
import { I18nProvider } from "./i18n/I18nProvider";
|
|
import { ToastProvider } from "./toast";
|
|
import EditJobDialog from "./components/EditJobDialog";
|
|
|
|
jest.mock("./api", () => ({ api: { get: jest.fn(), put: jest.fn() } }));
|
|
jest.mock("./hooks/useCompanies", () => ({ useCompanies: () => ({ companies: [{ id: 2, name: "Synthetic company" }] }) }));
|
|
const client = api as jest.Mocked<typeof api>;
|
|
function deferred() { let resolve!: (value: any) => void; const promise = new Promise<any>((done) => { resolve = done; }); return { promise, resolve }; }
|
|
const job = (id: number, version: number, jobTitle: string) => ({ id, version, jobTitle, status: "Applied", company: { id: 2, name: "Synthetic company" }, dateApplied: "2026-10-01" });
|
|
const view = (id: number, onSaved = jest.fn(), onClose = jest.fn()) => <LocalizationProvider dateAdapter={AdapterDateFns}><ToastProvider><I18nProvider><EditJobDialog open jobId={id} onSaved={onSaved} onClose={onClose} /></I18nProvider></ToastProvider></LocalizationProvider>;
|
|
beforeEach(() => { jest.resetAllMocks(); localStorage.clear(); client.get.mockResolvedValue({ data: job(1, 7, "Recorded title") } as any); });
|
|
|
|
test.each([409, 0])("job save rejection (%s) preserves the draft and sends the source revision", async (status) => {
|
|
client.put.mockRejectedValue({ response: { status, data: { code: "revision_conflict" } } });
|
|
const onSaved = jest.fn(); const onClose = jest.fn();
|
|
render(view(1, onSaved, onClose));
|
|
await screen.findByDisplayValue("Recorded title");
|
|
fireEvent.change(screen.getByLabelText("Job title"), { target: { value: "Recoverable title" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
|
await waitFor(() => expect(screen.getByRole("button", { name: "Save" })).toBeEnabled());
|
|
expect(screen.getByLabelText("Job title")).toHaveValue("Recoverable title");
|
|
expect(client.put).toHaveBeenCalledWith("/jobapplications/1", expect.objectContaining({ jobTitle: "Recoverable title", expectedVersion: 7 }));
|
|
expect(onSaved).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("a held job save excludes editing and closing until the captured snapshot settles", async () => {
|
|
const pending = deferred(); client.put.mockReturnValue(pending.promise);
|
|
const onClose = jest.fn(); render(view(1, jest.fn(), onClose));
|
|
await screen.findByDisplayValue("Recorded title");
|
|
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
|
expect(screen.getByLabelText("Job title")).toBeDisabled();
|
|
expect(screen.getByRole("combobox", { name: "Current status" })).toHaveAttribute("aria-disabled", "true");
|
|
expect(screen.getByRole("button", { name: "Cancel" })).toBeDisabled();
|
|
fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });
|
|
expect(onClose).not.toHaveBeenCalled();
|
|
await act(async () => pending.resolve({ data: undefined, headers: { "x-job-version": "8" } }));
|
|
});
|
|
|
|
test("a delayed old job read cannot overwrite the newer job session", async () => {
|
|
const oldRead = deferred(); client.get.mockReturnValueOnce(oldRead.promise).mockResolvedValueOnce({ data: job(2, 11, "New session title") } as any);
|
|
const mounted = render(view(1)); mounted.rerender(view(2));
|
|
await screen.findByDisplayValue("New session title");
|
|
fireEvent.change(screen.getByLabelText("Job title"), { target: { value: "New session draft" } });
|
|
await act(async () => oldRead.resolve({ data: job(1, 7, "Late old title") }));
|
|
expect(screen.getByLabelText("Job title")).toHaveValue("New session draft");
|
|
});
|