Skip to content
File

Blob: tests/sites/static-renderer-prep.test.ts

typescript115 lines
1import { describe, expect, it } from "vitest";
2import { renderToReadableStream, renderToStaticMarkup } from "react-dom/server";
3import { runWithSitesReactRenderContext } from "@/sites/react-render-context";
4import { renderBlandSitesDocumentToReactElement } from "@/sites/static-renderer";
5import { createTestSitesPageRenderContext } from "./render-context";
6 
7const CONTENT = {
8 type: "doc",
9 content: [
10 {
11 type: "paragraph",
12 attrs: { bid: "intro-bid" },
13 content: [{ type: "text", text: "Intro" }],
14 },
15 {
16 type: "callout",
17 attrs: { bid: "callout-bid", kind: "warning" },
18 content: [{ type: "paragraph", content: [{ type: "text", text: "Check this" }] }],
19 },
20 {
21 type: "codeBlock",
22 attrs: { bid: "code-bid", language: "ts" },
23 content: [{ type: "text", text: "const answer = 42" }],
24 },
25 {
26 type: "image",
27 attrs: {
28 bid: "image-bid",
29 src: "/uploads/example.png",
30 alt: "Example image",
31 title: null,
32 align: "center",
33 width: 480,
34 naturalWidth: 1280,
35 naturalHeight: 720,
36 pendingInsertId: "pending-1",
37 },
38 },
39 {
40 type: "paragraph",
41 attrs: { bid: "mention-bid" },
42 content: [
43 { type: "text", text: "See " },
44 { type: "pageMention", attrs: { pageId: "page-roadmap" } },
45 ],
46 },
47 ],
48};
49 
50function renderElement() {
51 return runWithSitesReactRenderContext(
52 createTestSitesPageRenderContext({
53 resolvePageMention: (pageId) =>
54 pageId === "page-roadmap"
55 ? { label: "Roadmap", href: "/sites/roadmap", ariaLabel: "Roadmap page" }
56 : { label: "Restricted", href: null, kind: "restricted" },
57 }),
58 () => renderBlandSitesDocumentToReactElement(CONTENT),
59 );
60}
61 
62describe("Bland Sites static renderer prep", () => {
63 it("renders Tiptap JSON to static markup through explicit custom node mappings", () => {
64 const html = renderToStaticMarkup(renderElement());
65 
66 expect(html).toContain('data-bid="intro-bid"');
67 expect(html).toContain('data-bid="callout-bid"');
68 expect(html).toContain('data-callout-kind="warning"');
69 expect(html).toContain("Check this");
70 expect(html).toContain('data-bid="code-bid"');
71 expect(html).toContain('data-language="typescript"');
72 expect(html).toContain("hljs-keyword");
73 expect(html).toContain(">const<");
74 expect(html).toContain("answer");
75 expect(html).toContain('data-bid="image-bid"');
76 expect(html).toContain('src="/uploads/example.png"');
77 expect(html).toContain("Example image");
78 expect(html).toContain('href="/sites/roadmap"');
79 expect(html).toContain("Roadmap");
80 });
81 
82 it("filters internal and null image attrs from rendered image HTML attributes", () => {
83 const html = renderToStaticMarkup(renderElement());
84 
85 // naturalWidth/naturalHeight appear inside the sites-image island prop
86 // payload (a <template> body) so the client hydration can reserve aspect
87 // ratio. Neither should leak as an HTML attribute on the <img>.
88 expect(html).not.toMatch(/<img[^>]*\bnaturalWidth=/);
89 expect(html).not.toMatch(/<img[^>]*\bnaturalHeight=/);
90 expect(html).not.toContain("pendingInsertId");
91 expect(html).not.toContain('title="null"');
92 });
93 
94 it("can stream the same React element for future Sites responses", async () => {
95 const stream = await renderToReadableStream(renderElement());
96 const html = await new Response(stream).text();
97 
98 expect(html).toContain("Intro");
99 expect(html).toContain("Roadmap");
100 });
101 
102 it("renders dimmed images in initial HTML and carries brightness into hydration", () => {
103 const html = renderToStaticMarkup(
104 renderBlandSitesDocumentToReactElement({
105 type: "doc",
106 content: [{ type: "image", attrs: { src: "/uploads/dimmed.png", brightness: 75 } }],
107 }),
108 );
109 expect(html).toMatch(/<img[^>]+style="filter:brightness\(0\.75\)"/);
110 expect(html).toContain('"brightness":75');
111 expect(html).not.toMatch(/<img[^>]*\sbrightness=/);
112 expect(html).not.toMatch(/<figure[^>]*filter:/);
113 });
114});