File
Blob: tests/sites/document.test.tsx
| 1 | import { renderToReadableStream, renderToStaticMarkup } from "react-dom/server"; |
| 2 | import { describe, expect, it } from "vitest"; |
| 3 | |
| 4 | import { renderSiteBuildingDocumentHtml, SitePageDocument } from "@/sites/document"; |
| 5 | import { runWithSitesReactRenderContext } from "@/sites/react-render-context"; |
| 6 | import { createTestSitesPageRenderContext, TEST_SITE_DOCUMENT_ASSETS } from "./render-context"; |
| 7 | |
| 8 | async 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 | |
| 23 | describe("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 | }); |