From 9f01dc13d8fc211f4883593d31448f6d87f9195e Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Thu, 7 Mar 2024 11:30:02 -0500 Subject: [PATCH 1/4] Pass ReactHooks-test.internal --- .../src/__tests__/ReactHooks-test.internal.js | 636 +++++++++++------- 1 file changed, 396 insertions(+), 240 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index c43d4ac3e1e01..77fb8621a2658 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -25,7 +25,6 @@ let waitForThrow; describe('ReactHooks', () => { beforeEach(() => { jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); React = require('react'); @@ -42,16 +41,18 @@ describe('ReactHooks', () => { if (__DEV__) { // useDebugValue is a DEV-only hook - it('useDebugValue throws when used in a class component', () => { + it('useDebugValue throws when used in a class component', async () => { class Example extends React.Component { render() { React.useDebugValue('abc'); return null; } } - expect(() => { - ReactTestRenderer.create(); - }).toThrow( + await expect(async () => { + await act(() => { + ReactTestRenderer.create(); + }); + }).rejects.toThrow( 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen' + ' for one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + @@ -569,7 +570,7 @@ describe('ReactHooks', () => { expect(root).toMatchRenderedOutput('105'); }); - it('warns about variable number of dependencies', () => { + it('warns about variable number of dependencies', async () => { const {useLayoutEffect} = React; function App(props) { useLayoutEffect(() => { @@ -577,10 +578,15 @@ describe('ReactHooks', () => { }, props.dependencies); return props.dependencies; } - const root = ReactTestRenderer.create(); + let root; + await act(() => { + root = ReactTestRenderer.create(); + }); assertLog(['Did commit: A']); - expect(() => { - root.update(); + await expect(async () => { + await act(() => { + root.update(); + }); }).toErrorDev([ 'Warning: The final argument passed to useLayoutEffect changed size ' + 'between renders. The order and size of this array must remain ' + @@ -590,7 +596,7 @@ describe('ReactHooks', () => { ]); }); - it('warns if switching from dependencies to no dependencies', () => { + it('warns if switching from dependencies to no dependencies', async () => { const {useMemo} = React; function App({text, hasDeps}) { const resolvedText = useMemo( @@ -603,13 +609,20 @@ describe('ReactHooks', () => { return resolvedText; } - const root = ReactTestRenderer.create(null); - root.update(); + let root; + await act(() => { + root = ReactTestRenderer.create(null); + }); + await act(() => { + root.update(); + }); assertLog(['Compute']); expect(root).toMatchRenderedOutput('HELLO'); - expect(() => { - root.update(); + await expect(async () => { + await act(() => { + root.update(); + }); }).toErrorDev([ 'Warning: useMemo received a final argument during this render, but ' + 'not during the previous render. Even though the final argument is ' + @@ -678,7 +691,7 @@ describe('ReactHooks', () => { }); }); - it('warns if deps is not an array for useImperativeHandle', () => { + it('warns if deps is not an array for useImperativeHandle', async () => { const {useImperativeHandle} = React; const App = React.forwardRef((props, ref) => { @@ -686,15 +699,23 @@ describe('ReactHooks', () => { return null; }); - expect(() => { - ReactTestRenderer.create(); + await expect(async () => { + await act(() => { + ReactTestRenderer.create(); + }); }).toErrorDev([ 'Warning: useImperativeHandle received a final argument that is not an array (instead, received `string`). ' + 'When specified, the final argument must be an array.', ]); - ReactTestRenderer.create(); - ReactTestRenderer.create(); - ReactTestRenderer.create(); + await act(() => { + ReactTestRenderer.create(); + }); + await act(() => { + ReactTestRenderer.create(); + }); + await act(() => { + ReactTestRenderer.create(); + }); }); it('does not forget render phase useState updates inside an effect', async () => { @@ -767,7 +788,7 @@ describe('ReactHooks', () => { expect(root).toMatchRenderedOutput('4'); }); - it('warns for bad useImperativeHandle first arg', () => { + it('warns for bad useImperativeHandle first arg', async () => { const {useImperativeHandle} = React; function App() { useImperativeHandle({ @@ -776,10 +797,12 @@ describe('ReactHooks', () => { return null; } - expect(() => { - expect(() => { - ReactTestRenderer.create(); - }).toThrow('create is not a function'); + await expect(async () => { + await expect(async () => { + await act(() => { + ReactTestRenderer.create(); + }); + }).rejects.toThrow('create is not a function'); }).toErrorDev([ 'Expected useImperativeHandle() first argument to either be a ' + 'ref callback or React.createRef() object. ' + @@ -789,7 +812,7 @@ describe('ReactHooks', () => { ]); }); - it('warns for bad useImperativeHandle second arg', () => { + it('warns for bad useImperativeHandle second arg', async () => { const {useImperativeHandle} = React; const App = React.forwardRef((props, ref) => { useImperativeHandle(ref, { @@ -798,8 +821,10 @@ describe('ReactHooks', () => { return null; }); - expect(() => { - ReactTestRenderer.create(); + await expect(async () => { + await act(() => { + ReactTestRenderer.create(); + }); }).toErrorDev([ 'Expected useImperativeHandle() second argument to be a function ' + 'that creates a handle. Instead received: object.', @@ -807,7 +832,7 @@ describe('ReactHooks', () => { }); // https://github.com/facebook/react/issues/14022 - it('works with ReactDOMServer calls inside a component', () => { + it('works with ReactDOMServer calls inside a component', async () => { const {useState} = React; function App(props) { const markup1 = ReactDOMServer.renderToString(

hello

); @@ -815,11 +840,14 @@ describe('ReactHooks', () => { const [counter] = useState(0); return markup1 + counter + markup2; } - const root = ReactTestRenderer.create(); + let root; + await act(() => { + root = ReactTestRenderer.create(); + }); expect(root.toJSON()).toMatchSnapshot(); }); - it("throws when calling hooks inside .memo's compare function", () => { + it("throws when calling hooks inside .memo's compare function", async () => { const {useState} = React; function App() { useState(0); @@ -830,9 +858,16 @@ describe('ReactHooks', () => { return false; }); - const root = ReactTestRenderer.create(); + let root; + await act(() => { + root = ReactTestRenderer.create(); + }); // trying to render again should trigger comparison and throw - expect(() => root.update()).toThrow( + await expect( + act(() => { + root.update(); + }), + ).rejects.toThrow( 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + ' one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + @@ -841,7 +876,11 @@ describe('ReactHooks', () => { 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.', ); // the next round, it does a fresh mount, so should render - expect(() => root.update()).not.toThrow( + await expect( + act(() => { + root.update(); + }), + ).resolves.not.toThrow( 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + ' one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + @@ -850,7 +889,11 @@ describe('ReactHooks', () => { 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.', ); // and then again, fail - expect(() => root.update()).toThrow( + await expect( + act(() => { + root.update(); + }), + ).rejects.toThrow( 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + ' one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + @@ -860,7 +903,7 @@ describe('ReactHooks', () => { ); }); - it('warns when calling hooks inside useMemo', () => { + it('warns when calling hooks inside useMemo', async () => { const {useMemo, useState} = React; function App() { useMemo(() => { @@ -868,12 +911,16 @@ describe('ReactHooks', () => { }); return null; } - expect(() => ReactTestRenderer.create()).toErrorDev( + await expect(async () => { + await act(() => { + ReactTestRenderer.create(); + }); + }).toErrorDev( 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.', ); }); - it('warns when reading context inside useMemo', () => { + it('warns when reading context inside useMemo', async () => { const {useMemo, createContext} = React; const ReactCurrentDispatcher = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED @@ -886,12 +933,14 @@ describe('ReactHooks', () => { }, []); } - expect(() => ReactTestRenderer.create()).toErrorDev( - 'Context can only be read while React is rendering', - ); + await expect(async () => { + await act(() => { + ReactTestRenderer.create(); + }); + }).toErrorDev('Context can only be read while React is rendering'); }); - it('warns when reading context inside useMemo after reading outside it', () => { + it('warns when reading context inside useMemo after reading outside it', async () => { const {useMemo, createContext} = React; const ReactCurrentDispatcher = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED @@ -908,9 +957,11 @@ describe('ReactHooks', () => { }, []); } - expect(() => ReactTestRenderer.create()).toErrorDev( - 'Context can only be read while React is rendering', - ); + await expect(async () => { + await act(() => { + ReactTestRenderer.create(); + }); + }).toErrorDev('Context can only be read while React is rendering'); expect(firstRead).toBe('light'); expect(secondRead).toBe('light'); }); @@ -938,7 +989,7 @@ describe('ReactHooks', () => { }); // Throws because there's no runtime cost for being strict here. - it('throws when reading context inside useLayoutEffect', () => { + it('throws when reading context inside useLayoutEffect', async () => { const {useLayoutEffect, createContext} = React; const ReactCurrentDispatcher = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED @@ -952,13 +1003,17 @@ describe('ReactHooks', () => { return null; } - expect(() => ReactTestRenderer.create()).toThrow( + await expect( + act(() => { + ReactTestRenderer.create(); + }), + ).rejects.toThrow( // The exact message doesn't matter, just make sure we don't allow this 'Context can only be read while React is rendering', ); }); - it('warns when reading context inside useReducer', () => { + it('warns when reading context inside useReducer', async () => { const {useReducer, createContext} = React; const ReactCurrentDispatcher = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED @@ -976,13 +1031,15 @@ describe('ReactHooks', () => { return null; } - expect(() => ReactTestRenderer.create()).toErrorDev([ - 'Context can only be read while React is rendering', - ]); + await expect(async () => { + await act(() => { + ReactTestRenderer.create(); + }); + }).toErrorDev(['Context can only be read while React is rendering']); }); // Edge case. - it('warns when reading context inside eager useReducer', () => { + it('warns when reading context inside eager useReducer', async () => { const {useState, createContext} = React; const ThemeContext = createContext('light'); @@ -1007,20 +1064,22 @@ describe('ReactHooks', () => { } } - expect(() => - ReactTestRenderer.create( - <> - - - , - ), - ).toErrorDev([ + await expect(async () => { + await act(() => { + ReactTestRenderer.create( + <> + + + , + ); + }); + }).toErrorDev([ 'Context can only be read while React is rendering', 'Cannot update a component (`Fn`) while rendering a different component (`Cls`).', ]); }); - it('warns when calling hooks inside useReducer', () => { + it('warns when calling hooks inside useReducer', async () => { const {useReducer, useState, useRef} = React; function App() { @@ -1035,10 +1094,12 @@ describe('ReactHooks', () => { return value; } - expect(() => { - expect(() => { - ReactTestRenderer.create(); - }).toThrow( + await expect(async () => { + await expect(async () => { + await act(() => { + ReactTestRenderer.create(); + }); + }).rejects.toThrow( 'Update hook called on initial render. This is likely a bug in React. Please file an issue.', ); }).toErrorDev([ @@ -1051,10 +1112,13 @@ describe('ReactHooks', () => { '1. useReducer useReducer\n' + '2. useState useRef\n' + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', ]); }); - it("warns when calling hooks inside useState's initialize function", () => { + it("warns when calling hooks inside useState's initialize function", async () => { const {useState, useRef} = React; function App() { useState(() => { @@ -1063,7 +1127,11 @@ describe('ReactHooks', () => { }); return null; } - expect(() => ReactTestRenderer.create()).toErrorDev( + await expect(async () => { + await act(() => { + ReactTestRenderer.create(); + }); + }).toErrorDev( 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.', ); }); @@ -1097,12 +1165,14 @@ describe('ReactHooks', () => { } } - expect(() => { - ReactTestRenderer.create( - - - , - ); + await expect(async () => { + await act(() => { + ReactTestRenderer.create( + + + , + ); + }); }).toErrorDev([ 'Context can only be read while React is rendering', 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', @@ -1132,19 +1202,21 @@ describe('ReactHooks', () => { }); // Verify warnings don't get permanently disabled. - expect(() => { - ReactTestRenderer.create( - - - , - ); + await expect(async () => { + await act(() => { + ReactTestRenderer.create( + + + , + ); + }); }).toErrorDev([ 'Context can only be read while React is rendering', 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', ]); }); - it('warns when reading context inside useMemo', () => { + it('warns when reading context inside useMemo', async () => { const {useMemo, createContext} = React; const ReactCurrentDispatcher = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED @@ -1157,12 +1229,14 @@ describe('ReactHooks', () => { }, []); } - expect(() => ReactTestRenderer.create()).toErrorDev( - 'Context can only be read while React is rendering', - ); + await expect(async () => { + await act(() => { + ReactTestRenderer.create(); + }); + }).toErrorDev('Context can only be read while React is rendering'); }); - it('double-invokes components with Hooks in Strict Mode', () => { + it('double-invokes components with Hooks in Strict Mode', async () => { ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = true; const {useState, StrictMode} = React; @@ -1211,74 +1285,105 @@ describe('ReactHooks', () => { }; } - const renderer = ReactTestRenderer.create(null); + let renderer; + await act(() => { + renderer = ReactTestRenderer.create(null); + }); renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) { renderCount = 0; - expect(() => renderer.update()).toErrorDev( + await expect(async () => { + await act(() => { + renderer.update(); + }); + }).toErrorDev( 'Warning: The component appears to be a function component that returns a class instance. ' + 'Change Factory to a class that extends React.Component instead. ' + "If you can't use a class try assigning the prototype on the function as a workaround. " + @@ -1287,90 +1392,120 @@ describe('ReactHooks', () => { ); expect(renderCount).toBe(1); renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class } renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update(); + await act(() => { + renderer.update(); + }); expect(renderCount).toBe(1); renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks renderCount = 0; - renderer.update( - - - , - ); + await act(() => { + renderer.update( + + + , + ); + }); expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks }); - it('double-invokes useMemo in DEV StrictMode despite []', () => { + it('double-invokes useMemo in DEV StrictMode despite []', async () => { ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = true; const {useMemo, StrictMode} = React; @@ -1383,11 +1518,13 @@ describe('ReactHooks', () => { } useMemoCount = 0; - ReactTestRenderer.create( - - - , - ); + await act(() => { + ReactTestRenderer.create( + + + , + ); + }); expect(useMemoCount).toBe(__DEV__ ? 2 : 1); // Has Hooks }); @@ -1588,18 +1725,18 @@ describe('ReactHooks', () => { root = ReactTestRenderer.create(); }); - await act(() => { - expect(() => { + await expect(async () => { + await act(() => { root.update(); - }).toThrow('Rendered fewer hooks than expected. '); - }); + }); + }).rejects.toThrow('Rendered fewer hooks than expected. '); }); }); it( 'warns on using differently ordered hooks ' + '(useImperativeHandleHelper, useMemoHelper) on subsequent renders', - () => { + async () => { function App(props) { /* eslint-disable no-unused-vars */ if (props.update) { @@ -1614,15 +1751,17 @@ describe('ReactHooks', () => { return null; /* eslint-enable no-unused-vars */ } - const root = ReactTestRenderer.create(); - expect(() => { - try { + let root; + await act(() => { + root = ReactTestRenderer.create(); + }); + await expect(async () => { + await act(() => { root.update(); - } catch (error) { - // Swapping certain types of hooks will cause runtime errors. - // This is okay as far as this test is concerned. - // We just want to verify that warnings are always logged. - } + }).catch(e => {}); + // Swapping certain types of hooks will cause runtime errors. + // This is okay as far as this test is concerned. + // We just want to verify that warnings are always logged. }).toErrorDev([ 'Warning: React has detected a change in the order of Hooks called by App. ' + 'This will lead to bugs and errors if not fixed. For more information, ' + @@ -1638,11 +1777,13 @@ describe('ReactHooks', () => { ]); // further warnings for this component are silenced - root.update(); + await act(() => { + root.update(); + }); }, ); - it('detects a bad hook order even if the component throws', () => { + it('detects a bad hook order even if the component throws', async () => { const {useState, useReducer} = React; function useCustomHook() { useState(0); @@ -1660,11 +1801,16 @@ describe('ReactHooks', () => { return null; /* eslint-enable no-unused-vars */ } - const root = ReactTestRenderer.create(); - expect(() => { - expect(() => root.update()).toThrow( - 'custom error', - ); + let root; + await act(() => { + root = ReactTestRenderer.create(); + }); + await expect(async () => { + await expect(async () => { + await act(() => { + root.update(); + }); + }).rejects.toThrow('custom error'); }).toErrorDev([ 'Warning: React has detected a change in the order of Hooks called by App. ' + 'This will lead to bugs and errors if not fixed. For more information, ' + @@ -1696,17 +1842,19 @@ describe('ReactHooks', () => { return null; } - expect(() => { - ReactTestRenderer.create( - <> - - - , - ); - }).toThrow('Hello'); + await expect(async () => { + await act(() => { + ReactTestRenderer.create( + <> + + + , + ); + }); + }).rejects.toThrow('Hello'); if (__DEV__) { - expect(console.error).toHaveBeenCalledTimes(2); + expect(jest.mocked(console.error)).toHaveBeenCalledTimes(2); expect(console.error.mock.calls[0][0]).toContain( 'Warning: Cannot update a component (`%s`) while rendering ' + 'a different component (`%s`).', @@ -1755,15 +1903,14 @@ describe('ReactHooks', () => { ReactTestRenderer.create(); }); - expect(() => { - globalListener(); - globalListener(); - }).toErrorDev([ - 'An update to C inside a test was not wrapped in act', - 'An update to C inside a test was not wrapped in act', - // Note: should *not* warn about updates on unmounted component. - // Because there's no way for component to know it got unmounted. - ]); + // Note: should *not* warn about updates on unmounted component. + // Because there's no way for component to know it got unmounted. + await expect( + act(() => { + globalListener(); + globalListener(); + }), + ).resolves.not.toThrow(); }); // Regression test for https://github.com/facebook/react/issues/14790 @@ -1771,11 +1918,12 @@ describe('ReactHooks', () => { const {Suspense, useState} = React; let wasSuspended = false; + let resolve; function trySuspend() { if (!wasSuspended) { - throw new Promise(resolve => { + throw new Promise(r => { wasSuspended = true; - resolve(); + resolve = r; }); } } @@ -1787,14 +1935,16 @@ describe('ReactHooks', () => { } const Wrapper = React.memo(Child); - const root = ReactTestRenderer.create( - - - , - ); + let root; + await act(() => { + root = ReactTestRenderer.create( + + + , + ); + }); expect(root).toMatchRenderedOutput('loading'); - await Promise.resolve(); - await waitForAll([]); + await act(resolve); expect(root).toMatchRenderedOutput('hello'); }); @@ -1803,11 +1953,12 @@ describe('ReactHooks', () => { const {Suspense, useState} = React; let wasSuspended = false; + let resolve; function trySuspend() { if (!wasSuspended) { - throw new Promise(resolve => { + throw new Promise(r => { wasSuspended = true; - resolve(); + resolve = r; }); } } @@ -1819,14 +1970,16 @@ describe('ReactHooks', () => { } const Wrapper = React.forwardRef(render); - const root = ReactTestRenderer.create( - - - , - ); + let root; + await act(() => { + root = ReactTestRenderer.create( + + + , + ); + }); expect(root).toMatchRenderedOutput('loading'); - await Promise.resolve(); - await waitForAll([]); + await act(resolve); expect(root).toMatchRenderedOutput('hello'); }); @@ -1835,11 +1988,12 @@ describe('ReactHooks', () => { const {Suspense, useState} = React; let wasSuspended = false; + let resolve; function trySuspend() { if (!wasSuspended) { - throw new Promise(resolve => { + throw new Promise(r => { wasSuspended = true; - resolve(); + resolve = r; }); } } @@ -1851,14 +2005,16 @@ describe('ReactHooks', () => { } const Wrapper = React.memo(React.forwardRef(render)); - const root = ReactTestRenderer.create( - - - , - ); + let root; + await act(() => { + root = ReactTestRenderer.create( + + + , + ); + }); expect(root).toMatchRenderedOutput('loading'); - await Promise.resolve(); - await waitForAll([]); + await act(resolve); expect(root).toMatchRenderedOutput('hello'); }); From 8343c4d7f7c9d910da3d4c39105d5eab458e7008 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Mon, 18 Mar 2024 12:00:22 -0400 Subject: [PATCH 2/4] Use concurrent root in ReactHooks-test --- .../src/__tests__/ReactHooks-test.internal.js | 127 ++++++++++++------ 1 file changed, 88 insertions(+), 39 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 77fb8621a2658..f6c6acf0e622c 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -50,7 +50,9 @@ describe('ReactHooks', () => { } await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, { + unstable_unstable_isConcurrent: true, + }); }); }).rejects.toThrow( 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen' + @@ -580,7 +582,9 @@ describe('ReactHooks', () => { } let root; await act(() => { - root = ReactTestRenderer.create(); + root = ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); assertLog(['Did commit: A']); await expect(async () => { @@ -611,7 +615,7 @@ describe('ReactHooks', () => { let root; await act(() => { - root = ReactTestRenderer.create(null); + root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); }); await act(() => { root.update(); @@ -643,7 +647,9 @@ describe('ReactHooks', () => { await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); }).toErrorDev([ 'Warning: useEffect received a final argument that is not an array (instead, received `string`). ' + @@ -657,7 +663,9 @@ describe('ReactHooks', () => { ]); await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); }).toErrorDev([ 'Warning: useEffect received a final argument that is not an array (instead, received `number`). ' + @@ -671,7 +679,9 @@ describe('ReactHooks', () => { ]); await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); }).toErrorDev([ 'Warning: useEffect received a final argument that is not an array (instead, received `object`). ' + @@ -685,9 +695,15 @@ describe('ReactHooks', () => { ]); await act(() => { - ReactTestRenderer.create(); - ReactTestRenderer.create(); - ReactTestRenderer.create(); + ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); + ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); + ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); }); @@ -701,20 +717,28 @@ describe('ReactHooks', () => { await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); }).toErrorDev([ 'Warning: useImperativeHandle received a final argument that is not an array (instead, received `string`). ' + 'When specified, the final argument must be an array.', ]); await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); }); @@ -734,7 +758,7 @@ describe('ReactHooks', () => { return counter; } - const root = ReactTestRenderer.create(null); + const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); await act(() => { root.update(); }); @@ -758,7 +782,7 @@ describe('ReactHooks', () => { return counter; } - const root = ReactTestRenderer.create(null); + const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); await act(() => { root.update(); }); @@ -781,7 +805,7 @@ describe('ReactHooks', () => { return counter; } - const root = ReactTestRenderer.create(null); + const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); await act(() => { root.update(); }); @@ -800,7 +824,7 @@ describe('ReactHooks', () => { await expect(async () => { await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); }).rejects.toThrow('create is not a function'); }).toErrorDev([ @@ -823,7 +847,7 @@ describe('ReactHooks', () => { await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); }).toErrorDev([ 'Expected useImperativeHandle() second argument to be a function ' + @@ -842,7 +866,7 @@ describe('ReactHooks', () => { } let root; await act(() => { - root = ReactTestRenderer.create(); + root = ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); expect(root.toJSON()).toMatchSnapshot(); }); @@ -860,7 +884,9 @@ describe('ReactHooks', () => { let root; await act(() => { - root = ReactTestRenderer.create(); + root = ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); // trying to render again should trigger comparison and throw await expect( @@ -913,7 +939,7 @@ describe('ReactHooks', () => { } await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); }).toErrorDev( 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.', @@ -935,7 +961,7 @@ describe('ReactHooks', () => { await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); }).toErrorDev('Context can only be read while React is rendering'); }); @@ -959,7 +985,7 @@ describe('ReactHooks', () => { await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); }).toErrorDev('Context can only be read while React is rendering'); expect(firstRead).toBe('light'); @@ -982,7 +1008,7 @@ describe('ReactHooks', () => { } await act(async () => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); // The exact message doesn't matter, just make sure we don't allow this await waitForThrow('Context can only be read while React is rendering'); }); @@ -1005,7 +1031,7 @@ describe('ReactHooks', () => { await expect( act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }), ).rejects.toThrow( // The exact message doesn't matter, just make sure we don't allow this @@ -1033,7 +1059,7 @@ describe('ReactHooks', () => { await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); }).toErrorDev(['Context can only be read while React is rendering']); }); @@ -1071,6 +1097,7 @@ describe('ReactHooks', () => { , + {unstable_isConcurrent: true}, ); }); }).toErrorDev([ @@ -1097,7 +1124,7 @@ describe('ReactHooks', () => { await expect(async () => { await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); }).rejects.toThrow( 'Update hook called on initial render. This is likely a bug in React. Please file an issue.', @@ -1113,8 +1140,6 @@ describe('ReactHooks', () => { '2. useState useRef\n' + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', ]); }); @@ -1129,7 +1154,7 @@ describe('ReactHooks', () => { } await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); }).toErrorDev( 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.', @@ -1171,11 +1196,15 @@ describe('ReactHooks', () => { , + {unstable_isConcurrent: true}, ); }); }).toErrorDev([ 'Context can only be read while React is rendering', 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', + + 'Context can only be read while React is rendering', + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', ]); function Valid() { @@ -1198,7 +1227,7 @@ describe('ReactHooks', () => { // Verify it doesn't think we're still inside a Hook. // Should have no warnings. await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); // Verify warnings don't get permanently disabled. @@ -1208,11 +1237,15 @@ describe('ReactHooks', () => { , + {unstable_isConcurrent: true}, ); }); }).toErrorDev([ 'Context can only be read while React is rendering', 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', + + 'Context can only be read while React is rendering', + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', ]); }); @@ -1231,7 +1264,7 @@ describe('ReactHooks', () => { await expect(async () => { await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); }).toErrorDev('Context can only be read while React is rendering'); }); @@ -1287,7 +1320,7 @@ describe('ReactHooks', () => { let renderer; await act(() => { - renderer = ReactTestRenderer.create(null); + renderer = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); }); renderCount = 0; @@ -1523,6 +1556,7 @@ describe('ReactHooks', () => { , + {unstable_isConcurrent: true}, ); }); expect(useMemoCount).toBe(__DEV__ ? 2 : 1); // Has Hooks @@ -1619,7 +1653,9 @@ describe('ReactHooks', () => { } let root; await act(() => { - root = ReactTestRenderer.create(); + root = ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); await expect(async () => { try { @@ -1668,7 +1704,9 @@ describe('ReactHooks', () => { } let root; await act(() => { - root = ReactTestRenderer.create(); + root = ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); await expect(async () => { @@ -1722,7 +1760,9 @@ describe('ReactHooks', () => { } let root; await act(() => { - root = ReactTestRenderer.create(); + root = ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); await expect(async () => { @@ -1753,7 +1793,9 @@ describe('ReactHooks', () => { } let root; await act(() => { - root = ReactTestRenderer.create(); + root = ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); await expect(async () => { await act(() => { @@ -1803,7 +1845,9 @@ describe('ReactHooks', () => { } let root; await act(() => { - root = ReactTestRenderer.create(); + root = ReactTestRenderer.create(, { + unstable_isConcurrent: true, + }); }); await expect(async () => { await expect(async () => { @@ -1849,6 +1893,7 @@ describe('ReactHooks', () => { , + {unstable_isConcurrent: true}, ); }); }).rejects.toThrow('Hello'); @@ -1900,7 +1945,7 @@ describe('ReactHooks', () => { } await act(() => { - ReactTestRenderer.create(); + ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); // Note: should *not* warn about updates on unmounted component. @@ -1941,6 +1986,7 @@ describe('ReactHooks', () => { , + {unstable_isConcurrent: true}, ); }); expect(root).toMatchRenderedOutput('loading'); @@ -1976,6 +2022,7 @@ describe('ReactHooks', () => { , + {unstable_isConcurrent: true}, ); }); expect(root).toMatchRenderedOutput('loading'); @@ -2011,6 +2058,7 @@ describe('ReactHooks', () => { , + {unstable_isConcurrent: true}, ); }); expect(root).toMatchRenderedOutput('loading'); @@ -2062,6 +2110,7 @@ describe('ReactHooks', () => { , + {unstable_isConcurrent: true}, ); }); From 9395fad4552d31bc353ccacf0111785af32cdb3f Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Mon, 18 Mar 2024 13:38:54 -0400 Subject: [PATCH 3/4] f --- .../react-reconciler/src/__tests__/ReactHooks-test.internal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index f6c6acf0e622c..782df1b45b122 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -51,7 +51,7 @@ describe('ReactHooks', () => { await expect(async () => { await act(() => { ReactTestRenderer.create(, { - unstable_unstable_isConcurrent: true, + unstable_isConcurrent: true, }); }); }).rejects.toThrow( From 7af3cd4968c6f481c0f2e36d585269dd988057aa Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Mon, 18 Mar 2024 15:37:54 -0400 Subject: [PATCH 4/4] remove jest.mocked --- .../react-reconciler/src/__tests__/ReactHooks-test.internal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 782df1b45b122..ce0a45be89f54 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -1899,7 +1899,7 @@ describe('ReactHooks', () => { }).rejects.toThrow('Hello'); if (__DEV__) { - expect(jest.mocked(console.error)).toHaveBeenCalledTimes(2); + expect(console.error).toHaveBeenCalledTimes(2); expect(console.error.mock.calls[0][0]).toContain( 'Warning: Cannot update a component (`%s`) while rendering ' + 'a different component (`%s`).',