mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-26 09:54:43 +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,42 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { pageDimensionsAsMillimeters, pageDimensionsAsPixels } from "./page";
|
||||
|
||||
describe("pageDimensionsAsPixels", () => {
|
||||
it("defines a4 dimensions matching ISO 216 at ~96 DPI", () => {
|
||||
expect(pageDimensionsAsPixels.a4).toEqual({ width: 794, height: 1123 });
|
||||
});
|
||||
|
||||
it("defines US letter dimensions", () => {
|
||||
expect(pageDimensionsAsPixels.letter).toEqual({ width: 816, height: 1056 });
|
||||
});
|
||||
|
||||
it("defines free-form dimensions same as a4", () => {
|
||||
expect(pageDimensionsAsPixels["free-form"]).toEqual({ width: 794, height: 1123 });
|
||||
});
|
||||
|
||||
it("uses portrait orientation (height > width) for all formats", () => {
|
||||
expect(pageDimensionsAsPixels.a4.height).toBeGreaterThan(pageDimensionsAsPixels.a4.width);
|
||||
expect(pageDimensionsAsPixels.letter.height).toBeGreaterThan(pageDimensionsAsPixels.letter.width);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pageDimensionsAsMillimeters", () => {
|
||||
it("defines a4 dimensions in millimeters per ISO 216", () => {
|
||||
expect(pageDimensionsAsMillimeters.a4).toEqual({ width: "210mm", height: "297mm" });
|
||||
});
|
||||
|
||||
it("defines letter dimensions in millimeters", () => {
|
||||
expect(pageDimensionsAsMillimeters.letter).toEqual({ width: "216mm", height: "279mm" });
|
||||
});
|
||||
|
||||
it("appends 'mm' unit suffix to all values", () => {
|
||||
for (const [, dims] of Object.entries(pageDimensionsAsMillimeters)) {
|
||||
expect(dims.width).toMatch(/mm$/);
|
||||
expect(dims.height).toMatch(/mm$/);
|
||||
}
|
||||
});
|
||||
|
||||
it("provides all three formats (a4, letter, free-form)", () => {
|
||||
expect(Object.keys(pageDimensionsAsMillimeters).sort()).toEqual(["a4", "free-form", "letter"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,272 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
basicsSchema,
|
||||
customFieldSchema,
|
||||
experienceItemSchema,
|
||||
layoutSchema,
|
||||
pageSchema,
|
||||
pictureSchema,
|
||||
resumeDataSchema,
|
||||
skillItemSchema,
|
||||
websiteSchema,
|
||||
} from "./data";
|
||||
import { defaultResumeData } from "./default";
|
||||
|
||||
describe("resumeDataSchema", () => {
|
||||
it("validates the default resume", () => {
|
||||
expect(resumeDataSchema.safeParse(defaultResumeData).success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects empty object", () => {
|
||||
expect(resumeDataSchema.safeParse({}).success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects missing top-level keys", () => {
|
||||
const partial = { ...defaultResumeData, basics: undefined };
|
||||
expect(resumeDataSchema.safeParse(partial).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("websiteSchema", () => {
|
||||
it("requires url and label fields", () => {
|
||||
expect(websiteSchema.safeParse({ url: "https://example.com", label: "Example" }).success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects missing url", () => {
|
||||
expect(websiteSchema.safeParse({ label: "Example" }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects missing label", () => {
|
||||
expect(websiteSchema.safeParse({ url: "https://example.com" }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("allows empty strings (caller decides display)", () => {
|
||||
expect(websiteSchema.safeParse({ url: "", label: "" }).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pictureSchema", () => {
|
||||
it("accepts the default picture config", () => {
|
||||
expect(pictureSchema.safeParse(defaultResumeData.picture).success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects size below 32", () => {
|
||||
const invalid = { ...defaultResumeData.picture, size: 16 };
|
||||
expect(pictureSchema.safeParse(invalid).success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects size above 512", () => {
|
||||
const invalid = { ...defaultResumeData.picture, size: 1024 };
|
||||
expect(pictureSchema.safeParse(invalid).success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects rotation below 0", () => {
|
||||
const invalid = { ...defaultResumeData.picture, rotation: -1 };
|
||||
expect(pictureSchema.safeParse(invalid).success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects rotation above 360", () => {
|
||||
const invalid = { ...defaultResumeData.picture, rotation: 361 };
|
||||
expect(pictureSchema.safeParse(invalid).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("basicsSchema", () => {
|
||||
it("validates default basics", () => {
|
||||
expect(basicsSchema.safeParse(defaultResumeData.basics).success).toBe(true);
|
||||
});
|
||||
|
||||
it("requires email to be a string but does not enforce email shape (catches at app level)", () => {
|
||||
const result = basicsSchema.safeParse({
|
||||
...defaultResumeData.basics,
|
||||
email: "not-an-email",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("supports customFields array", () => {
|
||||
const result = basicsSchema.safeParse({
|
||||
...defaultResumeData.basics,
|
||||
customFields: [{ id: "1", icon: "phone", text: "555-0000", link: "" }],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("customFieldSchema", () => {
|
||||
it("requires id, icon, and text", () => {
|
||||
const result = customFieldSchema.safeParse({
|
||||
id: "1",
|
||||
icon: "phone",
|
||||
text: "555-0000",
|
||||
link: "",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects missing id", () => {
|
||||
expect(customFieldSchema.safeParse({ icon: "phone", text: "555-0000", link: "" }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to empty link via .catch when missing", () => {
|
||||
const result = customFieldSchema.safeParse({ id: "1", icon: "phone", text: "x" });
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.link).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("experienceItemSchema", () => {
|
||||
it("requires company name (min 1)", () => {
|
||||
const result = experienceItemSchema.safeParse({
|
||||
id: "abcdef0123456789",
|
||||
hidden: false,
|
||||
company: "",
|
||||
position: "Engineer",
|
||||
location: "",
|
||||
period: "",
|
||||
website: { url: "", label: "", inlineLink: false },
|
||||
description: "",
|
||||
roles: [],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("validates a complete experience item", () => {
|
||||
const result = experienceItemSchema.safeParse({
|
||||
id: "abcdef0123456789",
|
||||
hidden: false,
|
||||
company: "Acme",
|
||||
position: "Engineer",
|
||||
location: "NYC",
|
||||
period: "2020 - 2023",
|
||||
website: { url: "https://acme.com", label: "Acme", inlineLink: false },
|
||||
description: "<p>Did things</p>",
|
||||
roles: [],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults roles to [] via .catch when missing", () => {
|
||||
const result = experienceItemSchema.safeParse({
|
||||
id: "x",
|
||||
hidden: false,
|
||||
company: "Acme",
|
||||
position: "",
|
||||
location: "",
|
||||
period: "",
|
||||
website: { url: "", label: "", inlineLink: false },
|
||||
description: "",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.roles).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("skillItemSchema", () => {
|
||||
it("requires name (min 1)", () => {
|
||||
const invalid = {
|
||||
id: "x",
|
||||
hidden: false,
|
||||
icon: "",
|
||||
iconColor: "",
|
||||
name: "",
|
||||
proficiency: "",
|
||||
level: 4,
|
||||
keywords: [],
|
||||
};
|
||||
expect(skillItemSchema.safeParse(invalid).success).toBe(false);
|
||||
});
|
||||
|
||||
it("clamps invalid level via .catch(0)", () => {
|
||||
const item = {
|
||||
id: "x",
|
||||
hidden: false,
|
||||
icon: "",
|
||||
iconColor: "",
|
||||
name: "TS",
|
||||
proficiency: "",
|
||||
level: 99,
|
||||
keywords: [],
|
||||
};
|
||||
const result = skillItemSchema.safeParse(item);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.level).toBe(0);
|
||||
});
|
||||
|
||||
it("allows level between 0 and 5", () => {
|
||||
for (const level of [0, 1, 2, 3, 4, 5]) {
|
||||
const item = {
|
||||
id: "x",
|
||||
hidden: false,
|
||||
icon: "",
|
||||
iconColor: "",
|
||||
name: "X",
|
||||
proficiency: "",
|
||||
level,
|
||||
keywords: [],
|
||||
};
|
||||
expect(skillItemSchema.safeParse(item).success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to empty array when keywords is invalid via .catch", () => {
|
||||
const item = {
|
||||
id: "x",
|
||||
hidden: false,
|
||||
icon: "",
|
||||
iconColor: "",
|
||||
name: "X",
|
||||
proficiency: "",
|
||||
level: 3,
|
||||
keywords: "not-an-array",
|
||||
};
|
||||
const result = skillItemSchema.safeParse(item);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.keywords).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("layoutSchema", () => {
|
||||
it("validates default layout", () => {
|
||||
expect(layoutSchema.safeParse(defaultResumeData.metadata.layout).success).toBe(true);
|
||||
});
|
||||
|
||||
it("clamps sidebarWidth out-of-range to 35", () => {
|
||||
const result = layoutSchema.safeParse({
|
||||
...defaultResumeData.metadata.layout,
|
||||
sidebarWidth: 999,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.sidebarWidth).toBe(35);
|
||||
});
|
||||
|
||||
it("clamps sidebarWidth below 10 to 35", () => {
|
||||
const result = layoutSchema.safeParse({
|
||||
...defaultResumeData.metadata.layout,
|
||||
sidebarWidth: 5,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.sidebarWidth).toBe(35);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pageSchema", () => {
|
||||
it("rejects negative gap or margin values", () => {
|
||||
const invalid = { ...defaultResumeData.metadata.page, gapX: -1 };
|
||||
expect(pageSchema.safeParse(invalid).success).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to 'a4' for unknown format via .catch", () => {
|
||||
const invalid = { ...defaultResumeData.metadata.page, format: "huge" };
|
||||
const result = pageSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.format).toBe("a4");
|
||||
});
|
||||
|
||||
it("accepts each known format", () => {
|
||||
for (const format of ["a4", "letter", "free-form"] as const) {
|
||||
const valid = { ...defaultResumeData.metadata.page, format };
|
||||
expect(pageSchema.safeParse(valid).success).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resumeDataSchema } from "./data";
|
||||
import { defaultResumeData } from "./default";
|
||||
|
||||
describe("defaultResumeData", () => {
|
||||
it("conforms to resumeDataSchema", () => {
|
||||
const result = resumeDataSchema.safeParse(defaultResumeData);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the 'onyx' template", () => {
|
||||
expect(defaultResumeData.metadata.template).toBe("onyx");
|
||||
});
|
||||
|
||||
it("uses en-US locale", () => {
|
||||
expect(defaultResumeData.metadata.page.locale).toBe("en-US");
|
||||
});
|
||||
|
||||
it("uses A4 format by default", () => {
|
||||
expect(defaultResumeData.metadata.page.format).toBe("a4");
|
||||
});
|
||||
|
||||
it("starts with no resume content", () => {
|
||||
expect(defaultResumeData.basics.name).toBe("");
|
||||
expect(defaultResumeData.summary.content).toBe("");
|
||||
for (const section of Object.values(defaultResumeData.sections)) {
|
||||
expect(section.items).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it("has exactly one default page layout", () => {
|
||||
expect(defaultResumeData.metadata.layout.pages).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("default page has standard main and sidebar columns", () => {
|
||||
const page = defaultResumeData.metadata.layout.pages[0];
|
||||
expect(page).toBeDefined();
|
||||
expect(page?.main).toContain("experience");
|
||||
expect(page?.main).toContain("education");
|
||||
expect(page?.sidebar).toContain("skills");
|
||||
});
|
||||
|
||||
it("uses serif font family for body", () => {
|
||||
expect(defaultResumeData.metadata.typography.body.fontFamily).toBe("IBM Plex Serif");
|
||||
});
|
||||
|
||||
it("default sidebar width is 35%", () => {
|
||||
expect(defaultResumeData.metadata.layout.sidebarWidth).toBe(35);
|
||||
});
|
||||
|
||||
it("custom sections are empty", () => {
|
||||
expect(defaultResumeData.customSections).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { templateSchema } from "./templates";
|
||||
|
||||
describe("templateSchema", () => {
|
||||
it("accepts known template names", () => {
|
||||
const validTemplates = [
|
||||
"azurill",
|
||||
"bronzor",
|
||||
"chikorita",
|
||||
"ditgar",
|
||||
"ditto",
|
||||
"gengar",
|
||||
"glalie",
|
||||
"kakuna",
|
||||
"lapras",
|
||||
"leafish",
|
||||
"meowth",
|
||||
"onyx",
|
||||
"pikachu",
|
||||
"rhyhorn",
|
||||
];
|
||||
for (const t of validTemplates) {
|
||||
expect(templateSchema.safeParse(t).success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects unknown template names", () => {
|
||||
expect(templateSchema.safeParse("unknown").success).toBe(false);
|
||||
expect(templateSchema.safeParse("").success).toBe(false);
|
||||
expect(templateSchema.safeParse("ONYX").success).toBe(false); // case-sensitive
|
||||
});
|
||||
|
||||
it("rejects non-string values", () => {
|
||||
expect(templateSchema.safeParse(null).success).toBe(false);
|
||||
expect(templateSchema.safeParse(undefined).success).toBe(false);
|
||||
expect(templateSchema.safeParse(42).success).toBe(false);
|
||||
});
|
||||
|
||||
it("returns the exact value for a valid template", () => {
|
||||
const result = templateSchema.safeParse("onyx");
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).toBe("onyx");
|
||||
}
|
||||
});
|
||||
|
||||
it("includes 14 templates", () => {
|
||||
const validTemplates = templateSchema.options;
|
||||
expect(validTemplates).toHaveLength(14);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user