feat(cv): qualify templates and reliable local import processing

This commit is contained in:
cesnimda
2026-10-28 14:36:12 +01:00
parent 73ea888375
commit 3f4db8ca81
69 changed files with 2597 additions and 180 deletions
@@ -1,7 +1,7 @@
import React from "react";
import "@testing-library/jest-dom";
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { CoverLetterDocumentFields } from "./components/CoverLetterDocument";
import { CoverLetterDocumentFields, CoverLetterDocumentPreview } from "./components/CoverLetterDocument";
import { ApplicationCoverLetterSection } from "./components/ApplicationAssets";
import { CoverLetterDocumentSettings } from "./applicationWorkspace";
import { api } from "./api";
@@ -116,11 +116,93 @@ test("design copy resolves null overrides through actual theme defaults and effe
await screen.findByRole("region", { name: "Design snapshot" });
expect(onChange).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: "Use this copy" }));
expect(onChange).toHaveBeenCalledWith({ ...settings, accentColor: "#112233", bodyFont: "Georgia, 'Times New Roman', serif", baseFontSizePt: 11, lineHeight: 1.5, pageMarginMm: 13.12, sourceCvId: 3 });
expect(onChange).toHaveBeenCalledWith({ ...settings, designVersion: 2, accentColor: "#112233", bodyFont: "Georgia, 'Times New Roman', serif", headingFont: "Georgia, 'Times New Roman', serif", baseFontSizePt: 11, lineHeight: 1.5, pageMarginMm: 13.12, sourceCvId: 3 });
expect(mockedApi.get).not.toHaveBeenCalledWith("/career/profile");
expect(mockedApi.put).not.toHaveBeenCalled(); expect(mockedApi.post).not.toHaveBeenCalled();
});
test("letter template chooser uses nine actual matching themes and preserves destination letter details", async () => {
const originalGet = mockedApi.get.getMockImplementation()!;
const themeRows = ["Modern", "Minimal", "Executive", "Technical", "ATS Classic", "Nordic", "Elegant", "Creative", "Code"].map(name => ({
id: name.toLowerCase().replace(/ /g, "-"), name, description: `${name} letter design`, available: name !== "Executive", requiresPro: name === "Executive",
accent: "#3157d5", bodyFont: "Arial, Helvetica, sans-serif", headingFont: "Georgia, 'Times New Roman', serif", baseFontSizePt: 10, lineHeight: 1.42, pageMarginMm: 18,
}));
mockedApi.get.mockImplementation(((url: string) => url === "/cv/themes" ? Promise.resolve({ data: themeRows }) : originalGet(url)) as any);
const onChange = jest.fn();
render(wrap(<CoverLetterDocumentFields settings={{ ...settings, pageSize: "letter" }} disabled={false} onChange={onChange} />));
fireEvent.click(screen.getByRole("button", { name: "Choose letter template" }));
const select = await screen.findByRole("combobox", { name: "Letter template" });
await waitFor(() => expect(select).not.toHaveAttribute("aria-disabled", "true"));
fireEvent.mouseDown(select);
expect(await screen.findAllByRole("option")).toHaveLength(9);
expect(screen.getByRole("option", { name: /Executive/ })).toHaveAttribute("aria-disabled", "true");
fireEvent.click(screen.getByRole("option", { name: "Nordic" }));
expect(onChange).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: "Use this template" }));
expect(onChange).toHaveBeenCalledWith({ ...settings, pageSize: "letter", designVersion: 2, themeId: "nordic", accentColor: "#3157d5", bodyFont: "Arial, Helvetica, sans-serif", headingFont: "Georgia, 'Times New Roman', serif", baseFontSizePt: 10, lineHeight: 1.42, pageMarginMm: 18 });
expect(mockedApi.put).not.toHaveBeenCalled();
expect(mockedApi.post).not.toHaveBeenCalled();
});
test("letter template load failures retry and cancel never changes the local letter", async () => {
const originalGet = mockedApi.get.getMockImplementation()!;
let attempts = 0;
mockedApi.get.mockImplementation(((url: string) => url === "/cv/themes" && ++attempts === 1 ? Promise.reject(new Error("Synthetic theme failure")) : originalGet(url)) as any);
const onChange = jest.fn();
render(wrap(<CoverLetterDocumentFields settings={settings} disabled={false} onChange={onChange} />));
fireEvent.click(screen.getByRole("button", { name: "Choose letter template" }));
expect(await screen.findByRole("alert")).toHaveTextContent("Could not load letter templates.");
expect(screen.getByRole("button", { name: "Use this template" })).toBeDisabled();
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
await waitFor(() => expect(screen.getByRole("button", { name: "Use this template" })).toBeEnabled());
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(onChange).not.toHaveBeenCalled();
});
test("applying a letter template updates the existing letter preview using the current body without saving", async () => {
mockedApi.post.mockResolvedValue({ data: { html: '<main class="page">Actual letter preview</main>' } } as any);
function Draft() {
const [draft, setDraft] = React.useState(settings);
return <><CoverLetterDocumentFields settings={draft} disabled={false} onChange={setDraft} />
<CoverLetterDocumentPreview jobId={7} text="Current authored letter body" settings={draft} active /></>;
}
render(wrap(<Draft />));
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledTimes(1));
fireEvent.click(screen.getByRole("button", { name: "Choose letter template" }));
await waitFor(() => expect(screen.getByRole("button", { name: "Use this template" })).toBeEnabled());
fireEvent.click(screen.getByRole("button", { name: "Use this template" }));
await waitFor(() => expect(mockedApi.post).toHaveBeenLastCalledWith('/jobapplications/7/cover-letter/preview', {
text: 'Current authored letter body', documentSettings: expect.objectContaining({ designVersion: 2, headingFont: "Georgia, 'Times New Roman', serif", recipient: settings.recipient, language: settings.language }),
}));
expect(mockedApi.put).not.toHaveBeenCalled();
expect(mockedApi.get).not.toHaveBeenCalledWith('/cv/themes/modern/preview');
});
test("disabled letter drafting cannot open the template chooser", () => {
const onChange = jest.fn();
render(wrap(<CoverLetterDocumentFields settings={settings} disabled onChange={onChange} />));
const chooser = screen.getByRole('button', { name: 'Choose letter template' });
expect(chooser).toBeDisabled();
fireEvent.click(chooser);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(onChange).not.toHaveBeenCalled();
expect(mockedApi.get).not.toHaveBeenCalled();
});
test("matching a customized CV copies heading density and paper but preserves letter language", async () => {
const originalGet = mockedApi.get.getMockImplementation()!;
mockedApi.get.mockImplementation(((url: string) => url === "/cv/variants/3" ? Promise.resolve({ data: { ...variant, settings: {
...variant.settings, headingFont: "Georgia, 'Times New Roman', serif", bodyFont: "Arial, Helvetica, sans-serif", accentColor: "#abcdef", pageMarginMm: 20, density: "roomy", pageSize: "letter",
} } }) : originalGet(url)) as any);
const onChange = jest.fn();
render(wrap(<CoverLetterDocumentFields settings={settings} disabled={false} onChange={onChange} />));
fireEvent.click(screen.getByRole("button", { name: "Match CV design" }));
await selectSource();
await screen.findByRole("region", { name: "Design snapshot" });
fireEvent.click(screen.getByRole("button", { name: "Use this copy" }));
expect(onChange).toHaveBeenCalledWith({ ...settings, pageSize: "letter", designVersion: 2, headingFont: "Georgia, 'Times New Roman', serif", bodyFont: "Arial, Helvetica, sans-serif", accentColor: "#abcdef", baseFontSizePt: 11, lineHeight: 1.5, pageMarginMm: 23.6, sourceCvId: 3 });
});
test("closing a copy dialog ignores a late selected CV read", async () => {
const selected = deferred<any>();
const originalGet = mockedApi.get.getMockImplementation()!;