mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-24 15:22:20 +10:00
test: add unit and component tests across the monorepo
Adds ~1000 tests to bring the previously-untested packages and apps under coverage: - packages/utils — string, color, date, file, level, locale, sanitize, field, html, network-icons, rate-limit, url, url-security, monorepo, resume/patch, resume/docx/link-utils, style helpers (~97% on the testable utility files) - packages/ui — 28 component test files plus the use-controlled-state, use-mobile, use-confirm, use-prompt hooks (95% statements) - packages/pdf — shared template helpers (filtering, columns, picture, metrics, section-links, page-size, rich-text-html, section-title) - packages/schema — resumeDataSchema, page, templates, default - packages/fonts — expanded coverage on font helpers - packages/ai — resume sanitize and extraction template - packages/api — resume-access-policy - apps/web — error-message, locale, theme, pwa, dialogs/store, and the resume/move-item / section-actions / make-section-item helpers Adds jsdom polyfills (ResizeObserver, IntersectionObserver, scrollIntoView, matchMedia) and an explicit React Testing Library cleanup hook to vitest.setup.ts so portal- and overlay-based components work without per-test setup.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getSectionItemRows, getSectionItemsLayout, shouldUseSectionTimeline } from "./columns";
|
||||
|
||||
describe("getSectionItemsLayout", () => {
|
||||
it("returns single-column layout for columns=1", () => {
|
||||
const layout = getSectionItemsLayout({ columns: 1, rowGap: 4, columnGap: 6 });
|
||||
expect(layout.columns).toBe(1);
|
||||
expect(layout.isGrid).toBe(false);
|
||||
expect(layout.rowStyle).toBeUndefined();
|
||||
expect(layout.itemStyle).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns grid layout for columns=2", () => {
|
||||
const layout = getSectionItemsLayout({ columns: 2, rowGap: 4, columnGap: 6 });
|
||||
expect(layout.columns).toBe(2);
|
||||
expect(layout.isGrid).toBe(true);
|
||||
expect(layout.rowStyle).toEqual({ flexDirection: "row", columnGap: 6 });
|
||||
expect(layout.itemStyle).toMatchObject({
|
||||
flexBasis: 0,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
maxWidth: "100%",
|
||||
overflow: "hidden",
|
||||
});
|
||||
});
|
||||
|
||||
it("clamps columns above 6 to 6", () => {
|
||||
const layout = getSectionItemsLayout({ columns: 99, rowGap: 4, columnGap: 6 });
|
||||
expect(layout.columns).toBe(6);
|
||||
});
|
||||
|
||||
it("clamps columns below 1 to 1", () => {
|
||||
const layout = getSectionItemsLayout({ columns: 0, rowGap: 4, columnGap: 6 });
|
||||
expect(layout.columns).toBe(1);
|
||||
});
|
||||
|
||||
it("treats non-integer columns as 1", () => {
|
||||
const layout = getSectionItemsLayout({ columns: 2.5, rowGap: 4, columnGap: 6 });
|
||||
expect(layout.columns).toBe(1);
|
||||
});
|
||||
|
||||
it("treats non-numeric columns as 1", () => {
|
||||
const layout = getSectionItemsLayout({ columns: "2", rowGap: 4, columnGap: 6 });
|
||||
expect(layout.columns).toBe(1);
|
||||
});
|
||||
|
||||
it("treats Infinity as 1", () => {
|
||||
const layout = getSectionItemsLayout({ columns: Number.POSITIVE_INFINITY, rowGap: 4, columnGap: 6 });
|
||||
expect(layout.columns).toBe(1);
|
||||
});
|
||||
|
||||
it("includes containerStyle.rowGap", () => {
|
||||
const layout = getSectionItemsLayout({ columns: 1, rowGap: 8, columnGap: 6 });
|
||||
expect(layout.containerStyle).toEqual({ rowGap: 8 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSectionItemRows", () => {
|
||||
it("groups items into rows of N columns", () => {
|
||||
expect(getSectionItemRows([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
|
||||
});
|
||||
|
||||
it("returns single-item rows for columns=1", () => {
|
||||
expect(getSectionItemRows([1, 2, 3], 1)).toEqual([[1], [2], [3]]);
|
||||
});
|
||||
|
||||
it("returns one row when items fit in one row", () => {
|
||||
expect(getSectionItemRows([1, 2], 3)).toEqual([[1, 2]]);
|
||||
});
|
||||
|
||||
it("returns empty array for empty input", () => {
|
||||
expect(getSectionItemRows([], 2)).toEqual([]);
|
||||
});
|
||||
|
||||
it("clamps invalid columns to 1", () => {
|
||||
expect(getSectionItemRows([1, 2], 0)).toEqual([[1], [2]]);
|
||||
expect(getSectionItemRows([1, 2], "not-a-number")).toEqual([[1], [2]]);
|
||||
});
|
||||
|
||||
it("clamps columns above 6 to 6", () => {
|
||||
expect(getSectionItemRows([1, 2, 3, 4, 5, 6, 7, 8], 99)).toEqual([
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
[7, 8],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldUseSectionTimeline", () => {
|
||||
it("returns true only for sectionTimeline=true, placement='main', columns=1", () => {
|
||||
expect(shouldUseSectionTimeline({ sectionTimeline: true, placement: "main", columns: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when placement is not 'main'", () => {
|
||||
expect(shouldUseSectionTimeline({ sectionTimeline: true, placement: "sidebar", columns: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when columns > 1 (after normalization)", () => {
|
||||
expect(shouldUseSectionTimeline({ sectionTimeline: true, placement: "main", columns: 2 })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when sectionTimeline=false", () => {
|
||||
expect(shouldUseSectionTimeline({ sectionTimeline: false, placement: "main", columns: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when columns is invalid (normalized to 1)", () => {
|
||||
expect(shouldUseSectionTimeline({ sectionTimeline: true, placement: "main", columns: 0 })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterItems, filterSections, hasVisibleItems, isSectionVisible, isVisibleSummary } from "./filtering";
|
||||
|
||||
describe("filterItems", () => {
|
||||
it("returns only items where hidden is false", () => {
|
||||
const items = [
|
||||
{ id: 1, hidden: false },
|
||||
{ id: 2, hidden: true },
|
||||
{ id: 3, hidden: false },
|
||||
];
|
||||
expect(filterItems(items)).toEqual([
|
||||
{ id: 1, hidden: false },
|
||||
{ id: 3, hidden: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns empty array when all items are hidden", () => {
|
||||
const items = [{ hidden: true }, { hidden: true }];
|
||||
expect(filterItems(items)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty array for empty input", () => {
|
||||
expect(filterItems([])).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves additional properties on items", () => {
|
||||
const items = [{ hidden: false, name: "Alice", level: 4 }];
|
||||
expect(filterItems(items)).toEqual([{ hidden: false, name: "Alice", level: 4 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasVisibleItems", () => {
|
||||
it("returns false when section.hidden is true", () => {
|
||||
expect(hasVisibleItems({ hidden: true, items: [{ hidden: false }] })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no items are visible", () => {
|
||||
expect(hasVisibleItems({ hidden: false, items: [{ hidden: true }] })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when at least one item is visible and section not hidden", () => {
|
||||
expect(hasVisibleItems({ hidden: false, items: [{ hidden: false }] })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for empty items", () => {
|
||||
expect(hasVisibleItems({ hidden: false, items: [] })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isVisibleSummary", () => {
|
||||
it("returns true when not hidden and content is non-empty", () => {
|
||||
expect(isVisibleSummary({ hidden: false, content: "<p>Some text</p>" })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when summary is hidden", () => {
|
||||
expect(isVisibleSummary({ hidden: true, content: "<p>Text</p>" })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when content is empty after trimming", () => {
|
||||
expect(isVisibleSummary({ hidden: false, content: " \n " })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when content is empty", () => {
|
||||
expect(isVisibleSummary({ hidden: false, content: "" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSectionVisible", () => {
|
||||
const data = {
|
||||
summary: { hidden: false, content: "<p>Hi</p>" },
|
||||
sections: {
|
||||
experience: { hidden: false, items: [{ hidden: false }] },
|
||||
skills: { hidden: false, items: [] },
|
||||
education: { hidden: true, items: [{ hidden: false }] },
|
||||
},
|
||||
customSections: [{ id: "ext-1", hidden: false, items: [{ hidden: false }] }],
|
||||
};
|
||||
|
||||
it("returns true for visible summary", () => {
|
||||
expect(isSectionVisible("summary", data)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for hidden summary", () => {
|
||||
expect(isSectionVisible("summary", { ...data, summary: { hidden: true, content: "<p>x</p>" } })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for built-in section with visible items", () => {
|
||||
expect(isSectionVisible("experience", data)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for built-in section with no items", () => {
|
||||
expect(isSectionVisible("skills", data)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for built-in section with hidden flag", () => {
|
||||
expect(isSectionVisible("education", data)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for matching custom section by id", () => {
|
||||
expect(isSectionVisible("ext-1", data)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unknown section id", () => {
|
||||
expect(isSectionVisible("does-not-exist", data)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterSections", () => {
|
||||
const data = {
|
||||
summary: { hidden: false, content: "<p>Hi</p>" },
|
||||
sections: {
|
||||
experience: { hidden: false, items: [{ hidden: false }] },
|
||||
skills: { hidden: false, items: [] },
|
||||
},
|
||||
customSections: [],
|
||||
};
|
||||
|
||||
it("returns only visible section ids in input order", () => {
|
||||
expect(filterSections(["summary", "experience", "skills"], data)).toEqual(["summary", "experience"]);
|
||||
});
|
||||
|
||||
it("returns empty array when no sections are visible", () => {
|
||||
const empty = {
|
||||
summary: { hidden: false, content: "" },
|
||||
sections: { skills: { hidden: false, items: [] } },
|
||||
customSections: [],
|
||||
};
|
||||
expect(filterSections(["summary", "skills"], empty)).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves order of input section ids", () => {
|
||||
expect(filterSections(["experience", "summary"], data)).toEqual(["experience", "summary"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getTemplateMetrics } from "./metrics";
|
||||
|
||||
describe("getTemplateMetrics", () => {
|
||||
it("maps margins to page paddings", () => {
|
||||
const metrics = getTemplateMetrics({ gapX: 4, gapY: 6, marginX: 14, marginY: 12 });
|
||||
expect(metrics.page.paddingHorizontal).toBe(14);
|
||||
expect(metrics.page.paddingVertical).toBe(12);
|
||||
});
|
||||
|
||||
it("uses marginY as headerGap and sectionGap", () => {
|
||||
const metrics = getTemplateMetrics({ gapX: 4, gapY: 6, marginX: 14, marginY: 20 });
|
||||
expect(metrics.headerGap).toBe(20);
|
||||
expect(metrics.sectionGap).toBe(20);
|
||||
});
|
||||
|
||||
it("uses marginX as columnGap", () => {
|
||||
const metrics = getTemplateMetrics({ gapX: 4, gapY: 6, marginX: 30, marginY: 12 });
|
||||
expect(metrics.columnGap).toBe(30);
|
||||
});
|
||||
|
||||
it("preserves itemGapX and itemGapY directly from gapX/gapY", () => {
|
||||
const metrics = getTemplateMetrics({ gapX: 7, gapY: 9, marginX: 14, marginY: 12 });
|
||||
expect(metrics.itemGapX).toBe(7);
|
||||
expect(metrics.itemGapY).toBe(9);
|
||||
});
|
||||
|
||||
it("provides gapX/gapY scaling functions", () => {
|
||||
const metrics = getTemplateMetrics({ gapX: 4, gapY: 6, marginX: 14, marginY: 12 });
|
||||
expect(metrics.gapX(2)).toBe(8);
|
||||
expect(metrics.gapY(3)).toBe(18);
|
||||
});
|
||||
|
||||
it("scaling by 0.5 returns half-gap", () => {
|
||||
const metrics = getTemplateMetrics({ gapX: 10, gapY: 20, marginX: 14, marginY: 12 });
|
||||
expect(metrics.gapX(0.5)).toBe(5);
|
||||
expect(metrics.gapY(0.5)).toBe(10);
|
||||
});
|
||||
|
||||
it("scaling by 0 returns 0", () => {
|
||||
const metrics = getTemplateMetrics({ gapX: 4, gapY: 6, marginX: 14, marginY: 12 });
|
||||
expect(metrics.gapX(0)).toBe(0);
|
||||
expect(metrics.gapY(0)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "./page-size";
|
||||
|
||||
describe("getTemplatePageSize", () => {
|
||||
it("returns 'A4' for a4 format", () => {
|
||||
expect(getTemplatePageSize("a4")).toBe("A4");
|
||||
});
|
||||
|
||||
it("returns 'LETTER' for letter format", () => {
|
||||
expect(getTemplatePageSize("letter")).toBe("LETTER");
|
||||
});
|
||||
|
||||
it("returns custom width for free-form format", () => {
|
||||
const result = getTemplatePageSize("free-form");
|
||||
expect(typeof result).toBe("object");
|
||||
if (typeof result === "object") {
|
||||
expect(result.width).toBeCloseTo(595.28, 2);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTemplatePageMinHeightStyle", () => {
|
||||
it("returns undefined for non-free-form formats", () => {
|
||||
expect(getTemplatePageMinHeightStyle("a4")).toBeUndefined();
|
||||
expect(getTemplatePageMinHeightStyle("letter")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns A4-style minHeight for free-form", () => {
|
||||
expect(getTemplatePageMinHeightStyle("free-form")).toEqual({ minHeight: 841.89 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { hasTemplatePicture } from "./picture";
|
||||
|
||||
const basePicture = {
|
||||
hidden: false,
|
||||
url: "",
|
||||
size: 80,
|
||||
rotation: 0,
|
||||
aspectRatio: 1,
|
||||
borderRadius: 0,
|
||||
borderColor: "rgba(0, 0, 0, 0.5)",
|
||||
borderWidth: 0,
|
||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||
shadowWidth: 0,
|
||||
} as const;
|
||||
|
||||
describe("hasTemplatePicture", () => {
|
||||
it("returns true when not hidden and url is non-empty", () => {
|
||||
expect(hasTemplatePicture({ ...basePicture, url: "/uploads/me.png" })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when hidden", () => {
|
||||
expect(hasTemplatePicture({ ...basePicture, url: "/uploads/me.png", hidden: true })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when url is empty", () => {
|
||||
expect(hasTemplatePicture(basePicture)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when url is whitespace only", () => {
|
||||
expect(hasTemplatePicture({ ...basePicture, url: " " })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeRichTextHtml } from "./rich-text-html";
|
||||
|
||||
describe("normalizeRichTextHtml", () => {
|
||||
it("wraps loose inline content in a <p>", () => {
|
||||
expect(normalizeRichTextHtml("hello world")).toBe("<p>hello world</p>");
|
||||
});
|
||||
|
||||
it("wraps inline tags in a <p>", () => {
|
||||
expect(normalizeRichTextHtml("<strong>bold</strong> text")).toBe("<p><strong>bold</strong> text</p>");
|
||||
});
|
||||
|
||||
it("preserves block-level <p> as-is", () => {
|
||||
expect(normalizeRichTextHtml("<p>Already wrapped</p>")).toBe("<p>Already wrapped</p>");
|
||||
});
|
||||
|
||||
it("preserves block-level <ul>", () => {
|
||||
const html = "<ul><li>a</li><li>b</li></ul>";
|
||||
expect(normalizeRichTextHtml(html)).toBe(html);
|
||||
});
|
||||
|
||||
it("flushes accumulated inlines before block-level tags", () => {
|
||||
expect(normalizeRichTextHtml("loose<ul><li>a</li></ul>")).toBe("<p>loose</p><ul><li>a</li></ul>");
|
||||
});
|
||||
|
||||
it("flushes accumulated inlines after block-level tags", () => {
|
||||
expect(normalizeRichTextHtml("<ul><li>a</li></ul>after")).toBe("<ul><li>a</li></ul><p>after</p>");
|
||||
});
|
||||
|
||||
it("treats <span> as inline", () => {
|
||||
expect(normalizeRichTextHtml("<span>x</span>")).toBe("<p><span>x</span></p>");
|
||||
});
|
||||
|
||||
it("trims input whitespace", () => {
|
||||
expect(normalizeRichTextHtml(" text ")).toBe("<p>text</p>");
|
||||
});
|
||||
|
||||
it("returns empty string for empty input", () => {
|
||||
expect(normalizeRichTextHtml("")).toBe("");
|
||||
});
|
||||
|
||||
it("treats <a> as inline (no need to wrap by itself)", () => {
|
||||
expect(normalizeRichTextHtml('<a href="x">link</a>')).toBe('<p><a href="x">link</a></p>');
|
||||
});
|
||||
|
||||
it("does not double-wrap inline tags inside block elements", () => {
|
||||
expect(normalizeRichTextHtml("<p><strong>x</strong></p>")).toBe("<p><strong>x</strong></p>");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getInlineItemWebsiteUrl, shouldRenderSeparateItemWebsite } from "./section-links";
|
||||
|
||||
describe("getInlineItemWebsiteUrl", () => {
|
||||
it("returns the url when url is set and inlineLink is true", () => {
|
||||
expect(getInlineItemWebsiteUrl({ url: "https://example.com", label: "", inlineLink: true })).toBe(
|
||||
"https://example.com",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns undefined when inlineLink is false", () => {
|
||||
expect(getInlineItemWebsiteUrl({ url: "https://example.com", label: "", inlineLink: false })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when url is empty", () => {
|
||||
expect(getInlineItemWebsiteUrl({ url: "", label: "", inlineLink: true })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when inlineLink is undefined", () => {
|
||||
expect(getInlineItemWebsiteUrl({ url: "https://example.com", label: "" })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldRenderSeparateItemWebsite", () => {
|
||||
it("returns true when url is set and inlineLink is false", () => {
|
||||
expect(shouldRenderSeparateItemWebsite({ url: "https://example.com", label: "", inlineLink: false })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when url is empty", () => {
|
||||
expect(shouldRenderSeparateItemWebsite({ url: "", label: "", inlineLink: false })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when inlineLink is true", () => {
|
||||
expect(shouldRenderSeparateItemWebsite({ url: "https://example.com", label: "", inlineLink: true })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when inlineLink is undefined", () => {
|
||||
expect(shouldRenderSeparateItemWebsite({ url: "https://example.com", label: "" })).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user