main
js 1,646 lines 42.9 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 * @flow
8 */
9
10 import {getLegacyRenderImplementation} from './utils';
11
12 describe('StoreStress (Legacy Mode)', () => {
13 let React;
14 let act;
15 let bridge;
16 let store;
17 let print;
18
19 function readValue(promise) {
20 if (typeof React.use === 'function') {
21 return React.use(promise);
22 }
23
24 // Support for React < 19.0
25 switch (promise.status) {
26 case 'fulfilled':
27 return promise.value;
28 case 'rejected':
29 throw promise.reason;
30 case 'pending':
31 throw promise;
32 default:
33 promise.status = 'pending';
34 promise.then(
35 value => {
36 promise.status = 'fulfilled';
37 promise.value = value;
38 },
39 reason => {
40 promise.status = 'rejected';
41 promise.reason = reason;
42 },
43 );
44 throw promise;
45 }
46 }
47
48 beforeEach(() => {
49 bridge = global.bridge;
50 store = global.store;
51 store.collapseNodesByDefault = false;
52
53 React = require('react');
54
55 const utils = require('./utils');
56 act = utils.act;
57
58 print = require('./__serializers__/storeSerializer').print;
59 });
60
61 const {render, unmount, createContainer, getContainer} =
62 getLegacyRenderImplementation();
63
64 // This is a stress test for the tree mount/update/unmount traversal.
65 // It renders different trees that should produce the same output.
66 // @reactVersion >= 16.9
67 // @reactVersion < 19
68 // @gate !disableLegacyMode
69 it('should handle a stress test with different tree operations (Legacy Mode)', () => {
70 let setShowX;
71 const A = () => 'a';
72 const B = () => 'b';
73 const C = () => {
74 // We'll be manually flipping this component back and forth in the test.
75 // We only do this for a single node in order to verify that DevTools
76 // can handle a subtree switching alternates while other subtrees are memoized.
77 const [showX, _setShowX] = React.useState(false);
78 setShowX = _setShowX;
79 return showX ? <X /> : 'c';
80 };
81 const D = () => 'd';
82 const E = () => 'e';
83 const X = () => 'x';
84 const a = <A key="a" />;
85 const b = <B key="b" />;
86 const c = <C key="c" />;
87 const d = <D key="d" />;
88 const e = <E key="e" />;
89
90 function Parent({children}) {
91 return children;
92 }
93
94 // 1. Render a normal version of [a, b, c, d, e].
95 act(() => render(<Parent>{[a, b, c, d, e]}</Parent>));
96 expect(store).toMatchInlineSnapshot(`
97 [root]
98 ▾ <Parent>
99 <A key="a">
100 <B key="b">
101 <C key="c">
102 <D key="d">
103 <E key="e">
104 `);
105 expect(getContainer().textContent).toMatch('abcde');
106 const snapshotForABCDE = print(store);
107
108 // 2. Render a version where <C /> renders an <X /> child instead of 'c'.
109 // This is how we'll test an update to a single component.
110 act(() => {
111 setShowX(true);
112 });
113 expect(store).toMatchInlineSnapshot(`
114 [root]
115 ▾ <Parent>
116 <A key="a">
117 <B key="b">
118 ▾ <C key="c">
119 <X>
120 <D key="d">
121 <E key="e">
122 `);
123 expect(getContainer().textContent).toMatch('abxde');
124 const snapshotForABXDE = print(store);
125
126 // 3. Verify flipping it back produces the original result.
127 act(() => {
128 setShowX(false);
129 });
130 expect(getContainer().textContent).toMatch('abcde');
131 expect(print(store)).toBe(snapshotForABCDE);
132
133 // 4. Clean up.
134 act(() => unmount());
135 expect(print(store)).toBe('');
136
137 // Now comes the interesting part.
138 // All of these cases are equivalent to [a, b, c, d, e] in output.
139 // We'll verify that DevTools produces the same snapshots for them.
140 // These cases are picked so that rendering them sequentially in the same
141 // container results in a combination of mounts, updates, unmounts, and reorders.
142 // prettier-ignore
143 const cases = [
144 [a, b, c, d, e],
145 [[a], b, c, d, e],
146 [[a, b], c, d, e],
147 [[a, b], c, [d, e]],
148 [[a, b], c, [d, '', e]],
149 [[a], b, c, d, [e]],
150 [a, b, [[c]], d, e],
151 [[a, ''], [b], [c], [d], [e]],
152 [a, b, [c, [d, ['', e]]]],
153 [a, b, c, d, e],
154 [<div key="0">{a}</div>, b, c, d, e],
155 [<div key="0">{a}{b}</div>, c, d, e],
156 [<div key="0">{a}{b}</div>, c, <div key="1">{d}{e}</div>],
157 [<div key="1">{a}{b}</div>, c, <div key="0">{d}{e}</div>],
158 [<div key="0">{a}{b}</div>, c, <div key="1">{d}{e}</div>],
159 [<div key="2">{a}{b}</div>, c, <div key="3">{d}{e}</div>],
160 [<span key="0">{a}</span>, b, c, d, [e]],
161 [a, b, <span key="0"><span>{c}</span></span>, d, e],
162 [<div key="0">{a}</div>, [b], <span key="1">{c}</span>, [d], <div key="2">{e}</div>],
163 [a, b, [c, <div key="0">{d}<span>{e}</span></div>], ''],
164 [a, [[]], b, c, [d, [[]], e]],
165 [[[a, b, c, d], e]],
166 [a, b, c, d, e],
167 ];
168
169 // 5. Test fresh mount for each case.
170 for (let i = 0; i < cases.length; i++) {
171 // Ensure fresh mount.
172 createContainer();
173
174 // Verify mounting 'abcde'.
175 act(() => render(<Parent>{cases[i]}</Parent>));
176 expect(getContainer().textContent).toMatch('abcde');
177 expect(print(store)).toEqual(snapshotForABCDE);
178
179 // Verify switching to 'abxde'.
180 act(() => {
181 setShowX(true);
182 });
183 expect(getContainer().textContent).toMatch('abxde');
184 expect(print(store)).toBe(snapshotForABXDE);
185
186 // Verify switching back to 'abcde'.
187 act(() => {
188 setShowX(false);
189 });
190 expect(getContainer().textContent).toMatch('abcde');
191 expect(print(store)).toBe(snapshotForABCDE);
192
193 // Clean up.
194 act(() => unmount());
195 expect(print(store)).toBe('');
196 }
197
198 // 6. Verify *updates* by reusing the container between iterations.
199 // There'll be no unmounting until the very end.
200 createContainer();
201 for (let i = 0; i < cases.length; i++) {
202 // Verify mounting 'abcde'.
203 act(() => render(<Parent>{cases[i]}</Parent>));
204 expect(getContainer().textContent).toMatch('abcde');
205 expect(print(store)).toEqual(snapshotForABCDE);
206
207 // Verify switching to 'abxde'.
208 act(() => {
209 setShowX(true);
210 });
211 expect(getContainer().textContent).toMatch('abxde');
212 expect(print(store)).toBe(snapshotForABXDE);
213
214 // Verify switching back to 'abcde'.
215 act(() => {
216 setShowX(false);
217 });
218 expect(getContainer().textContent).toMatch('abcde');
219 expect(print(store)).toBe(snapshotForABCDE);
220 // Don't unmount. Reuse the container between iterations.
221 }
222 act(() => unmount());
223 expect(print(store)).toBe('');
224 });
225
226 // @reactVersion >= 16.9
227 // @reactVersion <= 18.2
228 it('should handle stress test with reordering (Legacy Mode)', () => {
229 const A = () => 'a';
230 const B = () => 'b';
231 const C = () => 'c';
232 const D = () => 'd';
233 const E = () => 'e';
234 const a = <A key="a" />;
235 const b = <B key="b" />;
236 const c = <C key="c" />;
237 const d = <D key="d" />;
238 const e = <E key="e" />;
239
240 // prettier-ignore
241 const steps = [
242 a,
243 b,
244 c,
245 d,
246 e,
247 [a],
248 [b],
249 [c],
250 [d],
251 [e],
252 [a, b],
253 [b, a],
254 [b, c],
255 [c, b],
256 [a, c],
257 [c, a],
258 ];
259
260 const stepsSnapshot = [
261 `
262 [root]
263 ▾ <Root>
264 <A key="a">
265 `,
266 `
267 [root]
268 ▾ <Root>
269 <B key="b">
270 `,
271 `
272 [root]
273 ▾ <Root>
274 <C key="c">
275 `,
276 `
277 [root]
278 ▾ <Root>
279 <D key="d">
280 `,
281 `
282 [root]
283 ▾ <Root>
284 <E key="e">
285 `,
286 `
287 [root]
288 ▾ <Root>
289 <A key="a">
290 `,
291 `
292 [root]
293 ▾ <Root>
294 <B key="b">
295 `,
296 `
297 [root]
298 ▾ <Root>
299 <C key="c">
300 `,
301 `
302 [root]
303 ▾ <Root>
304 <D key="d">
305 `,
306 `
307 [root]
308 ▾ <Root>
309 <E key="e">
310 `,
311 `
312 [root]
313 ▾ <Root>
314 <A key="a">
315 <B key="b">
316 `,
317 `
318 [root]
319 ▾ <Root>
320 <B key="b">
321 <A key="a">
322 `,
323 `
324 [root]
325 ▾ <Root>
326 <B key="b">
327 <C key="c">
328 `,
329 `
330 [root]
331 ▾ <Root>
332 <C key="c">
333 <B key="b">
334 `,
335 `
336 [root]
337 ▾ <Root>
338 <A key="a">
339 <C key="c">
340 `,
341 `
342 [root]
343 ▾ <Root>
344 <C key="c">
345 <A key="a">
346 `,
347 ];
348
349 const Root = ({children}) => {
350 return children;
351 };
352
353 // 1. Capture the expected render result.
354 const snapshots = [];
355 for (let i = 0; i < steps.length; i++) {
356 createContainer();
357
358 act(() => render(<Root>{steps[i]}</Root>));
359 // We snapshot each step once so it doesn't regress.
360 expect(store).toMatchInlineSnapshot(stepsSnapshot[i]);
361 snapshots.push(print(store));
362 act(() => unmount());
363 expect(print(store)).toBe('');
364 }
365
366 // 2. Verify that we can update from every step to every other step and back.
367 for (let i = 0; i < steps.length; i++) {
368 for (let j = 0; j < steps.length; j++) {
369 createContainer();
370
371 act(() => render(<Root>{steps[i]}</Root>));
372 expect(print(store)).toMatch(snapshots[i]);
373 act(() => render(<Root>{steps[j]}</Root>));
374 expect(print(store)).toMatch(snapshots[j]);
375 act(() => render(<Root>{steps[i]}</Root>));
376 expect(print(store)).toMatch(snapshots[i]);
377 act(() => unmount());
378 expect(print(store)).toBe('');
379 }
380 }
381
382 // 3. Same test as above, but this time we wrap children in a host component.
383 for (let i = 0; i < steps.length; i++) {
384 for (let j = 0; j < steps.length; j++) {
385 createContainer();
386
387 act(() =>
388 render(
389 <Root>
390 <div>{steps[i]}</div>
391 </Root>,
392 ),
393 );
394 expect(print(store)).toMatch(snapshots[i]);
395 act(() =>
396 render(
397 <Root>
398 <div>{steps[j]}</div>
399 </Root>,
400 ),
401 );
402 expect(print(store)).toMatch(snapshots[j]);
403 act(() =>
404 render(
405 <Root>
406 <div>{steps[i]}</div>
407 </Root>,
408 ),
409 );
410 expect(print(store)).toMatch(snapshots[i]);
411 act(() => unmount());
412 expect(print(store)).toBe('');
413 }
414 }
415 });
416
417 // @reactVersion >= 18.0
418 // @reactVersion <= 18.2
419 it('should handle a stress test for Suspense (Legacy Mode)', async () => {
420 const A = () => 'a';
421 const B = () => 'b';
422 const C = () => 'c';
423 const X = () => 'x';
424 const Y = () => 'y';
425 const Z = () => 'z';
426 const a = <A key="a" />;
427 const b = <B key="b" />;
428 const c = <C key="c" />;
429 const z = <Z key="z" />;
430
431 // prettier-ignore
432 const steps = [
433 a,
434 [a],
435 [a, b, c],
436 [c, b, a],
437 [c, null, a],
438 <React.Fragment>{c}{a}</React.Fragment>,
439 <div>{c}{a}</div>,
440 <div><span>{a}</span>{b}</div>,
441 [[a]],
442 null,
443 b,
444 a,
445 ];
446
447 // Excluding Suspense tree here due to different measurement semantics for fallbacks
448 const stepsSnapshot = [
449 `
450 "[root]
451 ▾ <Root>
452 <X>
453 ▾ <Suspense>
454 <A key="a">
455 <Y>"
456 `,
457 `
458 "[root]
459 ▾ <Root>
460 <X>
461 ▾ <Suspense>
462 <A key="a">
463 <Y>"
464 `,
465 `
466 "[root]
467 ▾ <Root>
468 <X>
469 ▾ <Suspense>
470 <A key="a">
471 <B key="b">
472 <C key="c">
473 <Y>"
474 `,
475 `
476 "[root]
477 ▾ <Root>
478 <X>
479 ▾ <Suspense>
480 <C key="c">
481 <B key="b">
482 <A key="a">
483 <Y>"
484 `,
485 `
486 "[root]
487 ▾ <Root>
488 <X>
489 ▾ <Suspense>
490 <C key="c">
491 <A key="a">
492 <Y>"
493 `,
494 `
495 "[root]
496 ▾ <Root>
497 <X>
498 ▾ <Suspense>
499 <C key="c">
500 <A key="a">
501 <Y>"
502 `,
503 `
504 "[root]
505 ▾ <Root>
506 <X>
507 ▾ <Suspense>
508 <C key="c">
509 <A key="a">
510 <Y>"
511 `,
512 `
513 "[root]
514 ▾ <Root>
515 <X>
516 ▾ <Suspense>
517 <A key="a">
518 <B key="b">
519 <Y>"
520 `,
521 `
522 "[root]
523 ▾ <Root>
524 <X>
525 ▾ <Suspense>
526 <A key="a">
527 <Y>"
528 `,
529 `
530 "[root]
531 ▾ <Root>
532 <X>
533 <Suspense>
534 <Y>"
535 `,
536 `
537 "[root]
538 ▾ <Root>
539 <X>
540 ▾ <Suspense>
541 <B key="b">
542 <Y>"
543 `,
544 `
545 "[root]
546 ▾ <Root>
547 <X>
548 ▾ <Suspense>
549 <A key="a">
550 <Y>"
551 `,
552 ];
553
554 const never = new Promise(() => {});
555 const Never = () => {
556 readValue(never);
557 };
558
559 const Root = ({children}) => {
560 return children;
561 };
562
563 // 1. For each step, check Suspense can render them as initial primary content.
564 // This is the only step where we use Jest snapshots.
565 const snapshots = [];
566 for (let i = 0; i < steps.length; i++) {
567 createContainer();
568
569 act(() =>
570 render(
571 <Root>
572 <X />
573 <React.Suspense fallback={z}>{steps[i]}</React.Suspense>
574 <Y />
575 </Root>,
576 ),
577 );
578 // We snapshot each step once so it doesn't regress.
579 expect(print(store, undefined, undefined, false)).toMatchInlineSnapshot(
580 stepsSnapshot[i],
581 );
582 snapshots.push(print(store, undefined, undefined, false));
583 act(() => unmount());
584 expect(print(store)).toBe('');
585 }
586
587 // 2. Verify check Suspense can render same steps as initial fallback content.
588 for (let i = 0; i < steps.length; i++) {
589 createContainer();
590
591 act(() =>
592 render(
593 <Root>
594 <X />
595 <React.Suspense fallback={steps[i]}>
596 <Z />
597 <Never />
598 <Z />
599 </React.Suspense>
600 <Y />
601 </Root>,
602 ),
603 );
604 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
605 act(() => unmount());
606 expect(print(store)).toBe('');
607 }
608
609 // 3. Verify we can update from each step to each step in primary mode.
610 for (let i = 0; i < steps.length; i++) {
611 for (let j = 0; j < steps.length; j++) {
612 // Always start with a fresh container and steps[i].
613 createContainer();
614
615 act(() =>
616 render(
617 <Root>
618 <X />
619 <React.Suspense fallback={z}>{steps[i]}</React.Suspense>
620 <Y />
621 </Root>,
622 ),
623 );
624 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
625 // Re-render with steps[j].
626 act(() =>
627 render(
628 <Root>
629 <X />
630 <React.Suspense fallback={z}>{steps[j]}</React.Suspense>
631 <Y />
632 </Root>,
633 ),
634 );
635 // Verify the successful transition to steps[j].
636 expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
637 // Check that we can transition back again.
638 act(() =>
639 render(
640 <Root>
641 <X />
642 <React.Suspense fallback={z}>{steps[i]}</React.Suspense>
643 <Y />
644 </Root>,
645 ),
646 );
647 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
648 // Clean up after every iteration.
649 act(() => unmount());
650 expect(print(store)).toBe('');
651 }
652 }
653
654 // 4. Verify we can update from each step to each step in fallback mode.
655 for (let i = 0; i < steps.length; i++) {
656 for (let j = 0; j < steps.length; j++) {
657 // Always start with a fresh container and steps[i].
658 createContainer();
659
660 act(() =>
661 render(
662 <Root>
663 <X />
664 <React.Suspense fallback={steps[i]}>
665 <Z />
666 <Never />
667 <Z />
668 </React.Suspense>
669 <Y />
670 </Root>,
671 ),
672 );
673 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
674 // Re-render with steps[j].
675 act(() =>
676 render(
677 <Root>
678 <X />
679 <React.Suspense fallback={steps[j]}>
680 <Z />
681 <Never />
682 <Z />
683 </React.Suspense>
684 <Y />
685 </Root>,
686 ),
687 );
688 // Verify the successful transition to steps[j].
689 expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
690 // Check that we can transition back again.
691 act(() =>
692 render(
693 <Root>
694 <X />
695 <React.Suspense fallback={steps[i]}>
696 <Z />
697 <Never />
698 <Z />
699 </React.Suspense>
700 <Y />
701 </Root>,
702 ),
703 );
704 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
705 // Clean up after every iteration.
706 act(() => unmount());
707 expect(print(store)).toBe('');
708 }
709 }
710
711 // 5. Verify we can update from each step to each step when moving primary -> fallback.
712 for (let i = 0; i < steps.length; i++) {
713 for (let j = 0; j < steps.length; j++) {
714 // Always start with a fresh container and steps[i].
715 createContainer();
716
717 act(() =>
718 render(
719 <Root>
720 <X />
721 <React.Suspense fallback={z}>{steps[i]}</React.Suspense>
722 <Y />
723 </Root>,
724 ),
725 );
726 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
727 // Re-render with steps[j].
728 act(() =>
729 render(
730 <Root>
731 <X />
732 <React.Suspense fallback={steps[j]}>
733 <Z />
734 <Never />
735 <Z />
736 </React.Suspense>
737 <Y />
738 </Root>,
739 ),
740 );
741 // Verify the successful transition to steps[j].
742 expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
743 // Check that we can transition back again.
744 act(() =>
745 render(
746 <Root>
747 <X />
748 <React.Suspense fallback={z}>{steps[i]}</React.Suspense>
749 <Y />
750 </Root>,
751 ),
752 );
753 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
754 // Clean up after every iteration.
755 act(() => unmount());
756 expect(print(store)).toBe('');
757 }
758 }
759
760 // 6. Verify we can update from each step to each step when moving fallback -> primary.
761 for (let i = 0; i < steps.length; i++) {
762 for (let j = 0; j < steps.length; j++) {
763 // Always start with a fresh container and steps[i].
764 createContainer();
765
766 act(() =>
767 render(
768 <Root>
769 <X />
770 <React.Suspense fallback={steps[i]}>
771 <Z />
772 <Never />
773 <Z />
774 </React.Suspense>
775 <Y />
776 </Root>,
777 ),
778 );
779 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
780 // Re-render with steps[j].
781 act(() =>
782 render(
783 <Root>
784 <X />
785 <React.Suspense fallback={z}>{steps[j]}</React.Suspense>
786 <Y />
787 </Root>,
788 ),
789 );
790 // Verify the successful transition to steps[j].
791 expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
792 // Check that we can transition back again.
793 act(() =>
794 render(
795 <Root>
796 <X />
797 <React.Suspense fallback={steps[i]}>
798 <Z />
799 <Never />
800 <Z />
801 </React.Suspense>
802 <Y />
803 </Root>,
804 ),
805 );
806 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
807 // Clean up after every iteration.
808 act(() => unmount());
809 expect(print(store)).toBe('');
810 }
811 }
812
813 // 7. Verify we can update from each step to each step when toggling Suspense.
814 for (let i = 0; i < steps.length; i++) {
815 for (let j = 0; j < steps.length; j++) {
816 // Always start with a fresh container and steps[i].
817 createContainer();
818
819 act(() =>
820 render(
821 <Root>
822 <X />
823 <React.Suspense fallback={steps[j]}>{steps[i]}</React.Suspense>
824 <Y />
825 </Root>,
826 ),
827 );
828
829 // We get ID from the index in the tree above:
830 // Root, X, Suspense, ...
831 // ^ (index is 2)
832 const suspenseID = store.getElementIDAtIndex(2);
833
834 // Force fallback.
835 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
836 act(() => {
837 bridge.send('overrideSuspense', {
838 id: suspenseID,
839 rendererID: store.getRendererIDForElement(suspenseID),
840 forceFallback: true,
841 });
842 });
843 expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
844
845 // Stop forcing fallback.
846 act(() => {
847 bridge.send('overrideSuspense', {
848 id: suspenseID,
849 rendererID: store.getRendererIDForElement(suspenseID),
850 forceFallback: false,
851 });
852 });
853 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
854
855 // Trigger actual fallback.
856 act(() =>
857 render(
858 <Root>
859 <X />
860 <React.Suspense fallback={steps[j]}>
861 <Z />
862 <Never />
863 <Z />
864 </React.Suspense>
865 <Y />
866 </Root>,
867 ),
868 );
869 expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
870
871 // Force fallback while we're in fallback mode.
872 act(() => {
873 bridge.send('overrideSuspense', {
874 id: suspenseID,
875 rendererID: store.getRendererIDForElement(suspenseID),
876 forceFallback: true,
877 });
878 });
879 // Keep seeing fallback content.
880 expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
881
882 // Switch to primary mode.
883 act(() =>
884 render(
885 <Root>
886 <X />
887 <React.Suspense fallback={steps[j]}>{steps[i]}</React.Suspense>
888 <Y />
889 </Root>,
890 ),
891 );
892 // Fallback is still forced though.
893 expect(print(store, undefined, undefined, false)).toEqual(snapshots[j]);
894
895 // Stop forcing fallback. This reverts to primary content.
896 act(() => {
897 bridge.send('overrideSuspense', {
898 id: suspenseID,
899 rendererID: store.getRendererIDForElement(suspenseID),
900 forceFallback: false,
901 });
902 });
903 // Now we see primary content.
904 expect(print(store, undefined, undefined, false)).toEqual(snapshots[i]);
905
906 // Clean up after every iteration.
907 act(() => unmount());
908 expect(print(store)).toBe('');
909 }
910 }
911 });
912
913 // @reactVersion >= 18.0
914 // @reactVersion <= 18.2
915 it('should handle a stress test for Suspense without type change (Legacy Mode)', () => {
916 const A = () => 'a';
917 const B = () => 'b';
918 const C = () => 'c';
919 const X = () => 'x';
920 const Y = () => 'y';
921 const Z = () => 'z';
922 const a = <A key="a" />;
923 const b = <B key="b" />;
924 const c = <C key="c" />;
925 const z = <Z key="z" />;
926
927 // prettier-ignore
928 const steps = [
929 a,
930 [a],
931 [a, b, c],
932 [c, b, a],
933 [c, null, a],
934 <React.Fragment>{c}{a}</React.Fragment>,
935 <div>{c}{a}</div>,
936 <div><span>{a}</span>{b}</div>,
937 [[a]],
938 null,
939 b,
940 a,
941 ];
942
943 const stepsSnapshot = [
944 `
945 [root]
946 ▾ <Root>
947 <X>
948 ▾ <Suspense>
949 ▾ <MaybeSuspend>
950 <A key="a">
951 <Z>
952 <Y>
953 [suspense-root] rects={[]}
954 <Suspense name="Unknown" rects={[]}>
955 `,
956 `
957 [root]
958 ▾ <Root>
959 <X>
960 ▾ <Suspense>
961 ▾ <MaybeSuspend>
962 <A key="a">
963 <Z>
964 <Y>
965 [suspense-root] rects={[]}
966 <Suspense name="Unknown" rects={[]}>
967 `,
968 `
969 [root]
970 ▾ <Root>
971 <X>
972 ▾ <Suspense>
973 ▾ <MaybeSuspend>
974 <A key="a">
975 <B key="b">
976 <C key="c">
977 <Z>
978 <Y>
979 [suspense-root] rects={[]}
980 <Suspense name="Unknown" rects={[]}>
981 `,
982 `
983 [root]
984 ▾ <Root>
985 <X>
986 ▾ <Suspense>
987 ▾ <MaybeSuspend>
988 <C key="c">
989 <B key="b">
990 <A key="a">
991 <Z>
992 <Y>
993 [suspense-root] rects={[]}
994 <Suspense name="Unknown" rects={[]}>
995 `,
996 `
997 [root]
998 ▾ <Root>
999 <X>
1000 ▾ <Suspense>
1001 ▾ <MaybeSuspend>
1002 <C key="c">
1003 <A key="a">
1004 <Z>
1005 <Y>
1006 [suspense-root] rects={[]}
1007 <Suspense name="Unknown" rects={[]}>
1008 `,
1009 `
1010 [root]
1011 ▾ <Root>
1012 <X>
1013 ▾ <Suspense>
1014 ▾ <MaybeSuspend>
1015 <C key="c">
1016 <A key="a">
1017 <Z>
1018 <Y>
1019 [suspense-root] rects={[]}
1020 <Suspense name="Unknown" rects={[]}>
1021 `,
1022 `
1023 [root]
1024 ▾ <Root>
1025 <X>
1026 ▾ <Suspense>
1027 ▾ <MaybeSuspend>
1028 <C key="c">
1029 <A key="a">
1030 <Z>
1031 <Y>
1032 [suspense-root] rects={[]}
1033 <Suspense name="Unknown" rects={[]}>
1034 `,
1035 `
1036 [root]
1037 ▾ <Root>
1038 <X>
1039 ▾ <Suspense>
1040 ▾ <MaybeSuspend>
1041 <A key="a">
1042 <B key="b">
1043 <Z>
1044 <Y>
1045 [suspense-root] rects={[]}
1046 <Suspense name="Unknown" rects={[]}>
1047 `,
1048 `
1049 [root]
1050 ▾ <Root>
1051 <X>
1052 ▾ <Suspense>
1053 ▾ <MaybeSuspend>
1054 <A key="a">
1055 <Z>
1056 <Y>
1057 [suspense-root] rects={[]}
1058 <Suspense name="Unknown" rects={[]}>
1059 `,
1060 `
1061 [root]
1062 ▾ <Root>
1063 <X>
1064 ▾ <Suspense>
1065 ▾ <MaybeSuspend>
1066 <Z>
1067 <Y>
1068 [suspense-root] rects={[]}
1069 <Suspense name="Unknown" rects={[]}>
1070 `,
1071 `
1072 [root]
1073 ▾ <Root>
1074 <X>
1075 ▾ <Suspense>
1076 ▾ <MaybeSuspend>
1077 <B key="b">
1078 <Z>
1079 <Y>
1080 [suspense-root] rects={[]}
1081 <Suspense name="Unknown" rects={[]}>
1082 `,
1083 `
1084 [root]
1085 ▾ <Root>
1086 <X>
1087 ▾ <Suspense>
1088 ▾ <MaybeSuspend>
1089 <A key="a">
1090 <Z>
1091 <Y>
1092 [suspense-root] rects={[]}
1093 <Suspense name="Unknown" rects={[]}>
1094 `,
1095 ];
1096
1097 const stepsSnapshotTwo = [
1098 `
1099 [root]
1100 ▾ <Root>
1101 <X>
1102 ▾ <Suspense>
1103 <A key="a">
1104 <Y>
1105 [suspense-root] rects={[]}
1106 <Suspense name="Unknown" rects={null}>
1107 `,
1108 `
1109 [root]
1110 ▾ <Root>
1111 <X>
1112 ▾ <Suspense>
1113 <A key="a">
1114 <Y>
1115 [suspense-root] rects={[]}
1116 <Suspense name="Unknown" rects={null}>
1117 `,
1118 `
1119 [root]
1120 ▾ <Root>
1121 <X>
1122 ▾ <Suspense>
1123 <A key="a">
1124 <B key="b">
1125 <C key="c">
1126 <Y>
1127 [suspense-root] rects={[]}
1128 <Suspense name="Unknown" rects={null}>
1129 `,
1130 `
1131 [root]
1132 ▾ <Root>
1133 <X>
1134 ▾ <Suspense>
1135 <C key="c">
1136 <B key="b">
1137 <A key="a">
1138 <Y>
1139 [suspense-root] rects={[]}
1140 <Suspense name="Unknown" rects={null}>
1141 `,
1142 `
1143 [root]
1144 ▾ <Root>
1145 <X>
1146 ▾ <Suspense>
1147 <C key="c">
1148 <A key="a">
1149 <Y>
1150 [suspense-root] rects={[]}
1151 <Suspense name="Unknown" rects={null}>
1152 `,
1153 `
1154 [root]
1155 ▾ <Root>
1156 <X>
1157 ▾ <Suspense>
1158 <C key="c">
1159 <A key="a">
1160 <Y>
1161 [suspense-root] rects={[]}
1162 <Suspense name="Unknown" rects={null}>
1163 `,
1164 `
1165 [root]
1166 ▾ <Root>
1167 <X>
1168 ▾ <Suspense>
1169 <C key="c">
1170 <A key="a">
1171 <Y>
1172 [suspense-root] rects={[]}
1173 <Suspense name="Unknown" rects={null}>
1174 `,
1175 `
1176 [root]
1177 ▾ <Root>
1178 <X>
1179 ▾ <Suspense>
1180 <A key="a">
1181 <B key="b">
1182 <Y>
1183 [suspense-root] rects={[]}
1184 <Suspense name="Unknown" rects={null}>
1185 `,
1186 `
1187 [root]
1188 ▾ <Root>
1189 <X>
1190 ▾ <Suspense>
1191 <A key="a">
1192 <Y>
1193 [suspense-root] rects={[]}
1194 <Suspense name="Unknown" rects={null}>
1195 `,
1196 `
1197 [root]
1198 ▾ <Root>
1199 <X>
1200 <Suspense>
1201 <Y>
1202 [suspense-root] rects={[]}
1203 <Suspense name="Unknown" rects={null}>
1204 `,
1205 `
1206 [root]
1207 ▾ <Root>
1208 <X>
1209 ▾ <Suspense>
1210 <B key="b">
1211 <Y>
1212 [suspense-root] rects={[]}
1213 <Suspense name="Unknown" rects={null}>
1214 `,
1215 `
1216 [root]
1217 ▾ <Root>
1218 <X>
1219 ▾ <Suspense>
1220 <A key="a">
1221 <Y>
1222 [suspense-root] rects={[]}
1223 <Suspense name="Unknown" rects={null}>
1224 `,
1225 ];
1226
1227 const never = new Promise(() => {});
1228 const Never = () => {
1229 readValue(never);
1230 };
1231
1232 const MaybeSuspend = ({children, suspend}) => {
1233 if (suspend) {
1234 return (
1235 <div>
1236 {children}
1237 <Never />
1238 <X />
1239 </div>
1240 );
1241 }
1242 return (
1243 <div>
1244 {children}
1245 <Z />
1246 </div>
1247 );
1248 };
1249
1250 const Root = ({children}) => {
1251 return children;
1252 };
1253
1254 // 1. For each step, check Suspense can render them as initial primary content.
1255 // This is the only step where we use Jest snapshots.
1256 const snapshots = [];
1257
1258 for (let i = 0; i < steps.length; i++) {
1259 createContainer();
1260
1261 act(() =>
1262 render(
1263 <Root>
1264 <X />
1265 <React.Suspense fallback={z}>
1266 <MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
1267 </React.Suspense>
1268 <Y />
1269 </Root>,
1270 ),
1271 );
1272 // We snapshot each step once so it doesn't regress.
1273 expect(store).toMatchInlineSnapshot(stepsSnapshot[i]);
1274 snapshots.push(print(store));
1275 act(() => unmount());
1276 expect(print(store)).toBe('');
1277 }
1278
1279 // 2. Verify check Suspense can render same steps as initial fallback content.
1280 // We don't actually assert here because the tree includes <MaybeSuspend>
1281 // which is different from the snapshots above. So we take more snapshots.
1282 const fallbackSnapshots = [];
1283 for (let i = 0; i < steps.length; i++) {
1284 createContainer();
1285
1286 act(() =>
1287 render(
1288 <Root>
1289 <X />
1290 <React.Suspense fallback={steps[i]}>
1291 <Z />
1292 <MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
1293 <Z />
1294 </React.Suspense>
1295 <Y />
1296 </Root>,
1297 ),
1298 );
1299 // We snapshot each step once so it doesn't regress.
1300 expect(store).toMatchInlineSnapshot(stepsSnapshotTwo[i]);
1301 fallbackSnapshots.push(print(store, undefined, undefined, false));
1302 act(() => unmount());
1303 expect(print(store)).toBe('');
1304 }
1305
1306 // 3. Verify we can update from each step to each step in primary mode.
1307 for (let i = 0; i < steps.length; i++) {
1308 for (let j = 0; j < steps.length; j++) {
1309 // Always start with a fresh container and steps[i].
1310 createContainer();
1311
1312 act(() =>
1313 render(
1314 <Root>
1315 <X />
1316 <React.Suspense fallback={z}>
1317 <MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
1318 </React.Suspense>
1319 <Y />
1320 </Root>,
1321 ),
1322 );
1323 expect(print(store)).toEqual(snapshots[i]);
1324 // Re-render with steps[j].
1325 act(() =>
1326 render(
1327 <Root>
1328 <X />
1329 <React.Suspense fallback={z}>
1330 <MaybeSuspend suspend={false}>{steps[j]}</MaybeSuspend>
1331 </React.Suspense>
1332 <Y />
1333 </Root>,
1334 ),
1335 );
1336 // Verify the successful transition to steps[j].
1337 expect(print(store)).toEqual(snapshots[j]);
1338 // Check that we can transition back again.
1339 act(() =>
1340 render(
1341 <Root>
1342 <X />
1343 <React.Suspense fallback={z}>
1344 <MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
1345 </React.Suspense>
1346 <Y />
1347 </Root>,
1348 ),
1349 );
1350 expect(print(store)).toEqual(snapshots[i]);
1351 // Clean up after every iteration.
1352 act(() => unmount());
1353 expect(print(store)).toBe('');
1354 }
1355 }
1356
1357 // 4. Verify we can update from each step to each step in fallback mode.
1358 for (let i = 0; i < steps.length; i++) {
1359 for (let j = 0; j < steps.length; j++) {
1360 // Always start with a fresh container and steps[i].
1361 createContainer();
1362
1363 act(() =>
1364 render(
1365 <Root>
1366 <X />
1367 <React.Suspense fallback={steps[i]}>
1368 <Z />
1369 <MaybeSuspend suspend={true}>
1370 <X />
1371 <Y />
1372 </MaybeSuspend>
1373 <Z />
1374 </React.Suspense>
1375 <Y />
1376 </Root>,
1377 ),
1378 );
1379 expect(print(store, undefined, undefined, false)).toEqual(
1380 fallbackSnapshots[i],
1381 );
1382 // Re-render with steps[j].
1383 act(() =>
1384 render(
1385 <Root>
1386 <X />
1387 <React.Suspense fallback={steps[j]}>
1388 <Z />
1389 <MaybeSuspend suspend={true}>
1390 <Y />
1391 <X />
1392 </MaybeSuspend>
1393 <Z />
1394 </React.Suspense>
1395 <Y />
1396 </Root>,
1397 ),
1398 );
1399 // Verify the successful transition to steps[j].
1400 expect(print(store, undefined, undefined, false)).toEqual(
1401 fallbackSnapshots[j],
1402 );
1403 // Check that we can transition back again.
1404 act(() =>
1405 render(
1406 <Root>
1407 <X />
1408 <React.Suspense fallback={steps[i]}>
1409 <Z />
1410 <MaybeSuspend suspend={true}>
1411 <X />
1412 <Y />
1413 </MaybeSuspend>
1414 <Z />
1415 </React.Suspense>
1416 <Y />
1417 </Root>,
1418 ),
1419 );
1420 expect(print(store, undefined, undefined, false)).toEqual(
1421 fallbackSnapshots[i],
1422 );
1423 // Clean up after every iteration.
1424 act(() => unmount());
1425 expect(print(store)).toBe('');
1426 }
1427 }
1428
1429 // 5. Verify we can update from each step to each step when moving primary -> fallback.
1430 for (let i = 0; i < steps.length; i++) {
1431 for (let j = 0; j < steps.length; j++) {
1432 // Always start with a fresh container and steps[i].
1433 createContainer();
1434
1435 act(() =>
1436 render(
1437 <Root>
1438 <X />
1439 <React.Suspense fallback={z}>
1440 <MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
1441 </React.Suspense>
1442 <Y />
1443 </Root>,
1444 ),
1445 );
1446 expect(print(store)).toEqual(snapshots[i]);
1447 // Re-render with steps[j].
1448 act(() =>
1449 render(
1450 <Root>
1451 <X />
1452 <React.Suspense fallback={steps[j]}>
1453 <MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
1454 </React.Suspense>
1455 <Y />
1456 </Root>,
1457 ),
1458 );
1459 // Verify the successful transition to steps[j].
1460 expect(print(store, undefined, undefined, false)).toEqual(
1461 fallbackSnapshots[j],
1462 );
1463 // Check that we can transition back again.
1464 act(() =>
1465 render(
1466 <Root>
1467 <X />
1468 <React.Suspense fallback={z}>
1469 <MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
1470 </React.Suspense>
1471 <Y />
1472 </Root>,
1473 ),
1474 );
1475 expect(print(store)).toEqual(snapshots[i]);
1476 // Clean up after every iteration.
1477 act(() => unmount());
1478 expect(print(store)).toBe('');
1479 }
1480 }
1481
1482 // 6. Verify we can update from each step to each step when moving fallback -> primary.
1483 for (let i = 0; i < steps.length; i++) {
1484 for (let j = 0; j < steps.length; j++) {
1485 // Always start with a fresh container and steps[i].
1486 createContainer();
1487
1488 act(() =>
1489 render(
1490 <Root>
1491 <X />
1492 <React.Suspense fallback={steps[i]}>
1493 <MaybeSuspend suspend={true}>{steps[j]}</MaybeSuspend>
1494 </React.Suspense>
1495 <Y />
1496 </Root>,
1497 ),
1498 );
1499 expect(print(store, undefined, undefined, false)).toEqual(
1500 fallbackSnapshots[i],
1501 );
1502 // Re-render with steps[j].
1503 act(() =>
1504 render(
1505 <Root>
1506 <X />
1507 <React.Suspense fallback={steps[i]}>
1508 <MaybeSuspend suspend={false}>{steps[j]}</MaybeSuspend>
1509 </React.Suspense>
1510 <Y />
1511 </Root>,
1512 ),
1513 );
1514 // Verify the successful transition to steps[j].
1515 expect(print(store)).toEqual(snapshots[j]);
1516 // Check that we can transition back again.
1517 act(() =>
1518 render(
1519 <Root>
1520 <X />
1521 <React.Suspense fallback={steps[i]}>
1522 <MaybeSuspend suspend={true}>{steps[j]}</MaybeSuspend>
1523 </React.Suspense>
1524 <Y />
1525 </Root>,
1526 ),
1527 );
1528 expect(print(store, undefined, undefined, false)).toEqual(
1529 fallbackSnapshots[i],
1530 );
1531 // Clean up after every iteration.
1532 act(() => unmount());
1533 expect(print(store)).toBe('');
1534 }
1535 }
1536
1537 // 7. Verify we can update from each step to each step when toggling Suspense.
1538 for (let i = 0; i < steps.length; i++) {
1539 for (let j = 0; j < steps.length; j++) {
1540 // Always start with a fresh container and steps[i].
1541 createContainer();
1542
1543 act(() =>
1544 render(
1545 <Root>
1546 <X />
1547 <React.Suspense fallback={steps[j]}>
1548 <MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
1549 </React.Suspense>
1550 <Y />
1551 </Root>,
1552 ),
1553 );
1554
1555 // We get ID from the index in the tree above:
1556 // Root, X, Suspense, ...
1557 // ^ (index is 2)
1558 const suspenseID = store.getElementIDAtIndex(2);
1559
1560 // Force fallback.
1561 expect(print(store)).toEqual(snapshots[i]);
1562 act(() => {
1563 bridge.send('overrideSuspense', {
1564 id: suspenseID,
1565 rendererID: store.getRendererIDForElement(suspenseID),
1566 forceFallback: true,
1567 });
1568 });
1569 expect(print(store, undefined, undefined, false)).toEqual(
1570 fallbackSnapshots[j],
1571 );
1572
1573 // Stop forcing fallback.
1574 act(() => {
1575 bridge.send('overrideSuspense', {
1576 id: suspenseID,
1577 rendererID: store.getRendererIDForElement(suspenseID),
1578 forceFallback: false,
1579 });
1580 });
1581 expect(print(store)).toEqual(snapshots[i]);
1582
1583 // Trigger actual fallback.
1584 act(() =>
1585 render(
1586 <Root>
1587 <X />
1588 <React.Suspense fallback={steps[j]}>
1589 <MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
1590 </React.Suspense>
1591 <Y />
1592 </Root>,
1593 ),
1594 );
1595 expect(print(store, undefined, undefined, false)).toEqual(
1596 fallbackSnapshots[j],
1597 );
1598
1599 // Force fallback while we're in fallback mode.
1600 act(() => {
1601 bridge.send('overrideSuspense', {
1602 id: suspenseID,
1603 rendererID: store.getRendererIDForElement(suspenseID),
1604 forceFallback: true,
1605 });
1606 });
1607 // Keep seeing fallback content.
1608 expect(print(store, undefined, undefined, false)).toEqual(
1609 fallbackSnapshots[j],
1610 );
1611
1612 // Switch to primary mode.
1613 act(() =>
1614 render(
1615 <Root>
1616 <X />
1617 <React.Suspense fallback={steps[j]}>
1618 <MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
1619 </React.Suspense>
1620 <Y />
1621 </Root>,
1622 ),
1623 );
1624 // Fallback is still forced though.
1625 expect(print(store, undefined, undefined, false)).toEqual(
1626 fallbackSnapshots[j],
1627 );
1628
1629 // Stop forcing fallback. This reverts to primary content.
1630 act(() => {
1631 bridge.send('overrideSuspense', {
1632 id: suspenseID,
1633 rendererID: store.getRendererIDForElement(suspenseID),
1634 forceFallback: false,
1635 });
1636 });
1637 // Now we see primary content.
1638 expect(print(store)).toEqual(snapshots[i]);
1639
1640 // Clean up after every iteration.
1641 act(() => unmount());
1642 expect(print(store)).toBe('');
1643 }
1644 }
1645 });
1646 });