@samitouri / QOS-React-1 / commits / de06211dbe

[DevTools] Send Suspense rects to frontend (#34170)

Sebastian "Sebbie" Silbermann committed Aug 12, 2025 at 16:48 UTC de06211dbe0ce641a435b52ed3c868720aa9c633
12 files changed +586 -134
packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js
+6
@@ -228,6 +228,8 @@ describe('commit tree', () => {
228 [root]
229 ▾ <App>
230 <Suspense>
231 + [shell]
232 + <Suspense name="App>?" rects={null}>
233 `);
234 utils.act(() => modernRender(<App renderChildren={true} />));
235 expect(store).toMatchInlineSnapshot(`
@@ -235,6 +237,8 @@ describe('commit tree', () => {
237 ▾ <App>
238 ▾ <Suspense>
239 <LazyInnerComponent>
240 + [shell]
241 + <Suspense name="App>?" rects={null}>
242 `);
243 utils.act(() => modernRender(<App renderChildren={false} />));
244 expect(store).toMatchInlineSnapshot(`
@@ -299,6 +303,8 @@ describe('commit tree', () => {
303 [root]
304 ▾ <App>
305 <Suspense>
306 + [shell]
307 + <Suspense name="App>?" rects={null}>
308 `);
309 utils.act(() => modernRender(<App renderChildren={false} />));
310 expect(store).toMatchInlineSnapshot(`
packages/react-devtools-shared/src/__tests__/store-test.js
+173 -58
@@ -24,6 +24,16 @@ describe('Store', () => {
24 let store;
25 let withErrorsOrWarningsIgnored;
26
27 + beforeAll(() => {
28 + // JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes
29 + Element.prototype.getClientRects = function (this: Element) {
30 + const textContent = this.textContent;
31 + return [
32 + new DOMRect(1, 2, textContent.length, textContent.split('\n').length),
33 + ];
34 + };
35 + });
36 +
37 beforeEach(() => {
38 global.IS_REACT_ACT_ENVIRONMENT = true;
39
@@ -123,6 +133,8 @@ describe('Store', () => {
133 <Suspense>
134 ▾ <Parent>
135 <Child>
136 + [shell]
137 + <Suspense name="Unknown" rects={null}>
138 `);
139 });
140
@@ -480,6 +492,8 @@ describe('Store', () => {
492 <Component key="Outside">
493 ▾ <Suspense>
494 <Loading>
495 + [shell]
496 + <Suspense name="Wrapper>?" rects={null}>
497 `);
498
499 await act(() => {
@@ -491,6 +505,8 @@ describe('Store', () => {
505 <Component key="Outside">
506 ▾ <Suspense>
507 <Component key="Inside">
508 + [shell]
509 + <Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
510 `);
511 });
512
@@ -513,23 +529,31 @@ describe('Store', () => {
529 }) => (
530 <React.Fragment>
531 <Component key="Outside" />
516 - <React.Suspense fallback={<Loading key="Parent Fallback" />}>
532 + <React.Suspense
533 + name="parent"
534 + fallback={<Loading key="Parent Fallback" />}>
535 <Component key="Unrelated at Start" />
518 - <React.Suspense fallback={<Loading key="Suspense 1 Fallback" />}>
536 + <React.Suspense
537 + name="one"
538 + fallback={<Loading key="Suspense 1 Fallback" />}>
539 {suspendFirst ? (
540 <Never />
541 ) : (
542 <Component key="Suspense 1 Content" />
543 )}
544 </React.Suspense>
525 - <React.Suspense fallback={<Loading key="Suspense 2 Fallback" />}>
545 + <React.Suspense
546 + name="two"
547 + fallback={<Loading key="Suspense 2 Fallback" />}>
548 {suspendSecond ? (
549 <Never />
550 ) : (
551 <Component key="Suspense 2 Content" />
552 )}
553 </React.Suspense>
532 - <React.Suspense fallback={<Loading key="Suspense 3 Fallback" />}>
554 + <React.Suspense
555 + name="three"
556 + fallback={<Loading key="Suspense 3 Fallback" />}>
557 <Never />
558 </React.Suspense>
559 {suspendParent && <Never />}
@@ -538,7 +562,7 @@ describe('Store', () => {
562 </React.Fragment>
563 );
564
541 - await act(() =>
565 + await actAsync(() =>
566 render(
567 <Wrapper
568 suspendParent={false}
@@ -551,15 +575,20 @@ describe('Store', () => {
575 [root]
576 ▾ <Wrapper>
577 <Component key="Outside">
554 - ▾ <Suspense>
578 + ▾ <Suspense name="parent">
579 <Component key="Unrelated at Start">
556 - ▾ <Suspense>
580 + ▾ <Suspense name="one">
581 <Component key="Suspense 1 Content">
558 - ▾ <Suspense>
582 + ▾ <Suspense name="two">
583 <Component key="Suspense 2 Content">
560 - ▾ <Suspense>
584 + ▾ <Suspense name="three">
585 <Loading key="Suspense 3 Fallback">
586 <Component key="Unrelated at End">
587 + [shell]
588 + <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
589 + <Suspense name="one" rects={null}>
590 + <Suspense name="two" rects={null}>
591 + <Suspense name="three" rects={null}>
592 `);
593 await act(() =>
594 render(
@@ -574,15 +603,20 @@ describe('Store', () => {
603 [root]
604 ▾ <Wrapper>
605 <Component key="Outside">
577 - ▾ <Suspense>
606 + ▾ <Suspense name="parent">
607 <Component key="Unrelated at Start">
579 - ▾ <Suspense>
608 + ▾ <Suspense name="one">
609 <Loading key="Suspense 1 Fallback">
581 - ▾ <Suspense>
610 + ▾ <Suspense name="two">
611 <Component key="Suspense 2 Content">
583 - ▾ <Suspense>
612 + ▾ <Suspense name="three">
613 <Loading key="Suspense 3 Fallback">
614 <Component key="Unrelated at End">
615 + [shell]
616 + <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
617 + <Suspense name="one" rects={null}>
618 + <Suspense name="two" rects={null}>
619 + <Suspense name="three" rects={null}>
620 `);
621 await act(() =>
622 render(
@@ -597,15 +631,20 @@ describe('Store', () => {
631 [root]
632 ▾ <Wrapper>
633 <Component key="Outside">
600 - ▾ <Suspense>
634 + ▾ <Suspense name="parent">
635 <Component key="Unrelated at Start">
602 - ▾ <Suspense>
636 + ▾ <Suspense name="one">
637 <Component key="Suspense 1 Content">
604 - ▾ <Suspense>
638 + ▾ <Suspense name="two">
639 <Loading key="Suspense 2 Fallback">
606 - ▾ <Suspense>
640 + ▾ <Suspense name="three">
641 <Loading key="Suspense 3 Fallback">
642 <Component key="Unrelated at End">
643 + [shell]
644 + <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
645 + <Suspense name="one" rects={null}>
646 + <Suspense name="two" rects={null}>
647 + <Suspense name="three" rects={null}>
648 `);
649 await act(() =>
650 render(
@@ -620,15 +659,20 @@ describe('Store', () => {
659 [root]
660 ▾ <Wrapper>
661 <Component key="Outside">
623 - ▾ <Suspense>
662 + ▾ <Suspense name="parent">
663 <Component key="Unrelated at Start">
625 - ▾ <Suspense>
664 + ▾ <Suspense name="one">
665 <Loading key="Suspense 1 Fallback">
627 - ▾ <Suspense>
666 + ▾ <Suspense name="two">
667 <Component key="Suspense 2 Content">
629 - ▾ <Suspense>
668 + ▾ <Suspense name="three">
669 <Loading key="Suspense 3 Fallback">
670 <Component key="Unrelated at End">
671 + [shell]
672 + <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
673 + <Suspense name="one" rects={null}>
674 + <Suspense name="two" rects={null}>
675 + <Suspense name="three" rects={null}>
676 `);
677 await act(() =>
678 render(
@@ -643,8 +687,13 @@ describe('Store', () => {
687 [root]
688 ▾ <Wrapper>
689 <Component key="Outside">
646 - ▾ <Suspense>
690 + ▾ <Suspense name="parent">
691 <Loading key="Parent Fallback">
692 + [shell]
693 + <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
694 + <Suspense name="one" rects={null}>
695 + <Suspense name="two" rects={null}>
696 + <Suspense name="three" rects={null}>
697 `);
698 await act(() =>
699 render(
@@ -659,15 +708,20 @@ describe('Store', () => {
708 [root]
709 ▾ <Wrapper>
710 <Component key="Outside">
662 - ▾ <Suspense>
711 + ▾ <Suspense name="parent">
712 <Component key="Unrelated at Start">
664 - ▾ <Suspense>
713 + ▾ <Suspense name="one">
714 <Loading key="Suspense 1 Fallback">
666 - ▾ <Suspense>
715 + ▾ <Suspense name="two">
716 <Loading key="Suspense 2 Fallback">
668 - ▾ <Suspense>
717 + ▾ <Suspense name="three">
718 <Loading key="Suspense 3 Fallback">
719 <Component key="Unrelated at End">
720 + [shell]
721 + <Suspense name="parent" 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}]}>
722 + <Suspense name="one" rects={null}>
723 + <Suspense name="two" rects={null}>
724 + <Suspense name="three" rects={null}>
725 `);
726 await act(() =>
727 render(
@@ -682,15 +736,20 @@ describe('Store', () => {
736 [root]
737 ▾ <Wrapper>
738 <Component key="Outside">
685 - ▾ <Suspense>
739 + ▾ <Suspense name="parent">
740 <Component key="Unrelated at Start">
687 - ▾ <Suspense>
741 + ▾ <Suspense name="one">
742 <Component key="Suspense 1 Content">
689 - ▾ <Suspense>
743 + ▾ <Suspense name="two">
744 <Component key="Suspense 2 Content">
691 - ▾ <Suspense>
745 + ▾ <Suspense name="three">
746 <Loading key="Suspense 3 Fallback">
747 <Component key="Unrelated at End">
748 + [shell]
749 + <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
750 + <Suspense name="one" rects={null}>
751 + <Suspense name="two" rects={null}>
752 + <Suspense name="three" rects={null}>
753 `);
754
755 const rendererID = getRendererID();
@@ -705,15 +764,20 @@ describe('Store', () => {
764 [root]
765 ▾ <Wrapper>
766 <Component key="Outside">
708 - ▾ <Suspense>
767 + ▾ <Suspense name="parent">
768 <Component key="Unrelated at Start">
710 - ▾ <Suspense>
769 + ▾ <Suspense name="one">
770 <Loading key="Suspense 1 Fallback">
712 - ▾ <Suspense>
771 + ▾ <Suspense name="two">
772 <Component key="Suspense 2 Content">
714 - ▾ <Suspense>
773 + ▾ <Suspense name="three">
774 <Loading key="Suspense 3 Fallback">
775 <Component key="Unrelated at End">
776 + [shell]
777 + <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
778 + <Suspense name="one" rects={null}>
779 + <Suspense name="two" rects={null}>
780 + <Suspense name="three" rects={null}>
781 `);
782 await act(() =>
783 agent.overrideSuspense({
@@ -726,8 +790,13 @@ describe('Store', () => {
790 [root]
791 ▾ <Wrapper>
792 <Component key="Outside">
729 - ▾ <Suspense>
793 + ▾ <Suspense name="parent">
794 <Loading key="Parent Fallback">
795 + [shell]
796 + <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
797 + <Suspense name="one" rects={null}>
798 + <Suspense name="two" rects={null}>
799 + <Suspense name="three" rects={null}>
800 `);
801 await act(() =>
802 render(
@@ -742,8 +811,13 @@ describe('Store', () => {
811 [root]
812 ▾ <Wrapper>
813 <Component key="Outside">
745 - ▾ <Suspense>
814 + ▾ <Suspense name="parent">
815 <Loading key="Parent Fallback">
816 + [shell]
817 + <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
818 + <Suspense name="one" rects={null}>
819 + <Suspense name="two" rects={null}>
820 + <Suspense name="three" rects={null}>
821 `);
822 await act(() =>
823 agent.overrideSuspense({
@@ -756,15 +830,20 @@ describe('Store', () => {
830 [root]
831 ▾ <Wrapper>
832 <Component key="Outside">
759 - ▾ <Suspense>
833 + ▾ <Suspense name="parent">
834 <Component key="Unrelated at Start">
761 - ▾ <Suspense>
835 + ▾ <Suspense name="one">
836 <Loading key="Suspense 1 Fallback">
763 - ▾ <Suspense>
837 + ▾ <Suspense name="two">
838 <Loading key="Suspense 2 Fallback">
765 - ▾ <Suspense>
839 + ▾ <Suspense name="three">
840 <Loading key="Suspense 3 Fallback">
841 <Component key="Unrelated at End">
842 + [shell]
843 + <Suspense name="parent" 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}]}>
844 + <Suspense name="one" rects={null}>
845 + <Suspense name="two" rects={null}>
846 + <Suspense name="three" rects={null}>
847 `);
848 await act(() =>
849 agent.overrideSuspense({
@@ -777,15 +856,20 @@ describe('Store', () => {
856 [root]
857 ▾ <Wrapper>
858 <Component key="Outside">
780 - ▾ <Suspense>
859 + ▾ <Suspense name="parent">
860 <Component key="Unrelated at Start">
782 - ▾ <Suspense>
861 + ▾ <Suspense name="one">
862 <Loading key="Suspense 1 Fallback">
784 - ▾ <Suspense>
863 + ▾ <Suspense name="two">
864 <Loading key="Suspense 2 Fallback">
786 - ▾ <Suspense>
865 + ▾ <Suspense name="three">
866 <Loading key="Suspense 3 Fallback">
867 <Component key="Unrelated at End">
868 + [shell]
869 + <Suspense name="parent" 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}]}>
870 + <Suspense name="one" rects={null}>
871 + <Suspense name="two" rects={null}>
872 + <Suspense name="three" rects={null}>
873 `);
874 await act(() =>
875 render(
@@ -800,15 +884,20 @@ describe('Store', () => {
884 [root]
885 ▾ <Wrapper>
886 <Component key="Outside">
803 - ▾ <Suspense>
887 + ▾ <Suspense name="parent">
888 <Component key="Unrelated at Start">
805 - ▾ <Suspense>
889 + ▾ <Suspense name="one">
890 <Component key="Suspense 1 Content">
807 - ▾ <Suspense>
891 + ▾ <Suspense name="two">
892 <Component key="Suspense 2 Content">
809 - ▾ <Suspense>
893 + ▾ <Suspense name="three">
894 <Loading key="Suspense 3 Fallback">
895 <Component key="Unrelated at End">
896 + [shell]
897 + <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
898 + <Suspense name="one" rects={null}>
899 + <Suspense name="two" rects={null}>
900 + <Suspense name="three" rects={null}>
901 `);
902 });
903
@@ -848,6 +937,8 @@ describe('Store', () => {
937 <Component key="A">
938 ▾ <Suspense>
939 <Loading>
940 + [shell]
941 + <Suspense name="Wrapper>?" rects={null}>
942 `);
943
944 await act(() => {
@@ -861,6 +952,8 @@ describe('Store', () => {
952 ▾ <Suspense>
953 <Component key="B">
954 <Component key="C">
955 + [shell]
956 + <Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
957 `);
958 });
959
@@ -1197,6 +1290,8 @@ describe('Store', () => {
1290 expect(store).toMatchInlineSnapshot(`
1291 [root]
1292 ▸ <Wrapper>
1293 + [shell]
1294 + <Suspense name="Wrapper>?" rects={null}>
1295 `);
1296
1297 // This test isn't meaningful unless we expand the suspended tree
@@ -1212,6 +1307,8 @@ describe('Store', () => {
1307 <Component key="Outside">
1308 ▾ <Suspense>
1309 <Loading>
1310 + [shell]
1311 + <Suspense name="Wrapper>?" rects={null}>
1312 `);
1313
1314 await act(() => {
@@ -1223,6 +1320,8 @@ describe('Store', () => {
1320 <Component key="Outside">
1321 ▾ <Suspense>
1322 <Component key="Inside">
1323 + [shell]
1324 + <Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
1325 `);
1326 });
1327
@@ -1447,6 +1546,8 @@ describe('Store', () => {
1546 expect(store).toMatchInlineSnapshot(`
1547 [root]
1548 ▸ <SuspenseTree>
1549 + [shell]
1550 + <Suspense name="SuspenseTree>?" rects={null}>
1551 `);
1552
1553 await act(() =>
@@ -1460,6 +1561,8 @@ describe('Store', () => {
1561 ▾ <SuspenseTree>
1562 ▾ <Suspense>
1563 ▸ <Parent>
1564 + [shell]
1565 + <Suspense name="SuspenseTree>?" rects={null}>
1566 `);
1567
1568 const rendererID = getRendererID();
@@ -1477,6 +1580,8 @@ describe('Store', () => {
1580 ▾ <SuspenseTree>
1581 ▾ <Suspense>
1582 <Fallback>
1583 + [shell]
1584 + <Suspense name="SuspenseTree>?" rects={null}>
1585 `);
1586
1587 await act(() =>
@@ -1491,6 +1596,8 @@ describe('Store', () => {
1596 ▾ <SuspenseTree>
1597 ▾ <Suspense>
1598 ▸ <Parent>
1599 + [shell]
1600 + <Suspense name="SuspenseTree>?" rects={null}>
1601 `);
1602 });
1603 });
@@ -1794,6 +1901,8 @@ describe('Store', () => {
1901 [root]
1902 ▾ <App>
1903 <Suspense>
1904 + [shell]
1905 + <Suspense name="App>?" rects={null}>
1906 `);
1907
1908 await Promise.resolve();
@@ -1806,6 +1915,8 @@ describe('Store', () => {
1915 ▾ <App>
1916 ▾ <Suspense>
1917 <LazyInnerComponent>
1918 + [shell]
1919 + <Suspense name="App>?" rects={null}>
1920 `);
1921
1922 // Render again to unmount it
@@ -2291,20 +2402,24 @@ describe('Store', () => {
2402 await actAsync(() => render(<App renderA={true} />));
2403
2404 expect(store).toMatchInlineSnapshot(`
2294 - [root]
2295 - ▾ <App>
2296 - ▾ <Suspense>
2297 - <ChildA>
2298 - `);
2405 + [root]
2406 + ▾ <App>
2407 + ▾ <Suspense>
2408 + <ChildA>
2409 + [shell]
2410 + <Suspense name="App>?" rects={null}>
2411 + `);
2412
2413 await actAsync(() => render(<App renderA={false} />));
2414
2415 expect(store).toMatchInlineSnapshot(`
2303 - [root]
2304 - ▾ <App>
2305 - ▾ <Suspense>
2306 - <ChildB>
2307 - `);
2416 + [root]
2417 + ▾ <App>
2418 + ▾ <Suspense>
2419 + <ChildB>
2420 + [shell]
2421 + <Suspense name="App>?" rects={null}>
2422 + `);
2423 });
2424 });
2425
packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js
+9
@@ -156,6 +156,9 @@ describe('Store component filters', () => {
156 <div>
157 ▾ <Suspense>
158 <div>
159 + [shell]
160 + <Suspense name="Unknown" rects={[]}>
161 + <Suspense name="Unknown" rects={[]}>
162 `);
163
164 await actAsync(
@@ -171,6 +174,9 @@ describe('Store component filters', () => {
174 <div>
175 ▾ <Suspense>
176 <div>
177 + [shell]
178 + <Suspense name="Unknown" rects={[]}>
179 + <Suspense name="Unknown" rects={[]}>
180 `);
181
182 await actAsync(
@@ -186,6 +192,9 @@ describe('Store component filters', () => {
192 <div>
193 ▾ <Suspense>
194 <div>
195 + [shell]
196 + <Suspense name="Unknown" rects={[]}>
197 + <Suspense name="Unknown" rects={[]}>
198 `);
199 });
200
packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js
+47 -51
@@ -32,7 +32,7 @@ describe('StoreStressConcurrent', () => {
32 // this helper with the real thing.
33 actAsync = require('./utils').actAsync;
34
35 - print = require('./__serializers__/storeSerializer').print;
35 + print = require('./__serializers__/storeSerializer').printStore;
36 });
37
38 // This is a stress test for the tree mount/update/unmount traversal.
@@ -67,8 +67,7 @@ describe('StoreStressConcurrent', () => {
67 let container = document.createElement('div');
68 let root = ReactDOMClient.createRoot(container);
69 act(() => root.render(<Parent>{[a, b, c, d, e]}</Parent>));
70 - expect(store).toMatchInlineSnapshot(
71 - `
70 + expect(store).toMatchInlineSnapshot(`
71 [root]
72 ▾ <Parent>
73 <A key="a">
@@ -76,8 +75,7 @@ describe('StoreStressConcurrent', () => {
75 <C key="c">
76 <D key="d">
77 <E key="e">
79 - `,
80 - );
78 + `);
79 expect(container.textContent).toMatch('abcde');
80 const snapshotForABCDE = print(store);
81
@@ -86,8 +84,7 @@ describe('StoreStressConcurrent', () => {
84 act(() => {
85 setShowX(true);
86 });
89 - expect(store).toMatchInlineSnapshot(
90 - `
87 + expect(store).toMatchInlineSnapshot(`
88 [root]
89 ▾ <Parent>
90 <A key="a">
@@ -96,8 +93,7 @@ describe('StoreStressConcurrent', () => {
93 <X>
94 <D key="d">
95 <E key="e">
99 - `,
100 - );
96 + `);
97 expect(container.textContent).toMatch('abxde');
98 const snapshotForABXDE = print(store);
99
@@ -419,7 +415,7 @@ describe('StoreStressConcurrent', () => {
415 ),
416 );
417 // We snapshot each step once so it doesn't regress.d
422 - snapshots.push(print(store));
418 + snapshots.push(print(store, false, null, false));
419 await act(() => root.unmount());
420 expect(print(store)).toBe('');
421 }
@@ -524,7 +520,7 @@ describe('StoreStressConcurrent', () => {
520 </Root>,
521 ),
522 );
527 - expect(print(store)).toEqual(snapshots[i]);
523 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
524 await act(() => root.unmount());
525 expect(print(store)).toBe('');
526 }
@@ -544,7 +540,7 @@ describe('StoreStressConcurrent', () => {
540 </Root>,
541 ),
542 );
547 - expect(print(store)).toEqual(snapshots[i]);
543 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
544 // Re-render with steps[j].
545 await act(() =>
546 root.render(
@@ -556,7 +552,7 @@ describe('StoreStressConcurrent', () => {
552 ),
553 );
554 // Verify the successful transition to steps[j].
559 - expect(print(store)).toEqual(snapshots[j]);
555 + expect(print(store, false, null, false)).toEqual(snapshots[j]);
556 // Check that we can transition back again.
557 await act(() =>
558 root.render(
@@ -567,7 +563,7 @@ describe('StoreStressConcurrent', () => {
563 </Root>,
564 ),
565 );
570 - expect(print(store)).toEqual(snapshots[i]);
566 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
567 // Clean up after every iteration.
568 await act(() => root.unmount());
569 expect(print(store)).toBe('');
@@ -593,7 +589,7 @@ describe('StoreStressConcurrent', () => {
589 </Root>,
590 ),
591 );
596 - expect(print(store)).toEqual(snapshots[i]);
592 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
593 // Re-render with steps[j].
594 await act(() =>
595 root.render(
@@ -609,7 +605,7 @@ describe('StoreStressConcurrent', () => {
605 ),
606 );
607 // Verify the successful transition to steps[j].
612 - expect(print(store)).toEqual(snapshots[j]);
608 + expect(print(store, false, null, false)).toEqual(snapshots[j]);
609 // Check that we can transition back again.
610 await act(() =>
611 root.render(
@@ -624,7 +620,7 @@ describe('StoreStressConcurrent', () => {
620 </Root>,
621 ),
622 );
627 - expect(print(store)).toEqual(snapshots[i]);
623 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
624 // Clean up after every iteration.
625 await act(() => root.unmount());
626 expect(print(store)).toBe('');
@@ -646,7 +642,7 @@ describe('StoreStressConcurrent', () => {
642 </Root>,
643 ),
644 );
649 - expect(print(store)).toEqual(snapshots[i]);
645 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
646 // Re-render with steps[j].
647 await act(() =>
648 root.render(
@@ -662,7 +658,7 @@ describe('StoreStressConcurrent', () => {
658 ),
659 );
660 // Verify the successful transition to steps[j].
665 - expect(print(store)).toEqual(snapshots[j]);
661 + expect(print(store, false, null, false)).toEqual(snapshots[j]);
662 // Check that we can transition back again.
663 await act(() =>
664 root.render(
@@ -673,7 +669,7 @@ describe('StoreStressConcurrent', () => {
669 </Root>,
670 ),
671 );
676 - expect(print(store)).toEqual(snapshots[i]);
672 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
673 // Clean up after every iteration.
674 await act(() => root.unmount());
675 expect(print(store)).toBe('');
@@ -699,7 +695,7 @@ describe('StoreStressConcurrent', () => {
695 </Root>,
696 ),
697 );
702 - expect(print(store)).toEqual(snapshots[i]);
698 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
699 // Re-render with steps[j].
700 await act(() =>
701 root.render(
@@ -711,7 +707,7 @@ describe('StoreStressConcurrent', () => {
707 ),
708 );
709 // Verify the successful transition to steps[j].
714 - expect(print(store)).toEqual(snapshots[j]);
710 + expect(print(store, false, null, false)).toEqual(snapshots[j]);
711 // Check that we can transition back again.
712 await act(() =>
713 root.render(
@@ -726,7 +722,7 @@ describe('StoreStressConcurrent', () => {
722 </Root>,
723 ),
724 );
729 - expect(print(store)).toEqual(snapshots[i]);
725 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
726 // Clean up after every iteration.
727 await act(() => root.unmount());
728 expect(print(store)).toBe('');
@@ -755,7 +751,7 @@ describe('StoreStressConcurrent', () => {
751 const suspenseID = store.getElementIDAtIndex(2);
752
753 // Force fallback.
758 - expect(print(store)).toEqual(snapshots[i]);
754 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
755 await actAsync(async () => {
756 bridge.send('overrideSuspense', {
757 id: suspenseID,
@@ -763,7 +759,7 @@ describe('StoreStressConcurrent', () => {
759 forceFallback: true,
760 });
761 });
766 - expect(print(store)).toEqual(snapshots[j]);
762 + expect(print(store, false, null, false)).toEqual(snapshots[j]);
763
764 // Stop forcing fallback.
765 await actAsync(async () => {
@@ -773,7 +769,7 @@ describe('StoreStressConcurrent', () => {
769 forceFallback: false,
770 });
771 });
776 - expect(print(store)).toEqual(snapshots[i]);
772 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
773
774 // Trigger actual fallback.
775 await act(() =>
@@ -789,7 +785,7 @@ describe('StoreStressConcurrent', () => {
785 </Root>,
786 ),
787 );
792 - expect(print(store)).toEqual(snapshots[j]);
788 + expect(print(store, false, null, false)).toEqual(snapshots[j]);
789
790 // Force fallback while we're in fallback mode.
791 await act(() => {
@@ -800,7 +796,7 @@ describe('StoreStressConcurrent', () => {
796 });
797 });
798 // Keep seeing fallback content.
803 - expect(print(store)).toEqual(snapshots[j]);
799 + expect(print(store, false, null, false)).toEqual(snapshots[j]);
800
801 // Switch to primary mode.
802 await act(() =>
@@ -813,7 +809,7 @@ describe('StoreStressConcurrent', () => {
809 ),
810 );
811 // Fallback is still forced though.
816 - expect(print(store)).toEqual(snapshots[j]);
812 + expect(print(store, false, null, false)).toEqual(snapshots[j]);
813
814 // Stop forcing fallback. This reverts to primary content.
815 await actAsync(async () => {
@@ -824,7 +820,7 @@ describe('StoreStressConcurrent', () => {
820 });
821 });
822 // Now we see primary content.
827 - expect(print(store)).toEqual(snapshots[i]);
823 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
824
825 // Clean up after every iteration.
826 await actAsync(async () => root.unmount());
@@ -910,7 +906,7 @@ describe('StoreStressConcurrent', () => {
906 ),
907 );
908 // We snapshot each step once so it doesn't regress.
913 - snapshots.push(print(store));
909 + snapshots.push(print(store, false, null, false));
910 await act(() => root.unmount());
911 expect(print(store)).toBe('');
912 }
@@ -935,7 +931,7 @@ describe('StoreStressConcurrent', () => {
931 ),
932 );
933 // We snapshot each step once so it doesn't regress.
938 - fallbackSnapshots.push(print(store));
934 + fallbackSnapshots.push(print(store, false, null, false));
935 await act(() => root.unmount());
936 expect(print(store)).toBe('');
937 }
@@ -1065,7 +1061,7 @@ describe('StoreStressConcurrent', () => {
1061 </Root>,
1062 ),
1063 );
1068 - expect(print(store)).toEqual(snapshots[i]);
1064 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
1065 // Re-render with steps[j].
1066 await act(() =>
1067 root.render(
@@ -1079,7 +1075,7 @@ describe('StoreStressConcurrent', () => {
1075 ),
1076 );
1077 // Verify the successful transition to steps[j].
1082 - expect(print(store)).toEqual(snapshots[j]);
1078 + expect(print(store, false, null, false)).toEqual(snapshots[j]);
1079 // Check that we can transition back again.
1080 await act(() =>
1081 root.render(
@@ -1092,7 +1088,7 @@ describe('StoreStressConcurrent', () => {
1088 </Root>,
1089 ),
1090 );
1095 - expect(print(store)).toEqual(snapshots[i]);
1091 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
1092 // Clean up after every iteration.
1093 await act(() => root.unmount());
1094 expect(print(store)).toBe('');
@@ -1121,7 +1117,7 @@ describe('StoreStressConcurrent', () => {
1117 </Root>,
1118 ),
1119 );
1124 - expect(print(store)).toEqual(fallbackSnapshots[i]);
1120 + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
1121 // Re-render with steps[j].
1122 await act(() =>
1123 root.render(
@@ -1140,7 +1136,7 @@ describe('StoreStressConcurrent', () => {
1136 ),
1137 );
1138 // Verify the successful transition to steps[j].
1143 - expect(print(store)).toEqual(fallbackSnapshots[j]);
1139 + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1140 // Check that we can transition back again.
1141 await act(() =>
1142 root.render(
@@ -1158,7 +1154,7 @@ describe('StoreStressConcurrent', () => {
1154 </Root>,
1155 ),
1156 );
1161 - expect(print(store)).toEqual(fallbackSnapshots[i]);
1157 + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
1158 // Clean up after every iteration.
1159 await act(() => root.unmount());
1160 expect(print(store)).toBe('');
@@ -1182,7 +1178,7 @@ describe('StoreStressConcurrent', () => {
1178 </Root>,
1179 ),
1180 );
1185 - expect(print(store)).toEqual(snapshots[i]);
1181 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
1182 // Re-render with steps[j].
1183 await act(() =>
1184 root.render(
@@ -1196,7 +1192,7 @@ describe('StoreStressConcurrent', () => {
1192 ),
1193 );
1194 // Verify the successful transition to steps[j].
1199 - expect(print(store)).toEqual(fallbackSnapshots[j]);
1195 + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1196 // Check that we can transition back again.
1197 await act(() =>
1198 root.render(
@@ -1209,7 +1205,7 @@ describe('StoreStressConcurrent', () => {
1205 </Root>,
1206 ),
1207 );
1212 - expect(print(store)).toEqual(snapshots[i]);
1208 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
1209 // Clean up after every iteration.
1210 await act(() => root.unmount());
1211 expect(print(store)).toBe('');
@@ -1233,7 +1229,7 @@ describe('StoreStressConcurrent', () => {
1229 </Root>,
1230 ),
1231 );
1236 - expect(print(store)).toEqual(fallbackSnapshots[i]);
1232 + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
1233 // Re-render with steps[j].
1234 await act(() =>
1235 root.render(
@@ -1247,7 +1243,7 @@ describe('StoreStressConcurrent', () => {
1243 ),
1244 );
1245 // Verify the successful transition to steps[j].
1250 - expect(print(store)).toEqual(snapshots[j]);
1246 + expect(print(store, false, null, false)).toEqual(snapshots[j]);
1247 // Check that we can transition back again.
1248 await act(() =>
1249 root.render(
@@ -1260,7 +1256,7 @@ describe('StoreStressConcurrent', () => {
1256 </Root>,
1257 ),
1258 );
1263 - expect(print(store)).toEqual(fallbackSnapshots[i]);
1259 + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
1260 // Clean up after every iteration.
1261 await act(() => root.unmount());
1262 expect(print(store)).toBe('');
@@ -1291,7 +1287,7 @@ describe('StoreStressConcurrent', () => {
1287 const suspenseID = store.getElementIDAtIndex(2);
1288
1289 // Force fallback.
1294 - expect(print(store)).toEqual(snapshots[i]);
1290 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
1291 await actAsync(async () => {
1292 bridge.send('overrideSuspense', {
1293 id: suspenseID,
@@ -1299,7 +1295,7 @@ describe('StoreStressConcurrent', () => {
1295 forceFallback: true,
1296 });
1297 });
1302 - expect(print(store)).toEqual(fallbackSnapshots[j]);
1298 + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1299
1300 // Stop forcing fallback.
1301 await actAsync(async () => {
@@ -1309,7 +1305,7 @@ describe('StoreStressConcurrent', () => {
1305 forceFallback: false,
1306 });
1307 });
1312 - expect(print(store)).toEqual(snapshots[i]);
1308 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
1309
1310 // Trigger actual fallback.
1311 await act(() =>
@@ -1323,7 +1319,7 @@ describe('StoreStressConcurrent', () => {
1319 </Root>,
1320 ),
1321 );
1326 - expect(print(store)).toEqual(fallbackSnapshots[j]);
1322 + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1323
1324 // Force fallback while we're in fallback mode.
1325 await act(() => {
@@ -1334,7 +1330,7 @@ describe('StoreStressConcurrent', () => {
1330 });
1331 });
1332 // Keep seeing fallback content.
1337 - expect(print(store)).toEqual(fallbackSnapshots[j]);
1333 + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1334
1335 // Switch to primary mode.
1336 await act(() =>
@@ -1349,7 +1345,7 @@ describe('StoreStressConcurrent', () => {
1345 ),
1346 );
1347 // Fallback is still forced though.
1352 - expect(print(store)).toEqual(fallbackSnapshots[j]);
1348 + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
1349
1350 // Stop forcing fallback. This reverts to primary content.
1351 await actAsync(async () => {
@@ -1360,7 +1356,7 @@ describe('StoreStressConcurrent', () => {
1356 });
1357 });
1358 // Now we see primary content.
1363 - expect(print(store)).toEqual(snapshots[i]);
1359 + expect(print(store, false, null, false)).toEqual(snapshots[i]);
1360
1361 // Clean up after every iteration.
1362 await act(() => root.unmount());
packages/react-devtools-shared/src/__tests__/treeContext-test.js
+31 -13
@@ -1368,6 +1368,9 @@ describe('TreeListContext', () => {
1368 ▾ <Child>
1369 ▾ <Suspense>
1370 <Grandchild>
1371 + [shell]
1372 + <Suspense name="Parent>?" rects={null}>
1373 + <Suspense name="Child>?" rects={null}>
1374 `);
1375
1376 const outerSuspenseID = ((store.getElementIDAtIndex(1): any): number);
@@ -1407,6 +1410,9 @@ describe('TreeListContext', () => {
1410 ▾ <Child>
1411 ▾ <Suspense>
1412 <Grandchild>
1413 + [shell]
1414 + <Suspense name="Parent>?" rects={null}>
1415 + <Suspense name="Child>?" rects={null}>
1416 `);
1417 });
1418 });
@@ -2361,16 +2367,20 @@ describe('TreeListContext', () => {
2367 jest.runAllTimers();
2368
2369 expect(state).toMatchInlineSnapshot(`
2364 - [root]
2365 - <Suspense>
2366 - `);
2370 + [root]
2371 + <Suspense>
2372 + [shell]
2373 + <Suspense name="Unknown" rects={null}>
2374 + `);
2375
2376 selectNextErrorOrWarning();
2377
2378 expect(state).toMatchInlineSnapshot(`
2371 - [root]
2372 - <Suspense>
2373 - `);
2379 + [root]
2380 + <Suspense>
2381 + [shell]
2382 + <Suspense name="Unknown" rects={null}>
2383 + `);
2384 });
2385
2386 it('should properly handle errors/warnings from components that dont mount because of Suspense', async () => {
@@ -2392,9 +2402,11 @@ describe('TreeListContext', () => {
2402 utils.act(() => TestRenderer.create(<Contexts />));
2403
2404 expect(state).toMatchInlineSnapshot(`
2395 - [root]
2396 - <Suspense>
2397 - `);
2405 + [root]
2406 + <Suspense>
2407 + [shell]
2408 + <Suspense name="Unknown" rects={null}>
2409 + `);
2410
2411 await Promise.resolve();
2412 withErrorsOrWarningsIgnored(['test-only:'], () =>
@@ -2414,6 +2426,8 @@ describe('TreeListContext', () => {
2426 ▾ <Suspense>
2427 <Child> ⚠
2428 <Child>
2429 + [shell]
2430 + <Suspense name="Unknown" rects={null}>
2431 `);
2432 });
2433
@@ -2442,6 +2456,8 @@ describe('TreeListContext', () => {
2456 ▾ <Suspense>
2457 ▾ <Fallback>
2458 <Child> ✕
2459 + [shell]
2460 + <Suspense name="Unknown" rects={null}>
2461 `);
2462
2463 await Promise.resolve();
@@ -2456,10 +2472,12 @@ describe('TreeListContext', () => {
2472 );
2473
2474 expect(state).toMatchInlineSnapshot(`
2459 - [root]
2460 - ▾ <Suspense>
2461 - <Child>
2462 - `);
2475 + [root]
2476 + ▾ <Suspense>
2477 + <Child>
2478 + [shell]
2479 + <Suspense name="Unknown" rects={null}>
2480 + `);
2481 });
2482 });
2483
packages/react-devtools-shared/src/backend/fiber/renderer.js
+77 -3
@@ -86,6 +86,7 @@ import {
86 SUSPENSE_TREE_OPERATION_ADD,
87 SUSPENSE_TREE_OPERATION_REMOVE,
88 SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
89 + SUSPENSE_TREE_OPERATION_RESIZE,
90 } from '../../constants';
91 import {inspectHooksOfFiber} from 'react-debug-tools';
92 import {
@@ -2558,6 +2559,20 @@ export function attach(
2559 pushOperation(fiberID);
2560 pushOperation(parentID);
2561 pushOperation(nameStringID);
2562 +
2563 + const rects = suspenseInstance.rects;
2564 + if (rects === null) {
2565 + pushOperation(-1);
2566 + } else {
2567 + pushOperation(rects.length);
2568 + for (let i = 0; i < rects.length; ++i) {
2569 + const rect = rects[i];
2570 + pushOperation(Math.round(rect.x));
2571 + pushOperation(Math.round(rect.y));
2572 + pushOperation(Math.round(rect.width));
2573 + pushOperation(Math.round(rect.height));
2574 + }
2575 + }
2576 }
2577
2578 function recordUnmount(fiberInstance: FiberInstance): void {
@@ -2606,7 +2621,30 @@ export function attach(
2621 }
2622
2623 function recordSuspenseResize(suspenseNode: SuspenseNode): void {
2609 - // TODO: Notify the front end of the change.
2624 + if (__DEBUG__) {
2625 + console.log('recordSuspenseResize()', suspenseNode);
2626 + }
2627 + const fiberInstance = suspenseNode.instance;
2628 + if (fiberInstance.kind !== FIBER_INSTANCE) {
2629 + // TODO: Resizes of filtered Suspense nodes are currently dropped.
2630 + return;
2631 + }
2632 +
2633 + pushOperation(SUSPENSE_TREE_OPERATION_RESIZE);
2634 + pushOperation(fiberInstance.id);
2635 + const rects = suspenseNode.rects;
2636 + if (rects === null) {
2637 + pushOperation(-1);
2638 + } else {
2639 + pushOperation(rects.length);
2640 + for (let i = 0; i < rects.length; ++i) {
2641 + const rect = rects[i];
2642 + pushOperation(Math.round(rect.x));
2643 + pushOperation(Math.round(rect.y));
2644 + pushOperation(Math.round(rect.width));
2645 + pushOperation(Math.round(rect.height));
2646 + }
2647 + }
2648 }
2649
2650 function recordSuspenseUnmount(suspenseInstance: SuspenseNode): void {
@@ -3442,7 +3480,25 @@ export function attach(
3480 // Measure this Suspense node. In general we shouldn't do this until we have
3481 // inserted the new children but since we know this is a FiberInstance we'll
3482 // just use the Fiber anyway.
3445 - newSuspenseNode.rects = measureInstance(newInstance);
3483 + // Fallbacks get attributed to the parent so we only measure if we're
3484 + // showing primary content.
3485 + if (OffscreenComponent === -1) {
3486 + const isTimedOut = fiber.memoizedState !== null;
3487 + if (!isTimedOut) {
3488 + newSuspenseNode.rects = measureInstance(newInstance);
3489 + }
3490 + } else {
3491 + const contentFiber = fiber.child;
3492 + if (contentFiber === null) {
3493 + throw new Error(
3494 + 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3495 + );
3496 + }
3497 + const isTimedOut = fiber.memoizedState !== null;
3498 + if (!isTimedOut) {
3499 + newSuspenseNode.rects = measureInstance(newInstance);
3500 + }
3501 + }
3502 recordSuspenseMount(newSuspenseNode, reconcilingParentSuspenseNode);
3503 }
3504 insertChild(newInstance);
@@ -3476,7 +3532,25 @@ export function attach(
3532 // Measure this Suspense node. In general we shouldn't do this until we have
3533 // inserted the new children but since we know this is a FiberInstance we'll
3534 // just use the Fiber anyway.
3479 - newSuspenseNode.rects = measureInstance(newInstance);
3535 + // Fallbacks get attributed to the parent so we only measure if we're
3536 + // showing primary content.
3537 + if (OffscreenComponent === -1) {
3538 + const isTimedOut = fiber.memoizedState !== null;
3539 + if (!isTimedOut) {
3540 + newSuspenseNode.rects = measureInstance(newInstance);
3541 + }
3542 + } else {
3543 + const contentFiber = fiber.child;
3544 + if (contentFiber === null) {
3545 + throw new Error(
3546 + 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3547 + );
3548 + }
3549 + const isTimedOut = fiber.memoizedState !== null;
3550 + if (!isTimedOut) {
3551 + newSuspenseNode.rects = measureInstance(newInstance);
3552 + }
3553 + }
3554 }
3555 insertChild(newInstance);
3556 if (__DEBUG__) {
packages/react-devtools-shared/src/constants.js
+1
@@ -27,6 +27,7 @@ export const TREE_OPERATION_SET_SUBTREE_MODE = 7;
27 export const SUSPENSE_TREE_OPERATION_ADD = 8;
28 export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
29 export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
30 +export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
31
32 export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
33 export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;
packages/react-devtools-shared/src/devtools/store.js
+74 -2
@@ -23,6 +23,7 @@ import {
23 SUSPENSE_TREE_OPERATION_ADD,
24 SUSPENSE_TREE_OPERATION_REMOVE,
25 SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
26 + SUSPENSE_TREE_OPERATION_RESIZE,
27 } from '../constants';
28 import {ElementTypeRoot} from '../frontend/types';
29 import {
@@ -1418,6 +1419,7 @@ export default class Store extends EventEmitter<{
1419 const id = operations[i + 1];
1420 const parentID = operations[i + 2];
1421 const nameStringID = operations[i + 3];
1422 + const numRects = ((operations[i + 4]: any): number);
1423 let name = stringTable[nameStringID];
1424
1425 if (this._idToSuspense.has(id)) {
@@ -1448,6 +1450,22 @@ export default class Store extends EventEmitter<{
1450 }
1451 }
1452
1453 + i += 5;
1454 + let rects: SuspenseNode['rects'];
1455 + if (numRects === -1) {
1456 + rects = null;
1457 + } else {
1458 + rects = [];
1459 + for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
1460 + const x = operations[i + 0];
1461 + const y = operations[i + 1];
1462 + const width = operations[i + 2];
1463 + const height = operations[i + 3];
1464 + rects.push({x, y, width, height});
1465 + i += 4;
1466 + }
1467 + }
1468 +
1469 if (__DEBUG__) {
1470 debug('Suspense Add', `node ${id} as child of ${parentID}`);
1471 }
@@ -1476,10 +1494,9 @@ export default class Store extends EventEmitter<{
1494 parentID,
1495 children: [],
1496 name,
1497 + rects,
1498 });
1499
1481 - i += 4;
1482 -
1500 hasSuspenseTreeChanged = true;
1501 break;
1502 }
@@ -1591,6 +1608,61 @@ export default class Store extends EventEmitter<{
1608 hasSuspenseTreeChanged = true;
1609 break;
1610 }
1611 + case SUSPENSE_TREE_OPERATION_RESIZE: {
1612 + const id = ((operations[i + 1]: any): number);
1613 + const numRects = ((operations[i + 2]: any): number);
1614 + i += 3;
1615 +
1616 + const suspense = this._idToSuspense.get(id);
1617 + if (suspense === undefined) {
1618 + this._throwAndEmitError(
1619 + Error(
1620 + `Cannot set rects for suspense node "${id}" because no matching node was found in the Store.`,
1621 + ),
1622 + );
1623 +
1624 + break;
1625 + }
1626 +
1627 + let nextRects: SuspenseNode['rects'];
1628 + if (numRects === -1) {
1629 + nextRects = null;
1630 + } else {
1631 + nextRects = [];
1632 + for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
1633 + const x = operations[i + 0];
1634 + const y = operations[i + 1];
1635 + const width = operations[i + 2];
1636 + const height = operations[i + 3];
1637 +
1638 + nextRects.push({x, y, width, height});
1639 +
1640 + i += 4;
1641 + }
1642 + }
1643 +
1644 + suspense.rects = nextRects;
1645 +
1646 + if (__DEBUG__) {
1647 + debug(
1648 + 'Resize',
1649 + `Suspense node ${id} resize to ${
1650 + nextRects === null
1651 + ? 'null'
1652 + : nextRects
1653 + .map(
1654 + rect =>
1655 + `(${rect.x},${rect.y},${rect.width},${rect.height})`,
1656 + )
1657 + .join(',')
1658 + }`,
1659 + );
1660 + }
1661 +
1662 + hasSuspenseTreeChanged = true;
1663 +
1664 + break;
1665 + }
1666 default:
1667 this._throwAndEmitError(
1668 new UnsupportedBridgeOperationError(
packages/react-devtools-shared/src/devtools/utils.js
+69 -2
@@ -10,7 +10,10 @@
10 import JSON5 from 'json5';
11
12 import type {ReactFunctionLocation} from 'shared/ReactTypes';
13 -import type {Element} from 'react-devtools-shared/src/frontend/types';
13 +import type {
14 + Element,
15 + SuspenseNode,
16 +} from 'react-devtools-shared/src/frontend/types';
17 import type {StateContext} from './views/Components/TreeContext';
18 import type Store from './store';
19
@@ -28,6 +31,11 @@ export function printElement(
31 key = ` key="${element.key}"`;
32 }
33
34 + let name = '';
35 + if (element.nameProp !== null) {
36 + name = ` name="${element.nameProp}"`;
37 + }
38 +
39 let hocDisplayNames = null;
40 if (element.hocDisplayNames !== null) {
41 hocDisplayNames = [...element.hocDisplayNames];
@@ -43,7 +51,45 @@ export function printElement(
51
52 return `${' '.repeat(element.depth + 1)}${prefix} <${
53 element.displayName || 'null'
46 - }${key}>${hocs}${suffix}`;
54 + }${key}${name}>${hocs}${suffix}`;
55 +}
56 +
57 +function printSuspense(
58 + suspense: SuspenseNode,
59 + includeWeight: boolean = false,
60 +): string {
61 + let name = '';
62 + if (suspense.name !== null) {
63 + name = ` name="${suspense.name}"`;
64 + }
65 +
66 + let printedRects = '';
67 + const rects = suspense.rects;
68 + if (rects === null) {
69 + printedRects = ' rects={null}';
70 + } else {
71 + printedRects = ` rects={[${rects.map(rect => `{x:${rect.x},y:${rect.y},width:${rect.width},height:${rect.height}}`).join(', ')}]}`;
72 + }
73 +
74 + return `<Suspense${name}${printedRects}>`;
75 +}
76 +
77 +function printSuspenseWithChildren(
78 + store: Store,
79 + suspense: SuspenseNode,
80 + depth: number,
81 +): Array<string> {
82 + const lines = [' '.repeat(depth) + printSuspense(suspense)];
83 + for (let i = 0; i < suspense.children.length; i++) {
84 + const childID = suspense.children[i];
85 + const child = store.getSuspenseByID(childID);
86 + if (child === null) {
87 + throw new Error(`Could not find Suspense node with ID "${childID}".`);
88 + }
89 + lines.push(...printSuspenseWithChildren(store, child, depth + 1));
90 + }
91 +
92 + return lines;
93 }
94
95 export function printOwnersList(
@@ -59,6 +105,7 @@ export function printStore(
105 store: Store,
106 includeWeight: boolean = false,
107 state: StateContext | null = null,
108 + includeSuspense: boolean = true,
109 ): string {
110 const snapshotLines = [];
111
@@ -129,6 +176,26 @@ export function printStore(
176 }
177
178 rootWeight += weight;
179 +
180 + if (includeSuspense) {
181 + const shell = store.getSuspenseByID(rootID);
182 + // Roots from legacy renderers don't have a separate Suspense tree
183 + if (shell !== null) {
184 + if (shell.children.length > 0) {
185 + snapshotLines.push('[shell]');
186 + for (let i = 0; i < shell.children.length; i++) {
187 + const childID = shell.children[i];
188 + const child = store.getSuspenseByID(childID);
189 + if (child === null) {
190 + throw new Error(
191 + `Could not find Suspense node with ID "${childID}".`,
192 + );
193 + }
194 + snapshotLines.push(...printSuspenseWithChildren(store, child, 1));
195 + }
196 + }
197 + }
198 + }
199 });
200
201 // Make sure the pretty-printed test align with the Store's reported number of total rows.
packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js
+38 -3
@@ -19,6 +19,7 @@ import {
19 SUSPENSE_TREE_OPERATION_ADD,
20 SUSPENSE_TREE_OPERATION_REMOVE,
21 SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
22 + SUSPENSE_TREE_OPERATION_RESIZE,
23 } from 'react-devtools-shared/src/constants';
24 import {
25 parseElementDisplayNameFromBackend,
@@ -376,16 +377,26 @@ function updateTree(
377 const fiberID = operations[i + 1];
378 const parentID = operations[i + 2];
379 const nameStringID = operations[i + 3];
380 + const numRects = operations[i + 4];
381 const name = stringTable[nameStringID];
382
381 - i += 4;
382 -
383 if (__DEBUG__) {
384 + let rects: string;
385 + if (numRects === -1) {
386 + rects = 'null';
387 + } else {
388 + rects =
389 + '[' +
390 + operations.slice(i + 5, i + 5 + numRects * 4).join(',') +
391 + ']';
392 + }
393 debug(
394 'Add suspense',
386 - `node ${fiberID} (${String(name)}) under ${parentID}`,
395 + `node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID}`,
396 );
397 }
398 +
399 + i += 5 + (numRects === -1 ? 0 : numRects * 4);
400 break;
401 }
402
@@ -416,6 +427,30 @@ function updateTree(
427 break;
428 }
429
430 + case SUSPENSE_TREE_OPERATION_RESIZE: {
431 + const suspenseID = ((operations[i + 1]: any): number);
432 + const numRects = ((operations[i + 2]: any): number);
433 +
434 + if (__DEBUG__) {
435 + if (numRects === -1) {
436 + debug('Suspense resize', `suspense ${suspenseID} rects null`);
437 + } else {
438 + const rects = ((operations.slice(
439 + i + 3,
440 + i + 3 + numRects * 4,
441 + ): any): Array<number>);
442 + debug(
443 + 'Suspense resize',
444 + `suspense ${suspenseID} rects [${rects.join(',')}]`,
445 + );
446 + }
447 + }
448 +
449 + i += 3 + (numRects === -1 ? 0 : numRects * 4);
450 +
451 + break;
452 + }
453 +
454 default:
455 throw Error(`Unsupported Bridge operation "${operation}"`);
456 }
packages/react-devtools-shared/src/frontend/types.js
+8
@@ -185,11 +185,19 @@ export type Element = {
185 compiledWithForget: boolean,
186 };
187
188 +export type Rect = {
189 + x: number,
190 + y: number,
191 + width: number,
192 + height: number,
193 +};
194 +
195 export type SuspenseNode = {
196 id: Element['id'],
197 parentID: SuspenseNode['id'] | 0,
198 children: Array<SuspenseNode['id']>,
199 name: string | null,
200 + rects: null | Array<Rect>,
201 };
202
203 // Serialized version of ReactIOInfo
packages/react-devtools-shared/src/utils.js
+53 -2
@@ -43,6 +43,7 @@ import {
43 SUSPENSE_TREE_OPERATION_ADD,
44 SUSPENSE_TREE_OPERATION_REMOVE,
45 SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
46 + SUSPENSE_TREE_OPERATION_RESIZE,
47 } from './constants';
48 import {
49 ComponentFilterElementType,
@@ -339,11 +340,34 @@ export function printOperationsArray(operations: Array<number>) {
340 const parentID = operations[i + 2];
341 const nameStringID = operations[i + 3];
342 const name = stringTable[nameStringID];
343 + const numRects = operations[i + 4];
344
343 - i += 4;
345 + i += 5;
346 +
347 + let rects: string;
348 + if (numRects === -1) {
349 + rects = 'null';
350 + } else {
351 + rects = '[';
352 + for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
353 + const offset = i + rectIndex * 4;
354 + const x = operations[offset + 0];
355 + const y = operations[offset + 1];
356 + const width = operations[offset + 2];
357 + const height = operations[offset + 3];
358 +
359 + if (rectIndex > 0) {
360 + rects += ', ';
361 + }
362 + rects += `(${x}, ${y}, ${width}, ${height})`;
363 +
364 + i += 4;
365 + }
366 + rects += ']';
367 + }
368
369 logs.push(
346 - `Add suspense node ${fiberID} (${String(name)}) under ${parentID}`,
370 + `Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID}`,
371 );
372 break;
373 }
@@ -372,6 +396,33 @@ export function printOperationsArray(operations: Array<number>) {
396 );
397 break;
398 }
399 + case SUSPENSE_TREE_OPERATION_RESIZE: {
400 + const id = ((operations[i + 1]: any): number);
401 + const numRects = ((operations[i + 2]: any): number);
402 + i += 3;
403 +
404 + if (numRects === -1) {
405 + logs.push(`Resize suspense node ${id} to null`);
406 + } else {
407 + let line = `Resize suspense node ${id} to [`;
408 + for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
409 + const x = operations[i + 0];
410 + const y = operations[i + 1];
411 + const width = operations[i + 2];
412 + const height = operations[i + 3];
413 +
414 + if (rectIndex > 0) {
415 + line += ', ';
416 + }
417 + line += `(${x}, ${y}, ${width}, ${height})`;
418 +
419 + i += 4;
420 + }
421 + logs.push(line + ']');
422 + }
423 +
424 + break;
425 + }
426 default:
427 throw Error(`Unsupported Bridge operation "${operation}"`);
428 }