diff --git a/packages/pdf/src/semantic/legacy-parity.test.ts b/packages/pdf/src/semantic/legacy-parity.test.ts index 6083f570c..2e243b6ad 100644 --- a/packages/pdf/src/semantic/legacy-parity.test.ts +++ b/packages/pdf/src/semantic/legacy-parity.test.ts @@ -3,9 +3,12 @@ import type { Template } from "@reactive-resume/schema/templates"; import type { LegacyParityHostNode } from "./legacy-parity"; import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; +import { pdf } from "@react-pdf/renderer"; +import { createElement } from "react"; import { styleRulesSchema } from "@reactive-resume/schema/resume/data"; import { defaultResumeData } from "@reactive-resume/schema/resume/default"; import { sampleResumeData } from "@reactive-resume/schema/resume/sample"; +import { ResumeDocument } from "../document"; import { convertLegacyStyleRules } from "./legacy-converter"; import { compareLegacyParityHostNodes, compareLegacySemanticPresentation } from "./legacy-parity"; @@ -157,6 +160,27 @@ const buildFixture = (rules: StyleRule[]): ResumeData => { return data; }; +const nodeText = (node: LegacyParityHostNode): string => + node.value ?? (node.children ?? []).map((child) => nodeText(child)).join(""); + +const renderHostDocument = async (data: ResumeData): Promise => { + const element = createElement(ResumeDocument, { data, template: "gengar" }) as unknown as Parameters[0]; + const instance = pdf(element); + await expect.poll(() => instance.container.document).not.toBeNull(); + return instance.container.document as LegacyParityHostNode; +}; + +const findEmptyTextNodes = (node: LegacyParityHostNode): LegacyParityHostNode[] => [ + ...(node.type === "TEXT" && nodeText(node).trim() === "" ? [node] : []), + ...(node.children ?? []).flatMap(findEmptyTextNodes), +]; + +const hasFontSize = (node: LegacyParityHostNode, fontSize: number): boolean => + (Array.isArray(node.style) ? node.style : [node.style]).some( + (style) => + style !== null && typeof style === "object" && (style as Readonly>).fontSize === fontSize, + ); + describe("compareLegacySemanticPresentation", () => { it("renders the mandatory target shapes in the shared parity document", () => { const data = buildFixture([]); @@ -231,6 +255,22 @@ describe("compareLegacySemanticPresentation", () => { expect(comparison.mismatches).toEqual([]); }); + it("omits empty Gengar skill proficiency text while preserving populated text", async () => { + const data = buildFixture([]); + data.metadata.template = "gengar"; + data.metadata.layout.pages = [{ fullWidth: false, main: [], sidebar: ["skills"] }]; + data.metadata.typography.body.fontSize = 9; + const [skill] = data.sections.skills.items; + if (!skill) throw new Error("Expected Gengar skill fixture."); + for (const proficiency of ["", " \t"]) { + skill.proficiency = proficiency; + expect(findEmptyTextNodes(await renderHostDocument(data)).filter((node) => hasFontSize(node, 9))).toEqual([]); + } + + skill.proficiency = "Expert"; + expect(nodeText(await renderHostDocument(data))).toContain("Expert"); + }); + it.each(["onyx", "meowth"] as const)( "matches combined separator and box-style primitives on %s", async (template) => { diff --git a/packages/pdf/src/templates/shared/sections.tsx b/packages/pdf/src/templates/shared/sections.tsx index 3acb41442..cb844b9b0 100644 --- a/packages/pdf/src/templates/shared/sections.tsx +++ b/packages/pdf/src/templates/shared/sections.tsx @@ -1153,7 +1153,7 @@ const SkillsSection = ({ sectionId = "skills", sectionData }: ItemSectionProps - {item.proficiency} + {hasSplitRowText(item.proficiency) && {item.proficiency}} {item.keywords.join(", ")}