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:
Amruth Pillai
2026-05-10 20:00:07 +02:00
parent 33103536ae
commit 6a01207b6b
83 changed files with 8097 additions and 4 deletions
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { buildAiExtractionTemplate } from "./extraction-template";
describe("buildAiExtractionTemplate", () => {
it("returns a copy of defaultResumeData with one item per section", () => {
const template = buildAiExtractionTemplate();
for (const [, section] of Object.entries(template.sections)) {
expect(Array.isArray(section.items)).toBe(true);
expect(section.items).toHaveLength(1);
}
});
it("populates the customFields with a single empty entry", () => {
const template = buildAiExtractionTemplate();
expect(template.basics.customFields).toEqual([{ id: "", icon: "", text: "", link: "" }]);
});
it("does not mutate defaultResumeData", () => {
const before = JSON.stringify(defaultResumeData);
buildAiExtractionTemplate();
expect(JSON.stringify(defaultResumeData)).toBe(before);
});
it("creates skill items with empty fields and zero level", () => {
const template = buildAiExtractionTemplate();
const item = template.sections.skills.items[0] as Record<string, unknown>;
expect(item.id).toBe("");
expect(item.hidden).toBe(false);
expect(item.name).toBe("");
expect(item.level).toBe(0);
expect(item.keywords).toEqual([]);
});
it("creates experience items with nested website object", () => {
const template = buildAiExtractionTemplate();
const item = template.sections.experience.items[0] as Record<string, unknown>;
expect(item.website).toEqual({ url: "", label: "", inlineLink: false });
expect(item.company).toBe("");
expect(item.period).toBe("");
});
it("preserves unmodified sections from default (e.g., title)", () => {
const template = buildAiExtractionTemplate();
expect(template.sections.skills.title).toBe(defaultResumeData.sections.skills.title);
expect(template.sections.skills.hidden).toBe(defaultResumeData.sections.skills.hidden);
expect(template.sections.skills.columns).toBe(defaultResumeData.sections.skills.columns);
});
it("creates languages items with empty language and zero level", () => {
const template = buildAiExtractionTemplate();
const item = template.sections.languages.items[0] as Record<string, unknown>;
expect(item.language).toBe("");
expect(item.level).toBe(0);
});
it("creates references items with phone field", () => {
const template = buildAiExtractionTemplate();
const item = template.sections.references.items[0] as Record<string, unknown>;
expect(item.phone).toBe("");
expect(item.name).toBe("");
});
});
+150
View File
@@ -0,0 +1,150 @@
import { describe, expect, it } from "vitest";
import { sanitizeAndParseResumeJson } from "./sanitize";
describe("sanitizeAndParseResumeJson", () => {
it("parses a minimal but well-formed resume JSON", () => {
const json = JSON.stringify({
basics: { name: "Alice" },
});
const result = sanitizeAndParseResumeJson(json);
expect(result.data.basics.name).toBe("Alice");
expect(result.diagnostics.coercions).toEqual([]);
});
it("repairs unquoted keys and trailing commas via jsonrepair", () => {
const broken = `{ basics: { name: "Bob", }, }`;
const result = sanitizeAndParseResumeJson(broken);
expect(result.data.basics.name).toBe("Bob");
});
it("strips text before and after the JSON block", () => {
const messy = `Some preamble text\n\n{ "basics": { "name": "Carol" } }\n\nSome trailing text`;
const result = sanitizeAndParseResumeJson(messy);
expect(result.data.basics.name).toBe("Carol");
});
it("coerces 'true'/'false' strings to booleans", () => {
const json = JSON.stringify({
basics: { name: "Test" },
summary: { hidden: "true", title: "", columns: 1, content: "" },
});
const result = sanitizeAndParseResumeJson(json);
expect(result.data.summary.hidden).toBe(true);
expect(result.diagnostics.coercions.length).toBeGreaterThan(0);
});
it("coerces numeric strings to numbers", () => {
const json = JSON.stringify({
basics: { name: "Test" },
summary: { hidden: false, title: "", columns: "2", content: "" },
});
const result = sanitizeAndParseResumeJson(json);
expect(result.data.summary.columns).toBe(2);
});
it("drops items missing required fields with diagnostics", () => {
const json = JSON.stringify({
basics: { name: "Test" },
sections: {
skills: {
items: [
{ name: "TypeScript", level: 4 },
{ name: "" }, // dropped: missing name
{}, // dropped: missing name
],
},
},
});
const result = sanitizeAndParseResumeJson(json);
expect(result.data.sections.skills.items).toHaveLength(1);
expect(result.diagnostics.droppedSectionItems.length).toBeGreaterThan(0);
expect(result.diagnostics.salvageApplied).toBe(true);
});
it("auto-generates ids for items lacking them", () => {
const json = JSON.stringify({
basics: { name: "Test" },
sections: {
skills: { items: [{ name: "Go" }] },
},
});
const result = sanitizeAndParseResumeJson(json);
const item = result.data.sections.skills.items[0];
expect(item).toBeDefined();
expect(item?.id?.length).toBeGreaterThan(0);
expect(result.diagnostics.salvageApplied).toBe(true);
});
it("defaults hidden=false on items missing the field", () => {
const json = JSON.stringify({
basics: { name: "Test" },
sections: {
skills: { items: [{ name: "Go" }] },
},
});
const result = sanitizeAndParseResumeJson(json);
expect(result.data.sections.skills.items[0]?.hidden).toBe(false);
});
it("uses defaultResumeData picture and metadata regardless of input", () => {
const json = JSON.stringify({
basics: { name: "Test" },
picture: { url: "https://attacker.com/pic.png", size: 1024 }, // overridden
metadata: { template: "unknown" }, // overridden
});
const result = sanitizeAndParseResumeJson(json);
expect(result.data.metadata.template).toBe("onyx");
expect(result.data.picture.url).toBe("");
});
it("preserves valid items alongside invalid ones in the same section", () => {
const json = JSON.stringify({
basics: { name: "Test" },
sections: {
experience: {
items: [
{ company: "Acme", position: "Engineer" },
{ position: "Designer" }, // dropped: missing company
],
},
},
});
const result = sanitizeAndParseResumeJson(json);
expect(result.data.sections.experience.items).toHaveLength(1);
expect(result.data.sections.experience.items[0]?.company).toBe("Acme");
});
it("clears customSections to empty array regardless of input", () => {
const json = JSON.stringify({
basics: { name: "Test" },
customSections: [{ key: "hack" }],
});
const result = sanitizeAndParseResumeJson(json);
expect(result.data.customSections).toEqual([]);
});
it("throws when input is non-JSON garbage", () => {
expect(() => sanitizeAndParseResumeJson("not even close to json")).toThrow();
});
it("preserves coercion path entries in diagnostics", () => {
const json = JSON.stringify({
basics: { name: "Test" },
summary: { hidden: "true", title: "", columns: 1, content: "" },
});
const result = sanitizeAndParseResumeJson(json);
const hiddenCoercion = result.diagnostics.coercions.find((c) => c.path === "summary.hidden");
expect(hiddenCoercion).toEqual({ path: "summary.hidden", fromType: "string", toType: "boolean" });
});
});