diff --git a/.eslintrc.js b/.eslintrc.js index 4e023cd9d3..2c9ad7a4c9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -608,6 +608,7 @@ module.exports = { symbol: 'readonly', SyntheticEvent: 'readonly', SyntheticMouseEvent: 'readonly', + SyntheticPointerEvent: 'readonly', Thenable: 'readonly', TimeoutID: 'readonly', WheelEventHandler: 'readonly', diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 1690f20ede..7cb3ef0495 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -1324,6 +1324,34 @@ const allTests = { `, errors: [asyncComponentHookError('use')], }, + { + code: normalizeIndent` + function App({p1, p2}) { + try { + use(p1); + } catch (error) { + console.error(error); + } + use(p2); + return
App
; + } + `, + errors: [tryCatchUseError('use')], + }, + { + code: normalizeIndent` + function App({p1, p2}) { + try { + doSomething(); + } catch { + use(p1); + } + use(p2); + return
App
; + } + `, + errors: [tryCatchUseError('use')], + }, ], }; @@ -1383,7 +1411,7 @@ if (__EXPERIMENTAL__) { const onEvent = useEffectEvent((text) => { console.log(text); }); - + useEffect(() => { onEvent('Hello world'); }); @@ -1421,7 +1449,7 @@ if (__EXPERIMENTAL__) { }); return onClick()} /> } - + // The useEffectEvent function shares an identifier name with the above function MyLastComponent({theme}) { const onClick = useEffectEvent(() => { @@ -1573,6 +1601,12 @@ function asyncComponentHookError(fn) { }; } +function tryCatchUseError(fn) { + return { + message: `React Hook "${fn}" cannot be called in a try/catch block.`, + }; +} + // For easier local testing if (!process.env.CI) { let only = []; diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index 8d42b319b4..2132ee75ad 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -111,6 +111,16 @@ function isInsideDoWhileLoop(node: Node | undefined): node is DoWhileStatement { return false; } +function isInsideTryCatch(node: Node | undefined): boolean { + while (node) { + if (node.type === 'TryStatement' || node.type === 'CatchClause') { + return true; + } + node = node.parent; + } + return false; +} + function isUseEffectEventIdentifier(node: Node): boolean { if (__EXPERIMENTAL__) { return node.type === 'Identifier' && node.name === 'useEffectEvent'; @@ -532,6 +542,16 @@ const rule = { continue; } + // Report an error if use() is called inside try/catch/finally. + if (isUseIdentifier(hook) && isInsideTryCatch(hook)) { + context.report({ + node: hook, + message: `React Hook "${getSourceCode().getText( + hook, + )}" cannot be called in a try/catch/finally block.`, + }); + } + // Report an error if a hook may be called more then once. // `use(...)` can be called in loops. if ( diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index 984408500d..2b35e82363 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -22,6 +22,8 @@ import { addObjectToProperties, } from 'shared/ReactPerformanceTrackProperties'; +import {getIODescription} from 'shared/ReactIODescription'; + const supportsUserTiming = enableProfilerTimer && typeof console !== 'undefined' && @@ -300,70 +302,6 @@ function getIOColor( } } -function getIODescription(value: any): string { - if (!__DEV__) { - return ''; - } - try { - switch (typeof value) { - case 'object': - // Test the object for a bunch of common property names that are useful identifiers. - // While we only have the return value here, it should ideally be a name that - // describes the arguments requested. - if (value === null) { - return ''; - } else if (value instanceof Error) { - // eslint-disable-next-line react-internal/safe-string-coercion - return String(value.message); - } else if (typeof value.url === 'string') { - return value.url; - } else if (typeof value.command === 'string') { - return value.command; - } else if ( - typeof value.request === 'object' && - typeof value.request.url === 'string' - ) { - return value.request.url; - } else if ( - typeof value.response === 'object' && - typeof value.response.url === 'string' - ) { - return value.response.url; - } else if ( - typeof value.id === 'string' || - typeof value.id === 'number' || - typeof value.id === 'bigint' - ) { - // eslint-disable-next-line react-internal/safe-string-coercion - return String(value.id); - } else if (typeof value.name === 'string') { - return value.name; - } else { - const str = value.toString(); - if (str.startWith('[object ') || str.length < 5 || str.length > 500) { - // This is probably not a useful description. - return ''; - } - return str; - } - case 'string': - if (value.length < 5 || value.length > 500) { - return ''; - } - return value; - case 'number': - case 'bigint': - // eslint-disable-next-line react-internal/safe-string-coercion - return String(value); - default: - // Not useful descriptors. - return ''; - } - } catch (x) { - return ''; - } -} - function getIOLongName( ioInfo: ReactIOInfo, description: string, diff --git a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js index 7a77ab20eb..1de972658c 100644 --- a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js +++ b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js @@ -857,7 +857,7 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.008, "type": "render-idle", @@ -873,25 +873,17 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.012, "type": "commit", }, - { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.02, - "type": "layout-effects", - }, { "batchUID": 0, "depth": 0, "duration": 0.004, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.023, + "timestamp": 0.021, "type": "passive-effects", }, ], @@ -899,9 +891,9 @@ describe('Timeline profiler', () => { { "batchUID": 1, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render-idle", }, { @@ -909,31 +901,23 @@ describe('Timeline profiler', () => { "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render", }, { "batchUID": 1, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.032, + "timestamp": 0.03, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.04, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.043, + "timestamp": 0.039, "type": "passive-effects", }, ], @@ -949,26 +933,26 @@ describe('Timeline profiler', () => { { "componentName": "App", "duration": 0.002, - "timestamp": 0.024, + "timestamp": 0.022, "type": "passive-effect-mount", "warning": null, }, { "componentName": "App", "duration": 0.001, - "timestamp": 0.029, + "timestamp": 0.027, "type": "render", "warning": null, }, { "componentName": "App", "duration": 0.001, - "timestamp": 0.044, + "timestamp": 0.04, "type": "passive-effect-mount", "warning": null, }, ], - "duration": 0.046, + "duration": 0.042, "flamechart": [], "internalModuleSourceToRanges": Map { undefined => [ @@ -1031,7 +1015,7 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.008, "type": "render-idle", @@ -1047,33 +1031,25 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.012, "type": "commit", }, - { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.02, - "type": "layout-effects", - }, { "batchUID": 0, "depth": 0, "duration": 0.004, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.023, + "timestamp": 0.021, "type": "passive-effects", }, { "batchUID": 1, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render-idle", }, { @@ -1081,31 +1057,23 @@ describe('Timeline profiler', () => { "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render", }, { "batchUID": 1, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.032, + "timestamp": 0.03, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.04, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.043, + "timestamp": 0.039, "type": "passive-effects", }, ], @@ -1149,7 +1117,7 @@ describe('Timeline profiler', () => { { "componentName": "App", "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.025, + "timestamp": 0.023, "type": "schedule-state-update", "warning": null, }, @@ -1254,6 +1222,15 @@ describe('Timeline profiler', () => { let promise = null; let resolvedValue = null; function readValue(value) { + if (React.use) { + if (promise === null) { + promise = Promise.resolve(true).then(() => { + return value; + }); + promise.displayName = 'Testing displayName'; + } + return React.use(promise); + } if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { @@ -1273,7 +1250,7 @@ describe('Timeline profiler', () => { const testMarks = [creactCpuProfilerSample()]; const root = ReactDOMClient.createRoot(document.createElement('div')); - utils.act(() => + await utils.actAsync(() => root.render( @@ -1823,6 +1800,14 @@ describe('Timeline profiler', () => { let promise = null; let resolvedValue = null; function readValue(value) { + if (React.use) { + if (promise === null) { + promise = Promise.resolve(true).then(() => { + return value; + }); + } + return React.use(promise); + } if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { @@ -1881,6 +1866,14 @@ describe('Timeline profiler', () => { let promise = null; let resolvedValue = null; function readValue(value) { + if (React.use) { + if (promise === null) { + promise = Promise.resolve(true).then(() => { + return value; + }); + } + return React.use(promise); + } if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { @@ -2192,14 +2185,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, @@ -2234,14 +2219,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 2, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 2, "depth": 0, @@ -2292,8 +2269,8 @@ describe('Timeline profiler', () => { 8 => "InputContinuous", 16 => "DefaultHydration", 32 => "Default", - 64 => "TransitionHydration", - 128 => "Transition", + 64 => undefined, + 128 => "TransitionHydration", 256 => "Transition", 512 => "Transition", 1024 => "Transition", @@ -2349,14 +2326,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, @@ -2389,14 +2358,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 2, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 2, "depth": 0, diff --git a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js index cead054d31..c8384b2fa4 100644 --- a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js @@ -215,7 +215,11 @@ describe('ProfilerStore', () => { it('should not throw while initializing context values for Fibers within a not-yet-mounted subtree', () => { const promise = new Promise(resolve => {}); const SuspendingView = () => { - throw promise; + if (React.use) { + React.use(promise); + } else { + throw promise; + } }; const App = () => { diff --git a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js index fb1fa6c5f2..c0f0804f41 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js @@ -682,6 +682,14 @@ describe('ProfilingCache', () => { it('should calculate durations correctly for suspended views', async () => { let data; const getData = () => { + if (React.use) { + if (!data) { + data = new Promise(resolve => { + resolve('abc'); + }); + } + return React.use(data); + } if (data) { return data; } else { diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index 93c7048b2b..e982ac5ecc 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -44,10 +44,10 @@ describe('Store', () => { const {render, unmount, createContainer} = getVersionedRenderImplementation(); // @reactVersion >= 18.0 - it('should not allow a root node to be collapsed', () => { + it('should not allow a root node to be collapsed', async () => { const Component = () =>
Hi
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -63,16 +63,16 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should properly handle a root with no visible nodes', () => { + it('should properly handle a root with no visible nodes', async () => { const Root = ({children}) => children; - act(() => render({null})); + await act(() => render({null})); expect(store).toMatchInlineSnapshot(` [root] `); - act(() => render(
)); + await act(() => render(
)); expect(store).toMatchInlineSnapshot(`[root]`); }); @@ -82,20 +82,24 @@ describe('Store', () => { // I'mnot yet sure of how to reduce the GitHub reported production case to a test though. // See https://github.com/facebook/react/issues/21445 // @reactVersion >= 18.0 - it('should handle when a component mounts before its owner', () => { + it('should handle when a component mounts before its owner', async () => { const promise = new Promise(resolve => {}); let Dynamic = null; const Owner = () => { Dynamic = ; - throw promise; + if (React.use) { + React.use(promise); + } else { + throw promise; + } }; const Parent = () => { return Dynamic; }; const Child = () => null; - act(() => + await act(() => render( <> @@ -114,11 +118,11 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should handle multibyte character strings', () => { + it('should handle multibyte character strings', async () => { const Component = () => null; Component.displayName = '🟩💜🔵'; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] <🟩💜🔵> @@ -126,7 +130,7 @@ describe('Store', () => { }); describe('StrictMode compliance', () => { - it('should mark strict root elements as strict', () => { + it('should mark strict root elements as strict', async () => { const App = () => ; const Component = () => null; @@ -134,7 +138,7 @@ describe('Store', () => { const root = ReactDOMClient.createRoot(container, { unstable_strictMode: true, }); - act(() => { + await act(() => { root.render(); }); @@ -143,13 +147,13 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should mark non strict root elements as not strict', () => { + it('should mark non strict root elements as not strict', async () => { const App = () => ; const Component = () => null; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => { + await act(() => { root.render(); }); @@ -157,7 +161,7 @@ describe('Store', () => { expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(true); }); - it('should mark StrictMode subtree elements as strict', () => { + it('should mark StrictMode subtree elements as strict', async () => { const App = () => ( @@ -167,7 +171,7 @@ describe('Store', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => { + await act(() => { root.render(); }); @@ -182,7 +186,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support mount and update operations', () => { + it('should support mount and update operations', async () => { const Grandparent = ({count}) => ( @@ -193,7 +197,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -209,7 +213,7 @@ describe('Store', () => { `); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -221,14 +225,14 @@ describe('Store', () => { `); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode - it('should support mount and update operations for multiple roots (legacy render)', () => { + it('should support mount and update operations for multiple roots (legacy render)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; @@ -236,7 +240,7 @@ describe('Store', () => { const containerA = document.createElement('div'); const containerB = document.createElement('div'); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -252,7 +256,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -268,7 +272,7 @@ describe('Store', () => { `); - act(() => ReactDOM.unmountComponentAtNode(containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -278,12 +282,12 @@ describe('Store', () => { `); - act(() => ReactDOM.unmountComponentAtNode(containerA)); + await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should support mount and update operations for multiple roots (createRoot)', () => { + it('should support mount and update operations for multiple roots (createRoot)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; @@ -294,7 +298,7 @@ describe('Store', () => { const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -310,7 +314,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -326,7 +330,7 @@ describe('Store', () => { `); - act(() => rootB.unmount()); + await act(() => rootB.unmount()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -336,12 +340,12 @@ describe('Store', () => { `); - act(() => rootA.unmount()); + await act(() => rootA.unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should filter DOM nodes from the store tree', () => { + it('should filter DOM nodes from the store tree', async () => { const Grandparent = () => (
@@ -357,7 +361,7 @@ describe('Store', () => { ); const Child = () =>
Hi!
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -369,10 +373,14 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should display Suspense nodes properly in various states', () => { + it('should display Suspense nodes properly in various states', async () => { const Loading = () =>
Loading...
; const SuspendingComponent = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Component = () => { return
Hello
; @@ -390,7 +398,7 @@ describe('Store', () => { ); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -399,7 +407,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { render(); }); expect(store).toMatchInlineSnapshot(` @@ -412,11 +420,15 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support nested Suspense nodes', () => { + it('should support nested Suspense nodes', async () => { const Component = () => null; const Loading = () =>
Loading...
; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Wrapper = ({ @@ -451,7 +463,7 @@ describe('Store', () => { ); - act(() => + await act(() => render( { `); - act(() => + await act(() => render( { `); - act(() => + await act(() => render( { `); - act(() => + await act(() => render( { `); - act(() => + await act(() => render( { â–¾ `); - act(() => + await act(() => render( { `); - act(() => + await act(() => render( { `); const rendererID = getRendererID(); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(4), rendererID, @@ -628,7 +640,7 @@ describe('Store', () => { `); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(2), rendererID, @@ -642,7 +654,7 @@ describe('Store', () => { â–¾ `); - act(() => + await act(() => render( { â–¾ `); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(2), rendererID, @@ -679,7 +691,7 @@ describe('Store', () => { `); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(4), rendererID, @@ -700,7 +712,7 @@ describe('Store', () => { `); - act(() => + await act(() => render( { `); }); - it('should display a partially rendered SuspenseList', () => { + it('should display a partially rendered SuspenseList', async () => { const Loading = () =>
Loading...
; const SuspendingComponent = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Component = () => { return
Hello
; @@ -747,7 +763,7 @@ describe('Store', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => { + await act(() => { root.render(); }); expect(store).toMatchInlineSnapshot(` @@ -759,7 +775,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { root.render(); }); expect(store).toMatchInlineSnapshot(` @@ -774,7 +790,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support collapsing parts of the tree', () => { + it('should support collapsing parts of the tree', async () => { const Grandparent = ({count}) => ( @@ -785,7 +801,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -801,7 +817,7 @@ describe('Store', () => { const parentOneID = store.getElementIDAtIndex(1); const parentTwoID = store.getElementIDAtIndex(4); - act(() => store.toggleIsCollapsed(parentOneID, true)); + await act(() => store.toggleIsCollapsed(parentOneID, true)); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -811,7 +827,7 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(parentTwoID, true)); + await act(() => store.toggleIsCollapsed(parentTwoID, true)); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -819,7 +835,7 @@ describe('Store', () => { â–¸ `); - act(() => store.toggleIsCollapsed(parentOneID, false)); + await act(() => store.toggleIsCollapsed(parentOneID, false)); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -829,13 +845,13 @@ describe('Store', () => { â–¸ `); - act(() => store.toggleIsCollapsed(grandparentID, true)); + await act(() => store.toggleIsCollapsed(grandparentID, true)); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); - act(() => store.toggleIsCollapsed(grandparentID, false)); + await act(() => store.toggleIsCollapsed(grandparentID, false)); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -847,7 +863,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support reordering of children', () => { + it('should support reordering of children', async () => { const Root = ({children}) => children; const Component = () => null; @@ -856,7 +872,7 @@ describe('Store', () => { const foo = ; const bar = ; - act(() => render({[foo, bar]})); + await act(() => render({[foo, bar]})); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -867,7 +883,7 @@ describe('Store', () => { `); - act(() => render({[bar, foo]})); + await act(() => render({[bar, foo]})); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -878,13 +894,17 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), true), + ); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -903,12 +923,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support mount and update operations', () => { + it('should support mount and update operations', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - act(() => + await act(() => render( @@ -922,7 +942,7 @@ describe('Store', () => { â–¸ `); - act(() => + await act(() => render( @@ -936,14 +956,14 @@ describe('Store', () => { â–¸ `); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode - it('should support mount and update operations for multiple roots (legacy render)', () => { + it('should support mount and update operations for multiple roots (legacy render)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; @@ -951,7 +971,7 @@ describe('Store', () => { const containerA = document.createElement('div'); const containerB = document.createElement('div'); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -962,7 +982,7 @@ describe('Store', () => { â–¸ `); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -973,18 +993,18 @@ describe('Store', () => { â–¸ `); - act(() => ReactDOM.unmountComponentAtNode(containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); - act(() => ReactDOM.unmountComponentAtNode(containerA)); + await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should support mount and update operations for multiple roots (createRoot)', () => { + it('should support mount and update operations for multiple roots (createRoot)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; @@ -995,7 +1015,7 @@ describe('Store', () => { const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -1006,7 +1026,7 @@ describe('Store', () => { â–¸ `); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -1017,18 +1037,18 @@ describe('Store', () => { â–¸ `); - act(() => rootB.unmount()); + await act(() => rootB.unmount()); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); - act(() => rootA.unmount()); + await act(() => rootA.unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should filter DOM nodes from the store tree', () => { + it('should filter DOM nodes from the store tree', async () => { const Grandparent = () => (
@@ -1044,13 +1064,15 @@ describe('Store', () => { ); const Child = () =>
Hi!
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -1058,7 +1080,9 @@ describe('Store', () => { â–¸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(1), false), + ); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -1069,10 +1093,14 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should display Suspense nodes properly in various states', () => { + it('should display Suspense nodes properly in various states', async () => { const Loading = () =>
Loading...
; const SuspendingComponent = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Component = () => { return
Hello
; @@ -1090,15 +1118,19 @@ describe('Store', () => { ); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¸ `); // This test isn't meaningful unless we expand the suspended tree - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(2), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(2), false), + ); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -1107,7 +1139,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { render(); }); expect(store).toMatchInlineSnapshot(` @@ -1120,7 +1152,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support expanding parts of the tree', () => { + it('should support expanding parts of the tree', async () => { const Grandparent = ({count}) => ( @@ -1131,7 +1163,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1139,7 +1171,7 @@ describe('Store', () => { const grandparentID = store.getElementIDAtIndex(0); - act(() => store.toggleIsCollapsed(grandparentID, false)); + await act(() => store.toggleIsCollapsed(grandparentID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1150,7 +1182,7 @@ describe('Store', () => { const parentOneID = store.getElementIDAtIndex(1); const parentTwoID = store.getElementIDAtIndex(2); - act(() => store.toggleIsCollapsed(parentOneID, false)); + await act(() => store.toggleIsCollapsed(parentOneID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1160,7 +1192,7 @@ describe('Store', () => { ▸ `); - act(() => store.toggleIsCollapsed(parentTwoID, false)); + await act(() => store.toggleIsCollapsed(parentTwoID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1172,7 +1204,7 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(parentOneID, true)); + await act(() => store.toggleIsCollapsed(parentOneID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1182,7 +1214,7 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(parentTwoID, true)); + await act(() => store.toggleIsCollapsed(parentTwoID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1190,7 +1222,7 @@ describe('Store', () => { ▸ `); - act(() => store.toggleIsCollapsed(grandparentID, true)); + await act(() => store.toggleIsCollapsed(grandparentID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1198,7 +1230,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support expanding deep parts of the tree', () => { + it('should support expanding deep parts of the tree', async () => { const Wrapper = ({forwardedRef}) => ( ); @@ -1211,7 +1243,7 @@ describe('Store', () => { const ref = React.createRef(); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1219,7 +1251,7 @@ describe('Store', () => { const deepestedNodeID = agent.getIDForHostInstance(ref.current); - act(() => store.toggleIsCollapsed(deepestedNodeID, false)); + await act(() => store.toggleIsCollapsed(deepestedNodeID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1231,13 +1263,13 @@ describe('Store', () => { const rootID = store.getElementIDAtIndex(0); - act(() => store.toggleIsCollapsed(rootID, true)); + await act(() => store.toggleIsCollapsed(rootID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => store.toggleIsCollapsed(rootID, false)); + await act(() => store.toggleIsCollapsed(rootID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1249,14 +1281,14 @@ describe('Store', () => { const id = store.getElementIDAtIndex(1); - act(() => store.toggleIsCollapsed(id, true)); + await act(() => store.toggleIsCollapsed(id, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ ▸ `); - act(() => store.toggleIsCollapsed(id, false)); + await act(() => store.toggleIsCollapsed(id, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1268,7 +1300,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support reordering of children', () => { + it('should support reordering of children', async () => { const Root = ({children}) => children; const Component = () => null; @@ -1277,19 +1309,21 @@ describe('Store', () => { const foo = ; const bar = ; - act(() => render({[foo, bar]})); + await act(() => render({[foo, bar]})); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => render({[bar, foo]})); + await act(() => render({[bar, foo]})); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1297,7 +1331,7 @@ describe('Store', () => { ▸ `); - act(() => { + await act(() => { store.toggleIsCollapsed(store.getElementIDAtIndex(2), false); store.toggleIsCollapsed(store.getElementIDAtIndex(1), false); }); @@ -1311,7 +1345,9 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), true), + ); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1319,7 +1355,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should not add new nodes when suspense is toggled', () => { + it('should not add new nodes when suspense is toggled', async () => { const SuspenseTree = () => { return ( Loading outer}> @@ -1332,14 +1368,18 @@ describe('Store', () => { const Parent = () => ; const Child = () => null; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(1), false), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1350,7 +1390,7 @@ describe('Store', () => { const rendererID = getRendererID(); const suspenseID = store.getElementIDAtIndex(1); - act(() => + await act(() => agent.overrideSuspense({ id: suspenseID, rendererID, @@ -1364,7 +1404,7 @@ describe('Store', () => { `); - act(() => + await act(() => agent.overrideSuspense({ id: suspenseID, rendererID, @@ -1386,7 +1426,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support a single root with a single child', () => { + it('should support a single root with a single child', async () => { const Grandparent = () => ( @@ -1396,7 +1436,7 @@ describe('Store', () => { const Parent = () => ; const Child = () => null; - act(() => render()); + await act(() => render()); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); @@ -1404,12 +1444,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support multiple roots with one children each', () => { + it('should support multiple roots with one children each', async () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; - act(() => { + await act(() => { render(); render(); }); @@ -1420,12 +1460,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support a single root with multiple top level children', () => { + it('should support a single root with multiple top level children', async () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; - act(() => + await act(() => render( @@ -1440,12 +1480,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support multiple roots with multiple top level children', () => { + it('should support multiple roots with multiple top level children', async () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; - act(() => { + await act(() => { render( @@ -1472,7 +1512,7 @@ describe('Store', () => { // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode - it('detects and updates profiling support based on the attached roots (legacy render)', () => { + it('detects and updates profiling support based on the attached roots (legacy render)', async () => { const Component = () => null; const containerA = document.createElement('div'); @@ -1480,19 +1520,19 @@ describe('Store', () => { expect(store.rootSupportsBasicProfiling).toBe(false); - act(() => legacyRender(, containerA)); + await act(() => legacyRender(, containerA)); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => legacyRender(, containerB)); - act(() => ReactDOM.unmountComponentAtNode(containerA)); + await act(() => legacyRender(, containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => ReactDOM.unmountComponentAtNode(containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store.rootSupportsBasicProfiling).toBe(false); }); // @reactVersion >= 18 - it('detects and updates profiling support based on the attached roots (createRoot)', () => { + it('detects and updates profiling support based on the attached roots (createRoot)', async () => { const Component = () => null; const containerA = document.createElement('div'); @@ -1503,26 +1543,26 @@ describe('Store', () => { expect(store.rootSupportsBasicProfiling).toBe(false); - act(() => rootA.render()); + await act(() => rootA.render()); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => rootB.render()); - act(() => rootA.unmount()); + await act(() => rootB.render()); + await act(() => rootA.unmount()); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => rootB.unmount()); + await act(() => rootB.unmount()); expect(store.rootSupportsBasicProfiling).toBe(false); }); // @reactVersion >= 18.0 - it('should properly serialize non-string key values', () => { + it('should properly serialize non-string key values', async () => { const Child = () => null; // Bypass React element's automatic stringifying of keys intentionally. // This is pretty hacky. const fauxElement = Object.assign({}, , {key: 123}); - act(() => render([fauxElement])); + await act(() => render([fauxElement])); expect(store).toMatchInlineSnapshot(` [root] @@ -1581,12 +1621,12 @@ describe('Store', () => { ); // Render once to start fetching the lazy component - act(() => render()); + await act(() => render()); await Promise.resolve(); // Render again after it resolves - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1638,7 +1678,7 @@ describe('Store', () => { const container = document.createElement('div'); // Render once to start fetching the lazy component - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1649,7 +1689,7 @@ describe('Store', () => { await Promise.resolve(); // Render again after it resolves - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1659,7 +1699,7 @@ describe('Store', () => { `); // Render again to unmount it - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1673,7 +1713,7 @@ describe('Store', () => { const root = ReactDOMClient.createRoot(container); // Render once to start fetching the lazy component - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1684,7 +1724,7 @@ describe('Store', () => { await Promise.resolve(); // Render again after it resolves - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1694,7 +1734,7 @@ describe('Store', () => { `); // Render again to unmount it - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1709,7 +1749,7 @@ describe('Store', () => { const container = document.createElement('div'); // Render once to start fetching the lazy component - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1718,7 +1758,7 @@ describe('Store', () => { `); // Render again to unmount it before it finishes loading - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1733,7 +1773,7 @@ describe('Store', () => { const root = ReactDOMClient.createRoot(container); // Render once to start fetching the lazy component - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1742,7 +1782,7 @@ describe('Store', () => { `); // Render again to unmount it before it finishes loading - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1753,15 +1793,15 @@ describe('Store', () => { describe('inline errors and warnings', () => { // @reactVersion >= 18.0 - it('during render are counted', () => { + it('during render are counted', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1770,8 +1810,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1782,7 +1822,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('during layout get counted', () => { + it('during layout get counted', async () => { function Example() { React.useLayoutEffect(() => { console.error('test-only: layout error'); @@ -1791,8 +1831,8 @@ describe('Store', () => { return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1801,8 +1841,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1818,7 +1858,7 @@ describe('Store', () => { } // @reactVersion >= 18.0 - it('are counted (after no delay)', () => { + it('are counted (after no delay)', async () => { function Example() { React.useEffect(() => { console.error('test-only: passive error'); @@ -1827,8 +1867,8 @@ describe('Store', () => { return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => { + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => { render(); }, false); }); @@ -1839,12 +1879,12 @@ describe('Store', () => { ✕⚠ `); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('are flushed early when there is a new commit', () => { + it('are flushed early when there is a new commit', async () => { function Example() { React.useEffect(() => { console.error('test-only: passive error'); @@ -1890,7 +1930,7 @@ describe('Store', () => { `); }); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); }); @@ -1898,7 +1938,7 @@ describe('Store', () => { // In React 19, JSX warnings were moved into the renderer - https://github.com/facebook/react/pull/29088 // The warning is moved to the Child instead of the Parent. // @reactVersion >= 19.0.1 - it('from react get counted [React >= 19.0.1]', () => { + it('from react get counted [React >= 19.0.1]', async () => { function Example() { return []; } @@ -1923,7 +1963,7 @@ describe('Store', () => { // @reactVersion >= 18.0 // @reactVersion < 19.0 - it('from react get counted [React 18.x]', () => { + it('from react get counted [React 18.x]', async () => { function Example() { return []; } @@ -1947,15 +1987,15 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('can be cleared for the whole app', () => { + it('can be cleared for the whole app', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -1988,15 +2028,15 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('can be cleared for particular Fiber (only warnings)', () => { + it('can be cleared for particular Fiber (only warnings)', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2033,15 +2073,15 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('can be cleared for a particular Fiber (only errors)', () => { + it('can be cleared for a particular Fiber (only errors)', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2078,7 +2118,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('are updated when fibers are removed from the tree', () => { + it('are updated when fibers are removed from the tree', async () => { function ComponentWithWarning() { console.warn('test-only: render warning'); return null; @@ -2093,8 +2133,8 @@ describe('Store', () => { return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2112,8 +2152,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2129,8 +2169,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2144,8 +2184,8 @@ describe('Store', () => { ⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(`[root]`); expect(store.componentWithErrorCount).toBe(0); diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 5f00af92bf..c0ab51a171 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -509,7 +509,11 @@ describe('Store component filters', () => { const Component = ({shouldSuspend}) => { if (shouldSuspend) { - throw promise; + if (React.use) { + React.use(promise); + } else { + throw promise; + } } return null; }; diff --git a/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js b/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js index 585499fd81..759ce79590 100644 --- a/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js @@ -522,7 +522,11 @@ describe('StoreStress (Legacy Mode)', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Root = ({children}) => { @@ -1144,7 +1148,11 @@ describe('StoreStress (Legacy Mode)', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const MaybeSuspend = ({children, suspend}) => { diff --git a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js index 8dd4ce4284..4389f78cd2 100644 --- a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js @@ -38,7 +38,7 @@ describe('StoreStressConcurrent', () => { // This is a stress test for the tree mount/update/unmount traversal. // It renders different trees that should produce the same output. // @reactVersion >= 18.0 - it('should handle a stress test with different tree operations (Concurrent Mode)', () => { + it('should handle a stress test with different tree operations (Concurrent Mode)', async () => { let setShowX; const A = () => 'a'; const B = () => 'b'; @@ -151,26 +151,26 @@ describe('StoreStressConcurrent', () => { root = ReactDOMClient.createRoot(container); // Verify mounting 'abcde'. - act(() => root.render({cases[i]})); + await act(() => root.render({cases[i]})); expect(container.textContent).toMatch('abcde'); expect(print(store)).toEqual(snapshotForABCDE); // Verify switching to 'abxde'. - act(() => { + await act(() => { setShowX(true); }); expect(container.textContent).toMatch('abxde'); expect(print(store)).toBe(snapshotForABXDE); // Verify switching back to 'abcde'. - act(() => { + await act(() => { setShowX(false); }); expect(container.textContent).toMatch('abcde'); expect(print(store)).toBe(snapshotForABCDE); // Clean up. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -180,19 +180,19 @@ describe('StoreStressConcurrent', () => { root = ReactDOMClient.createRoot(container); for (let i = 0; i < cases.length; i++) { // Verify mounting 'abcde'. - act(() => root.render({cases[i]})); + await act(() => root.render({cases[i]})); expect(container.textContent).toMatch('abcde'); expect(print(store)).toEqual(snapshotForABCDE); // Verify switching to 'abxde'. - act(() => { + await act(() => { setShowX(true); }); expect(container.textContent).toMatch('abxde'); expect(print(store)).toBe(snapshotForABXDE); // Verify switching back to 'abcde'. - act(() => { + await act(() => { setShowX(false); }); expect(container.textContent).toMatch('abcde'); @@ -204,7 +204,7 @@ describe('StoreStressConcurrent', () => { }); // @reactVersion >= 18.0 - it('should handle stress test with reordering (Concurrent Mode)', () => { + it('should handle stress test with reordering (Concurrent Mode)', async () => { const A = () => 'a'; const B = () => 'b'; const C = () => 'c'; @@ -245,10 +245,10 @@ describe('StoreStressConcurrent', () => { let container = document.createElement('div'); for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => root.render({steps[i]})); + await act(() => root.render({steps[i]})); // We snapshot each step once so it doesn't regress. snapshots.push(print(store)); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -316,13 +316,13 @@ describe('StoreStressConcurrent', () => { for (let j = 0; j < steps.length; j++) { container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => root.render({steps[i]})); + await act(() => root.render({steps[i]})); expect(print(store)).toMatch(snapshots[i]); - act(() => root.render({steps[j]})); + await act(() => root.render({steps[j]})); expect(print(store)).toMatch(snapshots[j]); - act(() => root.render({steps[i]})); + await act(() => root.render({steps[i]})); expect(print(store)).toMatch(snapshots[i]); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -332,7 +332,7 @@ describe('StoreStressConcurrent', () => { for (let j = 0; j < steps.length; j++) { container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render(
{steps[i]}
@@ -340,7 +340,7 @@ describe('StoreStressConcurrent', () => { ), ); expect(print(store)).toMatch(snapshots[i]); - act(() => + await act(() => root.render(
{steps[j]}
@@ -348,7 +348,7 @@ describe('StoreStressConcurrent', () => { ), ); expect(print(store)).toMatch(snapshots[j]); - act(() => + await act(() => root.render(
{steps[i]}
@@ -356,7 +356,7 @@ describe('StoreStressConcurrent', () => { ), ); expect(print(store)).toMatch(snapshots[i]); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -392,7 +392,11 @@ describe('StoreStressConcurrent', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Root = ({children}) => { @@ -405,7 +409,7 @@ describe('StoreStressConcurrent', () => { let container = document.createElement('div'); for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -416,7 +420,7 @@ describe('StoreStressConcurrent', () => { ); // We snapshot each step once so it doesn't regress.d snapshots.push(print(store)); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -507,7 +511,7 @@ describe('StoreStressConcurrent', () => { // 2. Verify check Suspense can render same steps as initial fallback content. for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -521,7 +525,7 @@ describe('StoreStressConcurrent', () => { ), ); expect(print(store)).toEqual(snapshots[i]); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -531,7 +535,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -542,7 +546,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -554,7 +558,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -565,7 +569,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -576,7 +580,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -591,7 +595,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -607,7 +611,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -622,7 +626,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -633,7 +637,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -644,7 +648,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -660,7 +664,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -671,7 +675,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -682,7 +686,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -697,7 +701,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -709,7 +713,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -724,7 +728,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -735,7 +739,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -772,7 +776,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(snapshots[i]); // Trigger actual fallback. - act(() => + await act(() => root.render( @@ -788,7 +792,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(snapshots[j]); // Force fallback while we're in fallback mode. - act(() => { + await act(() => { bridge.send('overrideSuspense', { id: suspenseID, rendererID: store.getRendererIDForElement(suspenseID), @@ -799,7 +803,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(snapshots[j]); // Switch to primary mode. - act(() => + await act(() => root.render( @@ -859,7 +863,11 @@ describe('StoreStressConcurrent', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const MaybeSuspend = ({children, suspend}) => { @@ -890,7 +898,7 @@ describe('StoreStressConcurrent', () => { let container = document.createElement('div'); for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -903,7 +911,7 @@ describe('StoreStressConcurrent', () => { ); // We snapshot each step once so it doesn't regress. snapshots.push(print(store)); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -913,7 +921,7 @@ describe('StoreStressConcurrent', () => { const fallbackSnapshots = []; for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -928,7 +936,7 @@ describe('StoreStressConcurrent', () => { ); // We snapshot each step once so it doesn't regress. fallbackSnapshots.push(print(store)); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -1046,7 +1054,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1059,7 +1067,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1073,7 +1081,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1086,7 +1094,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1097,7 +1105,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1115,7 +1123,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(fallbackSnapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1134,7 +1142,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(fallbackSnapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1152,7 +1160,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(fallbackSnapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1163,7 +1171,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1176,7 +1184,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1190,7 +1198,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(fallbackSnapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1203,7 +1211,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1214,7 +1222,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1227,7 +1235,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(fallbackSnapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1241,7 +1249,7 @@ describe('StoreStressConcurrent', () => { // Verify the successful transition to steps[j]. expect(print(store)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1254,7 +1262,7 @@ describe('StoreStressConcurrent', () => { ); expect(print(store)).toEqual(fallbackSnapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1265,7 +1273,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1304,7 +1312,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(snapshots[i]); // Trigger actual fallback. - act(() => + await act(() => root.render( @@ -1318,7 +1326,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(fallbackSnapshots[j]); // Force fallback while we're in fallback mode. - act(() => { + await act(() => { bridge.send('overrideSuspense', { id: suspenseID, rendererID: store.getRendererIDForElement(suspenseID), @@ -1329,7 +1337,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(fallbackSnapshots[j]); // Switch to primary mode. - act(() => + await act(() => root.render( @@ -1355,7 +1363,7 @@ describe('StoreStressConcurrent', () => { expect(print(store)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 31db8a7433..b5b7fe9658 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -105,6 +105,8 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent import is from 'shared/objectIs'; import hasOwnProperty from 'shared/hasOwnProperty'; +import {getIODescription} from 'shared/ReactIODescription'; + import { getStackByFiberInDevAndProd, getOwnerStackByFiberInDev, @@ -4116,9 +4118,26 @@ export function attach( parentInstance, asyncInfo.owner, ); + const value: any = ioInfo.value; + let resolvedValue = undefined; + if ( + typeof value === 'object' && + value !== null && + typeof value.then === 'function' + ) { + switch (value.status) { + case 'fulfilled': + resolvedValue = value.value; + break; + case 'rejected': + resolvedValue = value.reason; + break; + } + } return { awaited: { name: ioInfo.name, + description: getIODescription(resolvedValue), start: ioInfo.start, end: ioInfo.end, value: ioInfo.value == null ? null : ioInfo.value, diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 979baa3e0a..6324e63da1 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -235,6 +235,7 @@ export type PathMatch = { // Serialized version of ReactIOInfo export type SerializedIOInfo = { name: string, + description: string, start: number, end: number, value: null | Promise, diff --git a/packages/react-devtools-shared/src/backendAPI.js b/packages/react-devtools-shared/src/backendAPI.js index 9f96215026..d6aa18cd31 100644 --- a/packages/react-devtools-shared/src/backendAPI.js +++ b/packages/react-devtools-shared/src/backendAPI.js @@ -218,6 +218,7 @@ function backendToFrontendSerializedAsyncInfo( return { awaited: { name: ioInfo.name, + description: ioInfo.description, start: ioInfo.start, end: ioInfo.end, value: ioInfo.value, diff --git a/packages/react-devtools-shared/src/devtools/cache.js b/packages/react-devtools-shared/src/devtools/cache.js index eda30ed56a..5ed2133b06 100644 --- a/packages/react-devtools-shared/src/devtools/cache.js +++ b/packages/react-devtools-shared/src/devtools/cache.js @@ -7,7 +7,12 @@ * @flow */ -import type {ReactContext, Thenable} from 'shared/ReactTypes'; +import type { + ReactContext, + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; import * as React from 'react'; import {createContext} from 'react'; @@ -26,27 +31,6 @@ import {createContext} from 'react'; export type {Thenable}; -interface Suspender { - then(resolve: () => mixed, reject: () => mixed): mixed; -} - -type PendingResult = { - status: 0, - value: Suspender, -}; - -type ResolvedResult = { - status: 1, - value: Value, -}; - -type RejectedResult = { - status: 2, - value: mixed, -}; - -type Result = PendingResult | ResolvedResult | RejectedResult; - export type Resource = { clear(): void, invalidate(Key): void, @@ -55,10 +39,6 @@ export type Resource = { write(Key, Value): void, }; -const Pending = 0; -const Resolved = 1; -const Rejected = 2; - let readContext; if (typeof React.use === 'function') { readContext = function (Context: ReactContext) { @@ -115,33 +95,25 @@ function accessResult( fetch: Input => Thenable, input: Input, key: Key, -): Result { +): Thenable { const entriesForResource = getEntriesForResource(resource); const entry = entriesForResource.get(key); if (entry === undefined) { const thenable = fetch(input); thenable.then( value => { - if (newResult.status === Pending) { - const resolvedResult: ResolvedResult = (newResult: any); - resolvedResult.status = Resolved; - resolvedResult.value = value; - } + const fulfilledThenable: FulfilledThenable = (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; }, error => { - if (newResult.status === Pending) { - const rejectedResult: RejectedResult = (newResult: any); - rejectedResult.status = Rejected; - rejectedResult.value = error; - } + const rejectedThenable: RejectedThenable = (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = error; }, ); - const newResult: PendingResult = { - status: Pending, - value: thenable, - }; - entriesForResource.set(key, newResult); - return newResult; + entriesForResource.set(key, thenable); + return thenable; } else { return entry; } @@ -167,23 +139,22 @@ export function createResource( readContext(CacheContext); const key = hashInput(input); - const result: Result = accessResult(resource, fetch, input, key); + const result: Thenable = accessResult(resource, fetch, input, key); + if (typeof React.use === 'function') { + return React.use(result); + } + switch (result.status) { - case Pending: { - const suspender = result.value; - throw suspender; - } - case Resolved: { + case 'fulfilled': { const value = result.value; return value; } - case Rejected: { - const error = result.value; + case 'rejected': { + const error = result.reason; throw error; } default: - // Should be unreachable - return (undefined: any); + throw result; } }, @@ -198,12 +169,13 @@ export function createResource( write(key: Key, value: Value): void { const entriesForResource = getEntriesForResource(resource); - const resolvedResult: ResolvedResult = { - status: Resolved, + const fulfilledThenable: FulfilledThenable = (Promise.resolve( value, - }; + ): any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; - entriesForResource.set(key, resolvedResult); + entriesForResource.set(key, fulfilledThenable); }, }; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.css b/packages/react-devtools-shared/src/devtools/views/Components/Components.css index 5624f23489..8df59f72f1 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.css @@ -16,7 +16,6 @@ .TreeWrapper { flex: 0 0 var(--horizontal-resize-percentage); - overflow: auto; } .InspectedElementWrapper { @@ -32,13 +31,20 @@ .ResizeBar { position: absolute; - left: -2px; + /* + * moving the bar out of its bounding box might cause its hitbox to overlap + * with another scrollbar creating disorienting UX where you both resize and scroll + * at the same time. + * If you adjust this value, double check that starting resize right on this edge + * doesn't also cause scroll + */ + left: 1px; width: 5px; height: 100%; cursor: ew-resize; } -@media screen and (max-width: 600px) { +@container devtools (width < 600px) { .Components { flex-direction: column; } @@ -52,7 +58,7 @@ } .ResizeBar { - top: -2px; + top: 1px; left: 0; width: 100%; height: 5px; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.js b/packages/react-devtools-shared/src/devtools/views/Components/Components.js index 1d99317ba0..b6278c2db9 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.js @@ -29,7 +29,6 @@ type Orientation = 'horizontal' | 'vertical'; type ResizeActionType = | 'ACTION_SET_DID_MOUNT' - | 'ACTION_SET_IS_RESIZING' | 'ACTION_SET_HORIZONTAL_PERCENTAGE' | 'ACTION_SET_VERTICAL_PERCENTAGE'; @@ -40,7 +39,6 @@ type ResizeAction = { type ResizeState = { horizontalPercentage: number, - isResizing: boolean, verticalPercentage: number, }; @@ -81,82 +79,81 @@ function Components(_: {}) { return () => clearTimeout(timeoutID); }, [horizontalPercentage, verticalPercentage]); - const {isResizing} = state; + const onResizeStart = (event: SyntheticPointerEvent) => { + const element = event.currentTarget; + element.setPointerCapture(event.pointerId); + }; - const onResizeStart = () => - dispatch({type: 'ACTION_SET_IS_RESIZING', payload: true}); + const onResizeEnd = (event: SyntheticPointerEvent) => { + const element = event.currentTarget; + element.releasePointerCapture(event.pointerId); + }; - let onResize; - let onResizeEnd; - if (isResizing) { - onResizeEnd = () => - dispatch({type: 'ACTION_SET_IS_RESIZING', payload: false}); + const onResize = (event: SyntheticPointerEvent) => { + const element = event.currentTarget; + const isResizing = element.hasPointerCapture(event.pointerId); + if (!isResizing) { + return; + } - // $FlowFixMe[missing-local-annot] - onResize = event => { - const resizeElement = resizeElementRef.current; - const wrapperElement = wrapperElementRef.current; + const resizeElement = resizeElementRef.current; + const wrapperElement = wrapperElementRef.current; - if (!isResizing || wrapperElement === null || resizeElement === null) { - return; - } + if (wrapperElement === null || resizeElement === null) { + return; + } - event.preventDefault(); + event.preventDefault(); - const orientation = getOrientation(wrapperElement); + const orientation = getOrientation(wrapperElement); - const {height, width, left, top} = wrapperElement.getBoundingClientRect(); + const {height, width, left, top} = wrapperElement.getBoundingClientRect(); - const currentMousePosition = + const currentMousePosition = + orientation === 'horizontal' ? event.clientX - left : event.clientY - top; + + const boundaryMin = MINIMUM_SIZE; + const boundaryMax = + orientation === 'horizontal' + ? width - MINIMUM_SIZE + : height - MINIMUM_SIZE; + + const isMousePositionInBounds = + currentMousePosition > boundaryMin && currentMousePosition < boundaryMax; + + if (isMousePositionInBounds) { + const resizedElementDimension = + orientation === 'horizontal' ? width : height; + const actionType = orientation === 'horizontal' - ? event.clientX - left - : event.clientY - top; + ? 'ACTION_SET_HORIZONTAL_PERCENTAGE' + : 'ACTION_SET_VERTICAL_PERCENTAGE'; + const percentage = (currentMousePosition / resizedElementDimension) * 100; - const boundaryMin = MINIMUM_SIZE; - const boundaryMax = - orientation === 'horizontal' - ? width - MINIMUM_SIZE - : height - MINIMUM_SIZE; + setResizeCSSVariable(resizeElement, orientation, percentage); - const isMousePositionInBounds = - currentMousePosition > boundaryMin && - currentMousePosition < boundaryMax; - - if (isMousePositionInBounds) { - const resizedElementDimension = - orientation === 'horizontal' ? width : height; - const actionType = - orientation === 'horizontal' - ? 'ACTION_SET_HORIZONTAL_PERCENTAGE' - : 'ACTION_SET_VERTICAL_PERCENTAGE'; - const percentage = - (currentMousePosition / resizedElementDimension) * 100; - - setResizeCSSVariable(resizeElement, orientation, percentage); - - dispatch({ - type: actionType, - payload: currentMousePosition / resizedElementDimension, - }); - } - }; - } + dispatch({ + type: actionType, + payload: currentMousePosition / resizedElementDimension, + }); + } + }; return ( -
+
-
+
@@ -193,18 +190,12 @@ function initResizeState(): ResizeState { return { horizontalPercentage, - isResizing: false, verticalPercentage, }; } function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState { switch (action.type) { - case 'ACTION_SET_IS_RESIZING': - return { - ...state, - isResizing: action.payload, - }; case 'ACTION_SET_HORIZONTAL_PERCENTAGE': return { ...state, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js index c782199122..44950acddf 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js @@ -20,7 +20,7 @@ import {getMetaValueLabel, serializeHooksForCopy} from '../utils'; import Store from '../../store'; import styles from './InspectedElementHooksTree.css'; import {meta} from '../../../hydration'; -import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache'; +import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation'; import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; import isArray from 'react-devtools-shared/src/isArray'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css index c0d3c95bec..41e510b7c1 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css @@ -75,11 +75,25 @@ color: var(--color-expand-collapse-toggle); } -.CollapsableHeaderTitle { - flex: 1 1 auto; +.CollapsableHeaderTitle, .CollapsableHeaderDescription, .CollapsableHeaderSeparator, .CollapsableHeaderFiller { font-family: var(--font-family-monospace); font-size: var(--font-size-monospace-normal); text-align: left; + white-space: nowrap; +} +.CollapsableHeaderTitle { + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; +} + +.CollapsableHeaderSeparator { + flex: 0 0 auto; + white-space: pre; +} + +.CollapsableHeaderFiller { + flex: 1 0 0; } .CollapsableContent { @@ -108,4 +122,4 @@ .TimeBarSpanErrored { background-color: var(--color-timespan-background-errored); -} \ No newline at end of file +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js index 93f4078a0e..9deddef14b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -38,6 +38,37 @@ type RowProps = { maxTime: number, }; +function getShortDescription(name: string, description: string): string { + const descMaxLength = 30 - name.length; + if (descMaxLength > 1) { + const l = description.length; + if (l > 0 && l <= descMaxLength) { + // We can fit the full description + return description; + } else if ( + description.startsWith('http://') || + description.startsWith('https://') || + description.startsWith('/') + ) { + // Looks like a URL. Let's see if we can extract something shorter. + // We don't have to do a full parse so let's try something cheaper. + let queryIdx = description.indexOf('?'); + if (queryIdx === -1) { + queryIdx = description.length; + } + if (description.charCodeAt(queryIdx - 1) === 47 /* "/" */) { + // Ends with slash. Look before that. + queryIdx--; + } + const slashIdx = description.lastIndexOf('/', queryIdx - 1); + // This may now be either the file name or the host. + // Include the slash to make it more obvious what we trimmed. + return '…' + description.slice(slashIdx, queryIdx); + } + } + return ''; +} + function SuspendedByRow({ bridge, element, @@ -50,6 +81,9 @@ function SuspendedByRow({ }: RowProps) { const [isOpen, setIsOpen] = useState(false); const name = asyncInfo.awaited.name; + const description = asyncInfo.awaited.description; + const longName = description === '' ? name : name + ' (' + description + ')'; + const shortDescription = getShortDescription(name, description); let stack; let owner; if (asyncInfo.stack === null || asyncInfo.stack.length === 0) { @@ -83,12 +117,22 @@ function SuspendedByRow({