mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-24 23:32:19 +10:00
feat: add semantic CSS stylesheets (#3274)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor Agent
parent
4ac19f81b3
commit
d2ffbf9618
@@ -0,0 +1,135 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user