Skip to content
File

Blob: tests/shared/sites/schemas.test.ts

typescript131 lines
1import { describe, expect, it } from "vitest";
2 
3import {
4 parseSitePmJsonEnvelope,
5 SitePmJsonEnvelopeSchema,
6 type SitePmJsonEnvelope,
7} from "@/shared/sites/pm-json-schemas";
8import { readIslandProps } from "@/shared/sites/island-schemas";
9 
10describe("SitePmJsonEnvelopeSchema", () => {
11 const envelope: SitePmJsonEnvelope = {
12 content: {
13 type: "doc",
14 content: [
15 {
16 type: "paragraph",
17 content: [{ type: "text", text: "hi" }],
18 },
19 ],
20 },
21 metrics: { words: 1, characters: 2 },
22 updatedAt: "2026-05-17T12:00:00.000Z",
23 };
24 
25 it("round-trips a valid envelope", () => {
26 expect(parseSitePmJsonEnvelope(JSON.stringify(envelope))).toEqual(envelope);
27 expect(SitePmJsonEnvelopeSchema.safeParse(envelope).success).toBe(true);
28 });
29 
30 it("preserves loose ProseMirror JSON keys", () => {
31 const parsed = parseSitePmJsonEnvelope(
32 JSON.stringify({
33 ...envelope,
34 content: {
35 type: "doc",
36 customRootKey: true,
37 content: [
38 {
39 type: "paragraph",
40 attrs: { bid: "abc" },
41 customNodeKey: "kept",
42 content: [
43 {
44 type: "text",
45 text: "hi",
46 marks: [{ type: "bold", customMarkKey: 1 }],
47 },
48 ],
49 },
50 ],
51 },
52 }),
53 );
54 
55 expect(parsed?.content.customRootKey).toBe(true);
56 expect(parsed?.content.content?.[0]?.customNodeKey).toBe("kept");
57 expect(parsed?.content.content?.[0]?.content?.[0]?.marks?.[0]?.customMarkKey).toBe(1);
58 });
59 
60 it("rejects malformed top-level JSON", () => {
61 expect(parseSitePmJsonEnvelope("not json")).toBeNull();
62 });
63 
64 it("rejects missing or wrongly-typed required fields", () => {
65 const { ...without } = envelope as Partial<SitePmJsonEnvelope>;
66 delete without.content;
67 expect(parseSitePmJsonEnvelope(JSON.stringify(without))).toBeNull();
68 
69 expect(parseSitePmJsonEnvelope(JSON.stringify({ ...envelope, updatedAt: 42 }))).toBeNull();
70 expect(parseSitePmJsonEnvelope(JSON.stringify({ ...envelope, metrics: { words: -1, characters: 0 } }))).toBeNull();
71 });
72 
73 it("rejects the old SiteRenderArtifact shape so eviction is automatic", () => {
74 expect(
75 parseSitePmJsonEnvelope(
76 JSON.stringify({
77 version: 1,
78 bodyHtml: "<p>x</p>",
79 outline: [],
80 metrics: { words: 0, characters: 0 },
81 }),
82 ),
83 ).toBeNull();
84 });
85});
86 
87describe("readIslandProps", () => {
88 it("parses sites-image props", () => {
89 const props = readIslandProps(
90 "sites-image",
91 JSON.stringify({ src: "/_assets/1/x", align: "center", naturalWidth: 100, naturalHeight: 50 }),
92 );
93 expect(props).toEqual({
94 src: "/_assets/1/x",
95 align: "center",
96 naturalWidth: 100,
97 naturalHeight: 50,
98 });
99 });
100 
101 it("rejects truncated JSON", () => {
102 expect(readIslandProps("sites-image", '{"src":"/_assets/1/x"')).toBeNull();
103 });
104 
105 it("accepts image brightness percentages and rejects unsafe hydration values", () => {
106 expect(readIslandProps("sites-image", JSON.stringify({ src: "/_assets/1/x", brightness: 75 }))?.brightness).toBe(
107 75,
108 );
109 for (const brightness of [0, 101, "75", "brightness(0.75)"]) {
110 expect(readIslandProps("sites-image", JSON.stringify({ src: "/_assets/1/x", brightness }))).toBeNull();
111 }
112 });
113 
114 it("rejects wrong types", () => {
115 expect(readIslandProps("sites-image", JSON.stringify({ src: 0 }))).toBeNull();
116 expect(readIslandProps("site-outline-controller", JSON.stringify({ items: "not an array" }))).toBeNull();
117 });
118 
119 it("parses empty copy-code props", () => {
120 expect(readIslandProps("copy-code", "{}")).toEqual({});
121 });
122 
123 it("parses outline controller items", () => {
124 const props = readIslandProps(
125 "site-outline-controller",
126 JSON.stringify({ items: [{ id: "intro", text: "Intro", level: 1, href: "#intro" }] }),
127 );
128 expect(props?.items).toHaveLength(1);
129 });
130});