main
js 3,656 lines 98.6 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 // Polyfills for test environment
13 global.ReadableStream =
14 require('web-streams-polyfill/ponyfill/es6').ReadableStream;
15 global.WritableStream =
16 require('web-streams-polyfill/ponyfill/es6').WritableStream;
17 global.TextEncoder = require('util').TextEncoder;
18 global.TextDecoder = require('util').TextDecoder;
19
20 const {
21 patchMessageChannel,
22 } = require('../../../../scripts/jest/patchMessageChannel');
23
24 let clientExports;
25 let serverExports;
26 let webpackMap;
27 let webpackServerMap;
28 let act;
29 let serverAct;
30 let getDebugInfo;
31 let React;
32 let ReactDOM;
33 let ReactDOMClient;
34 let ReactDOMFizzServer;
35 let ReactServerDOMServer;
36 let ReactServerDOMStaticServer;
37 let ReactServerDOMClient;
38 let Suspense;
39 let use;
40 let ReactServer;
41 let ReactServerDOM;
42 let ReactServerScheduler;
43 let assertConsoleErrorDev;
44
45 describe('ReactFlightDOMBrowser', () => {
46 beforeEach(() => {
47 jest.resetModules();
48
49 ReactServerScheduler = require('scheduler');
50 patchMessageChannel(ReactServerScheduler);
51 serverAct = require('internal-test-utils').serverAct;
52 getDebugInfo = require('internal-test-utils').getDebugInfo.bind(null, {
53 ignoreProps: true,
54 useFixedTime: true,
55 });
56
57 // Simulate the condition resolution
58
59 jest.mock('react', () => require('react/react.react-server'));
60 ReactServer = require('react');
61 ReactServerDOM = require('react-dom');
62
63 jest.mock('react-server-dom-webpack/server', () =>
64 require('react-server-dom-webpack/server.browser'),
65 );
66 const WebpackMock = require('./utils/WebpackMock');
67 clientExports = WebpackMock.clientExports;
68 serverExports = WebpackMock.serverExports;
69 webpackMap = WebpackMock.webpackMap;
70 webpackServerMap = WebpackMock.webpackServerMap;
71 ReactServerDOMServer = require('react-server-dom-webpack/server');
72 jest.mock('react-server-dom-webpack/static', () =>
73 require('react-server-dom-webpack/static.browser'),
74 );
75 ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
76
77 __unmockReact();
78 jest.resetModules();
79
80 patchMessageChannel();
81
82 ({act, assertConsoleErrorDev} = require('internal-test-utils'));
83 React = require('react');
84 ReactDOM = require('react-dom');
85 ReactDOMClient = require('react-dom/client');
86 ReactDOMFizzServer = require('react-dom/server.browser');
87 jest.mock('react-server-dom-webpack/client', () =>
88 require('react-server-dom-webpack/client.browser'),
89 );
90 ReactServerDOMClient = require('react-server-dom-webpack/client');
91 Suspense = React.Suspense;
92 use = React.use;
93 });
94
95 function makeDelayedText(Model) {
96 let error, _resolve, _reject;
97 let promise = new Promise((resolve, reject) => {
98 _resolve = () => {
99 promise = null;
100 resolve();
101 };
102 _reject = e => {
103 error = e;
104 promise = null;
105 reject(e);
106 };
107 });
108 function DelayedText({children}, data) {
109 if (promise) {
110 throw promise;
111 }
112 if (error) {
113 throw error;
114 }
115 return <Model>{children}</Model>;
116 }
117 return [DelayedText, _resolve, _reject];
118 }
119
120 const theInfinitePromise = new Promise(() => {});
121 function InfiniteSuspend() {
122 throw theInfinitePromise;
123 }
124
125 function requireServerRef(ref) {
126 let name = '';
127 let resolvedModuleData = webpackServerMap[ref];
128 if (resolvedModuleData) {
129 // The potentially aliased name.
130 name = resolvedModuleData.name;
131 } else {
132 // We didn't find this specific export name but we might have the * export
133 // which contains this name as well.
134 // TODO: It's unfortunate that we now have to parse this string. We should
135 // probably go back to encoding path and name separately on the client reference.
136 const idx = ref.lastIndexOf('#');
137 if (idx !== -1) {
138 name = ref.slice(idx + 1);
139 resolvedModuleData = webpackServerMap[ref.slice(0, idx)];
140 }
141 if (!resolvedModuleData) {
142 throw new Error(
143 'Could not find the module "' +
144 ref +
145 '" in the React Client Manifest. ' +
146 'This is probably a bug in the React Server Components bundler.',
147 );
148 }
149 }
150 const mod = __webpack_require__(resolvedModuleData.id);
151 if (name === '*') {
152 return mod;
153 }
154 return mod[name];
155 }
156
157 async function callServer(actionId, body) {
158 const fn = requireServerRef(actionId);
159 const args = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
160 return fn.apply(null, args);
161 }
162
163 function createDelayedStream(
164 stream: ReadableStream<Uint8Array>,
165 ): ReadableStream<Uint8Array> {
166 return new ReadableStream({
167 async start(controller) {
168 const reader = stream.getReader();
169 while (true) {
170 const {done, value} = await reader.read();
171 if (done) {
172 controller.close();
173 } else {
174 // Artificially delay between enqueuing chunks.
175 await new Promise(resolve => setTimeout(resolve));
176 controller.enqueue(value);
177 }
178 }
179 },
180 });
181 }
182
183 function normalizeCodeLocInfo(str) {
184 return (
185 str &&
186 str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
187 return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
188 })
189 );
190 }
191
192 it('should resolve HTML using W3C streams', async () => {
193 function Text({children}) {
194 return <span>{children}</span>;
195 }
196 function HTML() {
197 return (
198 <div>
199 <Text>hello</Text>
200 <Text>world</Text>
201 </div>
202 );
203 }
204
205 function App() {
206 const model = {
207 html: <HTML />,
208 };
209 return model;
210 }
211
212 const stream = await serverAct(() =>
213 ReactServerDOMServer.renderToReadableStream(<App />),
214 );
215 const response = ReactServerDOMClient.createFromReadableStream(stream);
216 const model = await response;
217 expect(model).toEqual({
218 html: (
219 <div>
220 <span>hello</span>
221 <span>world</span>
222 </div>
223 ),
224 });
225 });
226
227 it('should resolve client components (with async chunks) when referenced in props', async () => {
228 let resolveClientComponentChunk;
229
230 const ClientOuter = clientExports(function ClientOuter({
231 Component,
232 children,
233 }) {
234 return <Component>{children}</Component>;
235 });
236
237 const ClientInner = clientExports(
238 function ClientInner({children}) {
239 return <span>{children}</span>;
240 },
241 '42',
242 '/test.js',
243 new Promise(resolve => (resolveClientComponentChunk = resolve)),
244 );
245
246 function Server() {
247 return <ClientOuter Component={ClientInner}>Hello, World!</ClientOuter>;
248 }
249
250 const stream = await serverAct(() =>
251 ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
252 );
253
254 function ClientRoot({response}) {
255 return use(response);
256 }
257
258 const response = ReactServerDOMClient.createFromReadableStream(stream);
259 const container = document.createElement('div');
260 const root = ReactDOMClient.createRoot(container);
261
262 await act(() => {
263 root.render(<ClientRoot response={response} />);
264 });
265
266 expect(container.innerHTML).toBe('');
267
268 await act(() => {
269 resolveClientComponentChunk();
270 });
271
272 expect(container.innerHTML).toBe('<span>Hello, World!</span>');
273 });
274
275 it('should resolve deduped objects within the same model root when it is blocked', async () => {
276 let resolveClientComponentChunk;
277
278 const ClientOuter = clientExports(function ClientOuter({Component, value}) {
279 return <Component value={value} />;
280 });
281
282 const ClientInner = clientExports(
283 function ClientInner({value}) {
284 return <pre>{JSON.stringify(value)}</pre>;
285 },
286 '42',
287 '/test.js',
288 new Promise(resolve => (resolveClientComponentChunk = resolve)),
289 );
290
291 function Server({value}) {
292 return <ClientOuter Component={ClientInner} value={value} />;
293 }
294
295 const shared = [1, 2, 3];
296 const value = [shared, shared];
297
298 const stream = await serverAct(() =>
299 ReactServerDOMServer.renderToReadableStream(
300 <Server value={value} />,
301 webpackMap,
302 ),
303 );
304
305 function ClientRoot({response}) {
306 return use(response);
307 }
308
309 const response = ReactServerDOMClient.createFromReadableStream(stream);
310 const container = document.createElement('div');
311 const root = ReactDOMClient.createRoot(container);
312
313 await act(() => {
314 root.render(<ClientRoot response={response} />);
315 });
316
317 expect(container.innerHTML).toBe('');
318
319 await act(() => {
320 resolveClientComponentChunk();
321 });
322
323 expect(container.innerHTML).toBe('<pre>[[1,2,3],[1,2,3]]</pre>');
324 });
325
326 it('should resolve deduped objects within the same model root when it is blocked and there is a listener attached to the root', async () => {
327 let resolveClientComponentChunk;
328
329 const ClientOuter = clientExports(function ClientOuter({Component, value}) {
330 return <Component value={value} />;
331 });
332
333 const ClientInner = clientExports(
334 function ClientInner({value}) {
335 return <pre>{JSON.stringify(value)}</pre>;
336 },
337 '42',
338 '/test.js',
339 new Promise(resolve => (resolveClientComponentChunk = resolve)),
340 );
341
342 function Server({value}) {
343 return <ClientOuter Component={ClientInner} value={value} />;
344 }
345
346 const shared = [1, 2, 3];
347 const value = [shared, shared];
348
349 const stream = await serverAct(() =>
350 ReactServerDOMServer.renderToReadableStream(
351 <Server value={value} />,
352 webpackMap,
353 ),
354 );
355
356 function ClientRoot({response}) {
357 return use(response);
358 }
359
360 const response = ReactServerDOMClient.createFromReadableStream(stream);
361 // make sure we have a listener so that `resolveModelChunk` initializes the chunk eagerly
362 response.then(() => {});
363
364 const container = document.createElement('div');
365 const root = ReactDOMClient.createRoot(container);
366
367 await act(() => {
368 root.render(<ClientRoot response={response} />);
369 });
370
371 expect(container.innerHTML).toBe('');
372
373 await act(() => {
374 resolveClientComponentChunk();
375 });
376
377 expect(container.innerHTML).toBe('<pre>[[1,2,3],[1,2,3]]</pre>');
378 });
379
380 it('should resolve deduped objects that are themselves blocked', async () => {
381 let resolveClientComponentChunk;
382
383 const Client = clientExports(
384 [4, 5],
385 '42',
386 '/test.js',
387 new Promise(resolve => (resolveClientComponentChunk = resolve)),
388 );
389
390 const shared = [1, 2, 3, Client];
391
392 const stream = await serverAct(() =>
393 ReactServerDOMServer.renderToReadableStream(
394 <div>
395 <Suspense fallback="Loading">
396 <span>
397 {shared /* this will serialize first and block nearest element */}
398 </span>
399 </Suspense>
400 {shared /* this will be referenced inside the blocked element */}
401 </div>,
402 webpackMap,
403 ),
404 );
405
406 function ClientRoot({response}) {
407 return use(response);
408 }
409
410 const response = ReactServerDOMClient.createFromReadableStream(stream);
411 const container = document.createElement('div');
412 const root = ReactDOMClient.createRoot(container);
413
414 await act(() => {
415 root.render(<ClientRoot response={response} />);
416 });
417
418 expect(container.innerHTML).toBe('');
419
420 await act(() => {
421 resolveClientComponentChunk();
422 });
423
424 expect(container.innerHTML).toBe('<div><span>12345</span>12345</div>');
425 });
426
427 it('should resolve deduped objects in nested children of blocked models', async () => {
428 let resolveOuterClientComponentChunk;
429 let resolveInnerClientComponentChunk;
430
431 const ClientOuter = clientExports(
432 function ClientOuter({children, value}) {
433 return children;
434 },
435 '1',
436 '/outer.js',
437 new Promise(resolve => (resolveOuterClientComponentChunk = resolve)),
438 );
439
440 function PassthroughServerComponent({children}) {
441 return children;
442 }
443
444 const ClientInner = clientExports(
445 function ClientInner({children}) {
446 return JSON.stringify(children);
447 },
448 '2',
449 '/inner.js',
450 new Promise(resolve => (resolveInnerClientComponentChunk = resolve)),
451 );
452
453 const value = {};
454
455 function Server() {
456 return (
457 <ClientOuter value={value}>
458 <PassthroughServerComponent>
459 <ClientInner>{value}</ClientInner>
460 </PassthroughServerComponent>
461 </ClientOuter>
462 );
463 }
464
465 const stream = await serverAct(() =>
466 ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
467 );
468
469 function ClientRoot({response}) {
470 return use(response);
471 }
472
473 const response = ReactServerDOMClient.createFromReadableStream(stream);
474 const container = document.createElement('div');
475 const root = ReactDOMClient.createRoot(container);
476
477 await act(() => {
478 root.render(<ClientRoot response={response} />);
479 });
480
481 expect(container.innerHTML).toBe('');
482
483 await act(() => {
484 resolveInnerClientComponentChunk();
485 resolveOuterClientComponentChunk();
486 });
487
488 expect(container.innerHTML).toBe('{}');
489 });
490
491 it('should resolve deduped objects in blocked models referencing other blocked models with blocked references', async () => {
492 let resolveFooClientComponentChunk;
493 let resolveBarClientComponentChunk;
494
495 function PassthroughServerComponent({children}) {
496 return children;
497 }
498
499 const FooClient = clientExports(
500 function FooClient({children}) {
501 return JSON.stringify(children);
502 },
503 '1',
504 '/foo.js',
505 new Promise(resolve => (resolveFooClientComponentChunk = resolve)),
506 );
507
508 const BarClient = clientExports(
509 function BarClient() {
510 return 'not used';
511 },
512 '2',
513 '/bar.js',
514 new Promise(resolve => (resolveBarClientComponentChunk = resolve)),
515 );
516
517 const shared = {foo: 1};
518
519 function Server() {
520 return (
521 <>
522 <PassthroughServerComponent>
523 <FooClient key="first" bar={BarClient}>
524 {shared}
525 </FooClient>
526 </PassthroughServerComponent>
527 <FooClient key="second" bar={BarClient}>
528 {shared}
529 </FooClient>
530 </>
531 );
532 }
533
534 const stream = await serverAct(() =>
535 ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
536 );
537
538 function ClientRoot({response}) {
539 return use(response);
540 }
541
542 const response = ReactServerDOMClient.createFromReadableStream(stream);
543 const container = document.createElement('div');
544 const root = ReactDOMClient.createRoot(container);
545
546 await act(() => {
547 root.render(<ClientRoot response={response} />);
548 });
549
550 expect(container.innerHTML).toBe('');
551
552 await act(() => {
553 resolveFooClientComponentChunk();
554 resolveBarClientComponentChunk();
555 });
556
557 expect(container.innerHTML).toBe('{"foo":1}{"foo":1}');
558 });
559
560 it('should handle deduped props of re-used elements in fragments (same-chunk reference)', async () => {
561 let resolveFooClientComponentChunk;
562
563 const FooClient = clientExports(
564 function Foo({children, item}) {
565 return children;
566 },
567 '1',
568 '/foo.js',
569 new Promise(resolve => (resolveFooClientComponentChunk = resolve)),
570 );
571
572 const shared = <div />;
573
574 function Server() {
575 return (
576 <FooClient track={shared}>
577 <>{shared}</>
578 </FooClient>
579 );
580 }
581
582 const stream = await serverAct(() =>
583 ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
584 );
585
586 function ClientRoot({response}) {
587 return use(response);
588 }
589
590 const response = ReactServerDOMClient.createFromReadableStream(stream);
591 const container = document.createElement('div');
592 const root = ReactDOMClient.createRoot(container);
593
594 await act(() => {
595 root.render(<ClientRoot response={response} />);
596 });
597
598 expect(container.innerHTML).toBe('');
599
600 await act(() => {
601 resolveFooClientComponentChunk();
602 });
603
604 expect(container.innerHTML).toBe('<div></div>');
605 });
606
607 it('should handle deduped props of re-used elements in server components (cross-chunk reference)', async () => {
608 let resolveFooClientComponentChunk;
609
610 function PassthroughServerComponent({children}) {
611 return children;
612 }
613
614 const FooClient = clientExports(
615 function Foo({children, item}) {
616 return children;
617 },
618 '1',
619 '/foo.js',
620 new Promise(resolve => (resolveFooClientComponentChunk = resolve)),
621 );
622
623 const shared = <div />;
624
625 function Server() {
626 return (
627 <FooClient track={shared}>
628 <PassthroughServerComponent>{shared}</PassthroughServerComponent>
629 </FooClient>
630 );
631 }
632
633 const stream = await serverAct(() =>
634 ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
635 );
636
637 function ClientRoot({response}) {
638 return use(response);
639 }
640
641 const response = ReactServerDOMClient.createFromReadableStream(stream);
642 const container = document.createElement('div');
643 const root = ReactDOMClient.createRoot(container);
644
645 await act(() => {
646 root.render(<ClientRoot response={response} />);
647 });
648
649 expect(container.innerHTML).toBe('');
650
651 await act(() => {
652 resolveFooClientComponentChunk();
653 });
654
655 expect(container.innerHTML).toBe('<div></div>');
656 });
657
658 it('should handle references to deduped owner objects', async () => {
659 // This is replicating React components as generated by @svgr/webpack:
660 let path1a: React.ReactNode;
661 let path1b: React.ReactNode;
662 let path2: React.ReactNode;
663
664 function Svg1() {
665 return ReactServer.createElement(
666 'svg',
667 {id: '1'},
668 path1a || (path1a = ReactServer.createElement('path', {})),
669 path1b || (path1b = ReactServer.createElement('path', {})),
670 );
671 }
672
673 function Svg2() {
674 return ReactServer.createElement(
675 'svg',
676 {id: '2'},
677 path2 || (path2 = ReactServer.createElement('path', {})),
678 );
679 }
680
681 function Server() {
682 return ReactServer.createElement(
683 ReactServer.Fragment,
684 {},
685 ReactServer.createElement(Svg1),
686 ReactServer.createElement(Svg2),
687 );
688 }
689
690 let stream = await serverAct(() =>
691 ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
692 );
693
694 function ClientRoot({response}) {
695 return use(response);
696 }
697
698 let response = ReactServerDOMClient.createFromReadableStream(stream);
699 const container = document.createElement('div');
700 const root = ReactDOMClient.createRoot(container);
701
702 await act(() => {
703 root.render(<ClientRoot response={response} />);
704 });
705
706 const expectedHtml =
707 '<svg id="1"><path></path><path></path></svg><svg id="2"><path></path></svg>';
708
709 expect(container.innerHTML).toBe(expectedHtml);
710
711 // Render a second time:
712
713 // Assigning the path elements to variables in module scope (here simulated
714 // with the test's function scope), and rendering a second time, prevents
715 // the owner of the path elements (i.e. Svg1/Svg2) to be deduped. The owner
716 // of the path in Svg1 is fully inlined. The owner of the owner of the path
717 // in Svg2 is Server, which is deduped and replaced with a reference to the
718 // owner of the owner of the path in Svg1. This nested owner is actually
719 // Server from the previous render pass, which is kinda broken and libraries
720 // probably shouldn't generate code like this. This reference can only be
721 // resolved properly if owners are specifically handled when resolving
722 // outlined models.
723
724 stream = await serverAct(() =>
725 ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
726 );
727
728 response = ReactServerDOMClient.createFromReadableStream(stream);
729
730 await act(() => {
731 root.render(<ClientRoot response={response} />);
732 });
733
734 expect(container.innerHTML).toBe(expectedHtml);
735
736 if (__DEV__) {
737 const resolvedPath1b = response.value[0].props.children[1];
738
739 expect(resolvedPath1b._owner).toEqual(
740 expect.objectContaining({
741 name: 'Svg1',
742 env: 'Server',
743 key: null,
744 owner: expect.objectContaining({
745 name: 'Server',
746 env: 'Server',
747 key: null,
748 }),
749 }),
750 );
751
752 const resolvedPath2 = response.value[1].props.children;
753
754 expect(resolvedPath2._owner).toEqual(
755 expect.objectContaining({
756 name: 'Svg2',
757 env: 'Server',
758 key: null,
759 owner: expect.objectContaining({
760 name: 'Server',
761 env: 'Server',
762 key: null,
763 }),
764 }),
765 );
766 }
767 });
768
769 it('should progressively reveal server components', async () => {
770 let reportedErrors = [];
771
772 // Client Components
773
774 class ErrorBoundary extends React.Component {
775 state = {hasError: false, error: null};
776 static getDerivedStateFromError(error) {
777 return {
778 hasError: true,
779 error,
780 };
781 }
782 render() {
783 if (this.state.hasError) {
784 return this.props.fallback(this.state.error);
785 }
786 return this.props.children;
787 }
788 }
789
790 let errorBoundaryFn;
791 if (__DEV__) {
792 errorBoundaryFn = e => (
793 <p>
794 {e.message} + {e.digest}
795 </p>
796 );
797 } else {
798 errorBoundaryFn = e => {
799 expect(e.message).toBe(
800 'An error occurred in the Server Components render. The specific message is omitted in production' +
801 ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
802 ' may provide additional details about the nature of the error.',
803 );
804 return <p>{e.digest}</p>;
805 };
806 }
807
808 function MyErrorBoundary({children}) {
809 return (
810 <ErrorBoundary fallback={errorBoundaryFn}>{children}</ErrorBoundary>
811 );
812 }
813
814 // Model
815 function Text({children}) {
816 return children;
817 }
818
819 const [Friends, resolveFriends] = makeDelayedText(Text);
820 const [Name, resolveName] = makeDelayedText(Text);
821 const [Posts, resolvePosts] = makeDelayedText(Text);
822 const [Photos, resolvePhotos] = makeDelayedText(Text);
823 const [Games, , rejectGames] = makeDelayedText(Text);
824
825 // View
826 function ProfileDetails({avatar}) {
827 return (
828 <div>
829 <Name>:name:</Name>
830 {avatar}
831 </div>
832 );
833 }
834 function ProfileSidebar({friends}) {
835 return (
836 <div>
837 <Photos>:photos:</Photos>
838 {friends}
839 </div>
840 );
841 }
842 function ProfilePosts({posts}) {
843 return <div>{posts}</div>;
844 }
845 function ProfileGames({games}) {
846 return <div>{games}</div>;
847 }
848
849 const MyErrorBoundaryClient = clientExports(MyErrorBoundary);
850
851 function ProfileContent() {
852 return (
853 <>
854 <ProfileDetails avatar={<Text>:avatar:</Text>} />
855 <Suspense fallback={<p>(loading sidebar)</p>}>
856 <ProfileSidebar friends={<Friends>:friends:</Friends>} />
857 </Suspense>
858 <Suspense fallback={<p>(loading posts)</p>}>
859 <ProfilePosts posts={<Posts>:posts:</Posts>} />
860 </Suspense>
861 <MyErrorBoundaryClient>
862 <Suspense fallback={<p>(loading games)</p>}>
863 <ProfileGames games={<Games>:games:</Games>} />
864 </Suspense>
865 </MyErrorBoundaryClient>
866 </>
867 );
868 }
869
870 const model = {
871 rootContent: <ProfileContent />,
872 };
873
874 function ProfilePage({response}) {
875 return use(response).rootContent;
876 }
877
878 const stream = await serverAct(() =>
879 ReactServerDOMServer.renderToReadableStream(model, webpackMap, {
880 onError(x) {
881 reportedErrors.push(x);
882 return __DEV__ ? `a dev digest` : `digest("${x.message}")`;
883 },
884 }),
885 );
886 const response = ReactServerDOMClient.createFromReadableStream(stream);
887
888 const container = document.createElement('div');
889 const root = ReactDOMClient.createRoot(container);
890 await act(() => {
891 root.render(
892 <Suspense fallback={<p>(loading)</p>}>
893 <ProfilePage response={response} />
894 </Suspense>,
895 );
896 });
897 expect(container.innerHTML).toBe('<p>(loading)</p>');
898
899 // This isn't enough to show anything.
900 await serverAct(async () => {
901 await act(() => {
902 resolveFriends();
903 });
904 });
905 expect(container.innerHTML).toBe('<p>(loading)</p>');
906
907 // We can now show the details. Sidebar and posts are still loading.
908 await serverAct(async () => {
909 await act(() => {
910 resolveName();
911 });
912 });
913 // Advance time enough to trigger a nested fallback.
914 jest.advanceTimersByTime(500);
915 expect(container.innerHTML).toBe(
916 '<div>:name::avatar:</div>' +
917 '<p>(loading sidebar)</p>' +
918 '<p>(loading posts)</p>' +
919 '<p>(loading games)</p>',
920 );
921
922 expect(reportedErrors).toEqual([]);
923
924 const theError = new Error('Game over');
925 // Let's *fail* loading games.
926 await serverAct(async () => {
927 await act(() => {
928 rejectGames(theError);
929 });
930 });
931
932 const gamesExpectedValue = __DEV__
933 ? '<p>Game over + a dev digest</p>'
934 : '<p>digest("Game over")</p>';
935
936 expect(container.innerHTML).toBe(
937 '<div>:name::avatar:</div>' +
938 '<p>(loading sidebar)</p>' +
939 '<p>(loading posts)</p>' +
940 gamesExpectedValue,
941 );
942
943 expect(reportedErrors).toEqual([theError]);
944 reportedErrors = [];
945
946 // We can now show the sidebar.
947 await serverAct(async () => {
948 await act(() => {
949 resolvePhotos();
950 });
951 });
952 expect(container.innerHTML).toBe(
953 '<div>:name::avatar:</div>' +
954 '<div>:photos::friends:</div>' +
955 '<p>(loading posts)</p>' +
956 gamesExpectedValue,
957 );
958
959 // Show everything.
960 await serverAct(async () => {
961 await act(() => {
962 resolvePosts();
963 });
964 });
965 expect(container.innerHTML).toBe(
966 '<div>:name::avatar:</div>' +
967 '<div>:photos::friends:</div>' +
968 '<div>:posts:</div>' +
969 gamesExpectedValue,
970 );
971
972 expect(reportedErrors).toEqual([]);
973 });
974
975 it('should close the stream upon completion when rendering to W3C streams', async () => {
976 // Model
977 function Text({children}) {
978 return children;
979 }
980
981 const [Friends, resolveFriends] = makeDelayedText(Text);
982 const [Name, resolveName] = makeDelayedText(Text);
983 const [Posts, resolvePosts] = makeDelayedText(Text);
984 const [Photos, resolvePhotos] = makeDelayedText(Text);
985
986 // View
987 function ProfileDetails({avatar}) {
988 return (
989 <div>
990 <Name>:name:</Name>
991 {avatar}
992 </div>
993 );
994 }
995 function ProfileSidebar({friends}) {
996 return (
997 <div>
998 <Photos>:photos:</Photos>
999 {friends}
1000 </div>
1001 );
1002 }
1003 function ProfilePosts({posts}) {
1004 return <div>{posts}</div>;
1005 }
1006
1007 function ProfileContent() {
1008 return (
1009 <Suspense fallback="(loading everything)">
1010 <ProfileDetails avatar={<Text>:avatar:</Text>} />
1011 <Suspense fallback={<p>(loading sidebar)</p>}>
1012 <ProfileSidebar friends={<Friends>:friends:</Friends>} />
1013 </Suspense>
1014 <Suspense fallback={<p>(loading posts)</p>}>
1015 <ProfilePosts posts={<Posts>:posts:</Posts>} />
1016 </Suspense>
1017 </Suspense>
1018 );
1019 }
1020
1021 const model = {
1022 rootContent: <ProfileContent />,
1023 };
1024
1025 const stream = await serverAct(() =>
1026 ReactServerDOMServer.renderToReadableStream(model, webpackMap),
1027 );
1028
1029 const reader = stream.getReader();
1030 const decoder = new TextDecoder();
1031
1032 let flightResponse = '';
1033 let isDone = false;
1034
1035 reader.read().then(function progress({done, value}) {
1036 if (done) {
1037 isDone = true;
1038 return;
1039 }
1040
1041 flightResponse += decoder.decode(value);
1042
1043 return reader.read().then(progress);
1044 });
1045
1046 // Advance time enough to trigger a nested fallback.
1047 jest.advanceTimersByTime(500);
1048
1049 await serverAct(() => {});
1050
1051 expect(flightResponse).toContain('(loading everything)');
1052 expect(flightResponse).toContain('(loading sidebar)');
1053 expect(flightResponse).toContain('(loading posts)');
1054 if (!__DEV__) {
1055 expect(flightResponse).not.toContain(':friends:');
1056 expect(flightResponse).not.toContain(':name:');
1057 }
1058
1059 await serverAct(() => {
1060 resolveFriends();
1061 });
1062
1063 expect(flightResponse).toContain(':friends:');
1064
1065 await serverAct(() => {
1066 resolveName();
1067 });
1068
1069 expect(flightResponse).toContain(':name:');
1070
1071 await serverAct(() => {
1072 resolvePhotos();
1073 });
1074
1075 expect(flightResponse).toContain(':photos:');
1076
1077 await serverAct(() => {
1078 resolvePosts();
1079 });
1080
1081 expect(flightResponse).toContain(':posts:');
1082
1083 // Final pending chunk is written; stream should be closed.
1084 expect(isDone).toBeTruthy();
1085 });
1086
1087 it('should be able to complete after aborting and throw the reason client-side', async () => {
1088 const reportedErrors = [];
1089
1090 let errorBoundaryFn;
1091 if (__DEV__) {
1092 errorBoundaryFn = e => (
1093 <p>
1094 {e.message} + {e.digest}
1095 </p>
1096 );
1097 } else {
1098 errorBoundaryFn = e => {
1099 expect(e.message).toBe(
1100 'An error occurred in the Server Components render. The specific message is omitted in production' +
1101 ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
1102 ' may provide additional details about the nature of the error.',
1103 );
1104 return <p>{e.digest}</p>;
1105 };
1106 }
1107
1108 class ErrorBoundary extends React.Component {
1109 state = {hasError: false, error: null};
1110 static getDerivedStateFromError(error) {
1111 return {
1112 hasError: true,
1113 error,
1114 };
1115 }
1116 render() {
1117 if (this.state.hasError) {
1118 return this.props.fallback(this.state.error);
1119 }
1120 return this.props.children;
1121 }
1122 }
1123
1124 const controller = new AbortController();
1125 const stream = await serverAct(() =>
1126 ReactServerDOMServer.renderToReadableStream(
1127 <div>
1128 <InfiniteSuspend />
1129 </div>,
1130 webpackMap,
1131 {
1132 signal: controller.signal,
1133 onError(x) {
1134 const message = typeof x === 'string' ? x : x.message;
1135 reportedErrors.push(x);
1136 return __DEV__ ? 'a dev digest' : `digest("${message}")`;
1137 },
1138 },
1139 ),
1140 );
1141 const response = ReactServerDOMClient.createFromReadableStream(stream);
1142
1143 const container = document.createElement('div');
1144 const root = ReactDOMClient.createRoot(container);
1145
1146 function App({res}) {
1147 return use(res);
1148 }
1149
1150 await act(() => {
1151 root.render(
1152 <ErrorBoundary fallback={errorBoundaryFn}>
1153 <Suspense fallback={<p>(loading)</p>}>
1154 <App res={response} />
1155 </Suspense>
1156 </ErrorBoundary>,
1157 );
1158 });
1159 expect(container.innerHTML).toBe('<p>(loading)</p>');
1160
1161 await act(() => {
1162 controller.abort('for reasons');
1163 });
1164 const expectedValue = __DEV__
1165 ? '<p>for reasons + a dev digest</p>'
1166 : '<p>digest("for reasons")</p>';
1167 expect(container.innerHTML).toBe(expectedValue);
1168
1169 expect(reportedErrors).toEqual(['for reasons']);
1170 });
1171
1172 it('should warn in DEV a child is missing keys', async () => {
1173 function ParentClient({children}) {
1174 return children;
1175 }
1176 const Parent = clientExports(ParentClient);
1177 const ParentModule = clientExports({Parent: ParentClient});
1178
1179 const container = document.createElement('div');
1180 const root = ReactDOMClient.createRoot(container);
1181
1182 const stream = await serverAct(() =>
1183 ReactServerDOMServer.renderToReadableStream(
1184 <>
1185 <Parent>{Array(6).fill(<div>no key</div>)}</Parent>
1186 <ParentModule.Parent>
1187 {Array(6).fill(<div>no key</div>)}
1188 </ParentModule.Parent>
1189 </>,
1190 webpackMap,
1191 ),
1192 );
1193 const result = await ReactServerDOMClient.createFromReadableStream(stream);
1194
1195 await act(() => {
1196 root.render(result);
1197 });
1198 assertConsoleErrorDev([
1199 'Each child in a list should have a unique "key" prop.\n\n' +
1200 'Check the top-level render call using <ParentClient>. ' +
1201 'See https://react.dev/link/warning-keys for more information.\n' +
1202 ' in div (at **)',
1203 ]);
1204 });
1205
1206 it('basic use(promise)', async () => {
1207 function Server() {
1208 return (
1209 ReactServer.use(Promise.resolve('A')) +
1210 ReactServer.use(Promise.resolve('B')) +
1211 ReactServer.use(Promise.resolve('C'))
1212 );
1213 }
1214
1215 const stream = await serverAct(() =>
1216 ReactServerDOMServer.renderToReadableStream(<Server />),
1217 );
1218 const response = ReactServerDOMClient.createFromReadableStream(stream);
1219
1220 function Client() {
1221 return use(response);
1222 }
1223
1224 const container = document.createElement('div');
1225 const root = ReactDOMClient.createRoot(container);
1226 await act(() => {
1227 root.render(
1228 <Suspense fallback="Loading...">
1229 <Client />
1230 </Suspense>,
1231 );
1232 });
1233 expect(container.innerHTML).toBe('ABC');
1234 });
1235
1236 it('use(promise) in multiple components', async () => {
1237 function Child({prefix}) {
1238 return (
1239 prefix +
1240 ReactServer.use(Promise.resolve('C')) +
1241 ReactServer.use(Promise.resolve('D'))
1242 );
1243 }
1244
1245 function Parent() {
1246 return (
1247 <Child
1248 prefix={
1249 ReactServer.use(Promise.resolve('A')) +
1250 ReactServer.use(Promise.resolve('B'))
1251 }
1252 />
1253 );
1254 }
1255
1256 const stream = await serverAct(() =>
1257 ReactServerDOMServer.renderToReadableStream(<Parent />),
1258 );
1259 const response = ReactServerDOMClient.createFromReadableStream(stream);
1260
1261 function Client() {
1262 return use(response);
1263 }
1264
1265 const container = document.createElement('div');
1266 const root = ReactDOMClient.createRoot(container);
1267 await act(() => {
1268 root.render(
1269 <Suspense fallback="Loading...">
1270 <Client />
1271 </Suspense>,
1272 );
1273 });
1274 expect(container.innerHTML).toBe('ABCD');
1275 });
1276
1277 it('using a rejected promise will throw', async () => {
1278 const promiseA = Promise.resolve('A');
1279 const promiseB = Promise.reject(new Error('Oops!'));
1280 const promiseC = Promise.resolve('C');
1281
1282 // Jest/Node will raise an unhandled rejected error unless we await this. It
1283 // works fine in the browser, though.
1284 await expect(promiseB).rejects.toThrow('Oops!');
1285
1286 function Server() {
1287 return (
1288 ReactServer.use(promiseA) +
1289 ReactServer.use(promiseB) +
1290 ReactServer.use(promiseC)
1291 );
1292 }
1293
1294 const reportedErrors = [];
1295 const stream = await serverAct(() =>
1296 ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap, {
1297 onError(x) {
1298 reportedErrors.push(x);
1299 return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
1300 },
1301 }),
1302 );
1303 const response = ReactServerDOMClient.createFromReadableStream(stream);
1304
1305 class ErrorBoundary extends React.Component {
1306 state = {error: null};
1307 static getDerivedStateFromError(error) {
1308 return {error};
1309 }
1310 render() {
1311 if (this.state.error) {
1312 return __DEV__
1313 ? this.state.error.message + ' + ' + this.state.error.digest
1314 : this.state.error.digest;
1315 }
1316 return this.props.children;
1317 }
1318 }
1319
1320 function Client() {
1321 return use(response);
1322 }
1323
1324 const container = document.createElement('div');
1325 const root = ReactDOMClient.createRoot(container);
1326 await act(() => {
1327 root.render(
1328 <ErrorBoundary>
1329 <Client />
1330 </ErrorBoundary>,
1331 );
1332 });
1333 expect(container.innerHTML).toBe(
1334 __DEV__ ? 'Oops! + a dev digest' : 'digest("Oops!")',
1335 );
1336 expect(reportedErrors.length).toBe(1);
1337 expect(reportedErrors[0].message).toBe('Oops!');
1338 });
1339
1340 it("use a promise that's already been instrumented and resolved", async () => {
1341 const thenable = {
1342 status: 'fulfilled',
1343 value: 'Hi',
1344 then() {},
1345 };
1346
1347 // This will never suspend because the thenable already resolved
1348 function Server() {
1349 return ReactServer.use(thenable);
1350 }
1351
1352 const stream = await serverAct(() =>
1353 ReactServerDOMServer.renderToReadableStream(<Server />),
1354 );
1355 const response = ReactServerDOMClient.createFromReadableStream(stream);
1356
1357 function Client() {
1358 return use(response);
1359 }
1360
1361 const container = document.createElement('div');
1362 const root = ReactDOMClient.createRoot(container);
1363 await act(() => {
1364 root.render(<Client />);
1365 });
1366 expect(container.innerHTML).toBe('Hi');
1367 });
1368
1369 it('unwraps thenable that fulfills synchronously without suspending', async () => {
1370 function Server() {
1371 const thenable = {
1372 then(resolve) {
1373 // This thenable immediately resolves, synchronously, without waiting
1374 // a microtask.
1375 resolve('Hi');
1376 },
1377 };
1378 try {
1379 return ReactServer.use(thenable);
1380 } catch {
1381 throw new Error(
1382 '`use` should not suspend because the thenable resolved synchronously.',
1383 );
1384 }
1385 }
1386
1387 // Because the thenable resolves synchronously, we should be able to finish
1388 // rendering synchronously, with no fallback.
1389 const stream = await serverAct(() =>
1390 ReactServerDOMServer.renderToReadableStream(<Server />),
1391 );
1392 const response = ReactServerDOMClient.createFromReadableStream(stream);
1393
1394 function Client() {
1395 return use(response);
1396 }
1397
1398 const container = document.createElement('div');
1399 const root = ReactDOMClient.createRoot(container);
1400 await act(() => {
1401 root.render(<Client />);
1402 });
1403 expect(container.innerHTML).toBe('Hi');
1404 });
1405
1406 it('can pass a higher order function by reference from server to client', async () => {
1407 let actionProxy;
1408
1409 function Client({action}) {
1410 actionProxy = action;
1411 return 'Click Me';
1412 }
1413
1414 function greet(transform, text) {
1415 return 'Hello ' + transform(text);
1416 }
1417
1418 function upper(text) {
1419 return text.toUpperCase();
1420 }
1421
1422 const ServerModuleA = serverExports({
1423 greet,
1424 });
1425 const ServerModuleB = serverExports({
1426 upper,
1427 });
1428 const ClientRef = clientExports(Client);
1429
1430 const boundFn = ServerModuleA.greet.bind(null, ServerModuleB.upper);
1431
1432 const stream = await serverAct(() =>
1433 ReactServerDOMServer.renderToReadableStream(
1434 <ClientRef action={boundFn} />,
1435 webpackMap,
1436 ),
1437 );
1438
1439 const response = ReactServerDOMClient.createFromReadableStream(stream, {
1440 async callServer(ref, args) {
1441 const body = await ReactServerDOMClient.encodeReply(args);
1442 return callServer(ref, body);
1443 },
1444 });
1445
1446 function App() {
1447 return use(response);
1448 }
1449
1450 const container = document.createElement('div');
1451 const root = ReactDOMClient.createRoot(container);
1452 await act(() => {
1453 root.render(<App />);
1454 });
1455 expect(container.innerHTML).toBe('Click Me');
1456 expect(typeof actionProxy).toBe('function');
1457 expect(actionProxy).not.toBe(boundFn);
1458
1459 const result = await actionProxy('hi');
1460 expect(result).toBe('Hello HI');
1461 });
1462
1463 it('can call a module split server function', async () => {
1464 let actionProxy;
1465
1466 function Client({action}) {
1467 actionProxy = action;
1468 return 'Click Me';
1469 }
1470
1471 function greet(text) {
1472 return 'Hello ' + text;
1473 }
1474
1475 const ServerModule = serverExports({
1476 // This gets split into another module
1477 split: greet,
1478 });
1479 const ClientRef = clientExports(Client);
1480
1481 const stream = await serverAct(() =>
1482 ReactServerDOMServer.renderToReadableStream(
1483 <ClientRef action={ServerModule.split} />,
1484 webpackMap,
1485 ),
1486 );
1487
1488 const response = ReactServerDOMClient.createFromReadableStream(stream, {
1489 async callServer(ref, args) {
1490 const body = await ReactServerDOMClient.encodeReply(args);
1491 return callServer(ref, body);
1492 },
1493 });
1494
1495 function App() {
1496 return use(response);
1497 }
1498
1499 const container = document.createElement('div');
1500 const root = ReactDOMClient.createRoot(container);
1501 await act(() => {
1502 root.render(<App />);
1503 });
1504 expect(container.innerHTML).toBe('Click Me');
1505 expect(typeof actionProxy).toBe('function');
1506
1507 const result = await actionProxy('Split');
1508 expect(result).toBe('Hello Split');
1509 });
1510
1511 it('can pass a server function by importing from client back to server', async () => {
1512 function greet(transform, text) {
1513 return 'Hello ' + transform(text);
1514 }
1515
1516 function upper(text) {
1517 return text.toUpperCase();
1518 }
1519
1520 const ServerModuleA = serverExports({
1521 greet,
1522 });
1523 const ServerModuleB = serverExports({
1524 upper,
1525 });
1526
1527 let actionProxy;
1528
1529 // This is a Proxy representing ServerModuleB in the Client bundle.
1530 const ServerModuleBImportedOnClient = {
1531 upper: ReactServerDOMClient.createServerReference(
1532 ServerModuleB.upper.$$id,
1533 async function (ref, args) {
1534 const body = await ReactServerDOMClient.encodeReply(args);
1535 return callServer(ref, body);
1536 },
1537 undefined,
1538 undefined,
1539 'upper',
1540 ),
1541 };
1542
1543 expect(ServerModuleBImportedOnClient.upper.name).toBe(
1544 __DEV__ ? 'upper' : 'action',
1545 );
1546 if (__DEV__) {
1547 expect(ServerModuleBImportedOnClient.upper.toString()).toBe(
1548 '(...args) => server(...args)',
1549 );
1550 }
1551
1552 function Client({action}) {
1553 // Client side pass a Server Reference into an action.
1554 actionProxy = text => action(ServerModuleBImportedOnClient.upper, text);
1555 return 'Click Me';
1556 }
1557
1558 const ClientRef = clientExports(Client);
1559
1560 const stream = await serverAct(() =>
1561 ReactServerDOMServer.renderToReadableStream(
1562 <ClientRef action={ServerModuleA.greet} />,
1563 webpackMap,
1564 ),
1565 );
1566
1567 const response = ReactServerDOMClient.createFromReadableStream(stream, {
1568 async callServer(ref, args) {
1569 const body = await ReactServerDOMClient.encodeReply(args);
1570 return callServer(ref, body);
1571 },
1572 });
1573
1574 function App() {
1575 return use(response);
1576 }
1577
1578 const container = document.createElement('div');
1579 const root = ReactDOMClient.createRoot(container);
1580 await act(() => {
1581 root.render(<App />);
1582 });
1583 expect(container.innerHTML).toBe('Click Me');
1584
1585 const result = await actionProxy('hi');
1586 expect(result).toBe('Hello HI');
1587 });
1588
1589 it('can bind arguments to a server reference', async () => {
1590 let actionProxy;
1591
1592 function Client({action}) {
1593 actionProxy = action;
1594 return 'Click Me';
1595 }
1596
1597 const greet = serverExports(function greet(a, b, c) {
1598 return a + ' ' + b + c;
1599 });
1600 const ClientRef = clientExports(Client);
1601
1602 const stream = await serverAct(() =>
1603 ReactServerDOMServer.renderToReadableStream(
1604 <ClientRef action={greet.bind(null, 'Hello').bind(null, 'World')} />,
1605 webpackMap,
1606 ),
1607 );
1608
1609 const response = ReactServerDOMClient.createFromReadableStream(stream, {
1610 async callServer(actionId, args) {
1611 const body = await ReactServerDOMClient.encodeReply(args);
1612 return callServer(actionId, body);
1613 },
1614 });
1615
1616 function App() {
1617 return use(response);
1618 }
1619
1620 const container = document.createElement('div');
1621 const root = ReactDOMClient.createRoot(container);
1622 await act(() => {
1623 root.render(<App />);
1624 });
1625 expect(container.innerHTML).toBe('Click Me');
1626 expect(typeof actionProxy).toBe('function');
1627 expect(actionProxy).not.toBe(greet);
1628
1629 const result = await actionProxy('!');
1630 expect(result).toBe('Hello World!');
1631 });
1632
1633 it('propagates server reference errors to the client', async () => {
1634 let actionProxy;
1635
1636 function Client({action}) {
1637 actionProxy = action;
1638 return 'Click Me';
1639 }
1640
1641 async function send(text) {
1642 throw new Error(`Error for ${text}`);
1643 }
1644
1645 const ServerModule = serverExports({send});
1646 const ClientRef = clientExports(Client);
1647
1648 const stream = await serverAct(() =>
1649 ReactServerDOMServer.renderToReadableStream(
1650 <ClientRef action={ServerModule.send} />,
1651 webpackMap,
1652 ),
1653 );
1654 const response = ReactServerDOMClient.createFromReadableStream(stream, {
1655 async callServer(actionId, args) {
1656 const body = await ReactServerDOMClient.encodeReply(args);
1657 const result = callServer(actionId, body);
1658 // Flight doesn't attach error handlers early enough. we suppress the warning
1659 // by putting a dummy catch on the result here
1660 result.catch(() => {});
1661 return ReactServerDOMClient.createFromReadableStream(
1662 ReactServerDOMServer.renderToReadableStream(result, null, {
1663 onError: error => 'test-error-digest',
1664 }),
1665 );
1666 },
1667 });
1668
1669 function App() {
1670 return use(response);
1671 }
1672
1673 const container = document.createElement('div');
1674 const root = ReactDOMClient.createRoot(container);
1675 await act(() => {
1676 root.render(<App />);
1677 });
1678
1679 let thrownError;
1680
1681 try {
1682 await serverAct(() => actionProxy('test'));
1683 } catch (error) {
1684 thrownError = error;
1685 }
1686
1687 if (__DEV__) {
1688 expect(thrownError).toEqual(new Error('Error for test'));
1689 } else {
1690 expect(thrownError).toEqual(
1691 new Error(
1692 '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.',
1693 ),
1694 );
1695
1696 expect(thrownError.digest).toBe('test-error-digest');
1697 }
1698 });
1699
1700 it('can use the same function twice as a server action', async () => {
1701 let actionProxy1;
1702 let actionProxy2;
1703
1704 function Client({action1, action2}) {
1705 actionProxy1 = action1;
1706 actionProxy2 = action2;
1707 return 'Click Me';
1708 }
1709
1710 function greet(text) {
1711 return 'Hello ' + text;
1712 }
1713
1714 const ServerModule = serverExports({
1715 greet,
1716 greet2: greet,
1717 });
1718 const ClientRef = clientExports(Client);
1719
1720 const stream = await serverAct(() =>
1721 ReactServerDOMServer.renderToReadableStream(
1722 <ClientRef
1723 action1={ServerModule.greet}
1724 action2={ServerModule.greet2}
1725 />,
1726 webpackMap,
1727 ),
1728 );
1729
1730 const response = ReactServerDOMClient.createFromReadableStream(stream, {
1731 async callServer(ref, args) {
1732 const body = await ReactServerDOMClient.encodeReply(args);
1733 return callServer(ref, body);
1734 },
1735 });
1736
1737 function App() {
1738 return use(response);
1739 }
1740
1741 const container = document.createElement('div');
1742 const root = ReactDOMClient.createRoot(container);
1743 await act(() => {
1744 root.render(<App />);
1745 });
1746 expect(container.innerHTML).toBe('Click Me');
1747 expect(typeof actionProxy1).toBe('function');
1748 expect(actionProxy1).not.toBe(greet);
1749
1750 // TODO: Ideally flight would be encoding this the same.
1751 expect(actionProxy1).not.toBe(actionProxy2);
1752
1753 const result = await actionProxy1('world');
1754 expect(result).toBe('Hello world');
1755 });
1756
1757 it('can pass an async server exports that resolves later to an outline object like a Map', async () => {
1758 let resolve;
1759 const chunkPromise = new Promise(r => (resolve = r));
1760
1761 function action() {}
1762 const serverModule = serverExports(
1763 {
1764 action: action,
1765 },
1766 chunkPromise,
1767 );
1768
1769 // Send the action to the client
1770 const stream = await serverAct(() =>
1771 ReactServerDOMServer.renderToReadableStream(
1772 {action: serverModule.action},
1773 webpackMap,
1774 ),
1775 );
1776
1777 // Snapshot updates change this formatting, so we let prettier ignore it.
1778 // prettier-ignore
1779 const response =
1780 await ReactServerDOMClient.createFromReadableStream(stream);
1781
1782 // Pass the action back to the server inside a Map
1783
1784 const map = new Map();
1785 map.set('action', response.action);
1786
1787 const body = await ReactServerDOMClient.encodeReply(map);
1788 const resultPromise = ReactServerDOMServer.decodeReply(
1789 body,
1790 webpackServerMap,
1791 );
1792
1793 // We couldn't yet resolve the server reference because we haven't loaded
1794 // its chunk yet in the new server instance. We now resolve it which loads
1795 // it asynchronously.
1796 await resolve();
1797
1798 const result = await resultPromise;
1799 expect(result instanceof Map).toBe(true);
1800 expect(result.get('action')).toBe(action);
1801 });
1802
1803 it('supports Float hints before the first await in server components in Fiber', async () => {
1804 function Component() {
1805 return <p>hello world</p>;
1806 }
1807
1808 const ClientComponent = clientExports(Component);
1809
1810 async function ServerComponent() {
1811 ReactServerDOM.preload('before', {as: 'style'});
1812 await 1;
1813 ReactServerDOM.preload('after', {as: 'style'});
1814 return <ClientComponent />;
1815 }
1816
1817 const stream = await serverAct(() =>
1818 ReactServerDOMServer.renderToReadableStream(
1819 <ServerComponent />,
1820 webpackMap,
1821 ),
1822 );
1823
1824 let response = null;
1825 function getResponse() {
1826 if (response === null) {
1827 response = ReactServerDOMClient.createFromReadableStream(stream);
1828 }
1829 return response;
1830 }
1831
1832 function App() {
1833 return getResponse();
1834 }
1835
1836 // pausing to let Flight runtime tick. This is a test only artifact of the fact that
1837 // we aren't operating separate module graphs for flight and fiber. In a real app
1838 // each would have their own dispatcher and there would be no cross dispatching.
1839 await 1;
1840
1841 const container = document.createElement('div');
1842 const root = ReactDOMClient.createRoot(container);
1843 await act(() => {
1844 root.render(<App />);
1845 });
1846 expect(document.head.innerHTML).toBe(
1847 gate(f => f.www)
1848 ? // The www entrypoints for ReactDOM and ReactDOMClient are unified so even
1849 // when you pull in just the top level the dispatcher for the Document is
1850 // loaded alongside it. In a normal environment there would be nothing to dispatch to
1851 // in a server environment so the preload calls would still only be dispatched to fizz
1852 // or the browser but not both. However in this contrived test environment the preloads
1853 // are being dispatched simultaneously causing an extraneous preload to show up. This test currently
1854 // asserts this be demonstrating that the preload call after the await point
1855 // is written to the document before the call before it. We still demonstrate that
1856 // flight handled the sync call because if the fiber implementation did it would appear
1857 // before the after call. In the future we will change this assertion once the fiber
1858 // implementation no long automatically gets pulled in
1859 '<link rel="preload" href="after" as="style"><link rel="preload" href="before" as="style">'
1860 : // For other release channels the client and isomorphic entrypoints are separate and thus we only
1861 // observe the expected preload from before the first await
1862 '<link rel="preload" href="before" as="style">',
1863 );
1864 expect(container.innerHTML).toBe('<p>hello world</p>');
1865 });
1866
1867 it('Does not support Float hints in server components anywhere in Fizz', async () => {
1868 // In environments that do not support AsyncLocalStorage the Flight client has no ability
1869 // to scope hint dispatching to a specific Request. In Fiber this isn't a problem because
1870 // the Browser scope acts like a singleton and we can dispatch away. But in Fizz we need to have
1871 // a reference to Resources and this is only possible during render unless you support AsyncLocalStorage.
1872 function Component() {
1873 return <p>hello world</p>;
1874 }
1875
1876 const ClientComponent = clientExports(Component);
1877
1878 async function ServerComponent() {
1879 ReactDOM.preload('before', {as: 'style'});
1880 await 1;
1881 ReactDOM.preload('after', {as: 'style'});
1882 return <ClientComponent />;
1883 }
1884
1885 const stream = await serverAct(() =>
1886 ReactServerDOMServer.renderToReadableStream(
1887 <ServerComponent />,
1888 webpackMap,
1889 ),
1890 );
1891
1892 let response = null;
1893 function getResponse() {
1894 if (response === null) {
1895 response = ReactServerDOMClient.createFromReadableStream(stream);
1896 }
1897 return response;
1898 }
1899
1900 function App() {
1901 return (
1902 <html>
1903 <body>{getResponse()}</body>
1904 </html>
1905 );
1906 }
1907
1908 let fizzPromise;
1909 await act(async () => {
1910 fizzPromise = ReactDOMFizzServer.renderToReadableStream(<App />);
1911 });
1912 const fizzStream = await fizzPromise;
1913
1914 const decoder = new TextDecoder();
1915 const reader = fizzStream.getReader();
1916 let content = '';
1917 while (true) {
1918 const {done, value} = await reader.read();
1919 if (done) {
1920 content += decoder.decode();
1921 break;
1922 }
1923 content += decoder.decode(value, {stream: true});
1924 }
1925
1926 expect(content).toEqual(
1927 '<!DOCTYPE html><html><head>' +
1928 (gate(flags => flags.enableFizzBlockingRender)
1929 ? '<link rel="expect" href="#_R_" blocking="render"/>'
1930 : '') +
1931 '</head>' +
1932 '<body><p>hello world</p>' +
1933 (gate(flags => flags.enableFizzBlockingRender)
1934 ? '<template id="_R_"></template>'
1935 : '') +
1936 '</body></html>',
1937 );
1938 });
1939
1940 it('should not continue rendering after the reader cancels', async () => {
1941 let hasLoaded = false;
1942 let resolve;
1943 let rendered = false;
1944 const promise = new Promise(r => (resolve = r));
1945 function Wait() {
1946 if (!hasLoaded) {
1947 throw promise;
1948 }
1949 rendered = true;
1950 return 'Done';
1951 }
1952 const errors = [];
1953 const stream = await serverAct(() =>
1954 ReactServerDOMServer.renderToReadableStream(
1955 <div>
1956 <Suspense fallback={<div>Loading</div>}>
1957 <Wait />
1958 </Suspense>
1959 </div>,
1960 null,
1961 {
1962 onError(x) {
1963 errors.push(x.message);
1964 },
1965 },
1966 ),
1967 );
1968
1969 expect(rendered).toBe(false);
1970
1971 const reader = stream.getReader();
1972 await reader.read();
1973 await reader.cancel();
1974
1975 expect(errors).toEqual([
1976 'The render was aborted by the server without a reason.',
1977 ]);
1978
1979 hasLoaded = true;
1980 resolve();
1981
1982 await jest.runAllTimers();
1983
1984 expect(rendered).toBe(false);
1985
1986 expect(errors).toEqual([
1987 'The render was aborted by the server without a reason.',
1988 ]);
1989 });
1990
1991 function passThrough(stream) {
1992 // Simulate more realistic network by splitting up and rejoining some chunks.
1993 // This lets us test that we don't accidentally rely on particular bounds of the chunks.
1994 return new ReadableStream({
1995 async start(controller) {
1996 const reader = stream.getReader();
1997 function push() {
1998 reader.read().then(({done, value}) => {
1999 if (done) {
2000 controller.close();
2001 return;
2002 }
2003 controller.enqueue(value);
2004 push();
2005 return;
2006 });
2007 }
2008 push();
2009 },
2010 });
2011 }
2012
2013 it('should supports streaming ReadableStream with objects', async () => {
2014 const errors = [];
2015 let controller1;
2016 let controller2;
2017 const s1 = new ReadableStream({
2018 start(c) {
2019 controller1 = c;
2020 },
2021 });
2022 const s2 = new ReadableStream({
2023 start(c) {
2024 controller2 = c;
2025 },
2026 });
2027 const rscStream = await serverAct(() =>
2028 ReactServerDOMServer.renderToReadableStream(
2029 {
2030 s1,
2031 s2,
2032 },
2033 {},
2034 {
2035 onError(x) {
2036 errors.push(x);
2037 return x;
2038 },
2039 },
2040 ),
2041 );
2042 const result = await ReactServerDOMClient.createFromReadableStream(
2043 passThrough(rscStream),
2044 );
2045
2046 const reader1 = result.s1.getReader();
2047 const reader2 = result.s2.getReader();
2048
2049 await serverAct(() => {
2050 controller1.enqueue({hello: 'world'});
2051 controller2.enqueue({hi: 'there'});
2052 });
2053
2054 expect(await reader1.read()).toEqual({
2055 value: {hello: 'world'},
2056 done: false,
2057 });
2058 expect(await reader2.read()).toEqual({
2059 value: {hi: 'there'},
2060 done: false,
2061 });
2062
2063 await serverAct(async () => {
2064 controller1.enqueue('text1');
2065 controller2.enqueue('text2');
2066 controller1.close();
2067 });
2068
2069 expect(await reader1.read()).toEqual({
2070 value: 'text1',
2071 done: false,
2072 });
2073 expect(await reader1.read()).toEqual({
2074 value: undefined,
2075 done: true,
2076 });
2077 expect(await reader2.read()).toEqual({
2078 value: 'text2',
2079 done: false,
2080 });
2081 await serverAct(async () => {
2082 controller2.error('rejected');
2083 });
2084 let error = null;
2085 try {
2086 await reader2.read();
2087 } catch (x) {
2088 error = x;
2089 }
2090 expect(error.digest).toBe('rejected');
2091 expect(errors).toEqual(['rejected']);
2092 });
2093
2094 it('should cancels the underlying ReadableStream when we are cancelled', async () => {
2095 let controller;
2096 let cancelReason;
2097 const s = new ReadableStream({
2098 start(c) {
2099 controller = c;
2100 },
2101 cancel(r) {
2102 cancelReason = r;
2103 },
2104 });
2105 let loggedReason;
2106 const rscStream = await serverAct(() =>
2107 ReactServerDOMServer.renderToReadableStream(
2108 s,
2109 {},
2110 {
2111 onError(reason) {
2112 loggedReason = reason;
2113 },
2114 },
2115 ),
2116 );
2117 const reader = rscStream.getReader();
2118 controller.enqueue('hi');
2119 const reason = new Error('aborted');
2120 reader.cancel(reason);
2121 await reader.read();
2122 expect(cancelReason).toBe(reason);
2123 expect(loggedReason).toBe(reason);
2124 });
2125
2126 it('should cancels the underlying ReadableStream when we abort', async () => {
2127 const errors = [];
2128 let controller;
2129 let cancelReason;
2130 const abortController = new AbortController();
2131 const s = new ReadableStream({
2132 start(c) {
2133 controller = c;
2134 },
2135 cancel(r) {
2136 cancelReason = r;
2137 },
2138 });
2139
2140 const rscStream = await serverAct(() =>
2141 ReactServerDOMServer.renderToReadableStream(
2142 s,
2143 {},
2144 {
2145 signal: abortController.signal,
2146 onError(x) {
2147 errors.push(x);
2148 return x.message;
2149 },
2150 },
2151 ),
2152 );
2153 const result = await ReactServerDOMClient.createFromReadableStream(
2154 passThrough(rscStream),
2155 );
2156 const reader = result.getReader();
2157
2158 controller.enqueue('hi');
2159
2160 await 0;
2161
2162 const reason = new Error('aborted');
2163 abortController.abort(reason);
2164
2165 // We should be able to read the part we already emitted before the abort
2166 expect(await reader.read()).toEqual({
2167 value: 'hi',
2168 done: false,
2169 });
2170
2171 expect(cancelReason).toBe(reason);
2172
2173 let error = null;
2174 try {
2175 await reader.read();
2176 } catch (x) {
2177 error = x;
2178 }
2179 expect(error.digest).toBe('aborted');
2180 expect(errors).toEqual([reason]);
2181 });
2182
2183 it('should supports streaming AsyncIterables with objects', async () => {
2184 let resolve;
2185 const wait = new Promise(r => (resolve = r));
2186 const errors = [];
2187 const multiShotIterable = {
2188 async *[Symbol.asyncIterator]() {
2189 const next = yield {hello: 'A'};
2190 expect(next).toBe(undefined);
2191 await wait;
2192 yield {hi: 'B'};
2193 return 'C';
2194 },
2195 };
2196 const singleShotIterator = (async function* () {
2197 const next = yield {hello: 'D'};
2198 expect(next).toBe(undefined);
2199 await wait;
2200 yield {hi: 'E'};
2201 // eslint-disable-next-line no-throw-literal
2202 throw 'F';
2203 })();
2204
2205 const rscStream = await serverAct(() =>
2206 ReactServerDOMServer.renderToReadableStream(
2207 {
2208 multiShotIterable,
2209 singleShotIterator,
2210 },
2211 {},
2212 {
2213 onError(x) {
2214 errors.push(x);
2215 return x;
2216 },
2217 },
2218 ),
2219 );
2220 const result = await ReactServerDOMClient.createFromReadableStream(
2221 passThrough(rscStream),
2222 );
2223
2224 const iterator1 = result.multiShotIterable[Symbol.asyncIterator]();
2225 const iterator2 = result.singleShotIterator[Symbol.asyncIterator]();
2226
2227 expect(iterator1).not.toBe(result.multiShotIterable);
2228 expect(iterator2).toBe(result.singleShotIterator);
2229
2230 expect(await iterator1.next()).toEqual({
2231 value: {hello: 'A'},
2232 done: false,
2233 });
2234 expect(await iterator2.next()).toEqual({
2235 value: {hello: 'D'},
2236 done: false,
2237 });
2238
2239 await serverAct(() => {
2240 resolve();
2241 });
2242
2243 expect(await iterator1.next()).toEqual({
2244 value: {hi: 'B'},
2245 done: false,
2246 });
2247 expect(await iterator2.next()).toEqual({
2248 value: {hi: 'E'},
2249 done: false,
2250 });
2251 expect(await iterator1.next()).toEqual({
2252 value: 'C', // Return value
2253 done: true,
2254 });
2255 expect(await iterator1.next()).toEqual({
2256 value: undefined,
2257 done: true,
2258 });
2259
2260 let error = null;
2261 try {
2262 await iterator2.next();
2263 } catch (x) {
2264 error = x;
2265 }
2266 expect(error.digest).toBe('F');
2267 expect(errors).toEqual(['F']);
2268
2269 // Multi-shot iterables should be able to do the same thing again
2270 const iterator3 = result.multiShotIterable[Symbol.asyncIterator]();
2271
2272 expect(iterator3).not.toBe(iterator1);
2273
2274 // We should be able to iterate over the iterable again and it should be
2275 // synchronously available using instrumented promises so that React can
2276 // rerender it synchronously.
2277 expect(iterator3.next().value).toEqual({
2278 value: {hello: 'A'},
2279 done: false,
2280 });
2281 expect(iterator3.next().value).toEqual({
2282 value: {hi: 'B'},
2283 done: false,
2284 });
2285 expect(iterator3.next().value).toEqual({
2286 value: 'C', // Return value
2287 done: true,
2288 });
2289 expect(iterator3.next().value).toEqual({
2290 value: undefined,
2291 done: true,
2292 });
2293
2294 expect(() => iterator3.next('this is not allowed')).toThrow(
2295 'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
2296 );
2297 });
2298
2299 it('should cancels the underlying AsyncIterable when we are cancelled', async () => {
2300 let resolve;
2301 const wait = new Promise(r => (resolve = r));
2302 let thrownReason;
2303 const iterator = (async function* () {
2304 try {
2305 await wait;
2306 yield 'a';
2307 yield 'b';
2308 } catch (x) {
2309 thrownReason = x;
2310 }
2311 yield 'c';
2312 })();
2313 let loggedReason;
2314
2315 const rscStream = await serverAct(() =>
2316 ReactServerDOMServer.renderToReadableStream(
2317 iterator,
2318 {},
2319 {
2320 onError(reason) {
2321 loggedReason = reason;
2322 },
2323 },
2324 ),
2325 );
2326
2327 const reader = rscStream.getReader();
2328
2329 const reason = new Error('aborted');
2330 reader.cancel(reason);
2331 await resolve();
2332 await reader.read();
2333 expect(thrownReason).toBe(reason);
2334 expect(loggedReason).toBe(reason);
2335 });
2336
2337 it('should cancels the underlying AsyncIterable when we abort', async () => {
2338 const errors = [];
2339 const abortController = new AbortController();
2340 let resolve;
2341 const wait = new Promise(r => (resolve = r));
2342 let thrownReason;
2343 const iterator = (async function* () {
2344 try {
2345 yield 'a';
2346 await wait;
2347 yield 'b';
2348 } catch (x) {
2349 thrownReason = x;
2350 }
2351 yield 'c';
2352 })();
2353 const rscStream = await serverAct(() =>
2354 ReactServerDOMServer.renderToReadableStream(
2355 iterator,
2356 {},
2357 {
2358 signal: abortController.signal,
2359 onError(x) {
2360 errors.push(x);
2361 return x.message;
2362 },
2363 },
2364 ),
2365 );
2366 const result = await ReactServerDOMClient.createFromReadableStream(
2367 passThrough(rscStream),
2368 );
2369
2370 const reason = new Error('aborted');
2371 abortController.abort(reason);
2372
2373 await serverAct(() => {
2374 resolve();
2375 });
2376
2377 // We should be able to read the part we already emitted before the abort
2378 expect(await result.next()).toEqual({
2379 value: 'a',
2380 done: false,
2381 });
2382
2383 expect(thrownReason).toBe(reason);
2384
2385 let error = null;
2386 try {
2387 await result.next();
2388 } catch (x) {
2389 error = x;
2390 }
2391 expect(error.digest).toBe('aborted');
2392 expect(errors).toEqual([reason]);
2393 });
2394
2395 it('can prerender', async () => {
2396 let resolveGreeting;
2397 const greetingPromise = new Promise(resolve => {
2398 resolveGreeting = resolve;
2399 });
2400
2401 function App() {
2402 return (
2403 <div>
2404 <Greeting />
2405 </div>
2406 );
2407 }
2408
2409 async function Greeting() {
2410 await greetingPromise;
2411 return 'hello world';
2412 }
2413
2414 const {pendingResult} = await serverAct(async () => {
2415 // destructure trick to avoid the act scope from awaiting the returned value
2416 return {
2417 pendingResult: ReactServerDOMStaticServer.prerender(
2418 <App />,
2419 webpackMap,
2420 ),
2421 };
2422 });
2423
2424 resolveGreeting();
2425 const {prelude} = await pendingResult;
2426
2427 function ClientRoot({response}) {
2428 return use(response);
2429 }
2430
2431 const response = ReactServerDOMClient.createFromReadableStream(
2432 passThrough(prelude),
2433 );
2434 const container = document.createElement('div');
2435 const root = ReactDOMClient.createRoot(container);
2436
2437 await act(() => {
2438 root.render(<ClientRoot response={response} />);
2439 });
2440 expect(container.innerHTML).toBe('<div>hello world</div>');
2441 });
2442
2443 it('does not propagate abort reasons errors when aborting a prerender', async () => {
2444 let resolveGreeting;
2445 const greetingPromise = new Promise(resolve => {
2446 resolveGreeting = resolve;
2447 });
2448
2449 function App() {
2450 return (
2451 <div>
2452 <Suspense fallback="loading...">
2453 <Greeting />
2454 </Suspense>
2455 </div>
2456 );
2457 }
2458
2459 async function Greeting() {
2460 await greetingPromise;
2461 return 'hello world';
2462 }
2463
2464 const controller = new AbortController();
2465 const errors = [];
2466 const {pendingResult} = await serverAct(async () => {
2467 // destructure trick to avoid the act scope from awaiting the returned value
2468 return {
2469 pendingResult: ReactServerDOMStaticServer.prerender(
2470 <App />,
2471 webpackMap,
2472 {
2473 signal: controller.signal,
2474 onError(err) {
2475 errors.push(err);
2476 },
2477 },
2478 ),
2479 };
2480 });
2481
2482 controller.abort('boom');
2483 resolveGreeting();
2484 const {prelude} = await serverAct(() => pendingResult);
2485 expect(errors).toEqual([]);
2486
2487 function ClientRoot({response}) {
2488 return use(response);
2489 }
2490
2491 const response = ReactServerDOMClient.createFromReadableStream(
2492 passThrough(prelude),
2493 );
2494 const container = document.createElement('div');
2495 errors.length = 0;
2496 const root = ReactDOMClient.createRoot(container, {
2497 onUncaughtError(err) {
2498 errors.push(err);
2499 },
2500 });
2501
2502 await act(() => {
2503 root.render(<ClientRoot response={response} />);
2504 });
2505
2506 expect(errors).toEqual([new Error('Connection closed.')]);
2507 expect(container.innerHTML).toBe('');
2508 });
2509
2510 it('renders Suspense fallback for unresolved promises with unstable_allowPartialStream', async () => {
2511 let resolveGreeting;
2512 const greetingPromise = new Promise(resolve => {
2513 resolveGreeting = resolve;
2514 });
2515
2516 function App() {
2517 return (
2518 <Suspense fallback="loading...">
2519 <Greeting />
2520 </Suspense>
2521 );
2522 }
2523
2524 async function Greeting() {
2525 const greeting = await greetingPromise;
2526 return greeting;
2527 }
2528
2529 const controller = new AbortController();
2530 const {pendingResult} = await serverAct(async () => {
2531 return {
2532 pendingResult: ReactServerDOMStaticServer.prerender(
2533 <App />,
2534 webpackMap,
2535 {
2536 signal: controller.signal,
2537 },
2538 ),
2539 };
2540 });
2541
2542 controller.abort();
2543 resolveGreeting('Hello, World!');
2544 const {prelude} = await serverAct(() => pendingResult);
2545
2546 function ClientRoot({response}) {
2547 return use(response);
2548 }
2549
2550 const response = ReactServerDOMClient.createFromReadableStream(
2551 passThrough(prelude),
2552 {
2553 unstable_allowPartialStream: true,
2554 },
2555 );
2556 const container = document.createElement('div');
2557 const errors = [];
2558 const root = ReactDOMClient.createRoot(container, {
2559 onUncaughtError(err) {
2560 errors.push(err);
2561 },
2562 });
2563
2564 await act(() => {
2565 root.render(<ClientRoot response={response} />);
2566 });
2567
2568 // With `unstable_allowPartialStream`, we should see the fallback instead of a
2569 // 'Connection closed.' error
2570 expect(errors).toEqual([]);
2571 expect(container.innerHTML).toBe('loading...');
2572 });
2573
2574 it('renders client components that are blocked on chunks with unstable_allowPartialStream', async () => {
2575 let resolveClientComponentChunk;
2576
2577 const ClientComponent = clientExports(
2578 function ClientComponent({children}) {
2579 return <div>{children}</div>;
2580 },
2581 '42',
2582 '/test.js',
2583 new Promise(resolve => (resolveClientComponentChunk = resolve)),
2584 );
2585
2586 function App() {
2587 return <ClientComponent>Hello, World!</ClientComponent>;
2588 }
2589
2590 const controller = new AbortController();
2591 const {pendingResult} = await serverAct(async () => {
2592 return {
2593 pendingResult: ReactServerDOMStaticServer.prerender(
2594 <App />,
2595 webpackMap,
2596 {
2597 signal: controller.signal,
2598 },
2599 ),
2600 };
2601 });
2602
2603 controller.abort();
2604 const {prelude} = await serverAct(() => pendingResult);
2605
2606 function ClientRoot({response}) {
2607 return use(response);
2608 }
2609
2610 const response = ReactServerDOMClient.createFromReadableStream(
2611 passThrough(prelude),
2612 {
2613 unstable_allowPartialStream: true,
2614 },
2615 );
2616 const container = document.createElement('div');
2617 const root = ReactDOMClient.createRoot(container);
2618
2619 await act(() => {
2620 root.render(<ClientRoot response={response} />);
2621 });
2622
2623 expect(container.innerHTML).toBe('');
2624
2625 await act(() => {
2626 resolveClientComponentChunk();
2627 });
2628
2629 expect(container.innerHTML).toBe('<div>Hello, World!</div>');
2630 });
2631
2632 it('closes inner ReadableStreams gracefully with unstable_allowPartialStream', async () => {
2633 let streamController;
2634 const innerStream = new ReadableStream({
2635 start(c) {
2636 streamController = c;
2637 },
2638 });
2639
2640 const abortController = new AbortController();
2641 const {pendingResult} = await serverAct(async () => {
2642 streamController.enqueue({hello: 'world'});
2643 return {
2644 pendingResult: ReactServerDOMStaticServer.prerender(
2645 {stream: innerStream},
2646 webpackMap,
2647 {
2648 signal: abortController.signal,
2649 },
2650 ),
2651 };
2652 });
2653
2654 abortController.abort();
2655 const {prelude} = await serverAct(() => pendingResult);
2656
2657 const response = await ReactServerDOMClient.createFromReadableStream(
2658 passThrough(prelude),
2659 {
2660 unstable_allowPartialStream: true,
2661 },
2662 );
2663
2664 // The inner stream should be readable up to what was enqueued.
2665 const reader = response.stream.getReader();
2666 const {value, done} = await reader.read();
2667 expect(value).toEqual({hello: 'world'});
2668 expect(done).toBe(false);
2669
2670 // The next read should signal the stream is done (closed, not errored).
2671 const final = await reader.read();
2672 expect(final.done).toBe(true);
2673 });
2674
2675 it('can dedupe references inside promises', async () => {
2676 const foo = {};
2677 const bar = {
2678 foo: foo,
2679 };
2680 foo.bar = bar;
2681
2682 const object = {
2683 foo: Promise.resolve(foo),
2684 bar: Promise.resolve(bar),
2685 };
2686
2687 const stream = await serverAct(() =>
2688 ReactServerDOMServer.renderToReadableStream(object, webpackMap),
2689 );
2690
2691 const response = await ReactServerDOMClient.createFromReadableStream(
2692 passThrough(stream),
2693 );
2694
2695 const responseFoo = await response.foo;
2696 const responseBar = await response.bar;
2697 expect(responseFoo.bar).toBe(responseBar);
2698 expect(responseBar.foo).toBe(responseFoo);
2699 });
2700
2701 it('can deduped outlined references inside promises', async () => {
2702 const foo = {};
2703 const bar = new Set([foo]); // This will be outlined which can create a future reference
2704 foo.bar = bar;
2705
2706 const object = {
2707 foo: Promise.resolve(foo),
2708 bar: Promise.resolve(bar),
2709 };
2710
2711 const stream = await serverAct(() =>
2712 ReactServerDOMServer.renderToReadableStream(object, webpackMap),
2713 );
2714
2715 const response = await ReactServerDOMClient.createFromReadableStream(
2716 passThrough(stream),
2717 );
2718
2719 const responseFoo = await response.foo;
2720 const responseBar = await response.bar;
2721 expect(responseFoo.bar).toBe(responseBar);
2722 expect(Array.from(responseBar)[0]).toBe(responseFoo);
2723 });
2724
2725 it('should resolve deduped references in maps used in client component props', async () => {
2726 const ClientComponent = clientExports(function ClientComponent({
2727 shared,
2728 map,
2729 }) {
2730 expect(map.get(42)).toBe(shared);
2731 return JSON.stringify({shared, map: Array.from(map)});
2732 });
2733
2734 function Server() {
2735 const shared = {id: 42};
2736 const map = new Map([[42, shared]]);
2737
2738 return <ClientComponent shared={shared} map={map} />;
2739 }
2740
2741 const stream = await serverAct(() =>
2742 ReactServerDOMServer.renderToReadableStream(<Server />, webpackMap),
2743 );
2744
2745 function ClientRoot({response}) {
2746 return use(response);
2747 }
2748
2749 const response = ReactServerDOMClient.createFromReadableStream(stream);
2750 const container = document.createElement('div');
2751 const root = ReactDOMClient.createRoot(container);
2752
2753 await act(() => {
2754 root.render(<ClientRoot response={response} />);
2755 });
2756
2757 expect(container.innerHTML).toBe(
2758 '{"shared":{"id":42},"map":[[42,{"id":42}]]}',
2759 );
2760 });
2761
2762 it('should resolve a cycle between debug info and the value it produces', async () => {
2763 function Inner({style}) {
2764 return <div style={style} />;
2765 }
2766
2767 function Component({style}) {
2768 return <Inner style={style} />;
2769 }
2770
2771 const style = {};
2772 const element = <Component style={style} />;
2773 style.element = element;
2774
2775 const stream = await serverAct(() =>
2776 ReactServerDOMServer.renderToReadableStream(element, webpackMap),
2777 );
2778
2779 function ClientRoot({response}) {
2780 return use(response);
2781 }
2782
2783 const response = ReactServerDOMClient.createFromReadableStream(stream);
2784 const container = document.createElement('div');
2785 const root = ReactDOMClient.createRoot(container);
2786
2787 await act(() => {
2788 root.render(<ClientRoot response={response} />);
2789 });
2790
2791 expect(container.innerHTML).toBe('<div></div>');
2792 });
2793
2794 it('does not close the response early when using a fast debug channel', async () => {
2795 function Component() {
2796 return <div>Hi</div>;
2797 }
2798
2799 let debugReadableStreamController;
2800
2801 const debugReadableStream = new ReadableStream({
2802 start(controller) {
2803 debugReadableStreamController = controller;
2804 },
2805 });
2806
2807 const rscStream = await serverAct(() =>
2808 ReactServerDOMServer.renderToReadableStream(<Component />, webpackMap, {
2809 debugChannel: {
2810 writable: new WritableStream({
2811 write(chunk) {
2812 debugReadableStreamController.enqueue(chunk);
2813 },
2814 close() {
2815 debugReadableStreamController.close();
2816 },
2817 }),
2818 },
2819 }),
2820 );
2821
2822 function ClientRoot({response}) {
2823 return use(response);
2824 }
2825
2826 const response = ReactServerDOMClient.createFromReadableStream(
2827 // Create a delayed stream to simulate that the RSC stream might be
2828 // transported slower than the debug channel, which must not lead to a
2829 // `Connection closed` error in the Flight client.
2830 createDelayedStream(rscStream),
2831 {
2832 debugChannel: {readable: debugReadableStream},
2833 },
2834 );
2835
2836 const container = document.createElement('div');
2837 const root = ReactDOMClient.createRoot(container);
2838
2839 await act(() => {
2840 root.render(<ClientRoot response={response} />);
2841 });
2842
2843 expect(container.innerHTML).toBe('<div>Hi</div>');
2844 });
2845
2846 it('can transport debug info through a dedicated debug channel', async () => {
2847 let ownerStack;
2848
2849 const ClientComponent = clientExports(() => {
2850 ownerStack = React.captureOwnerStack ? React.captureOwnerStack() : null;
2851 return <p>Hi</p>;
2852 });
2853
2854 function App() {
2855 return ReactServer.createElement(
2856 ReactServer.Suspense,
2857 null,
2858 ReactServer.createElement(ClientComponent, null),
2859 );
2860 }
2861
2862 let debugReadableStreamController;
2863
2864 const debugReadableStream = new ReadableStream({
2865 start(controller) {
2866 debugReadableStreamController = controller;
2867 },
2868 });
2869
2870 const rscStream = await serverAct(() =>
2871 ReactServerDOMServer.renderToReadableStream(
2872 ReactServer.createElement(App, null),
2873 webpackMap,
2874 {
2875 debugChannel: {
2876 writable: new WritableStream({
2877 write(chunk) {
2878 debugReadableStreamController.enqueue(chunk);
2879 },
2880 close() {
2881 debugReadableStreamController.close();
2882 },
2883 }),
2884 },
2885 },
2886 ),
2887 );
2888
2889 function ClientRoot({response}) {
2890 return use(response);
2891 }
2892
2893 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
2894 replayConsoleLogs: true,
2895 debugChannel: {
2896 readable: debugReadableStream,
2897 // Explicitly not defining a writable side here. Its presence was
2898 // previously used as a condition to wait for referenced debug chunks.
2899 },
2900 });
2901
2902 const container = document.createElement('div');
2903 const root = ReactDOMClient.createRoot(container);
2904
2905 await act(() => {
2906 root.render(<ClientRoot response={response} />);
2907 });
2908
2909 if (__DEV__) {
2910 expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
2911 }
2912
2913 expect(container.innerHTML).toBe('<p>Hi</p>');
2914 });
2915
2916 it('should not have missing key warnings when a static child is blocked on debug info', async () => {
2917 const ClientComponent = clientExports(function ClientComponent({element}) {
2918 return (
2919 <div>
2920 <span>Hi</span>
2921 {element}
2922 </div>
2923 );
2924 });
2925
2926 let debugReadableStreamController;
2927
2928 const debugReadableStream = new ReadableStream({
2929 start(controller) {
2930 debugReadableStreamController = controller;
2931 },
2932 });
2933
2934 const stream = await serverAct(() =>
2935 ReactServerDOMServer.renderToReadableStream(
2936 <ClientComponent element={<span>Sebbie</span>} />,
2937 webpackMap,
2938 {
2939 debugChannel: {
2940 writable: new WritableStream({
2941 write(chunk) {
2942 debugReadableStreamController.enqueue(chunk);
2943 },
2944 close() {
2945 debugReadableStreamController.close();
2946 },
2947 }),
2948 },
2949 },
2950 ),
2951 );
2952
2953 function ClientRoot({response}) {
2954 return use(response);
2955 }
2956
2957 const response = ReactServerDOMClient.createFromReadableStream(stream, {
2958 debugChannel: {readable: createDelayedStream(debugReadableStream)},
2959 });
2960
2961 const container = document.createElement('div');
2962 const root = ReactDOMClient.createRoot(container);
2963
2964 await act(() => {
2965 root.render(<ClientRoot response={response} />);
2966 });
2967
2968 // Wait for the debug info to be processed.
2969 await act(() => {});
2970
2971 expect(container.innerHTML).toBe(
2972 '<div><span>Hi</span><span>Sebbie</span></div>',
2973 );
2974 });
2975
2976 it('should fully resolve debug info when transported through a (slow) debug channel', async () => {
2977 function Paragraph({children}) {
2978 return ReactServer.createElement('p', null, children);
2979 }
2980
2981 let debugReadableStreamController;
2982
2983 const debugReadableStream = new ReadableStream({
2984 start(controller) {
2985 debugReadableStreamController = controller;
2986 },
2987 });
2988
2989 const app = ReactServer.createElement(
2990 ReactServer.Fragment,
2991 null,
2992 ReactServer.createElement(Paragraph, null, 'foo'),
2993 ReactServer.createElement(Paragraph, null, 'bar'),
2994 );
2995 const stream = await serverAct(() =>
2996 ReactServerDOMServer.renderToReadableStream(
2997 {
2998 root: app,
2999 },
3000 webpackMap,
3001 {
3002 debugChannel: {
3003 writable: new WritableStream({
3004 write(chunk) {
3005 debugReadableStreamController.enqueue(chunk);
3006 },
3007 close() {
3008 debugReadableStreamController.close();
3009 },
3010 }),
3011 },
3012 },
3013 ),
3014 );
3015
3016 function ClientRoot({response}) {
3017 const {root} = use(response);
3018 return root;
3019 }
3020
3021 const [slowDebugStream1, slowDebugStream2] =
3022 createDelayedStream(debugReadableStream).tee();
3023
3024 const response = ReactServerDOMClient.createFromReadableStream(stream, {
3025 debugChannel: {readable: slowDebugStream1},
3026 });
3027
3028 const container = document.createElement('div');
3029 const clientRoot = ReactDOMClient.createRoot(container);
3030
3031 await act(() => {
3032 clientRoot.render(<ClientRoot response={response} />);
3033 });
3034
3035 if (__DEV__) {
3036 const debugStreamReader = slowDebugStream2.getReader();
3037 while (true) {
3038 const {done} = await debugStreamReader.read();
3039 if (done) {
3040 break;
3041 }
3042 // Allow the client to process each debug chunk as it arrives.
3043 await act(() => {});
3044 }
3045 }
3046
3047 expect(container.innerHTML).toBe('<p>foo</p><p>bar</p>');
3048
3049 if (
3050 __DEV__ &&
3051 gate(
3052 flags =>
3053 flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
3054 )
3055 ) {
3056 const result = await response;
3057 const firstParagraph = result.root[0];
3058
3059 expect(getDebugInfo(firstParagraph)).toMatchInlineSnapshot(`
3060 [
3061 {
3062 "time": 0,
3063 },
3064 {
3065 "env": "Server",
3066 "key": null,
3067 "name": "Paragraph",
3068 "props": {},
3069 "stack": [
3070 [
3071 "Object.<anonymous>",
3072 "/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js",
3073 2992,
3074 19,
3075 2976,
3076 89,
3077 ],
3078 ],
3079 },
3080 {
3081 "time": 0,
3082 },
3083 ]
3084 `);
3085 }
3086 });
3087
3088 it('should abort the cache signal when a render completes while debug objects are still retained', async () => {
3089 // A debug channel with a readable side lets the client fetch debug objects
3090 // lazily. React serializes each component's props into the debug model, and
3091 // it defers the part of an object tree that exceeds the model's object
3092 // limit. A deferred object stays retained, and its debug chunk stays
3093 // pending, until the client asks for it or the channel closes. The render
3094 // below finishes while one such object is outstanding.
3095 function createDeepJSX(n) {
3096 if (n <= 0) {
3097 return null;
3098 }
3099 return <div>{createDeepJSX(n - 1)}</div>;
3100 }
3101
3102 let cacheSignal;
3103
3104 function ServerComponent() {
3105 cacheSignal = ReactServer.cacheSignal();
3106 return <div>not using props</div>;
3107 }
3108
3109 let debugChannelReadableController;
3110 const debugChunks = [];
3111
3112 const debugChannelReadable = new ReadableStream({
3113 start(controller) {
3114 debugChannelReadableController = controller;
3115 },
3116 });
3117
3118 const stream = await serverAct(() =>
3119 ReactServerDOMServer.renderToReadableStream(
3120 // These children nest deeper than the debug model's object limit.
3121 <ServerComponent>{createDeepJSX(20)}</ServerComponent>,
3122 webpackMap,
3123 {
3124 debugChannel: {
3125 readable: debugChannelReadable,
3126 writable: new WritableStream({
3127 write(chunk) {
3128 debugChunks.push(chunk);
3129 },
3130 }),
3131 },
3132 },
3133 ),
3134 );
3135
3136 const reader = stream.getReader();
3137 while (true) {
3138 const {done} = await reader.read();
3139 if (done) {
3140 break;
3141 }
3142 }
3143 await serverAct(() => {});
3144
3145 if (__DEV__) {
3146 // Fail loudly if the setup stops deferring anything, for example because
3147 // the object limit changed. Without a retained object this test passes
3148 // for the wrong reason.
3149 const debugOutput = debugChunks
3150 .map(chunk => new TextDecoder().decode(chunk))
3151 .join('');
3152 expect(debugOutput).toContain('$Y');
3153 }
3154
3155 expect(cacheSignal.aborted).toBe(true);
3156
3157 // Closing the debug channel drops the retained objects. The signal must
3158 // already be aborted at that point, and must stay aborted.
3159 await serverAct(() => {
3160 debugChannelReadableController.close();
3161 });
3162 await serverAct(() => {});
3163
3164 expect(cacheSignal.aborted).toBe(true);
3165 });
3166
3167 it('should resolve a cycle between debug info and the value it produces when using a debug channel', async () => {
3168 // Same as `should resolve a cycle between debug info and the value it produces`, but using a debug channel.
3169
3170 function Inner({style}) {
3171 return <div style={style} />;
3172 }
3173
3174 function Component({style}) {
3175 return <Inner style={style} />;
3176 }
3177
3178 const style = {};
3179 const element = <Component style={style} />;
3180 style.element = element;
3181
3182 let debugReadableStreamController;
3183
3184 const debugReadableStream = new ReadableStream({
3185 start(controller) {
3186 debugReadableStreamController = controller;
3187 },
3188 });
3189
3190 const rscStream = await serverAct(() =>
3191 ReactServerDOMServer.renderToReadableStream(element, webpackMap, {
3192 debugChannel: {
3193 writable: new WritableStream({
3194 write(chunk) {
3195 debugReadableStreamController.enqueue(chunk);
3196 },
3197 close() {
3198 debugReadableStreamController.close();
3199 },
3200 }),
3201 },
3202 }),
3203 );
3204
3205 function ClientRoot({response}) {
3206 return use(response);
3207 }
3208
3209 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
3210 debugChannel: {readable: debugReadableStream},
3211 });
3212
3213 const container = document.createElement('div');
3214 const root = ReactDOMClient.createRoot(container);
3215
3216 await act(() => {
3217 root.render(<ClientRoot response={response} />);
3218 });
3219
3220 expect(container.innerHTML).toBe('<div></div>');
3221 });
3222
3223 // Long enough to exceed MAX_ROW_SIZE in ReactFlightServer, which makes the
3224 // element prop that follows it be outlined into its own row.
3225 const longText = 'a'.repeat(4000);
3226
3227 it('should not have missing key warnings when a static child is outlined', async () => {
3228 const ClientComponent = clientExports(function ClientComponent({
3229 text,
3230 element,
3231 }) {
3232 return (
3233 <div>
3234 <span>{text.length}</span>
3235 {element}
3236 </div>
3237 );
3238 });
3239
3240 const stream = await serverAct(() =>
3241 ReactServerDOMServer.renderToReadableStream(
3242 <ClientComponent text={longText} element={<span>Hello</span>} />,
3243 webpackMap,
3244 ),
3245 );
3246
3247 function ClientRoot({response}) {
3248 return use(response);
3249 }
3250
3251 const response = ReactServerDOMClient.createFromReadableStream(stream);
3252
3253 const container = document.createElement('div');
3254 const root = ReactDOMClient.createRoot(container);
3255
3256 await act(() => {
3257 root.render(<ClientRoot response={response} />);
3258 });
3259
3260 expect(container.innerHTML).toBe(
3261 '<div><span>4000</span><span>Hello</span></div>',
3262 );
3263 });
3264
3265 it('should not have missing key warnings when an outlined static child is blocked on debug info', async () => {
3266 const ClientComponent = clientExports(function ClientComponent({
3267 text,
3268 element,
3269 }) {
3270 return (
3271 <div>
3272 <span>{text.length}</span>
3273 {element}
3274 </div>
3275 );
3276 });
3277
3278 let debugReadableStreamController;
3279
3280 const debugReadableStream = new ReadableStream({
3281 start(controller) {
3282 debugReadableStreamController = controller;
3283 },
3284 });
3285
3286 const stream = await serverAct(() =>
3287 ReactServerDOMServer.renderToReadableStream(
3288 <ClientComponent text={longText} element={<span>Hello</span>} />,
3289 webpackMap,
3290 {
3291 debugChannel: {
3292 writable: new WritableStream({
3293 write(chunk) {
3294 debugReadableStreamController.enqueue(chunk);
3295 },
3296 close() {
3297 debugReadableStreamController.close();
3298 },
3299 }),
3300 },
3301 },
3302 ),
3303 );
3304
3305 function ClientRoot({response}) {
3306 return use(response);
3307 }
3308
3309 const response = ReactServerDOMClient.createFromReadableStream(stream, {
3310 debugChannel: {readable: createDelayedStream(debugReadableStream)},
3311 });
3312
3313 const container = document.createElement('div');
3314 const root = ReactDOMClient.createRoot(container);
3315
3316 await act(() => {
3317 root.render(<ClientRoot response={response} />);
3318 });
3319
3320 // Wait for the debug info to be processed.
3321 await act(() => {});
3322
3323 expect(container.innerHTML).toBe(
3324 '<div><span>4000</span><span>Hello</span></div>',
3325 );
3326 });
3327
3328 it('should have missing key warnings when an outlined element is used in an array', async () => {
3329 const ClientComponent = clientExports(function ClientComponent({
3330 text,
3331 element,
3332 }) {
3333 return (
3334 <div>
3335 <span>{text.length}</span>
3336 {[element]}
3337 </div>
3338 );
3339 });
3340
3341 const stream = await serverAct(() =>
3342 ReactServerDOMServer.renderToReadableStream(
3343 <ClientComponent text={longText} element={<span>Hello</span>} />,
3344 webpackMap,
3345 ),
3346 );
3347
3348 function ClientRoot({response}) {
3349 return use(response);
3350 }
3351
3352 const response = ReactServerDOMClient.createFromReadableStream(stream);
3353
3354 const container = document.createElement('div');
3355 const root = ReactDOMClient.createRoot(container);
3356
3357 await act(() => {
3358 root.render(<ClientRoot response={response} />);
3359 });
3360
3361 assertConsoleErrorDev([
3362 'Each child in a list should have a unique "key" prop.\n\n' +
3363 'Check the render method of `div`. ' +
3364 'See https://react.dev/link/warning-keys for more information.\n' +
3365 ' in span (at **)',
3366 ]);
3367
3368 expect(container.innerHTML).toBe(
3369 '<div><span>4000</span><span>Hello</span></div>',
3370 );
3371 });
3372
3373 it('should have missing key warnings when an outlined element that is blocked on debug info is used in an array', async () => {
3374 const ClientComponent = clientExports(function ClientComponent({
3375 text,
3376 element,
3377 }) {
3378 return (
3379 <div>
3380 <span>{text.length}</span>
3381 {[element]}
3382 </div>
3383 );
3384 });
3385
3386 let debugReadableStreamController;
3387
3388 const debugReadableStream = new ReadableStream({
3389 start(controller) {
3390 debugReadableStreamController = controller;
3391 },
3392 });
3393
3394 const stream = await serverAct(() =>
3395 ReactServerDOMServer.renderToReadableStream(
3396 <ClientComponent text={longText} element={<span>Hello</span>} />,
3397 webpackMap,
3398 {
3399 debugChannel: {
3400 writable: new WritableStream({
3401 write(chunk) {
3402 debugReadableStreamController.enqueue(chunk);
3403 },
3404 close() {
3405 debugReadableStreamController.close();
3406 },
3407 }),
3408 },
3409 },
3410 ),
3411 );
3412
3413 function ClientRoot({response}) {
3414 return use(response);
3415 }
3416
3417 const response = ReactServerDOMClient.createFromReadableStream(stream, {
3418 debugChannel: {readable: createDelayedStream(debugReadableStream)},
3419 });
3420
3421 const container = document.createElement('div');
3422 const root = ReactDOMClient.createRoot(container);
3423
3424 await act(() => {
3425 root.render(<ClientRoot response={response} />);
3426 });
3427
3428 // The element can only be rendered, and therefore validated, after it's
3429 // unblocked by the debug info.
3430 await act(() => {});
3431
3432 assertConsoleErrorDev([
3433 'Each child in a list should have a unique "key" prop.\n\n' +
3434 'Check the render method of `div`. ' +
3435 'See https://react.dev/link/warning-keys for more information.\n' +
3436 ' in span (at **)',
3437 ]);
3438
3439 expect(container.innerHTML).toBe(
3440 '<div><span>4000</span><span>Hello</span></div>',
3441 );
3442 });
3443
3444 describe('abort signal lifetime', () => {
3445 // Collects the lifetime signal that React bounds each abort listener with.
3446 // React passes that signal to addEventListener instead of calling
3447 // removeEventListener, so the runtime performs the removal and nothing here
3448 // observes it directly. An aborted lifetime is what shows the listener is
3449 // gone. ReactFlightDOMNode-test asserts the removal itself, which needs a
3450 // Node API that jsdom does not have.
3451 function trackAbortListenerLifetimes(signal) {
3452 const lifetimes = [];
3453 const add = signal.addEventListener.bind(signal);
3454 signal.addEventListener = (type, listener, options) => {
3455 if (type === 'abort') {
3456 lifetimes.push(options.signal);
3457 }
3458 return add(type, listener, options);
3459 };
3460 return lifetimes;
3461 }
3462
3463 async function drain(stream) {
3464 const reader = stream.getReader();
3465 while (true) {
3466 const {done} = await reader.read();
3467 if (done) {
3468 return;
3469 }
3470 }
3471 }
3472
3473 function App() {
3474 return <div>hello world</div>;
3475 }
3476
3477 it('detaches the listener when a prerender completes', async () => {
3478 const controller = new AbortController();
3479 const lifetimes = trackAbortListenerLifetimes(controller.signal);
3480
3481 const {prelude} = await serverAct(() =>
3482 ReactServerDOMStaticServer.prerender(<App />, webpackMap, {
3483 signal: controller.signal,
3484 }),
3485 );
3486 expect(lifetimes).toHaveLength(1);
3487 expect(lifetimes[0].aborted).toBe(false);
3488
3489 await serverAct(() => drain(prelude));
3490 expect(lifetimes[0].aborted).toBe(true);
3491 });
3492
3493 it('detaches the listener when a render completes', async () => {
3494 const controller = new AbortController();
3495 const lifetimes = trackAbortListenerLifetimes(controller.signal);
3496
3497 const stream = await serverAct(() =>
3498 ReactServerDOMServer.renderToReadableStream(<App />, webpackMap, {
3499 signal: controller.signal,
3500 }),
3501 );
3502 expect(lifetimes).toHaveLength(1);
3503 expect(lifetimes[0].aborted).toBe(false);
3504
3505 await serverAct(() => drain(stream));
3506 expect(lifetimes[0].aborted).toBe(true);
3507 });
3508
3509 it('detaches the listener when the signal aborts mid-render', async () => {
3510 let resolveGreeting;
3511 const greetingPromise = new Promise(resolve => {
3512 resolveGreeting = resolve;
3513 });
3514
3515 async function Greeting() {
3516 await greetingPromise;
3517 return 'hello world';
3518 }
3519
3520 const controller = new AbortController();
3521 const lifetimes = trackAbortListenerLifetimes(controller.signal);
3522
3523 const {pendingResult} = await serverAct(async () => {
3524 return {
3525 pendingResult: ReactServerDOMStaticServer.prerender(
3526 <Greeting />,
3527 webpackMap,
3528 {signal: controller.signal, onError() {}},
3529 ),
3530 };
3531 });
3532 expect(lifetimes).toHaveLength(1);
3533 expect(lifetimes[0].aborted).toBe(false);
3534
3535 controller.abort('boom');
3536 resolveGreeting();
3537 await serverAct(() => pendingResult);
3538
3539 expect(lifetimes[0].aborted).toBe(true);
3540 });
3541
3542 it('detaches the listener when the stream is cancelled', async () => {
3543 let resolveGreeting;
3544 const greetingPromise = new Promise(resolve => {
3545 resolveGreeting = resolve;
3546 });
3547
3548 async function Greeting() {
3549 await greetingPromise;
3550 return 'hello world';
3551 }
3552
3553 const controller = new AbortController();
3554 const lifetimes = trackAbortListenerLifetimes(controller.signal);
3555
3556 const stream = await serverAct(() =>
3557 ReactServerDOMServer.renderToReadableStream(<Greeting />, webpackMap, {
3558 signal: controller.signal,
3559 onError() {},
3560 }),
3561 );
3562 expect(lifetimes).toHaveLength(1);
3563 expect(lifetimes[0].aborted).toBe(false);
3564
3565 await serverAct(() => stream.cancel('boom'));
3566 resolveGreeting();
3567
3568 expect(lifetimes[0].aborted).toBe(true);
3569 });
3570
3571 it('attaches no listener when the signal is already aborted', async () => {
3572 const controller = new AbortController();
3573 controller.abort('boom');
3574 const lifetimes = trackAbortListenerLifetimes(controller.signal);
3575
3576 await serverAct(() =>
3577 ReactServerDOMStaticServer.prerender(<App />, webpackMap, {
3578 signal: controller.signal,
3579 onError() {},
3580 }),
3581 );
3582
3583 expect(lifetimes).toHaveLength(0);
3584 });
3585
3586 // The composite-signal case lives in ReactFlightDOMNode-test, because
3587 // jsdom's AbortSignal has no AbortSignal.any.
3588 });
3589
3590 describe('with console.createTask', () => {
3591 // Stands in for what a browser console does with fake tasks: whatever runs
3592 // inside a task is shown under that task's name in the async stack. This is
3593 // the same setup that `ReactServer-test` uses to assert on task names.
3594 let currentTask;
3595
3596 beforeEach(() => {
3597 const {AsyncLocalStorage} = require('node:async_hooks');
3598 currentTask = new AsyncLocalStorage();
3599 (console: any).createTask = taskName => ({
3600 run: taskFn => {
3601 const parentTask = currentTask.getStore() || '';
3602 return currentTask.run(parentTask + '\n' + taskName, taskFn);
3603 },
3604 });
3605
3606 // `supportsCreateTask` is captured when ReactFlightClient is required, so
3607 // the client modules need to be required again with this in place.
3608 jest.resetModules();
3609 patchMessageChannel();
3610 ({act} = require('internal-test-utils'));
3611 React = require('react');
3612 use = React.use;
3613 ReactDOMClient = require('react-dom/client');
3614 ReactServerDOMClient = require('react-server-dom-webpack/client');
3615 });
3616
3617 afterEach(() => {
3618 delete (console: any).createTask;
3619 });
3620
3621 // @gate __DEV__
3622 it('renders a client component inside a "use client" task', async () => {
3623 let taskWhileRendering;
3624
3625 const ClientComponent = clientExports(function ClientComponent() {
3626 taskWhileRendering = currentTask.getStore();
3627 return <span>Hello</span>;
3628 });
3629
3630 const stream = await serverAct(() =>
3631 ReactServerDOMServer.renderToReadableStream(
3632 <ClientComponent />,
3633 webpackMap,
3634 ),
3635 );
3636
3637 function ClientRoot({response}) {
3638 return use(response);
3639 }
3640
3641 const response = ReactServerDOMClient.createFromReadableStream(stream);
3642
3643 const container = document.createElement('div');
3644 const root = ReactDOMClient.createRoot(container);
3645
3646 await act(() => {
3647 root.render(<ClientRoot response={response} />);
3648 });
3649
3650 expect(container.innerHTML).toBe('<span>Hello</span>');
3651 // The element's type is a lazy node wrapping the client reference, so the
3652 // task that the component renders in marks the boundary into the client.
3653 expect(taskWhileRendering).toBe('\n"use client"');
3654 });
3655 });
3656 });