import type { SemanticNode } from "@reactive-resume/resume/stylesheet/types"; import type { ResumeData } from "@reactive-resume/schema/resume/data"; import { describe, expect, it, vi } 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 { parseNormalizedRichTextHtml, richTextMarkClassName } from "../templates/shared/rich-text-html"; import { getRichTextSemanticNodeKey } from "./rich-text-keys"; import { buildSemanticTree } from "./tree"; vi.mock("@react-pdf/renderer", async (importOriginal) => ({ ...(await importOriginal()), })); type HostNode = { type: string; style?: unknown; props?: { style?: unknown }; children?: HostNode[]; }; const collectStyles = (node: HostNode): Record[] => { const style = node.style ?? node.props?.style; const current = Array.isArray(style) ? style : style ? [style] : []; return [...(current as Record[]), ...(node.children ?? []).flatMap(collectStyles)]; }; const collectNodeKinds = (node: SemanticNode): SemanticNode["kind"][] => [ node.kind, ...node.children.flatMap(collectNodeKinds), ]; const collectNodeKeys = (node: SemanticNode): string[] => [node.key, ...node.children.flatMap(collectNodeKeys)]; 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 buildFixture = (): ResumeData => { const data = structuredClone(defaultResumeData); const applied = { languageVersion: 1, text: ` @version 1; paragraph { color: #123456; } strong { color: #654321; } list-marker { color: #abcdef; } `, }; data.picture.hidden = true; data.basics.name = "Ada Lovelace"; data.summary.content = "

First bold

  • Item
"; data.metadata.layout.pages = [{ fullWidth: true, main: ["summary"], sidebar: [] }]; data.metadata.stylesheet = { mode: "semantic", source: applied, applied }; return data; }; describe("semantic rich-text bindings", () => { it("applies occurrence-specific paragraph, strong, and marker styles to existing primitives", async () => { const element = createElement(ResumeDocument, { data: buildFixture(), template: "onyx", }) as unknown as Parameters[0]; const instance = pdf(element); await vi.waitFor(() => expect(instance.container.document).not.toBeNull()); const colors = collectStyles(instance.container.document as HostNode).map(({ color }) => color); expect(colors).toEqual(expect.arrayContaining(["#123456", "#654321", "#abcdef"])); }); it.each([ ["ltr", "en-US", "

- Alpha
- Beta

", ["paragraph", "hard-break"]], ["rtl", "he-IL", "

‏- אלפא
‏- בטא

", ["list", "list-item", "list-marker", "list-item-content"]], ] as const)( "builds the %s descriptor from the same normalized rich-text elements rendered by the PDF", (_direction, locale, content, expectedKinds) => { const data = buildFixture(); data.metadata.page.locale = locale; data.summary.content = content; const page = data.metadata.layout.pages[0]; if (!page) throw new Error("Expected a page fixture."); const tree = buildSemanticTree({ data, template: "onyx", page, pageNumber: 1, showHeader: false, }); const kinds = collectNodeKinds(tree); for (const kind of expectedKinds) expect(kinds).toContain(kind); if (locale === "he-IL") { expect(kinds).not.toContain("paragraph"); expect(kinds).not.toContain("hard-break"); } }, ); it.each([ ["ltr", "en-US", ""], ["rtl", "he-IL", "

‏- אלפא
‏- בטא

"], ] as const)("keeps every %s descriptor key aligned with the renderer traversal", (direction, locale, suffix) => { const content = [ "

Heading

", "

Quote bold em u s c", 'span mark link
next

', "
  • Item
  1. One

", suffix, ].join(""); const data = buildFixture(); data.metadata.page.locale = locale; data.summary.content = content; const page = data.metadata.layout.pages[0]; if (!page) throw new Error("Expected a page fixture."); const tree = buildSemanticTree({ data, template: "onyx", page, pageNumber: 1, showHeader: false }); const richText = findSemanticNode(tree, (candidate) => candidate.kind === "rich-text"); if (!richText) throw new Error("Expected a rich-text descriptor."); const parsed = parseNormalizedRichTextHtml(content, { direction }); const rendererKeys = parsed.querySelectorAll("*").flatMap((element) => { const key = getRichTextSemanticNodeKey(richText.key, element, richTextMarkClassName); if (element.rawTagName.toLowerCase() !== "li") return [key]; return [`${key}/list-marker-0`, `${key}/list-item-content-0`, key]; }); expect(new Set(rendererKeys)).toEqual(new Set(collectNodeKeys(richText).slice(1))); }); });