@samitouri / QOS-React-2 / commits / b526a0a419

[Flight][Fizz] schedule work async (#29551)

While most builds of Flight and Fizz schedule work in new tasks some do execute work synchronously. While this is necessary for legacy APIs like renderToString for modern APIs there really isn't a great reason to do this synchronously. We could schedule works as microtasks but we actually want to yield so the runtime can run events and other things that will unblock additional work before starting the next work loop. This change updates all non-legacy uses to be async using the best availalble macrotask scheduler. Browser now uses postMessage Bun uses setTimeout because while it also supports setImmediate the scheduling is not as eager as the same API in node the FB build also uses setTimeout This change required a number of changes to tests which were utilizing the sync nature of work in the Browser builds to avoid having to manage timers and tasks. I added a patch to install MessageChannel which is required by the browser builds and made this patched version integrate with the Scheduler mock. This way we can effectively use `act` to flush flight and fizz work similar to how we do this on the client.

Josh Story committed Jun 6, 2024 at 10:07 UTC b526a0a419029eea31f4d967951b6feca123012d
22 files changed +1419 -837
packages/react-dom/src/__tests__/ReactClassComponentPropResolutionFizz-test.js
+22 -4
@@ -10,6 +10,7 @@
10 'use strict';
11
12 import {insertNodesAndExecuteScripts} from '../test-utils/FizzTestUtils';
13 +import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
14
15 // Polyfills for test environment
16 global.ReadableStream =
@@ -21,12 +22,16 @@ let ReactDOMServer;
22 let Scheduler;
23 let assertLog;
24 let container;
25 +let act;
26
27 describe('ReactClassComponentPropResolutionFizz', () => {
28 beforeEach(() => {
29 jest.resetModules();
28 - React = require('react');
30 Scheduler = require('scheduler');
31 + patchMessageChannel(Scheduler);
32 + act = require('internal-test-utils').act;
33 +
34 + React = require('react');
35 ReactDOMServer = require('react-dom/server.browser');
36 assertLog = require('internal-test-utils').assertLog;
37 container = document.createElement('div');
@@ -37,6 +42,17 @@ describe('ReactClassComponentPropResolutionFizz', () => {
42 document.body.removeChild(container);
43 });
44
45 + async function serverAct(callback) {
46 + let maybePromise;
47 + await act(() => {
48 + maybePromise = callback();
49 + if (maybePromise && typeof maybePromise.catch === 'function') {
50 + maybePromise.catch(() => {});
51 + }
52 + });
53 + return maybePromise;
54 + }
55 +
56 async function readIntoContainer(stream) {
57 const reader = stream.getReader();
58 let result = '';
@@ -57,7 +73,7 @@ describe('ReactClassComponentPropResolutionFizz', () => {
73 return text;
74 }
75
60 - test('resolves ref and default props before calling lifecycle methods', async () => {
76 + it('resolves ref and default props before calling lifecycle methods', async () => {
77 function getPropKeys(props) {
78 return Object.keys(props).join(', ');
79 }
@@ -80,11 +96,13 @@ describe('ReactClassComponentPropResolutionFizz', () => {
96 };
97
98 // `ref` should never appear as a prop. `default` always should.
99 +
100 const ref = React.createRef();
84 - const stream = await ReactDOMServer.renderToReadableStream(
85 - <Component text="Yay" ref={ref} />,
101 + const stream = await serverAct(() =>
102 + ReactDOMServer.renderToReadableStream(<Component text="Yay" ref={ref} />),
103 );
104 await readIntoContainer(stream);
105 +
106 assertLog([
107 'constructor: text, default',
108 'componentWillMount: text, default',
packages/react-dom/src/__tests__/ReactDOMFizzDeferredValue-test.js
+24 -6
@@ -13,6 +13,7 @@ import {
13 insertNodesAndExecuteScripts,
14 getVisibleChildren,
15 } from '../test-utils/FizzTestUtils';
16 +import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
17
18 // Polyfills for test environment
19 global.ReadableStream =
@@ -33,13 +34,14 @@ let Suspense;
34 describe('ReactDOMFizzForm', () => {
35 beforeEach(() => {
36 jest.resetModules();
36 - React = require('react');
37 Scheduler = require('scheduler');
38 + patchMessageChannel(Scheduler);
39 + act = require('internal-test-utils').act;
40 + React = require('react');
41 ReactDOMServer = require('react-dom/server.browser');
42 ReactDOMClient = require('react-dom/client');
43 useDeferredValue = React.useDeferredValue;
44 Suspense = React.Suspense;
42 - act = require('internal-test-utils').act;
45 assertLog = require('internal-test-utils').assertLog;
46 waitForPaint = require('internal-test-utils').waitForPaint;
47 container = document.createElement('div');
@@ -50,6 +52,17 @@ describe('ReactDOMFizzForm', () => {
52 document.body.removeChild(container);
53 });
54
55 + async function serverAct(callback) {
56 + let maybePromise;
57 + await act(() => {
58 + maybePromise = callback();
59 + if (maybePromise && typeof maybePromise.catch === 'function') {
60 + maybePromise.catch(() => {});
61 + }
62 + });
63 + return maybePromise;
64 + }
65 +
66 async function readIntoContainer(stream) {
67 const reader = stream.getReader();
68 let result = '';
@@ -76,7 +89,9 @@ describe('ReactDOMFizzForm', () => {
89 return useDeferredValue('Final', 'Initial');
90 }
91
79 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
92 + const stream = await serverAct(() =>
93 + ReactDOMServer.renderToReadableStream(<App />),
94 + );
95 await readIntoContainer(stream);
96 expect(container.textContent).toEqual('Initial');
97
@@ -107,7 +122,9 @@ describe('ReactDOMFizzForm', () => {
122 );
123 }
124
110 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
125 + const stream = await serverAct(() =>
126 + ReactDOMServer.renderToReadableStream(<App />),
127 + );
128 await readIntoContainer(stream);
129 expect(container.textContent).toEqual('Loading...');
130
@@ -153,8 +170,9 @@ describe('ReactDOMFizzForm', () => {
170
171 const cRef = React.createRef();
172
156 - // The server renders using the "initial" value for B.
157 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
173 + const stream = await serverAct(() =>
174 + ReactDOMServer.renderToReadableStream(<App />),
175 + );
176 await readIntoContainer(stream);
177 assertLog(['A', 'B [Initial]', 'C']);
178 expect(getVisibleChildren(container)).toEqual(
packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js
+57 -15
@@ -10,6 +10,7 @@
10 'use strict';
11
12 import {insertNodesAndExecuteScripts} from '../test-utils/FizzTestUtils';
13 +import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
14
15 // Polyfills for test environment
16 global.ReadableStream =
@@ -24,10 +25,13 @@ let ReactDOMClient;
25 let useFormStatus;
26 let useOptimistic;
27 let useActionState;
28 +let Scheduler;
29
30 describe('ReactDOMFizzForm', () => {
31 beforeEach(() => {
32 jest.resetModules();
33 + Scheduler = require('scheduler');
34 + patchMessageChannel(Scheduler);
35 React = require('react');
36 ReactDOMServer = require('react-dom/server.browser');
37 ReactDOMClient = require('react-dom/client');
@@ -48,6 +52,14 @@ describe('ReactDOMFizzForm', () => {
52 document.body.removeChild(container);
53 });
54
55 + async function serverAct(callback) {
56 + let maybePromise;
57 + await act(() => {
58 + maybePromise = callback();
59 + });
60 + return maybePromise;
61 + }
62 +
63 function submit(submitter) {
64 const form = submitter.form || submitter;
65 if (!submitter.form) {
@@ -96,7 +108,9 @@ describe('ReactDOMFizzForm', () => {
108 );
109 }
110
99 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
111 + const stream = await serverAct(() =>
112 + ReactDOMServer.renderToReadableStream(<App />),
113 + );
114 await readIntoContainer(stream);
115 await act(async () => {
116 ReactDOMClient.hydrateRoot(container, <App />);
@@ -143,7 +157,9 @@ describe('ReactDOMFizzForm', () => {
157 );
158 }
159
146 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
160 + const stream = await serverAct(() =>
161 + ReactDOMServer.renderToReadableStream(<App />),
162 + );
163 await readIntoContainer(stream);
164 await act(async () => {
165 ReactDOMClient.hydrateRoot(container, <App />);
@@ -175,7 +191,9 @@ describe('ReactDOMFizzForm', () => {
191 );
192 }
193
178 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
194 + const stream = await serverAct(() =>
195 + ReactDOMServer.renderToReadableStream(<App />),
196 + );
197 await readIntoContainer(stream);
198 await expect(async () => {
199 await act(async () => {
@@ -197,7 +215,9 @@ describe('ReactDOMFizzForm', () => {
215 );
216 }
217
200 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
218 + const stream = await serverAct(() =>
219 + ReactDOMServer.renderToReadableStream(<App />),
220 + );
221 await readIntoContainer(stream);
222 // This should ideally warn because only the client provides a function that doesn't line up.
223 await act(async () => {
@@ -231,7 +251,9 @@ describe('ReactDOMFizzForm', () => {
251 );
252 }
253
234 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
254 + const stream = await serverAct(() =>
255 + ReactDOMServer.renderToReadableStream(<App />),
256 + );
257 await readIntoContainer(stream);
258 let root;
259 await act(async () => {
@@ -278,7 +300,9 @@ describe('ReactDOMFizzForm', () => {
300 );
301 }
302
281 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
303 + const stream = await serverAct(() =>
304 + ReactDOMServer.renderToReadableStream(<App />),
305 + );
306 await readIntoContainer(stream);
307 let root;
308 await act(async () => {
@@ -334,7 +358,9 @@ describe('ReactDOMFizzForm', () => {
358 // Specifying the extra form fields are a DEV error, but we expect it
359 // to eventually still be patched up after an update.
360 await expect(async () => {
337 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
361 + const stream = await serverAct(() =>
362 + ReactDOMServer.renderToReadableStream(<App />),
363 + );
364 await readIntoContainer(stream);
365 }).toErrorDev([
366 'Cannot specify a encType or method for a form that specifies a function as the action.',
@@ -379,7 +405,9 @@ describe('ReactDOMFizzForm', () => {
405 return 'Pending: ' + pending;
406 }
407
382 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
408 + const stream = await serverAct(() =>
409 + ReactDOMServer.renderToReadableStream(<App />),
410 + );
411 await readIntoContainer(stream);
412 expect(container.textContent).toBe('Pending: false');
413
@@ -400,7 +428,9 @@ describe('ReactDOMFizzForm', () => {
428 );
429 }
430
403 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
431 + const stream = await serverAct(() =>
432 + ReactDOMServer.renderToReadableStream(<App />),
433 + );
434 await readIntoContainer(stream);
435
436 // Dispatch an event before hydration
@@ -441,7 +471,9 @@ describe('ReactDOMFizzForm', () => {
471 );
472 }
473
444 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
474 + const stream = await serverAct(() =>
475 + ReactDOMServer.renderToReadableStream(<App />),
476 + );
477 await readIntoContainer(stream);
478
479 submit(container.getElementsByTagName('input')[1]);
@@ -463,7 +495,9 @@ describe('ReactDOMFizzForm', () => {
495 return optimisticState;
496 }
497
466 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
498 + const stream = await serverAct(() =>
499 + ReactDOMServer.renderToReadableStream(<App />),
500 + );
501 await readIntoContainer(stream);
502 expect(container.textContent).toBe('hi');
503
@@ -484,7 +518,9 @@ describe('ReactDOMFizzForm', () => {
518 return state;
519 }
520
487 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
521 + const stream = await serverAct(() =>
522 + ReactDOMServer.renderToReadableStream(<App />),
523 + );
524 await readIntoContainer(stream);
525 expect(container.textContent).toBe('0');
526
@@ -521,7 +557,9 @@ describe('ReactDOMFizzForm', () => {
557 );
558 }
559
524 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
560 + const stream = await serverAct(() =>
561 + ReactDOMServer.renderToReadableStream(<App />),
562 + );
563 await readIntoContainer(stream);
564
565 const form = container.firstChild;
@@ -581,7 +619,9 @@ describe('ReactDOMFizzForm', () => {
619 );
620 }
621
584 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
622 + const stream = await serverAct(() =>
623 + ReactDOMServer.renderToReadableStream(<App />),
624 + );
625 await readIntoContainer(stream);
626
627 const input = container.getElementsByTagName('input')[1];
@@ -651,7 +691,9 @@ describe('ReactDOMFizzForm', () => {
691 );
692 }
693
654 - const stream = await ReactDOMServer.renderToReadableStream(<App />);
694 + const stream = await serverAct(() =>
695 + ReactDOMServer.renderToReadableStream(<App />),
696 + );
697 await readIntoContainer(stream);
698
699 const barField = container.querySelector('[name=bar]');
packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
+191 -148
@@ -9,6 +9,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,15 +19,33 @@ global.TextEncoder = require('util').TextEncoder;
19 let React;
20 let ReactDOMFizzServer;
21 let Suspense;
22 +let Scheduler;
23 +let act;
24
25 describe('ReactDOMFizzServerBrowser', () => {
26 beforeEach(() => {
27 jest.resetModules();
28 +
29 + Scheduler = require('scheduler');
30 + patchMessageChannel(Scheduler);
31 + act = require('internal-test-utils').act;
32 +
33 React = require('react');
34 ReactDOMFizzServer = require('react-dom/server.browser');
35 Suspense = React.Suspense;
36 });
37
38 + async function serverAct(callback) {
39 + let maybePromise;
40 + await act(() => {
41 + maybePromise = callback();
42 + if (maybePromise && typeof maybePromise.catch === 'function') {
43 + maybePromise.catch(() => {});
44 + }
45 + });
46 + return maybePromise;
47 + }
48 +
49 const theError = new Error('This is an error');
50 function Throw() {
51 throw theError;
@@ -48,18 +68,20 @@ describe('ReactDOMFizzServerBrowser', () => {
68 }
69
70 it('should call renderToReadableStream', async () => {
51 - const stream = await ReactDOMFizzServer.renderToReadableStream(
52 - <div>hello world</div>,
71 + const stream = await serverAct(() =>
72 + ReactDOMFizzServer.renderToReadableStream(<div>hello world</div>),
73 );
74 const result = await readResult(stream);
75 expect(result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
76 });
77
78 it('should emit DOCTYPE at the root of the document', async () => {
59 - const stream = await ReactDOMFizzServer.renderToReadableStream(
60 - <html>
61 - <body>hello world</body>
62 - </html>,
79 + const stream = await serverAct(() =>
80 + ReactDOMFizzServer.renderToReadableStream(
81 + <html>
82 + <body>hello world</body>
83 + </html>,
84 + ),
85 );
86 const result = await readResult(stream);
87 expect(result).toMatchInlineSnapshot(
@@ -68,13 +90,12 @@ describe('ReactDOMFizzServerBrowser', () => {
90 });
91
92 it('should emit bootstrap script src at the end', async () => {
71 - const stream = await ReactDOMFizzServer.renderToReadableStream(
72 - <div>hello world</div>,
73 - {
93 + const stream = await serverAct(() =>
94 + ReactDOMFizzServer.renderToReadableStream(<div>hello world</div>, {
95 bootstrapScriptContent: 'INIT();',
96 bootstrapScripts: ['init.js'],
97 bootstrapModules: ['init.mjs'],
77 - },
98 + }),
99 );
100 const result = await readResult(stream);
101 expect(result).toMatchInlineSnapshot(
@@ -93,23 +114,22 @@ describe('ReactDOMFizzServerBrowser', () => {
114 return 'Done';
115 }
116 let isComplete = false;
96 - const stream = await ReactDOMFizzServer.renderToReadableStream(
97 - <div>
98 - <Suspense fallback="Loading">
99 - <Wait />
100 - </Suspense>
101 - </div>,
117 + const stream = await serverAct(() =>
118 + ReactDOMFizzServer.renderToReadableStream(
119 + <div>
120 + <Suspense fallback="Loading">
121 + <Wait />
122 + </Suspense>
123 + </div>,
124 + ),
125 );
126
127 stream.allReady.then(() => (isComplete = true));
128
106 - await jest.runAllTimers();
129 expect(isComplete).toBe(false);
130 // Resolve the loading.
131 hasLoaded = true;
110 - await resolve();
111 -
112 - await jest.runAllTimers();
132 + await serverAct(() => resolve());
133
134 expect(isComplete).toBe(true);
135
@@ -123,15 +143,17 @@ describe('ReactDOMFizzServerBrowser', () => {
143 const reportedErrors = [];
144 let caughtError = null;
145 try {
126 - await ReactDOMFizzServer.renderToReadableStream(
127 - <div>
128 - <Throw />
129 - </div>,
130 - {
131 - onError(x) {
132 - reportedErrors.push(x);
146 + await serverAct(() =>
147 + ReactDOMFizzServer.renderToReadableStream(
148 + <div>
149 + <Throw />
150 + </div>,
151 + {
152 + onError(x) {
153 + reportedErrors.push(x);
154 + },
155 },
134 - },
156 + ),
157 );
158 } catch (error) {
159 caughtError = error;
@@ -144,17 +166,19 @@ describe('ReactDOMFizzServerBrowser', () => {
166 const reportedErrors = [];
167 let caughtError = null;
168 try {
147 - await ReactDOMFizzServer.renderToReadableStream(
148 - <div>
149 - <Suspense fallback={<Throw />}>
150 - <InfiniteSuspend />
151 - </Suspense>
152 - </div>,
153 - {
154 - onError(x) {
155 - reportedErrors.push(x);
169 + await serverAct(() =>
170 + ReactDOMFizzServer.renderToReadableStream(
171 + <div>
172 + <Suspense fallback={<Throw />}>
173 + <InfiniteSuspend />
174 + </Suspense>
175 + </div>,
176 + {
177 + onError(x) {
178 + reportedErrors.push(x);
179 + },
180 },
157 - },
181 + ),
182 );
183 } catch (error) {
184 caughtError = error;
@@ -165,17 +189,19 @@ describe('ReactDOMFizzServerBrowser', () => {
189
190 it('should not error the stream when an error is thrown inside suspense boundary', async () => {
191 const reportedErrors = [];
168 - const stream = await ReactDOMFizzServer.renderToReadableStream(
169 - <div>
170 - <Suspense fallback={<div>Loading</div>}>
171 - <Throw />
172 - </Suspense>
173 - </div>,
174 - {
175 - onError(x) {
176 - reportedErrors.push(x);
192 + const stream = await serverAct(() =>
193 + ReactDOMFizzServer.renderToReadableStream(
194 + <div>
195 + <Suspense fallback={<div>Loading</div>}>
196 + <Throw />
197 + </Suspense>
198 + </div>,
199 + {
200 + onError(x) {
201 + reportedErrors.push(x);
202 + },
203 },
178 - },
204 + ),
205 );
206
207 const result = await readResult(stream);
@@ -186,18 +212,20 @@ describe('ReactDOMFizzServerBrowser', () => {
212 it('should be able to complete by aborting even if the promise never resolves', async () => {
213 const errors = [];
214 const controller = new AbortController();
189 - const stream = await ReactDOMFizzServer.renderToReadableStream(
190 - <div>
191 - <Suspense fallback={<div>Loading</div>}>
192 - <InfiniteSuspend />
193 - </Suspense>
194 - </div>,
195 - {
196 - signal: controller.signal,
197 - onError(x) {
198 - errors.push(x.message);
215 + const stream = await serverAct(() =>
216 + ReactDOMFizzServer.renderToReadableStream(
217 + <div>
218 + <Suspense fallback={<div>Loading</div>}>
219 + <InfiniteSuspend />
220 + </Suspense>
221 + </div>,
222 + {
223 + signal: controller.signal,
224 + onError(x) {
225 + errors.push(x.message);
226 + },
227 },
200 - },
228 + ),
229 );
230
231 controller.abort();
@@ -211,20 +239,20 @@ describe('ReactDOMFizzServerBrowser', () => {
239 it('should reject if aborting before the shell is complete', async () => {
240 const errors = [];
241 const controller = new AbortController();
214 - const promise = ReactDOMFizzServer.renderToReadableStream(
215 - <div>
216 - <InfiniteSuspend />
217 - </div>,
218 - {
219 - signal: controller.signal,
220 - onError(x) {
221 - errors.push(x.message);
242 + const promise = serverAct(() =>
243 + ReactDOMFizzServer.renderToReadableStream(
244 + <div>
245 + <InfiniteSuspend />
246 + </div>,
247 + {
248 + signal: controller.signal,
249 + onError(x) {
250 + errors.push(x.message);
251 + },
252 },
223 - },
253 + ),
254 );
255
226 - await jest.runAllTimers();
227 -
256 const theReason = new Error('aborted for reasons');
257 controller.abort(theReason);
258
@@ -249,16 +277,18 @@ describe('ReactDOMFizzServerBrowser', () => {
277 </Suspense>
278 );
279 }
252 - const streamPromise = ReactDOMFizzServer.renderToReadableStream(
253 - <div>
254 - <App />
255 - </div>,
256 - {
257 - signal: controller.signal,
258 - onError(x) {
259 - errors.push(x.message);
280 + const streamPromise = serverAct(() =>
281 + ReactDOMFizzServer.renderToReadableStream(
282 + <div>
283 + <App />
284 + </div>,
285 + {
286 + signal: controller.signal,
287 + onError(x) {
288 + errors.push(x.message);
289 + },
290 },
261 - },
291 + ),
292 );
293
294 let caughtError = null;
@@ -277,18 +307,20 @@ describe('ReactDOMFizzServerBrowser', () => {
307 const theReason = new Error('aborted for reasons');
308 controller.abort(theReason);
309
280 - const promise = ReactDOMFizzServer.renderToReadableStream(
281 - <div>
282 - <Suspense fallback={<div>Loading</div>}>
283 - <InfiniteSuspend />
284 - </Suspense>
285 - </div>,
286 - {
287 - signal: controller.signal,
288 - onError(x) {
289 - errors.push(x.message);
310 + const promise = serverAct(() =>
311 + ReactDOMFizzServer.renderToReadableStream(
312 + <div>
313 + <Suspense fallback={<div>Loading</div>}>
314 + <InfiniteSuspend />
315 + </Suspense>
316 + </div>,
317 + {
318 + signal: controller.signal,
319 + onError(x) {
320 + errors.push(x.message);
321 + },
322 },
291 - },
323 + ),
324 );
325
326 // Technically we could still continue rendering the shell but currently the
@@ -317,17 +349,19 @@ describe('ReactDOMFizzServerBrowser', () => {
349 return 'Done';
350 }
351 const errors = [];
320 - const stream = await ReactDOMFizzServer.renderToReadableStream(
321 - <div>
322 - <Suspense fallback={<div>Loading</div>}>
323 - <Wait />
324 - </Suspense>
325 - </div>,
326 - {
327 - onError(x) {
328 - errors.push(x.message);
352 + const stream = await serverAct(() =>
353 + ReactDOMFizzServer.renderToReadableStream(
354 + <div>
355 + <Suspense fallback={<div>Loading</div>}>
356 + <Wait />
357 + </Suspense>
358 + </div>,
359 + {
360 + onError(x) {
361 + errors.push(x.message);
362 + },
363 },
330 - },
364 + ),
365 );
366
367 stream.allReady.then(() => (isComplete = true));
@@ -344,9 +378,7 @@ describe('ReactDOMFizzServerBrowser', () => {
378 ]);
379
380 hasLoaded = true;
347 - resolve();
348 -
349 - await jest.runAllTimers();
381 + await serverAct(() => resolve());
382
383 expect(rendered).toBe(false);
384 expect(isComplete).toBe(true);
@@ -366,14 +398,16 @@ describe('ReactDOMFizzServerBrowser', () => {
398 // as such for now. I don't think it needs to be maintained if in the future
399 // the view sizes change or become dynamic becasue of the use of byobRequest
400 let stream;
369 - stream = await ReactDOMFizzServer.renderToReadableStream(
370 - <>
371 - <div>
372 - <span>{''}</span>
373 - </div>
374 - <div>{str492}</div>
375 - <div>{str492}</div>
376 - </>,
401 + stream = await serverAct(() =>
402 + ReactDOMFizzServer.renderToReadableStream(
403 + <>
404 + <div>
405 + <span>{''}</span>
406 + </div>
407 + <div>{str492}</div>
408 + <div>{str492}</div>
409 + </>,
410 + ),
411 );
412
413 let result;
@@ -385,10 +419,12 @@ describe('ReactDOMFizzServerBrowser', () => {
419 // this size 2049 was chosen to be a couple base 2 orders larger than the current view
420 // size. if the size changes in the future hopefully this will still exercise
421 // a chunk that is too large for the view size.
388 - stream = await ReactDOMFizzServer.renderToReadableStream(
389 - <>
390 - <div>{str2049}</div>
391 - </>,
422 + stream = await serverAct(() =>
423 + ReactDOMFizzServer.renderToReadableStream(
424 + <>
425 + <div>{str2049}</div>
426 + </>,
427 + ),
428 );
429
430 result = await readResult(stream);
@@ -419,13 +455,15 @@ describe('ReactDOMFizzServerBrowser', () => {
455
456 const errors = [];
457 const controller = new AbortController();
422 - await ReactDOMFizzServer.renderToReadableStream(<App />, {
423 - signal: controller.signal,
424 - onError(x) {
425 - errors.push(x);
426 - return 'a digest';
427 - },
428 - });
458 + await serverAct(() =>
459 + ReactDOMFizzServer.renderToReadableStream(<App />, {
460 + signal: controller.signal,
461 + onError(x) {
462 + errors.push(x);
463 + return 'a digest';
464 + },
465 + }),
466 + );
467
468 controller.abort('foobar');
469
@@ -456,13 +494,15 @@ describe('ReactDOMFizzServerBrowser', () => {
494
495 const errors = [];
496 const controller = new AbortController();
459 - await ReactDOMFizzServer.renderToReadableStream(<App />, {
460 - signal: controller.signal,
461 - onError(x) {
462 - errors.push(x.message);
463 - return 'a digest';
464 - },
465 - });
497 + await serverAct(() =>
498 + ReactDOMFizzServer.renderToReadableStream(<App />, {
499 + signal: controller.signal,
500 + onError(x) {
501 + errors.push(x.message);
502 + return 'a digest';
503 + },
504 + }),
505 + );
506
507 controller.abort(new Error('uh oh'));
508
@@ -471,13 +511,15 @@ describe('ReactDOMFizzServerBrowser', () => {
511
512 // https://github.com/facebook/react/pull/25534/files - fix transposed escape functions
513 it('should encode title properly', async () => {
474 - const stream = await ReactDOMFizzServer.renderToReadableStream(
475 - <html>
476 - <head>
477 - <title>foo</title>
478 - </head>
479 - <body>bar</body>
480 - </html>,
514 + const stream = await serverAct(() =>
515 + ReactDOMFizzServer.renderToReadableStream(
516 + <html>
517 + <head>
518 + <title>foo</title>
519 + </head>
520 + <body>bar</body>
521 + </html>,
522 + ),
523 );
524
525 const result = await readResult(stream);
@@ -488,14 +530,13 @@ describe('ReactDOMFizzServerBrowser', () => {
530
531 it('should support nonce attribute for bootstrap scripts', async () => {
532 const nonce = 'R4nd0m';
491 - const stream = await ReactDOMFizzServer.renderToReadableStream(
492 - <div>hello world</div>,
493 - {
533 + const stream = await serverAct(() =>
534 + ReactDOMFizzServer.renderToReadableStream(<div>hello world</div>, {
535 nonce,
536 bootstrapScriptContent: 'INIT();',
537 bootstrapScripts: ['init.js'],
538 bootstrapModules: ['init.mjs'],
498 - },
539 + }),
540 );
541 const result = await readResult(stream);
542 expect(result).toMatchInlineSnapshot(
@@ -523,14 +564,16 @@ describe('ReactDOMFizzServerBrowser', () => {
564
565 let caughtError = null;
566 try {
526 - await ReactDOMFizzServer.renderToReadableStream(<App />, {
527 - onError(error) {
528 - errors.push(error.message);
529 - },
530 - onPostpone(reason) {
531 - postponed.push(reason);
532 - },
533 - });
567 + await serverAct(() =>
568 + ReactDOMFizzServer.renderToReadableStream(<App />, {
569 + onError(error) {
570 + errors.push(error.message);
571 + },
572 + onPostpone(reason) {
573 + postponed.push(reason);
574 + },
575 + }),
576 + );
577 } catch (error) {
578 caughtError = error;
579 }
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+315 -198
@@ -9,6 +9,8 @@
9
10 'use strict';
11
12 +import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
13 +
14 import {
15 getVisibleChildren,
16 insertNodesAndExecuteScripts,
@@ -26,10 +28,17 @@ let ReactDOMFizzServer;
28 let ReactDOMFizzStatic;
29 let Suspense;
30 let container;
31 +let Scheduler;
32 +let act;
33
34 describe('ReactDOMFizzStaticBrowser', () => {
35 beforeEach(() => {
36 jest.resetModules();
37 +
38 + Scheduler = require('scheduler');
39 + patchMessageChannel(Scheduler);
40 + act = require('internal-test-utils').act;
41 +
42 React = require('react');
43 ReactDOM = require('react-dom');
44 ReactDOMFizzServer = require('react-dom/server.browser');
@@ -45,6 +54,17 @@ describe('ReactDOMFizzStaticBrowser', () => {
54 document.body.removeChild(container);
55 });
56
57 + async function serverAct(callback) {
58 + let maybePromise;
59 + await act(() => {
60 + maybePromise = callback();
61 + if (maybePromise && typeof maybePromise.catch === 'function') {
62 + maybePromise.catch(() => {});
63 + }
64 + });
65 + return maybePromise;
66 + }
67 +
68 const theError = new Error('This is an error');
69 function Throw() {
70 throw theError;
@@ -113,17 +133,21 @@ describe('ReactDOMFizzStaticBrowser', () => {
133
134 // @gate experimental
135 it('should call prerender', async () => {
116 - const result = await ReactDOMFizzStatic.prerender(<div>hello world</div>);
136 + const result = await serverAct(() =>
137 + ReactDOMFizzStatic.prerender(<div>hello world</div>),
138 + );
139 const prelude = await readContent(result.prelude);
140 expect(prelude).toMatchInlineSnapshot(`"<div>hello world</div>"`);
141 });
142
143 // @gate experimental
144 it('should emit DOCTYPE at the root of the document', async () => {
123 - const result = await ReactDOMFizzStatic.prerender(
124 - <html>
125 - <body>hello world</body>
126 - </html>,
145 + const result = await serverAct(() =>
146 + ReactDOMFizzStatic.prerender(
147 + <html>
148 + <body>hello world</body>
149 + </html>,
150 + ),
151 );
152 const prelude = await readContent(result.prelude);
153 expect(prelude).toMatchInlineSnapshot(
@@ -133,11 +157,13 @@ describe('ReactDOMFizzStaticBrowser', () => {
157
158 // @gate experimental
159 it('should emit bootstrap script src at the end', async () => {
136 - const result = await ReactDOMFizzStatic.prerender(<div>hello world</div>, {
137 - bootstrapScriptContent: 'INIT();',
138 - bootstrapScripts: ['init.js'],
139 - bootstrapModules: ['init.mjs'],
140 - });
160 + const result = await serverAct(() =>
161 + ReactDOMFizzStatic.prerender(<div>hello world</div>, {
162 + bootstrapScriptContent: 'INIT();',
163 + bootstrapScripts: ['init.js'],
164 + bootstrapModules: ['init.mjs'],
165 + }),
166 + );
167 const prelude = await readContent(result.prelude);
168 expect(prelude).toMatchInlineSnapshot(
169 `"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
@@ -155,12 +181,14 @@ describe('ReactDOMFizzStaticBrowser', () => {
181 }
182 return 'Done';
183 }
158 - const resultPromise = ReactDOMFizzStatic.prerender(
159 - <div>
160 - <Suspense fallback="Loading">
161 - <Wait />
162 - </Suspense>
163 - </div>,
184 + const resultPromise = serverAct(() =>
185 + ReactDOMFizzStatic.prerender(
186 + <div>
187 + <Suspense fallback="Loading">
188 + <Wait />
189 + </Suspense>
190 + </div>,
191 + ),
192 );
193
194 await jest.runAllTimers();
@@ -171,9 +199,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
199
200 const result = await resultPromise;
201 const prelude = await readContent(result.prelude);
174 - expect(prelude).toMatchInlineSnapshot(
175 - `"<div><!--$-->Done<!-- --><!--/$--></div>"`,
176 - );
202 + expect(prelude).toMatchInlineSnapshot(`"<div><!--$-->Done<!--/$--></div>"`);
203 });
204
205 // @gate experimental
@@ -181,15 +207,17 @@ describe('ReactDOMFizzStaticBrowser', () => {
207 const reportedErrors = [];
208 let caughtError = null;
209 try {
184 - await ReactDOMFizzStatic.prerender(
185 - <div>
186 - <Throw />
187 - </div>,
188 - {
189 - onError(x) {
190 - reportedErrors.push(x);
210 + await serverAct(() =>
211 + ReactDOMFizzStatic.prerender(
212 + <div>
213 + <Throw />
214 + </div>,
215 + {
216 + onError(x) {
217 + reportedErrors.push(x);
218 + },
219 },
192 - },
220 + ),
221 );
222 } catch (error) {
223 caughtError = error;
@@ -203,17 +231,19 @@ describe('ReactDOMFizzStaticBrowser', () => {
231 const reportedErrors = [];
232 let caughtError = null;
233 try {
206 - await ReactDOMFizzStatic.prerender(
207 - <div>
208 - <Suspense fallback={<Throw />}>
209 - <InfiniteSuspend />
210 - </Suspense>
211 - </div>,
212 - {
213 - onError(x) {
214 - reportedErrors.push(x);
234 + await serverAct(() =>
235 + ReactDOMFizzStatic.prerender(
236 + <div>
237 + <Suspense fallback={<Throw />}>
238 + <InfiniteSuspend />
239 + </Suspense>
240 + </div>,
241 + {
242 + onError(x) {
243 + reportedErrors.push(x);
244 + },
245 },
216 - },
246 + ),
247 );
248 } catch (error) {
249 caughtError = error;
@@ -225,17 +255,19 @@ describe('ReactDOMFizzStaticBrowser', () => {
255 // @gate experimental
256 it('should not error the stream when an error is thrown inside suspense boundary', async () => {
257 const reportedErrors = [];
228 - const result = await ReactDOMFizzStatic.prerender(
229 - <div>
230 - <Suspense fallback={<div>Loading</div>}>
231 - <Throw />
232 - </Suspense>
233 - </div>,
234 - {
235 - onError(x) {
236 - reportedErrors.push(x);
258 + const result = await serverAct(() =>
259 + ReactDOMFizzStatic.prerender(
260 + <div>
261 + <Suspense fallback={<div>Loading</div>}>
262 + <Throw />
263 + </Suspense>
264 + </div>,
265 + {
266 + onError(x) {
267 + reportedErrors.push(x);
268 + },
269 },
238 - },
270 + ),
271 );
272
273 const prelude = await readContent(result.prelude);
@@ -247,21 +279,22 @@ describe('ReactDOMFizzStaticBrowser', () => {
279 it('should be able to complete by aborting even if the promise never resolves', async () => {
280 const errors = [];
281 const controller = new AbortController();
250 - const resultPromise = ReactDOMFizzStatic.prerender(
251 - <div>
252 - <Suspense fallback={<div>Loading</div>}>
253 - <InfiniteSuspend />
254 - </Suspense>
255 - </div>,
256 - {
257 - signal: controller.signal,
258 - onError(x) {
259 - errors.push(x.message);
282 + let resultPromise;
283 + await serverAct(() => {
284 + resultPromise = ReactDOMFizzStatic.prerender(
285 + <div>
286 + <Suspense fallback={<div>Loading</div>}>
287 + <InfiniteSuspend />
288 + </Suspense>
289 + </div>,
290 + {
291 + signal: controller.signal,
292 + onError(x) {
293 + errors.push(x.message);
294 + },
295 },
261 - },
262 - );
263 -
264 - await jest.runAllTimers();
296 + );
297 + });
298
299 controller.abort();
300
@@ -277,16 +310,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
310 it('should reject if aborting before the shell is complete', async () => {
311 const errors = [];
312 const controller = new AbortController();
280 - const promise = ReactDOMFizzStatic.prerender(
281 - <div>
282 - <InfiniteSuspend />
283 - </div>,
284 - {
285 - signal: controller.signal,
286 - onError(x) {
287 - errors.push(x.message);
313 + const promise = serverAct(() =>
314 + ReactDOMFizzStatic.prerender(
315 + <div>
316 + <InfiniteSuspend />
317 + </div>,
318 + {
319 + signal: controller.signal,
320 + onError(x) {
321 + errors.push(x.message);
322 + },
323 },
289 - },
324 + ),
325 );
326
327 await jest.runAllTimers();
@@ -316,16 +351,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
351 </Suspense>
352 );
353 }
319 - const streamPromise = ReactDOMFizzStatic.prerender(
320 - <div>
321 - <App />
322 - </div>,
323 - {
324 - signal: controller.signal,
325 - onError(x) {
326 - errors.push(x.message);
354 + const streamPromise = serverAct(() =>
355 + ReactDOMFizzStatic.prerender(
356 + <div>
357 + <App />
358 + </div>,
359 + {
360 + signal: controller.signal,
361 + onError(x) {
362 + errors.push(x.message);
363 + },
364 },
328 - },
365 + ),
366 );
367
368 let caughtError = null;
@@ -345,18 +382,20 @@ describe('ReactDOMFizzStaticBrowser', () => {
382 const theReason = new Error('aborted for reasons');
383 controller.abort(theReason);
384
348 - const promise = ReactDOMFizzStatic.prerender(
349 - <div>
350 - <Suspense fallback={<div>Loading</div>}>
351 - <InfiniteSuspend />
352 - </Suspense>
353 - </div>,
354 - {
355 - signal: controller.signal,
356 - onError(x) {
357 - errors.push(x.message);
385 + const promise = serverAct(() =>
386 + ReactDOMFizzStatic.prerender(
387 + <div>
388 + <Suspense fallback={<div>Loading</div>}>
389 + <InfiniteSuspend />
390 + </Suspense>
391 + </div>,
392 + {
393 + signal: controller.signal,
394 + onError(x) {
395 + errors.push(x.message);
396 + },
397 },
359 - },
398 + ),
399 );
400
401 // Technically we could still continue rendering the shell but currently the
@@ -396,12 +435,15 @@ describe('ReactDOMFizzStaticBrowser', () => {
435
436 const errors = [];
437 const controller = new AbortController();
399 - const resultPromise = ReactDOMFizzStatic.prerender(<App />, {
400 - signal: controller.signal,
401 - onError(x) {
402 - errors.push(x);
403 - return 'a digest';
404 - },
438 + let resultPromise;
439 + await serverAct(() => {
440 + resultPromise = ReactDOMFizzStatic.prerender(<App />, {
441 + signal: controller.signal,
442 + onError(x) {
443 + errors.push(x);
444 + return 'a digest';
445 + },
446 + });
447 });
448
449 controller.abort('foobar');
@@ -436,12 +478,15 @@ describe('ReactDOMFizzStaticBrowser', () => {
478
479 const errors = [];
480 const controller = new AbortController();
439 - const resultPromise = ReactDOMFizzStatic.prerender(<App />, {
440 - signal: controller.signal,
441 - onError(x) {
442 - errors.push(x.message);
443 - return 'a digest';
444 - },
481 + let resultPromise;
482 + await serverAct(() => {
483 + resultPromise = ReactDOMFizzStatic.prerender(<App />, {
484 + signal: controller.signal,
485 + onError(x) {
486 + errors.push(x.message);
487 + return 'a digest';
488 + },
489 + });
490 });
491
492 controller.abort(new Error('uh oh'));
@@ -471,14 +516,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
516 );
517 }
518
474 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
519 + const prerendered = await serverAct(() =>
520 + ReactDOMFizzStatic.prerender(<App />),
521 + );
522 expect(prerendered.postponed).not.toBe(null);
523
524 prerendering = false;
525
479 - const resumed = await ReactDOMFizzServer.resume(
480 - <App />,
481 - JSON.parse(JSON.stringify(prerendered.postponed)),
526 + const resumed = await serverAct(() =>
527 + ReactDOMFizzServer.resume(
528 + <App />,
529 + JSON.parse(JSON.stringify(prerendered.postponed)),
530 + ),
531 );
532
533 await readIntoContainer(prerendered.prelude);
@@ -513,14 +562,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
562 );
563 }
564
516 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
565 + const prerendered = await serverAct(() =>
566 + ReactDOMFizzStatic.prerender(<App />),
567 + );
568 expect(prerendered.postponed).not.toBe(null);
569
570 prerendering = false;
571
521 - const resumed = await ReactDOMFizzServer.resume(
522 - <App />,
523 - JSON.parse(JSON.stringify(prerendered.postponed)),
572 + const resumed = await serverAct(() =>
573 + ReactDOMFizzServer.resume(
574 + <App />,
575 + JSON.parse(JSON.stringify(prerendered.postponed)),
576 + ),
577 );
578
579 await readIntoContainer(prerendered.prelude);
@@ -552,14 +605,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
605 );
606 }
607
555 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
608 + const prerendered = await serverAct(() =>
609 + ReactDOMFizzStatic.prerender(<App />),
610 + );
611 expect(prerendered.postponed).not.toBe(null);
612
613 prerendering = false;
614
560 - const resumed = await ReactDOMFizzServer.resume(
561 - <App />,
562 - JSON.parse(JSON.stringify(prerendered.postponed)),
615 + const resumed = await serverAct(() =>
616 + ReactDOMFizzServer.resume(
617 + <App />,
618 + JSON.parse(JSON.stringify(prerendered.postponed)),
619 + ),
620 );
621
622 await readIntoContainer(prerendered.prelude);
@@ -600,14 +657,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
657 );
658 }
659
603 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
660 + const prerendered = await serverAct(() =>
661 + ReactDOMFizzStatic.prerender(<App />),
662 + );
663 expect(prerendered.postponed).not.toBe(null);
664
665 prerendering = false;
666
608 - const resumed = await ReactDOMFizzServer.resume(
609 - <App />,
610 - JSON.parse(JSON.stringify(prerendered.postponed)),
667 + const resumed = await serverAct(() =>
668 + ReactDOMFizzServer.resume(
669 + <App />,
670 + JSON.parse(JSON.stringify(prerendered.postponed)),
671 + ),
672 );
673
674 await readIntoContainer(prerendered.prelude);
@@ -641,14 +702,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
702 );
703 }
704
644 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
705 + const prerendered = await serverAct(() =>
706 + ReactDOMFizzStatic.prerender(<App />),
707 + );
708 expect(prerendered.postponed).not.toBe(null);
709
710 prerendering = false;
711
649 - const resumed = await ReactDOMFizzServer.resume(
650 - <App />,
651 - JSON.parse(JSON.stringify(prerendered.postponed)),
712 + const resumed = await serverAct(() =>
713 + ReactDOMFizzServer.resume(
714 + <App />,
715 + JSON.parse(JSON.stringify(prerendered.postponed)),
716 + ),
717 );
718
719 await readIntoContainer(prerendered.prelude);
@@ -682,14 +747,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
747 );
748 }
749
685 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
750 + const prerendered = await serverAct(() =>
751 + ReactDOMFizzStatic.prerender(<App />),
752 + );
753 expect(prerendered.postponed).not.toBe(null);
754
755 prerendering = false;
756
690 - const content = await ReactDOMFizzServer.resume(
691 - <App />,
692 - JSON.parse(JSON.stringify(prerendered.postponed)),
757 + const content = await serverAct(() =>
758 + ReactDOMFizzServer.resume(
759 + <App />,
760 + JSON.parse(JSON.stringify(prerendered.postponed)),
761 + ),
762 );
763
764 const html = await readContent(concat(prerendered.prelude, content));
@@ -748,9 +817,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
817 {virtual: true},
818 );
819
751 - const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
752 - bootstrapScripts: ['init.js'],
753 - });
820 + const prerendered = await serverAct(() =>
821 + ReactDOMFizzStatic.prerender(<App />, {
822 + bootstrapScripts: ['init.js'],
823 + }),
824 + );
825 expect(prerendered.postponed).not.toBe(null);
826
827 await readIntoContainer(prerendered.prelude);
@@ -779,9 +850,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
850 ]);
851
852 prerendering = false;
782 - const content = await ReactDOMFizzServer.resume(
783 - <App />,
784 - JSON.parse(JSON.stringify(prerendered.postponed)),
853 + const content = await serverAct(() =>
854 + ReactDOMFizzServer.resume(
855 + <App />,
856 + JSON.parse(JSON.stringify(prerendered.postponed)),
857 + ),
858 );
859
860 await readIntoContainer(content);
@@ -860,14 +933,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
933 );
934 }
935
863 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
936 + const prerendered = await serverAct(() =>
937 + ReactDOMFizzStatic.prerender(<App />),
938 + );
939 expect(prerendered.postponed).not.toBe(null);
940
941 prerendering = false;
942
868 - const resumed = await ReactDOMFizzServer.resume(
869 - <App />,
870 - JSON.parse(JSON.stringify(prerendered.postponed)),
943 + const resumed = await serverAct(() =>
944 + ReactDOMFizzServer.resume(
945 + <App />,
946 + JSON.parse(JSON.stringify(prerendered.postponed)),
947 + ),
948 );
949
950 await readIntoContainer(prerendered.prelude);
@@ -911,14 +988,18 @@ describe('ReactDOMFizzStaticBrowser', () => {
988 );
989 }
990
914 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
991 + const prerendered = await serverAct(() =>
992 + ReactDOMFizzStatic.prerender(<App />),
993 + );
994 expect(prerendered.postponed).not.toBe(null);
995
996 prerendering = false;
997
919 - const resumed = await ReactDOMFizzServer.resume(
920 - <App />,
921 - JSON.parse(JSON.stringify(prerendered.postponed)),
998 + const resumed = await serverAct(() =>
999 + ReactDOMFizzServer.resume(
1000 + <App />,
1001 + JSON.parse(JSON.stringify(prerendered.postponed)),
1002 + ),
1003 );
1004
1005 await readIntoContainer(prerendered.prelude);
@@ -957,7 +1038,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
1038 );
1039 }
1040
960 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
1041 + const prerendered = await serverAct(() =>
1042 + ReactDOMFizzStatic.prerender(<App />),
1043 + );
1044 // TODO: This should actually be null because we should've been able to fully
1045 // resolve the render on the server eventually, even though the fallback postponed.
1046 // So we should not need to resume.
@@ -967,9 +1050,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
1050
1051 expect(getVisibleChildren(container)).toEqual(<div>Outer</div>);
1052
970 - const resumed = await ReactDOMFizzServer.resume(
971 - <App />,
972 - JSON.parse(JSON.stringify(prerendered.postponed)),
1053 + const resumed = await serverAct(() =>
1054 + ReactDOMFizzServer.resume(
1055 + <App />,
1056 + JSON.parse(JSON.stringify(prerendered.postponed)),
1057 + ),
1058 );
1059
1060 await readIntoContainer(resumed);
@@ -1020,7 +1105,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
1105 );
1106 }
1107
1023 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
1108 + const prerendered = await serverAct(() =>
1109 + ReactDOMFizzStatic.prerender(<App />),
1110 + );
1111 expect(prerendered.postponed).not.toBe(null);
1112
1113 await readIntoContainer(prerendered.prelude);
@@ -1033,14 +1120,16 @@ describe('ReactDOMFizzStaticBrowser', () => {
1120 prerendering = false;
1121
1122 const errors = [];
1036 - const resumed = await ReactDOMFizzServer.resume(
1037 - <App />,
1038 - JSON.parse(JSON.stringify(prerendered.postponed)),
1039 - {
1040 - onError(x) {
1041 - errors.push(x.message);
1123 + const resumed = await serverAct(() =>
1124 + ReactDOMFizzServer.resume(
1125 + <App />,
1126 + JSON.parse(JSON.stringify(prerendered.postponed)),
1127 + {
1128 + onError(x) {
1129 + errors.push(x.message);
1130 + },
1131 },
1043 - },
1132 + ),
1133 );
1134
1135 expect(errors).toEqual([
@@ -1085,7 +1174,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
1174 );
1175 }
1176
1088 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
1177 + const prerendered = await serverAct(() =>
1178 + ReactDOMFizzStatic.prerender(<App />),
1179 + );
1180 expect(prerendered.postponed).not.toBe(null);
1181
1182 await readIntoContainer(prerendered.prelude);
@@ -1098,15 +1189,17 @@ describe('ReactDOMFizzStaticBrowser', () => {
1189
1190 const errors = [];
1191
1101 - const resumedPromise = ReactDOMFizzServer.resume(
1102 - <App />,
1103 - JSON.parse(JSON.stringify(prerendered.postponed)),
1104 - {
1105 - signal: controller.signal,
1106 - onError(x) {
1107 - errors.push(x);
1192 + const resumedPromise = serverAct(() =>
1193 + ReactDOMFizzServer.resume(
1194 + <App />,
1195 + JSON.parse(JSON.stringify(prerendered.postponed)),
1196 + {
1197 + signal: controller.signal,
1198 + onError(x) {
1199 + errors.push(x);
1200 + },
1201 },
1109 - },
1202 + ),
1203 );
1204
1205 controller.abort('abort');
@@ -1160,16 +1253,20 @@ describe('ReactDOMFizzStaticBrowser', () => {
1253 );
1254 }
1255
1163 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
1256 + const prerendered = await serverAct(() =>
1257 + ReactDOMFizzStatic.prerender(<App />),
1258 + );
1259 expect(prerendered.postponed).not.toBe(null);
1260
1261 await readIntoContainer(prerendered.prelude);
1262
1263 prerendering = false;
1264
1170 - const resumedPromise = ReactDOMFizzServer.resume(
1171 - <App />,
1172 - JSON.parse(JSON.stringify(prerendered.postponed)),
1265 + const resumedPromise = serverAct(() =>
1266 + ReactDOMFizzServer.resume(
1267 + <App />,
1268 + JSON.parse(JSON.stringify(prerendered.postponed)),
1269 + ),
1270 );
1271
1272 await jest.runAllTimers();
@@ -1204,16 +1301,20 @@ describe('ReactDOMFizzStaticBrowser', () => {
1301 );
1302 }
1303
1207 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
1304 + const prerendered = await serverAct(() =>
1305 + ReactDOMFizzStatic.prerender(<App />),
1306 + );
1307 expect(prerendered.postponed).not.toBe(null);
1308
1309 prerendering = false;
1310
1311 expect(await readContent(prerendered.prelude)).toBe('');
1312
1214 - const content = await ReactDOMFizzServer.resume(
1215 - <App />,
1216 - JSON.parse(JSON.stringify(prerendered.postponed)),
1313 + const content = await serverAct(() =>
1314 + ReactDOMFizzServer.resume(
1315 + <App />,
1316 + JSON.parse(JSON.stringify(prerendered.postponed)),
1317 + ),
1318 );
1319
1320 expect(await readContent(content)).toBe(
@@ -1246,16 +1347,20 @@ describe('ReactDOMFizzStaticBrowser', () => {
1347 );
1348 }
1349
1249 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
1350 + const prerendered = await serverAct(() =>
1351 + ReactDOMFizzStatic.prerender(<App />),
1352 + );
1353 expect(prerendered.postponed).not.toBe(null);
1354
1355 prerendering = false;
1356
1357 expect(await readContent(prerendered.prelude)).toBe('');
1358
1256 - const content = await ReactDOMFizzServer.resume(
1257 - <App />,
1258 - JSON.parse(JSON.stringify(prerendered.postponed)),
1359 + const content = await serverAct(() =>
1360 + ReactDOMFizzServer.resume(
1361 + <App />,
1362 + JSON.parse(JSON.stringify(prerendered.postponed)),
1363 + ),
1364 );
1365
1366 expect(await readContent(content)).toBe(
@@ -1293,16 +1398,20 @@ describe('ReactDOMFizzStaticBrowser', () => {
1398 );
1399 }
1400
1296 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
1401 + const prerendered = await serverAct(() =>
1402 + ReactDOMFizzStatic.prerender(<App />),
1403 + );
1404 expect(prerendered.postponed).not.toBe(null);
1405
1406 prerendering = false;
1407
1408 expect(await readContent(prerendered.prelude)).toBe('');
1409
1303 - const content = await ReactDOMFizzServer.resume(
1304 - <App />,
1305 - JSON.parse(JSON.stringify(prerendered.postponed)),
1410 + const content = await serverAct(() =>
1411 + ReactDOMFizzServer.resume(
1412 + <App />,
1413 + JSON.parse(JSON.stringify(prerendered.postponed)),
1414 + ),
1415 );
1416
1417 expect(await readContent(content)).toBe(
@@ -1356,9 +1465,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
1465 );
1466 }
1467
1359 - const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
1360 - onHeaders,
1361 - });
1468 + const prerendered = await serverAct(() =>
1469 + ReactDOMFizzStatic.prerender(<App />, {
1470 + onHeaders,
1471 + }),
1472 + );
1473 expect(prerendered.postponed).not.toBe(null);
1474
1475 prerendering = false;
@@ -1375,9 +1486,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
1486 }),
1487 );
1488
1378 - const content = await ReactDOMFizzServer.resume(
1379 - <App />,
1380 - JSON.parse(JSON.stringify(prerendered.postponed)),
1489 + const content = await serverAct(() =>
1490 + ReactDOMFizzServer.resume(
1491 + <App />,
1492 + JSON.parse(JSON.stringify(prerendered.postponed)),
1493 + ),
1494 );
1495
1496 const decoder = new TextDecoder();
@@ -1391,7 +1504,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
1504
1505 await 1;
1506 hasLoaded = true;
1394 - resolve();
1507 + await serverAct(resolve);
1508
1509 while (true) {
1510 ({value, done} = await reader.read());
@@ -1425,10 +1538,12 @@ describe('ReactDOMFizzStaticBrowser', () => {
1538 throw new Error('bad onHeaders');
1539 }
1540
1428 - const prerendered = await ReactDOMFizzStatic.prerender(<div>hello</div>, {
1429 - onHeaders,
1430 - onError,
1431 - });
1541 + const prerendered = await serverAct(() =>
1542 + ReactDOMFizzStatic.prerender(<div>hello</div>, {
1543 + onHeaders,
1544 + onError,
1545 + }),
1546 + );
1547 expect(prerendered.postponed).toBe(null);
1548 expect(errors).toEqual(['bad onHeaders']);
1549
@@ -1469,9 +1584,11 @@ describe('ReactDOMFizzStaticBrowser', () => {
1584 {virtual: true},
1585 );
1586
1472 - const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
1473 - bootstrapScripts: ['init.js'],
1474 - });
1587 + const prerendered = await serverAct(() =>
1588 + ReactDOMFizzStatic.prerender(<App />, {
1589 + bootstrapScripts: ['init.js'],
1590 + }),
1591 + );
1592
1593 const postponedSerializedState = JSON.stringify(prerendered.postponed);
1594
@@ -1497,9 +1614,8 @@ describe('ReactDOMFizzStaticBrowser', () => {
1614
1615 prerendering = false;
1616
1500 - const content = await ReactDOMFizzServer.resume(
1501 - <App />,
1502 - JSON.parse(postponedSerializedState),
1617 + const content = await serverAct(() =>
1618 + ReactDOMFizzServer.resume(<App />, JSON.parse(postponedSerializedState)),
1619 );
1620
1621 await readIntoContainer(content);
@@ -1542,7 +1658,9 @@ describe('ReactDOMFizzStaticBrowser', () => {
1658 );
1659 }
1660
1545 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
1661 + const prerendered = await serverAct(() =>
1662 + ReactDOMFizzStatic.prerender(<App />),
1663 + );
1664 const postponedState = JSON.stringify(prerendered.postponed);
1665
1666 await readIntoContainer(prerendered.prelude);
@@ -1550,9 +1668,8 @@ describe('ReactDOMFizzStaticBrowser', () => {
1668
1669 isPrerendering = false;
1670
1553 - const dynamic = await ReactDOMFizzServer.resume(
1554 - <App />,
1555 - JSON.parse(postponedState),
1671 + const dynamic = await serverAct(() =>
1672 + ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState)),
1673 );
1674
1675 await readIntoContainer(dynamic);
packages/react-dom/src/__tests__/ReactDOMFizzStaticFloat-test.js
+40 -19
@@ -9,6 +9,8 @@
9
10 'use strict';
11
12 +import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
13 +
14 import {
15 getVisibleChildren,
16 insertNodesAndExecuteScripts,
@@ -25,10 +27,16 @@ let ReactDOMFizzServer;
27 let ReactDOMFizzStatic;
28 let Suspense;
29 let container;
30 +let Scheduler;
31 +let act;
32
33 describe('ReactDOMFizzStaticFloat', () => {
34 beforeEach(() => {
35 jest.resetModules();
36 + Scheduler = require('scheduler');
37 + patchMessageChannel(Scheduler);
38 + act = require('internal-test-utils').act;
39 +
40 React = require('react');
41 ReactDOM = require('react-dom');
42 ReactDOMFizzServer = require('react-dom/server.browser');
@@ -44,6 +52,17 @@ describe('ReactDOMFizzStaticFloat', () => {
52 document.body.removeChild(container);
53 });
54
55 + async function serverAct(callback) {
56 + let maybePromise;
57 + await act(() => {
58 + maybePromise = callback();
59 + if (maybePromise && typeof maybePromise.catch === 'function') {
60 + maybePromise.catch(() => {});
61 + }
62 + });
63 + return maybePromise;
64 + }
65 +
66 async function readIntoContainer(stream) {
67 const reader = stream.getReader();
68 let result = '';
@@ -135,7 +154,9 @@ describe('ReactDOMFizzStaticFloat', () => {
154 virtual: true,
155 });
156
138 - const prerendered = await ReactDOMFizzStatic.prerender(<App />);
157 + const prerendered = await serverAct(() =>
158 + ReactDOMFizzStatic.prerender(<App />),
159 + );
160 expect(prerendered.postponed).not.toBe(null);
161
162 await readIntoContainer(prerendered.prelude);
@@ -171,28 +192,28 @@ describe('ReactDOMFizzStaticFloat', () => {
192 ]);
193
194 prerendering = false;
174 - const content = await ReactDOMFizzServer.resume(
175 - <App />,
176 - JSON.parse(JSON.stringify(prerendered.postponed)),
195 + const content = await serverAct(() =>
196 + ReactDOMFizzServer.resume(
197 + <App />,
198 + JSON.parse(JSON.stringify(prerendered.postponed)),
199 + ),
200 );
201
202 await readIntoContainer(content);
203
181 - // Dispatch load event to injected stylesheet
182 - const linkCreds = document.querySelector(
183 - 'link[rel="stylesheet"][href="style creds"]',
184 - );
185 - const linkAnon = document.querySelector(
186 - 'link[rel="stylesheet"][href="style anon"]',
187 - );
188 - const event = document.createEvent('Events');
189 - event.initEvent('load', true, true);
190 - linkCreds.dispatchEvent(event);
191 - linkAnon.dispatchEvent(event);
192 -
193 - // Wait for the instruction microtasks to flush.
194 - await 0;
195 - await 0;
204 + await act(() => {
205 + // Dispatch load event to injected stylesheet
206 + const linkCreds = document.querySelector(
207 + 'link[rel="stylesheet"][href="style creds"]',
208 + );
209 + const linkAnon = document.querySelector(
210 + 'link[rel="stylesheet"][href="style anon"]',
211 + );
212 + const event = document.createEvent('Events');
213 + event.initEvent('load', true, true);
214 + linkCreds.dispatchEvent(event);
215 + linkAnon.dispatchEvent(event);
216 + });
217
218 expect(getVisibleChildren(document)).toEqual(
219 <html>
packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOM-test.js
+28 -13
@@ -9,16 +9,14 @@
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
18 -// Don't wait before processing work on the server.
19 -// TODO: we can replace this with FlightServer.act().
20 -global.setImmediate = cb => cb();
21 -
20 let act;
21 let use;
22 let clientExports;
@@ -29,6 +27,8 @@ let ReactDOMClient;
27 let ReactServerDOMServer;
28 let ReactServerDOMClient;
29 let Suspense;
30 +let ReactServerScheduler;
31 +let reactServerAct;
32
33 describe('ReactFlightDOM', () => {
34 beforeEach(() => {
@@ -37,6 +37,10 @@ describe('ReactFlightDOM', () => {
37 // condition
38 jest.resetModules();
39
40 + ReactServerScheduler = require('scheduler');
41 + patchSetImmediate(ReactServerScheduler);
42 + reactServerAct = require('internal-test-utils').act;
43 +
44 // Simulate the condition resolution
45 jest.mock('react-server-dom-turbopack/server', () =>
46 require('react-server-dom-turbopack/server.node.unbundled'),
@@ -61,6 +65,17 @@ describe('ReactFlightDOM', () => {
65 ReactServerDOMClient = require('react-server-dom-turbopack/client');
66 });
67
68 + async function serverAct(callback) {
69 + let maybePromise;
70 + await reactServerAct(() => {
71 + maybePromise = callback();
72 + if (maybePromise && typeof maybePromise.catch === 'function') {
73 + maybePromise.catch(() => {});
74 + }
75 + });
76 + return maybePromise;
77 + }
78 +
79 function getTestStream() {
80 const writable = new Stream.PassThrough();
81 const readable = new ReadableStream({
@@ -100,9 +115,8 @@ describe('ReactFlightDOM', () => {
115 }
116
117 const {writable, readable} = getTestStream();
103 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
104 - <App />,
105 - turbopackMap,
118 + const {pipe} = await serverAct(() =>
119 + ReactServerDOMServer.renderToPipeableStream(<App />, turbopackMap),
120 );
121 pipe(writable);
122 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -149,9 +163,8 @@ describe('ReactFlightDOM', () => {
163 }
164
165 const {writable, readable} = getTestStream();
152 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
153 - <RootModel />,
154 - turbopackMap,
166 + const {pipe} = await serverAct(() =>
167 + ReactServerDOMServer.renderToPipeableStream(<RootModel />, turbopackMap),
168 );
169 pipe(writable);
170 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -191,9 +204,11 @@ describe('ReactFlightDOM', () => {
204 const AsyncModuleRef2 = await clientExports(AsyncModule2);
205
206 const {writable, readable} = getTestStream();
194 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
195 - <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
196 - turbopackMap,
207 + const {pipe} = await serverAct(() =>
208 + ReactServerDOMServer.renderToPipeableStream(
209 + <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
210 + turbopackMap,
211 + ),
212 );
213 pipe(writable);
214 const response = ReactServerDOMClient.createFromReadableStream(readable);
packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMBrowser-test.js
+22 -1
@@ -9,6 +9,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;
@@ -18,11 +20,17 @@ global.TextDecoder = require('util').TextDecoder;
20 let React;
21 let ReactServerDOMServer;
22 let ReactServerDOMClient;
23 +let ReactServerScheduler;
24 +let reactServerAct;
25
26 describe('ReactFlightDOMBrowser', () => {
27 beforeEach(() => {
28 jest.resetModules();
29
30 + ReactServerScheduler = require('scheduler');
31 + patchMessageChannel(ReactServerScheduler);
32 + reactServerAct = require('internal-test-utils').act;
33 +
34 // Simulate the condition resolution
35 jest.mock('react', () => require('react/react.react-server'));
36 jest.mock('react-server-dom-turbopack/server', () =>
@@ -38,6 +46,17 @@ describe('ReactFlightDOMBrowser', () => {
46 ReactServerDOMClient = require('react-server-dom-turbopack/client');
47 });
48
49 + async function serverAct(callback) {
50 + let maybePromise;
51 + await reactServerAct(() => {
52 + maybePromise = callback();
53 + if (maybePromise && typeof maybePromise.catch === 'function') {
54 + maybePromise.catch(() => {});
55 + }
56 + });
57 + return maybePromise;
58 + }
59 +
60 it('should resolve HTML using W3C streams', async () => {
61 function Text({children}) {
62 return <span>{children}</span>;
@@ -58,7 +77,9 @@ describe('ReactFlightDOMBrowser', () => {
77 return model;
78 }
79
61 - const stream = ReactServerDOMServer.renderToReadableStream(<App />);
80 + const stream = await serverAct(() =>
81 + ReactServerDOMServer.renderToReadableStream(<App />),
82 + );
83 const response = ReactServerDOMClient.createFromReadableStream(stream);
84 const model = await response;
85 expect(model).toEqual({
packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMNode-test.js
+22 -8
@@ -9,9 +9,7 @@
9
10 'use strict';
11
12 -// Don't wait before processing work on the server.
13 -// TODO: we can replace this with FlightServer.act().
14 -global.setImmediate = cb => cb();
12 +import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
13
14 let clientExports;
15 let turbopackMap;
@@ -23,11 +21,17 @@ let ReactServerDOMServer;
21 let ReactServerDOMClient;
22 let Stream;
23 let use;
24 +let ReactServerScheduler;
25 +let reactServerAct;
26
27 describe('ReactFlightDOMNode', () => {
28 beforeEach(() => {
29 jest.resetModules();
30
31 + ReactServerScheduler = require('scheduler');
32 + patchSetImmediate(ReactServerScheduler);
33 + reactServerAct = require('internal-test-utils').act;
34 +
35 // Simulate the condition resolution
36 jest.mock('react', () => require('react/react.react-server'));
37 jest.mock('react-server-dom-turbopack/server', () =>
@@ -55,6 +59,17 @@ describe('ReactFlightDOMNode', () => {
59 use = React.use;
60 });
61
62 + async function serverAct(callback) {
63 + let maybePromise;
64 + await reactServerAct(() => {
65 + maybePromise = callback();
66 + if (maybePromise && typeof maybePromise.catch === 'function') {
67 + maybePromise.catch(() => {});
68 + }
69 + });
70 + return maybePromise;
71 + }
72 +
73 function readResult(stream) {
74 return new Promise((resolve, reject) => {
75 let buffer = '';
@@ -102,9 +117,8 @@ describe('ReactFlightDOMNode', () => {
117 return <ClientComponentOnTheClient />;
118 }
119
105 - const stream = ReactServerDOMServer.renderToPipeableStream(
106 - <App />,
107 - turbopackMap,
120 + const stream = await serverAct(() =>
121 + ReactServerDOMServer.renderToPipeableStream(<App />, turbopackMap),
122 );
123 const readable = new Stream.PassThrough();
124
@@ -121,8 +135,8 @@ describe('ReactFlightDOMNode', () => {
135 return use(response);
136 }
137
124 - const ssrStream = await ReactDOMServer.renderToPipeableStream(
125 - <ClientRoot />,
138 + const ssrStream = await serverAct(() =>
139 + ReactDOMServer.renderToPipeableStream(<ClientRoot />),
140 );
141 const result = await readResult(ssrStream);
142 expect(result).toEqual(
packages/react-server-dom-turbopack/src/__tests__/ReactFlightTurbopackDOMReply-test.js
+7
@@ -9,6 +9,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;
@@ -19,10 +21,15 @@ global.TextDecoder = require('util').TextDecoder;
21 let turbopackServerMap;
22 let ReactServerDOMServer;
23 let ReactServerDOMClient;
24 +let ReactServerScheduler;
25
26 describe('ReactFlightDOMReply', () => {
27 beforeEach(() => {
28 jest.resetModules();
29 +
30 + ReactServerScheduler = require('scheduler');
31 + patchMessageChannel(ReactServerScheduler);
32 +
33 // Simulate the condition resolution
34 jest.mock('react', () => require('react/react.react-server'));
35 jest.mock('react-server-dom-turbopack/server', () =>
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+211 -153
@@ -9,16 +9,14 @@
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
18 -// Don't wait before processing work on the server.
19 -// TODO: we can replace this with FlightServer.act().
20 -global.setImmediate = cb => cb();
21 -
20 let act;
21 let use;
22 let clientExports;
@@ -36,6 +34,8 @@ let ReactDOMStaticServer;
34 let Suspense;
35 let ErrorBoundary;
36 let JSDOM;
37 +let ReactServerScheduler;
38 +let reactServerAct;
39
40 describe('ReactFlightDOM', () => {
41 beforeEach(() => {
@@ -46,6 +46,10 @@ describe('ReactFlightDOM', () => {
46
47 JSDOM = require('jsdom').JSDOM;
48
49 + ReactServerScheduler = require('scheduler');
50 + patchSetImmediate(ReactServerScheduler);
51 + reactServerAct = require('internal-test-utils').act;
52 +
53 // Simulate the condition resolution
54 jest.mock('react', () => require('react/react.react-server'));
55 FlightReact = require('react');
@@ -92,6 +96,17 @@ describe('ReactFlightDOM', () => {
96 };
97 });
98
99 + async function serverAct(callback) {
100 + let maybePromise;
101 + await reactServerAct(() => {
102 + maybePromise = callback();
103 + if (maybePromise && typeof maybePromise.catch === 'function') {
104 + maybePromise.catch(() => {});
105 + }
106 + });
107 + return maybePromise;
108 + }
109 +
110 function getTestStream() {
111 const writable = new Stream.PassThrough();
112 const readable = new ReadableStream({
@@ -181,9 +196,8 @@ describe('ReactFlightDOM', () => {
196 }
197
198 const {writable, readable} = getTestStream();
184 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
185 - <App />,
186 - webpackMap,
199 + const {pipe} = await serverAct(() =>
200 + ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
201 );
202 pipe(writable);
203 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -230,9 +244,8 @@ describe('ReactFlightDOM', () => {
244 }
245
246 const {writable, readable} = getTestStream();
233 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
234 - <RootModel />,
235 - webpackMap,
247 + const {pipe} = await serverAct(() =>
248 + ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
249 );
250 pipe(writable);
251 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -266,9 +279,8 @@ describe('ReactFlightDOM', () => {
279 }
280
281 const {writable, readable} = getTestStream();
269 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
270 - <RootModel />,
271 - webpackMap,
282 + const {pipe} = await serverAct(() =>
283 + ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
284 );
285 pipe(writable);
286 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -300,9 +312,8 @@ describe('ReactFlightDOM', () => {
312 }
313
314 const {writable, readable} = getTestStream();
303 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
304 - <RootModel />,
305 - webpackMap,
315 + const {pipe} = await serverAct(() =>
316 + ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
317 );
318 pipe(writable);
319 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -349,9 +360,11 @@ describe('ReactFlightDOM', () => {
360 );
361
362 const {writable, readable} = getTestStream();
352 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
353 - <Component greeting={hi} />,
354 - webpackMap,
363 + const {pipe} = await serverAct(() =>
364 + ReactServerDOMServer.renderToPipeableStream(
365 + <Component greeting={hi} />,
366 + webpackMap,
367 + ),
368 );
369 pipe(writable);
370 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -386,9 +399,11 @@ describe('ReactFlightDOM', () => {
399 const {Component} = clientExports(Module);
400
401 const {writable, readable} = getTestStream();
389 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
390 - <Component greeting={'Hello'} />,
391 - webpackMap,
402 + const {pipe} = await serverAct(() =>
403 + ReactServerDOMServer.renderToPipeableStream(
404 + <Component greeting={'Hello'} />,
405 + webpackMap,
406 + ),
407 );
408 pipe(writable);
409 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -424,9 +439,11 @@ describe('ReactFlightDOM', () => {
439 const {split: Component} = clientExports(Module);
440
441 const {writable, readable} = getTestStream();
427 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
428 - <Component greeting={'Hello'} />,
429 - webpackMap,
442 + const {pipe} = await serverAct(() =>
443 + ReactServerDOMServer.renderToPipeableStream(
444 + <Component greeting={'Hello'} />,
445 + webpackMap,
446 + ),
447 );
448 pipe(writable);
449 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -464,9 +481,11 @@ describe('ReactFlightDOM', () => {
481 const AsyncModuleRef2 = await clientExports(AsyncModule2);
482
483 const {writable, readable} = getTestStream();
467 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
468 - <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
469 - webpackMap,
484 + const {pipe} = await serverAct(() =>
485 + ReactServerDOMServer.renderToPipeableStream(
486 + <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
487 + webpackMap,
488 + ),
489 );
490 pipe(writable);
491 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -502,9 +521,11 @@ describe('ReactFlightDOM', () => {
521 }
522
523 const {writable, readable} = getTestStream();
505 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
506 - <ServerComponent />,
507 - webpackMap,
524 + const {pipe} = await serverAct(() =>
525 + ReactServerDOMServer.renderToPipeableStream(
526 + <ServerComponent />,
527 + webpackMap,
528 + ),
529 );
530 pipe(writable);
531 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -539,9 +560,8 @@ describe('ReactFlightDOM', () => {
560 const ThenRef = clientExports(thenExports).then;
561
562 const {writable, readable} = getTestStream();
542 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
543 - <ThenRef />,
544 - webpackMap,
563 + const {pipe} = await serverAct(() =>
564 + ReactServerDOMServer.renderToPipeableStream(<ThenRef />, webpackMap),
565 );
566 pipe(writable);
567 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -719,15 +739,13 @@ describe('ReactFlightDOM', () => {
739 }
740
741 const {writable, readable} = getTestStream();
722 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
723 - model,
724 - webpackMap,
725 - {
742 + const {pipe} = await serverAct(() =>
743 + ReactServerDOMServer.renderToPipeableStream(model, webpackMap, {
744 onError(x) {
745 reportedErrors.push(x);
746 return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
747 },
730 - },
748 + }),
749 );
750 pipe(writable);
751 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -744,14 +762,18 @@ describe('ReactFlightDOM', () => {
762 expect(container.innerHTML).toBe('<p>(loading)</p>');
763
764 // This isn't enough to show anything.
747 - await act(() => {
748 - resolveFriends();
765 + await serverAct(async () => {
766 + await act(() => {
767 + resolveFriends();
768 + });
769 });
770 expect(container.innerHTML).toBe('<p>(loading)</p>');
771
772 // We can now show the details. Sidebar and posts are still loading.
753 - await act(() => {
754 - resolveName();
773 + await serverAct(async () => {
774 + await act(() => {
775 + resolveName();
776 + });
777 });
778 // Advance time enough to trigger a nested fallback.
779 await act(() => {
@@ -768,9 +790,11 @@ describe('ReactFlightDOM', () => {
790
791 const theError = new Error('Game over');
792 // Let's *fail* loading games.
771 - await act(async () => {
772 - await rejectGames(theError);
773 - await 'the inner async function';
793 + await serverAct(async () => {
794 + await act(async () => {
795 + await rejectGames(theError);
796 + await 'the inner async function';
797 + });
798 });
799 const expectedGamesValue = __DEV__
800 ? '<p>Game over + a dev digest</p>'
@@ -786,9 +810,11 @@ describe('ReactFlightDOM', () => {
810 reportedErrors = [];
811
812 // We can now show the sidebar.
789 - await act(async () => {
790 - await resolvePhotos();
791 - await 'the inner async function';
813 + await serverAct(async () => {
814 + await act(async () => {
815 + await resolvePhotos();
816 + await 'the inner async function';
817 + });
818 });
819 expect(container.innerHTML).toBe(
820 '<div>:name::avatar:</div>' +
@@ -798,9 +824,11 @@ describe('ReactFlightDOM', () => {
824 );
825
826 // Show everything.
801 - await act(async () => {
802 - await resolvePosts();
803 - await 'the inner async function';
827 + await serverAct(async () => {
828 + await act(async () => {
829 + await resolvePosts();
830 + await 'the inner async function';
831 + });
832 });
833 expect(container.innerHTML).toBe(
834 '<div>:name::avatar:</div>' +
@@ -867,14 +895,16 @@ describe('ReactFlightDOM', () => {
895 const [Photos, resolvePhotosData] = makeDelayedText();
896 const suspendedChunk = createSuspendedChunk(<p>loading</p>);
897 const {writable, readable} = getTestStream();
870 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
871 - suspendedChunk.row,
872 - webpackMap,
873 - {
874 - onError(error) {
875 - reportedErrors.push(error);
898 + const {pipe} = await serverAct(() =>
899 + ReactServerDOMServer.renderToPipeableStream(
900 + suspendedChunk.row,
901 + webpackMap,
902 + {
903 + onError(error) {
904 + reportedErrors.push(error);
905 + },
906 },
877 - },
907 + ),
908 );
909 pipe(writable);
910 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -900,16 +930,20 @@ describe('ReactFlightDOM', () => {
930 </Suspense>
931 );
932
903 - await act(async () => {
904 - suspendedChunk.resolve({value, done: false, next: donePromise.promise});
905 - donePromise.resolve({value, done: true});
933 + await serverAct(async () => {
934 + await act(async () => {
935 + suspendedChunk.resolve({value, done: false, next: donePromise.promise});
936 + donePromise.resolve({value, done: true});
937 + });
938 });
939
940 expect(container.innerHTML).toBe('<p>loading posts and photos</p>');
941
910 - await act(async () => {
911 - await resolvePostsData('posts');
912 - await resolvePhotosData('photos');
942 + await serverAct(async () => {
943 + await act(async () => {
944 + await resolvePostsData('posts');
945 + await resolvePhotosData('photos');
946 + });
947 });
948
949 expect(container.innerHTML).toBe('<div>posts</div><div>photos</div>');
@@ -945,9 +979,11 @@ describe('ReactFlightDOM', () => {
979 const root = ReactDOMClient.createRoot(container);
980
981 const stream1 = getTestStream();
948 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
949 - <App color="red" />,
950 - webpackMap,
982 + const {pipe} = await serverAct(() =>
983 + ReactServerDOMServer.renderToPipeableStream(
984 + <App color="red" />,
985 + webpackMap,
986 + ),
987 );
988 pipe(stream1.writable);
989 const response1 = ReactServerDOMClient.createFromReadableStream(
@@ -973,9 +1009,11 @@ describe('ReactFlightDOM', () => {
1009 inputB.value = 'goodbye';
1010
1011 const stream2 = getTestStream();
976 - const {pipe: pipe2} = ReactServerDOMServer.renderToPipeableStream(
977 - <App color="blue" />,
978 - webpackMap,
1012 + const {pipe: pipe2} = await serverAct(() =>
1013 + ReactServerDOMServer.renderToPipeableStream(
1014 + <App color="blue" />,
1015 + webpackMap,
1016 + ),
1017 );
1018 pipe2(stream2.writable);
1019 const response2 = ReactServerDOMClient.createFromReadableStream(
@@ -1005,18 +1043,20 @@ describe('ReactFlightDOM', () => {
1043 const reportedErrors = [];
1044
1045 const {writable, readable} = getTestStream();
1008 - const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
1009 - <div>
1010 - <InfiniteSuspend />
1011 - </div>,
1012 - webpackMap,
1013 - {
1014 - onError(x) {
1015 - reportedErrors.push(x);
1016 - const message = typeof x === 'string' ? x : x.message;
1017 - return __DEV__ ? 'a dev digest' : `digest("${message}")`;
1046 + const {pipe, abort} = await serverAct(() =>
1047 + ReactServerDOMServer.renderToPipeableStream(
1048 + <div>
1049 + <InfiniteSuspend />
1050 + </div>,
1051 + webpackMap,
1052 + {
1053 + onError(x) {
1054 + reportedErrors.push(x);
1055 + const message = typeof x === 'string' ? x : x.message;
1056 + return __DEV__ ? 'a dev digest' : `digest("${message}")`;
1057 + },
1058 },
1019 - },
1059 + ),
1060 );
1061 pipe(writable);
1062 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -1067,16 +1107,18 @@ describe('ReactFlightDOM', () => {
1107 const ClientReference = clientModuleError(new Error('module init error'));
1108
1109 const {writable, readable} = getTestStream();
1070 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1071 - <div>
1072 - <ClientComponent prop={ClientReference} />
1073 - </div>,
1074 - webpackMap,
1075 - {
1076 - onError(x) {
1077 - reportedErrors.push(x);
1110 + const {pipe} = await serverAct(() =>
1111 + ReactServerDOMServer.renderToPipeableStream(
1112 + <div>
1113 + <ClientComponent prop={ClientReference} />
1114 + </div>,
1115 + webpackMap,
1116 + {
1117 + onError(x) {
1118 + reportedErrors.push(x);
1119 + },
1120 },
1079 - },
1121 + ),
1122 );
1123 pipe(writable);
1124 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -1117,16 +1159,18 @@ describe('ReactFlightDOM', () => {
1159 );
1160
1161 const {writable, readable} = getTestStream();
1120 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1121 - <div>
1122 - <ClientComponent prop={ClientReference} />
1123 - </div>,
1124 - webpackMap,
1125 - {
1126 - onError(x) {
1127 - reportedErrors.push(x);
1162 + const {pipe} = await serverAct(() =>
1163 + ReactServerDOMServer.renderToPipeableStream(
1164 + <div>
1165 + <ClientComponent prop={ClientReference} />
1166 + </div>,
1167 + webpackMap,
1168 + {
1169 + onError(x) {
1170 + reportedErrors.push(x);
1171 + },
1172 },
1129 - },
1173 + ),
1174 );
1175 pipe(writable);
1176 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -1176,17 +1220,19 @@ describe('ReactFlightDOM', () => {
1220 }
1221
1222 const {writable, readable} = getTestStream();
1179 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1180 - <div>
1181 - <ClientComponent />
1182 - </div>,
1183 - webpackMap,
1184 - {
1185 - onError(x) {
1186 - reportedErrors.push(x.message);
1187 - return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1223 + const {pipe} = await serverAct(() =>
1224 + ReactServerDOMServer.renderToPipeableStream(
1225 + <div>
1226 + <ClientComponent />
1227 + </div>,
1228 + webpackMap,
1229 + {
1230 + onError(x) {
1231 + reportedErrors.push(x.message);
1232 + return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1233 + },
1234 },
1189 - },
1235 + ),
1236 );
1237 pipe(writable);
1238
@@ -1255,9 +1301,11 @@ describe('ReactFlightDOM', () => {
1301 }
1302
1303 const {writable, readable} = getTestStream();
1258 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1259 - <ServerComponent />,
1260 - webpackMap,
1304 + const {pipe} = await serverAct(() =>
1305 + ReactServerDOMServer.renderToPipeableStream(
1306 + <ServerComponent />,
1307 + webpackMap,
1308 + ),
1309 );
1310 pipe(writable);
1311 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -1311,15 +1359,17 @@ describe('ReactFlightDOM', () => {
1359 }
1360
1361 const {writable, readable} = getTestStream();
1314 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1315 - <ServerComponent />,
1316 - webpackMap,
1317 - {
1318 - onError(x) {
1319 - reportedErrors.push(x);
1320 - return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1362 + const {pipe} = await serverAct(() =>
1363 + ReactServerDOMServer.renderToPipeableStream(
1364 + <ServerComponent />,
1365 + webpackMap,
1366 + {
1367 + onError(x) {
1368 + reportedErrors.push(x);
1369 + return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1370 + },
1371 },
1322 - },
1372 + ),
1373 );
1374 pipe(writable);
1375 const response = ReactServerDOMClient.createFromReadableStream(readable);
@@ -1368,9 +1418,11 @@ describe('ReactFlightDOM', () => {
1418 }
1419
1420 const {writable, readable} = getTestStream();
1371 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1372 - <ServerComponent />,
1373 - webpackMap,
1421 + const {pipe} = await serverAct(() =>
1422 + ReactServerDOMServer.renderToPipeableStream(
1423 + <ServerComponent />,
1424 + webpackMap,
1425 + ),
1426 );
1427 pipe(writable);
1428
@@ -1463,9 +1515,8 @@ describe('ReactFlightDOM', () => {
1515
1516 const {writable, readable} = getTestStream();
1517
1466 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1467 - <App />,
1468 - webpackMap,
1518 + const {pipe} = await serverAct(() =>
1519 + ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
1520 );
1521 pipe(writable);
1522
@@ -1485,11 +1536,10 @@ describe('ReactFlightDOM', () => {
1536 function onError(error, errorInfo) {
1537 errors.push(error, errorInfo);
1538 }
1488 - const result = await ReactDOMStaticServer.prerenderToNodeStream(
1489 - <Response />,
1490 - {
1539 + const result = await serverAct(() =>
1540 + ReactDOMStaticServer.prerenderToNodeStream(<Response />, {
1541 onError,
1492 - },
1542 + }),
1543 );
1544
1545 const prelude = await new Promise((resolve, reject) => {
@@ -1554,9 +1604,11 @@ describe('ReactFlightDOM', () => {
1604 // module graphs and we are contriving the sequencing to work in a way where
1605 // the right HostDispatcher is in scope during the Flight Server Float calls and the
1606 // Flight Client hint dispatches
1557 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1558 - <ServerComponent />,
1559 - webpackMap,
1607 + const {pipe} = await serverAct(() =>
1608 + ReactServerDOMServer.renderToPipeableStream(
1609 + <ServerComponent />,
1610 + webpackMap,
1611 + ),
1612 );
1613 pipe(flightWritable);
1614
@@ -1577,7 +1629,7 @@ describe('ReactFlightDOM', () => {
1629 );
1630 }
1631
1580 - await act(async () => {
1632 + await serverAct(async () => {
1633 ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
1634 });
1635
@@ -1680,11 +1732,11 @@ describe('ReactFlightDOM', () => {
1732 // pausing to let Flight runtime tick. This is a test only artifact of the fact that
1733 // we aren't operating separate module graphs for flight and fiber. In a real app
1734 // each would have their own dispatcher and there would be no cross dispatching.
1683 - await 1;
1735 + await serverAct(() => {});
1736
1737 const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream();
1738 const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream();
1687 - await act(async () => {
1739 + await serverAct(async () => {
1740 ReactDOMFizzServer.renderToPipeableStream(
1741 <App stream={flightReadable1} />,
1742 ).pipe(fizzWritable1);
@@ -1751,10 +1803,12 @@ describe('ReactFlightDOM', () => {
1803
1804 const {writable, readable} = getTestStream();
1805
1754 - ReactServerDOMServer.renderToPipeableStream(
1755 - <ServerComponent />,
1756 - webpackMap,
1757 - ).pipe(writable);
1806 + await serverAct(() =>
1807 + ReactServerDOMServer.renderToPipeableStream(
1808 + <ServerComponent />,
1809 + webpackMap,
1810 + ).pipe(writable),
1811 + );
1812
1813 const hintRows = [];
1814 async function collectHints(stream) {
@@ -1798,16 +1852,18 @@ describe('ReactFlightDOM', () => {
1852 class InvalidValue {}
1853
1854 const {writable} = getTestStream();
1801 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1802 - <div>
1803 - <ClientComponent prop={ClientReference} invalid={InvalidValue} />
1804 - </div>,
1805 - webpackMap,
1806 - {
1807 - onError(x) {
1808 - reportedErrors.push(x);
1855 + const {pipe} = await serverAct(() =>
1856 + ReactServerDOMServer.renderToPipeableStream(
1857 + <div>
1858 + <ClientComponent prop={ClientReference} invalid={InvalidValue} />
1859 + </div>,
1860 + webpackMap,
1861 + {
1862 + onError(x) {
1863 + reportedErrors.push(x);
1864 + },
1865 },
1810 - },
1866 + ),
1867 );
1868 pipe(writable);
1869
@@ -1839,9 +1895,11 @@ describe('ReactFlightDOM', () => {
1895 }
1896
1897 const {writable, readable} = getTestStream();
1842 - const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1843 - <ServerComponent />,
1844 - webpackMap,
1898 + const {pipe} = await serverAct(() =>
1899 + ReactServerDOMServer.renderToPipeableStream(
1900 + <ServerComponent />,
1901 + webpackMap,
1902 + ),
1903 );
1904 pipe(writable);
1905 const response = ReactServerDOMClient.createFromReadableStream(readable);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+312 -209
@@ -15,6 +15,10 @@ global.ReadableStream =
15 global.TextEncoder = require('util').TextEncoder;
16 global.TextDecoder = require('util').TextDecoder;
17
18 +const {
19 + patchMessageChannel,
20 +} = require('../../../../scripts/jest/patchMessageChannel');
21 +
22 let clientExports;
23 let serverExports;
24 let webpackMap;
@@ -30,11 +34,18 @@ let Suspense;
34 let use;
35 let ReactServer;
36 let ReactServerDOM;
37 +let Scheduler;
38 +let ReactServerScheduler;
39 +let reactServerAct;
40
41 describe('ReactFlightDOMBrowser', () => {
42 beforeEach(() => {
43 jest.resetModules();
44
45 + ReactServerScheduler = require('scheduler');
46 + patchMessageChannel(ReactServerScheduler);
47 + reactServerAct = require('internal-test-utils').act;
48 +
49 // Simulate the condition resolution
50
51 jest.mock('react', () => require('react/react.react-server'));
@@ -54,6 +65,9 @@ describe('ReactFlightDOMBrowser', () => {
65 __unmockReact();
66 jest.resetModules();
67
68 + Scheduler = require('scheduler');
69 + patchMessageChannel(Scheduler);
70 +
71 act = require('internal-test-utils').act;
72 React = require('react');
73 ReactDOM = require('react-dom');
@@ -64,6 +78,17 @@ describe('ReactFlightDOMBrowser', () => {
78 use = React.use;
79 });
80
81 + async function serverAct(callback) {
82 + let maybePromise;
83 + await reactServerAct(() => {
84 + maybePromise = callback();
85 + if (maybePromise && typeof maybePromise.catch === 'function') {
86 + maybePromise.catch(() => {});
87 + }
88 + });
89 + return maybePromise;
90 + }
91 +
92 function makeDelayedText(Model) {
93 let error, _resolve, _reject;
94 let promise = new Promise((resolve, reject) => {
@@ -152,7 +177,9 @@ describe('ReactFlightDOMBrowser', () => {
177 return model;
178 }
179
155 - const stream = ReactServerDOMServer.renderToReadableStream(<App />);
180 + const stream = await serverAct(() =>
181 + ReactServerDOMServer.renderToReadableStream(<App />),
182 + );
183 const response = ReactServerDOMClient.createFromReadableStream(stream);
184 const model = await response;
185 expect(model).toEqual({
@@ -185,7 +212,9 @@ describe('ReactFlightDOMBrowser', () => {
212 return model;
213 }
214
188 - const stream = ReactServerDOMServer.renderToReadableStream(<App />);
215 + const stream = await serverAct(() =>
216 + ReactServerDOMServer.renderToReadableStream(<App />),
217 + );
218 const response = ReactServerDOMClient.createFromReadableStream(stream);
219 const model = await response;
220 expect(model).toEqual({
@@ -221,9 +250,8 @@ describe('ReactFlightDOMBrowser', () => {
250 return <ClientOuter Component={ClientInner}>Hello, World!</ClientOuter>;
251 }
252
224 - const stream = ReactServerDOMServer.renderToReadableStream(
225 - <Server />,
226 - webpackMap,
253 + const stream = await serverAct(() =>
254 + ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
255 );
256
257 function ClientRoot({response}) {
@@ -270,9 +298,11 @@ describe('ReactFlightDOMBrowser', () => {
298 const shared = [1, 2, 3];
299 const value = [shared, shared];
300
273 - const stream = ReactServerDOMServer.renderToReadableStream(
274 - <Server value={value} />,
275 - webpackMap,
301 + const stream = await serverAct(() =>
302 + ReactServerDOMServer.renderToReadableStream(
303 + <Server value={value} />,
304 + webpackMap,
305 + ),
306 );
307
308 function ClientRoot({response}) {
@@ -319,9 +349,11 @@ describe('ReactFlightDOMBrowser', () => {
349 const shared = [1, 2, 3];
350 const value = [shared, shared];
351
322 - const stream = ReactServerDOMServer.renderToReadableStream(
323 - <Server value={value} />,
324 - webpackMap,
352 + const stream = await serverAct(() =>
353 + ReactServerDOMServer.renderToReadableStream(
354 + <Server value={value} />,
355 + webpackMap,
356 + ),
357 );
358
359 function ClientRoot({response}) {
@@ -457,15 +489,13 @@ describe('ReactFlightDOMBrowser', () => {
489 return use(response).rootContent;
490 }
491
460 - const stream = ReactServerDOMServer.renderToReadableStream(
461 - model,
462 - webpackMap,
463 - {
492 + const stream = await serverAct(() =>
493 + ReactServerDOMServer.renderToReadableStream(model, webpackMap, {
494 onError(x) {
495 reportedErrors.push(x);
496 return __DEV__ ? `a dev digest` : `digest("${x.message}")`;
497 },
468 - },
498 + }),
499 );
500 const response = ReactServerDOMClient.createFromReadableStream(stream);
501
@@ -481,14 +511,18 @@ describe('ReactFlightDOMBrowser', () => {
511 expect(container.innerHTML).toBe('<p>(loading)</p>');
512
513 // This isn't enough to show anything.
484 - await act(() => {
485 - resolveFriends();
514 + await serverAct(async () => {
515 + await act(() => {
516 + resolveFriends();
517 + });
518 });
519 expect(container.innerHTML).toBe('<p>(loading)</p>');
520
521 // We can now show the details. Sidebar and posts are still loading.
490 - await act(() => {
491 - resolveName();
522 + await serverAct(async () => {
523 + await act(() => {
524 + resolveName();
525 + });
526 });
527 // Advance time enough to trigger a nested fallback.
528 jest.advanceTimersByTime(500);
@@ -503,8 +537,10 @@ describe('ReactFlightDOMBrowser', () => {
537
538 const theError = new Error('Game over');
539 // Let's *fail* loading games.
506 - await act(() => {
507 - rejectGames(theError);
540 + await serverAct(async () => {
541 + await act(() => {
542 + rejectGames(theError);
543 + });
544 });
545
546 const gamesExpectedValue = __DEV__
@@ -522,8 +558,10 @@ describe('ReactFlightDOMBrowser', () => {
558 reportedErrors = [];
559
560 // We can now show the sidebar.
525 - await act(() => {
526 - resolvePhotos();
561 + await serverAct(async () => {
562 + await act(() => {
563 + resolvePhotos();
564 + });
565 });
566 expect(container.innerHTML).toBe(
567 '<div>:name::avatar:</div>' +
@@ -533,8 +571,10 @@ describe('ReactFlightDOMBrowser', () => {
571 );
572
573 // Show everything.
536 - await act(() => {
537 - resolvePosts();
574 + await serverAct(async () => {
575 + await act(() => {
576 + resolvePosts();
577 + });
578 });
579 expect(container.innerHTML).toBe(
580 '<div>:name::avatar:</div>' +
@@ -596,9 +636,8 @@ describe('ReactFlightDOMBrowser', () => {
636 rootContent: <ProfileContent />,
637 };
638
599 - const stream = ReactServerDOMServer.renderToReadableStream(
600 - model,
601 - webpackMap,
639 + const stream = await serverAct(() =>
640 + ReactServerDOMServer.renderToReadableStream(model, webpackMap),
641 );
642
643 const reader = stream.getReader();
@@ -621,7 +660,7 @@ describe('ReactFlightDOMBrowser', () => {
660 // Advance time enough to trigger a nested fallback.
661 jest.advanceTimersByTime(500);
662
624 - await act(() => {});
663 + await serverAct(() => {});
664
665 expect(flightResponse).toContain('(loading everything)');
666 expect(flightResponse).toContain('(loading sidebar)');
@@ -629,25 +668,25 @@ describe('ReactFlightDOMBrowser', () => {
668 expect(flightResponse).not.toContain(':friends:');
669 expect(flightResponse).not.toContain(':name:');
670
632 - await act(() => {
671 + await serverAct(() => {
672 resolveFriends();
673 });
674
675 expect(flightResponse).toContain(':friends:');
676
638 - await act(() => {
677 + await serverAct(() => {
678 resolveName();
679 });
680
681 expect(flightResponse).toContain(':name:');
682
644 - await act(() => {
683 + await serverAct(() => {
684 resolvePhotos();
685 });
686
687 expect(flightResponse).toContain(':photos:');
688
650 - await act(() => {
689 + await serverAct(() => {
690 resolvePosts();
691 });
692
@@ -695,19 +734,21 @@ describe('ReactFlightDOMBrowser', () => {
734 }
735
736 const controller = new AbortController();
698 - const stream = ReactServerDOMServer.renderToReadableStream(
699 - <div>
700 - <InfiniteSuspend />
701 - </div>,
702 - webpackMap,
703 - {
704 - signal: controller.signal,
705 - onError(x) {
706 - const message = typeof x === 'string' ? x : x.message;
707 - reportedErrors.push(x);
708 - return __DEV__ ? 'a dev digest' : `digest("${message}")`;
737 + const stream = await serverAct(() =>
738 + ReactServerDOMServer.renderToReadableStream(
739 + <div>
740 + <InfiniteSuspend />
741 + </div>,
742 + webpackMap,
743 + {
744 + signal: controller.signal,
745 + onError(x) {
746 + const message = typeof x === 'string' ? x : x.message;
747 + reportedErrors.push(x);
748 + return __DEV__ ? 'a dev digest' : `digest("${message}")`;
749 + },
750 },
710 - },
751 + ),
752 );
753 const response = ReactServerDOMClient.createFromReadableStream(stream);
754
@@ -751,17 +792,20 @@ describe('ReactFlightDOMBrowser', () => {
792 const root = ReactDOMClient.createRoot(container);
793
794 await expect(async () => {
754 - const stream = ReactServerDOMServer.renderToReadableStream(
755 - <>
756 - <Parent>{Array(6).fill(<div>no key</div>)}</Parent>
757 - <ParentModule.Parent>
758 - {Array(6).fill(<div>no key</div>)}
759 - </ParentModule.Parent>
760 - </>,
761 - webpackMap,
795 + const stream = await serverAct(() =>
796 + ReactServerDOMServer.renderToReadableStream(
797 + <>
798 + <Parent>{Array(6).fill(<div>no key</div>)}</Parent>
799 + <ParentModule.Parent>
800 + {Array(6).fill(<div>no key</div>)}
801 + </ParentModule.Parent>
802 + </>,
803 + webpackMap,
804 + ),
805 );
806 const result =
807 await ReactServerDOMClient.createFromReadableStream(stream);
808 +
809 await act(() => {
810 root.render(result);
811 });
@@ -777,7 +821,9 @@ describe('ReactFlightDOMBrowser', () => {
821 );
822 }
823
780 - const stream = ReactServerDOMServer.renderToReadableStream(<Server />);
824 + const stream = await serverAct(() =>
825 + ReactServerDOMServer.renderToReadableStream(<Server />),
826 + );
827 const response = ReactServerDOMClient.createFromReadableStream(stream);
828
829 function Client() {
@@ -816,7 +862,9 @@ describe('ReactFlightDOMBrowser', () => {
862 );
863 }
864
819 - const stream = ReactServerDOMServer.renderToReadableStream(<Parent />);
865 + const stream = await serverAct(() =>
866 + ReactServerDOMServer.renderToReadableStream(<Parent />),
867 + );
868 const response = ReactServerDOMClient.createFromReadableStream(stream);
869
870 function Client() {
@@ -853,15 +901,13 @@ describe('ReactFlightDOMBrowser', () => {
901 }
902
903 const reportedErrors = [];
856 - const stream = ReactServerDOMServer.renderToReadableStream(
857 - <Server />,
858 - webpackMap,
859 - {
904 + const stream = await serverAct(() =>
905 + ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap, {
906 onError(x) {
907 reportedErrors.push(x);
908 return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
909 },
864 - },
910 + }),
911 );
912 const response = ReactServerDOMClient.createFromReadableStream(stream);
913
@@ -912,7 +958,9 @@ describe('ReactFlightDOMBrowser', () => {
958 return ReactServer.use(thenable);
959 }
960
915 - const stream = ReactServerDOMServer.renderToReadableStream(<Server />);
961 + const stream = await serverAct(() =>
962 + ReactServerDOMServer.renderToReadableStream(<Server />),
963 + );
964 const response = ReactServerDOMClient.createFromReadableStream(stream);
965
966 function Client() {
@@ -947,7 +995,9 @@ describe('ReactFlightDOMBrowser', () => {
995
996 // Because the thenable resolves synchronously, we should be able to finish
997 // rendering synchronously, with no fallback.
950 - const stream = ReactServerDOMServer.renderToReadableStream(<Server />);
998 + const stream = await serverAct(() =>
999 + ReactServerDOMServer.renderToReadableStream(<Server />),
1000 + );
1001 const response = ReactServerDOMClient.createFromReadableStream(stream);
1002
1003 function Client() {
@@ -988,9 +1038,11 @@ describe('ReactFlightDOMBrowser', () => {
1038
1039 const boundFn = ServerModuleA.greet.bind(null, ServerModuleB.upper);
1040
991 - const stream = ReactServerDOMServer.renderToReadableStream(
992 - <ClientRef action={boundFn} />,
993 - webpackMap,
1041 + const stream = await serverAct(() =>
1042 + ReactServerDOMServer.renderToReadableStream(
1043 + <ClientRef action={boundFn} />,
1044 + webpackMap,
1045 + ),
1046 );
1047
1048 const response = ReactServerDOMClient.createFromReadableStream(stream, {
@@ -1035,9 +1087,11 @@ describe('ReactFlightDOMBrowser', () => {
1087 });
1088 const ClientRef = clientExports(Client);
1089
1038 - const stream = ReactServerDOMServer.renderToReadableStream(
1039 - <ClientRef action={ServerModule.split} />,
1040 - webpackMap,
1090 + const stream = await serverAct(() =>
1091 + ReactServerDOMServer.renderToReadableStream(
1092 + <ClientRef action={ServerModule.split} />,
1093 + webpackMap,
1094 + ),
1095 );
1096
1097 const response = ReactServerDOMClient.createFromReadableStream(stream, {
@@ -1100,9 +1154,11 @@ describe('ReactFlightDOMBrowser', () => {
1154
1155 const ClientRef = clientExports(Client);
1156
1103 - const stream = ReactServerDOMServer.renderToReadableStream(
1104 - <ClientRef action={ServerModuleA.greet} />,
1105 - webpackMap,
1157 + const stream = await serverAct(() =>
1158 + ReactServerDOMServer.renderToReadableStream(
1159 + <ClientRef action={ServerModuleA.greet} />,
1160 + webpackMap,
1161 + ),
1162 );
1163
1164 const response = ReactServerDOMClient.createFromReadableStream(stream, {
@@ -1140,9 +1196,11 @@ describe('ReactFlightDOMBrowser', () => {
1196 });
1197 const ClientRef = clientExports(Client);
1198
1143 - const stream = ReactServerDOMServer.renderToReadableStream(
1144 - <ClientRef action={greet.bind(null, 'Hello').bind(null, 'World')} />,
1145 - webpackMap,
1199 + const stream = await serverAct(() =>
1200 + ReactServerDOMServer.renderToReadableStream(
1201 + <ClientRef action={greet.bind(null, 'Hello').bind(null, 'World')} />,
1202 + webpackMap,
1203 + ),
1204 );
1205
1206 const response = ReactServerDOMClient.createFromReadableStream(stream, {
@@ -1178,26 +1236,29 @@ describe('ReactFlightDOMBrowser', () => {
1236 }
1237
1238 async function send(text) {
1181 - return Promise.reject(new Error(`Error for ${text}`));
1239 + throw new Error(`Error for ${text}`);
1240 }
1241
1242 const ServerModule = serverExports({send});
1243 const ClientRef = clientExports(Client);
1244
1187 - const stream = ReactServerDOMServer.renderToReadableStream(
1188 - <ClientRef action={ServerModule.send} />,
1189 - webpackMap,
1245 + const stream = await serverAct(() =>
1246 + ReactServerDOMServer.renderToReadableStream(
1247 + <ClientRef action={ServerModule.send} />,
1248 + webpackMap,
1249 + ),
1250 );
1191 -
1251 const response = ReactServerDOMClient.createFromReadableStream(stream, {
1252 async callServer(actionId, args) {
1253 const body = await ReactServerDOMClient.encodeReply(args);
1254 + const result = callServer(actionId, body);
1255 + // Flight doesn't attach error handlers early enough. we suppress the warning
1256 + // by putting a dummy catch on the result here
1257 + result.catch(() => {});
1258 return ReactServerDOMClient.createFromReadableStream(
1196 - ReactServerDOMServer.renderToReadableStream(
1197 - callServer(actionId, body),
1198 - null,
1199 - {onError: error => 'test-error-digest'},
1200 - ),
1259 + ReactServerDOMServer.renderToReadableStream(result, null, {
1260 + onError: error => 'test-error-digest',
1261 + }),
1262 );
1263 },
1264 });
@@ -1212,17 +1273,17 @@ describe('ReactFlightDOMBrowser', () => {
1273 root.render(<App />);
1274 });
1275
1215 - if (__DEV__) {
1216 - await expect(actionProxy('test')).rejects.toThrow('Error for test');
1217 - } else {
1218 - let thrownError;
1276 + let thrownError;
1277
1220 - try {
1221 - await actionProxy('test');
1222 - } catch (error) {
1223 - thrownError = error;
1224 - }
1278 + try {
1279 + await serverAct(() => actionProxy('test'));
1280 + } catch (error) {
1281 + thrownError = error;
1282 + }
1283
1284 + if (__DEV__) {
1285 + expect(thrownError).toEqual(new Error('Error for test'));
1286 + } else {
1287 expect(thrownError).toEqual(
1288 new Error(
1289 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.',
@@ -1253,9 +1314,14 @@ describe('ReactFlightDOMBrowser', () => {
1314 });
1315 const ClientRef = clientExports(Client);
1316
1256 - const stream = ReactServerDOMServer.renderToReadableStream(
1257 - <ClientRef action1={ServerModule.greet} action2={ServerModule.greet2} />,
1258 - webpackMap,
1317 + const stream = await serverAct(() =>
1318 + ReactServerDOMServer.renderToReadableStream(
1319 + <ClientRef
1320 + action1={ServerModule.greet}
1321 + action2={ServerModule.greet2}
1322 + />,
1323 + webpackMap,
1324 + ),
1325 );
1326
1327 const response = ReactServerDOMClient.createFromReadableStream(stream, {
@@ -1298,9 +1364,11 @@ describe('ReactFlightDOMBrowser', () => {
1364 );
1365
1366 // Send the action to the client
1301 - const stream = ReactServerDOMServer.renderToReadableStream(
1302 - {action: serverModule.action},
1303 - webpackMap,
1367 + const stream = await serverAct(() =>
1368 + ReactServerDOMServer.renderToReadableStream(
1369 + {action: serverModule.action},
1370 + webpackMap,
1371 + ),
1372 );
1373 const response =
1374 await ReactServerDOMClient.createFromReadableStream(stream);
@@ -1340,9 +1408,11 @@ describe('ReactFlightDOMBrowser', () => {
1408 return <ClientComponent />;
1409 }
1410
1343 - const stream = ReactServerDOMServer.renderToReadableStream(
1344 - <ServerComponent />,
1345 - webpackMap,
1411 + const stream = await serverAct(() =>
1412 + ReactServerDOMServer.renderToReadableStream(
1413 + <ServerComponent />,
1414 + webpackMap,
1415 + ),
1416 );
1417
1418 let response = null;
@@ -1406,9 +1476,11 @@ describe('ReactFlightDOMBrowser', () => {
1476 return <ClientComponent />;
1477 }
1478
1409 - const stream = ReactServerDOMServer.renderToReadableStream(
1410 - <ServerComponent />,
1411 - webpackMap,
1479 + const stream = await serverAct(() =>
1480 + ReactServerDOMServer.renderToReadableStream(
1481 + <ServerComponent />,
1482 + webpackMap,
1483 + ),
1484 );
1485
1486 let response = null;
@@ -1427,15 +1499,11 @@ describe('ReactFlightDOMBrowser', () => {
1499 );
1500 }
1501
1430 - // pausing to let Flight runtime tick. This is a test only artifact of the fact that
1431 - // we aren't operating separate module graphs for flight and fiber. In a real app
1432 - // each would have their own dispatcher and there would be no cross dispatching.
1433 - await 1;
1434 -
1435 - let fizzStream;
1502 + let fizzPromise;
1503 await act(async () => {
1437 - fizzStream = await ReactDOMFizzServer.renderToReadableStream(<App />);
1504 + fizzPromise = ReactDOMFizzServer.renderToReadableStream(<App />);
1505 });
1506 + const fizzStream = await fizzPromise;
1507
1508 const decoder = new TextDecoder();
1509 const reader = fizzStream.getReader();
@@ -1464,16 +1532,18 @@ describe('ReactFlightDOMBrowser', () => {
1532
1533 let postponed = null;
1534
1467 - const stream = ReactServerDOMServer.renderToReadableStream(
1468 - <Suspense fallback="Loading...">
1469 - <Server />
1470 - </Suspense>,
1471 - null,
1472 - {
1473 - onPostpone(reason) {
1474 - postponed = reason;
1535 + const stream = await serverAct(() =>
1536 + ReactServerDOMServer.renderToReadableStream(
1537 + <Suspense fallback="Loading...">
1538 + <Server />
1539 + </Suspense>,
1540 + null,
1541 + {
1542 + onPostpone(reason) {
1543 + postponed = reason;
1544 + },
1545 },
1476 - },
1546 + ),
1547 );
1548 const response = ReactServerDOMClient.createFromReadableStream(stream);
1549
@@ -1512,18 +1582,20 @@ describe('ReactFlightDOMBrowser', () => {
1582 return 'Done';
1583 }
1584 const errors = [];
1515 - const stream = await ReactServerDOMServer.renderToReadableStream(
1516 - <div>
1517 - <Suspense fallback={<div>Loading</div>}>
1518 - <Wait />
1519 - </Suspense>
1520 - </div>,
1521 - null,
1522 - {
1523 - onError(x) {
1524 - errors.push(x.message);
1585 + const stream = await serverAct(() =>
1586 + ReactServerDOMServer.renderToReadableStream(
1587 + <div>
1588 + <Suspense fallback={<div>Loading</div>}>
1589 + <Wait />
1590 + </Suspense>
1591 + </div>,
1592 + null,
1593 + {
1594 + onError(x) {
1595 + errors.push(x.message);
1596 + },
1597 },
1526 - },
1598 + ),
1599 );
1600
1601 expect(rendered).toBe(false);
@@ -1559,20 +1631,22 @@ describe('ReactFlightDOMBrowser', () => {
1631 let error = null;
1632
1633 const controller = new AbortController();
1562 - const stream = ReactServerDOMServer.renderToReadableStream(
1563 - <Suspense fallback="Loading...">
1564 - <Server />
1565 - </Suspense>,
1566 - null,
1567 - {
1568 - onError(x) {
1569 - error = x;
1570 - },
1571 - onPostpone(reason) {
1572 - postponed = reason;
1634 + const stream = await serverAct(() =>
1635 + ReactServerDOMServer.renderToReadableStream(
1636 + <Suspense fallback="Loading...">
1637 + <Server />
1638 + </Suspense>,
1639 + null,
1640 + {
1641 + onError(x) {
1642 + error = x;
1643 + },
1644 + onPostpone(reason) {
1645 + postponed = reason;
1646 + },
1647 + signal: controller.signal,
1648 },
1574 - signal: controller.signal,
1575 - },
1649 + ),
1650 );
1651
1652 try {
@@ -1589,7 +1663,7 @@ describe('ReactFlightDOMBrowser', () => {
1663
1664 const container = document.createElement('div');
1665 const root = ReactDOMClient.createRoot(container);
1592 - await act(async () => {
1666 + await act(() => {
1667 root.render(
1668 <div>
1669 Shell: <Client />
@@ -1643,27 +1717,33 @@ describe('ReactFlightDOMBrowser', () => {
1717 controller2 = c;
1718 },
1719 });
1646 - const rscStream = ReactServerDOMServer.renderToReadableStream(
1647 - {
1648 - s1,
1649 - s2,
1650 - },
1651 - {},
1652 - {
1653 - onError(x) {
1654 - errors.push(x);
1655 - return x;
1720 + const rscStream = await serverAct(() =>
1721 + ReactServerDOMServer.renderToReadableStream(
1722 + {
1723 + s1,
1724 + s2,
1725 },
1657 - },
1726 + {},
1727 + {
1728 + onError(x) {
1729 + errors.push(x);
1730 + return x;
1731 + },
1732 + },
1733 + ),
1734 );
1735 const result = await ReactServerDOMClient.createFromReadableStream(
1736 passThrough(rscStream),
1737 );
1738 +
1739 const reader1 = result.s1.getReader();
1740 const reader2 = result.s2.getReader();
1741
1665 - controller1.enqueue({hello: 'world'});
1666 - controller2.enqueue({hi: 'there'});
1742 + await serverAct(() => {
1743 + controller1.enqueue({hello: 'world'});
1744 + controller2.enqueue({hi: 'there'});
1745 + });
1746 +
1747 expect(await reader1.read()).toEqual({
1748 value: {hello: 'world'},
1749 done: false,
@@ -1673,10 +1753,11 @@ describe('ReactFlightDOMBrowser', () => {
1753 done: false,
1754 });
1755
1676 - controller1.enqueue('text1');
1677 - controller2.enqueue('text2');
1678 - controller1.close();
1679 - controller2.error('rejected');
1756 + await serverAct(async () => {
1757 + controller1.enqueue('text1');
1758 + controller2.enqueue('text2');
1759 + controller1.close();
1760 + });
1761
1762 expect(await reader1.read()).toEqual({
1763 value: 'text1',
@@ -1690,6 +1771,9 @@ describe('ReactFlightDOMBrowser', () => {
1771 value: 'text2',
1772 done: false,
1773 });
1774 + await serverAct(async () => {
1775 + controller2.error('rejected');
1776 + });
1777 let error = null;
1778 try {
1779 await reader2.read();
@@ -1713,14 +1797,16 @@ describe('ReactFlightDOMBrowser', () => {
1797 },
1798 });
1799 let loggedReason;
1716 - const rscStream = ReactServerDOMServer.renderToReadableStream(
1717 - s,
1718 - {},
1719 - {
1720 - onError(reason) {
1721 - loggedReason = reason;
1800 + const rscStream = await serverAct(() =>
1801 + ReactServerDOMServer.renderToReadableStream(
1802 + s,
1803 + {},
1804 + {
1805 + onError(reason) {
1806 + loggedReason = reason;
1807 + },
1808 },
1723 - },
1809 + ),
1810 );
1811 const reader = rscStream.getReader();
1812 controller.enqueue('hi');
@@ -1745,21 +1831,25 @@ describe('ReactFlightDOMBrowser', () => {
1831 cancelReason = r;
1832 },
1833 });
1748 - const rscStream = ReactServerDOMServer.renderToReadableStream(
1749 - s,
1750 - {},
1751 - {
1752 - signal: abortController.signal,
1753 - onError(x) {
1754 - errors.push(x);
1755 - return x.message;
1834 +
1835 + const rscStream = await serverAct(() =>
1836 + ReactServerDOMServer.renderToReadableStream(
1837 + s,
1838 + {},
1839 + {
1840 + signal: abortController.signal,
1841 + onError(x) {
1842 + errors.push(x);
1843 + return x.message;
1844 + },
1845 },
1757 - },
1846 + ),
1847 );
1848 const result = await ReactServerDOMClient.createFromReadableStream(
1849 passThrough(rscStream),
1850 );
1851 const reader = result.getReader();
1852 +
1853 controller.enqueue('hi');
1854
1855 await 0;
@@ -1808,18 +1898,20 @@ describe('ReactFlightDOMBrowser', () => {
1898 throw 'F';
1899 })();
1900
1811 - const rscStream = ReactServerDOMServer.renderToReadableStream(
1812 - {
1813 - multiShotIterable,
1814 - singleShotIterator,
1815 - },
1816 - {},
1817 - {
1818 - onError(x) {
1819 - errors.push(x);
1820 - return x;
1901 + const rscStream = await serverAct(() =>
1902 + ReactServerDOMServer.renderToReadableStream(
1903 + {
1904 + multiShotIterable,
1905 + singleShotIterator,
1906 },
1822 - },
1907 + {},
1908 + {
1909 + onError(x) {
1910 + errors.push(x);
1911 + return x;
1912 + },
1913 + },
1914 + ),
1915 );
1916 const result = await ReactServerDOMClient.createFromReadableStream(
1917 passThrough(rscStream),
@@ -1840,7 +1932,9 @@ describe('ReactFlightDOMBrowser', () => {
1932 done: false,
1933 });
1934
1843 - await resolve();
1935 + await serverAct(() => {
1936 + resolve();
1937 + });
1938
1939 expect(await iterator1.next()).toEqual({
1940 value: {hi: 'B'},
@@ -1914,16 +2008,21 @@ describe('ReactFlightDOMBrowser', () => {
2008 yield 'c';
2009 })();
2010 let loggedReason;
1917 - const rscStream = ReactServerDOMServer.renderToReadableStream(
1918 - iterator,
1919 - {},
1920 - {
1921 - onError(reason) {
1922 - loggedReason = reason;
2011 +
2012 + const rscStream = await serverAct(() =>
2013 + ReactServerDOMServer.renderToReadableStream(
2014 + iterator,
2015 + {},
2016 + {
2017 + onError(reason) {
2018 + loggedReason = reason;
2019 + },
2020 },
1924 - },
2021 + ),
2022 );
2023 +
2024 const reader = rscStream.getReader();
2025 +
2026 const reason = new Error('aborted');
2027 reader.cancel(reason);
2028 await resolve();
@@ -1949,16 +2048,18 @@ describe('ReactFlightDOMBrowser', () => {
2048 }
2049 yield 'c';
2050 })();
1952 - const rscStream = ReactServerDOMServer.renderToReadableStream(
1953 - iterator,
1954 - {},
1955 - {
1956 - signal: abortController.signal,
1957 - onError(x) {
1958 - errors.push(x);
1959 - return x.message;
2051 + const rscStream = await serverAct(() =>
2052 + ReactServerDOMServer.renderToReadableStream(
2053 + iterator,
2054 + {},
2055 + {
2056 + signal: abortController.signal,
2057 + onError(x) {
2058 + errors.push(x);
2059 + return x.message;
2060 + },
2061 },
1961 - },
2062 + ),
2063 );
2064 const result = await ReactServerDOMClient.createFromReadableStream(
2065 passThrough(rscStream),
@@ -1967,7 +2068,9 @@ describe('ReactFlightDOMBrowser', () => {
2068 const reason = new Error('aborted');
2069 abortController.abort(reason);
2070
1970 - await resolve();
2071 + await serverAct(() => {
2072 + resolve();
2073 + });
2074
2075 // We should be able to read the part we already emitted before the abort
2076 expect(await result.next()).toEqual({
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+54 -33
@@ -9,13 +9,11 @@
9
10 'use strict';
11
12 +import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
13 +
14 global.ReadableStream =
15 require('web-streams-polyfill/ponyfill/es6').ReadableStream;
16
15 -// Don't wait before processing work on the server.
16 -// TODO: we can replace this with FlightServer.act().
17 -global.setImmediate = cb => cb();
18 -
17 let clientExports;
18 let webpackMap;
19 let webpackModules;
@@ -26,11 +24,17 @@ let ReactServerDOMServer;
24 let ReactServerDOMClient;
25 let Stream;
26 let use;
27 +let ReactServerScheduler;
28 +let reactServerAct;
29
30 describe('ReactFlightDOMNode', () => {
31 beforeEach(() => {
32 jest.resetModules();
33
34 + ReactServerScheduler = require('scheduler');
35 + patchSetImmediate(ReactServerScheduler);
36 + reactServerAct = require('internal-test-utils').act;
37 +
38 // Simulate the condition resolution
39 jest.mock('react', () => require('react/react.react-server'));
40 jest.mock('react-server-dom-webpack/server', () =>
@@ -58,6 +62,17 @@ describe('ReactFlightDOMNode', () => {
62 use = React.use;
63 });
64
65 + async function serverAct(callback) {
66 + let maybePromise;
67 + await reactServerAct(() => {
68 + maybePromise = callback();
69 + if (maybePromise && typeof maybePromise.catch === 'function') {
70 + maybePromise.catch(() => {});
71 + }
72 + });
73 + return maybePromise;
74 + }
75 +
76 function readResult(stream) {
77 return new Promise((resolve, reject) => {
78 let buffer = '';
@@ -110,9 +125,8 @@ describe('ReactFlightDOMNode', () => {
125 return <ClientComponentOnTheClient />;
126 }
127
113 - const stream = ReactServerDOMServer.renderToPipeableStream(
114 - <App />,
115 - webpackMap,
128 + const stream = await serverAct(() =>
129 + ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
130 );
131 const readable = new Stream.PassThrough();
132 let response;
@@ -128,8 +142,8 @@ describe('ReactFlightDOMNode', () => {
142 return use(response);
143 }
144
131 - const ssrStream = await ReactDOMServer.renderToPipeableStream(
132 - <ClientRoot />,
145 + const ssrStream = await serverAct(() =>
146 + ReactDOMServer.renderToPipeableStream(<ClientRoot />),
147 );
148 const result = await readResult(ssrStream);
149 expect(result).toEqual(
@@ -140,9 +154,11 @@ describe('ReactFlightDOMNode', () => {
154 it('should encode long string in a compact format', async () => {
155 const testString = '"\n\t'.repeat(500) + '🙃';
156
143 - const stream = ReactServerDOMServer.renderToPipeableStream({
144 - text: testString,
145 - });
157 + const stream = await serverAct(() =>
158 + ReactServerDOMServer.renderToPipeableStream({
159 + text: testString,
160 + }),
161 + );
162
163 const readable = new Stream.PassThrough();
164
@@ -187,7 +203,9 @@ describe('ReactFlightDOMNode', () => {
203 new BigUint64Array(buffer, 0),
204 new DataView(buffer, 3),
205 ];
190 - const stream = ReactServerDOMServer.renderToPipeableStream(buffers);
206 + const stream = await serverAct(() =>
207 + ReactServerDOMServer.renderToPipeableStream(buffers),
208 + );
209 const readable = new Stream.PassThrough();
210 const promise = ReactServerDOMClient.createFromNodeStream(readable, {
211 moduleMap: {},
@@ -232,9 +250,8 @@ describe('ReactFlightDOMNode', () => {
250 return <ClientComponentOnTheClient />;
251 }
252
235 - const stream = ReactServerDOMServer.renderToPipeableStream(
236 - <App />,
237 - webpackMap,
253 + const stream = await serverAct(() =>
254 + ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
255 );
256 const readable = new Stream.PassThrough();
257 let response;
@@ -253,8 +270,8 @@ describe('ReactFlightDOMNode', () => {
270 return use(response);
271 }
272
256 - const ssrStream = await ReactDOMServer.renderToPipeableStream(
257 - <ClientRoot />,
273 + const ssrStream = await serverAct(() =>
274 + ReactDOMServer.renderToPipeableStream(<ClientRoot />),
275 );
276 const result = await readResult(ssrStream);
277 expect(result).toEqual(
@@ -275,14 +292,16 @@ describe('ReactFlightDOMNode', () => {
292 },
293 });
294
278 - const rscStream = ReactServerDOMServer.renderToPipeableStream(
279 - s,
280 - {},
281 - {
282 - onError(error) {
283 - return error.message;
295 + const rscStream = await serverAct(() =>
296 + ReactServerDOMServer.renderToPipeableStream(
297 + s,
298 + {},
299 + {
300 + onError(error) {
301 + return error.message;
302 + },
303 },
285 - },
304 + ),
305 );
306
307 const writable = new Stream.PassThrough();
@@ -317,15 +336,17 @@ describe('ReactFlightDOMNode', () => {
336 cancelReason = r;
337 },
338 });
320 - const rscStream = ReactServerDOMServer.renderToPipeableStream(
321 - s,
322 - {},
323 - {
324 - onError(x) {
325 - errors.push(x);
326 - return x.message;
339 + const rscStream = await serverAct(() =>
340 + ReactServerDOMServer.renderToPipeableStream(
341 + s,
342 + {},
343 + {
344 + onError(x) {
345 + errors.push(x);
346 + return x.message;
347 + },
348 },
328 - },
349 + ),
350 );
351
352 const readable = new Stream.PassThrough();
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js
+32 -12
@@ -9,6 +9,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;
@@ -20,10 +22,17 @@ let webpackServerMap;
22 let React;
23 let ReactServerDOMServer;
24 let ReactServerDOMClient;
25 +let ReactServerScheduler;
26 +let reactServerAct;
27
28 describe('ReactFlightDOMReply', () => {
29 beforeEach(() => {
30 jest.resetModules();
31 +
32 + ReactServerScheduler = require('scheduler');
33 + patchMessageChannel(ReactServerScheduler);
34 + reactServerAct = require('internal-test-utils').act;
35 +
36 // Simulate the condition resolution
37 jest.mock('react', () => require('react/react.react-server'));
38 jest.mock('react-server-dom-webpack/server', () =>
@@ -39,6 +48,17 @@ describe('ReactFlightDOMReply', () => {
48 ReactServerDOMClient = require('react-server-dom-webpack/client');
49 });
50
51 + async function serverAct(callback) {
52 + let maybePromise;
53 + await reactServerAct(() => {
54 + maybePromise = callback();
55 + if (maybePromise && typeof maybePromise.catch === 'function') {
56 + maybePromise.catch(() => {});
57 + }
58 + });
59 + return maybePromise;
60 + }
61 +
62 // This method should exist on File but is not implemented in JSDOM
63 async function arrayBuffer(file) {
64 return new Promise((resolve, reject) => {
@@ -369,12 +389,10 @@ describe('ReactFlightDOMReply', () => {
389 webpackServerMap,
390 {temporaryReferences: temporaryReferencesServer},
391 );
372 - const stream = ReactServerDOMServer.renderToReadableStream(
373 - serverPayload,
374 - null,
375 - {
392 + const stream = await serverAct(() =>
393 + ReactServerDOMServer.renderToReadableStream(serverPayload, null, {
394 temporaryReferences: temporaryReferencesServer,
377 - },
395 + }),
396 );
397 const response = await ReactServerDOMClient.createFromReadableStream(
398 stream,
@@ -408,13 +426,15 @@ describe('ReactFlightDOMReply', () => {
426 webpackServerMap,
427 {temporaryReferences: temporaryReferencesServer},
428 );
411 - const stream = ReactServerDOMServer.renderToReadableStream(
412 - {
413 - root: serverPayload,
414 - obj: serverPayload.obj,
415 - },
416 - null,
417 - {temporaryReferences: temporaryReferencesServer},
429 + const stream = await serverAct(() =>
430 + ReactServerDOMServer.renderToReadableStream(
431 + {
432 + root: serverPayload,
433 + obj: serverPayload.obj,
434 + },
435 + null,
436 + {temporaryReferences: temporaryReferencesServer},
437 + ),
438 );
439 const response = await ReactServerDOMClient.createFromReadableStream(
440 stream,
packages/react-server/src/ReactFlightServer.js
+7 -2
@@ -3506,9 +3506,14 @@ function enqueueFlush(request: Request): void {
3506 // happen when we start flowing again
3507 request.destination !== null
3508 ) {
3509 - const destination = request.destination;
3509 request.flushScheduled = true;
3511 - scheduleWork(() => flushCompletedChunks(request, destination));
3510 + scheduleWork(() => {
3511 + request.flushScheduled = false;
3512 + const destination = request.destination;
3513 + if (destination) {
3514 + flushCompletedChunks(request, destination);
3515 + }
3516 + });
3517 }
3518 }
3519
packages/react-server/src/ReactServerStreamConfigBrowser.js
+11 -1
@@ -13,8 +13,18 @@ export type PrecomputedChunk = Uint8Array;
13 export opaque type Chunk = Uint8Array;
14 export type BinaryChunk = Uint8Array;
15
16 +const channel = new MessageChannel();
17 +const taskQueue = [];
18 +channel.port1.onmessage = () => {
19 + const task = taskQueue.shift();
20 + if (task) {
21 + task();
22 + }
23 +};
24 +
25 export function scheduleWork(callback: () => void) {
17 - callback();
26 + taskQueue.push(callback);
27 + channel.port2.postMessage(null);
28 }
29
30 export function flushBuffered(destination: Destination) {
packages/react-server/src/ReactServerStreamConfigBun.js
+1 -1
@@ -22,7 +22,7 @@ export opaque type Chunk = string;
22 export type BinaryChunk = $ArrayBufferView;
23
24 export function scheduleWork(callback: () => void) {
25 - callback();
25 + setTimeout(callback, 0);
26 }
27
28 export function flushBuffered(destination: Destination) {
packages/react-server/src/forks/ReactServerStreamConfig.dom-fb-experimental.js
+15 -1
@@ -42,8 +42,22 @@ export interface Destination {
42 onError(error: mixed): void;
43 }
44
45 +function handleErrorInNextTick(error: any) {
46 + setTimeout(() => {
47 + throw error;
48 + });
49 +}
50 +
51 +const LocalPromise = Promise;
52 +
53 +/**
54 + * Since this environment doesn't have a way to schedule tasks from JS we schedule
55 + * using a microtask instead. This isn't necessarily ideal since we would like to give
56 + * other IO a chance to run before performing work typically but it's the best we can
57 + * do in this environment
58 + */
59 export function scheduleWork(callback: () => void) {
46 - callback();
60 + LocalPromise.resolve().then(callback).catch(handleErrorInNextTick);
61 }
62
63 export function beginWriting(destination: Destination) {
packages/react/src/__tests__/ReactMismatchedVersions-test.js
+5
@@ -9,6 +9,8 @@
9
10 'use strict';
11
12 +import {patchMessageChannel} from '../../../../scripts/jest/patchMessageChannel';
13 +
14 describe('ReactMismatchedVersions-test', () => {
15 // Polyfills for test environment
16 global.ReadableStream =
@@ -20,6 +22,9 @@ describe('ReactMismatchedVersions-test', () => {
22
23 beforeEach(() => {
24 jest.resetModules();
25 +
26 + patchMessageChannel();
27 +
28 jest.mock('react', () => {
29 const actualReact = jest.requireActual('react');
30 return {
scripts/jest/patchMessageChannel.js new
+30
@@ -0,0 +1,30 @@
1 +'use strict';
2 +
3 +export function patchMessageChannel(Scheduler) {
4 + global.MessageChannel = class {
5 + constructor() {
6 + const port1 = {
7 + onmesssage: () => {},
8 + };
9 +
10 + this.port1 = port1;
11 +
12 + this.port2 = {
13 + postMessage(msg) {
14 + if (Scheduler) {
15 + Scheduler.unstable_scheduleCallback(
16 + Scheduler.unstable_NormalPriority,
17 + () => {
18 + port1.onmessage(msg);
19 + }
20 + );
21 + } else {
22 + throw new Error(
23 + 'MessageChannel patch was used without providing a Scheduler implementation. This is useful for tests that require this class to exist but are not actually utilizing the MessageChannel class. However it appears some test is trying to use this class so you should pass a Scheduler implemenation to the patch method'
24 + );
25 + }
26 + },
27 + };
28 + }
29 + };
30 +}
scripts/jest/patchSetImmediate.js new
+13
@@ -0,0 +1,13 @@
1 +'use strict';
2 +
3 +export function patchSetImmediate(Scheduler) {
4 + if (!Scheduler) {
5 + throw new Error(
6 + 'setImmediate patch was used without providing a Scheduler implementation. If you are patching setImmediate you must provide a Scheduler.'
7 + );
8 + }
9 +
10 + global.setImmediate = cb => {
11 + Scheduler.unstable_scheduleCallback(Scheduler.unstable_NormalPriority, cb);
12 + };
13 +}
scripts/jest/setupEnvironment.js
-13
@@ -21,19 +21,6 @@ global.__EXPERIMENTAL__ =
21 global.__VARIANT__ = !!process.env.VARIANT;
22
23 if (typeof window !== 'undefined') {
24 - global.requestIdleCallback = function (callback) {
25 - return setTimeout(() => {
26 - callback({
27 - timeRemaining() {
28 - return Infinity;
29 - },
30 - });
31 - });
32 - };
33 -
34 - global.cancelIdleCallback = function (callbackID) {
35 - clearTimeout(callbackID);
36 - };
24 } else {
25 global.AbortController =
26 require('abortcontroller-polyfill/dist/cjs-ponyfill').AbortController;