main
js 253 lines 6.76 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 {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
13
14 // Polyfills for test environment
15 global.ReadableStream =
16 require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17 global.WritableStream =
18 require('web-streams-polyfill/ponyfill/es6').WritableStream;
19 global.TextEncoder = require('util').TextEncoder;
20 global.TextDecoder = require('util').TextDecoder;
21
22 let clientExports;
23 let React;
24 let ReactDOMClient;
25 let ReactServerDOMServer;
26 let ReactServerDOMClient;
27 let ReactServer;
28 let ReactServerScheduler;
29 let act;
30 let serverAct;
31 let turbopackMap;
32 let use;
33
34 describe('ReactFlightTurbopackDOMBrowser', () => {
35 beforeEach(() => {
36 jest.resetModules();
37
38 ReactServerScheduler = require('scheduler');
39 patchMessageChannel(ReactServerScheduler);
40 serverAct = require('internal-test-utils').serverAct;
41
42 // Simulate the condition resolution
43 jest.mock('react', () => require('react/react.react-server'));
44 ReactServer = require('react');
45
46 jest.mock('react-server-dom-turbopack/server', () =>
47 require('react-server-dom-turbopack/server.browser'),
48 );
49 const TurbopackMock = require('./utils/TurbopackMock');
50 clientExports = TurbopackMock.clientExports;
51 turbopackMap = TurbopackMock.turbopackMap;
52
53 ReactServerDOMServer = require('react-server-dom-turbopack/server.browser');
54
55 __unmockReact();
56 jest.resetModules();
57
58 ({act} = require('internal-test-utils'));
59 React = require('react');
60 ReactDOMClient = require('react-dom/client');
61 jest.mock('react-server-dom-turbopack/client', () =>
62 require('react-server-dom-turbopack/client.browser'),
63 );
64 ReactServerDOMClient = require('react-server-dom-turbopack/client');
65 use = React.use;
66 });
67
68 function createDelayedStream(
69 stream: ReadableStream<Uint8Array>,
70 ): ReadableStream<Uint8Array> {
71 return new ReadableStream({
72 async start(controller) {
73 const reader = stream.getReader();
74 while (true) {
75 const {done, value} = await reader.read();
76 if (done) {
77 controller.close();
78 } else {
79 // Artificially delay between enqueuing chunks.
80 await new Promise(resolve => setTimeout(resolve));
81 controller.enqueue(value);
82 }
83 }
84 },
85 });
86 }
87
88 function normalizeCodeLocInfo(str) {
89 return (
90 str &&
91 str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
92 return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
93 })
94 );
95 }
96
97 it('should resolve HTML using W3C streams', async () => {
98 function Text({children}) {
99 return <span>{children}</span>;
100 }
101 function HTML() {
102 return (
103 <div>
104 <Text>hello</Text>
105 <Text>world</Text>
106 </div>
107 );
108 }
109
110 function App() {
111 const model = {
112 html: <HTML />,
113 };
114 return model;
115 }
116
117 const stream = await serverAct(() =>
118 ReactServerDOMServer.renderToReadableStream(<App />),
119 );
120 const response = ReactServerDOMClient.createFromReadableStream(stream);
121 const model = await response;
122 expect(model).toEqual({
123 html: (
124 <div>
125 <span>hello</span>
126 <span>world</span>
127 </div>
128 ),
129 });
130 });
131
132 it('does not close the response early when using a fast debug channel', async () => {
133 function Component() {
134 return <div>Hi</div>;
135 }
136
137 let debugReadableStreamController;
138
139 const debugReadableStream = new ReadableStream({
140 start(controller) {
141 debugReadableStreamController = controller;
142 },
143 });
144
145 const rscStream = await serverAct(() =>
146 ReactServerDOMServer.renderToReadableStream(<Component />, turbopackMap, {
147 debugChannel: {
148 writable: new WritableStream({
149 write(chunk) {
150 debugReadableStreamController.enqueue(chunk);
151 },
152 close() {
153 debugReadableStreamController.close();
154 },
155 }),
156 },
157 }),
158 );
159
160 function ClientRoot({response}) {
161 return use(response);
162 }
163
164 const response = ReactServerDOMClient.createFromReadableStream(
165 // Create a delayed stream to simulate that the RSC stream might be
166 // transported slower than the debug channel, which must not lead to a
167 // `Connection closed` error in the Flight client.
168 createDelayedStream(rscStream),
169 {
170 debugChannel: {readable: debugReadableStream},
171 },
172 );
173
174 const container = document.createElement('div');
175 const root = ReactDOMClient.createRoot(container);
176
177 await act(() => {
178 root.render(<ClientRoot response={response} />);
179 });
180
181 expect(container.innerHTML).toBe('<div>Hi</div>');
182 });
183
184 it('can transport debug info through a dedicated debug channel', async () => {
185 let ownerStack;
186
187 const ClientComponent = clientExports(() => {
188 ownerStack = React.captureOwnerStack ? React.captureOwnerStack() : null;
189 return <p>Hi</p>;
190 });
191
192 function App() {
193 return ReactServer.createElement(
194 ReactServer.Suspense,
195 null,
196 ReactServer.createElement(ClientComponent, null),
197 );
198 }
199
200 let debugReadableStreamController;
201
202 const debugReadableStream = new ReadableStream({
203 start(controller) {
204 debugReadableStreamController = controller;
205 },
206 });
207
208 const rscStream = await serverAct(() =>
209 ReactServerDOMServer.renderToReadableStream(
210 ReactServer.createElement(App, null),
211 turbopackMap,
212 {
213 debugChannel: {
214 writable: new WritableStream({
215 write(chunk) {
216 debugReadableStreamController.enqueue(chunk);
217 },
218 close() {
219 debugReadableStreamController.close();
220 },
221 }),
222 },
223 },
224 ),
225 );
226
227 function ClientRoot({response}) {
228 return use(response);
229 }
230
231 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
232 replayConsoleLogs: true,
233 debugChannel: {
234 readable: debugReadableStream,
235 // Explicitly not defining a writable side here. Its presence was
236 // previously used as a condition to wait for referenced debug chunks.
237 },
238 });
239
240 const container = document.createElement('div');
241 const root = ReactDOMClient.createRoot(container);
242
243 await act(() => {
244 root.render(<ClientRoot response={response} />);
245 });
246
247 if (__DEV__) {
248 expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
249 }
250
251 expect(container.innerHTML).toBe('<p>Hi</p>');
252 });
253 });