main
js 4,160 lines 126 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 semver from 'semver';
11
12 import {getVersionedRenderImplementation} from './utils';
13 import {ReactVersion} from '../../../../ReactVersions';
14
15 const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion;
16
17 describe('Store', () => {
18 let React;
19 let ReactDOM;
20 let ReactDOMClient;
21 let agent;
22 let act;
23 let actAsync;
24 let bridge;
25 let createDisplayNameFilter;
26 let getRendererID;
27 let legacyRender;
28 let previousComponentFilters;
29 let store;
30 let withErrorsOrWarningsIgnored;
31
32 function readValue(promise) {
33 if (typeof React.use === 'function') {
34 return React.use(promise);
35 }
36
37 // Support for React < 19.0
38 switch (promise.status) {
39 case 'fulfilled':
40 return promise.value;
41 case 'rejected':
42 throw promise.reason;
43 case 'pending':
44 throw promise;
45 default:
46 promise.status = 'pending';
47 promise.then(
48 value => {
49 promise.status = 'fulfilled';
50 promise.value = value;
51 },
52 reason => {
53 promise.status = 'rejected';
54 promise.reason = reason;
55 },
56 );
57 throw promise;
58 }
59 }
60
61 beforeAll(() => {
62 // JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes
63 Element.prototype.getClientRects = function (this: Element) {
64 const textContent = this.textContent;
65 return [
66 new DOMRect(1, 2, textContent.length, textContent.split('\n').length),
67 ];
68 };
69 });
70
71 beforeEach(() => {
72 global.IS_REACT_ACT_ENVIRONMENT = true;
73
74 agent = global.agent;
75 bridge = global.bridge;
76 store = global.store;
77
78 previousComponentFilters = store.componentFilters;
79
80 React = require('react');
81 ReactDOM = require('react-dom');
82 ReactDOMClient = require('react-dom/client');
83
84 const utils = require('./utils');
85 act = utils.act;
86 actAsync = utils.actAsync;
87 getRendererID = utils.getRendererID;
88 legacyRender = utils.legacyRender;
89 createDisplayNameFilter = utils.createDisplayNameFilter;
90 withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
91 });
92
93 afterEach(() => {
94 store.componentFilters = previousComponentFilters;
95 });
96
97 const {render, unmount, createContainer} = getVersionedRenderImplementation();
98
99 // @reactVersion >= 18.0
100 it('should not allow a root node to be collapsed', async () => {
101 const Component = () => <div>Hi</div>;
102
103 await act(() => render(<Component count={4} />));
104 expect(store).toMatchInlineSnapshot(`
105 [root]
106 <Component>
107 `);
108
109 expect(store.roots).toHaveLength(1);
110
111 const rootID = store.roots[0];
112
113 expect(() => store.toggleIsCollapsed(rootID, true)).toThrow(
114 'Root nodes cannot be collapsed',
115 );
116 });
117
118 // @reactVersion >= 18.0
119 it('should properly handle a root with no visible nodes', async () => {
120 const Root = ({children}) => children;
121
122 await act(() => render(<Root>{null}</Root>));
123 expect(store).toMatchInlineSnapshot(`
124 [root]
125 <Root>
126 `);
127
128 await act(() => render(<div />));
129 expect(store).toMatchInlineSnapshot(`[root]`);
130 });
131
132 it('throws when a transition timeline is requested during initial paint', () => {
133 const errorListener = jest.fn();
134 store.addListener('error', errorListener);
135
136 expect(() =>
137 store.getSuspendableDocumentOrderSuspenseTransition(false, 1),
138 ).toThrow(
139 'Cannot get a transition timeline during the initial paint. This is a bug in React DevTools.',
140 );
141 expect(errorListener).toHaveBeenCalledTimes(1);
142
143 store.removeListener('error', errorListener);
144 });
145
146 // @reactVersion >= 18.0
147 it('throws before removing a node that is not a child of its parent', () => {
148 function FirstChild() {
149 return null;
150 }
151 function SecondChild() {
152 return null;
153 }
154 function Parent({showFirstChild}) {
155 return (
156 <>
157 {showFirstChild && <FirstChild />}
158 <SecondChild />
159 </>
160 );
161 }
162
163 act(() => render(<Parent showFirstChild={true} />));
164
165 const parent = store.getElementAtIndex(0);
166 expect(parent.displayName).toBe('Parent');
167 const firstChildIndex = parent.children.findIndex(id => {
168 const child = store.getElementByID(id);
169 return child !== null && child.displayName === 'FirstChild';
170 });
171 expect(firstChildIndex).not.toBe(-1);
172 const firstChildID = parent.children[firstChildIndex];
173
174 // Corrupt only the frontend relationship. The removal operation below is
175 // still produced canonically by rendering React.
176 parent.children.splice(firstChildIndex, 1);
177
178 const errorListener = jest.fn();
179 store.addListener('error', errorListener);
180 let caughtError = null;
181 try {
182 act(() => render(<Parent showFirstChild={false} />));
183 } catch (error) {
184 caughtError = error;
185 } finally {
186 // The test Bridge invokes listeners synchronously, so discard the batch
187 // whose Store listener intentionally threw.
188 bridge._messageQueue.length = 0;
189 }
190
191 const expectedMessage =
192 `Cannot remove node "${firstChildID}" from parent "${parent.id}" ` +
193 `because it is not a child of the parent.`;
194 expect(caughtError).toMatchObject({message: expectedMessage});
195 expect(errorListener).toHaveBeenCalledWith(caughtError);
196 expect(store.containsElement(firstChildID)).toBe(true);
197
198 parent.children.splice(firstChildIndex, 0, firstChildID);
199 store.removeListener('error', errorListener);
200 });
201
202 // @reactVersion >= 18.0
203 it('receives operations queued while the frontend transport reconnects', () => {
204 const App = ({children}) => children ?? null;
205 const Parent = ({children}) => children ?? null;
206 const Child = () => null;
207
208 act(() => render(<App />));
209
210 const bridgeWall = (bridge.wall: any);
211
212 bridgeWall.disconnect();
213 try {
214 act(() =>
215 render(
216 <App>
217 <Parent />
218 </App>,
219 ),
220 );
221
222 expect(store).toMatchInlineSnapshot(`
223 [root]
224 <App>
225 `);
226 } finally {
227 bridgeWall.reconnect();
228 }
229
230 expect(store).toMatchInlineSnapshot(`
231 [root]
232 ▾ <App>
233 <Parent>
234 `);
235
236 act(() =>
237 render(
238 <App>
239 <Parent>
240 <Child />
241 </Parent>
242 </App>,
243 ),
244 );
245
246 expect(store).toMatchInlineSnapshot(`
247 [root]
248 ▾ <App>
249 ▾ <Parent>
250 <Child>
251 `);
252 });
253
254 // This test is not the same cause as what's reported on GitHub,
255 // but the resulting behavior (owner mounting after descendant) is the same.
256 // Thec ase below is admittedly contrived and relies on side effects.
257 // I'mnot yet sure of how to reduce the GitHub reported production case to a test though.
258 // See https://github.com/facebook/react/issues/21445
259 // @reactVersion >= 18.0
260 it('should handle when a component mounts before its owner', async () => {
261 const promise = new Promise(resolve => {});
262
263 let Dynamic = null;
264 const Owner = () => {
265 Dynamic = <Child />;
266 readValue(promise);
267 };
268 const Parent = () => {
269 return Dynamic;
270 };
271 const Child = () => null;
272
273 await act(() =>
274 render(
275 <>
276 <React.Suspense fallback="Loading...">
277 <Owner />
278 </React.Suspense>
279 <Parent />
280 </>,
281 ),
282 );
283 expect(store).toMatchInlineSnapshot(`
284 [root]
285 <Suspense>
286 ▾ <Parent>
287 <Child>
288 [suspense-root] rects={null}
289 <Suspense name="Unknown" uniqueSuspenders={true} rects={null}>
290 `);
291 });
292
293 // @reactVersion >= 18.0
294 it('should handle multibyte character strings', async () => {
295 const Component = () => null;
296 Component.displayName = '🟩💜🔵';
297
298 await act(() => render(<Component />));
299 expect(store).toMatchInlineSnapshot(`
300 [root]
301 <🟩💜🔵>
302 `);
303 });
304
305 it('should handle reorder of filtered elements', async () => {
306 function IgnoreMePassthrough({children}) {
307 return children;
308 }
309 function PassThrough({children}) {
310 return children;
311 }
312
313 await actAsync(
314 async () =>
315 (store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]),
316 );
317
318 await act(() => {
319 render(
320 <PassThrough key="e" name="e">
321 <IgnoreMePassthrough key="e1">
322 <PassThrough name="e-child-one">
323 <p>e1</p>
324 </PassThrough>
325 </IgnoreMePassthrough>
326 <IgnoreMePassthrough key="e2">
327 <PassThrough name="e-child-two">
328 <div>e2</div>
329 </PassThrough>
330 </IgnoreMePassthrough>
331 </PassThrough>,
332 );
333 });
334
335 expect(store).toMatchInlineSnapshot(`
336 [root]
337 ▾ <PassThrough key="e">
338 ▾ <PassThrough>
339 <p>
340 ▾ <PassThrough>
341 <div>
342 `);
343
344 await act(() => {
345 render(
346 <PassThrough key="e" name="e">
347 <IgnoreMePassthrough key="e2">
348 <PassThrough name="e-child-two">
349 <div>e2</div>
350 </PassThrough>
351 </IgnoreMePassthrough>
352 <IgnoreMePassthrough key="e1">
353 <PassThrough name="e-child-one">
354 <p>e1</p>
355 </PassThrough>
356 </IgnoreMePassthrough>
357 </PassThrough>,
358 );
359 });
360
361 expect(store).toMatchInlineSnapshot(`
362 [root]
363 ▾ <PassThrough key="e">
364 ▾ <PassThrough>
365 <div>
366 ▾ <PassThrough>
367 <p>
368 `);
369 });
370
371 describe('StrictMode compliance', () => {
372 it('should mark strict root elements as strict', async () => {
373 const App = () => <Component />;
374 const Component = () => null;
375
376 const container = document.createElement('div');
377 const root = ReactDOMClient.createRoot(container, {
378 unstable_strictMode: true,
379 });
380 await act(() => {
381 root.render(<App />);
382 });
383
384 expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(false);
385 expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(false);
386 });
387
388 // @reactVersion >= 18.0
389 it('should mark non strict root elements as not strict', async () => {
390 const App = () => <Component />;
391 const Component = () => null;
392
393 const container = document.createElement('div');
394 const root = ReactDOMClient.createRoot(container);
395 await act(() => {
396 root.render(<App />);
397 });
398
399 expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(true);
400 expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(true);
401 });
402
403 it('should mark StrictMode subtree elements as strict', async () => {
404 const App = () => (
405 <React.StrictMode>
406 <Component />
407 </React.StrictMode>
408 );
409 const Component = () => null;
410
411 const container = document.createElement('div');
412 const root = ReactDOMClient.createRoot(container);
413 await act(() => {
414 root.render(<App />);
415 });
416
417 expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(true);
418 expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(false);
419 });
420 });
421
422 describe('Activity hidden state', () => {
423 // @reactVersion >= 19
424 it('should mark Activity subtree elements as hidden when mode is hidden', async () => {
425 const Activity = React.Activity || React.unstable_Activity;
426
427 function Child() {
428 return <div>child</div>;
429 }
430
431 function App({hidden}) {
432 return (
433 <Activity mode={hidden ? 'hidden' : 'visible'}>
434 <Child />
435 </Activity>
436 );
437 }
438
439 await actAsync(() => {
440 render(<App hidden={true} />);
441 });
442
443 // Activity element should be marked as hidden and collapsed
444 const activityElement = store.getElementAtIndex(1);
445 expect(activityElement.displayName).toBe('Activity');
446 expect(activityElement.isActivityHidden).toBe(true);
447 expect(activityElement.isInsideHiddenActivity).toBe(false);
448 expect(activityElement.isCollapsed).toBe(true);
449
450 // Expand to access children
451 store.toggleIsCollapsed(activityElement.id, false);
452
453 // Children should still be in the tree but marked as inside hidden Activity
454 const childElement = store.getElementAtIndex(2);
455 expect(childElement.displayName).toBe('Child');
456 expect(childElement.isInsideHiddenActivity).toBe(true);
457 });
458
459 // @reactVersion >= 19
460 it('should not mark Activity subtree as hidden when mode is visible', async () => {
461 const Activity = React.Activity || React.unstable_Activity;
462
463 function Child() {
464 return <div>child</div>;
465 }
466
467 function App() {
468 return (
469 <Activity mode="visible">
470 <Child />
471 </Activity>
472 );
473 }
474
475 await actAsync(() => {
476 render(<App />);
477 });
478
479 const activityElement = store.getElementAtIndex(1);
480 expect(activityElement.displayName).toBe('Activity');
481 expect(activityElement.isActivityHidden).toBe(false);
482 expect(activityElement.isInsideHiddenActivity).toBe(false);
483 expect(activityElement.isCollapsed).toBe(false);
484
485 const childElement = store.getElementAtIndex(2);
486 expect(childElement.displayName).toBe('Child');
487 expect(childElement.isInsideHiddenActivity).toBe(false);
488 });
489
490 // @reactVersion >= 19
491 it('should update hidden state when Activity mode toggles', async () => {
492 const Activity = React.Activity || React.unstable_Activity;
493
494 function Child() {
495 return <div>child</div>;
496 }
497
498 function App({hidden}) {
499 return (
500 <Activity mode={hidden ? 'hidden' : 'visible'}>
501 <Child />
502 </Activity>
503 );
504 }
505
506 // Start visible
507 await actAsync(() => {
508 render(<App hidden={false} />);
509 });
510
511 let activityElement = store.getElementAtIndex(1);
512 expect(activityElement.isActivityHidden).toBe(false);
513 expect(activityElement.isCollapsed).toBe(false);
514
515 let childElement = store.getElementAtIndex(2);
516 expect(childElement.isInsideHiddenActivity).toBe(false);
517
518 // Toggle to hidden — children remain but subtree collapses
519 await actAsync(() => {
520 render(<App hidden={true} />);
521 });
522
523 activityElement = store.getElementAtIndex(1);
524 expect(activityElement.isActivityHidden).toBe(true);
525 expect(activityElement.isCollapsed).toBe(true);
526
527 // Expand to verify children are still marked
528 store.toggleIsCollapsed(activityElement.id, false);
529
530 childElement = store.getElementAtIndex(2);
531 expect(childElement.displayName).toBe('Child');
532 expect(childElement.isInsideHiddenActivity).toBe(true);
533
534 // Toggle back to visible — subtree expands automatically
535 await actAsync(() => {
536 render(<App hidden={false} />);
537 });
538
539 activityElement = store.getElementAtIndex(1);
540 expect(activityElement.isActivityHidden).toBe(false);
541 expect(activityElement.isCollapsed).toBe(false);
542
543 childElement = store.getElementAtIndex(2);
544 expect(childElement.isInsideHiddenActivity).toBe(false);
545 });
546
547 // @reactVersion >= 19
548 it('should propagate hidden state to deeply nested children', async () => {
549 const Activity = React.Activity || React.unstable_Activity;
550
551 function GrandChild() {
552 return <div>grandchild</div>;
553 }
554 function Child() {
555 return <GrandChild />;
556 }
557
558 function App({hidden}) {
559 return (
560 <Activity mode={hidden ? 'hidden' : 'visible'}>
561 <Child />
562 </Activity>
563 );
564 }
565
566 await actAsync(() => {
567 render(<App hidden={true} />);
568 });
569
570 const activityElement = store.getElementAtIndex(1);
571 expect(activityElement.displayName).toBe('Activity');
572 expect(activityElement.isActivityHidden).toBe(true);
573 expect(activityElement.isCollapsed).toBe(true);
574
575 // Expand to access children
576 store.toggleIsCollapsed(activityElement.id, false);
577
578 const childElement = store.getElementAtIndex(2);
579 expect(childElement.displayName).toBe('Child');
580 expect(childElement.isInsideHiddenActivity).toBe(true);
581
582 const grandChildElement = store.getElementAtIndex(3);
583 expect(grandChildElement.displayName).toBe('GrandChild');
584 expect(grandChildElement.isInsideHiddenActivity).toBe(true);
585 });
586
587 // @reactVersion >= 19
588 it('should collapse hidden Activity subtree by default', async () => {
589 const Activity = React.Activity || React.unstable_Activity;
590
591 function Child() {
592 return <div>child</div>;
593 }
594
595 function App({hidden}) {
596 return (
597 <Activity mode={hidden ? 'hidden' : 'visible'}>
598 <Child />
599 </Activity>
600 );
601 }
602
603 // Hidden Activity should be collapsed
604 await actAsync(() => {
605 render(<App hidden={true} />);
606 });
607
608 expect(store).toMatchInlineSnapshot(`
609 [root]
610 ▾ <App>
611 ▸ <Activity mode="hidden">
612 `);
613
614 // Toggle to visible — should expand
615 await actAsync(() => {
616 render(<App hidden={false} />);
617 });
618
619 expect(store).toMatchInlineSnapshot(`
620 [root]
621 ▾ <App>
622 ▾ <Activity mode="visible">
623 <Child>
624 `);
625
626 // Toggle back to hidden — should collapse again
627 await actAsync(() => {
628 render(<App hidden={true} />);
629 });
630
631 expect(store).toMatchInlineSnapshot(`
632 [root]
633 ▾ <App>
634 ▸ <Activity mode="hidden">
635 `);
636 });
637
638 // @reactVersion >= 19
639 it('should dim nested visible Activity inside a hidden Activity', async () => {
640 const Activity = React.Activity || React.unstable_Activity;
641
642 function Leaf() {
643 return <div>leaf</div>;
644 }
645
646 function App() {
647 return (
648 <Activity mode="hidden" name="outer">
649 <Activity mode="visible" name="inner">
650 <Leaf />
651 </Activity>
652 </Activity>
653 );
654 }
655
656 await actAsync(() => {
657 render(<App />);
658 });
659
660 // Outer Activity: hidden, collapsed, not dimmed itself
661 const outerActivity = store.getElementAtIndex(1);
662 expect(outerActivity.displayName).toBe('Activity');
663 expect(outerActivity.nameProp).toBe('outer');
664 expect(outerActivity.isActivityHidden).toBe(true);
665 expect(outerActivity.isInsideHiddenActivity).toBe(false);
666 expect(outerActivity.isCollapsed).toBe(true);
667
668 // Expand to access inner elements
669 store.toggleIsCollapsed(outerActivity.id, false);
670
671 // Inner Activity: visible, but inside hidden outer so still dimmed
672 const innerActivity = store.getElementAtIndex(2);
673 expect(innerActivity.displayName).toBe('Activity');
674 expect(innerActivity.nameProp).toBe('inner');
675 expect(innerActivity.isActivityHidden).toBe(false);
676 expect(innerActivity.isInsideHiddenActivity).toBe(true);
677
678 // Leaf: inside both, dimmed
679 const leaf = store.getElementAtIndex(3);
680 expect(leaf.displayName).toBe('Leaf');
681 expect(leaf.isInsideHiddenActivity).toBe(true);
682 });
683 });
684
685 describe('collapseNodesByDefault:false', () => {
686 beforeEach(() => {
687 store.collapseNodesByDefault = false;
688 });
689
690 // @reactVersion >= 18.0
691 it('should support mount and update operations', async () => {
692 const Grandparent = ({count}) => (
693 <React.Fragment>
694 <Parent count={count} />
695 <Parent count={count} />
696 </React.Fragment>
697 );
698 const Parent = ({count}) =>
699 new Array(count).fill(true).map((_, index) => <Child key={index} />);
700 const Child = () => <div>Hi!</div>;
701
702 await act(() => render(<Grandparent count={4} />));
703 expect(store).toMatchInlineSnapshot(`
704 [root]
705 ▾ <Grandparent>
706 ▾ <Parent>
707 <Child key="0">
708 <Child key="1">
709 <Child key="2">
710 <Child key="3">
711 ▾ <Parent>
712 <Child key="0">
713 <Child key="1">
714 <Child key="2">
715 <Child key="3">
716 `);
717
718 await act(() => render(<Grandparent count={2} />));
719 expect(store).toMatchInlineSnapshot(`
720 [root]
721 ▾ <Grandparent>
722 ▾ <Parent>
723 <Child key="0">
724 <Child key="1">
725 ▾ <Parent>
726 <Child key="0">
727 <Child key="1">
728 `);
729
730 await act(() => unmount());
731 expect(store).toMatchInlineSnapshot(``);
732 });
733
734 // @reactVersion >= 18.0
735 // @reactVersion < 19
736 // @gate !disableLegacyMode
737 it('should support mount and update operations for multiple roots (legacy render)', async () => {
738 const Parent = ({count}) =>
739 new Array(count).fill(true).map((_, index) => <Child key={index} />);
740 const Child = () => <div>Hi!</div>;
741
742 const containerA = document.createElement('div');
743 const containerB = document.createElement('div');
744
745 await act(() => {
746 legacyRender(<Parent key="A" count={3} />, containerA);
747 legacyRender(<Parent key="B" count={2} />, containerB);
748 });
749 expect(store).toMatchInlineSnapshot(`
750 [root]
751 ▾ <Parent key="A">
752 <Child key="0">
753 <Child key="1">
754 <Child key="2">
755 [root]
756 ▾ <Parent key="B">
757 <Child key="0">
758 <Child key="1">
759 `);
760
761 await act(() => {
762 legacyRender(<Parent key="A" count={4} />, containerA);
763 legacyRender(<Parent key="B" count={1} />, containerB);
764 });
765 expect(store).toMatchInlineSnapshot(`
766 [root]
767 ▾ <Parent key="A">
768 <Child key="0">
769 <Child key="1">
770 <Child key="2">
771 <Child key="3">
772 [root]
773 ▾ <Parent key="B">
774 <Child key="0">
775 `);
776
777 await act(() => ReactDOM.unmountComponentAtNode(containerB));
778 expect(store).toMatchInlineSnapshot(`
779 [root]
780 ▾ <Parent key="A">
781 <Child key="0">
782 <Child key="1">
783 <Child key="2">
784 <Child key="3">
785 `);
786
787 await act(() => ReactDOM.unmountComponentAtNode(containerA));
788 expect(store).toMatchInlineSnapshot(``);
789 });
790
791 // @reactVersion >= 18.0
792 it('should support mount and update operations for multiple roots (createRoot)', async () => {
793 const Parent = ({count}) =>
794 new Array(count).fill(true).map((_, index) => <Child key={index} />);
795 const Child = () => <div>Hi!</div>;
796
797 const containerA = document.createElement('div');
798 const containerB = document.createElement('div');
799
800 const rootA = ReactDOMClient.createRoot(containerA);
801 const rootB = ReactDOMClient.createRoot(containerB);
802
803 await act(() => {
804 rootA.render(<Parent key="A" count={3} />);
805 rootB.render(<Parent key="B" count={2} />);
806 });
807 expect(store).toMatchInlineSnapshot(`
808 [root]
809 ▾ <Parent key="A">
810 <Child key="0">
811 <Child key="1">
812 <Child key="2">
813 [root]
814 ▾ <Parent key="B">
815 <Child key="0">
816 <Child key="1">
817 `);
818
819 await act(() => {
820 rootA.render(<Parent key="A" count={4} />);
821 rootB.render(<Parent key="B" count={1} />);
822 });
823 expect(store).toMatchInlineSnapshot(`
824 [root]
825 ▾ <Parent key="A">
826 <Child key="0">
827 <Child key="1">
828 <Child key="2">
829 <Child key="3">
830 [root]
831 ▾ <Parent key="B">
832 <Child key="0">
833 `);
834
835 await act(() => rootB.unmount());
836 expect(store).toMatchInlineSnapshot(`
837 [root]
838 ▾ <Parent key="A">
839 <Child key="0">
840 <Child key="1">
841 <Child key="2">
842 <Child key="3">
843 `);
844
845 await act(() => rootA.unmount());
846 expect(store).toMatchInlineSnapshot(``);
847 });
848
849 // @reactVersion >= 18.0
850 it('should filter DOM nodes from the store tree', async () => {
851 const Grandparent = () => (
852 <div>
853 <div>
854 <Parent />
855 </div>
856 <Parent />
857 </div>
858 );
859 const Parent = () => (
860 <div>
861 <Child />
862 </div>
863 );
864 const Child = () => <div>Hi!</div>;
865
866 await act(() => render(<Grandparent count={4} />));
867 expect(store).toMatchInlineSnapshot(`
868 [root]
869 ▾ <Grandparent>
870 ▾ <Parent>
871 <Child>
872 ▾ <Parent>
873 <Child>
874 `);
875 });
876
877 // @reactVersion >= 18.0
878 it('should display Suspense nodes properly in various states', async () => {
879 const Loading = () => <div>Loading...</div>;
880 const never = new Promise(() => {});
881 const SuspendingComponent = () => {
882 readValue(never);
883 };
884 const Component = () => {
885 return <div>Hello</div>;
886 };
887 const Wrapper = ({shouldSuspense}) => (
888 <React.Fragment>
889 <Component key="Outside" />
890 <React.Suspense fallback={<Loading />}>
891 {shouldSuspense ? (
892 <SuspendingComponent />
893 ) : (
894 <Component key="Inside" />
895 )}
896 </React.Suspense>
897 </React.Fragment>
898 );
899
900 await act(() => render(<Wrapper shouldSuspense={true} />));
901 expect(store).toMatchInlineSnapshot(`
902 [root]
903 ▾ <Wrapper>
904 <Component key="Outside">
905 ▾ <Suspense>
906 <Loading>
907 [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
908 <Suspense name="Wrapper" uniqueSuspenders={true} rects={null}>
909 `);
910
911 await act(() => {
912 render(<Wrapper shouldSuspense={false} />);
913 });
914 expect(store).toMatchInlineSnapshot(`
915 [root]
916 ▾ <Wrapper>
917 <Component key="Outside">
918 ▾ <Suspense>
919 <Component key="Inside">
920 [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
921 <Suspense name="Wrapper" uniqueSuspenders={true} rects={[{x:1,y:2,width:5,height:1}]}>
922 `);
923 });
924
925 // @reactVersion >= 18.0
926 it('should support nested Suspense nodes', async () => {
927 const Component = () => null;
928 const Loading = () => <div>Loading...</div>;
929 const never = new Promise(() => {});
930 const Never = () => {
931 readValue(never);
932 };
933
934 const Wrapper = ({
935 suspendFirst = false,
936 suspendSecond = false,
937 suspendParent = false,
938 }) => (
939 <React.Fragment>
940 <Component key="Outside" />
941 <React.Suspense
942 name="parent"
943 fallback={<Loading key="Parent Fallback" />}>
944 <Component key="Unrelated at Start" />
945 <React.Suspense
946 name="one"
947 fallback={<Loading key="Suspense 1 Fallback" />}>
948 {suspendFirst ? (
949 <Never />
950 ) : (
951 <Component key="Suspense 1 Content" />
952 )}
953 </React.Suspense>
954 <React.Suspense
955 name="two"
956 fallback={<Loading key="Suspense 2 Fallback" />}>
957 {suspendSecond ? (
958 <Never />
959 ) : (
960 <Component key="Suspense 2 Content" />
961 )}
962 </React.Suspense>
963 <React.Suspense
964 name="three"
965 fallback={<Loading key="Suspense 3 Fallback" />}>
966 <Never />
967 </React.Suspense>
968 {suspendParent && <Never />}
969 <Component key="Unrelated at End" />
970 </React.Suspense>
971 </React.Fragment>
972 );
973
974 await actAsync(() =>
975 render(
976 <Wrapper
977 suspendParent={false}
978 suspendFirst={false}
979 suspendSecond={false}
980 />,
981 ),
982 );
983 expect(store).toMatchInlineSnapshot(`
984 [root]
985 ▾ <Wrapper>
986 <Component key="Outside">
987 ▾ <Suspense name="parent">
988 <Component key="Unrelated at Start">
989 ▾ <Suspense name="one">
990 <Component key="Suspense 1 Content">
991 ▾ <Suspense name="two">
992 <Component key="Suspense 2 Content">
993 ▾ <Suspense name="three">
994 <Loading key="Suspense 3 Fallback">
995 <Component key="Unrelated at End">
996 [suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
997 <Suspense name="parent" uniqueSuspenders={false} rects={[{x:1,y:2,width:10,height:1}]}>
998 <Suspense name="one" uniqueSuspenders={false} rects={null}>
999 <Suspense name="two" uniqueSuspenders={false} rects={null}>
1000 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1001 `);
1002 await act(() =>
1003 render(
1004 <Wrapper
1005 suspendParent={false}
1006 suspendFirst={true}
1007 suspendSecond={false}
1008 />,
1009 ),
1010 );
1011 expect(store).toMatchInlineSnapshot(`
1012 [root]
1013 ▾ <Wrapper>
1014 <Component key="Outside">
1015 ▾ <Suspense name="parent">
1016 <Component key="Unrelated at Start">
1017 ▾ <Suspense name="one">
1018 <Loading key="Suspense 1 Fallback">
1019 ▾ <Suspense name="two">
1020 <Component key="Suspense 2 Content">
1021 ▾ <Suspense name="three">
1022 <Loading key="Suspense 3 Fallback">
1023 <Component key="Unrelated at End">
1024 [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
1025 <Suspense name="parent" uniqueSuspenders={false} rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
1026 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1027 <Suspense name="two" uniqueSuspenders={false} rects={null}>
1028 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1029 `);
1030 await act(() =>
1031 render(
1032 <Wrapper
1033 suspendParent={false}
1034 suspendFirst={false}
1035 suspendSecond={true}
1036 />,
1037 ),
1038 );
1039 expect(store).toMatchInlineSnapshot(`
1040 [root]
1041 ▾ <Wrapper>
1042 <Component key="Outside">
1043 ▾ <Suspense name="parent">
1044 <Component key="Unrelated at Start">
1045 ▾ <Suspense name="one">
1046 <Component key="Suspense 1 Content">
1047 ▾ <Suspense name="two">
1048 <Loading key="Suspense 2 Fallback">
1049 ▾ <Suspense name="three">
1050 <Loading key="Suspense 3 Fallback">
1051 <Component key="Unrelated at End">
1052 [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
1053 <Suspense name="parent" uniqueSuspenders={false} rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
1054 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1055 <Suspense name="two" uniqueSuspenders={true} rects={null}>
1056 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1057 `);
1058 await act(() =>
1059 render(
1060 <Wrapper
1061 suspendParent={false}
1062 suspendFirst={true}
1063 suspendSecond={false}
1064 />,
1065 ),
1066 );
1067 expect(store).toMatchInlineSnapshot(`
1068 [root]
1069 ▾ <Wrapper>
1070 <Component key="Outside">
1071 ▾ <Suspense name="parent">
1072 <Component key="Unrelated at Start">
1073 ▾ <Suspense name="one">
1074 <Loading key="Suspense 1 Fallback">
1075 ▾ <Suspense name="two">
1076 <Component key="Suspense 2 Content">
1077 ▾ <Suspense name="three">
1078 <Loading key="Suspense 3 Fallback">
1079 <Component key="Unrelated at End">
1080 [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
1081 <Suspense name="parent" uniqueSuspenders={false} rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
1082 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1083 <Suspense name="two" uniqueSuspenders={true} rects={null}>
1084 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1085 `);
1086 await act(() =>
1087 render(
1088 <Wrapper
1089 suspendParent={true}
1090 suspendFirst={true}
1091 suspendSecond={false}
1092 />,
1093 ),
1094 );
1095 expect(store).toMatchInlineSnapshot(`
1096 [root]
1097 ▾ <Wrapper>
1098 <Component key="Outside">
1099 ▾ <Suspense name="parent">
1100 <Loading key="Parent Fallback">
1101 [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
1102 <Suspense name="parent" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
1103 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1104 <Suspense name="two" uniqueSuspenders={true} rects={null}>
1105 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1106 `);
1107 await act(() =>
1108 render(
1109 <Wrapper
1110 suspendParent={false}
1111 suspendFirst={true}
1112 suspendSecond={true}
1113 />,
1114 ),
1115 );
1116 expect(store).toMatchInlineSnapshot(`
1117 [root]
1118 ▾ <Wrapper>
1119 <Component key="Outside">
1120 ▾ <Suspense name="parent">
1121 <Component key="Unrelated at Start">
1122 ▾ <Suspense name="one">
1123 <Loading key="Suspense 1 Fallback">
1124 ▾ <Suspense name="two">
1125 <Loading key="Suspense 2 Fallback">
1126 ▾ <Suspense name="three">
1127 <Loading key="Suspense 3 Fallback">
1128 <Component key="Unrelated at End">
1129 [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
1130 <Suspense name="parent" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
1131 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1132 <Suspense name="two" uniqueSuspenders={true} rects={null}>
1133 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1134 `);
1135 await act(() =>
1136 render(
1137 <Wrapper
1138 suspendParent={false}
1139 suspendFirst={false}
1140 suspendSecond={false}
1141 />,
1142 ),
1143 );
1144 expect(store).toMatchInlineSnapshot(`
1145 [root]
1146 ▾ <Wrapper>
1147 <Component key="Outside">
1148 ▾ <Suspense name="parent">
1149 <Component key="Unrelated at Start">
1150 ▾ <Suspense name="one">
1151 <Component key="Suspense 1 Content">
1152 ▾ <Suspense name="two">
1153 <Component key="Suspense 2 Content">
1154 ▾ <Suspense name="three">
1155 <Loading key="Suspense 3 Fallback">
1156 <Component key="Unrelated at End">
1157 [suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
1158 <Suspense name="parent" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}]}>
1159 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1160 <Suspense name="two" uniqueSuspenders={true} rects={null}>
1161 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1162 `);
1163
1164 const rendererID = getRendererID();
1165 await act(() =>
1166 agent.overrideSuspense({
1167 id: store.getElementIDAtIndex(4),
1168 rendererID,
1169 forceFallback: true,
1170 }),
1171 );
1172 expect(store).toMatchInlineSnapshot(`
1173 [root]
1174 ▾ <Wrapper>
1175 <Component key="Outside">
1176 ▾ <Suspense name="parent">
1177 <Component key="Unrelated at Start">
1178 ▾ <Suspense name="one">
1179 <Loading key="Suspense 1 Fallback">
1180 ▾ <Suspense name="two">
1181 <Component key="Suspense 2 Content">
1182 ▾ <Suspense name="three">
1183 <Loading key="Suspense 3 Fallback">
1184 <Component key="Unrelated at End">
1185 [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
1186 <Suspense name="parent" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
1187 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1188 <Suspense name="two" uniqueSuspenders={true} rects={null}>
1189 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1190 `);
1191 await act(() =>
1192 agent.overrideSuspense({
1193 id: store.getElementIDAtIndex(2),
1194 rendererID,
1195 forceFallback: true,
1196 }),
1197 );
1198 expect(store).toMatchInlineSnapshot(`
1199 [root]
1200 ▾ <Wrapper>
1201 <Component key="Outside">
1202 ▾ <Suspense name="parent">
1203 <Loading key="Parent Fallback">
1204 [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
1205 <Suspense name="parent" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
1206 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1207 <Suspense name="two" uniqueSuspenders={true} rects={null}>
1208 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1209 `);
1210 await act(() =>
1211 render(
1212 <Wrapper
1213 suspendParent={false}
1214 suspendFirst={true}
1215 suspendSecond={true}
1216 />,
1217 ),
1218 );
1219 expect(store).toMatchInlineSnapshot(`
1220 [root]
1221 ▾ <Wrapper>
1222 <Component key="Outside">
1223 ▾ <Suspense name="parent">
1224 <Loading key="Parent Fallback">
1225 [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
1226 <Suspense name="parent" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
1227 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1228 <Suspense name="two" uniqueSuspenders={true} rects={null}>
1229 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1230 `);
1231 await actAsync(() =>
1232 agent.overrideSuspense({
1233 id: store.getElementIDAtIndex(2),
1234 rendererID,
1235 forceFallback: false,
1236 }),
1237 );
1238 expect(store).toMatchInlineSnapshot(`
1239 [root]
1240 ▾ <Wrapper>
1241 <Component key="Outside">
1242 ▾ <Suspense name="parent">
1243 <Component key="Unrelated at Start">
1244 ▾ <Suspense name="one">
1245 <Loading key="Suspense 1 Fallback">
1246 ▾ <Suspense name="two">
1247 <Loading key="Suspense 2 Fallback">
1248 ▾ <Suspense name="three">
1249 <Loading key="Suspense 3 Fallback">
1250 <Component key="Unrelated at End">
1251 [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
1252 <Suspense name="parent" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
1253 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1254 <Suspense name="two" uniqueSuspenders={true} rects={null}>
1255 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1256 `);
1257 await act(() =>
1258 agent.overrideSuspense({
1259 id: store.getElementIDAtIndex(4),
1260 rendererID,
1261 forceFallback: false,
1262 }),
1263 );
1264 expect(store).toMatchInlineSnapshot(`
1265 [root]
1266 ▾ <Wrapper>
1267 <Component key="Outside">
1268 ▾ <Suspense name="parent">
1269 <Component key="Unrelated at Start">
1270 ▾ <Suspense name="one">
1271 <Loading key="Suspense 1 Fallback">
1272 ▾ <Suspense name="two">
1273 <Loading key="Suspense 2 Fallback">
1274 ▾ <Suspense name="three">
1275 <Loading key="Suspense 3 Fallback">
1276 <Component key="Unrelated at End">
1277 [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}
1278 <Suspense name="parent" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
1279 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1280 <Suspense name="two" uniqueSuspenders={true} rects={null}>
1281 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1282 `);
1283 await act(() =>
1284 render(
1285 <Wrapper
1286 suspendParent={false}
1287 suspendFirst={false}
1288 suspendSecond={false}
1289 />,
1290 ),
1291 );
1292 expect(store).toMatchInlineSnapshot(`
1293 [root]
1294 ▾ <Wrapper>
1295 <Component key="Outside">
1296 ▾ <Suspense name="parent">
1297 <Component key="Unrelated at Start">
1298 ▾ <Suspense name="one">
1299 <Component key="Suspense 1 Content">
1300 ▾ <Suspense name="two">
1301 <Component key="Suspense 2 Content">
1302 ▾ <Suspense name="three">
1303 <Loading key="Suspense 3 Fallback">
1304 <Component key="Unrelated at End">
1305 [suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
1306 <Suspense name="parent" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}]}>
1307 <Suspense name="one" uniqueSuspenders={true} rects={null}>
1308 <Suspense name="two" uniqueSuspenders={true} rects={null}>
1309 <Suspense name="three" uniqueSuspenders={true} rects={null}>
1310 `);
1311 });
1312
1313 // @reactVersion >= 18.0
1314 it('can override multiple Suspense simultaneously', async () => {
1315 const Component = () => {
1316 return <div>Hello</div>;
1317 };
1318 const App = () => (
1319 <React.Fragment>
1320 <Component key="Outside" />
1321 <React.Suspense
1322 name="parent"
1323 fallback={<Component key="Parent Fallback" />}>
1324 <Component key="Unrelated at Start" />
1325 <React.Suspense
1326 name="one"
1327 fallback={<Component key="Suspense 1 Fallback" />}>
1328 <Component key="Suspense 1 Content" />
1329 </React.Suspense>
1330 <React.Suspense
1331 name="two"
1332 fallback={<Component key="Suspense 2 Fallback" />}>
1333 <Component key="Suspense 2 Content" />
1334 </React.Suspense>
1335 <React.Suspense
1336 name="three"
1337 fallback={<Component key="Suspense 3 Fallback" />}>
1338 <Component key="Suspense 3 Content" />
1339 </React.Suspense>
1340 <Component key="Unrelated at End" />
1341 </React.Suspense>
1342 </React.Fragment>
1343 );
1344
1345 await actAsync(() => render(<App />));
1346
1347 expect(store).toMatchInlineSnapshot(`
1348 [root]
1349 ▾ <App>
1350 <Component key="Outside">
1351 ▾ <Suspense name="parent">
1352 <Component key="Unrelated at Start">
1353 ▾ <Suspense name="one">
1354 <Component key="Suspense 1 Content">
1355 ▾ <Suspense name="two">
1356 <Component key="Suspense 2 Content">
1357 ▾ <Suspense name="three">
1358 <Component key="Suspense 3 Content">
1359 <Component key="Unrelated at End">
1360 [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
1361 <Suspense name="parent" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
1362 <Suspense name="one" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}]}>
1363 <Suspense name="two" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}]}>
1364 <Suspense name="three" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}]}>
1365 `);
1366
1367 await actAsync(() => {
1368 agent.overrideSuspenseMilestone({
1369 rendererID: getRendererID(),
1370 suspendedSet: [
1371 store.getElementIDAtIndex(4),
1372 store.getElementIDAtIndex(8),
1373 ],
1374 });
1375 });
1376
1377 expect(store).toMatchInlineSnapshot(`
1378 [root]
1379 ▾ <App>
1380 <Component key="Outside">
1381 ▾ <Suspense name="parent">
1382 <Component key="Unrelated at Start">
1383 ▾ <Suspense name="one">
1384 <Component key="Suspense 1 Fallback">
1385 ▾ <Suspense name="two">
1386 <Component key="Suspense 2 Content">
1387 ▾ <Suspense name="three">
1388 <Component key="Suspense 3 Fallback">
1389 <Component key="Unrelated at End">
1390 [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
1391 <Suspense name="parent" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
1392 <Suspense name="one" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}]}>
1393 <Suspense name="two" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}]}>
1394 <Suspense name="three" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}]}>
1395 `);
1396
1397 await actAsync(() => {
1398 agent.overrideSuspenseMilestone({
1399 rendererID: getRendererID(),
1400 suspendedSet: [],
1401 });
1402 });
1403
1404 expect(store).toMatchInlineSnapshot(`
1405 [root]
1406 ▾ <App>
1407 <Component key="Outside">
1408 ▾ <Suspense name="parent">
1409 <Component key="Unrelated at Start">
1410 ▾ <Suspense name="one">
1411 <Component key="Suspense 1 Content">
1412 ▾ <Suspense name="two">
1413 <Component key="Suspense 2 Content">
1414 ▾ <Suspense name="three">
1415 <Component key="Suspense 3 Content">
1416 <Component key="Unrelated at End">
1417 [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
1418 <Suspense name="parent" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
1419 <Suspense name="one" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}]}>
1420 <Suspense name="two" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}]}>
1421 <Suspense name="three" uniqueSuspenders={false} rects={[{x:1,y:2,width:5,height:1}]}>
1422 `);
1423 });
1424
1425 it('should display a partially rendered SuspenseList', async () => {
1426 const Loading = () => <div>Loading...</div>;
1427 const never = new Promise(() => {});
1428 const SuspendingComponent = () => {
1429 readValue(never);
1430 };
1431 const Component = () => {
1432 return <div>Hello</div>;
1433 };
1434 const Wrapper = ({shouldSuspense}) => (
1435 <React.Fragment>
1436 <React.unstable_SuspenseList revealOrder="forwards" tail="collapsed">
1437 <Component key="A" />
1438 <React.Suspense fallback={<Loading />}>
1439 {shouldSuspense ? <SuspendingComponent /> : <Component key="B" />}
1440 </React.Suspense>
1441 <Component key="C" />
1442 </React.unstable_SuspenseList>
1443 </React.Fragment>
1444 );
1445
1446 const container = document.createElement('div');
1447 const root = ReactDOMClient.createRoot(container);
1448 await act(() => {
1449 root.render(<Wrapper shouldSuspense={true} />);
1450 });
1451 expect(store).toMatchInlineSnapshot(`
1452 [root]
1453 ▾ <Wrapper>
1454 ▾ <SuspenseList>
1455 <Component key="A">
1456 ▾ <Suspense>
1457 <Loading>
1458 [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
1459 <Suspense name="Wrapper" uniqueSuspenders={true} rects={null}>
1460 `);
1461
1462 await act(() => {
1463 root.render(<Wrapper shouldSuspense={false} />);
1464 });
1465 expect(store).toMatchInlineSnapshot(`
1466 [root]
1467 ▾ <Wrapper>
1468 ▾ <SuspenseList>
1469 <Component key="A">
1470 ▾ <Suspense>
1471 <Component key="B">
1472 <Component key="C">
1473 [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
1474 <Suspense name="Wrapper" uniqueSuspenders={true} rects={[{x:1,y:2,width:5,height:1}]}>
1475 `);
1476 });
1477
1478 // @reactVersion >= 18.0
1479 it('should support collapsing parts of the tree', async () => {
1480 const Grandparent = ({count}) => (
1481 <React.Fragment>
1482 <Parent count={count} />
1483 <Parent count={count} />
1484 </React.Fragment>
1485 );
1486 const Parent = ({count}) =>
1487 new Array(count).fill(true).map((_, index) => <Child key={index} />);
1488 const Child = () => <div>Hi!</div>;
1489
1490 await act(() => render(<Grandparent count={2} />));
1491 expect(store).toMatchInlineSnapshot(`
1492 [root]
1493 ▾ <Grandparent>
1494 ▾ <Parent>
1495 <Child key="0">
1496 <Child key="1">
1497 ▾ <Parent>
1498 <Child key="0">
1499 <Child key="1">
1500 `);
1501
1502 const grandparentID = store.getElementIDAtIndex(0);
1503 const parentOneID = store.getElementIDAtIndex(1);
1504 const parentTwoID = store.getElementIDAtIndex(4);
1505
1506 await act(() => store.toggleIsCollapsed(parentOneID, true));
1507 expect(store).toMatchInlineSnapshot(`
1508 [root]
1509 ▾ <Grandparent>
1510 ▸ <Parent>
1511 ▾ <Parent>
1512 <Child key="0">
1513 <Child key="1">
1514 `);
1515
1516 await act(() => store.toggleIsCollapsed(parentTwoID, true));
1517 expect(store).toMatchInlineSnapshot(`
1518 [root]
1519 ▾ <Grandparent>
1520 ▸ <Parent>
1521 ▸ <Parent>
1522 `);
1523
1524 await act(() => store.toggleIsCollapsed(parentOneID, false));
1525 expect(store).toMatchInlineSnapshot(`
1526 [root]
1527 ▾ <Grandparent>
1528 ▾ <Parent>
1529 <Child key="0">
1530 <Child key="1">
1531 ▸ <Parent>
1532 `);
1533
1534 await act(() => store.toggleIsCollapsed(grandparentID, true));
1535 expect(store).toMatchInlineSnapshot(`
1536 [root]
1537 ▸ <Grandparent>
1538 `);
1539
1540 await act(() => store.toggleIsCollapsed(grandparentID, false));
1541 expect(store).toMatchInlineSnapshot(`
1542 [root]
1543 ▾ <Grandparent>
1544 ▾ <Parent>
1545 <Child key="0">
1546 <Child key="1">
1547 ▸ <Parent>
1548 `);
1549 });
1550
1551 // @reactVersion >= 18.0
1552 it('should support reordering of children', async () => {
1553 const Root = ({children}) => children;
1554 const Component = () => null;
1555
1556 const Foo = () => [<Component key="0" />];
1557 const Bar = () => [<Component key="0" />, <Component key="1" />];
1558 const foo = <Foo key="foo" />;
1559 const bar = <Bar key="bar" />;
1560
1561 await act(() => render(<Root>{[foo, bar]}</Root>));
1562 expect(store).toMatchInlineSnapshot(`
1563 [root]
1564 ▾ <Root>
1565 ▾ <Foo key="foo">
1566 <Component key="0">
1567 ▾ <Bar key="bar">
1568 <Component key="0">
1569 <Component key="1">
1570 `);
1571
1572 await act(() => render(<Root>{[bar, foo]}</Root>));
1573 expect(store).toMatchInlineSnapshot(`
1574 [root]
1575 ▾ <Root>
1576 ▾ <Bar key="bar">
1577 <Component key="0">
1578 <Component key="1">
1579 ▾ <Foo key="foo">
1580 <Component key="0">
1581 `);
1582
1583 await act(() =>
1584 store.toggleIsCollapsed(store.getElementIDAtIndex(0), true),
1585 );
1586 expect(store).toMatchInlineSnapshot(`
1587 [root]
1588 ▸ <Root>
1589 `);
1590
1591 await act(() =>
1592 store.toggleIsCollapsed(store.getElementIDAtIndex(0), false),
1593 );
1594 expect(store).toMatchInlineSnapshot(`
1595 [root]
1596 ▾ <Root>
1597 ▾ <Bar key="bar">
1598 <Component key="0">
1599 <Component key="1">
1600 ▾ <Foo key="foo">
1601 <Component key="0">
1602 `);
1603 });
1604 });
1605
1606 describe('collapseNodesByDefault:true', () => {
1607 beforeEach(() => {
1608 store.collapseNodesByDefault = true;
1609 });
1610
1611 // @reactVersion >= 18.0
1612 it('should support mount and update operations', async () => {
1613 const Parent = ({count}) =>
1614 new Array(count).fill(true).map((_, index) => <Child key={index} />);
1615 const Child = () => <div>Hi!</div>;
1616
1617 await act(() =>
1618 render(
1619 <React.Fragment>
1620 <Parent count={1} />
1621 <Parent count={3} />
1622 </React.Fragment>,
1623 ),
1624 );
1625 expect(store).toMatchInlineSnapshot(`
1626 [root]
1627 ▸ <Parent>
1628 ▸ <Parent>
1629 `);
1630
1631 await act(() =>
1632 render(
1633 <React.Fragment>
1634 <Parent count={2} />
1635 <Parent count={1} />
1636 </React.Fragment>,
1637 ),
1638 );
1639 expect(store).toMatchInlineSnapshot(`
1640 [root]
1641 ▸ <Parent>
1642 ▸ <Parent>
1643 `);
1644
1645 await act(() => unmount());
1646 expect(store).toMatchInlineSnapshot(``);
1647 });
1648
1649 // @reactVersion >= 18.0
1650 // @reactVersion < 19
1651 // @gate !disableLegacyMode
1652 it('should support mount and update operations for multiple roots (legacy render)', async () => {
1653 const Parent = ({count}) =>
1654 new Array(count).fill(true).map((_, index) => <Child key={index} />);
1655 const Child = () => <div>Hi!</div>;
1656
1657 const containerA = document.createElement('div');
1658 const containerB = document.createElement('div');
1659
1660 await act(() => {
1661 legacyRender(<Parent key="A" count={3} />, containerA);
1662 legacyRender(<Parent key="B" count={2} />, containerB);
1663 });
1664 expect(store).toMatchInlineSnapshot(`
1665 [root]
1666 ▸ <Parent key="A">
1667 [root]
1668 ▸ <Parent key="B">
1669 `);
1670
1671 await act(() => {
1672 legacyRender(<Parent key="A" count={4} />, containerA);
1673 legacyRender(<Parent key="B" count={1} />, containerB);
1674 });
1675 expect(store).toMatchInlineSnapshot(`
1676 [root]
1677 ▸ <Parent key="A">
1678 [root]
1679 ▸ <Parent key="B">
1680 `);
1681
1682 await act(() => ReactDOM.unmountComponentAtNode(containerB));
1683 expect(store).toMatchInlineSnapshot(`
1684 [root]
1685 ▸ <Parent key="A">
1686 `);
1687
1688 await act(() => ReactDOM.unmountComponentAtNode(containerA));
1689 expect(store).toMatchInlineSnapshot(``);
1690 });
1691
1692 // @reactVersion >= 18.0
1693 it('should support mount and update operations for multiple roots (createRoot)', async () => {
1694 const Parent = ({count}) =>
1695 new Array(count).fill(true).map((_, index) => <Child key={index} />);
1696 const Child = () => <div>Hi!</div>;
1697
1698 const containerA = document.createElement('div');
1699 const containerB = document.createElement('div');
1700
1701 const rootA = ReactDOMClient.createRoot(containerA);
1702 const rootB = ReactDOMClient.createRoot(containerB);
1703
1704 await act(() => {
1705 rootA.render(<Parent key="A" count={3} />);
1706 rootB.render(<Parent key="B" count={2} />);
1707 });
1708 expect(store).toMatchInlineSnapshot(`
1709 [root]
1710 ▸ <Parent key="A">
1711 [root]
1712 ▸ <Parent key="B">
1713 `);
1714
1715 await act(() => {
1716 rootA.render(<Parent key="A" count={4} />);
1717 rootB.render(<Parent key="B" count={1} />);
1718 });
1719 expect(store).toMatchInlineSnapshot(`
1720 [root]
1721 ▸ <Parent key="A">
1722 [root]
1723 ▸ <Parent key="B">
1724 `);
1725
1726 await act(() => rootB.unmount());
1727 expect(store).toMatchInlineSnapshot(`
1728 [root]
1729 ▸ <Parent key="A">
1730 `);
1731
1732 await act(() => rootA.unmount());
1733 expect(store).toMatchInlineSnapshot(``);
1734 });
1735
1736 // @reactVersion >= 18.0
1737 it('should filter DOM nodes from the store tree', async () => {
1738 const Grandparent = () => (
1739 <div>
1740 <div>
1741 <Parent />
1742 </div>
1743 <Parent />
1744 </div>
1745 );
1746 const Parent = () => (
1747 <div>
1748 <Child />
1749 </div>
1750 );
1751 const Child = () => <div>Hi!</div>;
1752
1753 await act(() => render(<Grandparent count={4} />));
1754 expect(store).toMatchInlineSnapshot(`
1755 [root]
1756 ▸ <Grandparent>
1757 `);
1758
1759 await act(() =>
1760 store.toggleIsCollapsed(store.getElementIDAtIndex(0), false),
1761 );
1762 expect(store).toMatchInlineSnapshot(`
1763 [root]
1764 ▾ <Grandparent>
1765 ▸ <Parent>
1766 ▸ <Parent>
1767 `);
1768
1769 await act(() =>
1770 store.toggleIsCollapsed(store.getElementIDAtIndex(1), false),
1771 );
1772 expect(store).toMatchInlineSnapshot(`
1773 [root]
1774 ▾ <Grandparent>
1775 ▾ <Parent>
1776 <Child>
1777 ▸ <Parent>
1778 `);
1779 });
1780
1781 // @reactVersion >= 18.0
1782 it('should display Suspense nodes properly in various states', async () => {
1783 const Loading = () => <div>Loading...</div>;
1784 const never = new Promise(() => {});
1785 const SuspendingComponent = () => {
1786 readValue(never);
1787 };
1788 const Component = () => {
1789 return <div>Hello</div>;
1790 };
1791 const Wrapper = ({shouldSuspense}) => (
1792 <React.Fragment>
1793 <Component key="Outside" />
1794 <React.Suspense fallback={<Loading />}>
1795 {shouldSuspense ? (
1796 <SuspendingComponent />
1797 ) : (
1798 <Component key="Inside" />
1799 )}
1800 </React.Suspense>
1801 </React.Fragment>
1802 );
1803
1804 await act(() => render(<Wrapper shouldSuspense={true} />));
1805 expect(store).toMatchInlineSnapshot(`
1806 [root]
1807 ▸ <Wrapper>
1808 [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
1809 <Suspense name="Wrapper" uniqueSuspenders={true} rects={null}>
1810 `);
1811
1812 // This test isn't meaningful unless we expand the suspended tree
1813 await act(() =>
1814 store.toggleIsCollapsed(store.getElementIDAtIndex(0), false),
1815 );
1816 await act(() =>
1817 store.toggleIsCollapsed(store.getElementIDAtIndex(2), false),
1818 );
1819 expect(store).toMatchInlineSnapshot(`
1820 [root]
1821 ▾ <Wrapper>
1822 <Component key="Outside">
1823 ▾ <Suspense>
1824 <Loading>
1825 [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]}
1826 <Suspense name="Wrapper" uniqueSuspenders={true} rects={null}>
1827 `);
1828
1829 await act(() => {
1830 render(<Wrapper shouldSuspense={false} />);
1831 });
1832 expect(store).toMatchInlineSnapshot(`
1833 [root]
1834 ▾ <Wrapper>
1835 <Component key="Outside">
1836 ▾ <Suspense>
1837 <Component key="Inside">
1838 [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}
1839 <Suspense name="Wrapper" uniqueSuspenders={true} rects={[{x:1,y:2,width:5,height:1}]}>
1840 `);
1841 });
1842
1843 // @reactVersion >= 18.0
1844 it('should support expanding parts of the tree', async () => {
1845 const Grandparent = ({count}) => (
1846 <React.Fragment>
1847 <Parent count={count} />
1848 <Parent count={count} />
1849 </React.Fragment>
1850 );
1851 const Parent = ({count}) =>
1852 new Array(count).fill(true).map((_, index) => <Child key={index} />);
1853 const Child = () => <div>Hi!</div>;
1854
1855 await act(() => render(<Grandparent count={2} />));
1856 expect(store).toMatchInlineSnapshot(`
1857 [root]
1858 ▸ <Grandparent>
1859 `);
1860
1861 const grandparentID = store.getElementIDAtIndex(0);
1862
1863 await act(() => store.toggleIsCollapsed(grandparentID, false));
1864 expect(store).toMatchInlineSnapshot(`
1865 [root]
1866 ▾ <Grandparent>
1867 ▸ <Parent>
1868 ▸ <Parent>
1869 `);
1870
1871 const parentOneID = store.getElementIDAtIndex(1);
1872 const parentTwoID = store.getElementIDAtIndex(2);
1873
1874 await act(() => store.toggleIsCollapsed(parentOneID, false));
1875 expect(store).toMatchInlineSnapshot(`
1876 [root]
1877 ▾ <Grandparent>
1878 ▾ <Parent>
1879 <Child key="0">
1880 <Child key="1">
1881 ▸ <Parent>
1882 `);
1883
1884 await act(() => store.toggleIsCollapsed(parentTwoID, false));
1885 expect(store).toMatchInlineSnapshot(`
1886 [root]
1887 ▾ <Grandparent>
1888 ▾ <Parent>
1889 <Child key="0">
1890 <Child key="1">
1891 ▾ <Parent>
1892 <Child key="0">
1893 <Child key="1">
1894 `);
1895
1896 await act(() => store.toggleIsCollapsed(parentOneID, true));
1897 expect(store).toMatchInlineSnapshot(`
1898 [root]
1899 ▾ <Grandparent>
1900 ▸ <Parent>
1901 ▾ <Parent>
1902 <Child key="0">
1903 <Child key="1">
1904 `);
1905
1906 await act(() => store.toggleIsCollapsed(parentTwoID, true));
1907 expect(store).toMatchInlineSnapshot(`
1908 [root]
1909 ▾ <Grandparent>
1910 ▸ <Parent>
1911 ▸ <Parent>
1912 `);
1913
1914 await act(() => store.toggleIsCollapsed(grandparentID, true));
1915 expect(store).toMatchInlineSnapshot(`
1916 [root]
1917 ▸ <Grandparent>
1918 `);
1919 });
1920
1921 // @reactVersion >= 18.0
1922 it('should support expanding deep parts of the tree', async () => {
1923 const Wrapper = ({forwardedRef}) => (
1924 <Nested depth={3} forwardedRef={forwardedRef} />
1925 );
1926 const Nested = ({depth, forwardedRef}) =>
1927 depth > 0 ? (
1928 <Nested depth={depth - 1} forwardedRef={forwardedRef} />
1929 ) : (
1930 <div ref={forwardedRef} />
1931 );
1932
1933 const ref = React.createRef();
1934
1935 await act(() => render(<Wrapper forwardedRef={ref} />));
1936 expect(store).toMatchInlineSnapshot(`
1937 [root]
1938 ▸ <Wrapper>
1939 `);
1940
1941 const deepestedNodeID = agent.getIDForHostInstance(ref.current).id;
1942
1943 await act(() => store.toggleIsCollapsed(deepestedNodeID, false));
1944 expect(store).toMatchInlineSnapshot(`
1945 [root]
1946 ▾ <Wrapper>
1947 ▾ <Nested>
1948 ▾ <Nested>
1949 ▾ <Nested>
1950 <Nested>
1951 `);
1952
1953 const rootID = store.getElementIDAtIndex(0);
1954
1955 await act(() => store.toggleIsCollapsed(rootID, true));
1956 expect(store).toMatchInlineSnapshot(`
1957 [root]
1958 ▸ <Wrapper>
1959 `);
1960
1961 await act(() => store.toggleIsCollapsed(rootID, false));
1962 expect(store).toMatchInlineSnapshot(`
1963 [root]
1964 ▾ <Wrapper>
1965 ▾ <Nested>
1966 ▾ <Nested>
1967 ▾ <Nested>
1968 <Nested>
1969 `);
1970
1971 const id = store.getElementIDAtIndex(1);
1972
1973 await act(() => store.toggleIsCollapsed(id, true));
1974 expect(store).toMatchInlineSnapshot(`
1975 [root]
1976 ▾ <Wrapper>
1977 ▸ <Nested>
1978 `);
1979
1980 await act(() => store.toggleIsCollapsed(id, false));
1981 expect(store).toMatchInlineSnapshot(`
1982 [root]
1983 ▾ <Wrapper>
1984 ▾ <Nested>
1985 ▾ <Nested>
1986 ▾ <Nested>
1987 <Nested>
1988 `);
1989 });
1990
1991 // @reactVersion >= 18.0
1992 it('should support reordering of children', async () => {
1993 const Root = ({children}) => children;
1994 const Component = () => null;
1995
1996 const Foo = () => [<Component key="0" />];
1997 const Bar = () => [<Component key="0" />, <Component key="1" />];
1998 const foo = <Foo key="foo" />;
1999 const bar = <Bar key="bar" />;
2000
2001 await act(() => render(<Root>{[foo, bar]}</Root>));
2002 expect(store).toMatchInlineSnapshot(`
2003 [root]
2004 ▸ <Root>
2005 `);
2006
2007 await act(() => render(<Root>{[bar, foo]}</Root>));
2008 expect(store).toMatchInlineSnapshot(`
2009 [root]
2010 ▸ <Root>
2011 `);
2012
2013 await act(() =>
2014 store.toggleIsCollapsed(store.getElementIDAtIndex(0), false),
2015 );
2016 expect(store).toMatchInlineSnapshot(`
2017 [root]
2018 ▾ <Root>
2019 ▸ <Bar key="bar">
2020 ▸ <Foo key="foo">
2021 `);
2022
2023 await act(() => {
2024 store.toggleIsCollapsed(store.getElementIDAtIndex(2), false);
2025 store.toggleIsCollapsed(store.getElementIDAtIndex(1), false);
2026 });
2027 expect(store).toMatchInlineSnapshot(`
2028 [root]
2029 ▾ <Root>
2030 ▾ <Bar key="bar">
2031 <Component key="0">
2032 <Component key="1">
2033 ▾ <Foo key="foo">
2034 <Component key="0">
2035 `);
2036
2037 await act(() =>
2038 store.toggleIsCollapsed(store.getElementIDAtIndex(0), true),
2039 );
2040 expect(store).toMatchInlineSnapshot(`
2041 [root]
2042 ▸ <Root>
2043 `);
2044 });
2045
2046 // @reactVersion >= 18.0
2047 it('should not add new nodes when suspense is toggled', async () => {
2048 const SuspenseTree = () => {
2049 return (
2050 <React.Suspense fallback={<Fallback>Loading outer</Fallback>}>
2051 <Parent />
2052 </React.Suspense>
2053 );
2054 };
2055
2056 const Fallback = () => null;
2057 const Parent = () => <Child />;
2058 const Child = () => null;
2059
2060 await act(() => render(<SuspenseTree />));
2061 expect(store).toMatchInlineSnapshot(`
2062 [root]
2063 ▸ <SuspenseTree>
2064 [suspense-root] rects={null}
2065 <Suspense name="SuspenseTree" uniqueSuspenders={false} rects={null}>
2066 `);
2067
2068 await act(() =>
2069 store.toggleIsCollapsed(store.getElementIDAtIndex(0), false),
2070 );
2071 await act(() =>
2072 store.toggleIsCollapsed(store.getElementIDAtIndex(1), false),
2073 );
2074 expect(store).toMatchInlineSnapshot(`
2075 [root]
2076 ▾ <SuspenseTree>
2077 ▾ <Suspense>
2078 ▸ <Parent>
2079 [suspense-root] rects={null}
2080 <Suspense name="SuspenseTree" uniqueSuspenders={false} rects={null}>
2081 `);
2082
2083 const rendererID = getRendererID();
2084 const suspenseID = store.getElementIDAtIndex(1);
2085
2086 await act(() =>
2087 agent.overrideSuspense({
2088 id: suspenseID,
2089 rendererID,
2090 forceFallback: true,
2091 }),
2092 );
2093 expect(store).toMatchInlineSnapshot(`
2094 [root]
2095 ▾ <SuspenseTree>
2096 ▾ <Suspense>
2097 <Fallback>
2098 [suspense-root] rects={null}
2099 <Suspense name="SuspenseTree" uniqueSuspenders={false} rects={null}>
2100 `);
2101
2102 await act(() =>
2103 agent.overrideSuspense({
2104 id: suspenseID,
2105 rendererID,
2106 forceFallback: false,
2107 }),
2108 );
2109 expect(store).toMatchInlineSnapshot(`
2110 [root]
2111 ▾ <SuspenseTree>
2112 ▾ <Suspense>
2113 ▸ <Parent>
2114 [suspense-root] rects={null}
2115 <Suspense name="SuspenseTree" uniqueSuspenders={false} rects={null}>
2116 `);
2117 });
2118 });
2119
2120 describe('getIndexOfElementID', () => {
2121 beforeEach(() => {
2122 store.collapseNodesByDefault = false;
2123 });
2124
2125 // @reactVersion >= 18.0
2126 it('should support a single root with a single child', async () => {
2127 const Grandparent = () => (
2128 <React.Fragment>
2129 <Parent />
2130 <Parent />
2131 </React.Fragment>
2132 );
2133 const Parent = () => <Child />;
2134 const Child = () => null;
2135
2136 await act(() => render(<Grandparent />));
2137
2138 for (let i = 0; i < store.numElements; i++) {
2139 expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i);
2140 }
2141 });
2142
2143 // @reactVersion >= 18.0
2144 it('should support multiple roots with one children each', async () => {
2145 const Grandparent = () => <Parent />;
2146 const Parent = () => <Child />;
2147 const Child = () => null;
2148
2149 await act(() => {
2150 render(<Grandparent />);
2151 render(<Grandparent />);
2152 });
2153
2154 for (let i = 0; i < store.numElements; i++) {
2155 expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i);
2156 }
2157 });
2158
2159 // @reactVersion >= 18.0
2160 it('should support a single root with multiple top level children', async () => {
2161 const Grandparent = () => <Parent />;
2162 const Parent = () => <Child />;
2163 const Child = () => null;
2164
2165 await act(() =>
2166 render(
2167 <React.Fragment>
2168 <Grandparent />
2169 <Grandparent />
2170 </React.Fragment>,
2171 ),
2172 );
2173
2174 for (let i = 0; i < store.numElements; i++) {
2175 expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i);
2176 }
2177 });
2178
2179 // @reactVersion >= 18.0
2180 it('should support multiple roots with multiple top level children', async () => {
2181 const Grandparent = () => <Parent />;
2182 const Parent = () => <Child />;
2183 const Child = () => null;
2184
2185 await act(() => {
2186 render(
2187 <React.Fragment>
2188 <Grandparent />
2189 <Grandparent />
2190 </React.Fragment>,
2191 );
2192
2193 createContainer();
2194
2195 render(
2196 <React.Fragment>
2197 <Grandparent />
2198 <Grandparent />
2199 </React.Fragment>,
2200 );
2201 });
2202
2203 for (let i = 0; i < store.numElements; i++) {
2204 expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i);
2205 }
2206 });
2207 });
2208
2209 // @reactVersion >= 18.0
2210 // @reactVersion < 19
2211 // @gate !disableLegacyMode
2212 it('detects and updates profiling support based on the attached roots (legacy render)', async () => {
2213 const Component = () => null;
2214
2215 const containerA = document.createElement('div');
2216 const containerB = document.createElement('div');
2217
2218 expect(store.rootSupportsBasicProfiling).toBe(false);
2219
2220 await act(() => legacyRender(<Component />, containerA));
2221 expect(store.rootSupportsBasicProfiling).toBe(true);
2222
2223 await act(() => legacyRender(<Component />, containerB));
2224 await act(() => ReactDOM.unmountComponentAtNode(containerA));
2225 expect(store.rootSupportsBasicProfiling).toBe(true);
2226
2227 await act(() => ReactDOM.unmountComponentAtNode(containerB));
2228 expect(store.rootSupportsBasicProfiling).toBe(false);
2229 });
2230
2231 // @reactVersion >= 18
2232 it('detects and updates profiling support based on the attached roots (createRoot)', async () => {
2233 const Component = () => null;
2234
2235 const containerA = document.createElement('div');
2236 const containerB = document.createElement('div');
2237
2238 const rootA = ReactDOMClient.createRoot(containerA);
2239 const rootB = ReactDOMClient.createRoot(containerB);
2240
2241 expect(store.rootSupportsBasicProfiling).toBe(false);
2242
2243 await act(() => rootA.render(<Component />));
2244 expect(store.rootSupportsBasicProfiling).toBe(true);
2245
2246 await act(() => rootB.render(<Component />));
2247 await act(() => rootA.unmount());
2248 expect(store.rootSupportsBasicProfiling).toBe(true);
2249
2250 await act(() => rootB.unmount());
2251 expect(store.rootSupportsBasicProfiling).toBe(false);
2252 });
2253
2254 // @reactVersion >= 18.0
2255 it('should properly serialize non-string key values', async () => {
2256 const Child = () => null;
2257
2258 // Bypass React element's automatic stringifying of keys intentionally.
2259 // This is pretty hacky.
2260 const fauxElement = Object.assign({}, <Child />, {key: 123});
2261
2262 await act(() => render([fauxElement]));
2263 expect(store).toMatchInlineSnapshot(`
2264 [root]
2265 <Child key="123">
2266 `);
2267 });
2268
2269 it('should show the right display names for special component types', async () => {
2270 const MyComponent = (props, ref) => null;
2271 const ForwardRefComponent = React.forwardRef(MyComponent);
2272 const MyComponent2 = (props, ref) => null;
2273 const ForwardRefComponentWithAnonymousFunction = React.forwardRef(() => (
2274 <MyComponent2 />
2275 ));
2276 const MyComponent3 = (props, ref) => null;
2277 const ForwardRefComponentWithCustomDisplayName =
2278 React.forwardRef(MyComponent3);
2279 ForwardRefComponentWithCustomDisplayName.displayName = 'Custom';
2280 const MyComponent4 = (props, ref) => null;
2281 const MemoComponent = React.memo(MyComponent4);
2282 const MemoForwardRefComponent = React.memo(ForwardRefComponent);
2283
2284 const FakeHigherOrderComponent = () => null;
2285 FakeHigherOrderComponent.displayName = 'withFoo(withBar(Baz))';
2286
2287 const MemoizedFakeHigherOrderComponent = React.memo(
2288 FakeHigherOrderComponent,
2289 );
2290 const ForwardRefFakeHigherOrderComponent = React.forwardRef(
2291 FakeHigherOrderComponent,
2292 );
2293
2294 const MemoizedFakeHigherOrderComponentWithDisplayNameOverride = React.memo(
2295 FakeHigherOrderComponent,
2296 );
2297 MemoizedFakeHigherOrderComponentWithDisplayNameOverride.displayName =
2298 'memoRefOverride';
2299 const ForwardRefFakeHigherOrderComponentWithDisplayNameOverride =
2300 React.forwardRef(FakeHigherOrderComponent);
2301 ForwardRefFakeHigherOrderComponentWithDisplayNameOverride.displayName =
2302 'forwardRefOverride';
2303
2304 const App = () => (
2305 <React.Fragment>
2306 <MyComponent />
2307 <ForwardRefComponent />
2308 <ForwardRefComponentWithAnonymousFunction />
2309 <ForwardRefComponentWithCustomDisplayName />
2310 <MemoComponent />
2311 <MemoForwardRefComponent />
2312 <FakeHigherOrderComponent />
2313 <MemoizedFakeHigherOrderComponent />
2314 <ForwardRefFakeHigherOrderComponent />
2315 <MemoizedFakeHigherOrderComponentWithDisplayNameOverride />
2316 <ForwardRefFakeHigherOrderComponentWithDisplayNameOverride />
2317 </React.Fragment>
2318 );
2319
2320 // Render once to start fetching the lazy component
2321 await act(() => render(<App />));
2322
2323 await Promise.resolve();
2324
2325 // Render again after it resolves
2326 await act(() => render(<App />));
2327
2328 expect(store).toMatchInlineSnapshot(`
2329 [root]
2330 ▾ <App>
2331 <MyComponent>
2332 <MyComponent> [ForwardRef]
2333 ▾ <Anonymous> [ForwardRef]
2334 <MyComponent2>
2335 <Custom>
2336 <MyComponent4> [Memo]
2337 ▾ <MyComponent> [Memo]
2338 <MyComponent> [ForwardRef]
2339 <Baz> [withFoo][withBar]
2340 <Baz> [Memo][withFoo][withBar]
2341 <Baz> [ForwardRef][withFoo][withBar]
2342 <memoRefOverride>
2343 <forwardRefOverride>
2344 `);
2345 });
2346
2347 describe('Lazy', () => {
2348 async function fakeImport(result) {
2349 return {default: result};
2350 }
2351
2352 const LazyInnerComponent = () => null;
2353
2354 const App = ({renderChildren}) => {
2355 if (renderChildren) {
2356 return (
2357 <React.Suspense fallback="Loading...">
2358 <LazyComponent />
2359 </React.Suspense>
2360 );
2361 } else {
2362 return null;
2363 }
2364 };
2365
2366 let LazyComponent;
2367 beforeEach(() => {
2368 LazyComponent = React.lazy(() => fakeImport(LazyInnerComponent));
2369 });
2370
2371 // @reactVersion >= 18.0
2372 // @reactVersion < 19
2373 // @gate !disableLegacyMode
2374 it('should support Lazy components (legacy render)', async () => {
2375 const container = document.createElement('div');
2376
2377 // Render once to start fetching the lazy component
2378 await act(() => legacyRender(<App renderChildren={true} />, container));
2379
2380 expect(store).toMatchInlineSnapshot(`
2381 [root]
2382 ▾ <App>
2383 <Suspense>
2384 `);
2385
2386 await Promise.resolve();
2387
2388 // Render again after it resolves
2389 await act(() => legacyRender(<App renderChildren={true} />, container));
2390
2391 expect(store).toMatchInlineSnapshot(`
2392 [root]
2393 ▾ <App>
2394 ▾ <Suspense>
2395 <LazyInnerComponent>
2396 `);
2397
2398 // Render again to unmount it
2399 await act(() => legacyRender(<App renderChildren={false} />, container));
2400
2401 expect(store).toMatchInlineSnapshot(`
2402 [root]
2403 <App>
2404 `);
2405 });
2406
2407 // @reactVersion >= 18.0
2408 it('should support Lazy components in (createRoot)', async () => {
2409 const container = document.createElement('div');
2410 const root = ReactDOMClient.createRoot(container);
2411
2412 // Render once to start fetching the lazy component
2413 await act(() => root.render(<App renderChildren={true} />));
2414
2415 expect(store).toMatchInlineSnapshot(`
2416 [root]
2417 ▾ <App>
2418 <Suspense>
2419 [suspense-root] rects={null}
2420 <Suspense name="App" uniqueSuspenders={true} rects={null}>
2421 `);
2422
2423 await Promise.resolve();
2424
2425 // Render again after it resolves
2426 await act(() => root.render(<App renderChildren={true} />));
2427
2428 expect(store).toMatchInlineSnapshot(`
2429 [root]
2430 ▾ <App>
2431 ▾ <Suspense>
2432 <LazyInnerComponent>
2433 [suspense-root] rects={null}
2434 <Suspense name="App" uniqueSuspenders={true} rects={null}>
2435 `);
2436
2437 // Render again to unmount it
2438 await act(() => root.render(<App renderChildren={false} />));
2439
2440 expect(store).toMatchInlineSnapshot(`
2441 [root]
2442 <App>
2443 `);
2444 });
2445
2446 // @reactVersion >= 18.0
2447 // @reactVersion < 19
2448 // @gate !disableLegacyMode
2449 it('should support Lazy components that are unmounted before they finish loading (legacy render)', async () => {
2450 const container = document.createElement('div');
2451
2452 // Render once to start fetching the lazy component
2453 await act(() => legacyRender(<App renderChildren={true} />, container));
2454
2455 expect(store).toMatchInlineSnapshot(`
2456 [root]
2457 ▾ <App>
2458 <Suspense>
2459 `);
2460
2461 // Render again to unmount it before it finishes loading
2462 await act(() => legacyRender(<App renderChildren={false} />, container));
2463
2464 expect(store).toMatchInlineSnapshot(`
2465 [root]
2466 <App>
2467 `);
2468 });
2469
2470 // @reactVersion >= 18.0
2471 // @reactVersion < 19
2472 it('should support Lazy components that are unmounted before they finish loading in (createRoot)', async () => {
2473 const container = document.createElement('div');
2474 const root = ReactDOMClient.createRoot(container);
2475
2476 // Render once to start fetching the lazy component
2477 await act(() => root.render(<App renderChildren={true} />));
2478
2479 expect(store).toMatchInlineSnapshot(`
2480 [root]
2481 ▾ <App>
2482 <Suspense>
2483 [suspense-root] rects={null}
2484 <Suspense name="App" rects={null}>
2485 `);
2486
2487 // Render again to unmount it before it finishes loading
2488 await act(() => root.render(<App renderChildren={false} />));
2489
2490 expect(store).toMatchInlineSnapshot(`
2491 [root]
2492 <App>
2493 `);
2494 });
2495 });
2496
2497 describe('inline errors and warnings', () => {
2498 // @reactVersion >= 18.0
2499 it('during render are counted', async () => {
2500 function Example() {
2501 console.error('test-only: render error');
2502 console.warn('test-only: render warning');
2503 return null;
2504 }
2505
2506 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2507 await act(() => render(<Example />));
2508 });
2509
2510 expect(store).toMatchInlineSnapshot(`
2511 ✕ 1, ⚠ 1
2512 [root]
2513 <Example> ✕⚠
2514 `);
2515
2516 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2517 await act(() => render(<Example rerender={1} />));
2518 });
2519
2520 expect(store).toMatchInlineSnapshot(`
2521 ✕ 2, ⚠ 2
2522 [root]
2523 <Example> ✕⚠
2524 `);
2525 });
2526
2527 // @reactVersion >= 18.0
2528 it('during layout get counted', async () => {
2529 function Example() {
2530 React.useLayoutEffect(() => {
2531 console.error('test-only: layout error');
2532 console.warn('test-only: layout warning');
2533 });
2534 return null;
2535 }
2536
2537 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2538 await act(() => render(<Example />));
2539 });
2540
2541 expect(store).toMatchInlineSnapshot(`
2542 ✕ 1, ⚠ 1
2543 [root]
2544 <Example> ✕⚠
2545 `);
2546
2547 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2548 await act(() => render(<Example rerender={1} />));
2549 });
2550
2551 expect(store).toMatchInlineSnapshot(`
2552 ✕ 2, ⚠ 2
2553 [root]
2554 <Example> ✕⚠
2555 `);
2556 });
2557
2558 describe('during passive effects', () => {
2559 function flushPendingBridgeOperations() {
2560 jest.runOnlyPendingTimers();
2561 }
2562
2563 // @reactVersion >= 18.0
2564 it('are counted (after no delay)', async () => {
2565 function Example() {
2566 React.useEffect(() => {
2567 console.error('test-only: passive error');
2568 console.warn('test-only: passive warning');
2569 });
2570 return null;
2571 }
2572
2573 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2574 await act(() => {
2575 render(<Example />);
2576 }, false);
2577 });
2578 flushPendingBridgeOperations();
2579 expect(store).toMatchInlineSnapshot(`
2580 ✕ 1, ⚠ 1
2581 [root]
2582 <Example> ✕⚠
2583 `);
2584
2585 await act(() => unmount());
2586 expect(store).toMatchInlineSnapshot(``);
2587 });
2588
2589 // @reactVersion >= 18.0
2590 it('are flushed early when there is a new commit', async () => {
2591 function Example() {
2592 React.useEffect(() => {
2593 console.error('test-only: passive error');
2594 console.warn('test-only: passive warning');
2595 });
2596 return null;
2597 }
2598
2599 function Noop() {
2600 return null;
2601 }
2602
2603 withErrorsOrWarningsIgnored(['test-only:'], () => {
2604 act(() => {
2605 render(
2606 <>
2607 <Example />
2608 </>,
2609 );
2610 }, false);
2611 flushPendingBridgeOperations();
2612 expect(store).toMatchInlineSnapshot(`
2613 ✕ 1, ⚠ 1
2614 [root]
2615 <Example> ✕⚠
2616 `);
2617
2618 // Before warnings and errors have flushed, flush another commit.
2619 act(() => {
2620 render(
2621 <>
2622 <Example />
2623 <Noop />
2624 </>,
2625 );
2626 }, false);
2627 flushPendingBridgeOperations();
2628 expect(store).toMatchInlineSnapshot(`
2629 ✕ 2, ⚠ 2
2630 [root]
2631 <Example> ✕⚠
2632 <Noop>
2633 `);
2634 });
2635
2636 await act(() => unmount());
2637 expect(store).toMatchInlineSnapshot(``);
2638 });
2639 });
2640
2641 // In React 19, JSX warnings were moved into the renderer - https://github.com/facebook/react/pull/29088
2642 // The warning is moved to the Child instead of the Parent.
2643 // @reactVersion >= 19.0.1
2644 it('from react get counted [React >= 19.0.1]', async () => {
2645 function Example() {
2646 return [<Child />];
2647 }
2648 function Child() {
2649 return null;
2650 }
2651
2652 withErrorsOrWarningsIgnored(
2653 ['Each child in a list should have a unique "key" prop'],
2654 () => {
2655 act(() => render(<Example />));
2656 },
2657 );
2658
2659 expect(store).toMatchInlineSnapshot(`
2660 ✕ 1, ⚠ 0
2661 [root]
2662 ▾ <Example>
2663 <Child> ✕
2664 `);
2665 });
2666
2667 // @reactVersion >= 18.0
2668 // @reactVersion < 19.0
2669 it('from react get counted [React 18.x]', async () => {
2670 function Example() {
2671 return [<Child />];
2672 }
2673 function Child() {
2674 return null;
2675 }
2676
2677 withErrorsOrWarningsIgnored(
2678 ['Each child in a list should have a unique "key" prop'],
2679 () => {
2680 act(() => render(<Example />));
2681 },
2682 );
2683
2684 expect(store).toMatchInlineSnapshot(`
2685 ✕ 1, ⚠ 0
2686 [root]
2687 ▾ <Example> ✕
2688 <Child>
2689 `);
2690 });
2691
2692 // @reactVersion >= 18.0
2693 it('can be cleared for the whole app', async () => {
2694 function Example() {
2695 console.error('test-only: render error');
2696 console.warn('test-only: render warning');
2697 return null;
2698 }
2699
2700 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2701 await act(() =>
2702 render(
2703 <React.Fragment>
2704 <Example />
2705 <Example />
2706 </React.Fragment>,
2707 ),
2708 );
2709 });
2710
2711 expect(store).toMatchInlineSnapshot(`
2712 ✕ 2, ⚠ 2
2713 [root]
2714 <Example> ✕⚠
2715 <Example> ✕⚠
2716 `);
2717
2718 const {
2719 clearErrorsAndWarnings,
2720 } = require('react-devtools-shared/src/backendAPI');
2721 clearErrorsAndWarnings({bridge, store});
2722
2723 // flush events to the renderer
2724 jest.runAllTimers();
2725
2726 expect(store).toMatchInlineSnapshot(`
2727 [root]
2728 <Example>
2729 <Example>
2730 `);
2731 });
2732
2733 // @reactVersion >= 18.0
2734 it('can be cleared for particular Fiber (only warnings)', async () => {
2735 function Example() {
2736 console.error('test-only: render error');
2737 console.warn('test-only: render warning');
2738 return null;
2739 }
2740
2741 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2742 await act(() =>
2743 render(
2744 <React.Fragment>
2745 <Example />
2746 <Example />
2747 </React.Fragment>,
2748 ),
2749 );
2750 });
2751
2752 expect(store).toMatchInlineSnapshot(`
2753 ✕ 2, ⚠ 2
2754 [root]
2755 <Example> ✕⚠
2756 <Example> ✕⚠
2757 `);
2758
2759 const id = ((store.getElementIDAtIndex(1): any): number);
2760 const rendererID = store.getRendererIDForElement(id);
2761
2762 const {
2763 clearWarningsForElement,
2764 } = require('react-devtools-shared/src/backendAPI');
2765 clearWarningsForElement({bridge, id, rendererID});
2766
2767 // Flush events to the renderer.
2768 jest.runAllTimers();
2769
2770 expect(store).toMatchInlineSnapshot(`
2771 ✕ 2, ⚠ 1
2772 [root]
2773 <Example> ✕⚠
2774 <Example> ✕
2775 `);
2776 });
2777
2778 // @reactVersion >= 18.0
2779 it('can be cleared for a particular Fiber (only errors)', async () => {
2780 function Example() {
2781 console.error('test-only: render error');
2782 console.warn('test-only: render warning');
2783 return null;
2784 }
2785
2786 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2787 await act(() =>
2788 render(
2789 <React.Fragment>
2790 <Example />
2791 <Example />
2792 </React.Fragment>,
2793 ),
2794 );
2795 });
2796
2797 expect(store).toMatchInlineSnapshot(`
2798 ✕ 2, ⚠ 2
2799 [root]
2800 <Example> ✕⚠
2801 <Example> ✕⚠
2802 `);
2803
2804 const id = ((store.getElementIDAtIndex(1): any): number);
2805 const rendererID = store.getRendererIDForElement(id);
2806
2807 const {
2808 clearErrorsForElement,
2809 } = require('react-devtools-shared/src/backendAPI');
2810 clearErrorsForElement({bridge, id, rendererID});
2811
2812 // Flush events to the renderer.
2813 jest.runAllTimers();
2814
2815 expect(store).toMatchInlineSnapshot(`
2816 ✕ 1, ⚠ 2
2817 [root]
2818 <Example> ✕⚠
2819 <Example> ⚠
2820 `);
2821 });
2822
2823 // @reactVersion >= 18.0
2824 it('are updated when fibers are removed from the tree', async () => {
2825 function ComponentWithWarning() {
2826 console.warn('test-only: render warning');
2827 return null;
2828 }
2829 function ComponentWithError() {
2830 console.error('test-only: render error');
2831 return null;
2832 }
2833 function ComponentWithWarningAndError() {
2834 console.error('test-only: render error');
2835 console.warn('test-only: render warning');
2836 return null;
2837 }
2838
2839 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2840 await act(() =>
2841 render(
2842 <React.Fragment>
2843 <ComponentWithError />
2844 <ComponentWithWarning />
2845 <ComponentWithWarningAndError />
2846 </React.Fragment>,
2847 ),
2848 );
2849 });
2850 expect(store).toMatchInlineSnapshot(`
2851 ✕ 2, ⚠ 2
2852 [root]
2853 <ComponentWithError> ✕
2854 <ComponentWithWarning> ⚠
2855 <ComponentWithWarningAndError> ✕⚠
2856 `);
2857
2858 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2859 await act(() =>
2860 render(
2861 <React.Fragment>
2862 <ComponentWithWarning />
2863 <ComponentWithWarningAndError />
2864 </React.Fragment>,
2865 ),
2866 );
2867 });
2868 expect(store).toMatchInlineSnapshot(`
2869 ✕ 1, ⚠ 2
2870 [root]
2871 <ComponentWithWarning> ⚠
2872 <ComponentWithWarningAndError> ✕⚠
2873 `);
2874
2875 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2876 await act(() =>
2877 render(
2878 <React.Fragment>
2879 <ComponentWithWarning />
2880 </React.Fragment>,
2881 ),
2882 );
2883 });
2884 expect(store).toMatchInlineSnapshot(`
2885 ✕ 0, ⚠ 2
2886 [root]
2887 <ComponentWithWarning> ⚠
2888 `);
2889
2890 withErrorsOrWarningsIgnored(['test-only:'], async () => {
2891 await act(() => render(<React.Fragment />));
2892 });
2893 expect(store).toMatchInlineSnapshot(``);
2894 expect(store.componentWithErrorCount).toBe(0);
2895 expect(store.componentWithWarningCount).toBe(0);
2896 });
2897
2898 // Regression test for https://github.com/facebook/react/issues/23202
2899 // @reactVersion >= 18.0
2900 it('suspense boundary children should not double unmount and error', async () => {
2901 async function fakeImport(result) {
2902 return {default: result};
2903 }
2904
2905 const ChildA = () => null;
2906 const ChildB = () => null;
2907
2908 const LazyChildA = React.lazy(() => fakeImport(ChildA));
2909 const LazyChildB = React.lazy(() => fakeImport(ChildB));
2910
2911 function App({renderA}) {
2912 return (
2913 <React.Suspense>
2914 {renderA ? <LazyChildA /> : <LazyChildB />}
2915 </React.Suspense>
2916 );
2917 }
2918
2919 await actAsync(() => render(<App renderA={true} />));
2920
2921 expect(store).toMatchInlineSnapshot(`
2922 [root]
2923 ▾ <App>
2924 ▾ <Suspense>
2925 <ChildA>
2926 [suspense-root] rects={null}
2927 <Suspense name="App" uniqueSuspenders={true} rects={null}>
2928 `);
2929
2930 await actAsync(() => render(<App renderA={false} />));
2931
2932 expect(store).toMatchInlineSnapshot(`
2933 [root]
2934 ▾ <App>
2935 ▾ <Suspense>
2936 <ChildB>
2937 [suspense-root] rects={null}
2938 <Suspense name="App" uniqueSuspenders={true} rects={null}>
2939 `);
2940 });
2941 });
2942
2943 // @reactVersion > 18.2
2944 it('does not show server components without any children reified children', async () => {
2945 // A Server Component that doesn't render into anything on the client doesn't show up.
2946 const ServerPromise = Promise.resolve(null);
2947 ServerPromise._debugInfo = [
2948 {
2949 name: 'ServerComponent',
2950 env: 'Server',
2951 owner: null,
2952 },
2953 ];
2954 const App = () => ServerPromise;
2955
2956 await actAsync(() => render(<App />));
2957 expect(store).toMatchInlineSnapshot(`
2958 [root]
2959 <App>
2960 `);
2961 });
2962
2963 // @reactVersion > 18.2
2964 it('does show a server component that renders into a filtered node', async () => {
2965 const ServerPromise = Promise.resolve(<div />);
2966 ServerPromise._debugInfo = [
2967 {
2968 name: 'ServerComponent',
2969 env: 'Server',
2970 owner: null,
2971 },
2972 ];
2973 const App = () => ServerPromise;
2974
2975 await actAsync(() => render(<App />));
2976 expect(store).toMatchInlineSnapshot(`
2977 [root]
2978 ▾ <App>
2979 <ServerComponent> [Server]
2980 `);
2981 });
2982
2983 it('can render the same server component twice', async () => {
2984 function ClientComponent() {
2985 return <div />;
2986 }
2987 const ServerPromise = Promise.resolve(<ClientComponent />);
2988 ServerPromise._debugInfo = [
2989 {
2990 name: 'ServerComponent',
2991 env: 'Server',
2992 owner: null,
2993 },
2994 ];
2995 const App = () => (
2996 <>
2997 {ServerPromise}
2998 <ClientComponent />
2999 {ServerPromise}
3000 </>
3001 );
3002
3003 await actAsync(() => render(<App />));
3004 expect(store).toMatchInlineSnapshot(`
3005 [root]
3006 ▾ <App>
3007 ▾ <ServerComponent> [Server]
3008 <ClientComponent>
3009 <ClientComponent>
3010 ▾ <ServerComponent> [Server]
3011 <ClientComponent>
3012 `);
3013 });
3014
3015 // @reactVersion > 18.2
3016 it('collapses multiple parent server components into one', async () => {
3017 function ClientComponent() {
3018 return <div />;
3019 }
3020 const ServerPromise = Promise.resolve(<ClientComponent />);
3021 ServerPromise._debugInfo = [
3022 {
3023 name: 'ServerComponent',
3024 env: 'Server',
3025 owner: null,
3026 },
3027 ];
3028 const ServerPromise2 = Promise.resolve(<ClientComponent />);
3029 ServerPromise2._debugInfo = [
3030 {
3031 name: 'ServerComponent2',
3032 env: 'Server',
3033 owner: null,
3034 },
3035 ];
3036 const App = ({initial}) => (
3037 <>
3038 {ServerPromise}
3039 {ServerPromise}
3040 {ServerPromise2}
3041 {initial ? null : ServerPromise2}
3042 </>
3043 );
3044
3045 await actAsync(() => render(<App initial={true} />));
3046 expect(store).toMatchInlineSnapshot(`
3047 [root]
3048 ▾ <App>
3049 ▾ <ServerComponent> [Server]
3050 <ClientComponent>
3051 <ClientComponent>
3052 ▾ <ServerComponent2> [Server]
3053 <ClientComponent>
3054 `);
3055
3056 await actAsync(() => render(<App initial={false} />));
3057 expect(store).toMatchInlineSnapshot(`
3058 [root]
3059 ▾ <App>
3060 ▾ <ServerComponent> [Server]
3061 <ClientComponent>
3062 <ClientComponent>
3063 ▾ <ServerComponent2> [Server]
3064 <ClientComponent>
3065 <ClientComponent>
3066 `);
3067 });
3068
3069 // @reactVersion > 18.2
3070 it('can reparent a child when the server components change', async () => {
3071 function ClientComponent() {
3072 return <div />;
3073 }
3074 const ServerPromise = Promise.resolve(<ClientComponent />);
3075 ServerPromise._debugInfo = [
3076 {
3077 name: 'ServerAB',
3078 env: 'Server',
3079 owner: null,
3080 },
3081 ];
3082 const ServerPromise2 = Promise.resolve(<ClientComponent />);
3083 ServerPromise2._debugInfo = [
3084 {
3085 name: 'ServerA',
3086 env: 'Server',
3087 owner: null,
3088 },
3089 {
3090 name: 'ServerB',
3091 env: 'Server',
3092 owner: null,
3093 },
3094 ];
3095 const App = ({initial}) => (initial ? ServerPromise : ServerPromise2);
3096
3097 await actAsync(() => render(<App initial={true} />));
3098 expect(store).toMatchInlineSnapshot(`
3099 [root]
3100 ▾ <App>
3101 ▾ <ServerAB> [Server]
3102 <ClientComponent>
3103 `);
3104
3105 await actAsync(() => render(<App initial={false} />));
3106 expect(store).toMatchInlineSnapshot(`
3107 [root]
3108 ▾ <App>
3109 ▾ <ServerA> [Server]
3110 ▾ <ServerB> [Server]
3111 <ClientComponent>
3112 `);
3113 });
3114
3115 // @reactVersion > 18.2
3116 it('splits a server component parent when a different child appears between', async () => {
3117 function ClientComponent() {
3118 return <div />;
3119 }
3120 const ServerPromise = Promise.resolve(<ClientComponent />);
3121 ServerPromise._debugInfo = [
3122 {
3123 name: 'ServerComponent',
3124 env: 'Server',
3125 owner: null,
3126 },
3127 ];
3128 const App = ({initial}) =>
3129 initial ? (
3130 <>
3131 {ServerPromise}
3132 {null}
3133 {ServerPromise}
3134 </>
3135 ) : (
3136 <>
3137 {ServerPromise}
3138 <ClientComponent />
3139 {ServerPromise}
3140 </>
3141 );
3142
3143 await actAsync(() => render(<App initial={true} />));
3144 // Initially the Server Component only appears once because the children
3145 // are consecutive.
3146 expect(store).toMatchInlineSnapshot(`
3147 [root]
3148 ▾ <App>
3149 ▾ <ServerComponent> [Server]
3150 <ClientComponent>
3151 <ClientComponent>
3152 `);
3153
3154 // Later the same instance gets split into two when it is no longer
3155 // consecutive so we need two virtual instances to represent two parents.
3156 await actAsync(() => render(<App initial={false} />));
3157 expect(store).toMatchInlineSnapshot(`
3158 [root]
3159 ▾ <App>
3160 ▾ <ServerComponent> [Server]
3161 <ClientComponent>
3162 <ClientComponent>
3163 ▾ <ServerComponent> [Server]
3164 <ClientComponent>
3165 `);
3166 });
3167
3168 // @reactVersion > 18.2
3169 it('can reorder keyed server components', async () => {
3170 function ClientComponent({text}) {
3171 return <div>{text}</div>;
3172 }
3173 function getServerComponent(key) {
3174 const ServerPromise = Promise.resolve(
3175 <ClientComponent key={key} text={key} />,
3176 );
3177 ServerPromise._debugInfo = [
3178 {
3179 name: 'ServerComponent',
3180 env: 'Server',
3181 owner: null,
3182 key: key,
3183 },
3184 ];
3185 return ServerPromise;
3186 }
3187 const set1 = ['A', 'B', 'C'].map(getServerComponent);
3188 const set2 = ['B', 'A', 'D'].map(getServerComponent);
3189
3190 const App = ({initial}) => (initial ? set1 : set2);
3191
3192 await actAsync(() => render(<App initial={true} />));
3193 expect(store).toMatchInlineSnapshot(`
3194 [root]
3195 ▾ <App>
3196 ▾ <ServerComponent key="A"> [Server]
3197 <ClientComponent key="A">
3198 ▾ <ServerComponent key="B"> [Server]
3199 <ClientComponent key="B">
3200 ▾ <ServerComponent key="C"> [Server]
3201 <ClientComponent key="C">
3202 `);
3203
3204 await actAsync(() => render(<App initial={false} />));
3205 expect(store).toMatchInlineSnapshot(`
3206 [root]
3207 ▾ <App>
3208 ▾ <ServerComponent key="B"> [Server]
3209 <ClientComponent key="B">
3210 ▾ <ServerComponent key="A"> [Server]
3211 <ClientComponent key="A">
3212 ▾ <ServerComponent key="D"> [Server]
3213 <ClientComponent key="D">
3214 `);
3215 });
3216
3217 // @reactVersion >= 19.0
3218 it('does not duplicate Server Component parents in keyed Fragments', async () => {
3219 // TODO: Use an actual Flight renderer.
3220 // See ReactFlight-test for the produced JSX from Flight.
3221 function ClientComponent() {
3222 return null;
3223 }
3224 // This used to be a keyed Fragment on the Server.
3225 const children = [<ClientComponent key="app" />];
3226 children._debugInfo = [
3227 {time: 12},
3228 {
3229 name: 'App',
3230 env: 'Server',
3231 key: null,
3232 stack: ' in Object.<anonymous> (at **)',
3233 props: {},
3234 },
3235 {time: 13},
3236 ];
3237
3238 const container = document.createElement('div');
3239 const root = ReactDOMClient.createRoot(container);
3240 await actAsync(() => {
3241 root.render([children]);
3242 });
3243
3244 expect(store).toMatchInlineSnapshot(`
3245 [root]
3246 ▾ <App> [Server]
3247 <ClientComponent key="app">
3248 `);
3249 });
3250
3251 // @reactVersion >= 17.0
3252 it('can reconcile Suspense in fallback positions', async () => {
3253 let resolveFallback;
3254 const fallbackPromise = new Promise(resolve => {
3255 resolveFallback = resolve;
3256 });
3257 let resolveContent;
3258 const contentPromise = new Promise(resolve => {
3259 resolveContent = resolve;
3260 });
3261
3262 function Component({children, promise}) {
3263 if (promise) {
3264 readValue(promise);
3265 }
3266 return <div>{children}</div>;
3267 }
3268
3269 await actAsync(() =>
3270 render(
3271 <React.Suspense
3272 name="content"
3273 fallback={
3274 <React.Suspense
3275 name="fallback"
3276 fallback={
3277 <Component key="fallback-fallback">
3278 Loading fallback...
3279 </Component>
3280 }>
3281 <Component key="fallback-content" promise={fallbackPromise}>
3282 Loading...
3283 </Component>
3284 </React.Suspense>
3285 }>
3286 <Component key="content" promise={contentPromise}>
3287 done
3288 </Component>
3289 </React.Suspense>,
3290 ),
3291 );
3292
3293 expect(store).toMatchInlineSnapshot(`
3294 [root]
3295 ▾ <Suspense name="content">
3296 ▾ <Suspense name="fallback">
3297 <Component key="fallback-fallback">
3298 [suspense-root] rects={[{x:1,y:2,width:19,height:1}]}
3299 <Suspense name="content" uniqueSuspenders={true} rects={null}>
3300 <Suspense name="fallback" uniqueSuspenders={true} rects={null}>
3301 `);
3302
3303 await actAsync(() => {
3304 resolveFallback();
3305 });
3306
3307 expect(store).toMatchInlineSnapshot(`
3308 [root]
3309 ▾ <Suspense name="content">
3310 ▾ <Suspense name="fallback">
3311 <Component key="fallback-content">
3312 [suspense-root] rects={[{x:1,y:2,width:10,height:1}]}
3313 <Suspense name="content" uniqueSuspenders={true} rects={null}>
3314 <Suspense name="fallback" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}]}>
3315 `);
3316
3317 await actAsync(() => {
3318 resolveContent();
3319 });
3320
3321 expect(store).toMatchInlineSnapshot(`
3322 [root]
3323 ▾ <Suspense name="content">
3324 <Component key="content">
3325 [suspense-root] rects={[{x:1,y:2,width:4,height:1}]}
3326 <Suspense name="content" uniqueSuspenders={true} rects={[{x:1,y:2,width:4,height:1}]}>
3327 `);
3328 });
3329
3330 // @reactVersion >= 17.0
3331 it('can reconcile resuspended Suspense with Suspense in fallback positions', async () => {
3332 let resolveHeadFallback;
3333 let resolveHeadContent;
3334 let resolveMainFallback;
3335 let resolveMainContent;
3336
3337 function Component({children, promise}) {
3338 if (promise) {
3339 readValue(promise);
3340 }
3341 return <div>{children}</div>;
3342 }
3343
3344 function WithSuspenseInFallback({fallbackPromise, contentPromise, name}) {
3345 return (
3346 <React.Suspense
3347 name={name}
3348 fallback={
3349 <React.Suspense
3350 name={`${name}-fallback`}
3351 fallback={
3352 <Component key={`${name}-fallback-fallback`}>
3353 Loading fallback...
3354 </Component>
3355 }>
3356 <Component
3357 key={`${name}-fallback-content`}
3358 promise={fallbackPromise}>
3359 Loading...
3360 </Component>
3361 </React.Suspense>
3362 }>
3363 <Component key={`${name}-content`} promise={contentPromise}>
3364 done
3365 </Component>
3366 </React.Suspense>
3367 );
3368 }
3369
3370 function App({
3371 headFallbackPromise,
3372 headContentPromise,
3373 mainContentPromise,
3374 mainFallbackPromise,
3375 tailContentPromise,
3376 tailFallbackPromise,
3377 }) {
3378 return (
3379 <>
3380 <WithSuspenseInFallback
3381 fallbackPromise={headFallbackPromise}
3382 contentPromise={headContentPromise}
3383 name="head"
3384 />
3385 <WithSuspenseInFallback
3386 fallbackPromise={mainFallbackPromise}
3387 contentPromise={mainContentPromise}
3388 name="main"
3389 />
3390 </>
3391 );
3392 }
3393
3394 const initialHeadContentPromise = new Promise(resolve => {
3395 resolveHeadContent = resolve;
3396 });
3397 const initialHeadFallbackPromise = new Promise(resolve => {
3398 resolveHeadFallback = resolve;
3399 });
3400 const initialMainContentPromise = new Promise(resolve => {
3401 resolveMainContent = resolve;
3402 });
3403 const initialMainFallbackPromise = new Promise(resolve => {
3404 resolveMainFallback = resolve;
3405 });
3406 await actAsync(() =>
3407 render(
3408 <App
3409 headFallbackPromise={initialHeadFallbackPromise}
3410 headContentPromise={initialHeadContentPromise}
3411 mainContentPromise={initialMainContentPromise}
3412 mainFallbackPromise={initialMainFallbackPromise}
3413 />,
3414 ),
3415 );
3416
3417 expect(store).toMatchInlineSnapshot(`
3418 [root]
3419 ▾ <App>
3420 ▾ <WithSuspenseInFallback>
3421 ▾ <Suspense name="head">
3422 ▾ <Suspense name="head-fallback">
3423 <Component key="head-fallback-fallback">
3424 ▾ <WithSuspenseInFallback>
3425 ▾ <Suspense name="main">
3426 ▾ <Suspense name="main-fallback">
3427 <Component key="main-fallback-fallback">
3428 [suspense-root] rects={[{x:1,y:2,width:19,height:1}, {x:1,y:2,width:19,height:1}]}
3429 <Suspense name="head" uniqueSuspenders={true} rects={null}>
3430 <Suspense name="head-fallback" uniqueSuspenders={true} rects={null}>
3431 <Suspense name="main" uniqueSuspenders={true} rects={null}>
3432 <Suspense name="main-fallback" uniqueSuspenders={true} rects={null}>
3433 `);
3434
3435 await actAsync(() => {
3436 resolveHeadFallback();
3437 resolveMainFallback();
3438 resolveHeadContent();
3439 resolveMainContent();
3440 });
3441
3442 expect(store).toMatchInlineSnapshot(`
3443 [root]
3444 ▾ <App>
3445 ▾ <WithSuspenseInFallback>
3446 ▾ <Suspense name="head">
3447 <Component key="head-content">
3448 ▾ <WithSuspenseInFallback>
3449 ▾ <Suspense name="main">
3450 <Component key="main-content">
3451 [suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:4,height:1}]}
3452 <Suspense name="head" uniqueSuspenders={true} rects={[{x:1,y:2,width:4,height:1}]}>
3453 <Suspense name="main" uniqueSuspenders={true} rects={[{x:1,y:2,width:4,height:1}]}>
3454 `);
3455
3456 // Resuspend head content
3457 const nextHeadContentPromise = new Promise(resolve => {
3458 resolveHeadContent = resolve;
3459 });
3460 await actAsync(() =>
3461 render(
3462 <App
3463 headFallbackPromise={initialHeadFallbackPromise}
3464 headContentPromise={nextHeadContentPromise}
3465 mainContentPromise={initialMainContentPromise}
3466 mainFallbackPromise={initialMainFallbackPromise}
3467 />,
3468 ),
3469 );
3470
3471 expect(store).toMatchInlineSnapshot(`
3472 [root]
3473 ▾ <App>
3474 ▾ <WithSuspenseInFallback>
3475 ▾ <Suspense name="head">
3476 ▾ <Suspense name="head-fallback">
3477 <Component key="head-fallback-content">
3478 ▾ <WithSuspenseInFallback>
3479 ▾ <Suspense name="main">
3480 <Component key="main-content">
3481 [suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:4,height:1}]}
3482 <Suspense name="head" uniqueSuspenders={true} rects={[{x:1,y:2,width:4,height:1}]}>
3483 <Suspense name="head-fallback" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}]}>
3484 <Suspense name="main" uniqueSuspenders={true} rects={[{x:1,y:2,width:4,height:1}]}>
3485 `);
3486
3487 // Resuspend head fallback
3488 const nextHeadFallbackPromise = new Promise(resolve => {
3489 resolveHeadFallback = resolve;
3490 });
3491 await actAsync(() =>
3492 render(
3493 <App
3494 headFallbackPromise={nextHeadFallbackPromise}
3495 headContentPromise={nextHeadContentPromise}
3496 mainContentPromise={initialMainContentPromise}
3497 mainFallbackPromise={initialMainFallbackPromise}
3498 />,
3499 ),
3500 );
3501
3502 expect(store).toMatchInlineSnapshot(`
3503 [root]
3504 ▾ <App>
3505 ▾ <WithSuspenseInFallback>
3506 ▾ <Suspense name="head">
3507 ▾ <Suspense name="head-fallback">
3508 <Component key="head-fallback-fallback">
3509 ▾ <WithSuspenseInFallback>
3510 ▾ <Suspense name="main">
3511 <Component key="main-content">
3512 [suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:19,height:1}, {x:1,y:2,width:4,height:1}]}
3513 <Suspense name="head" uniqueSuspenders={true} rects={[{x:1,y:2,width:4,height:1}]}>
3514 <Suspense name="head-fallback" uniqueSuspenders={true} rects={[{x:1,y:2,width:10,height:1}]}>
3515 <Suspense name="main" uniqueSuspenders={true} rects={[{x:1,y:2,width:4,height:1}]}>
3516 `);
3517
3518 await actAsync(() => render(null));
3519
3520 expect(store).toMatchInlineSnapshot(``);
3521 });
3522
3523 it('should handle an empty root', async () => {
3524 await actAsync(() => render(null));
3525 expect(store).toMatchInlineSnapshot(``);
3526
3527 await actAsync(() => render(<span />));
3528 expect(store).toMatchInlineSnapshot(`[root]`);
3529 });
3530
3531 // @reactVersion >= 19.0
3532 it('should reconcile promise-as-a-child', async () => {
3533 function Component({children}) {
3534 return <div>{children}</div>;
3535 }
3536
3537 await actAsync(() =>
3538 render(
3539 <React.Suspense>
3540 {Promise.resolve(<Component key="A">A</Component>)}
3541 </React.Suspense>,
3542 ),
3543 );
3544 expect(store).toMatchInlineSnapshot(`
3545 [root]
3546 ▾ <Suspense>
3547 <Component key="A">
3548 [suspense-root] rects={[{x:1,y:2,width:1,height:1}]}
3549 <Suspense name="Unknown" uniqueSuspenders={true} rects={[{x:1,y:2,width:1,height:1}]}>
3550 `);
3551
3552 await actAsync(() =>
3553 render(
3554 <React.Suspense>
3555 {Promise.resolve(<Component key="not-A">not A</Component>)}
3556 </React.Suspense>,
3557 ),
3558 );
3559
3560 expect(store).toMatchInlineSnapshot(`
3561 [root]
3562 ▾ <Suspense>
3563 <Component key="not-A">
3564 [suspense-root] rects={[{x:1,y:2,width:5,height:1}]}
3565 <Suspense name="Unknown" uniqueSuspenders={true} rects={[{x:1,y:2,width:5,height:1}]}>
3566 `);
3567
3568 await actAsync(() => render(null));
3569 expect(store).toMatchInlineSnapshot(``);
3570 });
3571
3572 // Can't suspend the root in React 17.
3573 // @reactVersion >= 18.0
3574 it('should track suspended-by in filtered fallback suspending the root', async () => {
3575 function IgnoreMe({promise}) {
3576 return readValue(promise);
3577 }
3578
3579 function Component({promise}) {
3580 return readValue(promise);
3581 }
3582
3583 await actAsync(
3584 async () =>
3585 (store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]),
3586 );
3587
3588 let resolveFallback;
3589 const fallbackPromise = new Promise(resolve => {
3590 resolveFallback = resolve;
3591 });
3592 let resolveContent;
3593 const contentPromise = new Promise(resolve => {
3594 resolveContent = resolve;
3595 });
3596
3597 await actAsync(() =>
3598 render(
3599 <React.Suspense
3600 name="main"
3601 fallback={<IgnoreMe promise={fallbackPromise} />}>
3602 <Component promise={contentPromise} />
3603 </React.Suspense>,
3604 ),
3605 );
3606 expect(store).toMatchInlineSnapshot(``);
3607
3608 await actAsync(() => resolveFallback('loading'));
3609 expect(store).toMatchInlineSnapshot(`
3610 [root]
3611 <Suspense name="main">
3612 [suspense-root] rects={null}
3613 <Suspense name="main" uniqueSuspenders={true} rects={null}>
3614 `);
3615
3616 await actAsync(() => resolveContent('content'));
3617 expect(store).toMatchInlineSnapshot(`
3618 [root]
3619 ▾ <Suspense name="main">
3620 <Component>
3621 [suspense-root] rects={null}
3622 <Suspense name="main" uniqueSuspenders={true} rects={null}>
3623 `);
3624 });
3625
3626 // @reactVersion >= 17.0
3627 it('should track suspended-by in filtered fallback', async () => {
3628 function IgnoreMe({promise}) {
3629 return readValue(promise);
3630 }
3631
3632 function Component({promise}) {
3633 if (promise) {
3634 return readValue(promise);
3635 }
3636 return null;
3637 }
3638
3639 await actAsync(
3640 async () =>
3641 (store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]),
3642 );
3643
3644 let resolveFallback;
3645 const fallbackPromise = new Promise(resolve => {
3646 resolveFallback = resolve;
3647 });
3648 let resolveContent;
3649 const contentPromise = new Promise(resolve => {
3650 resolveContent = resolve;
3651 });
3652
3653 await actAsync(() =>
3654 render(
3655 <React.Suspense
3656 fallback={<Component key="root-fallback" />}
3657 name="root">
3658 <React.Suspense
3659 name="main"
3660 fallback={<IgnoreMe promise={fallbackPromise} />}>
3661 <Component promise={contentPromise} />
3662 </React.Suspense>
3663 </React.Suspense>,
3664 ),
3665 );
3666
3667 if (semver.lt(ReactVersionTestingAgainst, '18.0.0')) {
3668 // React 17 commits partial trees hidden which causes the "main"
3669 // Suspense boundary to be included.
3670 // React 18 and upwards excluded partial tree entirely.
3671 expect(store).toMatchInlineSnapshot(`
3672 [root]
3673 ▾ <Suspense name="root">
3674 <Component key="root-fallback">
3675 [suspense-root] rects={null}
3676 <Suspense name="root" rects={null}>
3677 <Suspense name="main" rects={null}>
3678 `);
3679 } else {
3680 expect(store).toMatchInlineSnapshot(`
3681 [root]
3682 ▾ <Suspense name="root">
3683 <Component key="root-fallback">
3684 [suspense-root] rects={null}
3685 <Suspense name="root" uniqueSuspenders={true} rects={null}>
3686 `);
3687 }
3688
3689 await actAsync(() => resolveFallback('loading'));
3690 expect(store).toMatchInlineSnapshot(`
3691 [root]
3692 ▾ <Suspense name="root">
3693 <Suspense name="main">
3694 [suspense-root] rects={null}
3695 <Suspense name="root" uniqueSuspenders={true} rects={null}>
3696 <Suspense name="main" uniqueSuspenders={true} rects={null}>
3697 `);
3698
3699 await actAsync(() => resolveContent('content'));
3700 expect(store).toMatchInlineSnapshot(`
3701 [root]
3702 ▾ <Suspense name="root">
3703 ▾ <Suspense name="main">
3704 <Component>
3705 [suspense-root] rects={null}
3706 <Suspense name="root" uniqueSuspenders={true} rects={null}>
3707 <Suspense name="main" uniqueSuspenders={true} rects={null}>
3708 `);
3709 });
3710
3711 // @reactVersion >= 19
3712 it('should keep suspended boundaries in the Suspense tree but not hidden Activity', async () => {
3713 const Activity = React.Activity || React.unstable_Activity;
3714
3715 const never = new Promise(() => {});
3716 function Never() {
3717 readValue(never);
3718 return null;
3719 }
3720 function Component({children}) {
3721 return <div>{children}</div>;
3722 }
3723
3724 function App({hidden}) {
3725 return (
3726 <>
3727 <Activity mode={hidden ? 'hidden' : 'visible'}>
3728 <React.Suspense name="inside-activity">
3729 <Component key="inside-activity">inside Activity</Component>
3730 </React.Suspense>
3731 </Activity>
3732 <React.Suspense name="outer-suspense">
3733 <React.Suspense name="inner-suspense">
3734 <Component key="inside-suspense">inside Suspense</Component>
3735 </React.Suspense>
3736 {hidden ? <Never /> : null}
3737 </React.Suspense>
3738 </>
3739 );
3740 }
3741
3742 await actAsync(() => {
3743 render(<App hidden={true} />);
3744 });
3745
3746 expect(store).toMatchInlineSnapshot(`
3747 [root]
3748 ▾ <App>
3749 ▸ <Activity mode="hidden">
3750 <Suspense name="outer-suspense">
3751 [suspense-root] rects={[{x:1,y:2,width:15,height:1}]}
3752 <Suspense name="inside-activity" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
3753 <Suspense name="outer-suspense" uniqueSuspenders={true} rects={null}>
3754 `);
3755
3756 // mount as visible
3757 await actAsync(() => {
3758 render(null);
3759 });
3760 await actAsync(() => {
3761 render(<App hidden={false} />);
3762 });
3763
3764 expect(store).toMatchInlineSnapshot(`
3765 [root]
3766 ▾ <App>
3767 ▾ <Activity mode="visible">
3768 ▾ <Suspense name="inside-activity">
3769 <Component key="inside-activity">
3770 ▾ <Suspense name="outer-suspense">
3771 ▾ <Suspense name="inner-suspense">
3772 <Component key="inside-suspense">
3773 [suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
3774 <Suspense name="inside-activity" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
3775 <Suspense name="outer-suspense" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
3776 <Suspense name="inner-suspense" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
3777 `);
3778
3779 await actAsync(() => {
3780 render(<App hidden={true} />);
3781 });
3782
3783 expect(store).toMatchInlineSnapshot(`
3784 [root]
3785 ▾ <App>
3786 ▸ <Activity mode="hidden">
3787 <Suspense name="outer-suspense">
3788 [suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
3789 <Suspense name="inside-activity" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
3790 <Suspense name="outer-suspense" uniqueSuspenders={true} rects={[{x:1,y:2,width:15,height:1}]}>
3791 <Suspense name="inner-suspense" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
3792 `);
3793
3794 await actAsync(() => {
3795 render(<App hidden={false} />);
3796 });
3797
3798 expect(store).toMatchInlineSnapshot(`
3799 [root]
3800 ▾ <App>
3801 ▾ <Activity mode="visible">
3802 ▾ <Suspense name="inside-activity">
3803 <Component key="inside-activity">
3804 ▾ <Suspense name="outer-suspense">
3805 ▾ <Suspense name="inner-suspense">
3806 <Component key="inside-suspense">
3807 [suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
3808 <Suspense name="inside-activity" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
3809 <Suspense name="outer-suspense" uniqueSuspenders={true} rects={[{x:1,y:2,width:15,height:1}]}>
3810 <Suspense name="inner-suspense" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
3811 `);
3812 });
3813
3814 // @reactVersion >= 19.0
3815 it('guesses a Suspense name based on the owner', async () => {
3816 let resolve;
3817 const promise = new Promise(_resolve => {
3818 resolve = _resolve;
3819 });
3820 function Inner() {
3821 return (
3822 <React.Suspense fallback={<p>Loading inner</p>}>
3823 <p>{promise}</p>
3824 </React.Suspense>
3825 );
3826 }
3827
3828 function Outer({children}) {
3829 return (
3830 <React.Suspense fallback={<p>Loading outer</p>}>
3831 <p>{promise}</p>
3832 {children}
3833 </React.Suspense>
3834 );
3835 }
3836
3837 await actAsync(() => {
3838 render(
3839 <Outer>
3840 <Inner />
3841 </Outer>,
3842 );
3843 });
3844
3845 expect(store).toMatchInlineSnapshot(`
3846 [root]
3847 ▾ <Outer>
3848 <Suspense>
3849 [suspense-root] rects={[{x:1,y:2,width:13,height:1}]}
3850 <Suspense name="Outer" uniqueSuspenders={true} rects={null}>
3851 `);
3852
3853 await actAsync(() => {
3854 resolve('loaded');
3855 });
3856
3857 expect(store).toMatchInlineSnapshot(`
3858 [root]
3859 ▾ <Outer>
3860 ▾ <Suspense>
3861 ▾ <Inner>
3862 <Suspense>
3863 [suspense-root] rects={[{x:1,y:2,width:6,height:1}, {x:1,y:2,width:6,height:1}]}
3864 <Suspense name="Outer" uniqueSuspenders={true} rects={[{x:1,y:2,width:6,height:1}, {x:1,y:2,width:6,height:1}]}>
3865 <Suspense name="Inner" uniqueSuspenders={false} rects={[{x:1,y:2,width:6,height:1}]}>
3866 `);
3867 });
3868
3869 // @reactVersion >= 19.0
3870 it('measures rects when reconnecting', async () => {
3871 function Component({children, promise}) {
3872 let content = '';
3873 if (promise) {
3874 const value = readValue(promise);
3875 if (typeof value === 'string') {
3876 content += value;
3877 }
3878 }
3879 return (
3880 <div>
3881 {content}
3882 {children}
3883 </div>
3884 );
3885 }
3886
3887 function App({outer, inner}) {
3888 return (
3889 <React.Suspense
3890 name="outer"
3891 fallback={<Component key="outer-fallback">loading outer</Component>}>
3892 <Component key="outer-content" promise={outer}>
3893 outer content
3894 </Component>
3895 <React.Suspense
3896 name="inner"
3897 fallback={
3898 <Component key="inner-fallback">loading inner</Component>
3899 }>
3900 <Component key="inner-content" promise={inner}>
3901 inner content
3902 </Component>
3903 </React.Suspense>
3904 </React.Suspense>
3905 );
3906 }
3907
3908 await actAsync(() => {
3909 render(<App outer={null} inner={null} />);
3910 });
3911
3912 expect(store).toMatchInlineSnapshot(`
3913 [root]
3914 ▾ <App>
3915 ▾ <Suspense name="outer">
3916 <Component key="outer-content">
3917 ▾ <Suspense name="inner">
3918 <Component key="inner-content">
3919 [suspense-root] rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}
3920 <Suspense name="outer" uniqueSuspenders={false} rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}>
3921 <Suspense name="inner" uniqueSuspenders={false} rects={[{x:1,y:2,width:13,height:1}]}>
3922 `);
3923
3924 let outerResolve;
3925 const outerPromise = new Promise(resolve => {
3926 outerResolve = resolve;
3927 });
3928
3929 let innerResolve;
3930 const innerPromise = new Promise(resolve => {
3931 innerResolve = resolve;
3932 });
3933 await actAsync(() => {
3934 render(<App outer={outerPromise} inner={innerPromise} />);
3935 });
3936
3937 expect(store).toMatchInlineSnapshot(`
3938 [root]
3939 ▾ <App>
3940 ▾ <Suspense name="outer">
3941 <Component key="outer-fallback">
3942 [suspense-root] rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}
3943 <Suspense name="outer" uniqueSuspenders={true} rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}>
3944 <Suspense name="inner" uniqueSuspenders={false} rects={[{x:1,y:2,width:13,height:1}]}>
3945 `);
3946
3947 await actAsync(() => {
3948 outerResolve('..');
3949 innerResolve('.');
3950 });
3951
3952 expect(store).toMatchInlineSnapshot(`
3953 [root]
3954 ▾ <App>
3955 ▾ <Suspense name="outer">
3956 <Component key="outer-content">
3957 ▾ <Suspense name="inner">
3958 <Component key="inner-content">
3959 [suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:14,height:1}]}
3960 <Suspense name="outer" uniqueSuspenders={true} rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:14,height:1}]}>
3961 <Suspense name="inner" uniqueSuspenders={true} rects={[{x:1,y:2,width:14,height:1}]}>
3962 `);
3963 });
3964
3965 // @reactVersion >= 19
3966 it('can reconcile newly visible Activity with filtered, stable children', async () => {
3967 const Activity = React.Activity || React.unstable_Activity;
3968
3969 function IgnoreMe({children}) {
3970 return children;
3971 }
3972
3973 function Component({children}) {
3974 return <div>{children}</div>;
3975 }
3976
3977 await actAsync(
3978 async () =>
3979 (store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]),
3980 );
3981
3982 const children = (
3983 <IgnoreMe>
3984 <Component key="left">Left</Component>
3985 </IgnoreMe>
3986 );
3987
3988 await actAsync(() => {
3989 render(<Activity mode="hidden">{children}</Activity>);
3990 });
3991
3992 expect(store).toMatchInlineSnapshot(`
3993 [root]
3994 ▸ <Activity mode="hidden">
3995 `);
3996
3997 await actAsync(() => {
3998 render(<Activity mode="visible">{children}</Activity>);
3999 });
4000
4001 expect(store).toMatchInlineSnapshot(`
4002 [root]
4003 ▾ <Activity mode="visible">
4004 ▾ <Component key="left">
4005 <div>
4006 `);
4007 });
4008
4009 // @reactVersion >= 17.0
4010 it('continues to consider Suspense boundary as blocking if some child still is suspended on removed io', async () => {
4011 function Component({promise}) {
4012 readValue(promise);
4013 return null;
4014 }
4015
4016 let resolve;
4017 const promise = new Promise(_resolve => {
4018 resolve = _resolve;
4019 });
4020
4021 await actAsync(() => {
4022 render(
4023 <React.Suspense fallback={null} name="outer">
4024 <Component key="one" promise={promise} />
4025 <Component key="two" promise={promise} />
4026 <React.Suspense fallback={null} name="inner">
4027 <Component key="three" promise={promise} />
4028 </React.Suspense>
4029 </React.Suspense>,
4030 );
4031 });
4032
4033 expect(store).toMatchInlineSnapshot(`
4034 [root]
4035 <Suspense name="outer">
4036 [suspense-root] rects={null}
4037 <Suspense name="outer" uniqueSuspenders={true} rects={null}>
4038 `);
4039
4040 await actAsync(() => {
4041 resolve('Hello, World!');
4042 });
4043
4044 expect(store).toMatchInlineSnapshot(`
4045 [root]
4046 ▾ <Suspense name="outer">
4047 <Component key="one">
4048 <Component key="two">
4049 ▾ <Suspense name="inner">
4050 <Component key="three">
4051 [suspense-root] rects={null}
4052 <Suspense name="outer" uniqueSuspenders={true} rects={null}>
4053 <Suspense name="inner" uniqueSuspenders={false} rects={null}>
4054 `);
4055
4056 // We remove one suspender.
4057 // The inner one shouldn't have unique suspenders because it's still blocked
4058 // by the outer one.
4059 await actAsync(() => {
4060 render(
4061 <React.Suspense fallback={null} name="outer">
4062 <Component key="one" promise={promise} />
4063 <React.Suspense fallback={null} name="inner">
4064 <Component key="three" promise={promise} />
4065 </React.Suspense>
4066 </React.Suspense>,
4067 );
4068 });
4069
4070 expect(store).toMatchInlineSnapshot(`
4071 [root]
4072 ▾ <Suspense name="outer">
4073 <Component key="one">
4074 ▾ <Suspense name="inner">
4075 <Component key="three">
4076 [suspense-root] rects={null}
4077 <Suspense name="outer" uniqueSuspenders={true} rects={null}>
4078 <Suspense name="inner" uniqueSuspenders={false} rects={null}>
4079 `);
4080
4081 // Now we remove all unique suspenders of the outer Suspense boundary.
4082 // The inner one is now independently revealed from the parent and should
4083 // be marked as having unique suspenders.
4084 // TODO: The outer boundary no longer has unique suspenders.
4085 await actAsync(() => {
4086 render(
4087 <React.Suspense fallback={null} name="outer">
4088 <React.Suspense fallback={null} name="inner">
4089 <Component key="three" promise={promise} />
4090 </React.Suspense>
4091 </React.Suspense>,
4092 );
4093 });
4094
4095 expect(store).toMatchInlineSnapshot(`
4096 [root]
4097 ▾ <Suspense name="outer">
4098 ▾ <Suspense name="inner">
4099 <Component key="three">
4100 [suspense-root] rects={null}
4101 <Suspense name="outer" uniqueSuspenders={true} rects={null}>
4102 <Suspense name="inner" uniqueSuspenders={true} rects={null}>
4103 `);
4104 });
4105
4106 // @reactVersion >= 19
4107 it('cleans up host hoistables', async () => {
4108 function Left() {
4109 return (
4110 <style href="test.css" precedence="medium">
4111 {'* {color:black}'}
4112 </style>
4113 );
4114 }
4115
4116 function Right() {
4117 return (
4118 <style href="test.css" precedence="medium">
4119 {'* {color:black}'}
4120 </style>
4121 );
4122 }
4123
4124 await actAsync(() => {
4125 render(
4126 <>
4127 <Left />
4128 <Right />
4129 </>,
4130 );
4131 });
4132
4133 // Ensure we're still testing deduplicated hoistables.
4134 expect(document.head.querySelectorAll('style')).toHaveLength(1);
4135 expect(store).toMatchInlineSnapshot(`
4136 [root]
4137 <Left>
4138 <Right>
4139 `);
4140 let style = document.head.querySelector('style');
4141 let styleID = agent.getIDForHostInstance(style).id;
4142 expect(store.containsElement(styleID)).toBe(true);
4143
4144 await actAsync(() => {
4145 render(
4146 <>
4147 <Right />
4148 </>,
4149 );
4150 });
4151
4152 expect(store).toMatchInlineSnapshot(`
4153 [root]
4154 <Right>
4155 `);
4156 style = document.head.querySelector('style');
4157 styleID = agent.getIDForHostInstance(style).id;
4158 expect(store.containsElement(styleID)).toBe(true);
4159 });
4160 });