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