Skip to content

Commit 3f33ccb

Browse files
rsbhclaude
andcommitted
test: pin SSR no-JS rendering contract
Contract tests for the prerender strategy in entry-server: large and nested lazy Suspense content must be inlined in place (no hidden divs, no $RC reveal scripts), plus a control test documenting that react-dom's default progressiveChunkSize would still outline large boundaries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6f45576 commit 3f33ccb

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { describe, expect, test } from 'bun:test';
2+
import React, { lazy, Suspense } from 'react';
3+
import { prerender } from 'react-dom/static.edge';
4+
5+
// Contract tests for the SSR rendering strategy used in entry-server.tsx.
6+
//
7+
// Pages must be fully readable with JavaScript disabled. React's streaming
8+
// renderer emits Suspense content as `<div hidden id="S:*">` segments revealed
9+
// by inline `$RC` scripts, and — crucially — `prerender` still "outlines"
10+
// completed boundaries larger than progressiveChunkSize (12,800 bytes by
11+
// default) into that same mechanism. entry-server.tsx therefore renders with
12+
// prerender + progressiveChunkSize: Number.MAX_SAFE_INTEGER. These tests pin
13+
// that contract against react-dom upgrades.
14+
15+
const NO_JS_RENDER_OPTIONS = { progressiveChunkSize: Number.MAX_SAFE_INTEGER };
16+
17+
// Well over the 12,800-byte default, like any real docs page.
18+
const BIG_TEXT = 'chronicle-no-js-content '.repeat(1000);
19+
20+
function lazyAfterTick<T extends React.ComponentType>(Component: T) {
21+
return lazy(
22+
() => new Promise<{ default: T }>(resolve => setTimeout(() => resolve({ default: Component }), 10))
23+
);
24+
}
25+
26+
async function renderToHtml(node: React.ReactNode, options?: { progressiveChunkSize?: number }) {
27+
const { prelude } = await prerender(
28+
<html lang="en">
29+
<head />
30+
<body>{node}</body>
31+
</html>,
32+
options
33+
);
34+
return new Response(prelude).text();
35+
}
36+
37+
function LargePage() {
38+
return (
39+
<main>
40+
<h1>Getting Started</h1>
41+
<p>{BIG_TEXT}</p>
42+
</main>
43+
);
44+
}
45+
46+
function expectFullyInlined(html: string) {
47+
expect(html).not.toContain('<!--$?-->'); // pending boundary marker
48+
expect(html).not.toContain('hidden id="S:'); // outlined content segment
49+
expect(html).not.toContain('$RC('); // client-side reveal script
50+
expect(html).not.toContain('<template id="B:'); // fallback placeholder
51+
}
52+
53+
describe('SSR no-JS contract (prerender + unbounded progressiveChunkSize)', () => {
54+
test('inlines large lazy Suspense content in place', async () => {
55+
const Page = lazyAfterTick(LargePage);
56+
const html = await renderToHtml(
57+
<Suspense fallback={<span>skeleton</span>}>
58+
<Page />
59+
</Suspense>,
60+
NO_JS_RENDER_OPTIONS
61+
);
62+
63+
expectFullyInlined(html);
64+
expect(html).toContain(BIG_TEXT.slice(0, 100));
65+
expect(html).not.toContain('skeleton');
66+
});
67+
68+
test('inlines nested Suspense boundaries (layout > page > toc shape)', async () => {
69+
const Toc = lazyAfterTick(() => <nav>toc-entries</nav>);
70+
const Page = lazyAfterTick(() => (
71+
<article>
72+
<p>{BIG_TEXT}</p>
73+
<Suspense fallback={null}>
74+
<Toc />
75+
</Suspense>
76+
</article>
77+
));
78+
const html = await renderToHtml(
79+
<Suspense fallback={<span>skeleton</span>}>
80+
<Page />
81+
</Suspense>,
82+
NO_JS_RENDER_OPTIONS
83+
);
84+
85+
expectFullyInlined(html);
86+
expect(html).toContain('toc-entries');
87+
expect(html).toContain(BIG_TEXT.slice(0, 100));
88+
});
89+
90+
test('waits for slow lazy chunks instead of emitting fallbacks', async () => {
91+
const Slow = lazy(
92+
() =>
93+
new Promise<{ default: React.ComponentType }>(resolve =>
94+
setTimeout(() => resolve({ default: () => <p>slow-chunk-content</p> }), 100)
95+
)
96+
);
97+
const html = await renderToHtml(
98+
<Suspense fallback={<span>skeleton</span>}>
99+
<Slow />
100+
</Suspense>,
101+
NO_JS_RENDER_OPTIONS
102+
);
103+
104+
expectFullyInlined(html);
105+
expect(html).toContain('slow-chunk-content');
106+
expect(html).not.toContain('skeleton');
107+
});
108+
109+
// Documents WHY entry-server.tsx must pass progressiveChunkSize: with the
110+
// react-dom default, a completed boundary this large is still outlined into
111+
// hidden divs that only inline scripts can reveal. If this test ever fails,
112+
// React changed the outlining behavior and the override can be revisited.
113+
test('default progressiveChunkSize would break no-JS rendering', async () => {
114+
const Page = lazyAfterTick(LargePage);
115+
const html = await renderToHtml(
116+
<Suspense fallback={<span>skeleton</span>}>
117+
<Page />
118+
</Suspense>
119+
);
120+
121+
expect(html).toContain('<!--$?-->');
122+
expect(html).toContain('hidden id="S:');
123+
expect(html).toContain('$RC(');
124+
expect(html).toContain('skeleton');
125+
});
126+
});

0 commit comments

Comments
 (0)