Skip to content
File

Blob: tests/sites/document.test.tsx

typescript109 lines
1import { renderToReadableStream, renderToStaticMarkup } from "react-dom/server";
2import { describe, expect, it } from "vitest";
3 
4import { renderSiteBuildingDocumentHtml, SitePageDocument } from "@/sites/document";
5import { runWithSitesReactRenderContext } from "@/sites/react-render-context";
6import { createTestSitesPageRenderContext, TEST_SITE_DOCUMENT_ASSETS } from "./render-context";
7 
8async function renderSiteDocumentHtml(): Promise<string> {
9 const context = createTestSitesPageRenderContext();
10 const stream = await runWithSitesReactRenderContext(context, () =>
11 renderToReadableStream(
12 <SitePageDocument>
13 <p>Body</p>
14 </SitePageDocument>,
15 {
16 bootstrapModules: context.assets.scriptSrc ? [context.assets.scriptSrc] : undefined,
17 },
18 ),
19 );
20 return new Response(stream).text();
21}
22 
23describe("SitePageDocument", () => {
24 it("streams one doctype, metadata, emoji attrs, CSS, and body-tail script resources", async () => {
25 const html = await renderSiteDocumentHtml();
26 
27 expect(html).toMatch(/^<!DOCTYPE html><html/);
28 expect(html).not.toContain("<!doctype html><!DOCTYPE html>");
29 expect(html).toContain('href="/site-assets/sites-test.css" data-precedence="default"');
30 expect(html).toContain('rel="preload" as="style" href="/site-assets/fonts-test.css"');
31 expect(html).toContain('rel="stylesheet" href="/site-assets/fonts-test.css"');
32 expect(html).toContain('rel="modulepreload"');
33 expect(html).toContain('href="/site-assets/sites-import-test.js"');
34 expect(html).toContain('src="/site-assets/sites-entry-test.js"');
35 expect(html).toContain('name="description" content="Body excerpt for search snippets"');
36 expect(html).toContain('property="og:description" content="Body excerpt for search snippets"');
37 expect(html).toMatch(
38 /<img[^>]+class="site-workspace-mark-img[^"]*"[^>]+loading="lazy"[^>]+decoding="async"[^>]+fetchPriority="low"[^>]+width="20"[^>]+height="20"/,
39 );
40 expect(html).toMatch(
41 /<img[^>]+class="site-icon-img[^"]*"[^>]+loading="lazy"[^>]+decoding="async"[^>]+fetchPriority="low"[^>]+width="28"[^>]+height="28"/,
42 );
43 expect(html.indexOf('rel="preload" as="style" href="/site-assets/fonts-test.css"')).toBeLessThan(
44 html.indexOf("<body"),
45 );
46 expect(html.lastIndexOf('rel="stylesheet" href="/site-assets/fonts-test.css"')).toBeGreaterThan(
47 html.indexOf("</main>"),
48 );
49 expect(html.indexOf('src="/site-assets/sites-entry-test.js"')).toBeGreaterThan(
50 html.lastIndexOf('rel="stylesheet" href="/site-assets/fonts-test.css"'),
51 );
52 });
53 
54 it("throws a clear error when rendered outside the Sites React context", () => {
55 expect(() =>
56 renderToStaticMarkup(
57 <SitePageDocument>
58 <p>Body</p>
59 </SitePageDocument>,
60 ),
61 ).toThrow("Sites React render context is required");
62 });
63 
64 it("keeps page document title and assets isolated across concurrent ALS streams", async () => {
65 async function renderIsolated(title: string, stylesheetHref: string) {
66 const context = createTestSitesPageRenderContext({
67 assets: { stylesheetHref, scriptSrc: null },
68 page: { title, canonicalUrl: `https://acme.sites.test/${title.toLowerCase().replaceAll(" ", "-")}` },
69 });
70 const stream = await runWithSitesReactRenderContext(context, () =>
71 renderToReadableStream(
72 <SitePageDocument>
73 <p>{title}</p>
74 </SitePageDocument>,
75 ),
76 );
77 return new Response(stream).text();
78 }
79 
80 const [first, second] = await Promise.all([
81 renderIsolated("First Page", "/site-assets/first.css"),
82 renderIsolated("Second Page", "/site-assets/second.css"),
83 ]);
84 
85 expect(first).toContain("<title>First Page</title>");
86 expect(first).toContain('href="/site-assets/first.css"');
87 expect(first).not.toContain("Second Page");
88 expect(first).not.toContain("/site-assets/second.css");
89 expect(second).toContain("<title>Second Page</title>");
90 expect(second).toContain('href="/site-assets/second.css"');
91 expect(second).not.toContain("First Page");
92 expect(second).not.toContain("/site-assets/first.css");
93 });
94 
95 it("renders the building status document without a meta refresh", () => {
96 const html = renderSiteBuildingDocumentHtml({
97 assets: TEST_SITE_DOCUMENT_ASSETS,
98 site: { workspaceName: "Acme", workspaceIcon: "A", homeHref: "/" },
99 });
100 
101 expect(html).toContain("<title>Preparing page - Acme</title>");
102 expect(html).toContain("Hang tight.");
103 expect(html).toContain("This page is being prepared. Come back in a moment.");
104 expect(html).toContain("Back to Acme");
105 expect(html).not.toContain('http-equiv="refresh"');
106 expect(html).not.toContain("httpEquiv");
107 });
108});