mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-19 21:11:45 +10:00
fix(pdf): ignore phantom gengar skill text nodes (#3289)
* docs(agents): align Redis compose commands with development guide - Problem: AGENTS.md omitted Redis from dev infrastructure compose commands while docs/contributing/development.mdx starts redis for local dev. - Fix: document full postgres/redis/seaweedfs compose command and note that REDIS_URL and ENCRYPTION_SECRET are required for AI agent features. - Verification: preflight upstream fetch; manual diff against development.mdx and compose.dev.yml redis service; duplicate PR gate passed. * fix(pdf): ignore phantom gengar skill text nodes - Problem: gengar template resumes with skills keywords fail semantic CSS activation because the legacy renderer emits a harmless empty text node that parity treats as a mismatch. - Fix: treat the specific empty text artifact as presentation-neutral in the legacy parity comparator and add a regression test for the phantom fontSize 9 node. - Verification: pnpm test src/semantic/legacy-parity.test.ts in packages/pdf passed (31 tests). * docs: clarify host and container Redis URLs - Problem: the development guide only showed the Docker Redis hostname, which fails for host-run development.\n- Fix: document localhost for host execution and redis for Docker execution.\n- Verification: pnpm test src/semantic/legacy-parity.test.ts (31 passed). * fix(pdf): omit empty skill proficiency text * test(pdf): cover blank skill proficiency --------- Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
co-authored by
Amruth Pillai
parent
bad431b2fc
commit
f64d02df7f
@@ -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<LegacyParityHostNode> => {
|
||||
const element = createElement(ResumeDocument, { data, template: "gengar" }) as unknown as Parameters<typeof pdf>[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<Record<string, unknown>>).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) => {
|
||||
|
||||
@@ -1153,7 +1153,7 @@ const SkillsSection = ({ sectionId = "skills", sectionData }: ItemSectionProps<S
|
||||
</SectionItemHeader>
|
||||
|
||||
<View>
|
||||
<Text semanticField="proficiency">{item.proficiency}</Text>
|
||||
{hasSplitRowText(item.proficiency) && <Text semanticField="proficiency">{item.proficiency}</Text>}
|
||||
<Small semanticField="keywords">{item.keywords.join(", ")}</Small>
|
||||
</View>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user