mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-13 06:24:54 +10:00
b932711f08
* feat: add section heading icons to PDF templates Add customizable Phosphor icons before section titles in PDF output. Users can toggle visibility globally via a new "Hide section heading icons" switch (independent of item-level icons) and customize individual section icons through the builder sidebar icon picker. - Add `icon` field to `baseSectionSchema` and `summarySchema` - Add `hideSectionIcons` to `pageSchema` (defaults to true for backward compat) - Implement `SectionHeadingIcon` component with heading font-size scaling - Support "none" sentinel for per-section icon hiding - Fallback to sensible defaults (briefcase, graduation-cap, etc.) for legacy data - Add icon picker to builder sidebar sections and custom section dialogs Closes #2632 * test: add unit tests for section heading icons - Add tests for getResumeSectionIcon() covering built-in sections, summary, custom sections, "none" sentinel, and default fallbacks - Add schema tests for baseSectionSchema icon field, summarySchema icon, and pageSchema hideSectionIcons default behavior * refactor: minor updates to icon display * Update apps/web/locales/es-ES.po Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Amruth Pillai <im.amruth@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
164 lines
4.7 KiB
TypeScript
164 lines
4.7 KiB
TypeScript
// @vitest-environment happy-dom
|
|
|
|
import type { CustomSection, ResumeData, SectionType } from "@reactive-resume/schema/resume/data";
|
|
import { describe, expect, it } from "vitest";
|
|
import { getDefaultSectionIconName } from "@reactive-resume/schema/resume/section-icons";
|
|
import { renderBuiltInSection, renderCustomSection, renderSummary, setRenderConfig } from "./section-renderers";
|
|
|
|
const baseConfig = {
|
|
headingFont: "Inter",
|
|
headingSizeHalfPt: 28,
|
|
bodyFont: "Inter",
|
|
bodySizeHalfPt: 20,
|
|
textColorHex: "111111",
|
|
primaryColorHex: "0563C1",
|
|
};
|
|
|
|
setRenderConfig(baseConfig);
|
|
|
|
const HEX = "#0563C1";
|
|
|
|
describe("renderSummary", () => {
|
|
it("returns [] when the section is hidden", () => {
|
|
const summary: ResumeData["summary"] = {
|
|
title: "Summary",
|
|
icon: getDefaultSectionIconName("summary"),
|
|
content: "<p>Hello</p>",
|
|
hidden: true,
|
|
columns: 1,
|
|
};
|
|
expect(renderSummary(summary, HEX)).toEqual([]);
|
|
});
|
|
|
|
it("returns [] when content is empty", () => {
|
|
const summary: ResumeData["summary"] = {
|
|
title: "Summary",
|
|
icon: getDefaultSectionIconName("summary"),
|
|
content: "",
|
|
hidden: false,
|
|
columns: 1,
|
|
};
|
|
expect(renderSummary(summary, HEX)).toEqual([]);
|
|
});
|
|
|
|
it("includes a heading paragraph when both title and content are present", () => {
|
|
const summary: ResumeData["summary"] = {
|
|
title: "Summary",
|
|
icon: getDefaultSectionIconName("summary"),
|
|
content: "<p>Hello world</p>",
|
|
hidden: false,
|
|
columns: 1,
|
|
};
|
|
const paragraphs = renderSummary(summary, HEX);
|
|
// One heading + the htmlToParagraphs output for one <p>.
|
|
expect(paragraphs.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it("omits the heading when title is empty but still renders content", () => {
|
|
const summary: ResumeData["summary"] = {
|
|
title: "",
|
|
icon: getDefaultSectionIconName("summary"),
|
|
content: "<p>Hello world</p>",
|
|
hidden: false,
|
|
columns: 1,
|
|
};
|
|
const paragraphs = renderSummary(summary, HEX);
|
|
expect(paragraphs.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
const emptySection = <T extends SectionType>(type: T): ResumeData["sections"][T] =>
|
|
({
|
|
title: "Section",
|
|
icon: getDefaultSectionIconName(type),
|
|
columns: 1,
|
|
hidden: false,
|
|
items: [],
|
|
}) as ResumeData["sections"][T];
|
|
|
|
describe("renderBuiltInSection", () => {
|
|
it("returns [] when the section has no items", () => {
|
|
expect(renderBuiltInSection("experience", emptySection("experience"), HEX)).toEqual([]);
|
|
});
|
|
|
|
it("returns [] when section.hidden is true", () => {
|
|
const section = { ...emptySection("experience"), hidden: true };
|
|
expect(renderBuiltInSection("experience", section, HEX)).toEqual([]);
|
|
});
|
|
|
|
it("returns [] for an unknown section type", () => {
|
|
expect(renderBuiltInSection("not-a-section" as never, emptySection("experience"), HEX)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("renderCustomSection", () => {
|
|
const baseCustom: CustomSection = {
|
|
id: "custom-1",
|
|
type: "summary",
|
|
title: "Notes",
|
|
icon: getDefaultSectionIconName("summary"),
|
|
columns: 1,
|
|
hidden: false,
|
|
items: [],
|
|
};
|
|
|
|
it("returns [] when the custom section is hidden", () => {
|
|
expect(renderCustomSection({ ...baseCustom, hidden: true }, HEX)).toEqual([]);
|
|
});
|
|
|
|
it("returns [] when all items are hidden or empty", () => {
|
|
const section: CustomSection = {
|
|
...baseCustom,
|
|
items: [{ id: "x", hidden: true } as never],
|
|
};
|
|
expect(renderCustomSection(section, HEX)).toEqual([]);
|
|
});
|
|
|
|
it("returns [] for an unknown custom section type", () => {
|
|
const section: CustomSection = {
|
|
...baseCustom,
|
|
type: "no-such-type" as never,
|
|
items: [{ id: "x", hidden: false, content: "<p>x</p>" } as never],
|
|
};
|
|
expect(renderCustomSection(section, HEX)).toEqual([]);
|
|
});
|
|
|
|
it("renders content for a summary-type custom section", () => {
|
|
const section: CustomSection = {
|
|
...baseCustom,
|
|
type: "summary",
|
|
items: [{ id: "x", hidden: false, content: "<p>Hello</p>" } as never],
|
|
};
|
|
const paragraphs = renderCustomSection(section, HEX);
|
|
// 1 heading + at least one paragraph for the HTML
|
|
expect(paragraphs.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it("renders recipient + content for a cover-letter custom section", () => {
|
|
const section: CustomSection = {
|
|
...baseCustom,
|
|
type: "cover-letter",
|
|
items: [
|
|
{
|
|
id: "x",
|
|
hidden: false,
|
|
recipient: "<p>Dear Jane,</p>",
|
|
content: "<p>Body</p>",
|
|
} as never,
|
|
],
|
|
};
|
|
const paragraphs = renderCustomSection(section, HEX);
|
|
// 1 heading + at least two paragraphs (recipient + body)
|
|
expect(paragraphs.length).toBeGreaterThanOrEqual(3);
|
|
});
|
|
});
|
|
|
|
describe("setRenderConfig", () => {
|
|
it("can be called repeatedly with a different config (no throw)", () => {
|
|
expect(() => setRenderConfig({ ...baseConfig, headingFont: "Roboto", primaryColorHex: "ff0000" })).not.toThrow();
|
|
|
|
// Restore for any later tests in the file
|
|
setRenderConfig(baseConfig);
|
|
});
|
|
});
|