main
js 3,391 lines 92.5 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
13 import {Readable} from 'stream';
14
15 // Polyfills for test environment
16 global.ReadableStream =
17 require('web-streams-polyfill/ponyfill/es6').ReadableStream;
18 global.TextEncoder = require('util').TextEncoder;
19 global.TextDecoder = require('util').TextDecoder;
20
21 let act;
22 let serverAct;
23 let use;
24 let clientExports;
25 let clientExportsESM;
26 let clientModuleError;
27 let webpackMap;
28 let Stream;
29 let FlightReact;
30 let React;
31 let FlightReactDOM;
32 let ReactDOMClient;
33 let ReactServerDOMServer;
34 let ReactServerDOMStaticServer;
35 let ReactServerDOMClient;
36 let ReactDOMFizzServer;
37 let Suspense;
38 let ErrorBoundary;
39 let JSDOM;
40 let assertConsoleErrorDev;
41
42 describe('ReactFlightDOM', () => {
43 beforeEach(() => {
44 // For this first reset we are going to load the dom-node version of react-server-dom-webpack/server
45 // This can be thought of as essentially being the React Server Components scope with react-server
46 // condition
47 jest.resetModules();
48
49 // Some of the tests pollute the head.
50 document.head.innerHTML = '';
51
52 JSDOM = require('jsdom').JSDOM;
53
54 patchSetImmediate();
55 serverAct = require('internal-test-utils').serverAct;
56
57 // Simulate the condition resolution
58 jest.mock('react', () => require('react/react.react-server'));
59 FlightReact = require('react');
60 FlightReactDOM = require('react-dom');
61
62 jest.mock('react-server-dom-webpack/server', () =>
63 require('react-server-dom-unbundled/server.node'),
64 );
65 jest.mock('react-server-dom-webpack/static', () =>
66 require('react-server-dom-unbundled/static.node'),
67 );
68 const WebpackMock = require('./utils/WebpackMock');
69 clientExports = WebpackMock.clientExports;
70 clientExportsESM = WebpackMock.clientExportsESM;
71 clientModuleError = WebpackMock.clientModuleError;
72 webpackMap = WebpackMock.webpackMap;
73
74 ReactServerDOMServer = require('react-server-dom-webpack/server');
75 ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
76
77 // This reset is to load modules for the SSR/Browser scope.
78 jest.unmock('react-server-dom-webpack/server');
79 __unmockReact();
80 jest.resetModules();
81 act = require('internal-test-utils').act;
82 assertConsoleErrorDev =
83 require('internal-test-utils').assertConsoleErrorDev;
84 Stream = require('stream');
85 React = require('react');
86 use = React.use;
87 Suspense = React.Suspense;
88 ReactDOMClient = require('react-dom/client');
89 ReactDOMFizzServer = require('react-dom/server.node');
90 jest.mock('react-server-dom-webpack/client', () =>
91 require('react-server-dom-webpack/client.browser'),
92 );
93 ReactServerDOMClient = require('react-server-dom-webpack/client');
94
95 ErrorBoundary = class extends React.Component {
96 state = {hasError: false, error: null};
97 static getDerivedStateFromError(error) {
98 return {
99 hasError: true,
100 error,
101 };
102 }
103 render() {
104 if (this.state.hasError) {
105 return this.props.fallback(this.state.error);
106 }
107 return this.props.children;
108 }
109 };
110 });
111
112 async function readInto(
113 container: Document | HTMLElement,
114 stream: ReadableStream,
115 ) {
116 const reader = stream.getReader();
117 const decoder = new TextDecoder();
118 let content = '';
119 while (true) {
120 const {done, value} = await reader.read();
121 if (done) {
122 content += decoder.decode();
123 break;
124 }
125 content += decoder.decode(value, {stream: true});
126 }
127 if (container.nodeType === 9 /* DOCUMENT */) {
128 const doc = new JSDOM(content).window.document;
129 container.documentElement.innerHTML = doc.documentElement.innerHTML;
130 while (container.documentElement.attributes.length > 0) {
131 container.documentElement.removeAttribute(
132 container.documentElement.attributes[0].name,
133 );
134 }
135 const attrs = doc.documentElement.attributes;
136 for (let i = 0; i < attrs.length; i++) {
137 container.documentElement.setAttribute(attrs[i].name, attrs[i].value);
138 }
139 } else {
140 container.innerHTML = content;
141 }
142 }
143
144 function getTestStream() {
145 const writable = new Stream.PassThrough();
146 const readable = new ReadableStream({
147 start(controller) {
148 writable.on('data', chunk => {
149 controller.enqueue(chunk);
150 });
151 writable.on('end', () => {
152 controller.close();
153 });
154 },
155 });
156 return {
157 readable,
158 writable,
159 };
160 }
161
162 function createUnclosingStream(
163 stream: ReadableStream<Uint8Array>,
164 ): ReadableStream<Uint8Array> {
165 const reader = stream.getReader();
166
167 const s = new ReadableStream({
168 async pull(controller) {
169 const {done, value} = await reader.read();
170 if (!done) {
171 controller.enqueue(value);
172 }
173 },
174 });
175
176 return s;
177 }
178
179 const theInfinitePromise = new Promise(() => {});
180 function InfiniteSuspend() {
181 throw theInfinitePromise;
182 }
183
184 function getMeaningfulChildren(element) {
185 const children = [];
186 let node = element.firstChild;
187 while (node) {
188 if (node.nodeType === 1) {
189 if (
190 // some tags are ambiguous and might be hidden because they look like non-meaningful children
191 // so we have a global override where if this data attribute is included we also include the node
192 node.hasAttribute('data-meaningful') ||
193 (node.tagName === 'SCRIPT' &&
194 node.hasAttribute('src') &&
195 node.hasAttribute('async')) ||
196 (node.tagName !== 'SCRIPT' &&
197 node.tagName !== 'TEMPLATE' &&
198 node.tagName !== 'template' &&
199 !node.hasAttribute('hidden') &&
200 !node.hasAttribute('aria-hidden') &&
201 // Ignore the render blocking expect
202 (node.getAttribute('rel') !== 'expect' ||
203 node.getAttribute('blocking') !== 'render'))
204 ) {
205 const props = {};
206 const attributes = node.attributes;
207 for (let i = 0; i < attributes.length; i++) {
208 if (
209 attributes[i].name === 'id' &&
210 attributes[i].value.includes(':')
211 ) {
212 // We assume this is a React added ID that's a non-visual implementation detail.
213 continue;
214 }
215 props[attributes[i].name] = attributes[i].value;
216 }
217 props.children = getMeaningfulChildren(node);
218 children.push(React.createElement(node.tagName.toLowerCase(), props));
219 }
220 } else if (node.nodeType === 3) {
221 children.push(node.data);
222 }
223 node = node.nextSibling;
224 }
225 return children.length === 0
226 ? undefined
227 : children.length === 1
228 ? children[0]
229 : children;
230 }
231
232 it('should resolve HTML using Node streams', async () => {
233 function Text({children}) {
234 return <span>{children}</span>;
235 }
236 function HTML() {
237 return (
238 <div>
239 <Text>hello</Text>
240 <Text>world</Text>
241 </div>
242 );
243 }
244
245 function App() {
246 const model = {
247 html: <HTML />,
248 };
249 return model;
250 }
251
252 const {writable, readable} = getTestStream();
253 const {pipe} = await serverAct(() =>
254 ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
255 );
256 pipe(writable);
257 const response = ReactServerDOMClient.createFromReadableStream(readable);
258 const model = await response;
259 expect(model).toEqual({
260 html: (
261 <div>
262 <span>hello</span>
263 <span>world</span>
264 </div>
265 ),
266 });
267 });
268
269 it('should resolve the root', async () => {
270 // Model
271 function Text({children}) {
272 return <span>{children}</span>;
273 }
274 function HTML() {
275 return (
276 <div>
277 <Text>hello</Text>
278 <Text>world</Text>
279 </div>
280 );
281 }
282 function RootModel() {
283 return {
284 html: <HTML />,
285 };
286 }
287
288 // View
289 function Message({response}) {
290 return <section>{use(response).html}</section>;
291 }
292 function App({response}) {
293 return (
294 <Suspense fallback={<h1>Loading...</h1>}>
295 <Message response={response} />
296 </Suspense>
297 );
298 }
299
300 const {writable, readable} = getTestStream();
301 const {pipe} = await serverAct(() =>
302 ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
303 );
304 pipe(writable);
305 const response = ReactServerDOMClient.createFromReadableStream(readable);
306
307 const container = document.createElement('div');
308 const root = ReactDOMClient.createRoot(container);
309 await act(() => {
310 root.render(<App response={response} />);
311 });
312 expect(container.innerHTML).toBe(
313 '<section><div><span>hello</span><span>world</span></div></section>',
314 );
315 });
316
317 it('should not get confused by $', async () => {
318 // Model
319 function RootModel() {
320 return {text: '$1'};
321 }
322
323 // View
324 function Message({response}) {
325 return <p>{use(response).text}</p>;
326 }
327 function App({response}) {
328 return (
329 <Suspense fallback={<h1>Loading...</h1>}>
330 <Message response={response} />
331 </Suspense>
332 );
333 }
334
335 const {writable, readable} = getTestStream();
336 const {pipe} = await serverAct(() =>
337 ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
338 );
339 pipe(writable);
340 const response = ReactServerDOMClient.createFromReadableStream(readable);
341
342 const container = document.createElement('div');
343 const root = ReactDOMClient.createRoot(container);
344 await act(() => {
345 root.render(<App response={response} />);
346 });
347 expect(container.innerHTML).toBe('<p>$1</p>');
348 });
349
350 it('should not get confused by @', async () => {
351 // Model
352 function RootModel() {
353 return {text: '@div'};
354 }
355
356 // View
357 function Message({response}) {
358 return <p>{use(response).text}</p>;
359 }
360 function App({response}) {
361 return (
362 <Suspense fallback={<h1>Loading...</h1>}>
363 <Message response={response} />
364 </Suspense>
365 );
366 }
367
368 const {writable, readable} = getTestStream();
369 const {pipe} = await serverAct(() =>
370 ReactServerDOMServer.renderToPipeableStream(<RootModel />, webpackMap),
371 );
372 pipe(writable);
373 const response = ReactServerDOMClient.createFromReadableStream(readable);
374
375 const container = document.createElement('div');
376 const root = ReactDOMClient.createRoot(container);
377 await act(() => {
378 root.render(<App response={response} />);
379 });
380 expect(container.innerHTML).toBe('<p>@div</p>');
381 });
382
383 it('should be able to esm compat test module references', async () => {
384 const ESMCompatModule = {
385 __esModule: true,
386 default: function ({greeting}) {
387 return greeting + ' World';
388 },
389 hi: 'Hello',
390 };
391
392 function Print({response}) {
393 return <p>{use(response)}</p>;
394 }
395
396 function App({response}) {
397 return (
398 <Suspense fallback={<h1>Loading...</h1>}>
399 <Print response={response} />
400 </Suspense>
401 );
402 }
403
404 function interopWebpack(obj) {
405 // Basically what Webpack's ESM interop feature testing does.
406 if (typeof obj === 'object' && obj.__esModule) {
407 return obj;
408 }
409 return Object.assign({default: obj}, obj);
410 }
411
412 const {default: Component, hi} = interopWebpack(
413 clientExports(ESMCompatModule),
414 );
415
416 const {writable, readable} = getTestStream();
417 const {pipe} = await serverAct(() =>
418 ReactServerDOMServer.renderToPipeableStream(
419 <Component greeting={hi} />,
420 webpackMap,
421 ),
422 );
423 pipe(writable);
424 const response = ReactServerDOMClient.createFromReadableStream(readable);
425
426 const container = document.createElement('div');
427 const root = ReactDOMClient.createRoot(container);
428 await act(() => {
429 root.render(<App response={response} />);
430 });
431 expect(container.innerHTML).toBe('<p>Hello World</p>');
432 });
433
434 it('should be able to render a named component export', async () => {
435 const Module = {
436 Component: function ({greeting}) {
437 return greeting + ' World';
438 },
439 };
440
441 function Print({response}) {
442 return <p>{use(response)}</p>;
443 }
444
445 function App({response}) {
446 return (
447 <Suspense fallback={<h1>Loading...</h1>}>
448 <Print response={response} />
449 </Suspense>
450 );
451 }
452
453 const {Component} = clientExports(Module);
454
455 const {writable, readable} = getTestStream();
456 const {pipe} = await serverAct(() =>
457 ReactServerDOMServer.renderToPipeableStream(
458 <Component greeting={'Hello'} />,
459 webpackMap,
460 ),
461 );
462 pipe(writable);
463 const response = ReactServerDOMClient.createFromReadableStream(readable);
464
465 const container = document.createElement('div');
466 const root = ReactDOMClient.createRoot(container);
467 await act(() => {
468 root.render(<App response={response} />);
469 });
470 expect(container.innerHTML).toBe('<p>Hello World</p>');
471 });
472
473 it('should be able to render a module split named component export', async () => {
474 const Module = {
475 // This gets split into a separate module from the original one.
476 split: function ({greeting}) {
477 return greeting + ' World';
478 },
479 };
480
481 function Print({response}) {
482 return <p>{use(response)}</p>;
483 }
484
485 function App({response}) {
486 return (
487 <Suspense fallback={<h1>Loading...</h1>}>
488 <Print response={response} />
489 </Suspense>
490 );
491 }
492
493 const {split: Component} = clientExports(Module);
494
495 const {writable, readable} = getTestStream();
496 const {pipe} = await serverAct(() =>
497 ReactServerDOMServer.renderToPipeableStream(
498 <Component greeting={'Hello'} />,
499 webpackMap,
500 ),
501 );
502 pipe(writable);
503 const response = ReactServerDOMClient.createFromReadableStream(readable);
504
505 const container = document.createElement('div');
506 const root = ReactDOMClient.createRoot(container);
507 await act(() => {
508 root.render(<App response={response} />);
509 });
510 expect(container.innerHTML).toBe('<p>Hello World</p>');
511 });
512
513 it('should unwrap async module references', async () => {
514 const AsyncModule = Promise.resolve(function AsyncModule({text}) {
515 return 'Async: ' + text;
516 });
517
518 const AsyncModule2 = Promise.resolve({
519 exportName: 'Module',
520 });
521
522 function Print({response}) {
523 return <p>{use(response)}</p>;
524 }
525
526 function App({response}) {
527 return (
528 <Suspense fallback={<h1>Loading...</h1>}>
529 <Print response={response} />
530 </Suspense>
531 );
532 }
533
534 const AsyncModuleRef = await clientExports(AsyncModule);
535 const AsyncModuleRef2 = await clientExports(AsyncModule2);
536
537 const {writable, readable} = getTestStream();
538 const {pipe} = await serverAct(() =>
539 ReactServerDOMServer.renderToPipeableStream(
540 <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
541 webpackMap,
542 ),
543 );
544 pipe(writable);
545 const response = ReactServerDOMClient.createFromReadableStream(readable);
546
547 const container = document.createElement('div');
548 const root = ReactDOMClient.createRoot(container);
549 await act(() => {
550 root.render(<App response={response} />);
551 });
552 expect(container.innerHTML).toBe('<p>Async: Module</p>');
553 });
554
555 it('should unwrap async module references using use', async () => {
556 const AsyncModule = Promise.resolve('Async Text');
557
558 function Print({response}) {
559 return use(response);
560 }
561
562 function App({response}) {
563 return (
564 <Suspense fallback={<h1>Loading...</h1>}>
565 <Print response={response} />
566 </Suspense>
567 );
568 }
569
570 const AsyncModuleRef = clientExports(AsyncModule);
571
572 function ServerComponent() {
573 const text = FlightReact.use(AsyncModuleRef);
574 return <p>{text}</p>;
575 }
576
577 const {writable, readable} = getTestStream();
578 const {pipe} = await serverAct(() =>
579 ReactServerDOMServer.renderToPipeableStream(
580 <ServerComponent />,
581 webpackMap,
582 ),
583 );
584 pipe(writable);
585 const response = ReactServerDOMClient.createFromReadableStream(readable);
586
587 const container = document.createElement('div');
588 const root = ReactDOMClient.createRoot(container);
589 await act(() => {
590 root.render(<App response={response} />);
591 });
592 expect(container.innerHTML).toBe('<p>Async Text</p>');
593 });
594
595 it('should unwrap async ESM module references', async () => {
596 const AsyncModule = Promise.resolve(function AsyncModule({text}) {
597 return 'Async: ' + text;
598 });
599
600 const AsyncModule2 = Promise.resolve({
601 exportName: 'Module',
602 });
603
604 function Print({response}) {
605 return <p>{use(response)}</p>;
606 }
607
608 function App({response}) {
609 return (
610 <Suspense fallback={<h1>Loading...</h1>}>
611 <Print response={response} />
612 </Suspense>
613 );
614 }
615
616 const AsyncModuleRef = await clientExportsESM(AsyncModule);
617 const AsyncModuleRef2 = await clientExportsESM(AsyncModule2);
618
619 const {writable, readable} = getTestStream();
620 const {pipe} = await serverAct(() =>
621 ReactServerDOMServer.renderToPipeableStream(
622 <AsyncModuleRef text={AsyncModuleRef2.exportName} />,
623 webpackMap,
624 ),
625 );
626 pipe(writable);
627 const response = ReactServerDOMClient.createFromReadableStream(readable);
628
629 const container = document.createElement('div');
630 const root = ReactDOMClient.createRoot(container);
631 await act(() => {
632 root.render(<App response={response} />);
633 });
634 expect(container.innerHTML).toBe('<p>Async: Module</p>');
635 });
636
637 it('should error when a bundler uses async ESM modules with createClientModuleProxy', async () => {
638 const AsyncModule = Promise.resolve(function AsyncModule() {
639 return 'This should not be rendered';
640 });
641
642 function Print({response}) {
643 return <p>{use(response)}</p>;
644 }
645
646 function App({response}) {
647 return (
648 <ErrorBoundary
649 fallback={error => (
650 <p>
651 {__DEV__ ? error.message + ' + ' : null}
652 {error.digest}
653 </p>
654 )}>
655 <Suspense fallback={<h1>Loading...</h1>}>
656 <Print response={response} />
657 </Suspense>
658 </ErrorBoundary>
659 );
660 }
661
662 const AsyncModuleRef = await clientExportsESM(AsyncModule, {
663 forceClientModuleProxy: true,
664 });
665
666 const {writable, readable} = getTestStream();
667 const {pipe} = await serverAct(() =>
668 ReactServerDOMServer.renderToPipeableStream(
669 <AsyncModuleRef />,
670 webpackMap,
671 {
672 onError(error) {
673 return __DEV__ ? 'a dev digest' : `digest(${error.message})`;
674 },
675 },
676 ),
677 );
678 pipe(writable);
679 const response = ReactServerDOMClient.createFromReadableStream(readable);
680
681 const container = document.createElement('div');
682 const root = ReactDOMClient.createRoot(container);
683 await act(() => {
684 root.render(<App response={response} />);
685 });
686
687 const errorMessage = `The module "${Object.keys(webpackMap).at(0)}" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.`;
688
689 expect(container.innerHTML).toBe(
690 __DEV__
691 ? `<p>${errorMessage} + a dev digest</p>`
692 : `<p>digest(${errorMessage})</p>`,
693 );
694 });
695
696 it('should be able to import a name called "then"', async () => {
697 const thenExports = {
698 then: function then() {
699 return 'and then';
700 },
701 };
702
703 function Print({response}) {
704 return <p>{use(response)}</p>;
705 }
706
707 function App({response}) {
708 return (
709 <Suspense fallback={<h1>Loading...</h1>}>
710 <Print response={response} />
711 </Suspense>
712 );
713 }
714
715 const ThenRef = clientExports(thenExports).then;
716
717 const {writable, readable} = getTestStream();
718 const {pipe} = await serverAct(() =>
719 ReactServerDOMServer.renderToPipeableStream(<ThenRef />, webpackMap),
720 );
721 pipe(writable);
722 const response = ReactServerDOMClient.createFromReadableStream(readable);
723
724 const container = document.createElement('div');
725 const root = ReactDOMClient.createRoot(container);
726 await act(() => {
727 root.render(<App response={response} />);
728 });
729 expect(container.innerHTML).toBe('<p>and then</p>');
730 });
731
732 it('throws when accessing a member below the client exports', () => {
733 const ClientModule = clientExports({
734 Component: {deep: 'thing'},
735 });
736 function dotting() {
737 return ClientModule.Component.deep;
738 }
739 expect(dotting).toThrow(
740 'Cannot access Component.deep on the server. ' +
741 'You cannot dot into a client module from a server component. ' +
742 'You can only pass the imported name through.',
743 );
744 });
745
746 it('throws when await a client module prop of client exports', async () => {
747 const ClientModule = clientExports({
748 Component: {deep: 'thing'},
749 });
750 async function awaitExport() {
751 const mod = await ClientModule;
752 return await Promise.resolve(mod.Component);
753 }
754 await expect(awaitExport()).rejects.toThrow(
755 `Cannot await or return from a thenable. ` +
756 `You cannot await a client module from a server component.`,
757 );
758 });
759
760 it('throws when accessing a symbol prop from client exports', () => {
761 const symbol = Symbol('test');
762 const ClientModule = clientExports({
763 Component: {deep: 'thing'},
764 });
765 function read() {
766 return ClientModule[symbol];
767 }
768 expect(read).toThrow(
769 'Cannot read Symbol exports. ' +
770 'Only named exports are supported on a client module imported on the server.',
771 );
772 });
773
774 it('does not throw when toString:ing client exports', () => {
775 const ClientModule = clientExports({
776 Component: {deep: 'thing'},
777 });
778 expect(Object.prototype.toString.call(ClientModule)).toBe(
779 '[object Object]',
780 );
781 expect(Object.prototype.toString.call(ClientModule.Component)).toBe(
782 '[object Function]',
783 );
784 });
785
786 it('does not throw when React inspects any deep props', () => {
787 const ClientModule = clientExports({
788 Component: function () {},
789 });
790 <ClientModule.Component key="this adds instrumentation" />;
791 });
792
793 it('does not throw when accessing a Context.Provider from client exports', () => {
794 const Context = React.createContext();
795 const ClientModule = clientExports({
796 Context,
797 });
798 function dotting() {
799 return ClientModule.Context.Provider;
800 }
801 expect(dotting).not.toThrow();
802 });
803
804 it('can render a client Context.Provider from a server component', async () => {
805 // Create a context in a client module
806 const TestContext = React.createContext('default');
807 const ClientModule = clientExports({
808 TestContext,
809 });
810
811 // Client component that reads context
812 function ClientConsumer() {
813 const value = React.useContext(TestContext);
814 return <span>{value}</span>;
815 }
816 const {ClientConsumer: ClientConsumerRef} = clientExports({ClientConsumer});
817
818 function Print({response}) {
819 return use(response);
820 }
821
822 function App({response}) {
823 return (
824 <Suspense fallback={<h1>Loading...</h1>}>
825 <Print response={response} />
826 </Suspense>
827 );
828 }
829
830 // Server component that provides context
831 function ServerApp() {
832 return (
833 <ClientModule.TestContext.Provider value="from-server">
834 <div>
835 <ClientConsumerRef />
836 </div>
837 </ClientModule.TestContext.Provider>
838 );
839 }
840
841 const {writable, readable} = getTestStream();
842 const {pipe} = await serverAct(() =>
843 ReactServerDOMServer.renderToPipeableStream(<ServerApp />, webpackMap),
844 );
845 pipe(writable);
846 const response = ReactServerDOMClient.createFromReadableStream(readable);
847
848 const container = document.createElement('div');
849 const root = ReactDOMClient.createRoot(container);
850 await act(() => {
851 root.render(<App response={response} />);
852 });
853
854 expect(container.innerHTML).toBe('<div><span>from-server</span></div>');
855 });
856
857 it('should progressively reveal server components', async () => {
858 let reportedErrors = [];
859
860 // Client Components
861
862 function MyErrorBoundary({children}) {
863 return (
864 <ErrorBoundary
865 fallback={e => (
866 <p>
867 {__DEV__ ? e.message + ' + ' : null}
868 {e.digest}
869 </p>
870 )}>
871 {children}
872 </ErrorBoundary>
873 );
874 }
875
876 // Model
877 function Text({children}) {
878 return children;
879 }
880
881 function makeDelayedText() {
882 let _resolve, _reject;
883 let promise = new Promise((resolve, reject) => {
884 _resolve = () => {
885 promise = null;
886 resolve();
887 };
888 _reject = e => {
889 promise = null;
890 reject(e);
891 };
892 });
893 async function DelayedText({children}) {
894 await promise;
895 return <Text>{children}</Text>;
896 }
897 return [DelayedText, _resolve, _reject];
898 }
899
900 const [Friends, resolveFriends] = makeDelayedText();
901 const [Name, resolveName] = makeDelayedText();
902 const [Posts, resolvePosts] = makeDelayedText();
903 const [Photos, resolvePhotos] = makeDelayedText();
904 const [Games, , rejectGames] = makeDelayedText();
905
906 // View
907 function ProfileDetails({avatar}) {
908 return (
909 <div>
910 <Name>:name:</Name>
911 {avatar}
912 </div>
913 );
914 }
915 function ProfileSidebar({friends}) {
916 return (
917 <div>
918 <Photos>:photos:</Photos>
919 {friends}
920 </div>
921 );
922 }
923 function ProfilePosts({posts}) {
924 return <div>{posts}</div>;
925 }
926 function ProfileGames({games}) {
927 return <div>{games}</div>;
928 }
929
930 const MyErrorBoundaryClient = clientExports(MyErrorBoundary);
931
932 function ProfileContent() {
933 return (
934 <>
935 <ProfileDetails avatar={<Text>:avatar:</Text>} />
936 <Suspense fallback={<p>(loading sidebar)</p>}>
937 <ProfileSidebar friends={<Friends>:friends:</Friends>} />
938 </Suspense>
939 <Suspense fallback={<p>(loading posts)</p>}>
940 <ProfilePosts posts={<Posts>:posts:</Posts>} />
941 </Suspense>
942 <MyErrorBoundaryClient>
943 <Suspense fallback={<p>(loading games)</p>}>
944 <ProfileGames games={<Games>:games:</Games>} />
945 </Suspense>
946 </MyErrorBoundaryClient>
947 </>
948 );
949 }
950
951 const model = {
952 rootContent: <ProfileContent />,
953 };
954
955 function ProfilePage({response}) {
956 return use(response).rootContent;
957 }
958
959 const {writable, readable} = getTestStream();
960 const {pipe} = await serverAct(() =>
961 ReactServerDOMServer.renderToPipeableStream(model, webpackMap, {
962 onError(x) {
963 reportedErrors.push(x);
964 return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
965 },
966 }),
967 );
968 pipe(writable);
969 const response = ReactServerDOMClient.createFromReadableStream(readable);
970
971 const container = document.createElement('div');
972 const root = ReactDOMClient.createRoot(container);
973 await act(() => {
974 root.render(
975 <Suspense fallback={<p>(loading)</p>}>
976 <ProfilePage response={response} />
977 </Suspense>,
978 );
979 });
980 expect(container.innerHTML).toBe('<p>(loading)</p>');
981
982 // This isn't enough to show anything.
983 await serverAct(async () => {
984 await act(() => {
985 resolveFriends();
986 });
987 });
988 expect(container.innerHTML).toBe('<p>(loading)</p>');
989
990 // We can now show the details. Sidebar and posts are still loading.
991 await serverAct(async () => {
992 await act(() => {
993 resolveName();
994 });
995 });
996 // Advance time enough to trigger a nested fallback.
997 await act(() => {
998 jest.advanceTimersByTime(500);
999 });
1000 expect(container.innerHTML).toBe(
1001 '<div>:name::avatar:</div>' +
1002 '<p>(loading sidebar)</p>' +
1003 '<p>(loading posts)</p>' +
1004 '<p>(loading games)</p>',
1005 );
1006
1007 expect(reportedErrors).toEqual([]);
1008
1009 const theError = new Error('Game over');
1010 // Let's *fail* loading games.
1011 await serverAct(async () => {
1012 await act(async () => {
1013 await rejectGames(theError);
1014 await 'the inner async function';
1015 });
1016 });
1017 const expectedGamesValue = __DEV__
1018 ? '<p>Game over + a dev digest</p>'
1019 : '<p>digest("Game over")</p>';
1020 expect(container.innerHTML).toBe(
1021 '<div>:name::avatar:</div>' +
1022 '<p>(loading sidebar)</p>' +
1023 '<p>(loading posts)</p>' +
1024 expectedGamesValue,
1025 );
1026
1027 expect(reportedErrors).toEqual([theError]);
1028 reportedErrors = [];
1029
1030 // We can now show the sidebar.
1031 await serverAct(async () => {
1032 await act(async () => {
1033 await resolvePhotos();
1034 await 'the inner async function';
1035 });
1036 });
1037 expect(container.innerHTML).toBe(
1038 '<div>:name::avatar:</div>' +
1039 '<div>:photos::friends:</div>' +
1040 '<p>(loading posts)</p>' +
1041 expectedGamesValue,
1042 );
1043
1044 // Show everything.
1045 await serverAct(async () => {
1046 await act(async () => {
1047 await resolvePosts();
1048 await 'the inner async function';
1049 });
1050 });
1051 expect(container.innerHTML).toBe(
1052 '<div>:name::avatar:</div>' +
1053 '<div>:photos::friends:</div>' +
1054 '<div>:posts:</div>' +
1055 expectedGamesValue,
1056 );
1057
1058 expect(reportedErrors).toEqual([]);
1059 });
1060
1061 it('should handle streaming async server components', async () => {
1062 const reportedErrors = [];
1063
1064 const Row = async ({current, next}) => {
1065 const chunk = await next;
1066
1067 if (chunk.done) {
1068 return chunk.value;
1069 }
1070
1071 return (
1072 <Suspense fallback={chunk.value}>
1073 <Row current={chunk.value} next={chunk.next} />
1074 </Suspense>
1075 );
1076 };
1077
1078 function createResolvablePromise() {
1079 let _resolve, _reject;
1080
1081 const promise = new Promise((resolve, reject) => {
1082 _resolve = resolve;
1083 _reject = reject;
1084 });
1085
1086 return {promise, resolve: _resolve, reject: _reject};
1087 }
1088
1089 function createSuspendedChunk(initialValue) {
1090 const {promise, resolve, reject} = createResolvablePromise();
1091
1092 return {
1093 row: (
1094 <Suspense fallback={initialValue}>
1095 <Row current={initialValue} next={promise} />
1096 </Suspense>
1097 ),
1098 resolve,
1099 reject,
1100 };
1101 }
1102
1103 function makeDelayedText() {
1104 const {promise, resolve, reject} = createResolvablePromise();
1105 async function DelayedText() {
1106 const data = await promise;
1107 return <div>{data}</div>;
1108 }
1109 return [DelayedText, resolve, reject];
1110 }
1111
1112 const [Posts, resolvePostsData] = makeDelayedText();
1113 const [Photos, resolvePhotosData] = makeDelayedText();
1114 const suspendedChunk = createSuspendedChunk(<p>loading</p>);
1115 const {writable, readable} = getTestStream();
1116 const {pipe} = await serverAct(() =>
1117 ReactServerDOMServer.renderToPipeableStream(
1118 suspendedChunk.row,
1119 webpackMap,
1120 {
1121 onError(error) {
1122 reportedErrors.push(error);
1123 },
1124 },
1125 ),
1126 );
1127 pipe(writable);
1128 const response = ReactServerDOMClient.createFromReadableStream(readable);
1129 const container = document.createElement('div');
1130 const root = ReactDOMClient.createRoot(container);
1131
1132 function ClientRoot() {
1133 return use(response);
1134 }
1135
1136 await act(() => {
1137 root.render(<ClientRoot />);
1138 });
1139
1140 expect(container.innerHTML).toBe('<p>loading</p>');
1141
1142 const donePromise = createResolvablePromise();
1143
1144 const value = (
1145 <Suspense fallback={<p>loading posts and photos</p>}>
1146 <Posts />
1147 <Photos />
1148 </Suspense>
1149 );
1150
1151 await serverAct(async () => {
1152 await act(async () => {
1153 suspendedChunk.resolve({value, done: false, next: donePromise.promise});
1154 donePromise.resolve({value, done: true});
1155 });
1156 });
1157
1158 expect(container.innerHTML).toBe('<p>loading posts and photos</p>');
1159
1160 await serverAct(async () => {
1161 await act(async () => {
1162 await resolvePostsData('posts');
1163 await resolvePhotosData('photos');
1164 });
1165 });
1166
1167 expect(container.innerHTML).toBe('<div>posts</div><div>photos</div>');
1168 expect(reportedErrors).toEqual([]);
1169 });
1170
1171 it('should preserve state of client components on refetch', async () => {
1172 // Client
1173
1174 function Page({response}) {
1175 return use(response);
1176 }
1177
1178 function Input() {
1179 return <input />;
1180 }
1181
1182 const InputClient = clientExports(Input);
1183
1184 // Server
1185
1186 function App({color}) {
1187 // Verify both DOM and Client children.
1188 return (
1189 <div style={{color}}>
1190 <input />
1191 <InputClient />
1192 </div>
1193 );
1194 }
1195
1196 const container = document.createElement('div');
1197 const root = ReactDOMClient.createRoot(container);
1198
1199 const stream1 = getTestStream();
1200 const {pipe} = await serverAct(() =>
1201 ReactServerDOMServer.renderToPipeableStream(
1202 <App color="red" />,
1203 webpackMap,
1204 ),
1205 );
1206 pipe(stream1.writable);
1207 const response1 = ReactServerDOMClient.createFromReadableStream(
1208 stream1.readable,
1209 );
1210 await act(() => {
1211 root.render(
1212 <Suspense fallback={<p>(loading)</p>}>
1213 <Page response={response1} />
1214 </Suspense>,
1215 );
1216 });
1217 expect(container.children.length).toBe(1);
1218 expect(container.children[0].tagName).toBe('DIV');
1219 expect(container.children[0].style.color).toBe('red');
1220
1221 // Change the DOM state for both inputs.
1222 const inputA = container.children[0].children[0];
1223 expect(inputA.tagName).toBe('INPUT');
1224 inputA.value = 'hello';
1225 const inputB = container.children[0].children[1];
1226 expect(inputB.tagName).toBe('INPUT');
1227 inputB.value = 'goodbye';
1228
1229 const stream2 = getTestStream();
1230 const {pipe: pipe2} = await serverAct(() =>
1231 ReactServerDOMServer.renderToPipeableStream(
1232 <App color="blue" />,
1233 webpackMap,
1234 ),
1235 );
1236 pipe2(stream2.writable);
1237 const response2 = ReactServerDOMClient.createFromReadableStream(
1238 stream2.readable,
1239 );
1240 await act(() => {
1241 root.render(
1242 <Suspense fallback={<p>(loading)</p>}>
1243 <Page response={response2} />
1244 </Suspense>,
1245 );
1246 });
1247 expect(container.children.length).toBe(1);
1248 expect(container.children[0].tagName).toBe('DIV');
1249 expect(container.children[0].style.color).toBe('blue');
1250
1251 // Verify we didn't destroy the DOM for either input.
1252 expect(inputA === container.children[0].children[0]).toBe(true);
1253 expect(inputA.tagName).toBe('INPUT');
1254 expect(inputA.value).toBe('hello');
1255 expect(inputB === container.children[0].children[1]).toBe(true);
1256 expect(inputB.tagName).toBe('INPUT');
1257 expect(inputB.value).toBe('goodbye');
1258 });
1259
1260 it('should be able to complete after aborting and throw the reason client-side', async () => {
1261 const reportedErrors = [];
1262
1263 const {writable, readable} = getTestStream();
1264 const {pipe, abort} = await serverAct(() =>
1265 ReactServerDOMServer.renderToPipeableStream(
1266 <div>
1267 <InfiniteSuspend />
1268 </div>,
1269 webpackMap,
1270 {
1271 onError(x) {
1272 reportedErrors.push(x);
1273 const message = typeof x === 'string' ? x : x.message;
1274 return __DEV__ ? 'a dev digest' : `digest("${message}")`;
1275 },
1276 },
1277 ),
1278 );
1279 pipe(writable);
1280 const response = ReactServerDOMClient.createFromReadableStream(readable);
1281
1282 const container = document.createElement('div');
1283 const root = ReactDOMClient.createRoot(container);
1284
1285 function App({res}) {
1286 return use(res);
1287 }
1288
1289 await act(() => {
1290 root.render(
1291 <ErrorBoundary
1292 fallback={e => (
1293 <p>
1294 {__DEV__ ? e.message + ' + ' : null}
1295 {e.digest}
1296 </p>
1297 )}>
1298 <Suspense fallback={<p>(loading)</p>}>
1299 <App res={response} />
1300 </Suspense>
1301 </ErrorBoundary>,
1302 );
1303 });
1304 expect(container.innerHTML).toBe('<p>(loading)</p>');
1305
1306 await act(() => {
1307 abort('for reasons');
1308 });
1309 if (__DEV__) {
1310 expect(container.innerHTML).toBe('<p>for reasons + a dev digest</p>');
1311 } else {
1312 expect(container.innerHTML).toBe('<p>digest("for reasons")</p>');
1313 }
1314
1315 expect(reportedErrors).toEqual(['for reasons']);
1316 });
1317
1318 it('should be able to recover from a direct reference erroring client-side', async () => {
1319 const reportedErrors = [];
1320
1321 const ClientComponent = clientExports(function ({prop}) {
1322 return 'This should never render';
1323 });
1324
1325 const ClientReference = clientModuleError(new Error('module init error'));
1326
1327 const {writable, readable} = getTestStream();
1328 const {pipe} = await serverAct(() =>
1329 ReactServerDOMServer.renderToPipeableStream(
1330 <div>
1331 <ClientComponent prop={ClientReference} />
1332 </div>,
1333 webpackMap,
1334 {
1335 onError(x) {
1336 reportedErrors.push(x);
1337 },
1338 },
1339 ),
1340 );
1341 pipe(writable);
1342 const response = ReactServerDOMClient.createFromReadableStream(readable);
1343
1344 const container = document.createElement('div');
1345 const root = ReactDOMClient.createRoot(container);
1346
1347 function App({res}) {
1348 return use(res);
1349 }
1350
1351 await act(() => {
1352 root.render(
1353 <ErrorBoundary fallback={e => <p>{e.message}</p>}>
1354 <Suspense fallback={<p>(loading)</p>}>
1355 <App res={response} />
1356 </Suspense>
1357 </ErrorBoundary>,
1358 );
1359 });
1360 expect(container.innerHTML).toBe('<p>module init error</p>');
1361
1362 expect(reportedErrors).toEqual([]);
1363 });
1364
1365 it('should be able to recover from a direct reference erroring client-side async', async () => {
1366 const reportedErrors = [];
1367
1368 const ClientComponent = clientExports(function ({prop}) {
1369 return 'This should never render';
1370 });
1371
1372 let rejectPromise;
1373 const ClientReference = await clientExports(
1374 new Promise((resolve, reject) => {
1375 rejectPromise = reject;
1376 }),
1377 );
1378
1379 const {writable, readable} = getTestStream();
1380 const {pipe} = await serverAct(() =>
1381 ReactServerDOMServer.renderToPipeableStream(
1382 <div>
1383 <ClientComponent prop={ClientReference} />
1384 </div>,
1385 webpackMap,
1386 {
1387 onError(x) {
1388 reportedErrors.push(x);
1389 },
1390 },
1391 ),
1392 );
1393 pipe(writable);
1394 const response = ReactServerDOMClient.createFromReadableStream(readable);
1395
1396 const container = document.createElement('div');
1397 const root = ReactDOMClient.createRoot(container);
1398
1399 function App({res}) {
1400 return use(res);
1401 }
1402
1403 await act(() => {
1404 root.render(
1405 <ErrorBoundary fallback={e => <p>{e.message}</p>}>
1406 <Suspense fallback={<p>(loading)</p>}>
1407 <App res={response} />
1408 </Suspense>
1409 </ErrorBoundary>,
1410 );
1411 });
1412
1413 expect(container.innerHTML).toBe('<p>(loading)</p>');
1414
1415 await act(() => {
1416 rejectPromise(new Error('async module init error'));
1417 });
1418
1419 expect(container.innerHTML).toBe('<p>async module init error</p>');
1420
1421 expect(reportedErrors).toEqual([]);
1422 });
1423
1424 it('should not retain stale error reason after reentrant module chunk initialization', async () => {
1425 function MyComponent() {
1426 return <div>hello from client component</div>;
1427 }
1428 const ClientComponent = clientExports(MyComponent);
1429
1430 let resolveAsyncComponent;
1431 async function AsyncComponent() {
1432 await new Promise(r => {
1433 resolveAsyncComponent = r;
1434 });
1435 return null;
1436 }
1437
1438 function ServerComponent() {
1439 return (
1440 <>
1441 <ClientComponent />
1442 <Suspense>
1443 <AsyncComponent />
1444 </Suspense>
1445 </>
1446 );
1447 }
1448
1449 const {writable: flightWritable, readable: flightReadable} =
1450 getTestStream();
1451 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
1452
1453 const {pipe} = await serverAct(() =>
1454 ReactServerDOMServer.renderToPipeableStream(
1455 <ServerComponent />,
1456 webpackMap,
1457 ),
1458 );
1459 pipe(flightWritable);
1460
1461 let response = null;
1462 function getResponse() {
1463 if (response === null) {
1464 response =
1465 ReactServerDOMClient.createFromReadableStream(flightReadable);
1466 }
1467 return response;
1468 }
1469
1470 // Simulate a module that calls captureOwnerStack() during evaluation.
1471 // In Fizz SSR, this causes a reentrant readChunk on the same module chunk.
1472 // The reentrant require throws a TDZ error.
1473 let evaluatingModuleId = null;
1474 const origRequire = global.__webpack_require__;
1475 global.__webpack_require__ = function (id) {
1476 if (id === evaluatingModuleId) {
1477 throw new ReferenceError(
1478 "Cannot access 'MyComponent' before initialization",
1479 );
1480 }
1481 const result = origRequire(id);
1482 if (result === MyComponent) {
1483 evaluatingModuleId = id;
1484 if (__DEV__) {
1485 React.captureOwnerStack();
1486 }
1487 evaluatingModuleId = null;
1488 }
1489 return result;
1490 };
1491
1492 function App() {
1493 return use(getResponse());
1494 }
1495
1496 await serverAct(async () => {
1497 ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
1498 });
1499
1500 global.__webpack_require__ = origRequire;
1501
1502 // Resolve the async component so the Flight stream closes after the client
1503 // module chunk was initialized.
1504 await serverAct(async () => {
1505 resolveAsyncComponent();
1506 });
1507
1508 const container = document.createElement('div');
1509 await readInto(container, fizzReadable);
1510 expect(container.innerHTML).toContain('hello from client component');
1511 });
1512
1513 it('should be able to recover from a direct reference erroring server-side', async () => {
1514 const reportedErrors = [];
1515
1516 const ClientComponent = clientExports(function ({prop}) {
1517 return 'This should never render';
1518 });
1519
1520 // We simulate a bug in the Webpack bundler which causes an error on the server.
1521 for (const id in webpackMap) {
1522 Object.defineProperty(webpackMap, id, {
1523 get: () => {
1524 throw new Error('bug in the bundler');
1525 },
1526 });
1527 }
1528
1529 const {writable, readable} = getTestStream();
1530 const {pipe} = await serverAct(() =>
1531 ReactServerDOMServer.renderToPipeableStream(
1532 <div>
1533 <ClientComponent />
1534 </div>,
1535 webpackMap,
1536 {
1537 onError(x) {
1538 reportedErrors.push(x.message);
1539 return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1540 },
1541 },
1542 ),
1543 );
1544 pipe(writable);
1545
1546 const response = ReactServerDOMClient.createFromReadableStream(readable);
1547
1548 const container = document.createElement('div');
1549 const root = ReactDOMClient.createRoot(container);
1550
1551 function App({res}) {
1552 return use(res);
1553 }
1554
1555 await act(() => {
1556 root.render(
1557 <ErrorBoundary
1558 fallback={e => (
1559 <p>
1560 {__DEV__ ? e.message + ' + ' : null}
1561 {e.digest}
1562 </p>
1563 )}>
1564 <Suspense fallback={<p>(loading)</p>}>
1565 <App res={response} />
1566 </Suspense>
1567 </ErrorBoundary>,
1568 );
1569 });
1570 if (__DEV__) {
1571 expect(container.innerHTML).toBe(
1572 '<p>bug in the bundler + a dev digest</p>',
1573 );
1574 } else {
1575 expect(container.innerHTML).toBe('<p>digest("bug in the bundler")</p>');
1576 }
1577
1578 expect(reportedErrors).toEqual(['bug in the bundler']);
1579 });
1580
1581 it('should pass a Promise through props and be able use() it on the client', async () => {
1582 async function getData() {
1583 return 'async hello';
1584 }
1585
1586 function Component({data}) {
1587 const text = use(data);
1588 return <p>{text}</p>;
1589 }
1590
1591 const ClientComponent = clientExports(Component);
1592
1593 function ServerComponent() {
1594 const data = getData(); // no await here
1595 return <ClientComponent data={data} />;
1596 }
1597
1598 function Print({response}) {
1599 return use(response);
1600 }
1601
1602 function App({response}) {
1603 return (
1604 <Suspense fallback={<h1>Loading...</h1>}>
1605 <Print response={response} />
1606 </Suspense>
1607 );
1608 }
1609
1610 const {writable, readable} = getTestStream();
1611 const {pipe} = await serverAct(() =>
1612 ReactServerDOMServer.renderToPipeableStream(
1613 <ServerComponent />,
1614 webpackMap,
1615 ),
1616 );
1617 pipe(writable);
1618 const response = ReactServerDOMClient.createFromReadableStream(readable);
1619
1620 const container = document.createElement('div');
1621 const root = ReactDOMClient.createRoot(container);
1622 await act(() => {
1623 root.render(<App response={response} />);
1624 });
1625 expect(container.innerHTML).toBe('<p>async hello</p>');
1626 });
1627
1628 it('should throw on the client if a passed promise eventually rejects', async () => {
1629 const reportedErrors = [];
1630 const theError = new Error('Server throw');
1631
1632 async function getData() {
1633 throw theError;
1634 }
1635
1636 function Component({data}) {
1637 const text = use(data);
1638 return <p>{text}</p>;
1639 }
1640
1641 const ClientComponent = clientExports(Component);
1642
1643 function ServerComponent() {
1644 const data = getData(); // no await here
1645 return <ClientComponent data={data} />;
1646 }
1647
1648 function Await({response}) {
1649 return use(response);
1650 }
1651
1652 function App({response}) {
1653 return (
1654 <Suspense fallback={<h1>Loading...</h1>}>
1655 <ErrorBoundary
1656 fallback={e => (
1657 <p>
1658 {__DEV__ ? e.message + ' + ' : null}
1659 {e.digest}
1660 </p>
1661 )}>
1662 <Await response={response} />
1663 </ErrorBoundary>
1664 </Suspense>
1665 );
1666 }
1667
1668 const {writable, readable} = getTestStream();
1669 const {pipe} = await serverAct(() =>
1670 ReactServerDOMServer.renderToPipeableStream(
1671 <ServerComponent />,
1672 webpackMap,
1673 {
1674 onError(x) {
1675 reportedErrors.push(x);
1676 return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1677 },
1678 },
1679 ),
1680 );
1681 pipe(writable);
1682 const response = ReactServerDOMClient.createFromReadableStream(readable);
1683
1684 const container = document.createElement('div');
1685 const root = ReactDOMClient.createRoot(container);
1686 await act(() => {
1687 root.render(<App response={response} />);
1688 });
1689 expect(container.innerHTML).toBe(
1690 __DEV__
1691 ? '<p>Server throw + a dev digest</p>'
1692 : '<p>digest("Server throw")</p>',
1693 );
1694 expect(reportedErrors).toEqual([theError]);
1695 });
1696
1697 it('should support float methods when rendering in Fiber', async () => {
1698 function Component() {
1699 return <p>hello world</p>;
1700 }
1701
1702 const ClientComponent = clientExports(Component);
1703
1704 async function ServerComponent() {
1705 FlightReactDOM.prefetchDNS('d before');
1706 FlightReactDOM.preconnect('c before');
1707 FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1708 FlightReactDOM.preload('l before', {as: 'style'});
1709 FlightReactDOM.preloadModule('lm before');
1710 FlightReactDOM.preloadModule('lm2 before', {
1711 crossOrigin: 'anonymous',
1712 fetchPriority: 'low',
1713 });
1714 FlightReactDOM.preinit('i before', {as: 'script'});
1715 FlightReactDOM.preinitModule('m before');
1716 FlightReactDOM.preinitModule('m2 before', {
1717 crossOrigin: 'anonymous',
1718 fetchPriority: 'high',
1719 });
1720 await 1;
1721 FlightReactDOM.prefetchDNS('d after');
1722 FlightReactDOM.preconnect('c after');
1723 FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1724 FlightReactDOM.preload('l after', {as: 'style'});
1725 FlightReactDOM.preloadModule('lm after');
1726 FlightReactDOM.preloadModule('lm2 after', {
1727 crossOrigin: 'anonymous',
1728 fetchPriority: 'low',
1729 });
1730 FlightReactDOM.preinit('i after', {as: 'script'});
1731 FlightReactDOM.preinitModule('m after');
1732 FlightReactDOM.preinitModule('m2 after', {
1733 crossOrigin: 'anonymous',
1734 fetchPriority: 'high',
1735 });
1736 return <ClientComponent />;
1737 }
1738
1739 const {writable, readable} = getTestStream();
1740 const {pipe} = await serverAct(() =>
1741 ReactServerDOMServer.renderToPipeableStream(
1742 <ServerComponent />,
1743 webpackMap,
1744 ),
1745 );
1746 pipe(writable);
1747
1748 let response = null;
1749 function getResponse() {
1750 if (response === null) {
1751 response = ReactServerDOMClient.createFromReadableStream(readable);
1752 }
1753 return response;
1754 }
1755
1756 function App() {
1757 return getResponse();
1758 }
1759
1760 // We pause to allow the float call after the await point to process before the
1761 // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing
1762 // because the module graphs are not different and the HostDispatcher is shared.
1763 // In a real environment the Fiber and Flight code would each have their own independent
1764 // dispatcher.
1765 // @TODO consider what happens when Server-Components-On-The-Client exist. we probably
1766 // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime
1767 // but we need to make sure that actually makes sense
1768 await 1;
1769
1770 const container = document.createElement('div');
1771 const root = ReactDOMClient.createRoot(container);
1772 await act(() => {
1773 root.render(<App />);
1774 });
1775
1776 expect(getMeaningfulChildren(document)).toEqual(
1777 <html>
1778 <head>
1779 <link rel="dns-prefetch" href="d before" />
1780 <link rel="preconnect" href="c before" />
1781 <link rel="preconnect" href="c2 before" crossorigin="" />
1782 <link rel="preload" as="style" href="l before" />
1783 <link rel="modulepreload" href="lm before" />
1784 <link
1785 rel="modulepreload"
1786 href="lm2 before"
1787 crossorigin=""
1788 fetchpriority="low"
1789 />
1790 <script async="" src="i before" />
1791 <script type="module" async="" src="m before" />
1792 <script
1793 type="module"
1794 async=""
1795 src="m2 before"
1796 crossorigin=""
1797 fetchpriority="high"
1798 />
1799 <link rel="dns-prefetch" href="d after" />
1800 <link rel="preconnect" href="c after" />
1801 <link rel="preconnect" href="c2 after" crossorigin="" />
1802 <link rel="preload" as="style" href="l after" />
1803 <link rel="modulepreload" href="lm after" />
1804 <link
1805 rel="modulepreload"
1806 href="lm2 after"
1807 crossorigin=""
1808 fetchpriority="low"
1809 />
1810 <script async="" src="i after" />
1811 <script type="module" async="" src="m after" />
1812 <script
1813 type="module"
1814 async=""
1815 src="m2 after"
1816 crossorigin=""
1817 fetchpriority="high"
1818 />
1819 </head>
1820 <body />
1821 </html>,
1822 );
1823 expect(getMeaningfulChildren(container)).toEqual(<p>hello world</p>);
1824 });
1825
1826 it('should support float methods when rendering in Fizz', async () => {
1827 function Component() {
1828 return <p>hello world</p>;
1829 }
1830
1831 const ClientComponent = clientExports(Component);
1832
1833 async function ServerComponent() {
1834 FlightReactDOM.prefetchDNS('d before');
1835 FlightReactDOM.preconnect('c before');
1836 FlightReactDOM.preconnect('c2 before', {crossOrigin: 'anonymous'});
1837 FlightReactDOM.preload('l before', {as: 'style'});
1838 FlightReactDOM.preloadModule('lm before');
1839 FlightReactDOM.preloadModule('lm2 before', {
1840 crossOrigin: 'anonymous',
1841 fetchPriority: 'low',
1842 });
1843 FlightReactDOM.preinit('i before', {as: 'script'});
1844 FlightReactDOM.preinitModule('m before');
1845 FlightReactDOM.preinitModule('m2 before', {
1846 crossOrigin: 'anonymous',
1847 fetchPriority: 'high',
1848 });
1849 await 1;
1850 FlightReactDOM.prefetchDNS('d after');
1851 FlightReactDOM.preconnect('c after');
1852 FlightReactDOM.preconnect('c2 after', {crossOrigin: 'anonymous'});
1853 FlightReactDOM.preload('l after', {as: 'style'});
1854 FlightReactDOM.preloadModule('lm after');
1855 FlightReactDOM.preloadModule('lm2 after', {
1856 crossOrigin: 'anonymous',
1857 fetchPriority: 'low',
1858 });
1859 FlightReactDOM.preinit('i after', {as: 'script'});
1860 FlightReactDOM.preinitModule('m after');
1861 FlightReactDOM.preinitModule('m2 after', {
1862 crossOrigin: 'anonymous',
1863 fetchPriority: 'high',
1864 });
1865 return <ClientComponent />;
1866 }
1867
1868 const {writable: flightWritable, readable: flightReadable} =
1869 getTestStream();
1870 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
1871
1872 // In a real environment you would want to call the render during the Fizz render.
1873 // The reason we cannot do this in our test is because we don't actually have two separate
1874 // module graphs and we are contriving the sequencing to work in a way where
1875 // the right HostDispatcher is in scope during the Flight Server Float calls and the
1876 // Flight Client hint dispatches
1877 const {pipe} = await serverAct(() =>
1878 ReactServerDOMServer.renderToPipeableStream(
1879 <ServerComponent />,
1880 webpackMap,
1881 ),
1882 );
1883 pipe(flightWritable);
1884
1885 let response = null;
1886 function getResponse() {
1887 if (response === null) {
1888 response =
1889 ReactServerDOMClient.createFromReadableStream(flightReadable);
1890 }
1891 return response;
1892 }
1893
1894 function App() {
1895 return (
1896 <html>
1897 <body>{getResponse()}</body>
1898 </html>
1899 );
1900 }
1901
1902 await serverAct(async () => {
1903 ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
1904 });
1905
1906 await readInto(document, fizzReadable);
1907 expect(getMeaningfulChildren(document)).toEqual(
1908 <html>
1909 <head>
1910 <link rel="dns-prefetch" href="d before" />
1911 <link rel="preconnect" href="c before" />
1912 <link rel="preconnect" href="c2 before" crossorigin="" />
1913 <link rel="dns-prefetch" href="d after" />
1914 <link rel="preconnect" href="c after" />
1915 <link rel="preconnect" href="c2 after" crossorigin="" />
1916 <script async="" src="i before" />
1917 <script type="module" async="" src="m before" />
1918 <script
1919 type="module"
1920 async=""
1921 src="m2 before"
1922 crossorigin=""
1923 fetchpriority="high"
1924 />
1925 <script async="" src="i after" />
1926 <script type="module" async="" src="m after" />
1927 <script
1928 type="module"
1929 async=""
1930 src="m2 after"
1931 crossorigin=""
1932 fetchpriority="high"
1933 />
1934 <link rel="preload" as="style" href="l before" />
1935 <link rel="modulepreload" href="lm before" />
1936 <link
1937 rel="modulepreload"
1938 href="lm2 before"
1939 crossorigin=""
1940 fetchpriority="low"
1941 />
1942 <link rel="preload" as="style" href="l after" />
1943 <link rel="modulepreload" href="lm after" />
1944 <link
1945 rel="modulepreload"
1946 href="lm2 after"
1947 crossorigin=""
1948 fetchpriority="low"
1949 />
1950 </head>
1951 <body>
1952 <p>hello world</p>
1953 </body>
1954 </html>,
1955 );
1956 });
1957
1958 it('supports Float hints from concurrent Flight -> Fizz renders', async () => {
1959 function Component() {
1960 return <p>hello world</p>;
1961 }
1962
1963 const ClientComponent = clientExports(Component);
1964
1965 async function ServerComponent1() {
1966 FlightReactDOM.preload('before1', {as: 'style'});
1967 await 1;
1968 FlightReactDOM.preload('after1', {as: 'style'});
1969 return <ClientComponent />;
1970 }
1971
1972 async function ServerComponent2() {
1973 FlightReactDOM.preload('before2', {as: 'style'});
1974 await 1;
1975 FlightReactDOM.preload('after2', {as: 'style'});
1976 return <ClientComponent />;
1977 }
1978
1979 const {writable: flightWritable1, readable: flightReadable1} =
1980 getTestStream();
1981 const {writable: flightWritable2, readable: flightReadable2} =
1982 getTestStream();
1983
1984 ReactServerDOMServer.renderToPipeableStream(
1985 <ServerComponent1 />,
1986 webpackMap,
1987 ).pipe(flightWritable1);
1988
1989 ReactServerDOMServer.renderToPipeableStream(
1990 <ServerComponent2 />,
1991 webpackMap,
1992 ).pipe(flightWritable2);
1993
1994 const responses = new Map();
1995 function getResponse(stream) {
1996 let response = responses.get(stream);
1997 if (!response) {
1998 response = ReactServerDOMClient.createFromReadableStream(stream);
1999 responses.set(stream, response);
2000 }
2001 return response;
2002 }
2003
2004 function App({stream}) {
2005 return (
2006 <html>
2007 <body>{getResponse(stream)}</body>
2008 </html>
2009 );
2010 }
2011
2012 // pausing to let Flight runtime tick. This is a test only artifact of the fact that
2013 // we aren't operating separate module graphs for flight and fiber. In a real app
2014 // each would have their own dispatcher and there would be no cross dispatching.
2015 await serverAct(() => {});
2016
2017 const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream();
2018 const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream();
2019 await serverAct(async () => {
2020 ReactDOMFizzServer.renderToPipeableStream(
2021 <App stream={flightReadable1} />,
2022 ).pipe(fizzWritable1);
2023 ReactDOMFizzServer.renderToPipeableStream(
2024 <App stream={flightReadable2} />,
2025 ).pipe(fizzWritable2);
2026 });
2027
2028 async function read(stream) {
2029 const decoder = new TextDecoder();
2030 const reader = stream.getReader();
2031 let buffer = '';
2032 while (true) {
2033 const {done, value} = await reader.read();
2034 if (done) {
2035 buffer += decoder.decode();
2036 break;
2037 }
2038 buffer += decoder.decode(value, {stream: true});
2039 }
2040 return buffer;
2041 }
2042
2043 const [content1, content2] = await Promise.all([
2044 read(fizzReadable1),
2045 read(fizzReadable2),
2046 ]);
2047
2048 expect(content1).toEqual(
2049 '<!DOCTYPE html><html><head><link rel="preload" href="before1" as="style"/>' +
2050 '<link rel="preload" href="after1" as="style"/>' +
2051 (gate(flags => flags.enableFizzBlockingRender)
2052 ? '<link rel="expect" href="#_R_" blocking="render"/>'
2053 : '') +
2054 '</head>' +
2055 '<body><p>hello world</p>' +
2056 (gate(flags => flags.enableFizzBlockingRender)
2057 ? '<template id="_R_"></template>'
2058 : '') +
2059 '</body></html>',
2060 );
2061 expect(content2).toEqual(
2062 '<!DOCTYPE html><html><head><link rel="preload" href="before2" as="style"/>' +
2063 '<link rel="preload" href="after2" as="style"/>' +
2064 (gate(flags => flags.enableFizzBlockingRender)
2065 ? '<link rel="expect" href="#_R_" blocking="render"/>'
2066 : '') +
2067 '</head>' +
2068 '<body><p>hello world</p>' +
2069 (gate(flags => flags.enableFizzBlockingRender)
2070 ? '<template id="_R_"></template>'
2071 : '') +
2072 '</body></html>',
2073 );
2074 });
2075
2076 it('supports deduping hints by Float key', async () => {
2077 function Component() {
2078 return <p>hello world</p>;
2079 }
2080
2081 const ClientComponent = clientExports(Component);
2082
2083 async function ServerComponent() {
2084 FlightReactDOM.prefetchDNS('dns');
2085 FlightReactDOM.preconnect('preconnect');
2086 FlightReactDOM.preload('load', {as: 'style'});
2087 FlightReactDOM.preinit('init', {as: 'script'});
2088 // again but vary preconnect to demonstrate crossOrigin participates in the key
2089 FlightReactDOM.prefetchDNS('dns');
2090 FlightReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'});
2091 FlightReactDOM.preload('load', {as: 'style'});
2092 FlightReactDOM.preinit('init', {as: 'script'});
2093 await 1;
2094 // after an async point
2095 FlightReactDOM.prefetchDNS('dns');
2096 FlightReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'});
2097 FlightReactDOM.preload('load', {as: 'style'});
2098 FlightReactDOM.preinit('init', {as: 'script'});
2099 return <ClientComponent />;
2100 }
2101
2102 const {writable, readable} = getTestStream();
2103
2104 await serverAct(() =>
2105 ReactServerDOMServer.renderToPipeableStream(
2106 <ServerComponent />,
2107 webpackMap,
2108 ).pipe(writable),
2109 );
2110
2111 const hintRows = [];
2112 async function collectHints(stream) {
2113 const decoder = new TextDecoder();
2114 const reader = stream.getReader();
2115 let buffer = '';
2116 while (true) {
2117 const {done, value} = await reader.read();
2118 if (done) {
2119 buffer += decoder.decode();
2120 if (buffer.includes(':H')) {
2121 hintRows.push(buffer);
2122 }
2123 break;
2124 }
2125 buffer += decoder.decode(value, {stream: true});
2126 let line;
2127 while ((line = buffer.indexOf('\n')) > -1) {
2128 const row = buffer.slice(0, line);
2129 buffer = buffer.slice(line + 1);
2130 if (row.includes(':H')) {
2131 hintRows.push(row);
2132 }
2133 }
2134 }
2135 }
2136
2137 await collectHints(readable);
2138 expect(hintRows.length).toEqual(6);
2139 });
2140
2141 it('preloads resources without needing to render them', async () => {
2142 function NoScriptComponent() {
2143 return (
2144 <p>
2145 <img src="image-do-not-load" />
2146 <link rel="stylesheet" href="css-do-not-load" />
2147 </p>
2148 );
2149 }
2150
2151 function Component() {
2152 return (
2153 <div>
2154 <img src="image-resource" />
2155 <img
2156 src="image-do-not-load"
2157 srcSet="image-preload-src-set"
2158 sizes="image-sizes"
2159 />
2160 <img src="image-do-not-load" loading="lazy" />
2161 <link
2162 rel="preload"
2163 href="video-resource"
2164 as="video"
2165 media="(orientation: landscape)"
2166 />
2167 <link rel="modulepreload" href="module-resource" />
2168 <picture>
2169 <source
2170 srcSet="image-not-yet-preloaded"
2171 media="(orientation: portrait)"
2172 />
2173 <img src="image-do-not-load" />
2174 </picture>
2175 <noscript>
2176 <NoScriptComponent />
2177 </noscript>
2178 <link rel="stylesheet" href="css-resource" />
2179 </div>
2180 );
2181 }
2182
2183 const {writable, readable} = getTestStream();
2184 const {pipe} = await serverAct(() =>
2185 ReactServerDOMServer.renderToPipeableStream(<Component />, webpackMap),
2186 );
2187 pipe(writable);
2188
2189 let response = null;
2190 function getResponse() {
2191 if (response === null) {
2192 response = ReactServerDOMClient.createFromReadableStream(readable);
2193 }
2194 return response;
2195 }
2196
2197 function App() {
2198 // Not rendered but use for its side-effects.
2199 getResponse();
2200 return (
2201 <html>
2202 <body>
2203 <p>hello world</p>
2204 </body>
2205 </html>
2206 );
2207 }
2208
2209 const root = ReactDOMClient.createRoot(document);
2210 await act(() => {
2211 root.render(<App />);
2212 });
2213
2214 expect(getMeaningfulChildren(document)).toEqual(
2215 <html>
2216 <head>
2217 <link rel="preload" as="image" href="image-resource" />
2218 <link
2219 rel="preload"
2220 as="image"
2221 imagesrcset="image-preload-src-set"
2222 imagesizes="image-sizes"
2223 />
2224 <link
2225 rel="preload"
2226 as="video"
2227 href="video-resource"
2228 media="(orientation: landscape)"
2229 />
2230 <link rel="modulepreload" href="module-resource" />
2231 <link rel="preload" as="style" href="css-resource" />
2232 </head>
2233 <body>
2234 <p>hello world</p>
2235 </body>
2236 </html>,
2237 );
2238 });
2239
2240 it('should be able to include a client reference in printed errors', async () => {
2241 const reportedErrors = [];
2242
2243 const ClientComponent = clientExports(function ({prop}) {
2244 return 'This should never render';
2245 });
2246
2247 const ClientReference = clientExports({});
2248
2249 class InvalidValue {}
2250
2251 const {writable} = getTestStream();
2252 const {pipe} = await serverAct(() =>
2253 ReactServerDOMServer.renderToPipeableStream(
2254 <div>
2255 <ClientComponent prop={ClientReference} invalid={InvalidValue} />
2256 </div>,
2257 webpackMap,
2258 {
2259 onError(x) {
2260 reportedErrors.push(x);
2261 },
2262 },
2263 ),
2264 );
2265 pipe(writable);
2266
2267 expect(reportedErrors.length).toBe(1);
2268 if (__DEV__) {
2269 expect(reportedErrors[0].message).toEqual(
2270 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". ' +
2271 'Or maybe you meant to call this function rather than return it.\n' +
2272 ' <... prop={client} invalid={function InvalidValue}>\n' +
2273 ' ^^^^^^^^^^^^^^^^^^^^^^^',
2274 );
2275 } else {
2276 expect(reportedErrors[0].message).toEqual(
2277 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". ' +
2278 'Or maybe you meant to call this function rather than return it.\n' +
2279 ' {prop: client, invalid: function InvalidValue}\n' +
2280 ' ^^^^^^^^^^^^^^^^^^^^^',
2281 );
2282 }
2283 });
2284
2285 it('should be able to render a client reference as return value', async () => {
2286 const ClientModule = clientExports({
2287 text: 'Hello World',
2288 });
2289
2290 function ServerComponent() {
2291 return ClientModule.text;
2292 }
2293
2294 const {writable, readable} = getTestStream();
2295 const {pipe} = await serverAct(() =>
2296 ReactServerDOMServer.renderToPipeableStream(
2297 <ServerComponent />,
2298 webpackMap,
2299 ),
2300 );
2301 pipe(writable);
2302 const response = ReactServerDOMClient.createFromReadableStream(readable);
2303
2304 const container = document.createElement('div');
2305 const root = ReactDOMClient.createRoot(container);
2306
2307 await act(() => {
2308 root.render(response);
2309 });
2310 expect(container.innerHTML).toBe('Hello World');
2311 });
2312
2313 it('can abort synchronously during render', async () => {
2314 function Sibling() {
2315 return <p>sibling</p>;
2316 }
2317
2318 function App() {
2319 return (
2320 <div>
2321 <Suspense fallback={<p>loading 1...</p>}>
2322 <ComponentThatAborts />
2323 <Sibling />
2324 </Suspense>
2325 <Suspense fallback={<p>loading 2...</p>}>
2326 <Sibling />
2327 </Suspense>
2328 <div>
2329 <Suspense fallback={<p>loading 3...</p>}>
2330 <div>
2331 <Sibling />
2332 </div>
2333 </Suspense>
2334 </div>
2335 </div>
2336 );
2337 }
2338
2339 const abortRef = {current: null};
2340 function ComponentThatAborts() {
2341 abortRef.current();
2342 return <p>hello world</p>;
2343 }
2344
2345 const {writable: flightWritable, readable: flightReadable} =
2346 getTestStream();
2347
2348 await serverAct(() => {
2349 const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2350 <App />,
2351 webpackMap,
2352 );
2353 abortRef.current = abort;
2354 pipe(flightWritable);
2355 });
2356 assertConsoleErrorDev([
2357 'Error: The render was aborted by the server without a reason.' +
2358 '\n in <stack>',
2359 ]);
2360
2361 const response =
2362 ReactServerDOMClient.createFromReadableStream(flightReadable);
2363
2364 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2365
2366 function ClientApp() {
2367 return use(response);
2368 }
2369
2370 const shellErrors = [];
2371 await serverAct(async () => {
2372 ReactDOMFizzServer.renderToPipeableStream(
2373 React.createElement(ClientApp),
2374 {
2375 onShellError(error) {
2376 shellErrors.push(error.message);
2377 },
2378 },
2379 ).pipe(fizzWritable);
2380 });
2381 assertConsoleErrorDev([
2382 '[Server] Error: The render was aborted by the server without a reason.' +
2383 '\n in <stack>',
2384 '[Server] Error: The render was aborted by the server without a reason.' +
2385 '\n in <stack>',
2386 '[Server] Error: The render was aborted by the server without a reason.' +
2387 '\n in <stack>',
2388 ]);
2389
2390 expect(shellErrors).toEqual([]);
2391
2392 const container = document.createElement('div');
2393 await readInto(container, fizzReadable);
2394 expect(getMeaningfulChildren(container)).toEqual(
2395 <div>
2396 <p>loading 1...</p>
2397 <p>loading 2...</p>
2398 <div>
2399 <p>loading 3...</p>
2400 </div>
2401 </div>,
2402 );
2403 });
2404
2405 it('can abort during render in an async tick', async () => {
2406 async function Sibling() {
2407 return <p>sibling</p>;
2408 }
2409
2410 function App() {
2411 return (
2412 <div>
2413 <Suspense fallback={<p>loading 1...</p>}>
2414 <ComponentThatAborts />
2415 <Sibling />
2416 </Suspense>
2417 <Suspense fallback={<p>loading 2...</p>}>
2418 <Sibling />
2419 </Suspense>
2420 <div>
2421 <Suspense fallback={<p>loading 3...</p>}>
2422 <div>
2423 <Sibling />
2424 </div>
2425 </Suspense>
2426 </div>
2427 </div>
2428 );
2429 }
2430
2431 const abortRef = {current: null};
2432 async function ComponentThatAborts() {
2433 await 1;
2434 abortRef.current();
2435 return <p>hello world</p>;
2436 }
2437
2438 const {writable: flightWritable, readable: flightReadable} =
2439 getTestStream();
2440
2441 await serverAct(() => {
2442 const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2443 <App />,
2444 webpackMap,
2445 );
2446 abortRef.current = abort;
2447 pipe(flightWritable);
2448 });
2449
2450 assertConsoleErrorDev([
2451 'Error: The render was aborted by the server without a reason.' +
2452 '\n in <stack>',
2453 ]);
2454
2455 const response =
2456 ReactServerDOMClient.createFromReadableStream(flightReadable);
2457
2458 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2459
2460 function ClientApp() {
2461 return use(response);
2462 }
2463
2464 const shellErrors = [];
2465 await serverAct(async () => {
2466 ReactDOMFizzServer.renderToPipeableStream(
2467 React.createElement(ClientApp),
2468 {
2469 onShellError(error) {
2470 shellErrors.push(error.message);
2471 },
2472 },
2473 ).pipe(fizzWritable);
2474 });
2475
2476 assertConsoleErrorDev([
2477 '[Server] Error: The render was aborted by the server without a reason.' +
2478 '\n in <stack>',
2479 '[Server] Error: The render was aborted by the server without a reason.' +
2480 '\n in <stack>',
2481 '[Server] Error: The render was aborted by the server without a reason.' +
2482 '\n in <stack>',
2483 ]);
2484
2485 expect(shellErrors).toEqual([]);
2486
2487 const container = document.createElement('div');
2488 await readInto(container, fizzReadable);
2489 expect(getMeaningfulChildren(container)).toEqual(
2490 <div>
2491 <p>loading 1...</p>
2492 <p>loading 2...</p>
2493 <div>
2494 <p>loading 3...</p>
2495 </div>
2496 </div>,
2497 );
2498 });
2499
2500 it('can abort during render in a lazy initializer for a component', async () => {
2501 function Sibling() {
2502 return <p>sibling</p>;
2503 }
2504
2505 function App() {
2506 return (
2507 <div>
2508 <Suspense fallback={<p>loading 1...</p>}>
2509 <LazyAbort />
2510 </Suspense>
2511 <Suspense fallback={<p>loading 2...</p>}>
2512 <Sibling />
2513 </Suspense>
2514 <div>
2515 <Suspense fallback={<p>loading 3...</p>}>
2516 <div>
2517 <Sibling />
2518 </div>
2519 </Suspense>
2520 </div>
2521 </div>
2522 );
2523 }
2524
2525 const abortRef = {current: null};
2526 const LazyAbort = React.lazy(() => {
2527 abortRef.current();
2528 return {
2529 then(cb) {
2530 cb({default: 'div'});
2531 },
2532 };
2533 });
2534
2535 const {writable: flightWritable, readable: flightReadable} =
2536 getTestStream();
2537
2538 await serverAct(() => {
2539 const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2540 <App />,
2541 webpackMap,
2542 );
2543 abortRef.current = abort;
2544 pipe(flightWritable);
2545 });
2546 assertConsoleErrorDev([
2547 'Error: The render was aborted by the server without a reason.' +
2548 '\n in <stack>',
2549 ]);
2550
2551 const response =
2552 ReactServerDOMClient.createFromReadableStream(flightReadable);
2553
2554 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2555
2556 function ClientApp() {
2557 return use(response);
2558 }
2559
2560 const shellErrors = [];
2561 await serverAct(async () => {
2562 ReactDOMFizzServer.renderToPipeableStream(
2563 React.createElement(ClientApp),
2564 {
2565 onShellError(error) {
2566 shellErrors.push(error.message);
2567 },
2568 },
2569 ).pipe(fizzWritable);
2570 });
2571 assertConsoleErrorDev([
2572 '[Server] Error: The render was aborted by the server without a reason.' +
2573 '\n in <stack>',
2574 '[Server] Error: The render was aborted by the server without a reason.' +
2575 '\n in <stack>',
2576 '[Server] Error: The render was aborted by the server without a reason.' +
2577 '\n in <stack>',
2578 ]);
2579
2580 expect(shellErrors).toEqual([]);
2581
2582 const container = document.createElement('div');
2583 await readInto(container, fizzReadable);
2584 expect(getMeaningfulChildren(container)).toEqual(
2585 <div>
2586 <p>loading 1...</p>
2587 <p>loading 2...</p>
2588 <div>
2589 <p>loading 3...</p>
2590 </div>
2591 </div>,
2592 );
2593 });
2594
2595 it('can abort during render in a lazy initializer for an element', async () => {
2596 function Sibling() {
2597 return <p>sibling</p>;
2598 }
2599
2600 function App() {
2601 return (
2602 <div>
2603 <Suspense fallback={<p>loading 1...</p>}>{lazyAbort}</Suspense>
2604 <Suspense fallback={<p>loading 2...</p>}>
2605 <Sibling />
2606 </Suspense>
2607 <div>
2608 <Suspense fallback={<p>loading 3...</p>}>
2609 <div>
2610 <Sibling />
2611 </div>
2612 </Suspense>
2613 </div>
2614 </div>
2615 );
2616 }
2617
2618 const abortRef = {current: null};
2619 const lazyAbort = React.lazy(() => {
2620 abortRef.current();
2621 return {
2622 then(cb) {
2623 cb({default: 'hello world'});
2624 },
2625 };
2626 });
2627
2628 const {writable: flightWritable, readable: flightReadable} =
2629 getTestStream();
2630
2631 await serverAct(() => {
2632 const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2633 <App />,
2634 webpackMap,
2635 );
2636 abortRef.current = abort;
2637 pipe(flightWritable);
2638 });
2639 assertConsoleErrorDev([
2640 'Error: The render was aborted by the server without a reason.' +
2641 '\n in <stack>',
2642 ]);
2643
2644 const response =
2645 ReactServerDOMClient.createFromReadableStream(flightReadable);
2646
2647 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2648
2649 function ClientApp() {
2650 return use(response);
2651 }
2652
2653 const shellErrors = [];
2654 await serverAct(async () => {
2655 ReactDOMFizzServer.renderToPipeableStream(
2656 React.createElement(ClientApp),
2657 {
2658 onShellError(error) {
2659 shellErrors.push(error.message);
2660 },
2661 },
2662 ).pipe(fizzWritable);
2663 });
2664 assertConsoleErrorDev([
2665 '[Server] Error: The render was aborted by the server without a reason.' +
2666 '\n in <stack>',
2667 '[Server] Error: The render was aborted by the server without a reason.' +
2668 '\n in <stack>',
2669 '[Server] Error: The render was aborted by the server without a reason.' +
2670 '\n in <stack>',
2671 ]);
2672
2673 expect(shellErrors).toEqual([]);
2674
2675 const container = document.createElement('div');
2676 await readInto(container, fizzReadable);
2677 expect(getMeaningfulChildren(container)).toEqual(
2678 <div>
2679 <p>loading 1...</p>
2680 <p>loading 2...</p>
2681 <div>
2682 <p>loading 3...</p>
2683 </div>
2684 </div>,
2685 );
2686 });
2687
2688 it('can abort during a synchronous thenable resolution', async () => {
2689 function Sibling() {
2690 return <p>sibling</p>;
2691 }
2692
2693 function App() {
2694 return (
2695 <div>
2696 <Suspense fallback={<p>loading 1...</p>}>{thenable}</Suspense>
2697 <Suspense fallback={<p>loading 2...</p>}>
2698 <Sibling />
2699 </Suspense>
2700 <div>
2701 <Suspense fallback={<p>loading 3...</p>}>
2702 <div>
2703 <Sibling />
2704 </div>
2705 </Suspense>
2706 </div>
2707 </div>
2708 );
2709 }
2710
2711 const abortRef = {current: null};
2712 const thenable = {
2713 then(cb) {
2714 abortRef.current();
2715 cb(thenable.value);
2716 },
2717 };
2718
2719 const {writable: flightWritable, readable: flightReadable} =
2720 getTestStream();
2721
2722 await serverAct(() => {
2723 const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2724 <App />,
2725 webpackMap,
2726 );
2727 abortRef.current = abort;
2728 pipe(flightWritable);
2729 });
2730
2731 assertConsoleErrorDev([
2732 'Error: The render was aborted by the server without a reason.' +
2733 '\n in <stack>',
2734 ]);
2735
2736 const response =
2737 ReactServerDOMClient.createFromReadableStream(flightReadable);
2738
2739 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2740
2741 function ClientApp() {
2742 return use(response);
2743 }
2744
2745 const shellErrors = [];
2746 await serverAct(async () => {
2747 ReactDOMFizzServer.renderToPipeableStream(
2748 React.createElement(ClientApp),
2749 {
2750 onShellError(error) {
2751 shellErrors.push(error.message);
2752 },
2753 },
2754 ).pipe(fizzWritable);
2755 });
2756 assertConsoleErrorDev([
2757 '[Server] Error: The render was aborted by the server without a reason.' +
2758 '\n in <stack>',
2759 '[Server] Error: The render was aborted by the server without a reason.' +
2760 '\n in <stack>',
2761 '[Server] Error: The render was aborted by the server without a reason.' +
2762 '\n in <stack>',
2763 ]);
2764
2765 expect(shellErrors).toEqual([]);
2766
2767 const container = document.createElement('div');
2768 await readInto(container, fizzReadable);
2769 expect(getMeaningfulChildren(container)).toEqual(
2770 <div>
2771 <p>loading 1...</p>
2772 <p>loading 2...</p>
2773 <div>
2774 <p>loading 3...</p>
2775 </div>
2776 </div>,
2777 );
2778 });
2779
2780 it('wont serialize thenables that were not already settled by the time an abort happens', async () => {
2781 function App() {
2782 return (
2783 <div>
2784 <Suspense fallback={<p>loading 1...</p>}>
2785 <ComponentThatAborts />
2786 </Suspense>
2787 <Suspense fallback={<p>loading 2...</p>}>{thenable1}</Suspense>
2788 <div>
2789 <Suspense fallback={<p>loading 3...</p>}>{thenable2}</Suspense>
2790 </div>
2791 </div>
2792 );
2793 }
2794
2795 const abortRef = {current: null};
2796 const thenable1 = {
2797 then(cb) {
2798 cb('hello world');
2799 },
2800 };
2801
2802 const thenable2 = {
2803 then(cb) {
2804 cb('hello world');
2805 },
2806 status: 'fulfilled',
2807 value: 'hello world',
2808 };
2809
2810 function ComponentThatAborts() {
2811 abortRef.current();
2812 return thenable1;
2813 }
2814
2815 const {writable: flightWritable, readable: flightReadable} =
2816 getTestStream();
2817
2818 await serverAct(() => {
2819 const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2820 <App />,
2821 webpackMap,
2822 );
2823 abortRef.current = abort;
2824 pipe(flightWritable);
2825 });
2826
2827 assertConsoleErrorDev([
2828 'Error: The render was aborted by the server without a reason.' +
2829 '\n in <stack>',
2830 ]);
2831
2832 const response =
2833 ReactServerDOMClient.createFromReadableStream(flightReadable);
2834
2835 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2836
2837 function ClientApp() {
2838 return use(response);
2839 }
2840
2841 const shellErrors = [];
2842 await serverAct(async () => {
2843 ReactDOMFizzServer.renderToPipeableStream(
2844 React.createElement(ClientApp),
2845 {
2846 onShellError(error) {
2847 shellErrors.push(error.message);
2848 },
2849 },
2850 ).pipe(fizzWritable);
2851 });
2852 assertConsoleErrorDev([
2853 '[Server] Error: The render was aborted by the server without a reason.' +
2854 '\n in <stack>',
2855 '[Server] Error: The render was aborted by the server without a reason.' +
2856 '\n in <stack>',
2857 ]);
2858
2859 expect(shellErrors).toEqual([]);
2860
2861 const container = document.createElement('div');
2862 await readInto(container, fizzReadable);
2863 expect(getMeaningfulChildren(container)).toEqual(
2864 <div>
2865 <p>loading 1...</p>
2866 <p>loading 2...</p>
2867 <div>hello world</div>
2868 </div>,
2869 );
2870 });
2871
2872 it('can error synchronously after aborting without an unhandled rejection error', async () => {
2873 function App() {
2874 return (
2875 <div>
2876 <Suspense fallback={<p>loading...</p>}>
2877 <ComponentThatAborts />
2878 </Suspense>
2879 </div>
2880 );
2881 }
2882
2883 const abortRef = {current: null};
2884
2885 async function ComponentThatAborts() {
2886 abortRef.current();
2887 throw new Error('boom');
2888 }
2889
2890 const {writable: flightWritable, readable: flightReadable} =
2891 getTestStream();
2892
2893 await serverAct(() => {
2894 const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2895 <App />,
2896 webpackMap,
2897 );
2898 abortRef.current = abort;
2899 pipe(flightWritable);
2900 });
2901
2902 assertConsoleErrorDev([
2903 'Error: The render was aborted by the server without a reason.' +
2904 '\n in <stack>',
2905 ]);
2906
2907 const response =
2908 ReactServerDOMClient.createFromReadableStream(flightReadable);
2909
2910 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
2911
2912 function ClientApp() {
2913 return use(response);
2914 }
2915
2916 const shellErrors = [];
2917 await serverAct(async () => {
2918 ReactDOMFizzServer.renderToPipeableStream(
2919 React.createElement(ClientApp),
2920 {
2921 onShellError(error) {
2922 shellErrors.push(error.message);
2923 },
2924 },
2925 ).pipe(fizzWritable);
2926 });
2927 assertConsoleErrorDev([
2928 '[Server] Error: The render was aborted by the server without a reason.' +
2929 '\n in <stack>',
2930 ]);
2931
2932 expect(shellErrors).toEqual([]);
2933
2934 const container = document.createElement('div');
2935 await readInto(container, fizzReadable);
2936 expect(getMeaningfulChildren(container)).toEqual(
2937 <div>
2938 <p>loading...</p>
2939 </div>,
2940 );
2941 });
2942
2943 it('can error synchronously after aborting in a synchronous Component', async () => {
2944 const rejectError = new Error('bam!');
2945 const rejectedPromise = Promise.reject(rejectError);
2946 rejectedPromise.catch(() => {});
2947 rejectedPromise.status = 'rejected';
2948 rejectedPromise.reason = rejectError;
2949
2950 const resolvedValue = <p>hello world</p>;
2951 const resolvedPromise = Promise.resolve(resolvedValue);
2952 resolvedPromise.status = 'fulfilled';
2953 resolvedPromise.value = resolvedValue;
2954
2955 function App() {
2956 return (
2957 <div>
2958 <Suspense fallback={<p>loading...</p>}>
2959 <ComponentThatAborts />
2960 </Suspense>
2961 <Suspense fallback={<p>loading too...</p>}>
2962 {rejectedPromise}
2963 </Suspense>
2964 <Suspense fallback={<p>loading three...</p>}>
2965 {resolvedPromise}
2966 </Suspense>
2967 </div>
2968 );
2969 }
2970
2971 const abortRef = {current: null};
2972
2973 // This test is specifically asserting that this works with Sync Server Component
2974 function ComponentThatAborts() {
2975 abortRef.current();
2976 throw new Error('boom');
2977 }
2978
2979 const {writable: flightWritable, readable: flightReadable} =
2980 getTestStream();
2981
2982 await serverAct(() => {
2983 const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
2984 <App />,
2985 webpackMap,
2986 {
2987 onError(e) {
2988 console.error(e);
2989 },
2990 },
2991 );
2992 abortRef.current = abort;
2993 pipe(flightWritable);
2994 });
2995
2996 assertConsoleErrorDev([
2997 'Error: The render was aborted by the server without a reason.' +
2998 '\n in <stack>',
2999 'Error: bam!\n in <stack>',
3000 ]);
3001
3002 const response =
3003 ReactServerDOMClient.createFromReadableStream(flightReadable);
3004
3005 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
3006
3007 function ClientApp() {
3008 return use(response);
3009 }
3010
3011 const shellErrors = [];
3012 await serverAct(async () => {
3013 ReactDOMFizzServer.renderToPipeableStream(
3014 React.createElement(ClientApp),
3015 {
3016 onShellError(error) {
3017 shellErrors.push(error.message);
3018 },
3019 },
3020 ).pipe(fizzWritable);
3021 });
3022 assertConsoleErrorDev([
3023 '[Server] Error: The render was aborted by the server without a reason.' +
3024 '\n in <stack>',
3025 '[Server] Error: bam!\n in <stack>',
3026 ]);
3027
3028 expect(shellErrors).toEqual([]);
3029
3030 const container = document.createElement('div');
3031 await readInto(container, fizzReadable);
3032 expect(getMeaningfulChildren(container)).toEqual(
3033 <div>
3034 <p>loading...</p>
3035 <p>loading too...</p>
3036 <p>hello world</p>
3037 </div>,
3038 );
3039 });
3040
3041 it('can prerender', async () => {
3042 let resolveGreeting;
3043 const greetingPromise = new Promise(resolve => {
3044 resolveGreeting = resolve;
3045 });
3046
3047 function App() {
3048 return (
3049 <div>
3050 <Greeting />
3051 </div>
3052 );
3053 }
3054
3055 async function Greeting() {
3056 await greetingPromise;
3057 return 'hello world';
3058 }
3059
3060 const {pendingResult} = await serverAct(async () => {
3061 // destructure trick to avoid the act scope from awaiting the returned value
3062 return {
3063 pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
3064 <App />,
3065 webpackMap,
3066 ),
3067 };
3068 });
3069
3070 resolveGreeting();
3071 const {prelude} = await pendingResult;
3072
3073 const response = ReactServerDOMClient.createFromReadableStream(
3074 Readable.toWeb(prelude),
3075 );
3076
3077 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
3078
3079 function ClientApp() {
3080 return use(response);
3081 }
3082
3083 const shellErrors = [];
3084 await serverAct(async () => {
3085 ReactDOMFizzServer.renderToPipeableStream(
3086 React.createElement(ClientApp),
3087 {
3088 onShellError(error) {
3089 shellErrors.push(error.message);
3090 },
3091 },
3092 ).pipe(fizzWritable);
3093 });
3094
3095 expect(shellErrors).toEqual([]);
3096
3097 const container = document.createElement('div');
3098 await readInto(container, fizzReadable);
3099 expect(getMeaningfulChildren(container)).toEqual(<div>hello world</div>);
3100 });
3101
3102 it('does not propagate abort reasons errors when aborting a prerender', async () => {
3103 let resolveGreeting;
3104 const greetingPromise = new Promise(resolve => {
3105 resolveGreeting = resolve;
3106 });
3107
3108 function App() {
3109 return (
3110 <div>
3111 <Suspense fallback="loading...">
3112 <Greeting />
3113 </Suspense>
3114 </div>
3115 );
3116 }
3117
3118 async function Greeting() {
3119 await greetingPromise;
3120 return 'hello world';
3121 }
3122
3123 const controller = new AbortController();
3124 const errors = [];
3125 const {pendingResult} = await serverAct(async () => {
3126 // destructure trick to avoid the act scope from awaiting the returned value
3127 return {
3128 pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
3129 <App />,
3130 webpackMap,
3131 {
3132 signal: controller.signal,
3133 onError(err) {
3134 errors.push(err);
3135 },
3136 },
3137 ),
3138 };
3139 });
3140
3141 await serverAct(() => {
3142 controller.abort('boom');
3143 });
3144 resolveGreeting();
3145 const {prelude} = await pendingResult;
3146
3147 expect(errors).toEqual([]);
3148
3149 const preludeWeb = Readable.toWeb(prelude);
3150 const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
3151
3152 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
3153
3154 function ClientApp() {
3155 return use(response);
3156 }
3157
3158 errors.length = 0;
3159 let abortFizz;
3160 await serverAct(async () => {
3161 const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
3162 React.createElement(ClientApp),
3163 {
3164 onError(error) {
3165 errors.push(error);
3166 },
3167 },
3168 );
3169 pipe(fizzWritable);
3170 abortFizz = abort;
3171 });
3172
3173 await serverAct(() => {
3174 abortFizz('bam');
3175 });
3176
3177 expect(errors).toEqual([new Error('Connection closed.')]);
3178
3179 const container = document.createElement('div');
3180 await readInto(container, fizzReadable);
3181 expect(getMeaningfulChildren(container)).toEqual(<div>loading...</div>);
3182 });
3183
3184 it('will leave async iterables in an incomplete state when halting', async () => {
3185 let resolve;
3186 const wait = new Promise(r => (resolve = r));
3187 const errors = [];
3188
3189 const multiShotIterable = {
3190 async *[Symbol.asyncIterator]() {
3191 yield {hello: 'A'};
3192 await wait;
3193 yield {hi: 'B'};
3194 return 'C';
3195 },
3196 };
3197
3198 const controller = new AbortController();
3199 const {pendingResult} = await serverAct(() => {
3200 return {
3201 pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
3202 {
3203 multiShotIterable,
3204 },
3205 {},
3206 {
3207 onError(x) {
3208 errors.push(x);
3209 },
3210 signal: controller.signal,
3211 },
3212 ),
3213 };
3214 });
3215
3216 controller.abort();
3217 await serverAct(() => resolve());
3218
3219 const {prelude} = await pendingResult;
3220
3221 const result = await ReactServerDOMClient.createFromReadableStream(
3222 createUnclosingStream(Readable.toWeb(prelude)),
3223 );
3224
3225 const iterator = result.multiShotIterable[Symbol.asyncIterator]();
3226
3227 expect(await iterator.next()).toEqual({
3228 value: {hello: 'A'},
3229 done: false,
3230 });
3231
3232 const race = Promise.race([
3233 iterator.next(),
3234 new Promise(r => setTimeout(() => r('timeout'), 10)),
3235 ]);
3236
3237 await 1;
3238 jest.advanceTimersByTime(100);
3239 expect(await race).toBe('timeout');
3240 });
3241
3242 it('will halt unfinished chunks inside Suspense when aborting a prerender', async () => {
3243 const controller = new AbortController();
3244 function ComponentThatAborts() {
3245 controller.abort('boom');
3246 return null;
3247 }
3248
3249 async function Greeting() {
3250 await 1;
3251 return 'hello world';
3252 }
3253
3254 async function Farewell() {
3255 return 'goodbye world';
3256 }
3257
3258 async function Wrapper() {
3259 return (
3260 <Suspense fallback="loading too...">
3261 <ComponentThatAborts />
3262 </Suspense>
3263 );
3264 }
3265
3266 function App() {
3267 return (
3268 <div>
3269 <Suspense fallback="loading...">
3270 <Greeting />
3271 </Suspense>
3272 <Wrapper />
3273 <Suspense fallback="loading three...">
3274 <Farewell />
3275 </Suspense>
3276 </div>
3277 );
3278 }
3279
3280 const errors = [];
3281 const {pendingResult} = await serverAct(() => {
3282 return {
3283 pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
3284 <App />,
3285 {},
3286 {
3287 onError(x) {
3288 errors.push(x);
3289 },
3290 signal: controller.signal,
3291 },
3292 ),
3293 };
3294 });
3295
3296 const {prelude} = await pendingResult;
3297
3298 expect(errors).toEqual([]);
3299
3300 const preludeWeb = Readable.toWeb(prelude);
3301 const response = ReactServerDOMClient.createFromReadableStream(preludeWeb);
3302
3303 const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
3304
3305 function ClientApp() {
3306 return use(response);
3307 }
3308 errors.length = 0;
3309 let abortFizz;
3310 await serverAct(async () => {
3311 const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(
3312 React.createElement(ClientApp),
3313 {
3314 onError(error, errorInfo) {
3315 errors.push(error);
3316 },
3317 },
3318 );
3319 pipe(fizzWritable);
3320 abortFizz = abort;
3321 });
3322
3323 await serverAct(() => {
3324 abortFizz('boom');
3325 });
3326
3327 // one error per boundary
3328 const err = new Error('Connection closed.');
3329 expect(errors).toEqual([err, err, err]);
3330
3331 const container = document.createElement('div');
3332 await readInto(container, fizzReadable);
3333 expect(getMeaningfulChildren(container)).toEqual(
3334 <div>
3335 {'loading...'}
3336 {'loading too...'}
3337 {'loading three...'}
3338 </div>,
3339 );
3340 });
3341
3342 it('rejecting a thenable after an abort before flush should not lead to a frozen readable', async () => {
3343 const ClientComponent = clientExports(function (props: {
3344 promise: Promise<void>,
3345 }) {
3346 return 'hello world';
3347 });
3348
3349 let reject;
3350 const promise = new Promise((_, re) => {
3351 reject = re;
3352 });
3353
3354 function App() {
3355 return (
3356 <div>
3357 <Suspense fallback="loading...">
3358 <ClientComponent promise={promise} />
3359 </Suspense>
3360 </div>
3361 );
3362 }
3363
3364 const errors = [];
3365 const {writable, readable} = getTestStream();
3366 const {pipe, abort} = await serverAct(() =>
3367 ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap, {
3368 onError(x) {
3369 errors.push(x);
3370 },
3371 }),
3372 );
3373 await serverAct(() => {
3374 abort('STOP');
3375 reject('STOP');
3376 });
3377 pipe(writable);
3378
3379 const reader = readable.getReader();
3380 while (true) {
3381 const {done} = await reader.read();
3382 if (done) {
3383 break;
3384 }
3385 }
3386
3387 expect(errors).toEqual(['STOP']);
3388
3389 // We expect it to get to the end here rather than hang on the reader.
3390 });
3391 });