mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-25 07:42: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,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("");
|
||||
});
|
||||
});
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user