diff --git a/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js b/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js index f5b7e5fded401..a7c0893060b00 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 1a5a0e6a26070..87524ffd045b4 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 d7aea2981d8c6..c29bff05383c5 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 4389f78cd26a3..e060cb3f06ba9 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 fa2031c6b5c8d..e7042418053ea 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 1d4541253f7a2..f5d202fe0164b 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 { @@ -3209,7 +3247,7 @@ export function attach( const debugInfo = thenable._debugInfo; if (debugInfo) { for (let j = 0; j < debugInfo.length; j++) { - const debugEntry = debugInfo[i]; + const debugEntry = debugInfo[j]; if (debugEntry.awaited) { const asyncInfo: ReactAsyncInfo = (debugEntry: any); insertSuspendedBy(asyncInfo); @@ -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 391eea6b23e11..ce6ed0b308a83 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 2d6b67ef12cc8..f4150c75570fc 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 8ce34bf61175c..0501e861bbd67 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/Components/InspectedElementView.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js index 1318e96c30d0d..d7fd2c9ae5b2d 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js @@ -174,9 +174,8 @@ export default function InspectedElementView({ {showStack ? : null} {showOwnersList && owners?.map(owner => ( - <> + 0 ? ( ) : null} - + ))} {rootType !== null && ( 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 d685263a22603..e0bd4e7c73c5e 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 0089059df9d7a..4c61a8b1e9d71 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 c585d90500dff..ea921c2988c3d 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}"`); } diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js index 6a562ba5e8578..cb475340c9c38 100644 --- a/packages/react/src/jsx/ReactJSXElement.js +++ b/packages/react/src/jsx/ReactJSXElement.js @@ -156,30 +156,9 @@ function elementRefGetterWithDeprecationWarning() { * will not work. Instead test $$typeof field against Symbol.for('react.transitional.element') to check * if something is a React Element. * - * @param {*} type - * @param {*} props - * @param {*} key - * @param {string|object} ref - * @param {*} owner - * @param {*} self A *temporary* helper to detect places where `this` is - * different from the `owner` when React.createElement is called, so that we - * can warn. We want to get rid of owner and replace string `ref`s with arrow - * functions, and as long as `this` and owner are the same, there will be no - * change in behavior. - * @param {*} source An annotation object (added by a transpiler or otherwise) - * indicating filename, line number, and/or other information. * @internal */ -function ReactElement( - type, - key, - self, - source, - owner, - props, - debugStack, - debugTask, -) { +function ReactElement(type, key, props, owner, debugStack, debugTask) { // Ignore whatever was passed as the ref argument and treat `props.ref` as // the source of truth. The only thing we use this for is `element.ref`, // which will log a deprecation warning on access. In the next release, we @@ -348,16 +327,7 @@ export function jsxProd(type, config, maybeKey) { } } - return ReactElement( - type, - key, - undefined, - undefined, - getOwner(), - props, - undefined, - undefined, - ); + return ReactElement(type, key, props, getOwner(), undefined, undefined); } // While `jsxDEV` should never be called when running in production, we do @@ -376,8 +346,6 @@ export function jsxProdSignatureRunningInDevWithDynamicChildren( type, config, maybeKey, - source, - self, ) { if (__DEV__) { const isStaticChildren = false; @@ -389,8 +357,6 @@ export function jsxProdSignatureRunningInDevWithDynamicChildren( config, maybeKey, isStaticChildren, - source, - self, __DEV__ && (trackActualOwner ? Error('react-stack-top-frame') @@ -407,8 +373,6 @@ export function jsxProdSignatureRunningInDevWithStaticChildren( type, config, maybeKey, - source, - self, ) { if (__DEV__) { const isStaticChildren = true; @@ -420,8 +384,6 @@ export function jsxProdSignatureRunningInDevWithStaticChildren( config, maybeKey, isStaticChildren, - source, - self, __DEV__ && (trackActualOwner ? Error('react-stack-top-frame') @@ -442,7 +404,7 @@ const didWarnAboutKeySpread = {}; * @param {object} props * @param {string} key */ -export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) { +export function jsxDEV(type, config, maybeKey, isStaticChildren) { const trackActualOwner = __DEV__ && ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit; @@ -451,8 +413,6 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) { config, maybeKey, isStaticChildren, - source, - self, __DEV__ && (trackActualOwner ? Error('react-stack-top-frame') @@ -469,8 +429,6 @@ function jsxDEVImpl( config, maybeKey, isStaticChildren, - source, - self, debugStack, debugTask, ) { @@ -491,7 +449,7 @@ function jsxDEVImpl( if (isStaticChildren) { if (isArray(children)) { for (let i = 0; i < children.length; i++) { - validateChildKeys(children[i], type); + validateChildKeys(children[i]); } if (Object.freeze) { @@ -505,7 +463,7 @@ function jsxDEVImpl( ); } } else { - validateChildKeys(children, type); + validateChildKeys(children); } } @@ -591,16 +549,7 @@ function jsxDEVImpl( defineKeyPropWarningGetter(props, displayName); } - return ReactElement( - type, - key, - self, - source, - getOwner(), - props, - debugStack, - debugTask, - ); + return ReactElement(type, key, props, getOwner(), debugStack, debugTask); } } @@ -620,7 +569,7 @@ export function createElement(type, config, children) { // prod. (Rendering will throw with a helpful message and as soon as the // type is fixed, the key warnings will appear.) for (let i = 2; i < arguments.length; i++) { - validateChildKeys(arguments[i], type); + validateChildKeys(arguments[i]); } // Unlike the jsx() runtime, createElement() doesn't warn about key spread. @@ -721,10 +670,8 @@ export function createElement(type, config, children) { return ReactElement( type, key, - undefined, - undefined, - getOwner(), props, + getOwner(), __DEV__ && (trackActualOwner ? Error('react-stack-top-frame') @@ -740,10 +687,8 @@ export function cloneAndReplaceKey(oldElement, newKey) { const clonedElement = ReactElement( oldElement.type, newKey, - undefined, - undefined, - !__DEV__ ? undefined : oldElement._owner, oldElement.props, + !__DEV__ ? undefined : oldElement._owner, __DEV__ && oldElement._debugStack, __DEV__ && oldElement._debugTask, ); @@ -829,16 +774,14 @@ export function cloneElement(element, config, children) { const clonedElement = ReactElement( element.type, key, - undefined, - undefined, - owner, props, + owner, __DEV__ && element._debugStack, __DEV__ && element._debugTask, ); for (let i = 2; i < arguments.length; i++) { - validateChildKeys(arguments[i], clonedElement.type); + validateChildKeys(arguments[i]); } return clonedElement; @@ -853,10 +796,9 @@ export function cloneElement(element, config, children) { * @param {ReactNode} node Statically passed child of any type. * @param {*} parentType node's parent's type. */ -function validateChildKeys(node, parentType) { +function validateChildKeys(node) { if (__DEV__) { - // With owner stacks is, no warnings happens. All we do is - // mark elements as being in a valid static child position so they + // Mark elements as being in a valid static child position so they // don't need keys. if (isValidElement(node)) { if (node._store) {