Files
jobtrackingapp/job-tracker-ui/src/edit-job-revision.test.tsx
T

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");
});