mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +10:00
263 lines
9.3 KiB
TypeScript
263 lines
9.3 KiB
TypeScript
import type { SemanticNode } from "@reactive-resume/resume/stylesheet";
|
|
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
|
import type { Template } from "@reactive-resume/schema/templates";
|
|
import { describe, expect, it } from "vitest";
|
|
import { pdf } from "@react-pdf/renderer";
|
|
import { createElement } from "react";
|
|
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
|
import { ResumeDocument } from "../document";
|
|
import { resolveResumeRuntime } from "./resolve";
|
|
|
|
type HostNode = {
|
|
type: string;
|
|
value?: string;
|
|
children?: HostNode[];
|
|
};
|
|
|
|
const textValues = (node: HostNode): string[] => [
|
|
...(node.type === "TEXT_INSTANCE" && node.value ? [node.value] : []),
|
|
...(node.children ?? []).flatMap((child) => textValues(child)),
|
|
];
|
|
const findSemanticNode = (
|
|
node: SemanticNode,
|
|
predicate: (candidate: SemanticNode) => boolean,
|
|
): SemanticNode | undefined => {
|
|
if (predicate(node)) return node;
|
|
for (const child of node.children) {
|
|
const match = findSemanticNode(child, predicate);
|
|
if (match) return match;
|
|
}
|
|
};
|
|
|
|
const renderTextValues = async (data: ResumeData, template: Template): Promise<string[]> => {
|
|
const element = createElement(ResumeDocument, { data, template }) as unknown as Parameters<typeof pdf>[0];
|
|
const instance = pdf(element);
|
|
await expect.poll(() => instance.container.document).not.toBeNull();
|
|
return textValues(instance.container.document as HostNode);
|
|
};
|
|
|
|
const semanticFixture = (rule: string): ResumeData => {
|
|
const data = structuredClone(defaultResumeData);
|
|
data.picture.hidden = true;
|
|
data.basics = {
|
|
name: "Ada Lovelace",
|
|
headline: "",
|
|
email: "",
|
|
phone: "",
|
|
location: "",
|
|
website: { url: "", label: "" },
|
|
customFields: [],
|
|
};
|
|
const stylesheet = { languageVersion: 1, text: `@version 1; ${rule}` };
|
|
data.metadata.stylesheet = { mode: "semantic", source: stylesheet, applied: stylesheet };
|
|
return data;
|
|
};
|
|
|
|
const expectBefore = (values: readonly string[], first: string, second: string) => {
|
|
const firstIndex = values.indexOf(first);
|
|
const secondIndex = values.indexOf(second);
|
|
expect(firstIndex, `${first} is present`).toBeGreaterThanOrEqual(0);
|
|
expect(secondIndex, `${second} is present`).toBeGreaterThanOrEqual(0);
|
|
expect(firstIndex, `${first} before ${second}`).toBeLessThan(secondIndex);
|
|
};
|
|
|
|
describe("semantic sibling ordering reaches final PDF output", () => {
|
|
it("projects contact-list hide and order onto existing contact siblings", async () => {
|
|
const data = semanticFixture(`
|
|
contact-item[name="location"] { display: none; }
|
|
contact-item[name="phone"] { order: -1; }
|
|
`);
|
|
data.basics.email = "ada@example.com";
|
|
data.basics.phone = "+44 123";
|
|
data.basics.location = "London";
|
|
data.metadata.layout.pages = [{ fullWidth: true, main: [], sidebar: [] }];
|
|
|
|
const values = await renderTextValues(data, "onyx");
|
|
|
|
expectBefore(values, "+44 123", "ada@example.com");
|
|
expect(values).not.toContain("London");
|
|
});
|
|
|
|
it("projects Chikorita contacts only within their two existing row hosts", async () => {
|
|
const data = semanticFixture(`
|
|
template-part[name="contact-row-secondary"] { order: -1; }
|
|
contact-item[name="location"] { display: none; }
|
|
contact-item[name="phone"] { order: -1; }
|
|
contact-item[name="custom"] { order: -1; }
|
|
`);
|
|
data.basics.email = "ada@example.com";
|
|
data.basics.phone = "+44 123";
|
|
data.basics.location = "London";
|
|
data.basics.website = { url: "https://example.com", label: "example.com" };
|
|
data.basics.customFields = [{ id: "custom-1", icon: "", text: "portfolio.example", link: "" }];
|
|
data.metadata.layout.pages = [{ fullWidth: true, main: [], sidebar: [] }];
|
|
|
|
const values = await renderTextValues(data, "chikorita");
|
|
|
|
expectBefore(values, "portfolio.example", "example.com");
|
|
expectBefore(values, "example.com", "+44 123");
|
|
expectBefore(values, "+44 123", "ada@example.com");
|
|
expect(values).not.toContain("London");
|
|
});
|
|
|
|
it("projects nested-role hide and order across the complete direct item sequence", async () => {
|
|
const data = semanticFixture(`
|
|
item[id="role-2"] { display: none; }
|
|
item[id="role-3"] { order: -1; }
|
|
`);
|
|
data.sections.experience.items = [
|
|
{
|
|
id: "experience-1",
|
|
hidden: false,
|
|
company: "Analytical Engines",
|
|
position: "",
|
|
location: "",
|
|
period: "",
|
|
website: { url: "https://example.com/company", label: "Company site", inlineLink: false },
|
|
description: "",
|
|
roles: [
|
|
{ id: "role-1", position: "First role", period: "", description: "" },
|
|
{ id: "role-2", position: "Hidden role", period: "", description: "" },
|
|
{ id: "role-3", position: "Last role", period: "", description: "" },
|
|
],
|
|
},
|
|
];
|
|
data.metadata.layout.pages = [{ fullWidth: true, main: ["experience"], sidebar: [] }];
|
|
|
|
const values = await renderTextValues(data, "onyx");
|
|
|
|
expectBefore(values, "Last role", "Analytical Engines");
|
|
expectBefore(values, "Company site", "First role");
|
|
expect(values).not.toContain("Hidden role");
|
|
});
|
|
|
|
it("projects hide and order across Meowth's existing inline-header part siblings", async () => {
|
|
const data = semanticFixture(`
|
|
template-part[name="inline-item-header-leading"] { display: none; }
|
|
template-part[name="inline-item-header-trailing"] { order: -1; }
|
|
`);
|
|
data.sections.experience.items = [
|
|
{
|
|
id: "experience-1",
|
|
hidden: false,
|
|
company: "Analytical Engines",
|
|
position: "Engineer",
|
|
location: "London",
|
|
period: "1842",
|
|
website: { url: "", label: "", inlineLink: false },
|
|
description: "",
|
|
roles: [],
|
|
},
|
|
];
|
|
data.metadata.layout.pages = [{ fullWidth: true, main: ["experience"], sidebar: [] }];
|
|
|
|
const values = await renderTextValues(data, "meowth");
|
|
|
|
expectBefore(values, "1842", "Analytical Engines");
|
|
expect(values).not.toContain("Engineer");
|
|
expect(values).not.toContain("London");
|
|
});
|
|
|
|
it("projects Meowth's education grade row at the item boundary and its fields within that row", async () => {
|
|
const data = semanticFixture(`
|
|
template-part[name="education-grade-row"] { order: -1; }
|
|
field[name="location"] { order: -1; }
|
|
`);
|
|
data.sections.education.items = [
|
|
{
|
|
id: "education-1",
|
|
hidden: false,
|
|
school: "University of London",
|
|
area: "Mathematics",
|
|
degree: "BSc",
|
|
grade: "First",
|
|
location: "London",
|
|
period: "1835",
|
|
website: { url: "", label: "", inlineLink: false },
|
|
description: "",
|
|
},
|
|
];
|
|
data.metadata.layout.pages = [{ fullWidth: true, main: ["education"], sidebar: [] }];
|
|
|
|
const values = await renderTextValues(data, "meowth");
|
|
|
|
expectBefore(values, "London", "First");
|
|
expectBefore(values, "First", "Mathematics");
|
|
expectBefore(values, "First", "University of London");
|
|
});
|
|
|
|
it("reorders Meowth inline-header parts without dropping untouched combined fields", async () => {
|
|
const data = semanticFixture(`
|
|
template-part[name="inline-item-header-trailing"] { order: -1; }
|
|
`);
|
|
data.sections.education.items = [
|
|
{
|
|
id: "education-1",
|
|
hidden: false,
|
|
school: "University of London",
|
|
area: "Mathematics",
|
|
degree: "BSc",
|
|
grade: "",
|
|
location: "",
|
|
period: "1835",
|
|
website: { url: "", label: "", inlineLink: false },
|
|
description: "",
|
|
},
|
|
];
|
|
data.metadata.layout.pages = [{ fullWidth: true, main: ["education"], sidebar: [] }];
|
|
|
|
const values = await renderTextValues(data, "meowth");
|
|
|
|
expectBefore(values, "1835", "Mathematics");
|
|
expectBefore(values, "Mathematics", "BSc");
|
|
expectBefore(values, "BSc", "University of London");
|
|
});
|
|
|
|
it("projects rich-text descendant hide and order before renderer mapping", async () => {
|
|
const data = semanticFixture(`
|
|
rich-text > list { display: none; }
|
|
paragraph > underline { order: -1; }
|
|
`);
|
|
data.summary.content = "<ul><li>Hidden run</li></ul><p><strong>First run</strong><u>Last run</u></p>";
|
|
data.metadata.layout.pages = [{ fullWidth: true, main: ["summary"], sidebar: [] }];
|
|
|
|
const values = await renderTextValues(data, "onyx");
|
|
|
|
expectBefore(values, "Last run", "First run");
|
|
expect(values).not.toContain("Hidden run");
|
|
});
|
|
|
|
it.each([
|
|
["en-US", "list-marker { display: none; }", "List content", "•"],
|
|
["ar-SA", "list-marker { order: -1; }", "•", "List content"],
|
|
] as const)(
|
|
"projects %s rich-list marker/content keys onto the existing row children",
|
|
async (locale, rule, first, second) => {
|
|
const data = semanticFixture(rule);
|
|
data.metadata.page.locale = locale;
|
|
data.summary.content = "<ul><li>List content</li></ul>";
|
|
data.metadata.layout.pages = [{ fullWidth: true, main: ["summary"], sidebar: [] }];
|
|
const runtime = resolveResumeRuntime({ data, template: "onyx", mode: "semantic" });
|
|
const renderedItem = findSemanticNode(runtime.renderTree, (node) => node.kind === "list-item");
|
|
|
|
expect(runtime.diagnostics.filter(({ severity }) => severity === "error")).toEqual([]);
|
|
expect(renderedItem?.children.map(({ kind }) => kind)).toEqual(
|
|
rule.includes("display") ? ["list-item-content"] : ["list-marker", "list-item-content"],
|
|
);
|
|
|
|
const values = await renderTextValues(data, "onyx");
|
|
|
|
if (rule.includes("display")) {
|
|
expect(values).toContain(first);
|
|
expect(values).not.toContain(second);
|
|
} else {
|
|
const firstIndex = values.indexOf(first);
|
|
const secondIndex = values.findIndex((value) => value.includes(second));
|
|
expect(firstIndex).toBeGreaterThanOrEqual(0);
|
|
expect(secondIndex).toBeGreaterThanOrEqual(0);
|
|
expect(firstIndex).toBeLessThan(secondIndex);
|
|
}
|
|
},
|
|
);
|
|
});
|