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 => { const element = createElement(ResumeDocument, { data, template }) as unknown as Parameters[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 = "

First runLast run

"; 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 = ""; 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); } }, ); });