main
js 3,739 lines 89.5 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 * @jest-environment node
9 */
10
11 let React;
12 let ReactNoop;
13 let Scheduler;
14 let act;
15 let Profiler;
16 let Suspense;
17 let SuspenseList;
18 let waitForAll;
19 let assertLog;
20 let waitFor;
21 let assertConsoleErrorDev;
22
23 describe('ReactSuspenseList', () => {
24 beforeEach(() => {
25 jest.resetModules();
26
27 React = require('react');
28 ReactNoop = require('react-noop-renderer');
29 Scheduler = require('scheduler');
30 Profiler = React.Profiler;
31 Suspense = React.Suspense;
32 if (gate(flags => flags.enableSuspenseList)) {
33 SuspenseList = React.unstable_SuspenseList;
34 }
35
36 const InternalTestUtils = require('internal-test-utils');
37 waitForAll = InternalTestUtils.waitForAll;
38 assertLog = InternalTestUtils.assertLog;
39 waitFor = InternalTestUtils.waitFor;
40 act = InternalTestUtils.act;
41 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
42 });
43
44 function Text(props) {
45 Scheduler.log(props.text);
46 return <span>{props.text}</span>;
47 }
48
49 function createAsyncText(text) {
50 let resolved = false;
51 const Component = function () {
52 if (!resolved) {
53 Scheduler.log('Suspend! [' + text + ']');
54 throw promise;
55 }
56 return <Text text={text} />;
57 };
58 const promise = new Promise(resolve => {
59 Component.resolve = function () {
60 resolved = true;
61 return resolve();
62 };
63 });
64 return Component;
65 }
66
67 // @gate enableSuspenseList
68 it('warns if an unsupported revealOrder option is used', async () => {
69 function Foo() {
70 return (
71 <SuspenseList revealOrder="something">
72 <Suspense fallback="Loading">Content</Suspense>
73 </SuspenseList>
74 );
75 }
76
77 await act(() => {
78 ReactNoop.render(<Foo />);
79 });
80 assertConsoleErrorDev([
81 '"something" is not a supported revealOrder on ' +
82 '<SuspenseList />. Did you mean "independent", "together", "forwards" or "backwards"?' +
83 '\n in SuspenseList (at **)' +
84 '\n in Foo (at **)',
85 ]);
86 });
87
88 // @gate enableSuspenseList
89 it('warns if a upper case revealOrder option is used', async () => {
90 function Foo() {
91 return (
92 <SuspenseList revealOrder="TOGETHER">
93 <Suspense fallback="Loading">Content</Suspense>
94 </SuspenseList>
95 );
96 }
97
98 await act(() => {
99 ReactNoop.render(<Foo />);
100 });
101 assertConsoleErrorDev([
102 '"TOGETHER" is not a valid value for revealOrder on ' +
103 '<SuspenseList />. Use lowercase "together" instead.' +
104 '\n in SuspenseList (at **)' +
105 '\n in Foo (at **)',
106 ]);
107 });
108
109 // @gate enableSuspenseList
110 it('warns if a misspelled revealOrder option is used', async () => {
111 function Foo() {
112 return (
113 <SuspenseList revealOrder="forward">
114 <Suspense fallback="Loading">Content</Suspense>
115 </SuspenseList>
116 );
117 }
118
119 await act(() => {
120 ReactNoop.render(<Foo />);
121 });
122 assertConsoleErrorDev([
123 '"forward" is not a valid value for revealOrder on ' +
124 '<SuspenseList />. React uses the -s suffix in the spelling. ' +
125 'Use "forwards" instead.' +
126 '\n in SuspenseList (at **)' +
127 '\n in Foo (at **)',
128 ]);
129 });
130
131 // @gate enableSuspenseList
132 it('warns if a single element is passed to a "forwards" list', async () => {
133 function Foo({children}) {
134 return (
135 <SuspenseList revealOrder="forwards" tail="visible">
136 {children}
137 </SuspenseList>
138 );
139 }
140
141 ReactNoop.render(<Foo />);
142 // No warning
143 await waitForAll([]);
144
145 ReactNoop.render(<Foo>{null}</Foo>);
146 // No warning
147 await waitForAll([]);
148
149 ReactNoop.render(<Foo>{false}</Foo>);
150 // No warning
151 await waitForAll([]);
152
153 await act(() => {
154 ReactNoop.render(
155 <Foo>
156 <Suspense fallback="Loading">Child</Suspense>
157 </Foo>,
158 );
159 });
160 assertConsoleErrorDev([
161 'A single row was passed to a <SuspenseList revealOrder="forwards" />. ' +
162 'This is not useful since it needs multiple rows. ' +
163 'Did you mean to pass multiple children or an array?' +
164 '\n in SuspenseList (at **)' +
165 '\n in Foo (at **)',
166 ]);
167 });
168
169 // @gate enableSuspenseList
170 it('warns if a single fragment is passed to a "backwards" list', async () => {
171 function Foo() {
172 return (
173 <SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
174 <>{[]}</>
175 </SuspenseList>
176 );
177 }
178
179 await act(() => {
180 ReactNoop.render(<Foo />);
181 });
182 assertConsoleErrorDev([
183 'A single row was passed to a <SuspenseList revealOrder="unstable_legacy-backwards" />. ' +
184 'This is not useful since it needs multiple rows. ' +
185 'Did you mean to pass multiple children or an array?' +
186 '\n in SuspenseList (at **)' +
187 '\n in Foo (at **)',
188 ]);
189 });
190
191 // @gate enableSuspenseList
192 it('warns if a nested array is passed to a "forwards" list', async () => {
193 function Foo({items}) {
194 return (
195 <SuspenseList revealOrder="forwards" tail="visible">
196 {items.map(name => (
197 <Suspense key={name} fallback="Loading">
198 {name}
199 </Suspense>
200 ))}
201 <div>Tail</div>
202 </SuspenseList>
203 );
204 }
205
206 await act(() => {
207 ReactNoop.render(<Foo items={['A', 'B']} />);
208 });
209 assertConsoleErrorDev([
210 'A nested array was passed to row #0 in <SuspenseList />. ' +
211 'Wrap it in an additional SuspenseList to configure its revealOrder: ' +
212 '<SuspenseList revealOrder=...> ... ' +
213 '<SuspenseList revealOrder=...>{array}</SuspenseList> ... ' +
214 '</SuspenseList>' +
215 '\n in SuspenseList (at **)' +
216 '\n in Foo (at **)',
217 ]);
218 });
219
220 // @gate enableSuspenseList
221 it('behaves as revealOrder=forwards by default', async () => {
222 const A = createAsyncText('A');
223 const B = createAsyncText('B');
224 const C = createAsyncText('C');
225
226 function Foo() {
227 return (
228 <SuspenseList>
229 <Suspense fallback={<Text text="Loading A" />}>
230 <A />
231 </Suspense>
232 <Suspense fallback={<Text text="Loading B" />}>
233 <B />
234 </Suspense>
235 <Suspense fallback={<Text text="Loading C" />}>
236 <C />
237 </Suspense>
238 </SuspenseList>
239 );
240 }
241
242 ReactNoop.render(<Foo />);
243
244 await waitForAll(['Suspend! [A]', 'Loading A']);
245
246 expect(ReactNoop).toMatchRenderedOutput(null);
247
248 await A.resolve();
249
250 await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
251
252 // Incremental loading is suspended.
253 jest.advanceTimersByTime(500);
254
255 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
256
257 await act(() => B.resolve());
258 assertLog(['B', 'Suspend! [C]', 'Loading C']);
259
260 // Incremental loading is suspended.
261 jest.advanceTimersByTime(500);
262
263 expect(ReactNoop).toMatchRenderedOutput(
264 <>
265 <span>A</span>
266 <span>B</span>
267 </>,
268 );
269
270 await act(() => C.resolve());
271 assertLog(['C']);
272
273 expect(ReactNoop).toMatchRenderedOutput(
274 <>
275 <span>A</span>
276 <span>B</span>
277 <span>C</span>
278 </>,
279 );
280 });
281
282 // @gate enableSuspenseList
283 it('shows content independently with revealOrder="independent"', async () => {
284 const A = createAsyncText('A');
285 const B = createAsyncText('B');
286 const C = createAsyncText('C');
287
288 function Foo() {
289 return (
290 <SuspenseList revealOrder="independent">
291 <Suspense fallback={<Text text="Loading A" />}>
292 <A />
293 </Suspense>
294 <Suspense fallback={<Text text="Loading B" />}>
295 <B />
296 </Suspense>
297 <Suspense fallback={<Text text="Loading C" />}>
298 <C />
299 </Suspense>
300 </SuspenseList>
301 );
302 }
303
304 await A.resolve();
305
306 ReactNoop.render(<Foo />);
307
308 await waitForAll([
309 'A',
310 'Suspend! [B]',
311 'Loading B',
312 'Suspend! [C]',
313 'Loading C',
314 // pre-warming
315 'Suspend! [B]',
316 'Suspend! [C]',
317 ]);
318
319 expect(ReactNoop).toMatchRenderedOutput(
320 <>
321 <span>A</span>
322 <span>Loading B</span>
323 <span>Loading C</span>
324 </>,
325 );
326
327 await act(() => C.resolve());
328 assertLog(
329 gate('alwaysThrottleRetries')
330 ? ['Suspend! [B]', 'C', 'Suspend! [B]']
331 : ['C'],
332 );
333
334 expect(ReactNoop).toMatchRenderedOutput(
335 <>
336 <span>A</span>
337 <span>Loading B</span>
338 <span>C</span>
339 </>,
340 );
341
342 await act(() => B.resolve());
343 assertLog(['B']);
344
345 expect(ReactNoop).toMatchRenderedOutput(
346 <>
347 <span>A</span>
348 <span>B</span>
349 <span>C</span>
350 </>,
351 );
352 });
353
354 // @gate enableSuspenseList && !disableLegacyMode
355 it('shows content independently in legacy mode regardless of option', async () => {
356 const A = createAsyncText('A');
357 const B = createAsyncText('B');
358 const C = createAsyncText('C');
359
360 function Foo() {
361 return (
362 <SuspenseList revealOrder="together">
363 <Suspense fallback={<Text text="Loading A" />}>
364 <A />
365 </Suspense>
366 <Suspense fallback={<Text text="Loading B" />}>
367 <B />
368 </Suspense>
369 <Suspense fallback={<Text text="Loading C" />}>
370 <C />
371 </Suspense>
372 </SuspenseList>
373 );
374 }
375
376 await A.resolve();
377
378 ReactNoop.renderLegacySyncRoot(<Foo />);
379
380 assertLog(['A', 'Suspend! [B]', 'Loading B', 'Suspend! [C]', 'Loading C']);
381
382 expect(ReactNoop).toMatchRenderedOutput(
383 <>
384 <span>A</span>
385 <span>Loading B</span>
386 <span>Loading C</span>
387 </>,
388 );
389
390 await act(() => {
391 C.resolve();
392 });
393
394 assertLog(['C']);
395
396 expect(ReactNoop).toMatchRenderedOutput(
397 <>
398 <span>A</span>
399 <span>Loading B</span>
400 <span>C</span>
401 </>,
402 );
403
404 await act(() => {
405 B.resolve();
406 });
407
408 assertLog(['B']);
409
410 expect(ReactNoop).toMatchRenderedOutput(
411 <>
412 <span>A</span>
413 <span>B</span>
414 <span>C</span>
415 </>,
416 );
417 });
418
419 // @gate enableSuspenseList
420 it('displays all "together"', async () => {
421 const A = createAsyncText('A');
422 const B = createAsyncText('B');
423 const C = createAsyncText('C');
424
425 function Foo() {
426 return (
427 <SuspenseList revealOrder="together">
428 <Suspense fallback={<Text text="Loading A" />}>
429 <A />
430 </Suspense>
431 <Suspense fallback={<Text text="Loading B" />}>
432 <B />
433 </Suspense>
434 <Suspense fallback={<Text text="Loading C" />}>
435 <C />
436 </Suspense>
437 </SuspenseList>
438 );
439 }
440
441 await A.resolve();
442
443 ReactNoop.render(<Foo />);
444
445 await waitForAll([
446 'A',
447 'Suspend! [B]',
448 'Loading B',
449 'Suspend! [C]',
450 'Loading C',
451 'Loading A',
452 'Loading B',
453 'Loading C',
454 ]);
455
456 expect(ReactNoop).toMatchRenderedOutput(
457 <>
458 <span>Loading A</span>
459 <span>Loading B</span>
460 <span>Loading C</span>
461 </>,
462 );
463
464 await act(() => B.resolve());
465 assertLog(['A', 'B', 'Suspend! [C]']);
466
467 expect(ReactNoop).toMatchRenderedOutput(
468 <>
469 <span>Loading A</span>
470 <span>Loading B</span>
471 <span>Loading C</span>
472 </>,
473 );
474
475 await act(() => C.resolve());
476 assertLog(['A', 'B', 'C']);
477
478 expect(ReactNoop).toMatchRenderedOutput(
479 <>
480 <span>A</span>
481 <span>B</span>
482 <span>C</span>
483 </>,
484 );
485 });
486
487 // @gate enableSuspenseList
488 it('displays all "together" even when nested as siblings', async () => {
489 const A = createAsyncText('A');
490 const B = createAsyncText('B');
491 const C = createAsyncText('C');
492
493 function Foo() {
494 return (
495 <SuspenseList revealOrder="together">
496 <div>
497 <Suspense fallback={<Text text="Loading A" />}>
498 <A />
499 </Suspense>
500 <Suspense fallback={<Text text="Loading B" />}>
501 <B />
502 </Suspense>
503 </div>
504 <div>
505 <Suspense fallback={<Text text="Loading C" />}>
506 <C />
507 </Suspense>
508 </div>
509 </SuspenseList>
510 );
511 }
512
513 await A.resolve();
514
515 ReactNoop.render(<Foo />);
516
517 await waitForAll([
518 'A',
519 'Suspend! [B]',
520 'Loading B',
521 'Suspend! [C]',
522 'Loading C',
523 'Loading A',
524 'Loading B',
525 'Loading C',
526 ]);
527
528 expect(ReactNoop).toMatchRenderedOutput(
529 <>
530 <div>
531 <span>Loading A</span>
532 <span>Loading B</span>
533 </div>
534 <div>
535 <span>Loading C</span>
536 </div>
537 </>,
538 );
539
540 await act(() => B.resolve());
541 assertLog(['A', 'B', 'Suspend! [C]']);
542
543 expect(ReactNoop).toMatchRenderedOutput(
544 <>
545 <div>
546 <span>Loading A</span>
547 <span>Loading B</span>
548 </div>
549 <div>
550 <span>Loading C</span>
551 </div>
552 </>,
553 );
554
555 await act(() => C.resolve());
556 assertLog(['A', 'B', 'C']);
557
558 expect(ReactNoop).toMatchRenderedOutput(
559 <>
560 <div>
561 <span>A</span>
562 <span>B</span>
563 </div>
564 <div>
565 <span>C</span>
566 </div>
567 </>,
568 );
569 });
570
571 // @gate enableSuspenseList
572 it('displays all "together" in nested SuspenseLists', async () => {
573 const A = createAsyncText('A');
574 const B = createAsyncText('B');
575 const C = createAsyncText('C');
576
577 function Foo() {
578 return (
579 <SuspenseList revealOrder="together">
580 <Suspense fallback={<Text text="Loading A" />}>
581 <A />
582 </Suspense>
583 <SuspenseList revealOrder="together">
584 <Suspense fallback={<Text text="Loading B" />}>
585 <B />
586 </Suspense>
587 <Suspense fallback={<Text text="Loading C" />}>
588 <C />
589 </Suspense>
590 </SuspenseList>
591 </SuspenseList>
592 );
593 }
594
595 await A.resolve();
596 await B.resolve();
597
598 ReactNoop.render(<Foo />);
599
600 await waitForAll([
601 'A',
602 'B',
603 'Suspend! [C]',
604 'Loading C',
605 'Loading B',
606 'Loading C',
607 'Loading A',
608 'Loading B',
609 'Loading C',
610 ]);
611
612 expect(ReactNoop).toMatchRenderedOutput(
613 <>
614 <span>Loading A</span>
615 <span>Loading B</span>
616 <span>Loading C</span>
617 </>,
618 );
619
620 await act(() => C.resolve());
621 assertLog(['A', 'B', 'C']);
622
623 expect(ReactNoop).toMatchRenderedOutput(
624 <>
625 <span>A</span>
626 <span>B</span>
627 <span>C</span>
628 </>,
629 );
630 });
631
632 // @gate enableSuspenseList
633 it('displays all "together" in nested SuspenseLists where the inner is "independent"', async () => {
634 const A = createAsyncText('A');
635 const B = createAsyncText('B');
636 const C = createAsyncText('C');
637
638 function Foo() {
639 return (
640 <SuspenseList revealOrder="together">
641 <Suspense fallback={<Text text="Loading A" />}>
642 <A />
643 </Suspense>
644 <SuspenseList revealOrder="independent">
645 <Suspense fallback={<Text text="Loading B" />}>
646 <B />
647 </Suspense>
648 <Suspense fallback={<Text text="Loading C" />}>
649 <C />
650 </Suspense>
651 </SuspenseList>
652 </SuspenseList>
653 );
654 }
655
656 await A.resolve();
657 await B.resolve();
658
659 ReactNoop.render(<Foo />);
660
661 await waitForAll([
662 'A',
663 'B',
664 'Suspend! [C]',
665 'Loading C',
666 'Loading A',
667 'Loading B',
668 'Loading C',
669 ]);
670
671 expect(ReactNoop).toMatchRenderedOutput(
672 <>
673 <span>Loading A</span>
674 <span>Loading B</span>
675 <span>Loading C</span>
676 </>,
677 );
678
679 await act(() => C.resolve());
680 assertLog(['A', 'B', 'C']);
681
682 expect(ReactNoop).toMatchRenderedOutput(
683 <>
684 <span>A</span>
685 <span>B</span>
686 <span>C</span>
687 </>,
688 );
689 });
690
691 // @gate enableSuspenseList
692 it('displays all "together" during an update', async () => {
693 const A = createAsyncText('A');
694 const B = createAsyncText('B');
695 const C = createAsyncText('C');
696 const D = createAsyncText('D');
697
698 function Foo({step}) {
699 return (
700 <SuspenseList revealOrder="together">
701 {step === 0 && (
702 <Suspense fallback={<Text text="Loading A" />}>
703 <A />
704 </Suspense>
705 )}
706 {step === 0 && (
707 <Suspense fallback={<Text text="Loading B" />}>
708 <B />
709 </Suspense>
710 )}
711 {step === 1 && (
712 <Suspense fallback={<Text text="Loading C" />}>
713 <C />
714 </Suspense>
715 )}
716 {step === 1 && (
717 <Suspense fallback={<Text text="Loading D" />}>
718 <D />
719 </Suspense>
720 )}
721 </SuspenseList>
722 );
723 }
724
725 // Mount
726 await A.resolve();
727 ReactNoop.render(<Foo step={0} />);
728 await waitForAll([
729 'A',
730 'Suspend! [B]',
731 'Loading B',
732 'Loading A',
733 'Loading B',
734 ]);
735 expect(ReactNoop).toMatchRenderedOutput(
736 <>
737 <span>Loading A</span>
738 <span>Loading B</span>
739 </>,
740 );
741 await act(() => B.resolve());
742 assertLog(['A', 'B']);
743 expect(ReactNoop).toMatchRenderedOutput(
744 <>
745 <span>A</span>
746 <span>B</span>
747 </>,
748 );
749
750 // Update
751 await C.resolve();
752 ReactNoop.render(<Foo step={1} />);
753 await waitForAll([
754 'C',
755 'Suspend! [D]',
756 'Loading D',
757 'Loading C',
758 'Loading D',
759 ]);
760 expect(ReactNoop).toMatchRenderedOutput(
761 <>
762 <span>Loading C</span>
763 <span>Loading D</span>
764 </>,
765 );
766 await act(() => D.resolve());
767 assertLog(['C', 'D']);
768 expect(ReactNoop).toMatchRenderedOutput(
769 <>
770 <span>C</span>
771 <span>D</span>
772 </>,
773 );
774 });
775
776 // @gate enableSuspenseList && enableSuspenseAvoidThisFallback
777 it('avoided boundaries can be coordinate with SuspenseList', async () => {
778 const A = createAsyncText('A');
779 const B = createAsyncText('B');
780 const C = createAsyncText('C');
781
782 function Foo({showMore}) {
783 return (
784 <Suspense fallback={<Text text="Loading" />}>
785 <SuspenseList revealOrder="together">
786 <Suspense
787 unstable_avoidThisFallback={true}
788 fallback={<Text text="Loading A" />}>
789 <A />
790 </Suspense>
791 {showMore ? (
792 <>
793 <Suspense
794 unstable_avoidThisFallback={true}
795 fallback={<Text text="Loading B" />}>
796 <B />
797 </Suspense>
798 <Suspense
799 unstable_avoidThisFallback={true}
800 fallback={<Text text="Loading C" />}>
801 <C />
802 </Suspense>
803 </>
804 ) : null}
805 </SuspenseList>
806 </Suspense>
807 );
808 }
809
810 ReactNoop.render(<Foo />);
811
812 await waitForAll([
813 'Suspend! [A]',
814 'Loading',
815 // pre-warming
816 'Suspend! [A]',
817 ]);
818
819 expect(ReactNoop).toMatchRenderedOutput(<span>Loading</span>);
820
821 await act(() => A.resolve());
822 assertLog(['A']);
823
824 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
825
826 // Let's do an update that should consult the avoided boundaries.
827 ReactNoop.render(<Foo showMore={true} />);
828
829 await waitForAll([
830 'A',
831 'Suspend! [B]',
832 'Loading B',
833 'Suspend! [C]',
834 'Loading C',
835 'A',
836 'Loading B',
837 'Loading C',
838 ]);
839
840 // This will suspend, since the boundaries are avoided. Give them
841 // time to display their loading states.
842 jest.advanceTimersByTime(500);
843
844 // A is already showing content so it doesn't turn into a fallback.
845 expect(ReactNoop).toMatchRenderedOutput(
846 <>
847 <span>A</span>
848 <span>Loading B</span>
849 <span>Loading C</span>
850 </>,
851 );
852
853 await act(() => B.resolve());
854 assertLog(['B', 'Suspend! [C]']);
855
856 // Even though we could now show B, we're still waiting on C.
857 expect(ReactNoop).toMatchRenderedOutput(
858 <>
859 <span>A</span>
860 <span>Loading B</span>
861 <span>Loading C</span>
862 </>,
863 );
864
865 await act(() => C.resolve());
866 assertLog(['B', 'C']);
867
868 expect(ReactNoop).toMatchRenderedOutput(
869 <>
870 <span>A</span>
871 <span>B</span>
872 <span>C</span>
873 </>,
874 );
875 });
876
877 // @gate enableSuspenseList
878 it('boundaries without fallbacks can be coordinate with SuspenseList', async () => {
879 const A = createAsyncText('A');
880 const B = createAsyncText('B');
881 const C = createAsyncText('C');
882
883 function Foo({showMore}) {
884 return (
885 <Suspense fallback={<Text text="Loading" />}>
886 <SuspenseList revealOrder="together">
887 <Suspense>
888 <A />
889 </Suspense>
890 {showMore ? (
891 <>
892 <Suspense>
893 <B />
894 </Suspense>
895 <Suspense>
896 <C />
897 </Suspense>
898 </>
899 ) : null}
900 </SuspenseList>
901 </Suspense>
902 );
903 }
904
905 ReactNoop.render(<Foo />);
906
907 await waitForAll([
908 'Suspend! [A]',
909 // null
910 ]);
911
912 expect(ReactNoop).toMatchRenderedOutput(null);
913
914 await act(() => A.resolve());
915 assertLog(['A']);
916
917 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
918
919 // Let's do an update that should consult the avoided boundaries.
920 ReactNoop.render(<Foo showMore={true} />);
921
922 await waitForAll([
923 'A',
924 'Suspend! [B]',
925 // null
926 'Suspend! [C]',
927 // null
928 'A',
929 // null
930 // null
931 ]);
932
933 // This will suspend, since the boundaries are avoided. Give them
934 // time to display their loading states.
935 jest.advanceTimersByTime(500);
936
937 // A is already showing content so it doesn't turn into a fallback.
938 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
939
940 await act(() => B.resolve());
941 assertLog(['B', 'Suspend! [C]']);
942
943 // Even though we could now show B, we're still waiting on C.
944 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
945
946 await act(() => C.resolve());
947 assertLog(['B', 'C']);
948
949 expect(ReactNoop).toMatchRenderedOutput(
950 <>
951 <span>A</span>
952 <span>B</span>
953 <span>C</span>
954 </>,
955 );
956 });
957
958 // @gate enableSuspenseList
959 it('displays each items in "forwards" order', async () => {
960 const A = createAsyncText('A');
961 const B = createAsyncText('B');
962 const C = createAsyncText('C');
963
964 function Foo() {
965 return (
966 <SuspenseList revealOrder="forwards" tail="visible">
967 <Suspense fallback={<Text text="Loading A" />}>
968 <A />
969 </Suspense>
970 <Suspense fallback={<Text text="Loading B" />}>
971 <B />
972 </Suspense>
973 <Suspense fallback={<Text text="Loading C" />}>
974 <C />
975 </Suspense>
976 </SuspenseList>
977 );
978 }
979
980 await C.resolve();
981
982 ReactNoop.render(<Foo />);
983
984 await waitForAll([
985 'Suspend! [A]',
986 'Loading A',
987 'Loading B',
988 'Loading C',
989 // pre-warming
990 'Suspend! [A]',
991 ]);
992
993 expect(ReactNoop).toMatchRenderedOutput(
994 <>
995 <span>Loading A</span>
996 <span>Loading B</span>
997 <span>Loading C</span>
998 </>,
999 );
1000
1001 await act(() => A.resolve());
1002 assertLog(['A', 'Suspend! [B]', 'Suspend! [B]']);
1003
1004 expect(ReactNoop).toMatchRenderedOutput(
1005 <>
1006 <span>A</span>
1007 <span>Loading B</span>
1008 <span>Loading C</span>
1009 </>,
1010 );
1011
1012 await act(() => B.resolve());
1013 assertLog(['B', 'C']);
1014
1015 expect(ReactNoop).toMatchRenderedOutput(
1016 <>
1017 <span>A</span>
1018 <span>B</span>
1019 <span>C</span>
1020 </>,
1021 );
1022 });
1023
1024 // @gate enableSuspenseList
1025 it('displays each items in "backwards" order', async () => {
1026 const A = createAsyncText('A');
1027 const B = createAsyncText('B');
1028 const C = createAsyncText('C');
1029
1030 function Foo() {
1031 return (
1032 <SuspenseList revealOrder="backwards" tail="visible">
1033 <Suspense fallback={<Text text="Loading C" />}>
1034 <C />
1035 </Suspense>
1036 <Suspense fallback={<Text text="Loading B" />}>
1037 <B />
1038 </Suspense>
1039 <Suspense fallback={<Text text="Loading A" />}>
1040 <A />
1041 </Suspense>
1042 </SuspenseList>
1043 );
1044 }
1045
1046 await A.resolve();
1047
1048 ReactNoop.render(<Foo />);
1049
1050 await waitForAll([
1051 'Suspend! [C]',
1052 'Loading C',
1053 'Loading B',
1054 'Loading A',
1055 // pre-warming
1056 'Suspend! [C]',
1057 ]);
1058
1059 expect(ReactNoop).toMatchRenderedOutput(
1060 <>
1061 <span>Loading A</span>
1062 <span>Loading B</span>
1063 <span>Loading C</span>
1064 </>,
1065 );
1066
1067 await act(() => C.resolve());
1068 assertLog([
1069 'C',
1070 'Suspend! [B]',
1071 // pre-warming
1072 'Suspend! [B]',
1073 ]);
1074
1075 expect(ReactNoop).toMatchRenderedOutput(
1076 <>
1077 <span>Loading A</span>
1078 <span>Loading B</span>
1079 <span>C</span>
1080 </>,
1081 );
1082
1083 await act(() => B.resolve());
1084 assertLog(['B', 'A']);
1085
1086 expect(ReactNoop).toMatchRenderedOutput(
1087 <>
1088 <span>A</span>
1089 <span>B</span>
1090 <span>C</span>
1091 </>,
1092 );
1093 });
1094
1095 // @gate enableSuspenseList
1096 it('displays each items in "backwards" order (legacy)', async () => {
1097 const A = createAsyncText('A');
1098 const B = createAsyncText('B');
1099 const C = createAsyncText('C');
1100
1101 function Foo() {
1102 return (
1103 <SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
1104 <Suspense fallback={<Text text="Loading A" />}>
1105 <A />
1106 </Suspense>
1107 <Suspense fallback={<Text text="Loading B" />}>
1108 <B />
1109 </Suspense>
1110 <Suspense fallback={<Text text="Loading C" />}>
1111 <C />
1112 </Suspense>
1113 </SuspenseList>
1114 );
1115 }
1116
1117 await A.resolve();
1118
1119 ReactNoop.render(<Foo />);
1120
1121 await waitForAll([
1122 'Suspend! [C]',
1123 'Loading C',
1124 'Loading B',
1125 'Loading A',
1126 // pre-warming
1127 'Suspend! [C]',
1128 ]);
1129
1130 expect(ReactNoop).toMatchRenderedOutput(
1131 <>
1132 <span>Loading A</span>
1133 <span>Loading B</span>
1134 <span>Loading C</span>
1135 </>,
1136 );
1137
1138 await act(() => C.resolve());
1139 assertLog([
1140 'C',
1141 'Suspend! [B]',
1142 // pre-warming
1143 'Suspend! [B]',
1144 ]);
1145
1146 expect(ReactNoop).toMatchRenderedOutput(
1147 <>
1148 <span>Loading A</span>
1149 <span>Loading B</span>
1150 <span>C</span>
1151 </>,
1152 );
1153
1154 await act(() => B.resolve());
1155 assertLog(['B', 'A']);
1156
1157 expect(ReactNoop).toMatchRenderedOutput(
1158 <>
1159 <span>A</span>
1160 <span>B</span>
1161 <span>C</span>
1162 </>,
1163 );
1164 });
1165
1166 // @gate enableSuspenseList
1167 it('displays added row at the top "together" and the bottom in "forwards" order', async () => {
1168 const A = createAsyncText('A');
1169 const B = createAsyncText('B');
1170 const C = createAsyncText('C');
1171 const D = createAsyncText('D');
1172 const E = createAsyncText('E');
1173 const F = createAsyncText('F');
1174
1175 function Foo({items}) {
1176 return (
1177 <SuspenseList revealOrder="forwards" tail="visible">
1178 {items.map(([key, Component]) => (
1179 <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
1180 <Component />
1181 </Suspense>
1182 ))}
1183 </SuspenseList>
1184 );
1185 }
1186
1187 await B.resolve();
1188 await D.resolve();
1189
1190 ReactNoop.render(
1191 <Foo
1192 items={[
1193 ['B', B],
1194 ['D', D],
1195 ]}
1196 />,
1197 );
1198
1199 await waitForAll(['B', 'D']);
1200
1201 expect(ReactNoop).toMatchRenderedOutput(
1202 <>
1203 <span>B</span>
1204 <span>D</span>
1205 </>,
1206 );
1207
1208 // Insert items in the beginning, middle and end.
1209 ReactNoop.render(
1210 <Foo
1211 items={[
1212 ['A', A],
1213 ['B', B],
1214 ['C', C],
1215 ['D', D],
1216 ['E', E],
1217 ['F', F],
1218 ]}
1219 />,
1220 );
1221
1222 await waitForAll([
1223 'Suspend! [A]',
1224 'Loading A',
1225 'B',
1226 'Suspend! [C]',
1227 'Loading C',
1228 'D',
1229 'Loading A',
1230 'B',
1231 'Loading C',
1232 'D',
1233 'Loading E',
1234 'Loading F',
1235 ]);
1236
1237 expect(ReactNoop).toMatchRenderedOutput(
1238 <>
1239 <span>Loading A</span>
1240 <span>B</span>
1241 <span>Loading C</span>
1242 <span>D</span>
1243 <span>Loading E</span>
1244 <span>Loading F</span>
1245 </>,
1246 );
1247
1248 await act(() => A.resolve());
1249 assertLog(['A', 'Suspend! [C]']);
1250
1251 // Even though we could show A, it is still in a fallback state because
1252 // C is not yet resolved. We need to resolve everything in the head first.
1253 expect(ReactNoop).toMatchRenderedOutput(
1254 <>
1255 <span>Loading A</span>
1256 <span>B</span>
1257 <span>Loading C</span>
1258 <span>D</span>
1259 <span>Loading E</span>
1260 <span>Loading F</span>
1261 </>,
1262 );
1263
1264 await act(() => C.resolve());
1265 assertLog([
1266 'A',
1267 'C',
1268 'Suspend! [E]',
1269 // pre-warming
1270 'Suspend! [E]',
1271 ]);
1272
1273 // We can now resolve the full head.
1274 expect(ReactNoop).toMatchRenderedOutput(
1275 <>
1276 <span>A</span>
1277 <span>B</span>
1278 <span>C</span>
1279 <span>D</span>
1280 <span>Loading E</span>
1281 <span>Loading F</span>
1282 </>,
1283 );
1284
1285 await act(() => E.resolve());
1286 assertLog([
1287 'E',
1288 'Suspend! [F]',
1289 // pre-warming
1290 'Suspend! [F]',
1291 ]);
1292
1293 // In the tail we can resolve one-by-one.
1294 expect(ReactNoop).toMatchRenderedOutput(
1295 <>
1296 <span>A</span>
1297 <span>B</span>
1298 <span>C</span>
1299 <span>D</span>
1300 <span>E</span>
1301 <span>Loading F</span>
1302 </>,
1303 );
1304
1305 await act(() => F.resolve());
1306 assertLog(['F']);
1307 expect(ReactNoop).toMatchRenderedOutput(
1308 <>
1309 <span>A</span>
1310 <span>B</span>
1311 <span>C</span>
1312 <span>D</span>
1313 <span>E</span>
1314 <span>F</span>
1315 </>,
1316 );
1317
1318 // We can also delete some items.
1319 ReactNoop.render(
1320 <Foo
1321 items={[
1322 ['D', D],
1323 ['E', E],
1324 ['F', F],
1325 ]}
1326 />,
1327 );
1328
1329 await waitForAll(['D', 'E', 'F']);
1330
1331 expect(ReactNoop).toMatchRenderedOutput(
1332 <>
1333 <span>D</span>
1334 <span>E</span>
1335 <span>F</span>
1336 </>,
1337 );
1338 });
1339
1340 // @gate enableSuspenseList
1341 it('displays added row at the top "together" and the bottom in "backwards" order', async () => {
1342 const A = createAsyncText('A');
1343 const B = createAsyncText('B');
1344 const D = createAsyncText('D');
1345 const F = createAsyncText('F');
1346
1347 function createSyncText(text) {
1348 return function () {
1349 return <Text text={text} />;
1350 };
1351 }
1352
1353 const As = createSyncText('A');
1354 const Bs = createSyncText('B');
1355 const Cs = createSyncText('C');
1356 const Ds = createSyncText('D');
1357 const Es = createSyncText('E');
1358 const Fs = createSyncText('F');
1359
1360 function Foo({items}) {
1361 return (
1362 <SuspenseList revealOrder="unstable_legacy-backwards" tail="visible">
1363 {items.map(([key, Component]) => (
1364 <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
1365 <Component />
1366 </Suspense>
1367 ))}
1368 </SuspenseList>
1369 );
1370 }
1371
1372 // The first pass doesn't suspend.
1373 ReactNoop.render(
1374 <Foo
1375 items={[
1376 ['A', As],
1377 ['B', Bs],
1378 ['C', Cs],
1379 ['D', Ds],
1380 ['E', Es],
1381 ['F', Fs],
1382 ]}
1383 />,
1384 );
1385 await waitForAll(['F', 'E', 'D', 'C', 'B', 'A']);
1386 expect(ReactNoop).toMatchRenderedOutput(
1387 <>
1388 <span>A</span>
1389 <span>B</span>
1390 <span>C</span>
1391 <span>D</span>
1392 <span>E</span>
1393 <span>F</span>
1394 </>,
1395 );
1396
1397 // Update items in the beginning, middle and end to start suspending.
1398 ReactNoop.render(
1399 <Foo
1400 items={[
1401 ['A', A],
1402 ['B', B],
1403 ['C', Cs],
1404 ['D', D],
1405 ['E', Es],
1406 ['F', F],
1407 ]}
1408 />,
1409 );
1410
1411 await waitForAll([
1412 'Suspend! [A]',
1413 'Loading A',
1414 'Suspend! [B]',
1415 'Loading B',
1416 'C',
1417 'Suspend! [D]',
1418 'Loading D',
1419 'E',
1420 'Suspend! [F]',
1421 'Loading F',
1422 'Suspend! [A]',
1423 'Loading A',
1424 'Suspend! [B]',
1425 'Loading B',
1426 'C',
1427 'Suspend! [D]',
1428 'Loading D',
1429 'E',
1430 'Suspend! [F]',
1431 'Loading F',
1432 // pre-warming
1433 'Suspend! [D]',
1434 'Suspend! [F]',
1435 ]);
1436
1437 // This will suspend, since the boundaries are avoided. Give them
1438 // time to display their loading states.
1439 jest.advanceTimersByTime(500);
1440
1441 expect(ReactNoop).toMatchRenderedOutput(
1442 <>
1443 <span hidden={true}>A</span>
1444 <span>Loading A</span>
1445 <span hidden={true}>B</span>
1446 <span>Loading B</span>
1447 <span>C</span>
1448 <span hidden={true}>D</span>
1449 <span>Loading D</span>
1450 <span>E</span>
1451 <span hidden={true}>F</span>
1452 <span>Loading F</span>
1453 </>,
1454 );
1455
1456 await F.resolve();
1457
1458 await waitForAll(['Suspend! [D]', 'F']);
1459
1460 // Even though we could show F, it is still in a fallback state because
1461 // E is not yet resolved. We need to resolve everything in the head first.
1462 expect(ReactNoop).toMatchRenderedOutput(
1463 <>
1464 <span hidden={true}>A</span>
1465 <span>Loading A</span>
1466 <span hidden={true}>B</span>
1467 <span>Loading B</span>
1468 <span>C</span>
1469 <span hidden={true}>D</span>
1470 <span>Loading D</span>
1471 <span>E</span>
1472 <span hidden={true}>F</span>
1473 <span>Loading F</span>
1474 </>,
1475 );
1476
1477 await act(() => D.resolve());
1478 assertLog([
1479 'D',
1480 'F',
1481 'Suspend! [B]',
1482 // pre-warming
1483 'Suspend! [B]',
1484 ]);
1485
1486 // We can now resolve the full head.
1487 expect(ReactNoop).toMatchRenderedOutput(
1488 <>
1489 <span hidden={true}>A</span>
1490 <span>Loading A</span>
1491 <span hidden={true}>B</span>
1492 <span>Loading B</span>
1493 <span>C</span>
1494 <span>D</span>
1495 <span>E</span>
1496 <span>F</span>
1497 </>,
1498 );
1499
1500 await act(() => B.resolve());
1501 assertLog([
1502 'B',
1503 'Suspend! [A]',
1504 // pre-warming
1505 'Suspend! [A]',
1506 ]);
1507
1508 // In the tail we can resolve one-by-one.
1509 expect(ReactNoop).toMatchRenderedOutput(
1510 <>
1511 <span hidden={true}>A</span>
1512 <span>Loading A</span>
1513 <span>B</span>
1514 <span>C</span>
1515 <span>D</span>
1516 <span>E</span>
1517 <span>F</span>
1518 </>,
1519 );
1520
1521 await act(() => A.resolve());
1522 assertLog(['A']);
1523
1524 expect(ReactNoop).toMatchRenderedOutput(
1525 <>
1526 <span>A</span>
1527 <span>B</span>
1528 <span>C</span>
1529 <span>D</span>
1530 <span>E</span>
1531 <span>F</span>
1532 </>,
1533 );
1534 });
1535
1536 // @gate enableSuspenseList
1537 it('switches to rendering fallbacks if the tail takes long CPU time', async () => {
1538 function Foo() {
1539 return (
1540 <SuspenseList revealOrder="forwards" tail="visible">
1541 <Suspense fallback={<Text text="Loading A" />}>
1542 <Text text="A" />
1543 </Suspense>
1544 <Suspense fallback={<Text text="Loading B" />}>
1545 <Text text="B" />
1546 </Suspense>
1547 <Suspense fallback={<Text text="Loading C" />}>
1548 <Text text="C" />
1549 </Suspense>
1550 </SuspenseList>
1551 );
1552 }
1553
1554 // This render is only CPU bound. Nothing suspends.
1555 await act(async () => {
1556 React.startTransition(() => {
1557 ReactNoop.render(<Foo />);
1558 });
1559
1560 await waitFor(['A']);
1561
1562 Scheduler.unstable_advanceTime(200);
1563 jest.advanceTimersByTime(200);
1564
1565 await waitFor(['B']);
1566
1567 Scheduler.unstable_advanceTime(300);
1568 jest.advanceTimersByTime(300);
1569
1570 // We've still not been able to show anything on the screen even though
1571 // we have two items ready.
1572 expect(ReactNoop).toMatchRenderedOutput(null);
1573
1574 // Time has now elapsed for so long that we're just going to give up
1575 // rendering the rest of the content. So that we can at least show
1576 // something.
1577 await waitFor([
1578 'Loading C',
1579 'C', // I'll flush through into the next render so that the first commits.
1580 ]);
1581
1582 expect(ReactNoop).toMatchRenderedOutput(
1583 <>
1584 <span>A</span>
1585 <span>B</span>
1586 <span>Loading C</span>
1587 </>,
1588 );
1589 });
1590 // Then we do a second pass to commit the last item.
1591 assertLog([]);
1592 expect(ReactNoop).toMatchRenderedOutput(
1593 <>
1594 <span>A</span>
1595 <span>B</span>
1596 <span>C</span>
1597 </>,
1598 );
1599 });
1600
1601 // @gate enableSuspenseList
1602 it('only shows one loading state at a time for "collapsed" tail insertions', async () => {
1603 const A = createAsyncText('A');
1604 const B = createAsyncText('B');
1605 const C = createAsyncText('C');
1606
1607 function Foo() {
1608 return (
1609 <SuspenseList revealOrder="forwards" tail="collapsed">
1610 <Suspense fallback={<Text text="Loading A" />}>
1611 <A />
1612 </Suspense>
1613 <Suspense fallback={<Text text="Loading B" />}>
1614 <B />
1615 </Suspense>
1616 <Suspense fallback={<Text text="Loading C" />}>
1617 <C />
1618 </Suspense>
1619 </SuspenseList>
1620 );
1621 }
1622
1623 ReactNoop.render(<Foo />);
1624
1625 await waitForAll([
1626 'Suspend! [A]',
1627 'Loading A',
1628 // pre-warming
1629 'Suspend! [A]',
1630 ]);
1631
1632 expect(ReactNoop).toMatchRenderedOutput(<span>Loading A</span>);
1633
1634 await A.resolve();
1635
1636 await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
1637
1638 // Incremental loading is suspended.
1639 jest.advanceTimersByTime(500);
1640
1641 expect(ReactNoop).toMatchRenderedOutput(
1642 <>
1643 <span>A</span>
1644 <span>Loading B</span>
1645 </>,
1646 );
1647
1648 await B.resolve();
1649
1650 await waitForAll(['B', 'Suspend! [C]', 'Loading C']);
1651
1652 // Incremental loading is suspended.
1653 jest.advanceTimersByTime(500);
1654
1655 expect(ReactNoop).toMatchRenderedOutput(
1656 <>
1657 <span>A</span>
1658 <span>B</span>
1659 <span>Loading C</span>
1660 </>,
1661 );
1662
1663 await act(() => C.resolve());
1664 await assertLog(['C']);
1665
1666 expect(ReactNoop).toMatchRenderedOutput(
1667 <>
1668 <span>A</span>
1669 <span>B</span>
1670 <span>C</span>
1671 </>,
1672 );
1673 });
1674
1675 // @gate enableSuspenseList
1676 it('behaves as tail=hidden if no tail option is specified', async () => {
1677 const A = createAsyncText('A');
1678 const B = createAsyncText('B');
1679 const C = createAsyncText('C');
1680
1681 function Foo() {
1682 return (
1683 <SuspenseList revealOrder="forwards">
1684 <Suspense fallback={<Text text="Loading A" />}>
1685 <A />
1686 </Suspense>
1687 <Suspense fallback={<Text text="Loading B" />}>
1688 <B />
1689 </Suspense>
1690 <Suspense fallback={<Text text="Loading C" />}>
1691 <C />
1692 </Suspense>
1693 </SuspenseList>
1694 );
1695 }
1696
1697 ReactNoop.render(<Foo />);
1698
1699 await waitForAll(['Suspend! [A]', 'Loading A']);
1700
1701 expect(ReactNoop).toMatchRenderedOutput(null);
1702
1703 await A.resolve();
1704
1705 await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
1706
1707 // Incremental loading is suspended.
1708 jest.advanceTimersByTime(500);
1709
1710 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
1711
1712 await act(() => B.resolve());
1713 assertLog(['B', 'Suspend! [C]', 'Loading C']);
1714
1715 // Incremental loading is suspended.
1716 jest.advanceTimersByTime(500);
1717
1718 expect(ReactNoop).toMatchRenderedOutput(
1719 <>
1720 <span>A</span>
1721 <span>B</span>
1722 </>,
1723 );
1724
1725 await act(() => C.resolve());
1726 assertLog(['C']);
1727
1728 expect(ReactNoop).toMatchRenderedOutput(
1729 <>
1730 <span>A</span>
1731 <span>B</span>
1732 <span>C</span>
1733 </>,
1734 );
1735 });
1736
1737 // @gate enableSuspenseList
1738 it('warns if an unsupported tail option is used', async () => {
1739 function Foo() {
1740 return (
1741 <SuspenseList revealOrder="forwards" tail="collapse">
1742 <Suspense fallback="Loading">A</Suspense>
1743 <Suspense fallback="Loading">B</Suspense>
1744 </SuspenseList>
1745 );
1746 }
1747
1748 await act(() => {
1749 ReactNoop.render(<Foo />);
1750 });
1751 assertConsoleErrorDev([
1752 '"collapse" is not a supported value for tail on ' +
1753 '<SuspenseList />. Did you mean "visible", "collapsed" or "hidden"?' +
1754 '\n in SuspenseList (at **)' +
1755 '\n in Foo (at **)',
1756 ]);
1757 });
1758
1759 // @gate enableSuspenseList
1760 it('warns if a tail option is used with "together"', async () => {
1761 function Foo() {
1762 return (
1763 <SuspenseList revealOrder="together" tail="collapsed">
1764 <Suspense fallback="Loading">Content</Suspense>
1765 </SuspenseList>
1766 );
1767 }
1768
1769 await act(() => {
1770 ReactNoop.render(<Foo />);
1771 });
1772 assertConsoleErrorDev([
1773 '<SuspenseList tail="collapsed" /> is only valid if ' +
1774 'revealOrder is "forwards" (default) or "backwards". ' +
1775 'Did you mean to specify revealOrder="forwards"?' +
1776 '\n in SuspenseList (at **)' +
1777 '\n in Foo (at **)',
1778 ]);
1779 });
1780
1781 // @gate enableSuspenseList
1782 it('renders one "collapsed" fallback even if CPU time elapsed', async () => {
1783 function Foo() {
1784 return (
1785 <SuspenseList revealOrder="forwards" tail="collapsed">
1786 <Suspense fallback={<Text text="Loading A" />}>
1787 <Text text="A" />
1788 </Suspense>
1789 <Suspense fallback={<Text text="Loading B" />}>
1790 <Text text="B" />
1791 </Suspense>
1792 <Suspense fallback={<Text text="Loading C" />}>
1793 <Text text="C" />
1794 </Suspense>
1795 <Suspense fallback={<Text text="Loading D" />}>
1796 <Text text="D" />
1797 </Suspense>
1798 </SuspenseList>
1799 );
1800 }
1801
1802 // This render is only CPU bound. Nothing suspends.
1803 await act(async () => {
1804 React.startTransition(() => {
1805 ReactNoop.render(<Foo />);
1806 });
1807
1808 await waitFor(['A']);
1809
1810 Scheduler.unstable_advanceTime(200);
1811 jest.advanceTimersByTime(200);
1812
1813 await waitFor(['B']);
1814
1815 Scheduler.unstable_advanceTime(300);
1816 jest.advanceTimersByTime(300);
1817
1818 // We've still not been able to show anything on the screen even though
1819 // we have two items ready.
1820 expect(ReactNoop).toMatchRenderedOutput(null);
1821
1822 // Time has now elapsed for so long that we're just going to give up
1823 // rendering the rest of the content. So that we can at least show
1824 // something.
1825 await waitFor([
1826 'Loading C',
1827 'C', // I'll flush through into the next render so that the first commits.
1828 ]);
1829
1830 expect(ReactNoop).toMatchRenderedOutput(
1831 <>
1832 <span>A</span>
1833 <span>B</span>
1834 <span>Loading C</span>
1835 </>,
1836 );
1837 });
1838 // Then we do a second pass to commit the last two items.
1839 assertLog(['D']);
1840 expect(ReactNoop).toMatchRenderedOutput(
1841 <>
1842 <span>A</span>
1843 <span>B</span>
1844 <span>C</span>
1845 <span>D</span>
1846 </>,
1847 );
1848 });
1849
1850 // @gate enableSuspenseList
1851 it('adding to the middle does not collapse insertions (forwards)', async () => {
1852 const A = createAsyncText('A');
1853 const B = createAsyncText('B');
1854 const C = createAsyncText('C');
1855 const D = createAsyncText('D');
1856 const E = createAsyncText('E');
1857 const F = createAsyncText('F');
1858
1859 function Foo({items}) {
1860 return (
1861 <SuspenseList revealOrder="forwards" tail="collapsed">
1862 {items.map(([key, Component]) => (
1863 <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
1864 <Component />
1865 </Suspense>
1866 ))}
1867 </SuspenseList>
1868 );
1869 }
1870
1871 ReactNoop.render(
1872 <Foo
1873 items={[
1874 ['A', A],
1875 ['D', D],
1876 ]}
1877 />,
1878 );
1879
1880 await A.resolve();
1881 await D.resolve();
1882
1883 await waitForAll(['A', 'D']);
1884
1885 // First render commits A and D.
1886 expect(ReactNoop).toMatchRenderedOutput(
1887 <>
1888 <span>A</span>
1889 <span>D</span>
1890 </>,
1891 );
1892
1893 // For the second render, we're going to insert items in the middle and end.
1894 ReactNoop.render(
1895 <Foo
1896 items={[
1897 ['A', A],
1898 ['B', B],
1899 ['C', C],
1900 ['D', D],
1901 ['E', E],
1902 ['F', F],
1903 ]}
1904 />,
1905 );
1906
1907 await waitForAll([
1908 'A',
1909 'Suspend! [B]',
1910 'Loading B',
1911 'Suspend! [C]',
1912 'Loading C',
1913 'D',
1914 'A',
1915 'Loading B',
1916 'Loading C',
1917 'D',
1918 'Loading E',
1919 ]);
1920
1921 // B and C don't get collapsed, but F gets collapsed with E.
1922 expect(ReactNoop).toMatchRenderedOutput(
1923 <>
1924 <span>A</span>
1925 <span>Loading B</span>
1926 <span>Loading C</span>
1927 <span>D</span>
1928 <span>Loading E</span>
1929 </>,
1930 );
1931
1932 await B.resolve();
1933
1934 await waitForAll(['B', 'Suspend! [C]']);
1935
1936 // Incremental loading is suspended.
1937 jest.advanceTimersByTime(500);
1938
1939 // Even though B is unsuspended, it's still in loading state because
1940 // it is blocked by C.
1941 expect(ReactNoop).toMatchRenderedOutput(
1942 <>
1943 <span>A</span>
1944 <span>Loading B</span>
1945 <span>Loading C</span>
1946 <span>D</span>
1947 <span>Loading E</span>
1948 </>,
1949 );
1950
1951 await C.resolve();
1952 await E.resolve();
1953
1954 await waitForAll(['B', 'C', 'E', 'Suspend! [F]', 'Loading F']);
1955
1956 jest.advanceTimersByTime(500);
1957
1958 expect(ReactNoop).toMatchRenderedOutput(
1959 <>
1960 <span>A</span>
1961 <span>B</span>
1962 <span>C</span>
1963 <span>D</span>
1964 <span>E</span>
1965 <span>Loading F</span>
1966 </>,
1967 );
1968
1969 await F.resolve();
1970
1971 await waitForAll(['F']);
1972
1973 jest.advanceTimersByTime(500);
1974
1975 expect(ReactNoop).toMatchRenderedOutput(
1976 <>
1977 <span>A</span>
1978 <span>B</span>
1979 <span>C</span>
1980 <span>D</span>
1981 <span>E</span>
1982 <span>F</span>
1983 </>,
1984 );
1985 });
1986
1987 // @gate enableSuspenseList
1988 it('adding to the middle does not collapse insertions (backwards)', async () => {
1989 const A = createAsyncText('A');
1990 const B = createAsyncText('B');
1991 const C = createAsyncText('C');
1992 const D = createAsyncText('D');
1993 const E = createAsyncText('E');
1994 const F = createAsyncText('F');
1995
1996 function Foo({items}) {
1997 return (
1998 <SuspenseList revealOrder="unstable_legacy-backwards" tail="collapsed">
1999 {items.map(([key, Component]) => (
2000 <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
2001 <Component />
2002 </Suspense>
2003 ))}
2004 </SuspenseList>
2005 );
2006 }
2007
2008 ReactNoop.render(
2009 <Foo
2010 items={[
2011 ['C', C],
2012 ['F', F],
2013 ]}
2014 />,
2015 );
2016
2017 await C.resolve();
2018 await F.resolve();
2019
2020 await waitForAll(['F', 'C']);
2021
2022 // First render commits C and F.
2023 expect(ReactNoop).toMatchRenderedOutput(
2024 <>
2025 <span>C</span>
2026 <span>F</span>
2027 </>,
2028 );
2029
2030 // For the second render, we're going to insert items in the middle and end.
2031 ReactNoop.render(
2032 <Foo
2033 items={[
2034 ['A', A],
2035 ['B', B],
2036 ['C', C],
2037 ['D', D],
2038 ['E', E],
2039 ['F', F],
2040 ]}
2041 />,
2042 );
2043
2044 await waitForAll([
2045 'C',
2046 'Suspend! [D]',
2047 'Loading D',
2048 'Suspend! [E]',
2049 'Loading E',
2050 'F',
2051 'C',
2052 'Loading D',
2053 'Loading E',
2054 'F',
2055 'Loading B',
2056 ]);
2057
2058 // D and E don't get collapsed, but A gets collapsed with B.
2059 expect(ReactNoop).toMatchRenderedOutput(
2060 <>
2061 <span>Loading B</span>
2062 <span>C</span>
2063 <span>Loading D</span>
2064 <span>Loading E</span>
2065 <span>F</span>
2066 </>,
2067 );
2068
2069 await D.resolve();
2070
2071 await waitForAll(['D', 'Suspend! [E]']);
2072
2073 // Incremental loading is suspended.
2074 jest.advanceTimersByTime(500);
2075
2076 // Even though D is unsuspended, it's still in loading state because
2077 // it is blocked by E.
2078 expect(ReactNoop).toMatchRenderedOutput(
2079 <>
2080 <span>Loading B</span>
2081 <span>C</span>
2082 <span>Loading D</span>
2083 <span>Loading E</span>
2084 <span>F</span>
2085 </>,
2086 );
2087
2088 await C.resolve();
2089 await E.resolve();
2090
2091 await B.resolve();
2092 await C.resolve();
2093 await D.resolve();
2094 await E.resolve();
2095
2096 await waitForAll(['D', 'E', 'B', 'Suspend! [A]', 'Loading A']);
2097
2098 jest.advanceTimersByTime(500);
2099
2100 expect(ReactNoop).toMatchRenderedOutput(
2101 <>
2102 <span>Loading A</span>
2103 <span>B</span>
2104 <span>C</span>
2105 <span>D</span>
2106 <span>E</span>
2107 <span>F</span>
2108 </>,
2109 );
2110
2111 await A.resolve();
2112
2113 await waitForAll(['A']);
2114
2115 jest.advanceTimersByTime(500);
2116
2117 expect(ReactNoop).toMatchRenderedOutput(
2118 <>
2119 <span>A</span>
2120 <span>B</span>
2121 <span>C</span>
2122 <span>D</span>
2123 <span>E</span>
2124 <span>F</span>
2125 </>,
2126 );
2127 });
2128
2129 // @gate enableSuspenseList
2130 it('adding to the middle of committed tail does not collapse insertions', async () => {
2131 const A = createAsyncText('A');
2132 const B = createAsyncText('B');
2133 const C = createAsyncText('C');
2134 const D = createAsyncText('D');
2135 const E = createAsyncText('E');
2136 const F = createAsyncText('F');
2137
2138 function SyncD() {
2139 return <Text text="D" />;
2140 }
2141
2142 function Foo({items}) {
2143 return (
2144 <SuspenseList revealOrder="forwards" tail="collapsed">
2145 {items.map(([key, Component]) => (
2146 <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
2147 <Component />
2148 </Suspense>
2149 ))}
2150 </SuspenseList>
2151 );
2152 }
2153
2154 ReactNoop.render(
2155 <Foo
2156 items={[
2157 ['A', A],
2158 ['D', SyncD],
2159 ]}
2160 />,
2161 );
2162
2163 await A.resolve();
2164
2165 await waitForAll(['A', 'D']);
2166
2167 // First render commits A and D.
2168 expect(ReactNoop).toMatchRenderedOutput(
2169 <>
2170 <span>A</span>
2171 <span>D</span>
2172 </>,
2173 );
2174
2175 // For the second render, we're going to insert items in the middle and end.
2176 // Note that D now suspends even though it didn't in the first pass.
2177 ReactNoop.render(
2178 <Foo
2179 items={[
2180 ['A', A],
2181 ['B', B],
2182 ['C', C],
2183 ['D', D],
2184 ['E', E],
2185 ['F', F],
2186 ]}
2187 />,
2188 );
2189
2190 await waitForAll([
2191 'A',
2192 'Suspend! [B]',
2193 'Loading B',
2194 'Suspend! [C]',
2195 'Loading C',
2196 'Suspend! [D]',
2197 'Loading D',
2198 'A',
2199 'Loading B',
2200 'Loading C',
2201 'Suspend! [D]',
2202 'Loading D',
2203 'Loading E',
2204 // pre-warming
2205 'Suspend! [B]',
2206 ]);
2207
2208 // This is suspended due to the update to D causing a loading state.
2209 jest.advanceTimersByTime(500);
2210
2211 // B and C don't get collapsed, but F gets collapsed with E.
2212 // Even though everything in the bottom of the list is suspended, we don't
2213 // collapse them because D was an update. Not an insertion.
2214 expect(ReactNoop).toMatchRenderedOutput(
2215 <>
2216 <span>A</span>
2217 <span>Loading B</span>
2218 <span>Loading C</span>
2219 <span hidden={true}>D</span>
2220 <span>Loading D</span>
2221 <span>Loading E</span>
2222 </>,
2223 );
2224
2225 await B.resolve();
2226
2227 await waitForAll([
2228 'B',
2229 'Suspend! [C]',
2230 ...(!gate('alwaysThrottleRetries') ? ['Suspend! [C]'] : []),
2231 ]);
2232
2233 // Incremental loading is suspended.
2234 jest.advanceTimersByTime(500);
2235
2236 // B is able to unblock here because it's part of the tail.
2237 // If D was still visible it wouldn't be part of the tail
2238 // and would be blocked on C like in the other test.
2239 expect(ReactNoop).toMatchRenderedOutput(
2240 <>
2241 <span>A</span>
2242 <span>B</span>
2243 <span>Loading C</span>
2244 <span hidden={true}>D</span>
2245 <span>Loading D</span>
2246 <span>Loading E</span>
2247 </>,
2248 );
2249
2250 await C.resolve();
2251 await D.resolve();
2252 await E.resolve();
2253
2254 await waitForAll(['C', 'D', 'E', 'Suspend! [F]', 'Loading F']);
2255
2256 jest.advanceTimersByTime(500);
2257
2258 expect(ReactNoop).toMatchRenderedOutput(
2259 <>
2260 <span>A</span>
2261 <span>B</span>
2262 <span>C</span>
2263 <span>D</span>
2264 <span>E</span>
2265 <span>Loading F</span>
2266 </>,
2267 );
2268
2269 await F.resolve();
2270
2271 await waitForAll(['F']);
2272
2273 jest.advanceTimersByTime(500);
2274
2275 expect(ReactNoop).toMatchRenderedOutput(
2276 <>
2277 <span>A</span>
2278 <span>B</span>
2279 <span>C</span>
2280 <span>D</span>
2281 <span>E</span>
2282 <span>F</span>
2283 </>,
2284 );
2285 });
2286
2287 // @gate enableSuspenseList
2288 it('only shows no initial loading state "hidden" tail insertions', async () => {
2289 const A = createAsyncText('A');
2290 const B = createAsyncText('B');
2291 const C = createAsyncText('C');
2292
2293 function Foo() {
2294 return (
2295 <SuspenseList revealOrder="forwards" tail="hidden">
2296 <Suspense fallback={<Text text="Loading A" />}>
2297 <A />
2298 </Suspense>
2299 <Suspense fallback={<Text text="Loading B" />}>
2300 <B />
2301 </Suspense>
2302 <Suspense fallback={<Text text="Loading C" />}>
2303 <C />
2304 </Suspense>
2305 </SuspenseList>
2306 );
2307 }
2308
2309 ReactNoop.render(<Foo />);
2310
2311 await waitForAll(['Suspend! [A]', 'Loading A']);
2312
2313 expect(ReactNoop).toMatchRenderedOutput(null);
2314
2315 await A.resolve();
2316
2317 await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
2318
2319 // Incremental loading is suspended.
2320 jest.advanceTimersByTime(500);
2321
2322 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
2323
2324 await act(() => B.resolve());
2325 assertLog(['B', 'Suspend! [C]', 'Loading C']);
2326
2327 // Incremental loading is suspended.
2328 jest.advanceTimersByTime(500);
2329
2330 expect(ReactNoop).toMatchRenderedOutput(
2331 <>
2332 <span>A</span>
2333 <span>B</span>
2334 </>,
2335 );
2336
2337 await act(() => C.resolve());
2338 assertLog(['C']);
2339
2340 expect(ReactNoop).toMatchRenderedOutput(
2341 <>
2342 <span>A</span>
2343 <span>B</span>
2344 <span>C</span>
2345 </>,
2346 );
2347 });
2348
2349 // @gate enableSuspenseList
2350 it('reveals "hidden" rows one by one without suspense boundaries', async () => {
2351 const A = createAsyncText('A');
2352 const B = createAsyncText('B');
2353 const C = createAsyncText('C');
2354
2355 function Foo() {
2356 return (
2357 <SuspenseList revealOrder="forwards" tail="hidden">
2358 <div>
2359 <A />
2360 </div>
2361 <B />
2362 <C />
2363 </SuspenseList>
2364 );
2365 }
2366
2367 ReactNoop.render(
2368 <Suspense fallback="Loading root">
2369 <Foo />
2370 </Suspense>,
2371 );
2372
2373 await waitForAll(['Suspend! [A]']);
2374
2375 // We can commit without any rows at all leaving empty.
2376 expect(ReactNoop).toMatchRenderedOutput(null);
2377
2378 await act(() => A.resolve());
2379 assertLog(['A', 'Suspend! [B]']);
2380
2381 expect(ReactNoop).toMatchRenderedOutput(
2382 <div>
2383 <span>A</span>
2384 </div>,
2385 );
2386
2387 await act(() => B.resolve());
2388 assertLog(['B', 'Suspend! [C]']);
2389
2390 // Incremental loading is suspended.
2391 jest.advanceTimersByTime(500);
2392
2393 expect(ReactNoop).toMatchRenderedOutput(
2394 <>
2395 <div>
2396 <span>A</span>
2397 </div>
2398 <span>B</span>
2399 </>,
2400 );
2401
2402 await act(() => C.resolve());
2403 assertLog(['C']);
2404
2405 expect(ReactNoop).toMatchRenderedOutput(
2406 <>
2407 <div>
2408 <span>A</span>
2409 </div>
2410 <span>B</span>
2411 <span>C</span>
2412 </>,
2413 );
2414 });
2415
2416 // @gate enableSuspenseList
2417 it('preserves already mounted rows when a new hidden on is inserted in the tail', async () => {
2418 const B = createAsyncText('B');
2419 const C = createAsyncText('C');
2420
2421 let count = 0;
2422 function MountCount({children}) {
2423 // This component should only mount once.
2424 React.useLayoutEffect(() => {
2425 count++;
2426 }, []);
2427 return children;
2428 }
2429
2430 function Foo({insert}) {
2431 return (
2432 <SuspenseList
2433 revealOrder="forwards"
2434 tail={insert ? 'hidden' : 'visible'}>
2435 <Text text="A" />
2436 {insert ? <B /> : null}
2437 <MountCount>
2438 <Suspense fallback={<Text text="Loading C" />}>
2439 <C />
2440 </Suspense>
2441 </MountCount>
2442 </SuspenseList>
2443 );
2444 }
2445
2446 await act(() => {
2447 ReactNoop.render(<Foo insert={false} />);
2448 });
2449 assertLog(['A', 'Suspend! [C]', 'Loading C', 'Suspend! [C]']);
2450
2451 expect(count).toBe(1);
2452
2453 expect(ReactNoop).toMatchRenderedOutput(
2454 <>
2455 <span>A</span>
2456 <span>Loading C</span>
2457 </>,
2458 );
2459
2460 await act(() => {
2461 ReactNoop.render(<Foo insert={true} />);
2462 });
2463
2464 assertLog(['A', 'Suspend! [B]', 'A', 'Suspend! [B]']);
2465
2466 expect(count).toBe(1);
2467
2468 expect(ReactNoop).toMatchRenderedOutput(
2469 <>
2470 <span>A</span>
2471 <span>Loading C</span>
2472 </>,
2473 );
2474
2475 await act(async () => {
2476 await B.resolve();
2477 await C.resolve();
2478 });
2479
2480 assertLog(['A', 'B', 'C']);
2481
2482 expect(count).toBe(1);
2483
2484 expect(ReactNoop).toMatchRenderedOutput(
2485 <>
2486 <span>A</span>
2487 <span>B</span>
2488 <span>C</span>
2489 </>,
2490 );
2491 });
2492
2493 // @gate enableSuspenseList
2494 it('reveals "collapsed" rows one by one after the first without boundaries', async () => {
2495 const A = createAsyncText('A');
2496 const B = createAsyncText('B');
2497 const C = createAsyncText('C');
2498
2499 function Foo() {
2500 return (
2501 <SuspenseList revealOrder="forwards" tail="collapsed">
2502 <A />
2503 <Suspense fallback={<Text text="Loading B" />}>
2504 <B />
2505 </Suspense>
2506 <C />
2507 </SuspenseList>
2508 );
2509 }
2510
2511 await act(async () => {
2512 ReactNoop.render(
2513 <Suspense fallback="Loading root">
2514 <Foo />
2515 </Suspense>,
2516 );
2517 await waitForAll(['Suspend! [A]', 'Suspend! [A]']);
2518 });
2519
2520 // The root is still blocked on the first row.
2521 expect(ReactNoop).toMatchRenderedOutput('Loading root');
2522
2523 await A.resolve();
2524
2525 await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
2526
2527 // Incremental loading is suspended.
2528 jest.advanceTimersByTime(500);
2529
2530 // Because we have a Suspense boundary that can commit we can now unblock the rest.
2531 // If it wasn't a boundary then we couldn't make progress because it would commit
2532 // without any loading state.
2533 expect(ReactNoop).toMatchRenderedOutput(
2534 <>
2535 <span>A</span>
2536 <span>Loading B</span>
2537 </>,
2538 );
2539
2540 await act(() => B.resolve());
2541 assertLog(['B', 'Suspend! [C]', 'B', 'Suspend! [C]']);
2542
2543 // Incremental loading is suspended.
2544 jest.advanceTimersByTime(500);
2545
2546 // Surprisingly unsuspending B actually causes the parent to resuspend
2547 // because C is now unblocked which resuspends the parent. Preventing the
2548 // Retry from committing. That's because we don't want to commit into a
2549 // state that doesn't have any loading indicators at all. That's what
2550 // "collapsed" is for. To ensure there's always a loading indicator.
2551 expect(ReactNoop).toMatchRenderedOutput(
2552 <>
2553 <span>A</span>
2554 <span>Loading B</span>
2555 </>,
2556 );
2557
2558 await act(() => C.resolve());
2559 assertLog(['B', 'C']);
2560
2561 expect(ReactNoop).toMatchRenderedOutput(
2562 <>
2563 <span>A</span>
2564 <span>B</span>
2565 <span>C</span>
2566 </>,
2567 );
2568 });
2569
2570 // @gate enableSuspenseList
2571 it('eventually resolves a nested forwards suspense list', async () => {
2572 const B = createAsyncText('B');
2573
2574 function Foo() {
2575 return (
2576 <SuspenseList revealOrder="together">
2577 <SuspenseList revealOrder="forwards" tail="visible">
2578 <Suspense fallback={<Text text="Loading A" />}>
2579 <Text text="A" />
2580 </Suspense>
2581 <Suspense fallback={<Text text="Loading B" />}>
2582 <B />
2583 </Suspense>
2584 <Suspense fallback={<Text text="Loading C" />}>
2585 <Text text="C" />
2586 </Suspense>
2587 </SuspenseList>
2588 <Suspense fallback={<Text text="Loading D" />}>
2589 <Text text="D" />
2590 </Suspense>
2591 </SuspenseList>
2592 );
2593 }
2594
2595 ReactNoop.render(<Foo />);
2596
2597 await waitForAll([
2598 'A',
2599 'Suspend! [B]',
2600 'Loading B',
2601 'Loading C',
2602 'D',
2603 // The second pass forces the fallbacks
2604 'Loading A',
2605 'Loading B',
2606 'Loading C',
2607 'Loading D',
2608 ]);
2609
2610 expect(ReactNoop).toMatchRenderedOutput(
2611 <>
2612 <span>Loading A</span>
2613 <span>Loading B</span>
2614 <span>Loading C</span>
2615 <span>Loading D</span>
2616 </>,
2617 );
2618
2619 await act(() => B.resolve());
2620 assertLog(['A', 'B', 'C', 'D']);
2621
2622 expect(ReactNoop).toMatchRenderedOutput(
2623 <>
2624 <span>A</span>
2625 <span>B</span>
2626 <span>C</span>
2627 <span>D</span>
2628 </>,
2629 );
2630 });
2631
2632 // @gate enableSuspenseList
2633 it('eventually resolves a nested forwards suspense list with a hidden tail', async () => {
2634 const B = createAsyncText('B');
2635
2636 function Foo() {
2637 return (
2638 <SuspenseList revealOrder="together">
2639 <SuspenseList revealOrder="forwards" tail="hidden">
2640 <Suspense fallback={<Text text="Loading A" />}>
2641 <Text text="A" />
2642 </Suspense>
2643 <Suspense fallback={<Text text="Loading B" />}>
2644 <B />
2645 </Suspense>
2646 </SuspenseList>
2647 <Suspense fallback={<Text text="Loading C" />}>
2648 <Text text="C" />
2649 </Suspense>
2650 </SuspenseList>
2651 );
2652 }
2653
2654 ReactNoop.render(<Foo />);
2655
2656 await waitForAll(['A', 'Suspend! [B]', 'Loading B', 'C', 'Loading C']);
2657
2658 expect(ReactNoop).toMatchRenderedOutput(<span>Loading C</span>);
2659
2660 await act(() => B.resolve());
2661 assertLog(['A', 'B', 'C']);
2662
2663 expect(ReactNoop).toMatchRenderedOutput(
2664 <>
2665 <span>A</span>
2666 <span>B</span>
2667 <span>C</span>
2668 </>,
2669 );
2670 });
2671
2672 // @gate enableSuspenseList
2673 it('eventually resolves two nested forwards suspense lists with a hidden tail', async () => {
2674 const B = createAsyncText('B');
2675
2676 function Foo({showB}) {
2677 return (
2678 <SuspenseList revealOrder="forwards" tail="visible">
2679 <SuspenseList revealOrder="forwards" tail="hidden">
2680 <Suspense fallback={<Text text="Loading A" />}>
2681 <Text text="A" />
2682 </Suspense>
2683 {showB ? (
2684 <Suspense fallback={<Text text="Loading B" />}>
2685 <B />
2686 </Suspense>
2687 ) : null}
2688 </SuspenseList>
2689 <Suspense fallback={<Text text="Loading C" />}>
2690 <Text text="C" />
2691 </Suspense>
2692 </SuspenseList>
2693 );
2694 }
2695
2696 ReactNoop.render(<Foo showB={false} />);
2697
2698 await waitForAll(['A', 'C']);
2699
2700 expect(ReactNoop).toMatchRenderedOutput(
2701 <>
2702 <span>A</span>
2703 <span>C</span>
2704 </>,
2705 );
2706
2707 // Showing the B later means that C has already committed
2708 // so we're now effectively in "together" mode for the head.
2709 ReactNoop.render(<Foo showB={true} />);
2710
2711 await waitForAll(['A', 'Suspend! [B]', 'Loading B', 'C', 'A', 'C']);
2712
2713 expect(ReactNoop).toMatchRenderedOutput(
2714 <>
2715 <span>A</span>
2716 <span>C</span>
2717 </>,
2718 );
2719
2720 await act(() => B.resolve());
2721 assertLog(['B']);
2722
2723 expect(ReactNoop).toMatchRenderedOutput(
2724 <>
2725 <span>A</span>
2726 <span>B</span>
2727 <span>C</span>
2728 </>,
2729 );
2730 });
2731
2732 // @gate enableSuspenseList
2733 it('can do unrelated adjacent updates', async () => {
2734 let updateAdjacent;
2735 function Adjacent() {
2736 const [text, setText] = React.useState('-');
2737 updateAdjacent = setText;
2738 return <Text text={text} />;
2739 }
2740
2741 function Foo() {
2742 return (
2743 <div>
2744 <SuspenseList revealOrder="forwards" tail="visible">
2745 <Text text="A" />
2746 <Text text="B" />
2747 </SuspenseList>
2748 <Adjacent />
2749 </div>
2750 );
2751 }
2752
2753 ReactNoop.render(<Foo />);
2754
2755 await waitForAll(['A', 'B', '-']);
2756
2757 expect(ReactNoop).toMatchRenderedOutput(
2758 <div>
2759 <span>A</span>
2760 <span>B</span>
2761 <span>-</span>
2762 </div>,
2763 );
2764
2765 // Update the row adjacent to the list
2766 await act(() => updateAdjacent('C'));
2767
2768 assertLog(['C']);
2769
2770 expect(ReactNoop).toMatchRenderedOutput(
2771 <div>
2772 <span>A</span>
2773 <span>B</span>
2774 <span>C</span>
2775 </div>,
2776 );
2777 });
2778
2779 // @gate enableSuspenseList
2780 it('is able to re-suspend the last rows during an update with hidden', async () => {
2781 const AsyncB = createAsyncText('B');
2782
2783 let setAsyncB;
2784
2785 function B() {
2786 const [shouldBeAsync, setAsync] = React.useState(false);
2787 setAsyncB = setAsync;
2788
2789 return shouldBeAsync ? (
2790 <Suspense fallback={<Text text="Loading B" />}>
2791 <AsyncB />
2792 </Suspense>
2793 ) : (
2794 <Text text="Sync B" />
2795 );
2796 }
2797
2798 function Foo({updateList}) {
2799 return (
2800 <SuspenseList revealOrder="forwards" tail="hidden">
2801 <Suspense key="A" fallback={<Text text="Loading A" />}>
2802 <Text text="A" />
2803 </Suspense>
2804 <B key="B" updateList={updateList} />
2805 </SuspenseList>
2806 );
2807 }
2808
2809 ReactNoop.render(<Foo />);
2810
2811 await waitForAll(['A', 'Sync B']);
2812
2813 expect(ReactNoop).toMatchRenderedOutput(
2814 <>
2815 <span>A</span>
2816 <span>Sync B</span>
2817 </>,
2818 );
2819
2820 const previousInst = setAsyncB;
2821
2822 // During an update we suspend on B.
2823 await act(() => setAsyncB(true));
2824
2825 assertLog([
2826 'Suspend! [B]',
2827 'Loading B',
2828 // The second pass is the "force hide" pass
2829 'Loading B',
2830 ]);
2831
2832 expect(ReactNoop).toMatchRenderedOutput(
2833 <>
2834 <span>A</span>
2835 <span>Loading B</span>
2836 </>,
2837 );
2838
2839 // Before we resolve we'll rerender the whole list.
2840 // This should leave the tree intact.
2841 await act(() => ReactNoop.render(<Foo updateList={true} />));
2842
2843 assertLog([
2844 'A',
2845 'Suspend! [B]',
2846 'Loading B',
2847 // pre-warming
2848 'Suspend! [B]',
2849 ]);
2850
2851 expect(ReactNoop).toMatchRenderedOutput(
2852 <>
2853 <span>A</span>
2854 <span>Loading B</span>
2855 </>,
2856 );
2857
2858 await act(() => AsyncB.resolve());
2859 assertLog(['B']);
2860
2861 expect(ReactNoop).toMatchRenderedOutput(
2862 <>
2863 <span>A</span>
2864 <span>B</span>
2865 </>,
2866 );
2867
2868 // This should be the same instance. I.e. it didn't
2869 // remount.
2870 expect(previousInst).toBe(setAsyncB);
2871 });
2872
2873 // @gate enableSuspenseList
2874 it('is able to interrupt a partially rendered tree and continue later', async () => {
2875 const AsyncA = createAsyncText('A');
2876
2877 let updateLowPri;
2878 let updateHighPri;
2879
2880 function Bar() {
2881 const [highPriState, setHighPriState] = React.useState(false);
2882 updateHighPri = setHighPriState;
2883 return highPriState ? <AsyncA /> : null;
2884 }
2885
2886 function Foo() {
2887 const [lowPriState, setLowPriState] = React.useState(false);
2888 updateLowPri = setLowPriState;
2889 return (
2890 <SuspenseList revealOrder="forwards" tail="hidden">
2891 <Suspense key="A" fallback={<Text text="Loading A" />}>
2892 <Bar />
2893 </Suspense>
2894 {lowPriState ? <Text text="B" /> : null}
2895 {lowPriState ? <Text text="C" /> : null}
2896 {lowPriState ? <Text text="D" /> : null}
2897 </SuspenseList>
2898 );
2899 }
2900
2901 ReactNoop.render(<Foo />);
2902
2903 await waitForAll([]);
2904
2905 expect(ReactNoop).toMatchRenderedOutput(null);
2906
2907 await act(async () => {
2908 // Add a few items at the end.
2909 React.startTransition(() => {
2910 updateLowPri(true);
2911 });
2912
2913 // Flush partially through.
2914 await waitFor(['B', 'C']);
2915
2916 // Schedule another update at higher priority.
2917 ReactNoop.flushSync(() => updateHighPri(true));
2918
2919 // That will intercept the previous render.
2920 assertLog([
2921 'Suspend! [A]',
2922 'Loading A',
2923 // Re-render at forced.
2924 'Suspend! [A]',
2925 'Loading A',
2926 ]);
2927 expect(ReactNoop).toMatchRenderedOutput(<span>Loading A</span>);
2928
2929 // Try again on low-pri.
2930 await waitForAll([
2931 'Suspend! [A]',
2932 'Loading A',
2933 // pre-warming
2934 'Suspend! [A]',
2935 ]);
2936 expect(ReactNoop).toMatchRenderedOutput(<span>Loading A</span>);
2937 });
2938
2939 await act(() => AsyncA.resolve());
2940 assertLog(['A', 'B', 'C', 'D']);
2941
2942 expect(ReactNoop).toMatchRenderedOutput(
2943 <>
2944 <span>A</span>
2945 <span>B</span>
2946 <span>C</span>
2947 <span>D</span>
2948 </>,
2949 );
2950 });
2951
2952 // @gate enableSuspenseList
2953 it('can resume class components when revealed together', async () => {
2954 const A = createAsyncText('A');
2955 const B = createAsyncText('B');
2956
2957 class ClassComponent extends React.Component {
2958 render() {
2959 return this.props.children;
2960 }
2961 }
2962
2963 function Foo() {
2964 return (
2965 <Suspense fallback={<Text text="Loading" />}>
2966 <SuspenseList revealOrder="together">
2967 <ClassComponent>
2968 <Suspense fallback={<Text text="Loading A" />}>
2969 <A />
2970 </Suspense>
2971 </ClassComponent>
2972 <ClassComponent>
2973 <Suspense fallback={<Text text="Loading B" />}>
2974 <B />
2975 </Suspense>
2976 </ClassComponent>
2977 </SuspenseList>
2978 </Suspense>
2979 );
2980 }
2981
2982 await A.resolve();
2983
2984 ReactNoop.render(<Foo />);
2985
2986 await waitForAll([
2987 'A',
2988 'Suspend! [B]',
2989 'Loading B',
2990 'Loading A',
2991 'Loading B',
2992 ]);
2993
2994 expect(ReactNoop).toMatchRenderedOutput(
2995 <>
2996 <span>Loading A</span>
2997 <span>Loading B</span>
2998 </>,
2999 );
3000
3001 await B.resolve();
3002
3003 ReactNoop.render(<Foo />);
3004
3005 await waitForAll(['A', 'B']);
3006
3007 expect(ReactNoop).toMatchRenderedOutput(
3008 <>
3009 <span>A</span>
3010 <span>B</span>
3011 </>,
3012 );
3013 });
3014
3015 // @gate enableSuspenseList
3016 it('should be able to progressively show CPU expensive rows with two pass rendering', async () => {
3017 function TwoPass({text}) {
3018 const [pass, setPass] = React.useState(0);
3019 React.useLayoutEffect(() => {
3020 Scheduler.log('Mount ' + text);
3021 setPass(1);
3022 }, []);
3023 return <Text text={pass === 0 ? 'First Pass ' + text : text} />;
3024 }
3025
3026 function Sleep({time, children}) {
3027 Scheduler.unstable_advanceTime(time);
3028 return children;
3029 }
3030
3031 function App() {
3032 Scheduler.log('App');
3033 return (
3034 <SuspenseList revealOrder="forwards" tail="hidden">
3035 <Suspense fallback={<Text text="Loading A" />}>
3036 <Sleep time={600}>
3037 <TwoPass text="A" />
3038 </Sleep>
3039 </Suspense>
3040 <Suspense fallback={<Text text="Loading B" />}>
3041 <Sleep time={600}>
3042 <TwoPass text="B" />
3043 </Sleep>
3044 </Suspense>
3045 <Sleep time={600}>
3046 <Text text="C" />
3047 </Sleep>
3048 </SuspenseList>
3049 );
3050 }
3051
3052 React.startTransition(() => {
3053 ReactNoop.render(<App />);
3054 });
3055
3056 await waitFor(['App', 'First Pass A', 'Mount A', 'A']);
3057 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
3058
3059 await waitFor(['First Pass B', 'Mount B', 'B']);
3060 expect(ReactNoop).toMatchRenderedOutput(
3061 <>
3062 <span>A</span>
3063 <span>B</span>
3064 </>,
3065 );
3066
3067 await waitForAll(['C']);
3068 expect(ReactNoop).toMatchRenderedOutput(
3069 <>
3070 <span>A</span>
3071 <span>B</span>
3072 <span>C</span>
3073 </>,
3074 );
3075 });
3076
3077 // @gate enableSuspenseList
3078 it('should be able to progressively show rows with two pass rendering and visible', async () => {
3079 function TwoPass({text}) {
3080 const [pass, setPass] = React.useState(0);
3081 React.useLayoutEffect(() => {
3082 Scheduler.log('Mount ' + text);
3083 setPass(1);
3084 }, []);
3085 return <Text text={pass === 0 ? 'First Pass ' + text : text} />;
3086 }
3087
3088 function Sleep({time, children}) {
3089 Scheduler.unstable_advanceTime(time);
3090 return children;
3091 }
3092
3093 function App() {
3094 Scheduler.log('App');
3095 return (
3096 <SuspenseList revealOrder="forwards" tail="visible">
3097 <Suspense fallback={<Text text="Loading A" />}>
3098 <Sleep time={600}>
3099 <TwoPass text="A" />
3100 </Sleep>
3101 </Suspense>
3102 <Suspense fallback={<Text text="Loading B" />}>
3103 <Sleep time={600}>
3104 <TwoPass text="B" />
3105 </Sleep>
3106 </Suspense>
3107 <Suspense fallback={<Text text="Loading C" />}>
3108 <Sleep time={600}>
3109 <Text text="C" />
3110 </Sleep>
3111 </Suspense>
3112 </SuspenseList>
3113 );
3114 }
3115
3116 React.startTransition(() => {
3117 ReactNoop.render(<App />);
3118 });
3119
3120 await waitFor([
3121 'App',
3122 'First Pass A',
3123 'Loading B',
3124 'Loading C',
3125 'Mount A',
3126 'A',
3127 ]);
3128 expect(ReactNoop).toMatchRenderedOutput(
3129 <>
3130 <span>A</span>
3131 <span>Loading B</span>
3132 <span>Loading C</span>
3133 </>,
3134 );
3135
3136 await waitFor(['First Pass B', 'Mount B', 'B']);
3137 expect(ReactNoop).toMatchRenderedOutput(
3138 <>
3139 <span>A</span>
3140 <span>B</span>
3141 <span>Loading C</span>
3142 </>,
3143 );
3144
3145 await waitForAll(['C']);
3146 expect(ReactNoop).toMatchRenderedOutput(
3147 <>
3148 <span>A</span>
3149 <span>B</span>
3150 <span>C</span>
3151 </>,
3152 );
3153 });
3154
3155 // @gate enableProfilerTimer
3156 // @gate enableSuspenseList
3157 it('counts the actual duration when profiling a SuspenseList', async () => {
3158 // Order of parameters: id, phase, actualDuration, treeBaseDuration
3159 const onRender = jest.fn();
3160
3161 const Fallback = () => {
3162 Scheduler.log('Fallback');
3163 Scheduler.unstable_advanceTime(3);
3164 return <span>Loading...</span>;
3165 };
3166
3167 const A = createAsyncText('A');
3168 const B = createAsyncText('B');
3169 const C = createAsyncText('C');
3170 const D = createAsyncText('D');
3171 await A.resolve();
3172 await B.resolve();
3173
3174 function Sleep({time, children}) {
3175 Scheduler.unstable_advanceTime(time);
3176 return children;
3177 }
3178
3179 function App({addRow, suspendTail}) {
3180 Scheduler.log('App');
3181 return (
3182 <Profiler id="root" onRender={onRender}>
3183 <SuspenseList revealOrder="forwards" tail="visible">
3184 <Suspense fallback={<Fallback />}>
3185 <Sleep time={1}>
3186 <A />
3187 </Sleep>
3188 </Suspense>
3189 <Suspense fallback={<Fallback />}>
3190 <Sleep time={4}>
3191 <B />
3192 </Sleep>
3193 </Suspense>
3194 <Suspense fallback={<Fallback />}>
3195 <Sleep time={5}>{suspendTail ? <C /> : <Text text="C" />}</Sleep>
3196 </Suspense>
3197 {addRow ? (
3198 <Suspense fallback={<Fallback />}>
3199 <Sleep time={12}>
3200 <D />
3201 </Sleep>
3202 </Suspense>
3203 ) : null}
3204 </SuspenseList>
3205 </Profiler>
3206 );
3207 }
3208
3209 ReactNoop.render(<App suspendTail={true} />);
3210
3211 await waitForAll([
3212 'App',
3213 'A',
3214 'B',
3215 'Suspend! [C]',
3216 'Fallback',
3217 // pre-warming
3218 'Suspend! [C]',
3219 ]);
3220 expect(ReactNoop).toMatchRenderedOutput(
3221 <>
3222 <span>A</span>
3223 <span>B</span>
3224 <span>Loading...</span>
3225 </>,
3226 );
3227 expect(onRender).toHaveBeenCalledTimes(
3228 gate('alwaysThrottleRetries') ? 1 : 2,
3229 );
3230
3231 // The treeBaseDuration should be the time to render each child. The last
3232 // one counts the fallback time.
3233 // The actualDuration should also include the 5ms spent rendering the
3234 // last suspended row.
3235
3236 // actualDuration
3237 expect(onRender.mock.calls[0][2]).toBe(1 + 4 + 5 + 3);
3238 // treeBaseDuration
3239 expect(onRender.mock.calls[0][3]).toBe(1 + 4 + 3);
3240
3241 ReactNoop.render(<App suspendTail={false} />);
3242
3243 await waitForAll(['App', 'A', 'B', 'C']);
3244
3245 expect(ReactNoop).toMatchRenderedOutput(
3246 <>
3247 <span>A</span>
3248 <span>B</span>
3249 <span>C</span>
3250 </>,
3251 );
3252 expect(onRender).toHaveBeenCalledTimes(
3253 gate('alwaysThrottleRetries') ? 2 : 3,
3254 );
3255
3256 // actualDuration
3257 expect(onRender.mock.calls[1][2]).toBe(
3258 gate('alwaysThrottleRetries') ? 1 + 4 + 5 : 5,
3259 );
3260 // treeBaseDuration
3261 expect(onRender.mock.calls[1][3]).toBe(
3262 gate('alwaysThrottleRetries') ? 1 + 4 + 5 : 8,
3263 );
3264
3265 ReactNoop.render(<App addRow={true} suspendTail={true} />);
3266
3267 await waitForAll([
3268 'App',
3269 'A',
3270 'B',
3271 'Suspend! [C]',
3272 'Fallback',
3273 // We rendered in together mode for the head, now we re-render with forced suspense.
3274 'A',
3275 'B',
3276 'Suspend! [C]',
3277 'Fallback',
3278 // Lastly we render the tail.
3279 'Fallback',
3280 // pre-warming
3281 'Suspend! [C]',
3282 ]);
3283
3284 // Flush suspended time.
3285 jest.advanceTimersByTime(1000);
3286
3287 expect(ReactNoop).toMatchRenderedOutput(
3288 <>
3289 <span>A</span>
3290 <span>B</span>
3291 <span hidden={true}>C</span>
3292 <span>Loading...</span>
3293 <span>Loading...</span>
3294 </>,
3295 );
3296 expect(onRender).toHaveBeenCalledTimes(
3297 gate('alwaysThrottleRetries') ? 4 : 5,
3298 );
3299
3300 // The treeBaseDuration should be the time to render the first two
3301 // children and then two fallbacks.
3302 // The actualDuration should also include rendering the content of
3303 // the first fallback, as well as the second pass to render the head
3304 // with force fallback mode.
3305
3306 // actualDuration
3307 expect(onRender.mock.calls[2][2]).toBe(
3308 gate('alwaysThrottleRetries') ? (1 + 4 + 5 + 3) * 2 + 3 : 10,
3309 );
3310 // treeBaseDuration
3311 expect(onRender.mock.calls[2][3]).toBe(
3312 gate('alwaysThrottleRetries') ? 1 + 4 + 3 + 3 : 10,
3313 );
3314
3315 await act(() => C.resolve());
3316 assertLog([
3317 'C',
3318 'Suspend! [D]',
3319 // pre-warming
3320 'Suspend! [D]',
3321 ]);
3322 expect(ReactNoop).toMatchRenderedOutput(
3323 <>
3324 <span>A</span>
3325 <span>B</span>
3326 <span>C</span>
3327 <span>Loading...</span>
3328 </>,
3329 );
3330
3331 expect(onRender).toHaveBeenCalledTimes(
3332 gate('alwaysThrottleRetries') ? 6 : 7,
3333 );
3334
3335 // actualDuration
3336 expect(onRender.mock.calls[5][2]).toBe(
3337 gate('alwaysThrottleRetries') ? 12 : 17,
3338 );
3339 // treeBaseDuration
3340 expect(onRender.mock.calls[5][3]).toBe(1 + 4 + 5 + 3);
3341 });
3342
3343 // @gate enableSuspenseList
3344 it('propagates despite a memo bailout', async () => {
3345 const A = createAsyncText('A');
3346 const B = createAsyncText('B');
3347 const C = createAsyncText('C');
3348
3349 const Bailout = React.memo(({children}) => {
3350 return children;
3351 });
3352
3353 function Foo() {
3354 // To test the part that relies on context propagation,
3355 // we need to bailout *above* the Suspense's parent.
3356 // Several layers of Bailout wrappers help verify we're
3357 // marking updates all the way to the propagation root.
3358 return (
3359 <SuspenseList revealOrder="forwards" tail="visible">
3360 <Bailout>
3361 <Bailout>
3362 <Bailout>
3363 <Bailout>
3364 <Suspense fallback={<Text text="Loading A" />}>
3365 <A />
3366 </Suspense>
3367 </Bailout>
3368 </Bailout>
3369 </Bailout>
3370 </Bailout>
3371 <Bailout>
3372 <Bailout>
3373 <Bailout>
3374 <Bailout>
3375 <Suspense fallback={<Text text="Loading B" />}>
3376 <B />
3377 </Suspense>
3378 </Bailout>
3379 </Bailout>
3380 </Bailout>
3381 </Bailout>
3382 <Bailout>
3383 <Bailout>
3384 <Bailout>
3385 <Bailout>
3386 <Suspense fallback={<Text text="Loading C" />}>
3387 <C />
3388 </Suspense>
3389 </Bailout>
3390 </Bailout>
3391 </Bailout>
3392 </Bailout>
3393 </SuspenseList>
3394 );
3395 }
3396
3397 await C.resolve();
3398
3399 ReactNoop.render(<Foo />);
3400
3401 await waitForAll([
3402 'Suspend! [A]',
3403 'Loading A',
3404 'Loading B',
3405 'Loading C',
3406 // pre-warming
3407 'Suspend! [A]',
3408 ]);
3409
3410 expect(ReactNoop).toMatchRenderedOutput(
3411 <>
3412 <span>Loading A</span>
3413 <span>Loading B</span>
3414 <span>Loading C</span>
3415 </>,
3416 );
3417
3418 await act(() => A.resolve());
3419 assertLog(['A', 'Suspend! [B]', 'Suspend! [B]']);
3420 expect(ReactNoop).toMatchRenderedOutput(
3421 <>
3422 <span>A</span>
3423 <span>Loading B</span>
3424 <span>Loading C</span>
3425 </>,
3426 );
3427
3428 await act(() => B.resolve());
3429 assertLog(['B', 'C']);
3430 expect(ReactNoop).toMatchRenderedOutput(
3431 <>
3432 <span>A</span>
3433 <span>B</span>
3434 <span>C</span>
3435 </>,
3436 );
3437 });
3438
3439 // @gate enableSuspenseList
3440 it(
3441 'regression test: SuspenseList should never force boundaries deeper than ' +
3442 'a single level into fallback mode',
3443 async () => {
3444 const A = createAsyncText('A');
3445
3446 function UnreachableFallback() {
3447 throw new Error('Should never be thrown!');
3448 }
3449
3450 function Repro({update}) {
3451 return (
3452 <SuspenseList revealOrder="forwards" tail="visible">
3453 {update && (
3454 <Suspense fallback={<Text text="Loading A..." />}>
3455 <A />
3456 </Suspense>
3457 )}
3458 <Suspense fallback={<Text text="Loading B..." />}>
3459 {update ? (
3460 <Suspense fallback={<UnreachableFallback />}>
3461 <Text text="B2" />
3462 </Suspense>
3463 ) : (
3464 <Text text="B1" />
3465 )}
3466 </Suspense>
3467 <Suspense fallback={<Text text="Loading C..." />}>
3468 <Text text="C" />
3469 </Suspense>
3470 {update && (
3471 <Suspense fallback={<Text text="Loading D..." />}>
3472 <Text text="D" />
3473 </Suspense>
3474 )}
3475 </SuspenseList>
3476 );
3477 }
3478
3479 // Initial mount. Only two rows are mounted, B and C.
3480 const root = ReactNoop.createRoot();
3481 await act(() => root.render(<Repro update={false} />));
3482 assertLog(['B1', 'C']);
3483 expect(root).toMatchRenderedOutput(
3484 <>
3485 <span>B1</span>
3486 <span>C</span>
3487 </>,
3488 );
3489
3490 // During the update, a few things happen simultaneously:
3491 // - A new row, A, is inserted into the head. This row suspends.
3492 // - The context in row B is replaced. The new content contains a nested
3493 // Suspense boundary.
3494 // - A new row, D, is inserted into the tail.
3495 await act(() => root.render(<Repro update={true} />));
3496 assertLog([
3497 // During the first pass, the new row, A, suspends. This means any new
3498 // rows in the tail should be forced into fallback mode.
3499 'Suspend! [A]',
3500 'Loading A...',
3501 'B2',
3502 'C',
3503
3504 // A second pass is used to render the fallbacks in the tail.
3505 //
3506 // Rows B and C were already mounted, so they should not be forced into
3507 // fallback mode.
3508 //
3509 // In the regression that this test was written for, the inner
3510 // Suspense boundary around B2 was incorrectly activated. Only the
3511 // nearest fallbacks per row should be activated, and only if they
3512 // haven't already mounted.
3513 'Loading A...',
3514 'B2',
3515 'C',
3516
3517 // D is part of the tail, so it should show a fallback.
3518 'Loading D...',
3519 ]);
3520 expect(root).toMatchRenderedOutput(
3521 <>
3522 <span>Loading A...</span>
3523 <span>B2</span>
3524 <span>C</span>
3525 <span>Loading D...</span>
3526 </>,
3527 );
3528
3529 // Now finish loading A.
3530 await act(() => A.resolve());
3531 assertLog(['A', 'D']);
3532 expect(root).toMatchRenderedOutput(
3533 <>
3534 <span>A</span>
3535 <span>B2</span>
3536 <span>C</span>
3537 <span>D</span>
3538 </>,
3539 );
3540 },
3541 );
3542
3543 // @gate enableSuspenseList && enableAsyncIterableChildren
3544 it('warns for async generator components in "forwards" order', async () => {
3545 async function* Generator() {
3546 yield 'A';
3547 yield 'B';
3548 }
3549 function Foo() {
3550 return (
3551 <SuspenseList revealOrder="forwards" tail="visible">
3552 <Generator />
3553 </SuspenseList>
3554 );
3555 }
3556
3557 await act(() => {
3558 React.startTransition(() => {
3559 ReactNoop.render(<Foo />);
3560 });
3561 });
3562 assertConsoleErrorDev([
3563 'A generator Component was passed to a <SuspenseList revealOrder="forwards" />. ' +
3564 'This is not supported as a way to generate lists. Instead, pass an ' +
3565 'iterable as the children.' +
3566 '\n in SuspenseList (at **)' +
3567 '\n in Foo (at **)',
3568 '<Generator> is an async Client Component. ' +
3569 'Only Server Components can be async at the moment. ' +
3570 "This error is often caused by accidentally adding `'use client'` " +
3571 'to a module that was originally written for the server.\n' +
3572 ' in Foo (at **)',
3573 // We get this warning because the generator's promise themselves are not cached.
3574 'A component was suspended by an uncached promise. ' +
3575 'Creating promises inside a Client Component or hook is not yet supported, ' +
3576 'except via a Suspense-compatible library or framework.\n' +
3577 ' in Foo (at **)',
3578 ]);
3579 });
3580
3581 // @gate enableSuspenseList && enableAsyncIterableChildren
3582 it('can display async iterable in "forwards" order', async () => {
3583 const A = createAsyncText('A');
3584 const B = createAsyncText('B');
3585
3586 // We use Cached elements to avoid rerender.
3587 const ASlot = (
3588 <Suspense key="A" fallback={<Text text="Loading A" />}>
3589 <A />
3590 </Suspense>
3591 );
3592
3593 const BSlot = (
3594 <Suspense key="B" fallback={<Text text="Loading B" />}>
3595 <B />
3596 </Suspense>
3597 );
3598
3599 const iterable = {
3600 async *[Symbol.asyncIterator]() {
3601 yield ASlot;
3602 yield BSlot;
3603 },
3604 };
3605
3606 function Foo() {
3607 return (
3608 <SuspenseList revealOrder="forwards" tail="visible">
3609 {iterable}
3610 </SuspenseList>
3611 );
3612 }
3613
3614 await act(() => {
3615 React.startTransition(() => {
3616 ReactNoop.render(<Foo />);
3617 });
3618 });
3619
3620 assertLog([
3621 'Suspend! [A]',
3622 'Loading A',
3623 'Loading B',
3624 // pre-warming
3625 'Suspend! [A]',
3626 ]);
3627
3628 assertConsoleErrorDev([
3629 // We get this warning because the generator's promise themselves are not cached.
3630 'A component was suspended by an uncached promise. ' +
3631 'Creating promises inside a Client Component or hook is not yet supported, ' +
3632 'except via a Suspense-compatible library or framework.\n' +
3633 ' in SuspenseList (at **)\n' +
3634 ' in Foo (at **)',
3635 'A component was suspended by an uncached promise. ' +
3636 'Creating promises inside a Client Component or hook is not yet supported, ' +
3637 'except via a Suspense-compatible library or framework.\n' +
3638 ' in SuspenseList (at **)\n' +
3639 ' in Foo (at **)',
3640 ]);
3641
3642 expect(ReactNoop).toMatchRenderedOutput(
3643 <>
3644 <span>Loading A</span>
3645 <span>Loading B</span>
3646 </>,
3647 );
3648
3649 await act(() => A.resolve());
3650 assertLog(['A', 'Suspend! [B]', 'Suspend! [B]']);
3651
3652 assertConsoleErrorDev([
3653 // We get this warning because the generator's promise themselves are not cached.
3654 'A component was suspended by an uncached promise. ' +
3655 'Creating promises inside a Client Component or hook is not yet supported, ' +
3656 'except via a Suspense-compatible library or framework.\n' +
3657 ' in SuspenseList (at **)\n' +
3658 ' in Foo (at **)',
3659 'A component was suspended by an uncached promise. ' +
3660 'Creating promises inside a Client Component or hook is not yet supported, ' +
3661 'except via a Suspense-compatible library or framework.\n' +
3662 ' in SuspenseList (at **)\n' +
3663 ' in Foo (at **)',
3664 ]);
3665
3666 expect(ReactNoop).toMatchRenderedOutput(
3667 <>
3668 <span>A</span>
3669 <span>Loading B</span>
3670 </>,
3671 );
3672
3673 await act(() => B.resolve());
3674 assertLog(['B']);
3675
3676 assertConsoleErrorDev([
3677 // We get this warning because the generator's promise themselves are not cached.
3678 'A component was suspended by an uncached promise. ' +
3679 'Creating promises inside a Client Component or hook is not yet supported, ' +
3680 'except via a Suspense-compatible library or framework.\n' +
3681 ' in SuspenseList (at **)\n' +
3682 ' in Foo (at **)',
3683 ]);
3684
3685 expect(ReactNoop).toMatchRenderedOutput(
3686 <>
3687 <span>A</span>
3688 <span>B</span>
3689 </>,
3690 );
3691 });
3692
3693 // @gate enableSuspenseList && enableAsyncIterableChildren
3694 it('warns if a nested async iterable is passed to a "forwards" list', async () => {
3695 function Foo({items}) {
3696 return (
3697 <SuspenseList revealOrder="forwards" tail="visible">
3698 {items}
3699 <div>Tail</div>
3700 </SuspenseList>
3701 );
3702 }
3703
3704 const iterable = {
3705 async *[Symbol.asyncIterator]() {
3706 yield (
3707 <Suspense key={'A'} fallback="Loading">
3708 A
3709 </Suspense>
3710 );
3711 yield (
3712 <Suspense key={'B'} fallback="Loading">
3713 B
3714 </Suspense>
3715 );
3716 },
3717 };
3718
3719 await act(() => {
3720 React.startTransition(() => {
3721 ReactNoop.render(<Foo items={iterable} />);
3722 });
3723 });
3724 assertConsoleErrorDev([
3725 'A nested async iterable was passed to row #0 in <SuspenseList />. ' +
3726 'Wrap it in an additional SuspenseList to configure its revealOrder: ' +
3727 '<SuspenseList revealOrder=...> ... ' +
3728 '<SuspenseList revealOrder=...>{async iterable}</SuspenseList> ... ' +
3729 '</SuspenseList>' +
3730 '\n in SuspenseList (at **)' +
3731 '\n in Foo (at **)',
3732 // We get this warning because the generator's promise themselves are not cached.
3733 'A component was suspended by an uncached promise. ' +
3734 'Creating promises inside a Client Component or hook is not yet supported, ' +
3735 'except via a Suspense-compatible library or framework.\n' +
3736 ' in Foo (at **)',
3737 ]);
3738 });
3739 });