feat(ui): redesign job workspace sections

This commit is contained in:
cesnimda
2026-10-07 01:58:31 +02:00
parent acd1d94684
commit ac6d78289d
123 changed files with 2842 additions and 205 deletions
+25 -9
View File
@@ -3,7 +3,7 @@ import "@testing-library/jest-dom";
import { fireEvent, render as rtlRender, screen, waitFor } from "@testing-library/react";
import {
ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection,
ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection, SubmittedPackagesSection,
} from "./components/ApplicationAssets";
import { api } from "./api";
import { I18nProvider } from "./i18n/I18nProvider";
@@ -74,6 +74,22 @@ beforeEach(() => {
afterEach(() => window.localStorage.removeItem("uiLanguage"));
test("document package history is read-only and separates captured notes from answers", async () => {
const summary = { id: 91, version: 1, cvVariantId: 3, cvVariantName: "Original CV", cvVariantVersion: 2, cvThemeId: "nordic", hasCv: false, hasCoverLetter: true, createdAtUtc: "2026-07-19T10:00:00Z", attachments: [] };
mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url.endsWith('/91') ? {
summary, renderedCvHtml: null, coverLetterText: "Frozen letter", applicationMaterialJson: JSON.stringify({ Notes: "Human notes\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nFrozen answer\n<<<END_APPLICATION_ANSWER_DRAFT>>>" }),
} : [summary] } as any));
render(<SubmittedPackagesSection jobId={7} allowCapture={false} />);
fireEvent.click(await screen.findByRole("button", { name: "View preserved package" }));
expect(await screen.findByRole("dialog")).toHaveTextContent("immutable submission snapshot");
expect(screen.getByText("Human notes", { exact: true })).toBeInTheDocument();
expect(screen.getByText("Frozen answer", { exact: true })).toBeInTheDocument();
expect(screen.queryByText(/<<<APPLICATION/)).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Freeze current package" })).not.toBeInTheDocument();
expect(mockedApi.post).not.toHaveBeenCalled();
expect(mockedApi.put).not.toHaveBeenCalled();
});
// ---------- CV ----------
test("cv section shows the attached variant and the ones available to attach", async () => {
@@ -161,7 +177,7 @@ test("editing marks the draft dirty and saving sends the new text", async () =>
const onDirtyChange = jest.fn();
render(<ApplicationCoverLetterSection jobId={7} onDirtyChange={onDirtyChange} />);
fireEvent.change(await screen.findByLabelText("Cover letter"), { target: { value: "Dear hiring team" } });
fireEvent.change(await screen.findByRole("textbox", { name: "Cover letter" }), { target: { value: "Dear hiring team" } });
expect(screen.getByText("Unsaved changes")).toBeInTheDocument();
expect(onDirtyChange).toHaveBeenLastCalledWith(true);
@@ -177,7 +193,7 @@ test("discarding returns to the saved text", async () => {
routeGet();
render(<ApplicationCoverLetterSection jobId={7} />);
fireEvent.change(await screen.findByLabelText("Cover letter"), { target: { value: "scratch" } });
fireEvent.change(await screen.findByRole("textbox", { name: "Cover letter" }), { target: { value: "scratch" } });
fireEvent.click(screen.getByRole("button", { name: /Discard changes/i }));
expect(await screen.findByDisplayValue("Dear team")).toBeInTheDocument();
@@ -202,7 +218,7 @@ test("an empty cover letter offers the template and an empty history", async ()
expect(await screen.findByText(/No versions yet/i)).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /Start from template/i }));
expect((screen.getByLabelText("Cover letter") as HTMLTextAreaElement).value)
expect((screen.getByRole("textbox", { name: "Cover letter" }) as HTMLTextAreaElement).value)
.toContain("Dear Hiring Manager");
});
@@ -213,7 +229,7 @@ test("the empty cover-letter template and rich-text controls follow Bokmål UI l
render(<ApplicationCoverLetterSection jobId={7} />);
fireEvent.click(await screen.findByRole("button", { name: /start fra mal/i }));
expect((screen.getByLabelText("Søknadsbrev") as HTMLTextAreaElement).value)
expect((screen.getByRole("textbox", { name: "Søknadsbrev" }) as HTMLTextAreaElement).value)
.toContain("Kjære rekrutteringsansvarlig");
expect(screen.getByRole("button", { name: "Fet" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Lenke" })).toBeInTheDocument();
@@ -250,9 +266,9 @@ test("AI cover letter suggestions use the linked CV and require explicit apply",
action: "generate",
}),
));
expect(screen.getByLabelText("Cover letter")).toHaveValue("Dear team");
expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("Dear team");
fireEvent.click(await screen.findByRole("button", { name: /Apply to editor/i }));
expect(screen.getByLabelText("Cover letter")).toHaveValue("A tailored, truthful suggestion.");
expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("A tailored, truthful suggestion.");
mockedApi.put.mockResolvedValue({ data: { ...coverLetter, text: "A tailored, truthful suggestion." } } as any);
fireEvent.click(screen.getByRole("button", { name: "Save" }));
@@ -272,10 +288,10 @@ test("[PRES-W06] rejecting an AI cover letter preserves the saved draft without
expect(screen.queryByText(/\{name\}/)).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Generate" }));
await screen.findByText("Unaccepted letter");
expect(screen.getByLabelText("Cover letter")).toHaveValue("Dear team");
expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("Dear team");
fireEvent.click(screen.getByRole("button", { name: "Reject" }));
expect(screen.queryByText("Unaccepted letter")).not.toBeInTheDocument();
expect(screen.getByLabelText("Cover letter")).toHaveValue("Dear team");
expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("Dear team");
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
expect(onDirtyChange).not.toHaveBeenCalledWith(true);
expect(mockedApi.put).not.toHaveBeenCalled();