main
js 333 lines 9.06 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
13
14 // Polyfills for test environment
15 global.ReadableStream =
16 require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17 global.TextEncoder = require('util').TextEncoder;
18 global.TextDecoder = require('util').TextDecoder;
19
20 let act;
21 let serverAct;
22 let use;
23 let clientExports;
24 let clientExportsESM;
25 let turbopackMap;
26 let Stream;
27 let React;
28 let ReactDOMClient;
29 let ReactServerDOMServer;
30 let ReactServerDOMClient;
31 let Suspense;
32 let ErrorBoundary;
33
34 describe('ReactFlightTurbopackDOM', () => {
35 beforeEach(() => {
36 // For this first reset we are going to load the dom-node version of react-server-dom-turbopack/server
37 // This can be thought of as essentially being the React Server Components scope with react-server
38 // condition
39 jest.resetModules();
40
41 patchSetImmediate();
42 serverAct = require('internal-test-utils').serverAct;
43
44 // Simulate the condition resolution
45 jest.mock('react-server-dom-turbopack/server', () =>
46 require('react-server-dom-turbopack/server.node'),
47 );
48 jest.mock('react', () => require('react/react.react-server'));
49
50 const TurbopackMock = require('./utils/TurbopackMock');
51 clientExports = TurbopackMock.clientExports;
52 clientExportsESM = TurbopackMock.clientExportsESM;
53 turbopackMap = TurbopackMock.turbopackMap;
54
55 ReactServerDOMServer = require('react-server-dom-turbopack/server');
56
57 // This reset is to load modules for the SSR/Browser scope.
58 jest.resetModules();
59 __unmockReact();
60 act = require('internal-test-utils').act;
61 Stream = require('stream');
62 React = require('react');
63 use = React.use;
64 Suspense = React.Suspense;
65 ReactDOMClient = require('react-dom/client');
66 jest.mock('react-server-dom-turbopack/client', () =>
67 require('react-server-dom-turbopack/client.browser'),
68 );
69 ReactServerDOMClient = require('react-server-dom-turbopack/client');
70
71 ErrorBoundary = class extends React.Component {
72 state = {hasError: false, error: null};
73 static getDerivedStateFromError(error) {
74 return {
75 hasError: true,
76 error,
77 };
78 }
79 render() {
80 if (this.state.hasError) {
81 return this.props.fallback(this.state.error);
82 }
83 return this.props.children;
84 }
85 };
86 });
87
88 function getTestStream() {
89 const writable = new Stream.PassThrough();
90 const readable = new ReadableStream({
91 start(controller) {
92 writable.on('data', chunk => {
93 controller.enqueue(chunk);
94 });
95 writable.on('end', () => {
96 controller.close();
97 });
98 },
99 });
100 return {
101 readable,
102 writable,
103 };
104 }
105
106 it('should resolve HTML using Node streams', async () => {
107 function Text({children}) {
108 return <span>{children}</span>;
109 }
110 function HTML() {
111 return (
112 <div>
113 <Text>hello</Text>
114 <Text>world</Text>
115 </div>
116 );
117 }
118
119 function App() {
120 const model = {
121 html: <HTML />,
122 };
123 return model;
124 }
125
126 const {writable, readable} = getTestStream();
127 const {pipe} = await serverAct(() =>
128 ReactServerDOMServer.renderToPipeableStream(<App />, turbopackMap),
129 );
130 pipe(writable);
131 const response = ReactServerDOMClient.createFromReadableStream(readable);
132 const model = await response;
133 expect(model).toEqual({
134 html: (
135 <div>
136 <span>hello</span>
137 <span>world</span>
138 </div>
139 ),
140 });
141 });
142
143 it('should resolve the root', async () => {
144 // Model
145 function Text({children}) {
146 return <span>{children}</span>;
147 }
148 function HTML() {
149 return (
150 <div>
151 <Text>hello</Text>
152 <Text>world</Text>
153 </div>
154 );
155 }
156 function RootModel() {
157 return {
158 html: <HTML />,
159 };
160 }
161
162 // View
163 function Message({response}) {
164 return <section>{use(response).html}</section>;
165 }
166 function App({response}) {
167 return (
168 <Suspense fallback={<h1>Loading...</h1>}>
169 <Message response={response} />
170 </Suspense>
171 );
172 }
173
174 const {writable, readable} = getTestStream();
175 const {pipe} = await serverAct(() =>
176 ReactServerDOMServer.renderToPipeableStream(<RootModel />, turbopackMap),
177 );
178 pipe(writable);
179 const response = ReactServerDOMClient.createFromReadableStream(readable);
180
181 const container = document.createElement('div');
182 const root = ReactDOMClient.createRoot(container);
183 await act(() => {
184 root.render(<App response={response} />);
185 });
186 expect(container.innerHTML).toBe(
187 '<section><div><span>hello</span><span>world</span></div></section>',
188 );
189 });
190
191 it('should unwrap async module references', async () => {
192 const AsyncModule = Promise.resolve(function AsyncModule({text}) {
193 return 'Async: ' + text;
194 });
195
196 const AsyncModule2 = Promise.resolve({
197 exportName: 'Module',
198 });
199
200 function Print({response}) {
201 return <p>{use(response)}</p>;
202 }
203
204 function App({response}) {
205 return (
206 <Suspense fallback={<h1>Loading...</h1>}>
207 <Print response={response} />
208 </Suspense>
209 );
210 }
211
212 const AsyncModuleRef = await clientExports(AsyncModule);
213 const AsyncModuleRef2 = await clientExports(AsyncModule2);
214
215 const {writable, readable} = getTestStream();
216 const {pipe} = await serverAct(() =>
217 ReactServerDOMServer.renderToPipeableStream(
218 <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
219 turbopackMap,
220 ),
221 );
222 pipe(writable);
223 const response = ReactServerDOMClient.createFromReadableStream(readable);
224
225 const container = document.createElement('div');
226 const root = ReactDOMClient.createRoot(container);
227 await act(() => {
228 root.render(<App response={response} />);
229 });
230 expect(container.innerHTML).toBe('<p>Async: Module</p>');
231 });
232
233 it('should unwrap async ESM module references', async () => {
234 const AsyncModule = Promise.resolve(function AsyncModule({text}) {
235 return 'Async: ' + text;
236 });
237
238 const AsyncModule2 = Promise.resolve({
239 exportName: 'Module',
240 });
241
242 function Print({response}) {
243 return <p>{use(response)}</p>;
244 }
245
246 function App({response}) {
247 return (
248 <Suspense fallback={<h1>Loading...</h1>}>
249 <Print response={response} />
250 </Suspense>
251 );
252 }
253
254 const AsyncModuleRef = await clientExportsESM(AsyncModule);
255 const AsyncModuleRef2 = await clientExportsESM(AsyncModule2);
256
257 const {writable, readable} = getTestStream();
258 const {pipe} = await serverAct(() =>
259 ReactServerDOMServer.renderToPipeableStream(
260 <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
261 turbopackMap,
262 ),
263 );
264 pipe(writable);
265 const response = ReactServerDOMClient.createFromReadableStream(readable);
266
267 const container = document.createElement('div');
268 const root = ReactDOMClient.createRoot(container);
269 await act(() => {
270 root.render(<App response={response} />);
271 });
272 expect(container.innerHTML).toBe('<p>Async: Module</p>');
273 });
274
275 it('should error when a bundler uses async ESM modules with createClientModuleProxy', async () => {
276 const AsyncModule = Promise.resolve(function AsyncModule() {
277 return 'This should not be rendered';
278 });
279
280 function Print({response}) {
281 return <p>{use(response)}</p>;
282 }
283
284 function App({response}) {
285 return (
286 <ErrorBoundary
287 fallback={error => (
288 <p>
289 {__DEV__ ? error.message + ' + ' : null}
290 {error.digest}
291 </p>
292 )}>
293 <Suspense fallback={<h1>Loading...</h1>}>
294 <Print response={response} />
295 </Suspense>
296 </ErrorBoundary>
297 );
298 }
299
300 const AsyncModuleRef = await clientExportsESM(AsyncModule, {
301 forceClientModuleProxy: true,
302 });
303
304 const {writable, readable} = getTestStream();
305 const {pipe} = await serverAct(() =>
306 ReactServerDOMServer.renderToPipeableStream(
307 <AsyncModuleRef />,
308 turbopackMap,
309 {
310 onError(error) {
311 return __DEV__ ? 'a dev digest' : `digest(${error.message})`;
312 },
313 },
314 ),
315 );
316 pipe(writable);
317 const response = ReactServerDOMClient.createFromReadableStream(readable);
318
319 const container = document.createElement('div');
320 const root = ReactDOMClient.createRoot(container);
321 await act(() => {
322 root.render(<App response={response} />);
323 });
324
325 const errorMessage = `The module "${Object.keys(turbopackMap).at(0)}" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.`;
326
327 expect(container.innerHTML).toBe(
328 __DEV__
329 ? `<p>${errorMessage} + a dev digest</p>`
330 : `<p>digest(${errorMessage})</p>`,
331 );
332 });
333 });