Files
Reactive-Resume/packages/pdf/src/semantic/public-projection.test.ts
T
2026-07-30 12:39:15 +02:00

136 lines
4.9 KiB
TypeScript

import type { PublicStyleProjection } from "./public-projection";
import { describe, expect, it } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import {
createPublicStyleProjection,
PUBLIC_STYLE_PROJECTION_FORMAT_VERSION,
SEMANTIC_TREE_VERSION,
validatePublicStyleProjection,
} from "./public-projection";
const buildData = () => {
const data = structuredClone(defaultResumeData);
const applied = {
languageVersion: 1,
text: "@version 1;\nname { color: #123456; }\n",
};
data.basics.name = "Ada Lovelace";
data.metadata.layout.pages = [{ fullWidth: true, main: [], sidebar: [] }];
data.metadata.stylesheet = { mode: "semantic", source: applied, applied };
return data;
};
describe("public semantic style projection", () => {
it("contains only resolved, JSON-safe presentation keyed by stable node key", async () => {
const projection = await createPublicStyleProjection({ data: buildData() });
const serialized = JSON.stringify(projection);
expect(projection).toMatchObject({
formatVersion: PUBLIC_STYLE_PROJECTION_FORMAT_VERSION,
languageVersion: 1,
semanticTreeVersion: SEMANTIC_TREE_VERSION,
registryFingerprint: expect.stringMatching(/^[a-f0-9]{64}$/),
adapterFingerprint: expect.stringMatching(/^[a-f0-9]{64}$/),
renderDataHash: expect.stringMatching(/^[a-f0-9]{64}$/),
});
expect(projection.nodes["page-1/region-header/header/name"]).toEqual({
style: { color: "#123456" },
});
expect(serialized).not.toContain("@version");
expect(serialized).not.toMatch(/source|comment|diagnostic|selector|variable|range/i);
expect(serialized).not.toContain("undefined");
});
it("carries final sibling visibility and order without stylesheet source", async () => {
const data = buildData();
data.basics.email = "ada@example.com";
data.basics.phone = "+44 123";
data.basics.location = "London";
const applied = {
languageVersion: 1,
text: `@version 1;
contact-item[name="location"] { display: none; }
contact-item[name="phone"] { order: -1; }
`,
};
data.metadata.stylesheet = { mode: "semantic", source: applied, applied };
const projection = await createPublicStyleProjection({ data });
expect(projection.nodes["page-1/region-header/header/contact-list/contact-location"]).toMatchObject({
hidden: true,
});
expect(projection.nodes["page-1/region-header/header/contact-list/contact-phone"]).toMatchObject({ order: 0 });
expect(projection.nodes["page-1/region-header/header/contact-list/contact-email"]).toMatchObject({ order: 1 });
});
it("rejects changed nodes and every version or fingerprint mismatch", async () => {
const data = buildData();
const valid = await createPublicStyleProjection({ data });
const cases = [
{ ...valid, formatVersion: 2 },
{ ...valid, languageVersion: 2 },
{ ...valid, semanticTreeVersion: 2 },
{ ...valid, registryFingerprint: "0".repeat(64) },
{ ...valid, adapterFingerprint: "0".repeat(64) },
{ ...valid, renderDataHash: "0".repeat(64) },
{
...valid,
nodes: {
...valid.nodes,
"page-1/region-header/header/name": { style: { color: "#ff0000" } },
},
},
];
for (const projection of cases) {
await expect(validatePublicStyleProjection(data, projection as unknown as PublicStyleProjection)).resolves.toBe(
false,
);
}
});
it("rejects projections hashed for different public render data", async () => {
const data = buildData();
const projection = await createPublicStyleProjection({ data });
const changed = structuredClone(data);
changed.basics.name = "Grace Hopper";
await expect(validatePublicStyleProjection(changed, projection)).resolves.toBe(false);
});
it("changes the projection hash when only the applied presentation changes", async () => {
const red = buildData();
const blue = buildData();
const blueApplied = {
languageVersion: 1,
text: "@version 1;\nname { color: #654321; }\n",
};
blue.metadata.stylesheet = { mode: "semantic", source: blueApplied, applied: blueApplied };
const redProjection = await createPublicStyleProjection({ data: red });
const blueProjection = await createPublicStyleProjection({ data: blue });
expect(redProjection.nodes["page-1/region-header/header/name"]).not.toEqual(
blueProjection.nodes["page-1/region-header/header/name"],
);
expect(redProjection.renderDataHash).not.toBe(blueProjection.renderDataHash);
});
it("rejects extra or non-JSON node fields instead of exposing compiler internals", async () => {
const data = buildData();
const projection = await createPublicStyleProjection({ data });
const node = projection.nodes["page-1/region-header/header/name"];
await expect(
validatePublicStyleProjection(data, {
...projection,
nodes: {
...projection.nodes,
"page-1/region-header/header/name": { ...node, diagnostics: [{ message: "private" }] },
},
} as unknown as PublicStyleProjection),
).resolves.toBe(false);
});
});