mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +10:00
fix(stylesheet): apply item-header to every header row on every template (#3357)
`SectionItemHeader` only rendered its own box when a template opted into `mainItemHeaderBorder` (only Ditgar did). Everywhere else it walked the header children and attached the resolved `item-header` style to the first descendant that happened to be a literal `View` or `InlineItemHeader`. Sections whose header starts with anything else — certifications, awards, projects, publications, references — matched nothing, so the style was silently dropped; stacked headers such as experience matched only their first row, so a second row went unstyled. The header now always renders its own `Div`, so `item-header` covers the whole header row of every section on every template. `Div` rather than `View` keeps the base row gap the rows used to inherit from the item box, and Ditgar keeps its tight header via `rowGap: 0` on its own `sectionItemHeader` slot, so rendered output is unchanged apart from the newly styled rows. `mainItemHeaderBorder` is now dead and removed. Fixes #3349
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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 { templateSchema } from "@reactive-resume/schema/templates";
|
||||
import { ResumeDocument } from "../document";
|
||||
import { resolveResumeRuntime } from "./resolve";
|
||||
|
||||
type HostNode = { type: string; style?: unknown; value?: string; children?: HostNode[] };
|
||||
|
||||
const HEADER_BACKGROUND = "#ff0000";
|
||||
const STYLESHEET = `@version 1;\nitem-header { background-color: ${HEADER_BACKGROUND}; }`;
|
||||
|
||||
const mergedStyle = (node: HostNode): Record<string, unknown> =>
|
||||
Object.assign({}, ...(Array.isArray(node.style) ? node.style : node.style ? [node.style] : []));
|
||||
const nodeText = (node: HostNode): string => node.value ?? (node.children ?? []).map(nodeText).join("");
|
||||
const flatten = (node: HostNode): HostNode[] => [node, ...(node.children ?? []).flatMap(flatten)];
|
||||
|
||||
const buildFixture = (): ResumeData => {
|
||||
const data = structuredClone(defaultResumeData);
|
||||
data.picture.hidden = true;
|
||||
data.sections.certifications.items = [
|
||||
{
|
||||
id: "certification/1",
|
||||
hidden: false,
|
||||
title: "Certified Kubernetes Administrator",
|
||||
issuer: "The Linux Foundation",
|
||||
date: "Sep 2023",
|
||||
website: { url: "", label: "", inlineLink: false },
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
data.sections.experience.items = [
|
||||
{
|
||||
id: "experience/1",
|
||||
hidden: false,
|
||||
company: "Braincore",
|
||||
position: "Automation Engineer",
|
||||
location: "Jakarta",
|
||||
period: "Jan 2024 - Mar 2025",
|
||||
website: { url: "", label: "", inlineLink: false },
|
||||
description: "",
|
||||
roles: [],
|
||||
},
|
||||
];
|
||||
const page = data.metadata.layout.pages[0];
|
||||
if (!page) throw new Error("Missing authored page");
|
||||
page.main = ["certifications", "experience"];
|
||||
page.sidebar = [];
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const renderHeaderBoxes = async (template: Template): Promise<string[]> => {
|
||||
const data = buildFixture();
|
||||
const semanticRuntime = resolveResumeRuntime({
|
||||
data,
|
||||
template,
|
||||
mode: "semantic",
|
||||
source: { languageVersion: 1, text: STYLESHEET },
|
||||
});
|
||||
const element = createElement(ResumeDocument, { data, template, semanticRuntime }) as unknown as Parameters<
|
||||
typeof pdf
|
||||
>[0];
|
||||
const instance = pdf(element);
|
||||
await expect.poll(() => instance.container.document).not.toBeNull();
|
||||
const document = instance.container.document as unknown as HostNode;
|
||||
|
||||
return flatten(document)
|
||||
.filter((node) => mergedStyle(node).backgroundColor === HEADER_BACKGROUND)
|
||||
.map((node) => nodeText(node));
|
||||
};
|
||||
|
||||
// https://github.com/amruthpillai/reactive-resume/issues/3349: the resolved `item-header` style used
|
||||
// to be bound onto the first child that happened to be a literal `View`, so sections whose header
|
||||
// starts with anything else lost it entirely and stacked headers only styled their first row.
|
||||
describe("item-header binding", () => {
|
||||
it.each(templateSchema.options)("covers every header row of every section on %s", async (template) => {
|
||||
const headers = await renderHeaderBoxes(template);
|
||||
|
||||
expect(headers).toHaveLength(2);
|
||||
const [certification, experience] = headers;
|
||||
expect(certification).toContain("Certified Kubernetes Administrator");
|
||||
expect(certification).toContain("The Linux Foundation");
|
||||
expect(certification).toContain("Sep 2023");
|
||||
expect(experience).toContain("Braincore");
|
||||
expect(experience).toContain("Automation Engineer");
|
||||
expect(experience).toContain("Jan 2024 - Mar 2025");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user