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 fallback} update={patch => { update(patch); setSettings(previous => ({ ...previous, ...patch })); }} />; } render(); 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" }); });