fix(ui): preserve application drafts and recovery states

This commit is contained in:
cesnimda
2026-10-13 23:27:16 +02:00
parent 627955afa9
commit 4c3254c472
19 changed files with 5388 additions and 118 deletions
@@ -1,6 +1,6 @@
import React 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 { createMemoryRouter, RouterProvider, Route, Routes, useLocation } from "react-router-dom";
import { api } from "./api";
@@ -27,7 +27,7 @@ jest.mock("./components/ApplicationAssets", () => ({
</div>
),
ApplicationCvSection: () => <div>CV section</div>,
ApplicationPackageDraftsSection: ({ onDirtyChange }: { onDirtyChange?: (dirty: boolean) => void }) => <div>Application drafts section<button onClick={() => onDirtyChange?.(true)}>Make answers dirty</button></div>,
ApplicationPackageDraftsSection: ({ jobId, initialApplicationAnswer, initialRecruiterMessage, onDirtyChange }: { jobId: number; initialApplicationAnswer: string; initialRecruiterMessage: string; onDirtyChange?: (dirty: boolean) => void }) => <div>Application drafts section<output data-testid="answer-props">{jobId}|{initialApplicationAnswer}|{initialRecruiterMessage}</output><button onClick={() => onDirtyChange?.(true)}>Make answers dirty</button></div>,
SubmittedPackagesSection: () => <div>Frozen packages section</div>,
}));
jest.mock("./components/InterviewPrep", () => ({ ApplicationInterviewPrep: () => <div>Interview section</div> }));
@@ -136,7 +136,7 @@ function renderTable(path = "/jobs") {
},
], { initialEntries: [path] });
return render(
const mounted = render(
<ToastProvider>
<I18nProvider>
<ConfirmProvider>
@@ -147,6 +147,7 @@ function renderTable(path = "/jobs") {
</I18nProvider>
</ToastProvider>,
);
return { ...mounted, router };
}
beforeEach(() => {
@@ -383,3 +384,30 @@ test("legacy correspondence and notes reveal distinct domain content", async ()
expect(screen.queryByText("Application drafts section")).not.toBeInTheDocument();
expect(screen.getByTestId("location")).toHaveTextContent("followMode=waiting-update");
});
test("holds answers until their aggregate matches the route and ignores a late previous-job aggregate", async () => {
let resolveB!: (value: any) => void;
let resolveC!: (value: any) => void;
const heldB = new Promise<any>((resolve) => { resolveB = resolve; });
const heldC = new Promise<any>((resolve) => { resolveC = resolve; });
const originalGet = mockedApi.get.getMockImplementation()!;
mockedApi.get.mockImplementation((url: string, ...rest: any[]) => {
if (url === "/jobapplications/43/workspace") return heldB;
if (url === "/jobapplications/44/workspace") return heldC;
return originalGet(url, ...rest);
});
const { router } = renderTable("/jobs/42?section=application&sub=answers");
expect(await screen.findByTestId("answer-props")).toHaveTextContent("42|Saved answer|Saved recruiter message");
await act(async () => { await router.navigate("/jobs/43?section=application&sub=answers"); });
await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith("/jobapplications/43/workspace"));
expect(screen.queryByTestId("answer-props")).not.toBeInTheDocument();
await act(async () => { await router.navigate("/jobs/44?section=application&sub=answers"); });
expect(screen.queryByTestId("answer-props")).not.toBeInTheDocument();
await act(async () => { resolveC({ data: { ...overview, id: 44, jobTitle: "Job C", applicationAnswerDraft: "C answer", recruiterMessageDraft: "C message" } }); await heldC; });
expect(await screen.findByTestId("answer-props")).toHaveTextContent("44|C answer|C message");
await act(async () => { resolveB({ data: { ...overview, id: 43, jobTitle: "Late Job B", applicationAnswerDraft: "B answer", recruiterMessageDraft: "B message" } }); await heldB; });
expect(screen.getByTestId("answer-props")).toHaveTextContent("44|C answer|C message");
expect(screen.getByRole("heading", { name: "Job C" })).toBeInTheDocument();
expect(mockedApi.post).not.toHaveBeenCalled();
expect(mockedApi.put).not.toHaveBeenCalled();
});