main
js 989 lines 23.3 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 * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
9 */
10
11 'use strict';
12 import {
13 insertNodesAndExecuteScripts,
14 getVisibleChildren,
15 } from '../test-utils/FizzTestUtils';
16
17 let JSDOM;
18 let React;
19 let Suspense;
20 let SuspenseList;
21 let assertLog;
22 let Scheduler;
23 let ReactDOMFizzServer;
24 let Stream;
25 let document;
26 let writable;
27 let container;
28 let buffer = '';
29 let hasErrored = false;
30 let hasCompleted = false;
31 let fatalError = undefined;
32
33 describe('ReactDOMFizzSuspenseList', () => {
34 beforeEach(() => {
35 jest.resetModules();
36 JSDOM = require('jsdom').JSDOM;
37 React = require('react');
38 assertLog = require('internal-test-utils').assertLog;
39 ReactDOMFizzServer = require('react-dom/server');
40 Stream = require('stream');
41
42 Suspense = React.Suspense;
43 SuspenseList = React.unstable_SuspenseList;
44
45 Scheduler = require('scheduler');
46
47 // Test Environment
48 const jsdom = new JSDOM(
49 '<!DOCTYPE html><html><head></head><body><div id="container">',
50 {
51 runScripts: 'dangerously',
52 },
53 );
54 document = jsdom.window.document;
55 container = document.getElementById('container');
56 global.window = jsdom.window;
57 // The Fizz runtime assumes requestAnimationFrame exists so we need to polyfill it.
58 global.requestAnimationFrame = global.window.requestAnimationFrame = cb =>
59 setTimeout(cb);
60
61 buffer = '';
62 hasErrored = false;
63 hasCompleted = false;
64
65 writable = new Stream.PassThrough();
66 writable.setEncoding('utf8');
67 writable.on('data', chunk => {
68 buffer += chunk;
69 });
70 writable.on('error', error => {
71 hasErrored = true;
72 fatalError = error;
73 });
74 writable.on('finish', () => {
75 hasCompleted = true;
76 });
77 });
78
79 afterEach(() => {
80 jest.restoreAllMocks();
81 });
82
83 async function serverAct(callback) {
84 await callback();
85 // Await one turn around the event loop.
86 // This assumes that we'll flush everything we have so far.
87 await new Promise(resolve => {
88 setImmediate(resolve);
89 });
90 if (hasErrored) {
91 throw fatalError;
92 }
93 // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
94 // We also want to execute any scripts that are embedded.
95 // We assume that we have now received a proper fragment of HTML.
96 const bufferedContent = buffer;
97 buffer = '';
98 const temp = document.createElement('body');
99 temp.innerHTML = bufferedContent;
100 await insertNodesAndExecuteScripts(temp, container, null);
101 jest.runAllTimers();
102 }
103
104 function Text(props) {
105 Scheduler.log(props.text);
106 return <span>{props.text}</span>;
107 }
108
109 function createAsyncText(text) {
110 let resolved = false;
111 let error = undefined;
112 const Component = function () {
113 if (error !== undefined) {
114 Scheduler.log('Error! [' + error.message + ']');
115 throw error;
116 }
117 if (!resolved) {
118 Scheduler.log('Suspend! [' + text + ']');
119 throw promise;
120 }
121 return <Text text={text} />;
122 };
123 const promise = new Promise(resolve => {
124 Component.resolve = function () {
125 resolved = true;
126 return resolve();
127 };
128 Component.reject = function (e) {
129 error = e;
130 return resolve();
131 };
132 });
133 return Component;
134 }
135
136 // @gate enableSuspenseList
137 it('shows content forwards by default', async () => {
138 const A = createAsyncText('A');
139 const B = createAsyncText('B');
140 const C = createAsyncText('C');
141
142 function Foo() {
143 return (
144 <div>
145 <SuspenseList>
146 <Suspense fallback={<Text text="Loading A" />}>
147 <A />
148 </Suspense>
149 <Suspense fallback={<Text text="Loading B" />}>
150 <B />
151 </Suspense>
152 <Suspense fallback={<Text text="Loading C" />}>
153 <C />
154 </Suspense>
155 </SuspenseList>
156 </div>
157 );
158 }
159
160 await C.resolve();
161
162 await serverAct(async () => {
163 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
164 pipe(writable);
165 });
166
167 assertLog([
168 'Suspend! [A]',
169 'Suspend! [B]', // TODO: Defer rendering the content after fallback if previous suspended,
170 'C',
171 'Loading A',
172 'Loading B',
173 'Loading C',
174 ]);
175
176 expect(getVisibleChildren(container)).toEqual(
177 <div>
178 <span>Loading A</span>
179 <span>Loading B</span>
180 <span>Loading C</span>
181 </div>,
182 );
183
184 await serverAct(() => A.resolve());
185 assertLog(['A']);
186
187 expect(getVisibleChildren(container)).toEqual(
188 <div>
189 <span>A</span>
190 <span>Loading B</span>
191 <span>Loading C</span>
192 </div>,
193 );
194
195 await serverAct(() => B.resolve());
196 assertLog(['B']);
197
198 expect(getVisibleChildren(container)).toEqual(
199 <div>
200 <span>A</span>
201 <span>B</span>
202 <span>C</span>
203 </div>,
204 );
205 });
206
207 // @gate enableSuspenseList
208 it('independently with revealOrder="independent"', async () => {
209 const A = createAsyncText('A');
210 const B = createAsyncText('B');
211 const C = createAsyncText('C');
212
213 function Foo() {
214 return (
215 <div>
216 <SuspenseList revealOrder="independent">
217 <Suspense fallback={<Text text="Loading A" />}>
218 <A />
219 </Suspense>
220 <Suspense fallback={<Text text="Loading B" />}>
221 <B />
222 </Suspense>
223 <Suspense fallback={<Text text="Loading C" />}>
224 <C />
225 </Suspense>
226 </SuspenseList>
227 </div>
228 );
229 }
230
231 await A.resolve();
232
233 await serverAct(async () => {
234 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
235 pipe(writable);
236 });
237
238 assertLog(['A', 'Suspend! [B]', 'Suspend! [C]', 'Loading B', 'Loading C']);
239
240 expect(getVisibleChildren(container)).toEqual(
241 <div>
242 <span>A</span>
243 <span>Loading B</span>
244 <span>Loading C</span>
245 </div>,
246 );
247
248 await serverAct(() => C.resolve());
249 assertLog(['C']);
250
251 expect(getVisibleChildren(container)).toEqual(
252 <div>
253 <span>A</span>
254 <span>Loading B</span>
255 <span>C</span>
256 </div>,
257 );
258
259 await serverAct(() => B.resolve());
260 assertLog(['B']);
261
262 expect(getVisibleChildren(container)).toEqual(
263 <div>
264 <span>A</span>
265 <span>B</span>
266 <span>C</span>
267 </div>,
268 );
269 });
270
271 // @gate enableSuspenseList
272 it('displays all "together"', async () => {
273 const A = createAsyncText('A');
274 const B = createAsyncText('B');
275 const C = createAsyncText('C');
276
277 function Foo() {
278 return (
279 <div>
280 <SuspenseList revealOrder="together">
281 <Suspense fallback={<Text text="Loading A" />}>
282 <A />
283 </Suspense>
284 <Suspense fallback={<Text text="Loading B" />}>
285 <B />
286 </Suspense>
287 <Suspense fallback={<Text text="Loading C" />}>
288 <C />
289 </Suspense>
290 </SuspenseList>
291 </div>
292 );
293 }
294
295 await A.resolve();
296
297 await serverAct(async () => {
298 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
299 pipe(writable);
300 });
301
302 assertLog([
303 'A',
304 'Suspend! [B]',
305 'Suspend! [C]',
306 'Loading A',
307 'Loading B',
308 'Loading C',
309 ]);
310
311 expect(getVisibleChildren(container)).toEqual(
312 <div>
313 <span>Loading A</span>
314 <span>Loading B</span>
315 <span>Loading C</span>
316 </div>,
317 );
318
319 await serverAct(() => B.resolve());
320 assertLog(['B']);
321
322 expect(getVisibleChildren(container)).toEqual(
323 <div>
324 <span>Loading A</span>
325 <span>Loading B</span>
326 <span>Loading C</span>
327 </div>,
328 );
329
330 await serverAct(() => C.resolve());
331 assertLog(['C']);
332
333 expect(getVisibleChildren(container)).toEqual(
334 <div>
335 <span>A</span>
336 <span>B</span>
337 <span>C</span>
338 </div>,
339 );
340 });
341
342 // @gate enableSuspenseList
343 it('displays all "together" in a single pass', async () => {
344 function Foo() {
345 return (
346 <div>
347 <SuspenseList revealOrder="together">
348 <Suspense fallback={<Text text="Loading A" />}>
349 <Text text="A" />
350 </Suspense>
351 <Suspense fallback={<Text text="Loading B" />}>
352 <Text text="B" />
353 </Suspense>
354 <Suspense fallback={<Text text="Loading C" />}>
355 <Text text="C" />
356 </Suspense>
357 </SuspenseList>
358 </div>
359 );
360 }
361
362 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
363 pipe(writable);
364 await 0;
365 const bufferedContent = buffer;
366 buffer = '';
367
368 assertLog(['A', 'B', 'C', 'Loading A', 'Loading B', 'Loading C']);
369
370 expect(bufferedContent).toMatchInlineSnapshot(
371 `"<div><!--$--><span>A</span><!--/$--><!--$--><span>B</span><!--/$--><!--$--><span>C</span><!--/$--></div>"`,
372 );
373 });
374
375 // @gate enableSuspenseList
376 it('displays all "together" even when nested as siblings', async () => {
377 const A = createAsyncText('A');
378 const B = createAsyncText('B');
379 const C = createAsyncText('C');
380
381 function Foo() {
382 return (
383 <div>
384 <SuspenseList revealOrder="together">
385 <div>
386 <Suspense fallback={<Text text="Loading A" />}>
387 <A />
388 </Suspense>
389 <Suspense fallback={<Text text="Loading B" />}>
390 <B />
391 </Suspense>
392 </div>
393 <div>
394 <Suspense fallback={<Text text="Loading C" />}>
395 <C />
396 </Suspense>
397 </div>
398 </SuspenseList>
399 </div>
400 );
401 }
402
403 await A.resolve();
404
405 await serverAct(async () => {
406 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
407 pipe(writable);
408 });
409
410 assertLog([
411 'A',
412 'Suspend! [B]',
413 'Suspend! [C]',
414 'Loading A',
415 'Loading B',
416 'Loading C',
417 ]);
418
419 expect(getVisibleChildren(container)).toEqual(
420 <div>
421 <div>
422 <span>Loading A</span>
423 <span>Loading B</span>
424 </div>
425 <div>
426 <span>Loading C</span>
427 </div>
428 </div>,
429 );
430
431 await serverAct(() => B.resolve());
432 assertLog(['B']);
433
434 expect(getVisibleChildren(container)).toEqual(
435 <div>
436 <div>
437 <span>Loading A</span>
438 <span>Loading B</span>
439 </div>
440 <div>
441 <span>Loading C</span>
442 </div>
443 </div>,
444 );
445
446 await serverAct(() => C.resolve());
447 assertLog(['C']);
448
449 expect(getVisibleChildren(container)).toEqual(
450 <div>
451 <div>
452 <span>A</span>
453 <span>B</span>
454 </div>
455 <div>
456 <span>C</span>
457 </div>
458 </div>,
459 );
460 });
461
462 // @gate enableSuspenseList
463 it('displays all "together" in nested SuspenseLists', async () => {
464 const A = createAsyncText('A');
465 const B = createAsyncText('B');
466 const C = createAsyncText('C');
467
468 function Foo() {
469 return (
470 <div>
471 <SuspenseList revealOrder="together">
472 <Suspense fallback={<Text text="Loading A" />}>
473 <A />
474 </Suspense>
475 <SuspenseList revealOrder="together">
476 <Suspense fallback={<Text text="Loading B" />}>
477 <B />
478 </Suspense>
479 <Suspense fallback={<Text text="Loading C" />}>
480 <C />
481 </Suspense>
482 </SuspenseList>
483 </SuspenseList>
484 </div>
485 );
486 }
487
488 await A.resolve();
489 await B.resolve();
490
491 await serverAct(async () => {
492 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
493 pipe(writable);
494 });
495
496 assertLog([
497 'A',
498 'B',
499 'Suspend! [C]',
500 'Loading A',
501 'Loading B',
502 'Loading C',
503 ]);
504
505 expect(getVisibleChildren(container)).toEqual(
506 <div>
507 <span>Loading A</span>
508 <span>Loading B</span>
509 <span>Loading C</span>
510 </div>,
511 );
512
513 await serverAct(() => C.resolve());
514 assertLog(['C']);
515
516 expect(getVisibleChildren(container)).toEqual(
517 <div>
518 <span>A</span>
519 <span>B</span>
520 <span>C</span>
521 </div>,
522 );
523 });
524
525 // @gate enableSuspenseList
526 it('displays all "together" in nested SuspenseLists where the inner is "independent"', async () => {
527 const A = createAsyncText('A');
528 const B = createAsyncText('B');
529 const C = createAsyncText('C');
530
531 function Foo() {
532 return (
533 <div>
534 <SuspenseList revealOrder="together">
535 <Suspense fallback={<Text text="Loading A" />}>
536 <A />
537 </Suspense>
538 <SuspenseList revealOrder="independent">
539 <Suspense fallback={<Text text="Loading B" />}>
540 <B />
541 </Suspense>
542 <Suspense fallback={<Text text="Loading C" />}>
543 <C />
544 </Suspense>
545 </SuspenseList>
546 </SuspenseList>
547 </div>
548 );
549 }
550
551 await A.resolve();
552 await B.resolve();
553
554 await serverAct(async () => {
555 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
556 pipe(writable);
557 });
558
559 assertLog([
560 'A',
561 'B',
562 'Suspend! [C]',
563 'Loading A',
564 'Loading B',
565 'Loading C',
566 ]);
567
568 expect(getVisibleChildren(container)).toEqual(
569 <div>
570 <span>Loading A</span>
571 <span>Loading B</span>
572 <span>Loading C</span>
573 </div>,
574 );
575
576 await serverAct(() => C.resolve());
577 assertLog(['C']);
578
579 expect(getVisibleChildren(container)).toEqual(
580 <div>
581 <span>A</span>
582 <span>B</span>
583 <span>C</span>
584 </div>,
585 );
586 });
587
588 // @gate enableSuspenseList
589 it('displays each items in "forwards" order', async () => {
590 const A = createAsyncText('A');
591 const B = createAsyncText('B');
592 const C = createAsyncText('C');
593
594 function Foo() {
595 return (
596 <div>
597 <SuspenseList revealOrder="forwards" tail="visible">
598 <Suspense fallback={<Text text="Loading A" />}>
599 <A />
600 </Suspense>
601 <Suspense fallback={<Text text="Loading B" />}>
602 <B />
603 </Suspense>
604 <Suspense fallback={<Text text="Loading C" />}>
605 <C />
606 </Suspense>
607 </SuspenseList>
608 </div>
609 );
610 }
611
612 await C.resolve();
613
614 await serverAct(async () => {
615 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
616 pipe(writable);
617 });
618
619 assertLog([
620 'Suspend! [A]',
621 'Suspend! [B]', // TODO: Defer rendering the content after fallback if previous suspended,
622 'C',
623 'Loading A',
624 'Loading B',
625 'Loading C',
626 ]);
627
628 expect(getVisibleChildren(container)).toEqual(
629 <div>
630 <span>Loading A</span>
631 <span>Loading B</span>
632 <span>Loading C</span>
633 </div>,
634 );
635
636 await serverAct(() => A.resolve());
637 assertLog(['A']);
638
639 expect(getVisibleChildren(container)).toEqual(
640 <div>
641 <span>A</span>
642 <span>Loading B</span>
643 <span>Loading C</span>
644 </div>,
645 );
646
647 await serverAct(() => B.resolve());
648 assertLog(['B']);
649
650 expect(getVisibleChildren(container)).toEqual(
651 <div>
652 <span>A</span>
653 <span>B</span>
654 <span>C</span>
655 </div>,
656 );
657 });
658
659 // @gate enableSuspenseList
660 it('displays each items in "backwards" mount order', async () => {
661 const A = createAsyncText('A');
662 const B = createAsyncText('B');
663 const C = createAsyncText('C');
664
665 function Foo() {
666 return (
667 <div>
668 <SuspenseList revealOrder="backwards" tail="visible">
669 <Suspense fallback={<Text text="Loading C" />}>
670 <C />
671 </Suspense>
672 <Suspense fallback={<Text text="Loading B" />}>
673 <B />
674 </Suspense>
675 <Suspense fallback={<Text text="Loading A" />}>
676 <A />
677 </Suspense>
678 </SuspenseList>
679 </div>
680 );
681 }
682
683 await A.resolve();
684
685 await serverAct(async () => {
686 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
687 pipe(writable);
688 });
689
690 assertLog([
691 'Suspend! [C]',
692 'Suspend! [B]', // TODO: Defer rendering the content after fallback if previous suspended,
693 'A',
694 'Loading C',
695 'Loading B',
696 'Loading A',
697 ]);
698
699 expect(getVisibleChildren(container)).toEqual(
700 <div>
701 <span>Loading A</span>
702 <span>Loading B</span>
703 <span>Loading C</span>
704 </div>,
705 );
706
707 await serverAct(() => C.resolve());
708 assertLog(['C']);
709
710 expect(getVisibleChildren(container)).toEqual(
711 <div>
712 <span>Loading A</span>
713 <span>Loading B</span>
714 <span>C</span>
715 </div>,
716 );
717
718 await serverAct(() => B.resolve());
719 assertLog(['B']);
720
721 expect(getVisibleChildren(container)).toEqual(
722 <div>
723 <span>A</span>
724 <span>B</span>
725 <span>C</span>
726 </div>,
727 );
728 });
729
730 // @gate enableSuspenseList
731 it('displays each items in "backwards" order in legacy mode', async () => {
732 const A = createAsyncText('A');
733 const B = createAsyncText('B');
734 const C = createAsyncText('C');
735
736 function Foo() {
737 return (
738 <div>
739 <SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
740 <Suspense fallback={<Text text="Loading A" />}>
741 <A />
742 </Suspense>
743 <Suspense fallback={<Text text="Loading B" />}>
744 <B />
745 </Suspense>
746 <Suspense fallback={<Text text="Loading C" />}>
747 <C />
748 </Suspense>
749 </SuspenseList>
750 </div>
751 );
752 }
753
754 await A.resolve();
755
756 await serverAct(async () => {
757 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
758 pipe(writable);
759 });
760
761 assertLog([
762 'Suspend! [C]',
763 'Suspend! [B]', // TODO: Defer rendering the content after fallback if previous suspended,
764 'A',
765 'Loading C',
766 'Loading B',
767 'Loading A',
768 ]);
769
770 expect(getVisibleChildren(container)).toEqual(
771 <div>
772 <span>Loading A</span>
773 <span>Loading B</span>
774 <span>Loading C</span>
775 </div>,
776 );
777
778 await serverAct(() => C.resolve());
779 assertLog(['C']);
780
781 expect(getVisibleChildren(container)).toEqual(
782 <div>
783 <span>Loading A</span>
784 <span>Loading B</span>
785 <span>C</span>
786 </div>,
787 );
788
789 await serverAct(() => B.resolve());
790 assertLog(['B']);
791
792 expect(getVisibleChildren(container)).toEqual(
793 <div>
794 <span>A</span>
795 <span>B</span>
796 <span>C</span>
797 </div>,
798 );
799 });
800
801 // @gate enableSuspenseList
802 it('waits for a nested SuspenseList to complete before resolving "forwards"', async () => {
803 const A = createAsyncText('A');
804 const B = createAsyncText('B');
805 const C = createAsyncText('C');
806
807 function Foo() {
808 return (
809 <div>
810 <SuspenseList revealOrder="forwards" tail="visible">
811 <SuspenseList revealOrder="backwards" tail="visible">
812 <Suspense fallback={<Text text="Loading B" />}>
813 <B />
814 </Suspense>
815 <Suspense fallback={<Text text="Loading A" />}>
816 <A />
817 </Suspense>
818 </SuspenseList>
819 <Suspense fallback={<Text text="Loading C" />}>
820 <C />
821 </Suspense>
822 </SuspenseList>
823 </div>
824 );
825 }
826
827 await C.resolve();
828
829 await serverAct(async () => {
830 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
831 pipe(writable);
832 });
833
834 assertLog([
835 'Suspend! [B]',
836 'Suspend! [A]',
837 'C',
838 'Loading B',
839 'Loading A',
840 'Loading C',
841 ]);
842
843 expect(getVisibleChildren(container)).toEqual(
844 <div>
845 <span>Loading A</span>
846 <span>Loading B</span>
847 <span>Loading C</span>
848 </div>,
849 );
850
851 await serverAct(() => A.resolve());
852 assertLog(['A']);
853
854 expect(getVisibleChildren(container)).toEqual(
855 <div>
856 <span>Loading A</span>
857 <span>Loading B</span>
858 <span>Loading C</span>
859 </div>,
860 );
861
862 await serverAct(() => B.resolve());
863 assertLog(['B']);
864
865 expect(getVisibleChildren(container)).toEqual(
866 <div>
867 <span>A</span>
868 <span>B</span>
869 <span>C</span>
870 </div>,
871 );
872 });
873
874 // @gate enableSuspenseList
875 it('can abort a pending SuspenseList', async () => {
876 const A = createAsyncText('A');
877
878 function Foo() {
879 return (
880 <div>
881 <SuspenseList revealOrder="forwards" tail="visible">
882 <Suspense fallback={<Text text="Loading A" />}>
883 <A />
884 </Suspense>
885 <Suspense fallback={<Text text="Loading B" />}>
886 <Text text="B" />
887 </Suspense>
888 </SuspenseList>
889 </div>
890 );
891 }
892
893 const errors = [];
894 let abortStream;
895 await serverAct(async () => {
896 const {pipe, abort} = ReactDOMFizzServer.renderToPipeableStream(<Foo />, {
897 onError(error) {
898 errors.push(error.message);
899 },
900 });
901 pipe(writable);
902 abortStream = abort;
903 });
904
905 assertLog([
906 'Suspend! [A]',
907 'B', // TODO: Defer rendering the content after fallback if previous suspended,
908 'Loading A',
909 'Loading B',
910 ]);
911
912 expect(getVisibleChildren(container)).toEqual(
913 <div>
914 <span>Loading A</span>
915 <span>Loading B</span>
916 </div>,
917 );
918
919 await serverAct(() => {
920 abortStream();
921 });
922
923 expect(hasCompleted).toBe(true);
924 expect(errors).toEqual([
925 'The render was aborted by the server without a reason.',
926 ]);
927 });
928
929 // @gate enableSuspenseList
930 it('can error a pending SuspenseList', async () => {
931 const A = createAsyncText('A');
932
933 function Foo() {
934 return (
935 <div>
936 <SuspenseList revealOrder="forwards" tail="visible">
937 <Suspense fallback={<Text text="Loading A" />}>
938 <A />
939 </Suspense>
940 <Suspense fallback={<Text text="Loading B" />}>
941 <Text text="B" />
942 </Suspense>
943 </SuspenseList>
944 </div>
945 );
946 }
947
948 const errors = [];
949 await serverAct(async () => {
950 const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />, {
951 onError(error) {
952 errors.push(error.message);
953 },
954 });
955 pipe(writable);
956 });
957
958 assertLog([
959 'Suspend! [A]',
960 'B', // TODO: Defer rendering the content after fallback if previous suspended,
961 'Loading A',
962 'Loading B',
963 ]);
964
965 expect(getVisibleChildren(container)).toEqual(
966 <div>
967 <span>Loading A</span>
968 <span>Loading B</span>
969 </div>,
970 );
971
972 await serverAct(async () => {
973 A.reject(new Error('hi'));
974 });
975
976 assertLog(['Error! [hi]']);
977
978 expect(getVisibleChildren(container)).toEqual(
979 <div>
980 <span>Loading A</span>
981 <span>B</span>
982 </div>,
983 );
984
985 expect(errors).toEqual(['hi']);
986 expect(hasErrored).toBe(false);
987 expect(hasCompleted).toBe(true);
988 });
989 });