mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +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 type { LegacyParityHostNode } from "./legacy-parity";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { describe, expect, it } from "vitest";
|
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 { styleRulesSchema } from "@reactive-resume/schema/resume/data";
|
||||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||||
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
|
||||||
|
import { ResumeDocument } from "../document";
|
||||||
import { convertLegacyStyleRules } from "./legacy-converter";
|
import { convertLegacyStyleRules } from "./legacy-converter";
|
||||||
import { compareLegacyParityHostNodes, compareLegacySemanticPresentation } from "./legacy-parity";
|
import { compareLegacyParityHostNodes, compareLegacySemanticPresentation } from "./legacy-parity";
|
||||||
|
|
||||||
@@ -157,6 +160,27 @@ const buildFixture = (rules: StyleRule[]): ResumeData => {
|
|||||||
return data;
|
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", () => {
|
describe("compareLegacySemanticPresentation", () => {
|
||||||
it("renders the mandatory target shapes in the shared parity document", () => {
|
it("renders the mandatory target shapes in the shared parity document", () => {
|
||||||
const data = buildFixture([]);
|
const data = buildFixture([]);
|
||||||
@@ -231,6 +255,22 @@ describe("compareLegacySemanticPresentation", () => {
|
|||||||
expect(comparison.mismatches).toEqual([]);
|
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)(
|
it.each(["onyx", "meowth"] as const)(
|
||||||
"matches combined separator and box-style primitives on %s",
|
"matches combined separator and box-style primitives on %s",
|
||||||
async (template) => {
|
async (template) => {
|
||||||
|
|||||||
@@ -1153,7 +1153,7 @@ const SkillsSection = ({ sectionId = "skills", sectionData }: ItemSectionProps<S
|
|||||||
</SectionItemHeader>
|
</SectionItemHeader>
|
||||||
|
|
||||||
<View>
|
<View>
|
||||||
<Text semanticField="proficiency">{item.proficiency}</Text>
|
{hasSplitRowText(item.proficiency) && <Text semanticField="proficiency">{item.proficiency}</Text>}
|
||||||
<Small semanticField="keywords">{item.keywords.join(", ")}</Small>
|
<Small semanticField="keywords">{item.keywords.join(", ")}</Small>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user