mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +10:00
136 lines
4.9 KiB
TypeScript
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);
|
|
});
|
|
});
|