diff --git a/packages/react-reconciler/src/__tests__/Activity-test.js b/packages/react-reconciler/src/__tests__/Activity-test.js index cc5c4b921ce98..191e72e3cb14d 100644 --- a/packages/react-reconciler/src/__tests__/Activity-test.js +++ b/packages/react-reconciler/src/__tests__/Activity-test.js @@ -14,6 +14,7 @@ let startTransition; let waitForPaint; let waitFor; let assertLog; +let assertConsoleErrorDev; describe('Activity', () => { beforeEach(() => { @@ -37,6 +38,7 @@ describe('Activity', () => { waitForPaint = InternalTestUtils.waitForPaint; waitFor = InternalTestUtils.waitFor; assertLog = InternalTestUtils.assertLog; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); function Text(props) { @@ -784,11 +786,14 @@ describe('Activity', () => { // would be null because it was nulled out when it was deleted, but there // was no null check before we accessed it. A weird edge case but we must // account for it. - expect(() => { - setState('Updated'); - }).toErrorDev( - "Can't perform a React state update on a component that hasn't mounted yet", - ); + setState('Updated'); + assertConsoleErrorDev([ + "Can't perform a React state update on a component that hasn't mounted yet. " + + 'This indicates that you have a side-effect in your render function that ' + + 'asynchronously later calls tries to update the component. ' + + 'Move this work to useEffect instead.\n' + + ' in Child (at **)', + ]); }); expect(root).toMatchRenderedOutput(null); }); diff --git a/packages/react-reconciler/src/__tests__/ReactActWarnings-test.js b/packages/react-reconciler/src/__tests__/ReactActWarnings-test.js index 561741c108622..82827406557dc 100644 --- a/packages/react-reconciler/src/__tests__/ReactActWarnings-test.js +++ b/packages/react-reconciler/src/__tests__/ReactActWarnings-test.js @@ -18,6 +18,7 @@ let Suspense; let startTransition; let getCacheForType; let caches; +let assertConsoleErrorDev; // These tests are mostly concerned with concurrent roots. The legacy root // behavior is covered by other older test suites and is unchanged from @@ -38,6 +39,7 @@ describe('act warnings', () => { const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; assertLog = InternalTestUtils.assertLog; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); function createTextCache() { @@ -171,9 +173,21 @@ describe('act warnings', () => { // Flag is true. Warn. await withActEnvironment(true, async () => { - expect(() => setState(2)).toErrorDev( - 'An update to App inside a test was not wrapped in act', - ); + setState(2); + assertConsoleErrorDev([ + 'An update to App inside a test was not wrapped in act(...).\n' + + '\n' + + 'When testing, code that causes React state updates should be wrapped into act(...):\n' + + '\n' + + 'act(() => {\n' + + ' /* fire events that update state */\n' + + '});\n' + + '/* assert on the output */\n' + + '\n' + + "This ensures that you're testing the behavior the user would see in the browser. " + + 'Learn more at https://react.dev/link/wrap-tests-with-act\n' + + ' in App (at **)', + ]); await waitForAll([2]); expect(root).toMatchRenderedOutput('2'); }); @@ -202,12 +216,11 @@ describe('act warnings', () => { // Default behavior. Flag is undefined. Warn. expect(global.IS_REACT_ACT_ENVIRONMENT).toBe(undefined); - expect(() => { - act(() => { - setState(1); - }); - }).toErrorDev( - 'The current testing environment is not configured to support act(...)', + act(() => { + setState(1); + }); + assertConsoleErrorDev( + ['The current testing environment is not configured to support act(...)'], {withoutStack: true}, ); assertLog([1]); @@ -224,12 +237,13 @@ describe('act warnings', () => { // Flag is false. Warn. await withActEnvironment(false, () => { - expect(() => { - act(() => { - setState(1); - }); - }).toErrorDev( - 'The current testing environment is not configured to support act(...)', + act(() => { + setState(1); + }); + assertConsoleErrorDev( + [ + 'The current testing environment is not configured to support act(...)', + ], {withoutStack: true}, ); assertLog([1]); @@ -240,10 +254,23 @@ describe('act warnings', () => { it('warns if root update is not wrapped', async () => { await withActEnvironment(true, () => { const root = ReactNoop.createRoot(); - expect(() => root.render('Hi')).toErrorDev( - // TODO: Better error message that doesn't make it look like "Root" is - // the name of a custom component - 'An update to Root inside a test was not wrapped in act(...)', + root.render('Hi'); + assertConsoleErrorDev( + [ + // TODO: Better error message that doesn't make it look like "Root" is + // the name of a custom component + 'An update to Root inside a test was not wrapped in act(...).\n' + + '\n' + + 'When testing, code that causes React state updates should be wrapped into act(...):\n' + + '\n' + + 'act(() => {\n' + + ' /* fire events that update state */\n' + + '});\n' + + '/* assert on the output */\n' + + '\n' + + "This ensures that you're testing the behavior the user would see in the browser. " + + 'Learn more at https://react.dev/link/wrap-tests-with-act', + ], {withoutStack: true}, ); }); @@ -265,9 +292,21 @@ describe('act warnings', () => { act(() => { root.render(); }); - expect(() => app.setState({count: 1})).toErrorDev( - 'An update to App inside a test was not wrapped in act(...)', - ); + app.setState({count: 1}); + assertConsoleErrorDev([ + 'An update to App inside a test was not wrapped in act(...).\n' + + '\n' + + 'When testing, code that causes React state updates should be wrapped into act(...):\n' + + '\n' + + 'act(() => {\n' + + ' /* fire events that update state */\n' + + '});\n' + + '/* assert on the output */\n' + + '\n' + + "This ensures that you're testing the behavior the user would see in the browser. " + + 'Learn more at https://react.dev/link/wrap-tests-with-act\n' + + ' in App (at **)', + ]); }); }); @@ -288,9 +327,21 @@ describe('act warnings', () => { // Even though this update is synchronous, we should still fire a warning, // because it could have spawned additional asynchronous work - expect(() => ReactNoop.flushSync(() => setState(1))).toErrorDev( - 'An update to App inside a test was not wrapped in act(...)', - ); + ReactNoop.flushSync(() => setState(1)); + assertConsoleErrorDev([ + 'An update to App inside a test was not wrapped in act(...).\n' + + '\n' + + 'When testing, code that causes React state updates should be wrapped into act(...):\n' + + '\n' + + 'act(() => {\n' + + ' /* fire events that update state */\n' + + '});\n' + + '/* assert on the output */\n' + + '\n' + + "This ensures that you're testing the behavior the user would see in the browser. " + + 'Learn more at https://react.dev/link/wrap-tests-with-act\n' + + ' in App (at **)', + ]); assertLog([1]); expect(root).toMatchRenderedOutput('1'); @@ -322,12 +373,36 @@ describe('act warnings', () => { expect(root).toMatchRenderedOutput('Loading...'); // This is a retry, not a ping, because we already showed a fallback. - expect(() => resolveText('Async')).toErrorDev( + resolveText('Async'); + assertConsoleErrorDev( [ - 'A suspended resource finished loading inside a test, but the event ' + - 'was not wrapped in act(...)', - - ...(gate('enableSiblingPrerendering') ? ['not wrapped in act'] : []), + 'A suspended resource finished loading inside a test, but the event was not wrapped in act(...).\n' + + '\n' + + 'When testing, code that resolves suspended data should be wrapped into act(...):\n' + + '\n' + + 'act(() => {\n' + + ' /* finish loading suspended data */\n' + + '});\n' + + '/* assert on the output */\n' + + '\n' + + "This ensures that you're testing the behavior the user would see in the browser. " + + 'Learn more at https://react.dev/link/wrap-tests-with-act', + + ...(gate('enableSiblingPrerendering') + ? [ + 'A suspended resource finished loading inside a test, but the event was not wrapped in act(...).\n' + + '\n' + + 'When testing, code that resolves suspended data should be wrapped into act(...):\n' + + '\n' + + 'act(() => {\n' + + ' /* finish loading suspended data */\n' + + '});\n' + + '/* assert on the output */\n' + + '\n' + + "This ensures that you're testing the behavior the user would see in the browser. " + + 'Learn more at https://react.dev/link/wrap-tests-with-act', + ] + : []), ], {withoutStack: true}, @@ -363,9 +438,21 @@ describe('act warnings', () => { expect(root).toMatchRenderedOutput('(empty)'); // This is a ping, not a retry, because no fallback is showing. - expect(() => resolveText('Async')).toErrorDev( - 'A suspended resource finished loading inside a test, but the event ' + - 'was not wrapped in act(...)', + resolveText('Async'); + assertConsoleErrorDev( + [ + 'A suspended resource finished loading inside a test, but the event was not wrapped in act(...).\n' + + '\n' + + 'When testing, code that resolves suspended data should be wrapped into act(...):\n' + + '\n' + + 'act(() => {\n' + + ' /* finish loading suspended data */\n' + + '});\n' + + '/* assert on the output */\n' + + '\n' + + "This ensures that you're testing the behavior the user would see in the browser. " + + 'Learn more at https://react.dev/link/wrap-tests-with-act', + ], {withoutStack: true}, ); }); diff --git a/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js b/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js index 843d5b2dd3fe6..43e071d4141cf 100644 --- a/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js +++ b/packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js @@ -7,6 +7,7 @@ let useTransition; let useState; let useOptimistic; let textCache; +let assertConsoleErrorDev; describe('ReactAsyncActions', () => { beforeEach(() => { @@ -21,6 +22,8 @@ describe('ReactAsyncActions', () => { Scheduler = require('scheduler'); act = require('internal-test-utils').act; assertLog = require('internal-test-utils').assertLog; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; useTransition = React.useTransition; useState = React.useState; useOptimistic = React.useOptimistic; @@ -1231,15 +1234,16 @@ describe('ReactAsyncActions', () => { assertLog(['A']); expect(root).toMatchRenderedOutput(
A
); - await expect(async () => { - await act(() => { - setLoadingProgress('25%'); - startTransition(() => setText('B')); - }); - }).toErrorDev( - 'An optimistic state update occurred outside a transition or ' + - 'action. To fix, move the update to an action, or wrap ' + - 'with startTransition.', + await act(() => { + setLoadingProgress('25%'); + startTransition(() => setText('B')); + }); + assertConsoleErrorDev( + [ + 'An optimistic state update occurred outside a transition or ' + + 'action. To fix, move the update to an action, or wrap ' + + 'with startTransition.', + ], {withoutStack: true}, ); assertLog(['Loading... (25%)', 'A', 'B']); diff --git a/packages/react-reconciler/src/__tests__/ReactFragment-test.js b/packages/react-reconciler/src/__tests__/ReactFragment-test.js index fe1d1f4f953db..31f5343546562 100644 --- a/packages/react-reconciler/src/__tests__/ReactFragment-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFragment-test.js @@ -12,6 +12,7 @@ let React; let ReactNoop; let waitForAll; +let assertConsoleErrorDev; describe('ReactFragment', () => { beforeEach(function () { @@ -22,6 +23,7 @@ describe('ReactFragment', () => { const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); it('should render a single child via noop renderer', async () => { @@ -740,9 +742,22 @@ describe('ReactFragment', () => { await waitForAll([]); ReactNoop.render(); - await expect(async () => await waitForAll([])).toErrorDev( - 'Each child in a list should have a unique "key" prop.', - ); + await waitForAll([]); + assertConsoleErrorDev([ + gate('enableOwnerStacks') + ? 'Each child in a list should have a unique "key" prop.\n' + + '\n' + + 'Check the render method of `div`. ' + + 'It was passed a child from Foo. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Foo (at **)' + : 'Each child in a list should have a unique "key" prop.\n' + + '\n' + + 'Check the render method of `Foo`. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Stateful (at **)\n' + + ' in Foo (at **)', + ]); expect(ops).toEqual([]); expect(ReactNoop).toMatchRenderedOutput( @@ -937,9 +952,17 @@ describe('ReactFragment', () => { } ReactNoop.render(); - await expect(async () => await waitForAll([])).toErrorDev( - 'Each child in a list should have a unique "key" prop.', - ); + await waitForAll([]); + console.log(gate('enableOwnerStacks')); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n' + + '\n' + + 'Check the top-level render call using . ' + + 'It was passed a child from Foo. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in span (at **)\n' + + ' in Foo (at **)', + ]); ReactNoop.render(); // The key warning gets deduped because it's in the same component. diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 8b0e903a839e9..367d83f38a57c 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -239,17 +239,18 @@ describe('ReactHooks', () => { await waitForAll(['Count: 0']); expect(root).toMatchRenderedOutput('0'); - await expect(async () => { - await act(() => - setCounter(1, () => { - throw new Error('Expected to ignore the callback.'); - }), - ); - }).toErrorDev( - 'State updates from the useState() and useReducer() Hooks ' + - "don't support the second callback argument. " + - 'To execute a side effect after rendering, ' + - 'declare it in the component body with useEffect().', + await act(() => + setCounter(1, () => { + throw new Error('Expected to ignore the callback.'); + }), + ); + assertConsoleErrorDev( + [ + 'State updates from the useState() and useReducer() Hooks ' + + "don't support the second callback argument. " + + 'To execute a side effect after rendering, ' + + 'declare it in the component body with useEffect().', + ], {withoutStack: true}, ); assertLog(['Count: 1']); @@ -273,17 +274,18 @@ describe('ReactHooks', () => { await waitForAll(['Count: 0']); expect(root).toMatchRenderedOutput('0'); - await expect(async () => { - await act(() => - dispatch(1, () => { - throw new Error('Expected to ignore the callback.'); - }), - ); - }).toErrorDev( - 'State updates from the useState() and useReducer() Hooks ' + - "don't support the second callback argument. " + - 'To execute a side effect after rendering, ' + - 'declare it in the component body with useEffect().', + await act(() => + dispatch(1, () => { + throw new Error('Expected to ignore the callback.'); + }), + ); + assertConsoleErrorDev( + [ + 'State updates from the useState() and useReducer() Hooks ' + + "don't support the second callback argument. " + + 'To execute a side effect after rendering, ' + + 'declare it in the component body with useEffect().', + ], {withoutStack: true}, ); assertLog(['Count: 1']); @@ -581,16 +583,17 @@ describe('ReactHooks', () => { }); }); assertLog(['Did commit: A']); - await expect(async () => { - await act(() => { - root.update(); - }); - }).toErrorDev([ + await act(() => { + root.update(); + }); + assertConsoleErrorDev([ 'The final argument passed to useLayoutEffect changed size ' + 'between renders. The order and size of this array must remain ' + - 'constant.\n\n' + + 'constant.\n' + + '\n' + 'Previous: [A]\n' + - 'Incoming: [A, B]\n', + 'Incoming: [A, B]\n' + + ' in App (at **)', ]); }); @@ -617,14 +620,14 @@ describe('ReactHooks', () => { assertLog(['Compute']); expect(root).toMatchRenderedOutput('HELLO'); - await expect(async () => { - await act(() => { - root.update(); - }); - }).toErrorDev([ + await act(() => { + root.update(); + }); + assertConsoleErrorDev([ 'useMemo received a final argument during this render, but ' + 'not during the previous render. Even though the final argument is ' + - 'optional, its type cannot change between renders.', + 'optional, its type cannot change between renders.\n' + + ' in App (at **)', ]); }); @@ -639,53 +642,62 @@ describe('ReactHooks', () => { return null; } - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); + await act(() => { + ReactTestRenderer.create(, { + unstable_isConcurrent: true, }); - }).toErrorDev([ + }); + assertConsoleErrorDev([ 'useEffect received a final argument that is not an array (instead, received `string`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', 'useLayoutEffect received a final argument that is not an array (instead, received `string`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', 'useMemo received a final argument that is not an array (instead, received `string`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', 'useCallback received a final argument that is not an array (instead, received `string`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', ]); - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); + await act(() => { + ReactTestRenderer.create(, { + unstable_isConcurrent: true, }); - }).toErrorDev([ + }); + assertConsoleErrorDev([ 'useEffect received a final argument that is not an array (instead, received `number`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', 'useLayoutEffect received a final argument that is not an array (instead, received `number`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', 'useMemo received a final argument that is not an array (instead, received `number`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', 'useCallback received a final argument that is not an array (instead, received `number`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', ]); - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); + await act(() => { + ReactTestRenderer.create(, { + unstable_isConcurrent: true, }); - }).toErrorDev([ + }); + assertConsoleErrorDev([ 'useEffect received a final argument that is not an array (instead, received `object`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', 'useLayoutEffect received a final argument that is not an array (instead, received `object`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', 'useMemo received a final argument that is not an array (instead, received `object`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', 'useCallback received a final argument that is not an array (instead, received `object`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', ]); await act(() => { @@ -710,15 +722,15 @@ describe('ReactHooks', () => { }); App.displayName = 'App'; - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); + await act(() => { + ReactTestRenderer.create(, { + unstable_isConcurrent: true, }); - }).toErrorDev([ + }); + assertConsoleErrorDev([ 'useImperativeHandle received a final argument that is not an array (instead, received `string`). ' + - 'When specified, the final argument must be an array.', + 'When specified, the final argument must be an array.\n' + + ' in App (at **)', ]); await act(() => { ReactTestRenderer.create(, { @@ -817,17 +829,18 @@ describe('ReactHooks', () => { } await expect(async () => { - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, {unstable_isConcurrent: true}); - }); - }).rejects.toThrow('create is not a function'); - }).toErrorDev([ + await act(() => { + ReactTestRenderer.create(, {unstable_isConcurrent: true}); + }); + }).rejects.toThrow('create is not a function'); + assertConsoleErrorDev([ + 'Expected useImperativeHandle() second argument to be a function ' + + 'that creates a handle. Instead received: undefined.\n' + + ' in App (at **)', 'Expected useImperativeHandle() first argument to either be a ' + 'ref callback or React.createRef() object. ' + - 'Instead received: an object with keys {focus}.', - 'Expected useImperativeHandle() second argument to be a function ' + - 'that creates a handle. Instead received: undefined.', + 'Instead received: an object with keys {focus}.\n' + + ' in App (at **)', ]); }); @@ -841,13 +854,13 @@ describe('ReactHooks', () => { }); App.displayName = 'App'; - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, {unstable_isConcurrent: true}); - }); - }).toErrorDev([ + await act(() => { + ReactTestRenderer.create(, {unstable_isConcurrent: true}); + }); + assertConsoleErrorDev([ 'Expected useImperativeHandle() second argument to be a function ' + - 'that creates a handle. Instead received: object.', + 'that creates a handle. Instead received: object.\n' + + ' in App (at **)', ]); }); @@ -933,13 +946,15 @@ describe('ReactHooks', () => { }); return null; } - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, {unstable_isConcurrent: true}); - }); - }).toErrorDev( - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.', - ); + await act(() => { + ReactTestRenderer.create(, {unstable_isConcurrent: true}); + }); + assertConsoleErrorDev([ + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' + + 'You can only call Hooks at the top level of your React function. ' + + 'For more information, see https://react.dev/link/rules-of-hooks\n' + + ' in App (at **)', + ]); }); it('warns when reading context inside useMemo', async () => { @@ -954,11 +969,16 @@ describe('ReactHooks', () => { }, []); } - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, {unstable_isConcurrent: true}); - }); - }).toErrorDev('Context can only be read while React is rendering'); + await act(() => { + ReactTestRenderer.create(, {unstable_isConcurrent: true}); + }); + assertConsoleErrorDev([ + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, ' + + 'but not inside Hooks like useReducer() or useMemo().\n' + + ' in App (at **)', + ]); }); it('warns when reading context inside useMemo after reading outside it', async () => { @@ -977,11 +997,16 @@ describe('ReactHooks', () => { }, []); } - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, {unstable_isConcurrent: true}); - }); - }).toErrorDev('Context can only be read while React is rendering'); + await act(() => { + ReactTestRenderer.create(, {unstable_isConcurrent: true}); + }); + assertConsoleErrorDev([ + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, ' + + 'but not inside Hooks like useReducer() or useMemo().\n' + + ' in App (at **)', + ]); expect(firstRead).toBe('light'); expect(secondRead).toBe('light'); }); @@ -1048,11 +1073,16 @@ describe('ReactHooks', () => { return null; } - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, {unstable_isConcurrent: true}); - }); - }).toErrorDev(['Context can only be read while React is rendering']); + await act(() => { + ReactTestRenderer.create(, {unstable_isConcurrent: true}); + }); + assertConsoleErrorDev([ + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, ' + + 'but not inside Hooks like useReducer() or useMemo().\n' + + ' in App (at **)', + ]); }); // Edge case. @@ -1078,19 +1108,25 @@ describe('ReactHooks', () => { } } - await expect(async () => { - await act(() => { - ReactTestRenderer.create( - <> - - - , - {unstable_isConcurrent: true}, - ); - }); - }).toErrorDev([ - 'Context can only be read while React is rendering', - 'Cannot update a component (`Fn`) while rendering a different component (`Cls`).', + await act(() => { + ReactTestRenderer.create( + <> + + + , + {unstable_isConcurrent: true}, + ); + }); + assertConsoleErrorDev([ + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, ' + + 'but not inside Hooks like useReducer() or useMemo().\n' + + ' in Cls (at **)', + 'Cannot update a component (`Fn`) while rendering a different component (`Cls`). ' + + 'To locate the bad setState() call inside `Cls`, ' + + 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' + + ' in Cls (at **)', ]); }); @@ -1110,24 +1146,32 @@ describe('ReactHooks', () => { } await expect(async () => { - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, {unstable_isConcurrent: true}); - }); - }).rejects.toThrow( - 'Update hook called on initial render. This is likely a bug in React. Please file an issue.', - ); - }).toErrorDev([ - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', + await act(() => { + ReactTestRenderer.create(, {unstable_isConcurrent: true}); + }); + }).rejects.toThrow( + 'Update hook called on initial render. This is likely a bug in React. Please file an issue.', + ); + assertConsoleErrorDev([ + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' + + 'You can only call Hooks at the top level of your React function. ' + + 'For more information, see https://react.dev/link/rules-of-hooks\n' + + ' in App (at **)', '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, ' + - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' + + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' + + '\n' + ' Previous render Next render\n' + ' ------------------------------------------------------\n' + '1. useReducer useReducer\n' + '2. useState useRef\n' + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks', + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + + '\n' + + ' in App (at **)', + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' + + 'You can only call Hooks at the top level of your React function. ' + + 'For more information, see https://react.dev/link/rules-of-hooks\n' + + ' in App (at **)', ]); }); @@ -1140,13 +1184,15 @@ describe('ReactHooks', () => { }); return null; } - await expect(async () => { - await act(() => { - ReactTestRenderer.create(, {unstable_isConcurrent: true}); - }); - }).toErrorDev( - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.', - ); + await act(() => { + ReactTestRenderer.create(, {unstable_isConcurrent: true}); + }); + assertConsoleErrorDev([ + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' + + 'You can only call Hooks at the top level of your React function. ' + + 'For more information, see https://react.dev/link/rules-of-hooks\n' + + ' in App (at **)', + ]); }); it('resets warning internal state when interrupted by an error', async () => { @@ -1177,21 +1223,37 @@ describe('ReactHooks', () => { } } - await expect(async () => { - await act(() => { - ReactTestRenderer.create( - - - , - {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', + await act(() => { + ReactTestRenderer.create( + + + , + {unstable_isConcurrent: true}, + ); + }); + assertConsoleErrorDev([ + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, ' + + 'but not inside Hooks like useReducer() or useMemo().\n' + + ' in App (at **)' + + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'), + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' + + 'You can only call Hooks at the top level of your React function. ' + + 'For more information, see https://react.dev/link/rules-of-hooks\n' + + ' in App (at **)' + + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'), + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, ' + + 'but not inside Hooks like useReducer() or useMemo().\n' + + ' in App (at **)' + + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'), + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' + + 'You can only call Hooks at the top level of your React function. ' + + 'For more information, see https://react.dev/link/rules-of-hooks\n' + + ' in App (at **)' + + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'), ]); function Valid() { @@ -1218,21 +1280,37 @@ describe('ReactHooks', () => { }); // Verify warnings don't get permanently disabled. - await expect(async () => { - await act(() => { - ReactTestRenderer.create( - - - , - {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', + await act(() => { + ReactTestRenderer.create( + + + , + {unstable_isConcurrent: true}, + ); + }); + assertConsoleErrorDev([ + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, ' + + 'but not inside Hooks like useReducer() or useMemo().\n' + + ' in App (at **)' + + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'), + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' + + 'You can only call Hooks at the top level of your React function. ' + + 'For more information, see https://react.dev/link/rules-of-hooks\n' + + ' in App (at **)' + + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'), + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, ' + + 'but not inside Hooks like useReducer() or useMemo().\n' + + ' in App (at **)' + + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'), + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' + + 'You can only call Hooks at the top level of your React function. ' + + 'For more information, see https://react.dev/link/rules-of-hooks\n' + + ' in App (at **)' + + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'), ]); }); @@ -1569,24 +1647,26 @@ describe('ReactHooks', () => { unstable_isConcurrent: true, }); }); - await expect(async () => { - try { - 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. - } - }).toErrorDev([ + try { + 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. + } + assertConsoleErrorDev([ '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, ' + - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' + + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' + + '\n' + ' Previous render Next render\n' + ' ------------------------------------------------------\n' + `1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameB)}\n` + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + + '\n' + + ' in App (at **)', ]); // further warnings for this component are silenced @@ -1618,25 +1698,27 @@ describe('ReactHooks', () => { }); }); - await expect(async () => { - try { - 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. - } - }).toErrorDev([ + try { + 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. + } + assertConsoleErrorDev([ '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, ' + - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' + + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' + + '\n' + ' Previous render Next render\n' + ' ------------------------------------------------------\n' + `1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameA)}\n` + `2. undefined use${hookNameB}\n` + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + + '\n' + + ' in App (at **)', ]); }); }); @@ -1701,24 +1783,26 @@ describe('ReactHooks', () => { unstable_isConcurrent: true, }); }); - await expect(async () => { - await act(() => { - root.update(); - }).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([ + await act(() => { + root.update(); + }).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. + assertConsoleErrorDev([ '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, ' + - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' + + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' + + '\n' + ' Previous render Next render\n' + ' ------------------------------------------------------\n' + `1. ${formatHookNamesToMatchErrorMessage( 'ImperativeHandle', 'Memo', )}\n` + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + + '\n' + + ' in App (at **)', ]); // further warnings for this component are silenced @@ -1751,19 +1835,21 @@ describe('ReactHooks', () => { }); }); await expect(async () => { - await expect(async () => { - await act(() => { - root.update(); - }); - }).rejects.toThrow('custom error'); - }).toErrorDev([ + await act(() => { + root.update(); + }); + }).rejects.toThrow('custom error'); + assertConsoleErrorDev([ '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, ' + - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' + + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' + + '\n' + ' Previous render Next render\n' + ' ------------------------------------------------------\n' + '1. useReducer useState\n' + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + + '\n' + + ' in App (at **)', ]); }); }); @@ -1798,8 +1884,10 @@ describe('ReactHooks', () => { }); }).rejects.toThrow('Hello'); assertConsoleErrorDev([ - 'Cannot update a component (`A`) while rendering ' + - 'a different component (`B`).', + 'Cannot update a component (`A`) while rendering a different component (`B`). ' + + 'To locate the bad setState() call inside `B`, ' + + 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' + + ' in B (at **)', ]); }); diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index dcb362669928e..e4febdb3e28f1 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -42,6 +42,7 @@ let waitForThrow; let waitForPaint; let assertLog; let useResourceEffect; +let assertConsoleErrorDev; describe('ReactHooksWithNoopRenderer', () => { beforeEach(() => { @@ -52,6 +53,8 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); act = require('internal-test-utils').act; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; useState = React.useState; useReducer = React.useReducer; useEffect = React.useEffect; @@ -232,17 +235,18 @@ describe('ReactHooksWithNoopRenderer', () => { }); it('throws when called outside the render phase', async () => { - expect(() => { - expect(() => useState(0)).toThrow( - "Cannot read property 'useState' of null", - ); - }).toErrorDev( - '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' + - '2. You might be breaking the Rules of Hooks\n' + - '3. You might have more than one copy of React in the same app\n' + - 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.', + expect(() => useState(0)).toThrow( + "Cannot read property 'useState' of null", + ); + assertConsoleErrorDev( + [ + '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' + + '2. You might be breaking the Rules of Hooks\n' + + '3. You might have more than one copy of React in the same app\n' + + 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.', + ], {withoutStack: true}, ); }); @@ -459,11 +463,12 @@ describe('ReactHooksWithNoopRenderer', () => { , ); - await expect( - async () => await waitForAll(['Foo [0]', 'Bar', 'Foo [1]']), - ).toErrorDev([ - 'Cannot update a component (`Foo`) while rendering a ' + - 'different component (`Bar`). To locate the bad setState() call inside `Bar`', + await waitForAll(['Foo [0]', 'Bar', 'Foo [1]']); + assertConsoleErrorDev([ + 'Cannot update a component (`Foo`) while rendering a different component (`Bar`). ' + + 'To locate the bad setState() call inside `Bar`, ' + + 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' + + ' in Bar (at **)', ]); // It should not warn again (deduplication). @@ -1645,6 +1650,12 @@ describe('ReactHooksWithNoopRenderer', () => { updateCount(props.count); }); assertLog([`Schedule update [${props.count}]`]); + assertConsoleErrorDev([ + 'flushSync was called from inside a lifecycle method. ' + + 'React cannot flush when React is already rendering. ' + + 'Consider moving this call to a scheduler task or micro task.\n' + + ' in Counter (at **)', + ]); // This shouldn't flush synchronously. expect(ReactNoop).not.toMatchRenderedOutput( , @@ -1652,17 +1663,14 @@ describe('ReactHooksWithNoopRenderer', () => { }, [props.count]); return ; } - await expect(async () => { - await act(async () => { - ReactNoop.render(, () => - Scheduler.log('Sync effect'), - ); - await waitFor(['Count: (empty)', 'Sync effect']); - expect(ReactNoop).toMatchRenderedOutput( - , - ); - }); - }).toErrorDev('flushSync was called from inside a lifecycle method'); + await act(async () => { + ReactNoop.render(, () => + Scheduler.log('Sync effect'), + ); + await waitFor(['Count: (empty)', 'Sync effect']); + expect(ReactNoop).toMatchRenderedOutput(); + }); + assertLog([`Count: 0`]); expect(ReactNoop).toMatchRenderedOutput(); }); @@ -2506,35 +2514,47 @@ describe('ReactHooksWithNoopRenderer', () => { } const root1 = ReactNoop.createRoot(); - await expect(async () => { - await act(() => { - root1.render(); - }); - }).toErrorDev([ - 'useEffect must not return anything besides a ' + - 'function, which is used for clean-up. You returned: 17', + await act(() => { + root1.render(); + }); + assertConsoleErrorDev([ + 'useEffect must not return anything besides a function, ' + + 'which is used for clean-up. You returned: 17\n' + + ' in App (at **)', ]); const root2 = ReactNoop.createRoot(); - await expect(async () => { - await act(() => { - root2.render(); - }); - }).toErrorDev([ - 'useEffect must not return anything besides a ' + - 'function, which is used for clean-up. You returned null. If your ' + - 'effect does not require clean up, return undefined (or nothing).', + await act(() => { + root2.render(); + }); + assertConsoleErrorDev([ + 'useEffect must not return anything besides a function, ' + + 'which is used for clean-up. You returned null. ' + + 'If your effect does not require clean up, return undefined (or nothing).\n' + + ' in App (at **)', ]); const root3 = ReactNoop.createRoot(); - await expect(async () => { - await act(() => { - root3.render(); - }); - }).toErrorDev([ - 'useEffect must not return anything besides a ' + - 'function, which is used for clean-up.\n\n' + - 'It looks like you wrote useEffect(async () => ...) or returned a Promise.', + await act(() => { + root3.render(); + }); + assertConsoleErrorDev([ + 'useEffect must not return anything besides a function, which is used for clean-up.\n' + + '\n' + + 'It looks like you wrote useEffect(async () => ...) or returned a Promise. ' + + 'Instead, write the async function inside your effect and call it immediately:\n' + + '\n' + + 'useEffect(() => {\n' + + ' async function fetchData() {\n' + + ' // You can await here\n' + + ' const response = await MyAPI.getData(someId);\n' + + ' // ...\n' + + ' }\n' + + ' fetchData();\n' + + "}, [someId]); // Or [] if effect doesn't need props or state\n" + + '\n' + + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching\n' + + ' in App (at **)', ]); // Error on unmount because React assumes the value is a function @@ -2895,35 +2915,48 @@ describe('ReactHooksWithNoopRenderer', () => { } const root1 = ReactNoop.createRoot(); - await expect(async () => { - await act(() => { - root1.render(); - }); - }).toErrorDev([ - 'useInsertionEffect must not return anything besides a ' + - 'function, which is used for clean-up. You returned: 17', + await act(() => { + root1.render(); + }); + assertConsoleErrorDev([ + 'useInsertionEffect must not return anything besides a function, ' + + 'which is used for clean-up. You returned: 17\n' + + ' in App (at **)', ]); const root2 = ReactNoop.createRoot(); - await expect(async () => { - await act(() => { - root2.render(); - }); - }).toErrorDev([ - 'useInsertionEffect must not return anything besides a ' + - 'function, which is used for clean-up. You returned null. If your ' + - 'effect does not require clean up, return undefined (or nothing).', + await act(() => { + root2.render(); + }); + assertConsoleErrorDev([ + 'useInsertionEffect must not return anything besides a function, ' + + 'which is used for clean-up. You returned null. ' + + 'If your effect does not require clean up, return undefined (or nothing).\n' + + ' in App (at **)', ]); const root3 = ReactNoop.createRoot(); - await expect(async () => { - await act(() => { - root3.render(); - }); - }).toErrorDev([ + await act(() => { + root3.render(); + }); + assertConsoleErrorDev([ 'useInsertionEffect must not return anything besides a ' + - 'function, which is used for clean-up.\n\n' + - 'It looks like you wrote useInsertionEffect(async () => ...) or returned a Promise.', + 'function, which is used for clean-up.\n' + + '\n' + + 'It looks like you wrote useInsertionEffect(async () => ...) or returned a Promise. ' + + 'Instead, write the async function inside your effect and call it immediately:\n' + + '\n' + + 'useInsertionEffect(() => {\n' + + ' async function fetchData() {\n' + + ' // You can await here\n' + + ' const response = await MyAPI.getData(someId);\n' + + ' // ...\n' + + ' }\n' + + ' fetchData();\n' + + "}, [someId]); // Or [] if effect doesn't need props or state\n" + + '\n' + + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching\n' + + ' in App (at **)', ]); // Error on unmount because React assumes the value is a function @@ -2946,11 +2979,13 @@ describe('ReactHooksWithNoopRenderer', () => { } const root = ReactNoop.createRoot(); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev(['useInsertionEffect must not schedule updates.']); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'useInsertionEffect must not schedule updates.\n' + + ' in App (at **)', + ]); await act(async () => { root.render(); @@ -2988,11 +3023,13 @@ describe('ReactHooksWithNoopRenderer', () => { await act(() => { root.render(); }); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev(['useInsertionEffect must not schedule updates.']); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'useInsertionEffect must not schedule updates.\n' + + ' in App (at **)', + ]); await act(async () => { root.render(); @@ -3036,19 +3073,17 @@ describe('ReactHooksWithNoopRenderer', () => { ); }); - if (gate(flags => flags.enableHiddenSubtreeInsertionEffectCleanup)) { - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev(['useInsertionEffect must not schedule updates.']); - } else { - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev([]); - } + await act(() => { + root.render(); + }); + assertConsoleErrorDev( + gate('enableHiddenSubtreeInsertionEffectCleanup') + ? [ + 'useInsertionEffect must not schedule updates.\n' + + ' in App (at **)', + ] + : [], + ); // Should not warn for regular effects after throw. function NotInsertion() { @@ -3225,35 +3260,47 @@ describe('ReactHooksWithNoopRenderer', () => { } const root1 = ReactNoop.createRoot(); - await expect(async () => { - await act(() => { - root1.render(); - }); - }).toErrorDev([ + await act(() => { + root1.render(); + }); + assertConsoleErrorDev([ 'useLayoutEffect must not return anything besides a ' + - 'function, which is used for clean-up. You returned: 17', + 'function, which is used for clean-up. You returned: 17\n' + + ' in App (at **)', ]); const root2 = ReactNoop.createRoot(); - await expect(async () => { - await act(() => { - root2.render(); - }); - }).toErrorDev([ + await act(() => { + root2.render(); + }); + assertConsoleErrorDev([ 'useLayoutEffect must not return anything besides a ' + 'function, which is used for clean-up. You returned null. If your ' + - 'effect does not require clean up, return undefined (or nothing).', + 'effect does not require clean up, return undefined (or nothing).\n' + + ' in App (at **)', ]); const root3 = ReactNoop.createRoot(); - await expect(async () => { - await act(() => { - root3.render(); - }); - }).toErrorDev([ + await act(() => { + root3.render(); + }); + assertConsoleErrorDev([ 'useLayoutEffect must not return anything besides a ' + 'function, which is used for clean-up.\n\n' + - 'It looks like you wrote useLayoutEffect(async () => ...) or returned a Promise.', + 'It looks like you wrote useLayoutEffect(async () => ...) or returned a Promise. ' + + 'Instead, write the async function inside your effect and call it immediately:\n' + + '\n' + + 'useLayoutEffect(() => {\n' + + ' async function fetchData() {\n' + + ' // You can await here\n' + + ' const response = await MyAPI.getData(someId);\n' + + ' // ...\n' + + ' }\n' + + ' fetchData();\n' + + "}, [someId]); // Or [] if effect doesn't need props or state\n" + + '\n' + + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching\n' + + ' in App (at **)', ]); // Error on unmount because React assumes the value is a function @@ -3300,13 +3347,14 @@ describe('ReactHooksWithNoopRenderer', () => { return null; } - await expect(async () => { - await act(() => { - ReactNoop.render(); - }); - }).toErrorDev( - 'useResourceEffect must provide a callback which returns a resource. ' + - 'If a managed resource is not needed here, use useEffect. Received undefined', + await act(() => { + ReactNoop.render(); + }); + assertConsoleErrorDev( + [ + 'useResourceEffect must provide a callback which returns a resource. ' + + 'If a managed resource is not needed here, use useEffect. Received undefined', + ], {withoutStack: true}, ); }); @@ -3328,14 +3376,14 @@ describe('ReactHooksWithNoopRenderer', () => { return null; } - await expect(async () => { - await act(() => { - ReactNoop.render(); - }); - }).toErrorDev( + await act(() => { + ReactNoop.render(); + }); + assertConsoleErrorDev([ 'useResourceEffect received a dependency array with no dependencies. ' + - 'When specified, the dependency array must have at least one dependency.', - ); + 'When specified, the dependency array must have at least one dependency.\n' + + ' in App (at **)', + ]); }); // @gate enableUseResourceEffectHook @@ -4472,21 +4520,23 @@ describe('ReactHooksWithNoopRenderer', () => { ); ReactNoop.render(); - await expect(async () => { - await waitForThrow( - 'Rendered more hooks than during the previous render.', - ); - assertLog([]); - }).toErrorDev([ + await waitForThrow( + 'Rendered more hooks than during the previous render.', + ); + assertLog([]); + assertConsoleErrorDev([ '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, ' + - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' + + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' + + '\n' + ' Previous render Next render\n' + ' ------------------------------------------------------\n' + '1. useState useState\n' + '2. useState useState\n' + '3. undefined useState\n' + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + + '\n' + + ' in App (at **)', ]); // Uncomment if/when we support this again @@ -4569,20 +4619,22 @@ describe('ReactHooksWithNoopRenderer', () => { await act(async () => { ReactNoop.render(); - await expect(async () => { - await waitForThrow( - 'Rendered more hooks than during the previous render.', - ); - assertLog(['Unmount A']); - }).toErrorDev([ + await waitForThrow( + 'Rendered more hooks than during the previous render.', + ); + assertLog(['Unmount A']); + assertConsoleErrorDev([ '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, ' + - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' + + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' + + '\n' + ' Previous render Next render\n' + ' ------------------------------------------------------\n' + '1. useEffect useEffect\n' + '2. undefined useEffect\n' + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + + '\n' + + ' in App (at **)', ]); }); diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js index 50ed1ba5ee9b6..22b1db7adb216 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js @@ -1798,9 +1798,18 @@ describe('ReactIncremental', () => { 'ShowBoth {"locale":"fr"}', ]); assertConsoleErrorDev([ - 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ShowLocale uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - 'ShowBoth uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.', + 'Intl uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Intl (at **)', + 'ShowLocale uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in ShowLocale (at **)\n' + + ' in Intl (at **)', + 'ShowBoth uses the legacy contextTypes API which will be removed soon. ' + + 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ShowBoth (at **)\n' + + ' in div (at **)\n' + + ' in Intl (at **)', ]); ReactNoop.render( @@ -1853,8 +1862,16 @@ describe('ReactIncremental', () => { 'ShowBoth {"locale":"en"}', ]); assertConsoleErrorDev([ - 'Router uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ShowRoute uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Router uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Router (at **)\n' + + ' in Intl (at **)', + 'ShowRoute uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in ShowRoute (at **)\n' + + ' in Indirection (at **)\n' + + ' in Router (at **)\n' + + ' in Intl (at **)', ]); }); @@ -1890,8 +1907,12 @@ describe('ReactIncremental', () => { 'Recurse {"n":0}', ]); assertConsoleErrorDev([ - 'Recurse uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Recurse uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Recurse uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Recurse (at **)', + 'Recurse uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Recurse (at **)', ]); }); @@ -1943,8 +1964,13 @@ describe('ReactIncremental', () => { 'ShowLocale {"locale":"fr"}', ]); assertConsoleErrorDev([ - 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ShowLocale uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Intl uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Intl (at **)', + 'ShowLocale uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in ShowLocale (at **)' + + (gate('enableOwnerStacks') ? '' : '\n in Intl (at **)'), ]); await waitForAll([ @@ -2034,9 +2060,23 @@ describe('ReactIncremental', () => { 'ShowLocaleFn:read {"locale":"fr"}', ]); assertConsoleErrorDev([ - 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.', + 'Intl uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Intl (at **)', + 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in ShowLocaleClass (at **)\n' + + ' in Stateful (at **)\n' + + ' in IndirectionClass (at **)\n' + + ' in IndirectionFn (at **)\n' + + ' in Intl (at **)', + 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. ' + + 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ShowLocaleFn (at **)\n' + + ' in Stateful (at **)\n' + + ' in IndirectionClass (at **)\n' + + ' in IndirectionFn (at **)\n' + + ' in Intl (at **)', ]); statefulInst.setState({x: 1}); @@ -2125,9 +2165,24 @@ describe('ReactIncremental', () => { ]); assertConsoleErrorDev([ - 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.', + 'Intl uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Intl (at **)\n' + + ' in Stateful (at **)', + 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in ShowLocaleClass (at **)\n' + + ' in IndirectionClass (at **)\n' + + ' in IndirectionFn (at **)\n' + + ' in Intl (at **)\n' + + ' in Stateful (at **)', + 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. ' + + 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ShowLocaleFn (at **)\n' + + ' in IndirectionClass (at **)\n' + + ' in IndirectionFn (at **)\n' + + ' in Intl (at **)\n' + + ' in Stateful (at **)', ]); statefulInst.setState({locale: 'gr'}); @@ -2187,7 +2242,11 @@ describe('ReactIncremental', () => { await waitForAll([]); assertConsoleErrorDev([ - 'Child uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Child uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + (gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') + + ' in Middle (at **)\n' + + ' in Root (at **)', ]); // Trigger an update in the middle of the tree @@ -2235,8 +2294,12 @@ describe('ReactIncremental', () => { // Init ReactNoop.render(); - await expect(async () => await waitForAll([])).toErrorDev([ - 'ContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + await waitForAll([]); + assertConsoleErrorDev([ + 'ContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + (gate('enableOwnerStacks') ? '' : ' in ContextProvider (at **)\n') + + ' in Root (at **)', ]); // Trigger an update in the middle of the tree @@ -2244,9 +2307,12 @@ describe('ReactIncremental', () => { instance.setState({ throwError: true, }); - await expect(async () => await waitForAll([])).toErrorDev( - 'Error boundaries should implement getDerivedStateFromError()', - ); + await waitForAll([]); + assertConsoleErrorDev([ + 'Root: Error boundaries should implement getDerivedStateFromError(). ' + + 'In that method, return a state update to display an error message or fallback UI.\n' + + ' in Root (at **)', + ]); }); // @gate !disableLegacyContext || !__DEV__ @@ -2292,7 +2358,9 @@ describe('ReactIncremental', () => { ]); assertConsoleErrorDev([ - 'MyComponent uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'MyComponent uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in MyComponent (at **)', ]); }); @@ -2427,8 +2495,15 @@ describe('ReactIncremental', () => { await waitForAll(['count:0']); assertConsoleErrorDev([ - 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in TopContextProvider (at **)', + 'Child uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Child (at **)' + + (gate('enableOwnerStacks') + ? '' + : '\n in Middle (at **)' + '\n in TopContextProvider (at **)'), ]); instance.updateCount(); await waitForAll(['count:1']); @@ -2487,9 +2562,22 @@ describe('ReactIncremental', () => { await waitForAll(['count:0']); assertConsoleErrorDev([ - 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in TopContextProvider (at **)', + 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in MiddleContextProvider (at **)' + + (gate('enableOwnerStacks') + ? '' + : '\n in TopContextProvider (at **)'), + 'Child uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Child (at **)' + + (gate('enableOwnerStacks') + ? '' + : '\n in MiddleContextProvider (at **)' + + '\n in TopContextProvider (at **)'), ]); instance.updateCount(); await waitForAll(['count:1']); @@ -2557,9 +2645,24 @@ describe('ReactIncremental', () => { await waitForAll(['count:0']); assertConsoleErrorDev([ - 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in TopContextProvider (at **)', + 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in MiddleContextProvider (at **)' + + (gate('enableOwnerStacks') + ? '' + : '\n in MiddleScu (at **)' + + '\n in TopContextProvider (at **)'), + 'Child uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Child (at **)' + + (gate('enableOwnerStacks') + ? '' + : '\n in MiddleContextProvider (at **)' + + '\n in MiddleScu (at **)' + + '\n in TopContextProvider (at **)'), ]); instance.updateCount(); await waitForAll([]); @@ -2637,9 +2740,24 @@ describe('ReactIncremental', () => { await waitForAll(['count:0, name:brian']); assertConsoleErrorDev([ - 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in TopContextProvider (at **)', + 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in MiddleContextProvider (at **)' + + (gate('enableOwnerStacks') + ? '' + : '\n in MiddleScu (at **)' + + '\n in TopContextProvider (at **)'), + 'Child uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Child (at **)' + + (gate('enableOwnerStacks') + ? '' + : '\n in MiddleContextProvider (at **)' + + '\n in MiddleScu (at **)' + + '\n in TopContextProvider (at **)'), ]); topInstance.updateCount(); await waitForAll([]); @@ -2743,11 +2861,19 @@ describe('ReactIncremental', () => { , ); - await expect(async () => { - await waitForAll([]); - }).toErrorDev([ - 'Boundary uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - 'Legacy context API has been detected within a strict-mode tree', + await waitForAll([]); + assertConsoleErrorDev([ + 'Boundary uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Boundary (at **)', + 'Legacy context API has been detected within a strict-mode tree.\n' + + '\n' + + 'The old API will be supported in all 16.x releases, but applications using it should migrate to the new version.\n' + + '\n' + + 'Please update the following components: Boundary\n' + + '\n' + + 'Learn more about this warning here: https://react.dev/link/legacy-context\n' + + ' in Boundary (at **)', ]); } diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js index 2f0f23dca895b..8569da6a85cb2 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js @@ -1212,12 +1212,18 @@ describe('ReactIncrementalErrorHandling', () => { , ); - await expect(async () => { - await waitForAll([]); - }).toErrorDev([ - 'Provider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Provider uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - 'Connector uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.', + await waitForAll([]); + assertConsoleErrorDev([ + 'Provider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Provider (at **)', + 'Provider uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Provider (at **)', + 'Connector uses the legacy contextTypes API which will be removed soon. ' + + 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Connector (at **)\n' + + ' in Provider (at **)', ]); // If the context stack does not unwind, span will get 'abcde' @@ -1250,9 +1256,19 @@ describe('ReactIncrementalErrorHandling', () => { await waitForAll([]); if (gate(flags => !flags.enableOwnerStacks)) { assertConsoleErrorDev([ - 'React.jsx: type is invalid -- expected a string', + 'React.jsx: type is invalid -- expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: undefined. ' + + "You likely forgot to export your component from the file it's defined in, " + + 'or you might have mixed up default and named imports.\n' + + ' in BrokenRender (at **)\n' + + ' in ErrorBoundary (at **)', // React retries once on error - 'React.jsx: type is invalid -- expected a string', + 'React.jsx: type is invalid -- expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: undefined. ' + + "You likely forgot to export your component from the file it's defined in, " + + 'or you might have mixed up default and named imports.\n' + + ' in BrokenRender (at **)\n' + + ' in ErrorBoundary (at **)', ]); } @@ -1305,9 +1321,19 @@ describe('ReactIncrementalErrorHandling', () => { await waitForAll([]); if (gate(flags => !flags.enableOwnerStacks)) { assertConsoleErrorDev([ - 'React.jsx: type is invalid -- expected a string', + 'React.jsx: type is invalid -- expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: undefined. ' + + "You likely forgot to export your component from the file it's defined in, " + + 'or you might have mixed up default and named imports.\n' + + ' in BrokenRender (at **)\n' + + ' in ErrorBoundary (at **)', // React retries once on error - 'React.jsx: type is invalid -- expected a string', + 'React.jsx: type is invalid -- expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: undefined. ' + + "You likely forgot to export your component from the file it's defined in, " + + 'or you might have mixed up default and named imports.\n' + + ' in BrokenRender (at **)\n' + + ' in ErrorBoundary (at **)', ]); } expect(ReactNoop).toMatchRenderedOutput( @@ -1330,7 +1356,12 @@ describe('ReactIncrementalErrorHandling', () => { ReactNoop.render(); if (gate(flags => !flags.enableOwnerStacks)) { assertConsoleErrorDev( - ['React.jsx: type is invalid -- expected a string'], + [ + 'React.jsx: type is invalid -- expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: undefined. ' + + "You likely forgot to export your component from the file it's defined in, " + + 'or you might have mixed up default and named imports.', + ], {withoutStack: true}, ); } diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js index 6aa4034636c31..75ca5f6b131dc 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js @@ -33,7 +33,7 @@ describe('ReactIncrementalErrorLogging', () => { waitForAll = InternalTestUtils.waitForAll; }); - // Note: in this test file we won't be using toErrorDev() matchers + // Note: in this test file we won't be using assertConsoleDev() matchers // because they filter out precisely the messages we want to test for. let oldConsoleWarn; let oldConsoleError; diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js index 4d4ab4f7c929e..b7a2d6b661f0a 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js @@ -18,6 +18,7 @@ let act; let waitForAll; let waitFor; let assertLog; +let assertConsoleErrorDev; describe('ReactIncrementalUpdates', () => { beforeEach(() => { @@ -34,6 +35,7 @@ describe('ReactIncrementalUpdates', () => { waitForAll = InternalTestUtils.waitForAll; waitFor = InternalTestUtils.waitFor; assertLog = InternalTestUtils.assertLog; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); function Text({text}) { @@ -366,20 +368,21 @@ describe('ReactIncrementalUpdates', () => { return {a: 'a'}; }); - await expect( - async () => - await waitForAll([ - 'setState updater', - // Updates in the render phase receive the currently rendering - // lane, so the update flushes immediately in the same render. - 'render', - ]), - ).toErrorDev( + await waitForAll([ + 'setState updater', + // Updates in the render phase receive the currently rendering + // lane, so the update flushes immediately in the same render. + 'render', + ]); + assertConsoleErrorDev([ 'An update (setState, replaceState, or forceUpdate) was scheduled ' + 'from inside an update function. Update functions should be pure, ' + 'with zero side-effects. Consider using componentDidUpdate or a ' + - 'callback.\n\nPlease update the following component: Foo', - ); + 'callback.\n' + + '\n' + + 'Please update the following component: Foo\n' + + ' in Foo (at **)', + ]); expect(instance.state).toEqual({a: 'a', b: 'b'}); // Test deduplication (no additional warnings expected) diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index 1b665b2b8bc7f..748bfff6b0e7a 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -721,12 +721,13 @@ describe('ReactLazy', () => { } T.defaultProps = {inner: 'Hi'}; const LazyText = lazy(() => fakeImport(T)); - expect(() => { - LazyText.defaultProps = {outer: 'Bye'}; - }).toErrorDev( - 'It is not supported to assign `defaultProps` to ' + - 'a lazy component import. Either specify them where the component ' + - 'is defined, or create a wrapping component around it.', + LazyText.defaultProps = {outer: 'Bye'}; + assertConsoleErrorDev( + [ + 'It is not supported to assign `defaultProps` to ' + + 'a lazy component import. Either specify them where the component ' + + 'is defined, or create a wrapping component around it.', + ], {withoutStack: true}, ); @@ -742,14 +743,15 @@ describe('ReactLazy', () => { await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi Bye'); - await expect(async () => { - await act(() => resolveFakeImport(T)); - assertLog(['Hi Bye']); - }).toErrorDev( + await act(() => resolveFakeImport(T)); + assertLog(['Hi Bye']); + assertConsoleErrorDev([ 'T: Support for defaultProps ' + 'will be removed from function components in a future major ' + - 'release. Use JavaScript default parameters instead.', - ); + 'release. Use JavaScript default parameters instead.\n' + + ' in T (at **)\n' + + ' in Suspense (at **)', + ]); expect(root).toMatchRenderedOutput('Hi Bye'); @@ -1026,11 +1028,13 @@ describe('ReactLazy', () => { expect(root).not.toMatchRenderedOutput('Inner default text'); // Mount - await expect(async () => { - await act(() => resolveFakeImport(T)); - assertLog(['Inner default text']); - }).toErrorDev([ - 'T: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', + await act(() => resolveFakeImport(T)); + assertLog(['Inner default text']); + assertConsoleErrorDev([ + 'T: Support for defaultProps will be removed from function components in a future major release. ' + + 'Use JavaScript default parameters instead.\n' + + ' in T (at **)\n' + + ' in Suspense (at **)', ]); expect(root).toMatchRenderedOutput('Inner default text'); @@ -1063,13 +1067,19 @@ describe('ReactLazy', () => { await waitForAll(['Started loading', 'Loading...']); expect(root).not.toMatchRenderedOutput(
AB
); - await expect(async () => { - await act(() => resolveFakeImport(Foo)); - assertLog(['A', 'B']); - }).toErrorDev( - (gate(flags => flags.enableOwnerStacks) ? '' : ' in Text (at **)\n') + - ' in Foo (at **)', - ); + await act(() => resolveFakeImport(Foo)); + assertLog(['A', 'B']); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n' + + '\n' + + 'Check the render method of `Foo`. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in Foo (at **)' + : ' in Text (at **)\n' + + ' in Foo (at **)\n' + + ' in Suspense (at **)'), + ]); expect(root).toMatchRenderedOutput(
AB
); }); @@ -1143,11 +1153,12 @@ describe('ReactLazy', () => { expect(root).not.toMatchRenderedOutput('4'); // Mount - await expect(async () => { - await act(() => resolveFakeImport(Add)); - }).toErrorDev( - 'Unknown: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', - ); + await act(() => resolveFakeImport(Add)); + assertConsoleErrorDev([ + 'Unknown: Support for defaultProps will be removed from memo components in a future major release. ' + + 'Use JavaScript default parameters instead.\n' + + ' in Suspense (at **)', + ]); expect(root).toMatchRenderedOutput('4'); // Update (shallowly equal) @@ -1231,11 +1242,14 @@ describe('ReactLazy', () => { expect(root).not.toMatchRenderedOutput('4'); // Mount - await expect(async () => { - await act(() => resolveFakeImport(Add)); - }).toErrorDev([ - 'Memo: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', - 'Unknown: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', + await act(() => resolveFakeImport(Add)); + assertConsoleErrorDev([ + 'Memo: Support for defaultProps will be removed from memo components in a future major release. ' + + 'Use JavaScript default parameters instead.\n' + + ' in Suspense (at **)', + 'Unknown: Support for defaultProps will be removed from memo components in a future major release. ' + + 'Use JavaScript default parameters instead.\n' + + ' in Suspense (at **)', ]); expect(root).toMatchRenderedOutput('4'); diff --git a/packages/react-reconciler/src/__tests__/ReactMemo-test.js b/packages/react-reconciler/src/__tests__/ReactMemo-test.js index 5fdf14af4d37b..2a0c21d8dd9a4 100644 --- a/packages/react-reconciler/src/__tests__/ReactMemo-test.js +++ b/packages/react-reconciler/src/__tests__/ReactMemo-test.js @@ -19,6 +19,7 @@ let Scheduler; let act; let waitForAll; let assertLog; +let assertConsoleErrorDev; describe('memo', () => { beforeEach(() => { @@ -33,6 +34,7 @@ describe('memo', () => { const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; assertLog = InternalTestUtils.assertLog; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); function Text(props) { @@ -397,17 +399,19 @@ describe('memo', () => { // The final layer uses memo() from test fixture (which might be lazy). Counter = memo(Counter); - await expect(async () => { - await act(() => { - ReactNoop.render( - }> - - , - ); - }); - assertLog(['Loading...', 15]); - }).toErrorDev([ - 'Counter: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', + await act(() => { + ReactNoop.render( + }> + + , + ); + }); + assertLog(['Loading...', 15]); + assertConsoleErrorDev([ + 'Counter: Support for defaultProps will be removed from memo components in a future major release. ' + + 'Use JavaScript default parameters instead.\n' + + (label === 'lazy' ? '' : ' in Indirection (at **)\n') + + ' in Suspense (at **)', ]); expect(ReactNoop).toMatchRenderedOutput(); @@ -431,17 +435,23 @@ describe('memo', () => { }); it('warns if the first argument is undefined', () => { - expect(() => memo()).toErrorDev( - 'memo: The first argument must be a component. Instead ' + - 'received: undefined', + memo(); + assertConsoleErrorDev( + [ + 'memo: The first argument must be a component. Instead ' + + 'received: undefined', + ], {withoutStack: true}, ); }); it('warns if the first argument is null', () => { - expect(() => memo(null)).toErrorDev( - 'memo: The first argument must be a component. Instead ' + - 'received: null', + memo(null); + assertConsoleErrorDev( + [ + 'memo: The first argument must be a component. Instead ' + + 'received: null', + ], {withoutStack: true}, ); }); @@ -458,16 +468,18 @@ describe('memo', () => { Outer.defaultProps = {outer: 100}; const root = ReactNoop.createRoot(); - await expect(async () => { - await act(() => { - root.render( -
- -
, - ); - }); - }).toErrorDev([ - 'Support for defaultProps will be removed from memo component', + await act(() => { + root.render( +
+ +
, + ); + }); + assertConsoleErrorDev([ + 'Inner: ' + + 'Support for defaultProps will be removed from memo components in a future major release. ' + + 'Use JavaScript default parameters instead.\n' + + ' in div (at **)', ]); expect(root).toMatchRenderedOutput(
111
); @@ -559,14 +571,15 @@ describe('memo', () => {

, ); - await expect(async () => { - await waitForAll([]); - }).toErrorDev( + await waitForAll([]); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop. ' + 'See https://react.dev/link/warning-keys for more information.\n' + ' in span (at **)\n' + - ' in ', - ); + (gate('enableOwnerStacks') + ? ' in **/ReactMemo-test.js:**:** (at **)' + : ' in p (at **)'), + ]); }); it('should use the inner function name for the stack', async () => { @@ -578,16 +591,15 @@ describe('memo', () => {

, ); - await expect(async () => { - await waitForAll([]); - }).toErrorDev( + await waitForAll([]); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the top-level render call using . It was passed a child from Inner. ' + 'See https://react.dev/link/warning-keys for more information.\n' + ' in span (at **)\n' + ' in Inner (at **)' + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'), - ); + ]); }); it('should use the inner name in the stack', async () => { @@ -601,16 +613,15 @@ describe('memo', () => {

, ); - await expect(async () => { - await waitForAll([]); - }).toErrorDev( + await waitForAll([]); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the top-level render call using . It was passed a child from Inner. ' + 'See https://react.dev/link/warning-keys for more information.\n' + ' in span (at **)\n' + ' in Inner (at **)' + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'), - ); + ]); }); it('can use the outer displayName in the stack', async () => { @@ -623,16 +634,15 @@ describe('memo', () => {

, ); - await expect(async () => { - await waitForAll([]); - }).toErrorDev( + await waitForAll([]); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the top-level render call using . It was passed a child from Outer. ' + 'See https://react.dev/link/warning-keys for more information.\n' + ' in span (at **)\n' + ' in Outer (at **)' + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'), - ); + ]); }); it('should prefer the inner to the outer displayName in the stack', async () => { @@ -647,16 +657,15 @@ describe('memo', () => {

, ); - await expect(async () => { - await waitForAll([]); - }).toErrorDev( + await waitForAll([]); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the top-level render call using . It was passed a child from Inner. ' + 'See https://react.dev/link/warning-keys for more information.\n' + ' in span (at **)\n' + ' in Inner (at **)' + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'), - ); + ]); }); } }); diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js index b8fbca8339f5b..58cb2f9e648b5 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js @@ -861,8 +861,11 @@ describe('ReactNewContext', () => { , ); - await expect(async () => await waitForAll([])).toErrorDev( - 'The `value` prop is required for the ``. Did you misspell it or forget to pass it?', + await waitForAll([]); + assertConsoleErrorDev( + [ + 'The `value` prop is required for the ``. Did you misspell it or forget to pass it?', + ], { withoutStack: true, }, @@ -1036,7 +1039,9 @@ describe('ReactNewContext', () => { ); await waitForAll(['LegacyProvider', 'App', 'Child']); assertConsoleErrorDev([ - 'LegacyProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'LegacyProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in LegacyProvider (at **)', ]); expect(ReactNoop).toMatchRenderedOutput(); @@ -1320,9 +1325,16 @@ describe('ReactNewContext', () => { } ReactNoop.render(); - await expect(async () => await waitForAll([])).toErrorDev([ - 'Context can only be read while React is rendering', - 'Cannot update during an existing state transition', + await waitForAll([]); + assertConsoleErrorDev([ + 'Cannot update during an existing state transition (such as within `render`). ' + + 'Render methods should be a pure function of props and state.\n' + + ' in Cls (at **)', + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, ' + + 'but not inside Hooks like useReducer() or useMemo().\n' + + ' in Cls (at **)', ]); }); }); @@ -1353,10 +1365,12 @@ describe('ReactNewContext', () => { return useContext(Context.Consumer); } ReactNoop.render(); - await expect(async () => await waitForAll([])).toErrorDev( + await waitForAll([]); + assertConsoleErrorDev([ 'Calling useContext(Context.Consumer) is not supported and will cause bugs. ' + - 'Did you mean to call useContext(Context) instead?', - ); + 'Did you mean to call useContext(Context) instead?\n' + + ' in Foo (at **)', + ]); }); // Context consumer bails out on propagating "deep" updates when `value` hasn't changed. diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js index 6da6ef7573fd9..921c9666a935e 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js @@ -13,6 +13,7 @@ let React; let ReactNoop; let waitForAll; let act; +let assertConsoleErrorDev; describe('ReactSuspense', () => { beforeEach(() => { @@ -24,6 +25,7 @@ describe('ReactSuspense', () => { const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; act = InternalTestUtils.act; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); function createThenable() { @@ -57,10 +59,10 @@ describe('ReactSuspense', () => { ); ReactNoop.render(elementBadType); - await expect(async () => await waitForAll([])).toErrorDev( - ['Unexpected type for suspenseCallback.'], - {withoutStack: true}, - ); + await waitForAll([]); + assertConsoleErrorDev(['Unexpected type for suspenseCallback.'], { + withoutStack: true, + }); const elementMissingCallback = ( @@ -69,7 +71,8 @@ describe('ReactSuspense', () => { ); ReactNoop.render(elementMissingCallback); - await expect(async () => await waitForAll([])).toErrorDev([]); + await waitForAll([]); + assertConsoleErrorDev([]); }); // @gate enableSuspenseCallback diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js index 63c398a657b09..7cfe72dfc4ec8 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js @@ -18,6 +18,7 @@ let SuspenseList; let waitForAll; let assertLog; let waitFor; +let assertConsoleErrorDev; describe('ReactSuspenseList', () => { beforeEach(() => { @@ -37,6 +38,7 @@ describe('ReactSuspenseList', () => { assertLog = InternalTestUtils.assertLog; waitFor = InternalTestUtils.waitFor; act = InternalTestUtils.act; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); function Text(props) { @@ -72,11 +74,10 @@ describe('ReactSuspenseList', () => { ); } - await expect(async () => { - await act(() => { - ReactNoop.render(); - }); - }).toErrorDev([ + await act(() => { + ReactNoop.render(); + }); + assertConsoleErrorDev([ '"something" is not a supported revealOrder on ' + '. Did you mean "together", "forwards" or "backwards"?' + '\n in SuspenseList (at **)' + @@ -94,11 +95,10 @@ describe('ReactSuspenseList', () => { ); } - await expect(async () => { - await act(() => { - ReactNoop.render(); - }); - }).toErrorDev([ + await act(() => { + ReactNoop.render(); + }); + assertConsoleErrorDev([ '"TOGETHER" is not a valid value for revealOrder on ' + '. Use lowercase "together" instead.' + '\n in SuspenseList (at **)' + @@ -116,11 +116,10 @@ describe('ReactSuspenseList', () => { ); } - await expect(async () => { - await act(() => { - ReactNoop.render(); - }); - }).toErrorDev([ + await act(() => { + ReactNoop.render(); + }); + assertConsoleErrorDev([ '"forward" is not a valid value for revealOrder on ' + '. React uses the -s suffix in the spelling. ' + 'Use "forwards" instead.' + @@ -147,15 +146,14 @@ describe('ReactSuspenseList', () => { // No warning await waitForAll([]); - await expect(async () => { - await act(() => { - ReactNoop.render( - - Child - , - ); - }); - }).toErrorDev([ + await act(() => { + ReactNoop.render( + + Child + , + ); + }); + assertConsoleErrorDev([ 'A single row was passed to a . ' + 'This is not useful since it needs multiple rows. ' + 'Did you mean to pass multiple children or an array?' + @@ -174,11 +172,10 @@ describe('ReactSuspenseList', () => { ); } - await expect(async () => { - await act(() => { - ReactNoop.render(); - }); - }).toErrorDev([ + await act(() => { + ReactNoop.render(); + }); + assertConsoleErrorDev([ 'A single row was passed to a . ' + 'This is not useful since it needs multiple rows. ' + 'Did you mean to pass multiple children or an array?' + @@ -202,11 +199,10 @@ describe('ReactSuspenseList', () => { ); } - await expect(async () => { - await act(() => { - ReactNoop.render(); - }); - }).toErrorDev([ + await act(() => { + ReactNoop.render(); + }); + assertConsoleErrorDev([ 'A nested array was passed to row #0 in . ' + 'Wrap it in an additional SuspenseList to configure its revealOrder: ' + ' ... ' + @@ -1555,11 +1551,10 @@ describe('ReactSuspenseList', () => { ); } - await expect(async () => { - await act(() => { - ReactNoop.render(); - }); - }).toErrorDev([ + await act(() => { + ReactNoop.render(); + }); + assertConsoleErrorDev([ '"collapse" is not a supported value for tail on ' + '. Did you mean "collapsed" or "hidden"?' + '\n in SuspenseList (at **)' + @@ -1577,11 +1572,10 @@ describe('ReactSuspenseList', () => { ); } - await expect(async () => { - await act(() => { - ReactNoop.render(); - }); - }).toErrorDev([ + await act(() => { + ReactNoop.render(); + }); + assertConsoleErrorDev([ ' is only valid if ' + 'revealOrder is "forwards" or "backwards". ' + 'Did you mean to specify revealOrder="forwards"?' + diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index 0dcf0eb4705ee..79968949e3d99 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js @@ -1979,7 +1979,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.render(); }); - // TODO: assert toErrorDev() when the warning is implemented again. + // TODO: assertConsoleErrorDev() when the warning is implemented again. await act(() => { ReactNoop.flushSync(() => _setShow(true)); }); @@ -2006,7 +2006,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.render(); }); - // TODO: assert toErrorDev() when the warning is implemented again. + // TODO: assertConsoleErrorDev() when the warning is implemented again. await act(() => { ReactNoop.flushSync(() => show()); }); @@ -2076,7 +2076,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { ReactNoop.render(); }); - // TODO: assert toErrorDev() when the warning is implemented again. + // TODO: assertConsoleErrorDev() when the warning is implemented again. await act(() => { ReactNoop.flushSync(() => _setShow(true)); }); diff --git a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js index 7a850325e9f2c..64d51a1ff3cd9 100644 --- a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js +++ b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js @@ -14,6 +14,7 @@ let Scheduler; let act; let waitForAll; let assertLog; +let assertConsoleErrorDev; let getCacheForType; let useState; @@ -43,11 +44,11 @@ describe('ReactInteractionTracing', () => { ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); - act = require('internal-test-utils').act; - const InternalTestUtils = require('internal-test-utils'); + act = InternalTestUtils.act; waitForAll = InternalTestUtils.waitForAll; assertLog = InternalTestUtils.assertLog; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; useState = React.useState; startTransition = React.startTransition; @@ -1412,14 +1413,12 @@ describe('ReactInteractionTracing', () => { root.render(); ReactNoop.expire(1000); await advanceTimers(1000); - await expect( - async () => - await waitForAll([ - 'Suspend [Page Two]', - 'Loading...', - 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, newName: marker two, type: marker}])', - ]), - ).toErrorDev(''); + + await waitForAll([ + 'Suspend [Page Two]', + 'Loading...', + 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, newName: marker two, type: marker}])', + ]); resolveText('Page Two'); ReactNoop.expire(1000); @@ -2220,17 +2219,18 @@ describe('ReactInteractionTracing', () => { ); ReactNoop.expire(1000); await advanceTimers(1000); - await expect(async () => { - // onMarkerComplete shouldn't be called for transitions with - // new keys - await waitForAll([ - 'two', - 'onTransitionStart(transition two, 1000)', - 'onTransitionComplete(transition two, 1000, 2000)', - ]); - }).toErrorDev( - 'Changing the name of a tracing marker after mount is not supported.', - ); + // onMarkerComplete shouldn't be called for transitions with + // new keys + await waitForAll([ + 'two', + 'onTransitionStart(transition two, 1000)', + 'onTransitionComplete(transition two, 1000, 2000)', + ]); + assertConsoleErrorDev([ + 'Changing the name of a tracing marker after mount is not supported. ' + + 'To remount the tracing marker, pass it a new key.\n' + + ' in App (at **)', + ]); startTransition( () => root.render(), {