145 lines
9.7 KiB
TypeScript
145 lines
9.7 KiB
TypeScript
import React, { useState } from "react";
|
|
import "@testing-library/jest-dom";
|
|
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
import CvCustomizeTab from "./components/cv/CvCustomizeTab";
|
|
import { applyCvTheme, CvTheme, CvVariantSettings, emptyCvVariantSettings } from "./cvBuilder";
|
|
import { ConfirmProvider } from "./confirm";
|
|
import { I18nProvider } from "./i18n/I18nProvider";
|
|
|
|
const themes: CvTheme[] = [
|
|
{ id: "modern", name: "Modern", category: "Professional", description: "Default", layout: "header-band", accent: "#3157d5", photoShape: "circle", supportsIcons: true, atsFriendly: true, requiresPro: false, available: true, swatches: [] },
|
|
{ id: "code", name: "Code", category: "Technical", description: "Text first", layout: "single", accent: "#334155", photoShape: "none", supportsIcons: false, atsFriendly: true, requiresPro: false, available: true, swatches: [], supportedSettings: ["accent", "typography", "spacing", "skills", "page"] },
|
|
{ id: "technical", name: "Technical", category: "Technical", description: "Sidebar", layout: "sidebar-left", accent: "#0f766e", photoShape: "circle", supportsIcons: true, atsFriendly: true, requiresPro: true, available: false, swatches: [] },
|
|
];
|
|
|
|
beforeEach(() => localStorage.clear());
|
|
|
|
function mount(initial = emptyCvVariantSettings(), mode: "customize" | "template" = "customize", availableThemes = themes) {
|
|
const update = jest.fn();
|
|
let current = initial;
|
|
function Harness() {
|
|
const [settings, setSettings] = useState(initial);
|
|
current = settings;
|
|
return <CvCustomizeTab mode={mode} settings={settings} themes={availableThemes} sectionLabel={(_, fallback) => fallback} update={patch => { update(patch); setSettings(previous => ({ ...previous, ...patch })); }} />;
|
|
}
|
|
render(<I18nProvider><ConfirmProvider><Harness /></ConfirmProvider></I18nProvider>);
|
|
return { update, settings: () => current };
|
|
}
|
|
|
|
async function choose(label: string, option: string) {
|
|
fireEvent.mouseDown(screen.getByRole("combobox", { name: label }));
|
|
fireEvent.click(await screen.findByRole("option", { name: option }));
|
|
}
|
|
|
|
test("[PRES-C08 C09 E08] common controls and advanced disclosure do not mutate document settings", () => {
|
|
const { update } = mount();
|
|
expect(screen.getByRole("region", { name: "Style" })).toBeInTheDocument();
|
|
expect(screen.getByRole("region", { name: "Layout" })).toBeInTheDocument();
|
|
expect(screen.getByRole("spinbutton", { name: "Page margins" })).toHaveValue(null);
|
|
expect(screen.getByRole("combobox", { name: "Page size" })).toBeInTheDocument();
|
|
expect(screen.getByRole("combobox", { name: "Heading font" })).toHaveTextContent("Template default");
|
|
const advanced = screen.getByRole("button", { name: "Advanced document settings" });
|
|
expect(advanced).toHaveAttribute("aria-expanded", "false");
|
|
fireEvent.click(advanced);
|
|
expect(advanced).toHaveAttribute("aria-expanded", "true");
|
|
expect(screen.getByRole("combobox", { name: "Heading treatment" })).toBeInTheDocument();
|
|
expect(screen.getByRole("switch", { name: "Show profile photo" })).toBeInTheDocument();
|
|
expect(update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("[PRES-C09 C10] numeric edits reject invalid ranges, keep exact valid values and reset only one setting", () => {
|
|
const initial = { ...emptyCvVariantSettings(), sectionGapMm: 7, pageMarginMm: 16 };
|
|
const { update, settings } = mount(initial);
|
|
const margins = screen.getByRole("spinbutton", { name: "Page margins" });
|
|
fireEvent.change(margins, { target: { value: "29" } });
|
|
expect(margins).toHaveAttribute("aria-invalid", "true");
|
|
expect(update).not.toHaveBeenCalled();
|
|
expect(settings().pageMarginMm).toBe(16);
|
|
fireEvent.change(margins, { target: { value: "20" } });
|
|
expect(update).toHaveBeenLastCalledWith({ pageMarginMm: 20 });
|
|
fireEvent.click(screen.getByRole("button", { name: "Use template default: Page margins" }));
|
|
expect(update).toHaveBeenLastCalledWith({ pageMarginMm: null });
|
|
expect(margins).toHaveValue(null);
|
|
expect(settings().sectionGapMm).toBe(7);
|
|
fireEvent.change(margins, { target: { value: "18" } });
|
|
fireEvent.change(margins, { target: { value: "" } });
|
|
expect(update).toHaveBeenLastCalledWith({ pageMarginMm: null });
|
|
});
|
|
|
|
test("[PRES-C07 C08 C09] capability filtering preserves dormant saved values and exposes inherited sidebar controls", async () => {
|
|
const initial = { ...emptyCvVariantSettings(), themeId: "technical", layout: null, headerStyle: "centered" as const, backgroundColor: "#eeeeee", overrides: { experience: { title: "CV only" } } };
|
|
const { update, settings } = mount(initial);
|
|
fireEvent.click(screen.getByRole("button", { name: "Advanced document settings" }));
|
|
expect(screen.getByRole("spinbutton", { name: "Sidebar width" })).toHaveValue(null);
|
|
fireEvent.click(screen.getByRole("button", { name: "Choose sidebar content" }));
|
|
expect(update).toHaveBeenLastCalledWith({ sidebarSections: ["contact", "skills", "languages"] });
|
|
fireEvent.click(screen.getByRole("switch", { name: "Projects" }));
|
|
expect(update).toHaveBeenLastCalledWith({ sidebarSections: ["contact", "skills", "languages", "projects"] });
|
|
fireEvent.click(screen.getByRole("button", { name: "Use template default: Sidebar content" }));
|
|
expect(settings().sidebarSections).toBeNull();
|
|
await choose("Body font", "Arial");
|
|
expect(update).toHaveBeenLastCalledWith({ bodyFont: "Arial, Helvetica, sans-serif" });
|
|
expect(settings().headerStyle).toBe("centered");
|
|
expect(settings().backgroundColor).toBe("#eeeeee");
|
|
expect(settings().overrides).toEqual(initial.overrides);
|
|
await choose("Skills presentation", "Grouped skills");
|
|
fireEvent.change(screen.getByRole("textbox", { name: "Skill groups" }), { target: { value: "Språk: Norsk, English" } });
|
|
expect(update).toHaveBeenLastCalledWith({ skillGroups: [{ name: "Språk", items: ["Norsk", "English"] }] });
|
|
await choose("Skills presentation", "Bullet list");
|
|
expect(screen.getByRole("textbox", { name: "Skill groups" })).toHaveValue("Språk: Norsk, English");
|
|
fireEvent.click(screen.getByRole("switch", { name: "Show profile photo" }));
|
|
expect(update).toHaveBeenLastCalledWith({ showPhoto: true });
|
|
});
|
|
|
|
test("[PRES-C08 C10 E09] locks prevent selection and confirmed template resets preserve content and document metadata", async () => {
|
|
const initial = { ...emptyCvVariantSettings(), accentColor: "#9f1239", pageMarginMm: 20, pageSize: "letter", language: "nb-NO", overrides: { experience: { title: "CV only" } }, skillGroups: [{ name: "Skills", items: ["C#"] }] };
|
|
const { update, settings } = mount(initial, "template");
|
|
const locked = screen.getByRole("button", { name: "Technical" });
|
|
expect(locked).toHaveAttribute("aria-disabled", "true");
|
|
expect(locked).toHaveAccessibleDescription("Technical Sidebar ATS-friendly Pro");
|
|
fireEvent.click(locked);
|
|
fireEvent.keyDown(locked, { key: "Enter" });
|
|
expect(update).not.toHaveBeenCalled();
|
|
fireEvent.keyDown(screen.getByRole("button", { name: "Modern" }), { key: " " });
|
|
fireEvent.click(within(await screen.findByRole("dialog", { name: "Apply template defaults?" })).getByRole("button", { name: "Cancel" }));
|
|
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
|
expect(update).not.toHaveBeenCalled();
|
|
fireEvent.keyDown(screen.getByRole("button", { name: "Code" }), { key: "Enter" });
|
|
const dialog = await screen.findByRole("dialog", { name: "Apply template defaults?" });
|
|
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
|
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
|
expect(update).not.toHaveBeenCalled();
|
|
fireEvent.click(screen.getByRole("button", { name: "Code" }));
|
|
fireEvent.click(within(await screen.findByRole("dialog", { name: "Apply template defaults?" })).getByRole("button", { name: "Apply template" }));
|
|
await waitFor(() => expect(update).toHaveBeenLastCalledWith(applyCvTheme(initial, "code")));
|
|
expect(settings().overrides).toEqual(initial.overrides);
|
|
expect(settings().pageSize).toBe("letter");
|
|
expect(settings().language).toBe("nb-NO");
|
|
expect(settings().skillGroups).toEqual(initial.skillGroups);
|
|
});
|
|
|
|
test("[PRES-C08 C09 E08] Bokmål controls expose their scope, values and non-colour selection state", () => {
|
|
localStorage.setItem("uiLanguage", "nb");
|
|
const { update } = mount({ ...emptyCvVariantSettings(), accentColor: "#3157d5" });
|
|
expect(screen.getByRole("region", { name: "Stil" })).toBeInTheDocument();
|
|
expect(screen.getByRole("region", { name: "Oppsett" })).toBeInTheDocument();
|
|
const accent = screen.getByRole("button", { name: "Bruk aksentfargen #3157d5" });
|
|
expect(accent).toHaveAttribute("aria-pressed", "true");
|
|
expect(screen.getByLabelText("Egendefinert aksentfarge")).toHaveValue("#3157d5");
|
|
fireEvent.click(screen.getByRole("button", { name: "Avanserte dokumentinnstillinger" }));
|
|
expect(update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("[PRES-C08 C09] unsupported and legacy saved values survive unrelated edits without implicit normalization", async () => {
|
|
const initial = { ...emptyCvVariantSettings(), themeId: "code", headerStyle: "centered" as const, layout: "sidebar-left" as const, pageMarginMm: 15.24, pageSize: "legacy-paper", backgroundColor: "#eeeeee" };
|
|
const { update, settings } = mount(initial);
|
|
expect(screen.getByRole("spinbutton", { name: "Page margins" })).toHaveValue(15.24);
|
|
expect(screen.getByRole("combobox", { name: "Page size" })).toHaveTextContent("Saved value: legacy-paper");
|
|
fireEvent.click(screen.getByRole("button", { name: "Advanced document settings" }));
|
|
expect(screen.queryByRole("combobox", { name: "Header treatment" })).not.toBeInTheDocument();
|
|
expect(update).not.toHaveBeenCalled();
|
|
await choose("Body font", "Arial");
|
|
expect(update).toHaveBeenCalledTimes(1);
|
|
expect(settings()).toEqual({ ...initial, bodyFont: "Arial, Helvetica, sans-serif" });
|
|
});
|