diff --git a/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js b/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js index f5b7e5fded..a7c0893060 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js @@ -228,6 +228,8 @@ describe('commit tree', () => { [root] ▾ + [shell] + `); utils.act(() => modernRender()); expect(store).toMatchInlineSnapshot(` @@ -235,6 +237,8 @@ describe('commit tree', () => { ▾ + [shell] + `); utils.act(() => modernRender()); expect(store).toMatchInlineSnapshot(` @@ -299,6 +303,8 @@ describe('commit tree', () => { [root] ▾ + [shell] + `); utils.act(() => modernRender()); expect(store).toMatchInlineSnapshot(` diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index 1a5a0e6a26..87524ffd04 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -24,6 +24,16 @@ describe('Store', () => { let store; let withErrorsOrWarningsIgnored; + beforeAll(() => { + // JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes + Element.prototype.getClientRects = function (this: Element) { + const textContent = this.textContent; + return [ + new DOMRect(1, 2, textContent.length, textContent.split('\n').length), + ]; + }; + }); + beforeEach(() => { global.IS_REACT_ACT_ENVIRONMENT = true; @@ -123,6 +133,8 @@ describe('Store', () => { + [shell] + `); }); @@ -480,6 +492,8 @@ describe('Store', () => { + [shell] + `); await act(() => { @@ -491,6 +505,8 @@ describe('Store', () => { + [shell] + `); }); @@ -513,23 +529,31 @@ describe('Store', () => { }) => ( - }> + }> - }> + }> {suspendFirst ? ( ) : ( )} - }> + }> {suspendSecond ? ( ) : ( )} - }> + }> {suspendParent && } @@ -538,7 +562,7 @@ describe('Store', () => { ); - await act(() => + await actAsync(() => render( { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); await act(() => render( @@ -574,15 +603,20 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); await act(() => render( @@ -597,15 +631,20 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); await act(() => render( @@ -620,15 +659,20 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); await act(() => render( @@ -643,8 +687,13 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ + [shell] + + + + `); await act(() => render( @@ -659,15 +708,20 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); await act(() => render( @@ -682,15 +736,20 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); const rendererID = getRendererID(); @@ -705,15 +764,20 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); await act(() => agent.overrideSuspense({ @@ -726,8 +790,13 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ + [shell] + + + + `); await act(() => render( @@ -742,8 +811,13 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ + [shell] + + + + `); await act(() => agent.overrideSuspense({ @@ -756,15 +830,20 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); await act(() => agent.overrideSuspense({ @@ -777,15 +856,20 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); await act(() => render( @@ -800,15 +884,20 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); }); @@ -848,6 +937,8 @@ describe('Store', () => { + [shell] + `); await act(() => { @@ -861,6 +952,8 @@ describe('Store', () => { ▾ + [shell] + `); }); @@ -1197,6 +1290,8 @@ describe('Store', () => { expect(store).toMatchInlineSnapshot(` [root] ▸ + [shell] + `); // This test isn't meaningful unless we expand the suspended tree @@ -1212,6 +1307,8 @@ describe('Store', () => { + [shell] + `); await act(() => { @@ -1223,6 +1320,8 @@ describe('Store', () => { + [shell] + `); }); @@ -1447,6 +1546,8 @@ describe('Store', () => { expect(store).toMatchInlineSnapshot(` [root] ▸ + [shell] + `); await act(() => @@ -1460,6 +1561,8 @@ describe('Store', () => { ▾ + [shell] + `); const rendererID = getRendererID(); @@ -1477,6 +1580,8 @@ describe('Store', () => { ▾ + [shell] + `); await act(() => @@ -1491,6 +1596,8 @@ describe('Store', () => { ▾ + [shell] + `); }); }); @@ -1794,6 +1901,8 @@ describe('Store', () => { [root] ▾ + [shell] + `); await Promise.resolve(); @@ -1806,6 +1915,8 @@ describe('Store', () => { ▾ + [shell] + `); // Render again to unmount it @@ -2291,20 +2402,24 @@ describe('Store', () => { await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` - [root] - ▾ - ▾ - - `); + [root] + ▾ + ▾ + + [shell] + + `); await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` - [root] - ▾ - ▾ - - `); + [root] + ▾ + ▾ + + [shell] + + `); }); }); diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index d7aea2981d..c29bff0538 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -156,6 +156,9 @@ describe('Store component filters', () => {
+ [shell] + + `); await actAsync( @@ -171,6 +174,9 @@ describe('Store component filters', () => {
+ [shell] + + `); await actAsync( @@ -186,6 +192,9 @@ describe('Store component filters', () => {
+ [shell] + + `); }); diff --git a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js index 4389f78cd2..e060cb3f06 100644 --- a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js @@ -32,7 +32,7 @@ describe('StoreStressConcurrent', () => { // this helper with the real thing. actAsync = require('./utils').actAsync; - print = require('./__serializers__/storeSerializer').print; + print = require('./__serializers__/storeSerializer').printStore; }); // This is a stress test for the tree mount/update/unmount traversal. @@ -67,8 +67,7 @@ describe('StoreStressConcurrent', () => { let container = document.createElement('div'); let root = ReactDOMClient.createRoot(container); act(() => root.render({[a, b, c, d, e]})); - expect(store).toMatchInlineSnapshot( - ` + expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -76,8 +75,7 @@ describe('StoreStressConcurrent', () => { - `, - ); + `); expect(container.textContent).toMatch('abcde'); const snapshotForABCDE = print(store); @@ -86,8 +84,7 @@ describe('StoreStressConcurrent', () => { act(() => { setShowX(true); }); - expect(store).toMatchInlineSnapshot( - ` + expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -96,8 +93,7 @@ describe('StoreStressConcurrent', () => { - `, - ); + `); expect(container.textContent).toMatch('abxde'); const snapshotForABXDE = print(store); @@ -419,7 +415,7 @@ describe('StoreStressConcurrent', () => { ), ); // We snapshot each step once so it doesn't regress.d - snapshots.push(print(store)); + snapshots.push(print(store, false, null, false)); await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -524,7 +520,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -544,7 +540,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. await act(() => root.render( @@ -556,7 +552,7 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. await act(() => root.render( @@ -567,7 +563,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. await act(() => root.unmount()); expect(print(store)).toBe(''); @@ -593,7 +589,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. await act(() => root.render( @@ -609,7 +605,7 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. await act(() => root.render( @@ -624,7 +620,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. await act(() => root.unmount()); expect(print(store)).toBe(''); @@ -646,7 +642,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. await act(() => root.render( @@ -662,7 +658,7 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. await act(() => root.render( @@ -673,7 +669,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. await act(() => root.unmount()); expect(print(store)).toBe(''); @@ -699,7 +695,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. await act(() => root.render( @@ -711,7 +707,7 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. await act(() => root.render( @@ -726,7 +722,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. await act(() => root.unmount()); expect(print(store)).toBe(''); @@ -755,7 +751,7 @@ describe('StoreStressConcurrent', () => { const suspenseID = store.getElementIDAtIndex(2); // Force fallback. - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); await actAsync(async () => { bridge.send('overrideSuspense', { id: suspenseID, @@ -763,7 +759,7 @@ describe('StoreStressConcurrent', () => { forceFallback: true, }); }); - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Stop forcing fallback. await actAsync(async () => { @@ -773,7 +769,7 @@ describe('StoreStressConcurrent', () => { forceFallback: false, }); }); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Trigger actual fallback. await act(() => @@ -789,7 +785,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Force fallback while we're in fallback mode. await act(() => { @@ -800,7 +796,7 @@ describe('StoreStressConcurrent', () => { }); }); // Keep seeing fallback content. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Switch to primary mode. await act(() => @@ -813,7 +809,7 @@ describe('StoreStressConcurrent', () => { ), ); // Fallback is still forced though. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Stop forcing fallback. This reverts to primary content. await actAsync(async () => { @@ -824,7 +820,7 @@ describe('StoreStressConcurrent', () => { }); }); // Now we see primary content. - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. await actAsync(async () => root.unmount()); @@ -910,7 +906,7 @@ describe('StoreStressConcurrent', () => { ), ); // We snapshot each step once so it doesn't regress. - snapshots.push(print(store)); + snapshots.push(print(store, false, null, false)); await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -935,7 +931,7 @@ describe('StoreStressConcurrent', () => { ), ); // We snapshot each step once so it doesn't regress. - fallbackSnapshots.push(print(store)); + fallbackSnapshots.push(print(store, false, null, false)); await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -1065,7 +1061,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. await act(() => root.render( @@ -1079,7 +1075,7 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. await act(() => root.render( @@ -1092,7 +1088,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. await act(() => root.unmount()); expect(print(store)).toBe(''); @@ -1121,7 +1117,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(fallbackSnapshots[i]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]); // Re-render with steps[j]. await act(() => root.render( @@ -1140,7 +1136,7 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Check that we can transition back again. await act(() => root.render( @@ -1158,7 +1154,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(fallbackSnapshots[i]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]); // Clean up after every iteration. await act(() => root.unmount()); expect(print(store)).toBe(''); @@ -1182,7 +1178,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. await act(() => root.render( @@ -1196,7 +1192,7 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Check that we can transition back again. await act(() => root.render( @@ -1209,7 +1205,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. await act(() => root.unmount()); expect(print(store)).toBe(''); @@ -1233,7 +1229,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(fallbackSnapshots[i]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]); // Re-render with steps[j]. await act(() => root.render( @@ -1247,7 +1243,7 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. await act(() => root.render( @@ -1260,7 +1256,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(fallbackSnapshots[i]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]); // Clean up after every iteration. await act(() => root.unmount()); expect(print(store)).toBe(''); @@ -1291,7 +1287,7 @@ describe('StoreStressConcurrent', () => { const suspenseID = store.getElementIDAtIndex(2); // Force fallback. - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); await actAsync(async () => { bridge.send('overrideSuspense', { id: suspenseID, @@ -1299,7 +1295,7 @@ describe('StoreStressConcurrent', () => { forceFallback: true, }); }); - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Stop forcing fallback. await actAsync(async () => { @@ -1309,7 +1305,7 @@ describe('StoreStressConcurrent', () => { forceFallback: false, }); }); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Trigger actual fallback. await act(() => @@ -1323,7 +1319,7 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Force fallback while we're in fallback mode. await act(() => { @@ -1334,7 +1330,7 @@ describe('StoreStressConcurrent', () => { }); }); // Keep seeing fallback content. - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Switch to primary mode. await act(() => @@ -1349,7 +1345,7 @@ describe('StoreStressConcurrent', () => { ), ); // Fallback is still forced though. - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Stop forcing fallback. This reverts to primary content. await actAsync(async () => { @@ -1360,7 +1356,7 @@ describe('StoreStressConcurrent', () => { }); }); // Now we see primary content. - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. await act(() => root.unmount()); diff --git a/packages/react-devtools-shared/src/__tests__/treeContext-test.js b/packages/react-devtools-shared/src/__tests__/treeContext-test.js index fa2031c6b5..e704241805 100644 --- a/packages/react-devtools-shared/src/__tests__/treeContext-test.js +++ b/packages/react-devtools-shared/src/__tests__/treeContext-test.js @@ -1368,6 +1368,9 @@ describe('TreeListContext', () => { ▾ + [shell] + + `); const outerSuspenseID = ((store.getElementIDAtIndex(1): any): number); @@ -1407,6 +1410,9 @@ describe('TreeListContext', () => { ▾ + [shell] + + `); }); }); @@ -2361,16 +2367,20 @@ describe('TreeListContext', () => { jest.runAllTimers(); expect(state).toMatchInlineSnapshot(` - [root] - - `); + [root] + + [shell] + + `); selectNextErrorOrWarning(); expect(state).toMatchInlineSnapshot(` - [root] - - `); + [root] + + [shell] + + `); }); it('should properly handle errors/warnings from components that dont mount because of Suspense', async () => { @@ -2392,9 +2402,11 @@ describe('TreeListContext', () => { utils.act(() => TestRenderer.create()); expect(state).toMatchInlineSnapshot(` - [root] - - `); + [root] + + [shell] + + `); await Promise.resolve(); withErrorsOrWarningsIgnored(['test-only:'], () => @@ -2414,6 +2426,8 @@ describe('TreeListContext', () => { ▾ + [shell] + `); }); @@ -2442,6 +2456,8 @@ describe('TreeListContext', () => { ▾ ✕ + [shell] + `); await Promise.resolve(); @@ -2456,10 +2472,12 @@ describe('TreeListContext', () => { ); expect(state).toMatchInlineSnapshot(` - [root] - ▾ - - `); + [root] + ▾ + + [shell] + + `); }); }); diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 1d4541253f..cdf5ca35b2 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -86,6 +86,7 @@ import { SUSPENSE_TREE_OPERATION_ADD, SUSPENSE_TREE_OPERATION_REMOVE, SUSPENSE_TREE_OPERATION_REORDER_CHILDREN, + SUSPENSE_TREE_OPERATION_RESIZE, } from '../../constants'; import {inspectHooksOfFiber} from 'react-debug-tools'; import { @@ -2558,6 +2559,20 @@ export function attach( pushOperation(fiberID); pushOperation(parentID); pushOperation(nameStringID); + + const rects = suspenseInstance.rects; + if (rects === null) { + pushOperation(-1); + } else { + pushOperation(rects.length); + for (let i = 0; i < rects.length; ++i) { + const rect = rects[i]; + pushOperation(Math.round(rect.x)); + pushOperation(Math.round(rect.y)); + pushOperation(Math.round(rect.width)); + pushOperation(Math.round(rect.height)); + } + } } function recordUnmount(fiberInstance: FiberInstance): void { @@ -2606,7 +2621,30 @@ export function attach( } function recordSuspenseResize(suspenseNode: SuspenseNode): void { - // TODO: Notify the front end of the change. + if (__DEBUG__) { + console.log('recordSuspenseResize()', suspenseNode); + } + const fiberInstance = suspenseNode.instance; + if (fiberInstance.kind !== FIBER_INSTANCE) { + // TODO: Resizes of filtered Suspense nodes are currently dropped. + return; + } + + pushOperation(SUSPENSE_TREE_OPERATION_RESIZE); + pushOperation(fiberInstance.id); + const rects = suspenseNode.rects; + if (rects === null) { + pushOperation(-1); + } else { + pushOperation(rects.length); + for (let i = 0; i < rects.length; ++i) { + const rect = rects[i]; + pushOperation(Math.round(rect.x)); + pushOperation(Math.round(rect.y)); + pushOperation(Math.round(rect.width)); + pushOperation(Math.round(rect.height)); + } + } } function recordSuspenseUnmount(suspenseInstance: SuspenseNode): void { @@ -3442,7 +3480,25 @@ export function attach( // Measure this Suspense node. In general we shouldn't do this until we have // inserted the new children but since we know this is a FiberInstance we'll // just use the Fiber anyway. - newSuspenseNode.rects = measureInstance(newInstance); + // Fallbacks get attributed to the parent so we only measure if we're + // showing primary content. + if (OffscreenComponent === -1) { + const isTimedOut = fiber.memoizedState !== null; + if (!isTimedOut) { + newSuspenseNode.rects = measureInstance(newInstance); + } + } else { + const contentFiber = fiber.child; + if (contentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a Suspense boundary.', + ); + } + const isTimedOut = fiber.memoizedState !== null; + if (!isTimedOut) { + newSuspenseNode.rects = measureInstance(newInstance); + } + } recordSuspenseMount(newSuspenseNode, reconcilingParentSuspenseNode); } insertChild(newInstance); @@ -3476,7 +3532,25 @@ export function attach( // Measure this Suspense node. In general we shouldn't do this until we have // inserted the new children but since we know this is a FiberInstance we'll // just use the Fiber anyway. - newSuspenseNode.rects = measureInstance(newInstance); + // Fallbacks get attributed to the parent so we only measure if we're + // showing primary content. + if (OffscreenComponent === -1) { + const isTimedOut = fiber.memoizedState !== null; + if (!isTimedOut) { + newSuspenseNode.rects = measureInstance(newInstance); + } + } else { + const contentFiber = fiber.child; + if (contentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a Suspense boundary.', + ); + } + const isTimedOut = fiber.memoizedState !== null; + if (!isTimedOut) { + newSuspenseNode.rects = measureInstance(newInstance); + } + } } insertChild(newInstance); if (__DEBUG__) { diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 391eea6b23..ce6ed0b308 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -27,6 +27,7 @@ export const TREE_OPERATION_SET_SUBTREE_MODE = 7; export const SUSPENSE_TREE_OPERATION_ADD = 8; export const SUSPENSE_TREE_OPERATION_REMOVE = 9; export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10; +export const SUSPENSE_TREE_OPERATION_RESIZE = 11; export const PROFILING_FLAG_BASIC_SUPPORT = 0b01; export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10; diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 2d6b67ef12..f4150c7557 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -23,6 +23,7 @@ import { SUSPENSE_TREE_OPERATION_ADD, SUSPENSE_TREE_OPERATION_REMOVE, SUSPENSE_TREE_OPERATION_REORDER_CHILDREN, + SUSPENSE_TREE_OPERATION_RESIZE, } from '../constants'; import {ElementTypeRoot} from '../frontend/types'; import { @@ -1418,6 +1419,7 @@ export default class Store extends EventEmitter<{ const id = operations[i + 1]; const parentID = operations[i + 2]; const nameStringID = operations[i + 3]; + const numRects = ((operations[i + 4]: any): number); let name = stringTable[nameStringID]; if (this._idToSuspense.has(id)) { @@ -1448,6 +1450,22 @@ export default class Store extends EventEmitter<{ } } + i += 5; + let rects: SuspenseNode['rects']; + if (numRects === -1) { + rects = null; + } else { + rects = []; + for (let rectIndex = 0; rectIndex < numRects; rectIndex++) { + const x = operations[i + 0]; + const y = operations[i + 1]; + const width = operations[i + 2]; + const height = operations[i + 3]; + rects.push({x, y, width, height}); + i += 4; + } + } + if (__DEBUG__) { debug('Suspense Add', `node ${id} as child of ${parentID}`); } @@ -1476,10 +1494,9 @@ export default class Store extends EventEmitter<{ parentID, children: [], name, + rects, }); - i += 4; - hasSuspenseTreeChanged = true; break; } @@ -1591,6 +1608,61 @@ export default class Store extends EventEmitter<{ hasSuspenseTreeChanged = true; break; } + case SUSPENSE_TREE_OPERATION_RESIZE: { + const id = ((operations[i + 1]: any): number); + const numRects = ((operations[i + 2]: any): number); + i += 3; + + const suspense = this._idToSuspense.get(id); + if (suspense === undefined) { + this._throwAndEmitError( + Error( + `Cannot set rects for suspense node "${id}" because no matching node was found in the Store.`, + ), + ); + + break; + } + + let nextRects: SuspenseNode['rects']; + if (numRects === -1) { + nextRects = null; + } else { + nextRects = []; + for (let rectIndex = 0; rectIndex < numRects; rectIndex++) { + const x = operations[i + 0]; + const y = operations[i + 1]; + const width = operations[i + 2]; + const height = operations[i + 3]; + + nextRects.push({x, y, width, height}); + + i += 4; + } + } + + suspense.rects = nextRects; + + if (__DEBUG__) { + debug( + 'Resize', + `Suspense node ${id} resize to ${ + nextRects === null + ? 'null' + : nextRects + .map( + rect => + `(${rect.x},${rect.y},${rect.width},${rect.height})`, + ) + .join(',') + }`, + ); + } + + hasSuspenseTreeChanged = true; + + break; + } default: this._throwAndEmitError( new UnsupportedBridgeOperationError( diff --git a/packages/react-devtools-shared/src/devtools/utils.js b/packages/react-devtools-shared/src/devtools/utils.js index 8ce34bf611..0501e861bb 100644 --- a/packages/react-devtools-shared/src/devtools/utils.js +++ b/packages/react-devtools-shared/src/devtools/utils.js @@ -10,7 +10,10 @@ import JSON5 from 'json5'; import type {ReactFunctionLocation} from 'shared/ReactTypes'; -import type {Element} from 'react-devtools-shared/src/frontend/types'; +import type { + Element, + SuspenseNode, +} from 'react-devtools-shared/src/frontend/types'; import type {StateContext} from './views/Components/TreeContext'; import type Store from './store'; @@ -28,6 +31,11 @@ export function printElement( key = ` key="${element.key}"`; } + let name = ''; + if (element.nameProp !== null) { + name = ` name="${element.nameProp}"`; + } + let hocDisplayNames = null; if (element.hocDisplayNames !== null) { hocDisplayNames = [...element.hocDisplayNames]; @@ -43,7 +51,45 @@ export function printElement( return `${' '.repeat(element.depth + 1)}${prefix} <${ element.displayName || 'null' - }${key}>${hocs}${suffix}`; + }${key}${name}>${hocs}${suffix}`; +} + +function printSuspense( + suspense: SuspenseNode, + includeWeight: boolean = false, +): string { + let name = ''; + if (suspense.name !== null) { + name = ` name="${suspense.name}"`; + } + + let printedRects = ''; + const rects = suspense.rects; + if (rects === null) { + printedRects = ' rects={null}'; + } else { + printedRects = ` rects={[${rects.map(rect => `{x:${rect.x},y:${rect.y},width:${rect.width},height:${rect.height}}`).join(', ')}]}`; + } + + return ``; +} + +function printSuspenseWithChildren( + store: Store, + suspense: SuspenseNode, + depth: number, +): Array { + const lines = [' '.repeat(depth) + printSuspense(suspense)]; + for (let i = 0; i < suspense.children.length; i++) { + const childID = suspense.children[i]; + const child = store.getSuspenseByID(childID); + if (child === null) { + throw new Error(`Could not find Suspense node with ID "${childID}".`); + } + lines.push(...printSuspenseWithChildren(store, child, depth + 1)); + } + + return lines; } export function printOwnersList( @@ -59,6 +105,7 @@ export function printStore( store: Store, includeWeight: boolean = false, state: StateContext | null = null, + includeSuspense: boolean = true, ): string { const snapshotLines = []; @@ -129,6 +176,26 @@ export function printStore( } rootWeight += weight; + + if (includeSuspense) { + const shell = store.getSuspenseByID(rootID); + // Roots from legacy renderers don't have a separate Suspense tree + if (shell !== null) { + if (shell.children.length > 0) { + snapshotLines.push('[shell]'); + for (let i = 0; i < shell.children.length; i++) { + const childID = shell.children[i]; + const child = store.getSuspenseByID(childID); + if (child === null) { + throw new Error( + `Could not find Suspense node with ID "${childID}".`, + ); + } + snapshotLines.push(...printSuspenseWithChildren(store, child, 1)); + } + } + } + } }); // Make sure the pretty-printed test align with the Store's reported number of total rows. diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js index d685263a22..e0bd4e7c73 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -19,6 +19,7 @@ import { SUSPENSE_TREE_OPERATION_ADD, SUSPENSE_TREE_OPERATION_REMOVE, SUSPENSE_TREE_OPERATION_REORDER_CHILDREN, + SUSPENSE_TREE_OPERATION_RESIZE, } from 'react-devtools-shared/src/constants'; import { parseElementDisplayNameFromBackend, @@ -376,16 +377,26 @@ function updateTree( const fiberID = operations[i + 1]; const parentID = operations[i + 2]; const nameStringID = operations[i + 3]; + const numRects = operations[i + 4]; const name = stringTable[nameStringID]; - i += 4; - if (__DEBUG__) { + let rects: string; + if (numRects === -1) { + rects = 'null'; + } else { + rects = + '[' + + operations.slice(i + 5, i + 5 + numRects * 4).join(',') + + ']'; + } debug( 'Add suspense', - `node ${fiberID} (${String(name)}) under ${parentID}`, + `node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID}`, ); } + + i += 5 + (numRects === -1 ? 0 : numRects * 4); break; } @@ -416,6 +427,30 @@ function updateTree( break; } + case SUSPENSE_TREE_OPERATION_RESIZE: { + const suspenseID = ((operations[i + 1]: any): number); + const numRects = ((operations[i + 2]: any): number); + + if (__DEBUG__) { + if (numRects === -1) { + debug('Suspense resize', `suspense ${suspenseID} rects null`); + } else { + const rects = ((operations.slice( + i + 3, + i + 3 + numRects * 4, + ): any): Array); + debug( + 'Suspense resize', + `suspense ${suspenseID} rects [${rects.join(',')}]`, + ); + } + } + + i += 3 + (numRects === -1 ? 0 : numRects * 4); + + break; + } + default: throw Error(`Unsupported Bridge operation "${operation}"`); } diff --git a/packages/react-devtools-shared/src/frontend/types.js b/packages/react-devtools-shared/src/frontend/types.js index 0089059df9..4c61a8b1e9 100644 --- a/packages/react-devtools-shared/src/frontend/types.js +++ b/packages/react-devtools-shared/src/frontend/types.js @@ -185,11 +185,19 @@ export type Element = { compiledWithForget: boolean, }; +export type Rect = { + x: number, + y: number, + width: number, + height: number, +}; + export type SuspenseNode = { id: Element['id'], parentID: SuspenseNode['id'] | 0, children: Array, name: string | null, + rects: null | Array, }; // Serialized version of ReactIOInfo diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index c585d90500..ea921c2988 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -43,6 +43,7 @@ import { SUSPENSE_TREE_OPERATION_ADD, SUSPENSE_TREE_OPERATION_REMOVE, SUSPENSE_TREE_OPERATION_REORDER_CHILDREN, + SUSPENSE_TREE_OPERATION_RESIZE, } from './constants'; import { ComponentFilterElementType, @@ -339,11 +340,34 @@ export function printOperationsArray(operations: Array) { const parentID = operations[i + 2]; const nameStringID = operations[i + 3]; const name = stringTable[nameStringID]; + const numRects = operations[i + 4]; - i += 4; + i += 5; + + let rects: string; + if (numRects === -1) { + rects = 'null'; + } else { + rects = '['; + for (let rectIndex = 0; rectIndex < numRects; rectIndex++) { + const offset = i + rectIndex * 4; + const x = operations[offset + 0]; + const y = operations[offset + 1]; + const width = operations[offset + 2]; + const height = operations[offset + 3]; + + if (rectIndex > 0) { + rects += ', '; + } + rects += `(${x}, ${y}, ${width}, ${height})`; + + i += 4; + } + rects += ']'; + } logs.push( - `Add suspense node ${fiberID} (${String(name)}) under ${parentID}`, + `Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID}`, ); break; } @@ -372,6 +396,33 @@ export function printOperationsArray(operations: Array) { ); break; } + case SUSPENSE_TREE_OPERATION_RESIZE: { + const id = ((operations[i + 1]: any): number); + const numRects = ((operations[i + 2]: any): number); + i += 3; + + if (numRects === -1) { + logs.push(`Resize suspense node ${id} to null`); + } else { + let line = `Resize suspense node ${id} to [`; + for (let rectIndex = 0; rectIndex < numRects; rectIndex++) { + const x = operations[i + 0]; + const y = operations[i + 1]; + const width = operations[i + 2]; + const height = operations[i + 3]; + + if (rectIndex > 0) { + line += ', '; + } + line += `(${x}, ${y}, ${width}, ${height})`; + + i += 4; + } + logs.push(line + ']'); + } + + break; + } default: throw Error(`Unsupported Bridge operation "${operation}"`); }