[assert helpers] react-reconciler (#31986)

Based off: https://github.com/facebook/react/pull/31984
This commit is contained in:
Ricky
2025-01-06 14:12:53 -05:00
committed by GitHub
parent 83be48b9de
commit 6b865330f4
17 changed files with 1100 additions and 637 deletions
+10 -5
View File
@@ -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);
});
@@ -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(<App />);
});
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(...)',
'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') ? ['not wrapped in 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},
);
});
@@ -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(<div>A</div>);
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']);
@@ -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(<Foo condition={false} />);
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,16 @@ describe('ReactFragment', () => {
}
ReactNoop.render(<Foo condition={true} />);
await expect(async () => await waitForAll([])).toErrorDev(
'Each child in a list should have a unique "key" prop.',
);
await waitForAll([]);
assertConsoleErrorDev([
'Each child in a list should have a unique "key" prop.\n' +
'\n' +
'Check the top-level render call using <Foo>. ' +
'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(<Foo condition={false} />);
// The key warning gets deduped because it's in the same component.
@@ -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(<App dependencies={['A', 'B']} />);
});
}).toErrorDev([
await act(() => {
root.update(<App dependencies={['A', 'B']} />);
});
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(<App text="Hello" hasDeps={false} />);
});
}).toErrorDev([
await act(() => {
root.update(<App text="Hello" hasDeps={false} />);
});
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(<App deps={'hello'} />, {
unstable_isConcurrent: true,
});
await act(() => {
ReactTestRenderer.create(<App deps={'hello'} />, {
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(<App deps={100500} />, {
unstable_isConcurrent: true,
});
await act(() => {
ReactTestRenderer.create(<App deps={100500} />, {
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(<App deps={{}} />, {
unstable_isConcurrent: true,
});
await act(() => {
ReactTestRenderer.create(<App deps={{}} />, {
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(<App deps={'hello'} />, {
unstable_isConcurrent: true,
});
await act(() => {
ReactTestRenderer.create(<App deps={'hello'} />, {
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(<App deps={null} />, {
@@ -817,17 +829,18 @@ describe('ReactHooks', () => {
}
await expect(async () => {
await expect(async () => {
await act(() => {
ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
});
}).rejects.toThrow('create is not a function');
}).toErrorDev([
await act(() => {
ReactTestRenderer.create(<App />, {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(<App />, {unstable_isConcurrent: true});
});
}).toErrorDev([
await act(() => {
ReactTestRenderer.create(<App />, {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(<App />, {unstable_isConcurrent: true});
});
}).toErrorDev(
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.',
);
await act(() => {
ReactTestRenderer.create(<App />, {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(<App />, {unstable_isConcurrent: true});
});
}).toErrorDev('Context can only be read while React is rendering');
await act(() => {
ReactTestRenderer.create(<App />, {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(<App />, {unstable_isConcurrent: true});
});
}).toErrorDev('Context can only be read while React is rendering');
await act(() => {
ReactTestRenderer.create(<App />, {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(<App />, {unstable_isConcurrent: true});
});
}).toErrorDev(['Context can only be read while React is rendering']);
await act(() => {
ReactTestRenderer.create(<App />, {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(
<>
<Fn />
<Cls />
</>,
{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(
<>
<Fn />
<Cls />
</>,
{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(<App />, {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(<App />, {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(<App />, {unstable_isConcurrent: true});
});
}).toErrorDev(
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.',
);
await act(() => {
ReactTestRenderer.create(<App />, {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(
<Boundary>
<App />
</Boundary>,
{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(
<Boundary>
<App />
</Boundary>,
{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(
<Boundary>
<App />
</Boundary>,
{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(
<Boundary>
<App />
</Boundary>,
{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(<App update={true} />);
});
} 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(<App update={true} />);
});
} 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(<App update={true} />);
});
} 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(<App update={true} />);
});
} 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(<App update={true} />);
}).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(<App update={true} />);
}).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(<App update={true} />);
});
}).rejects.toThrow('custom error');
}).toErrorDev([
await act(() => {
root.update(<App update={true} />);
});
}).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 **)',
]);
});
@@ -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', () => {
<Bar triggerUpdate={true} />
</>,
);
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(
<span prop={`Count: ${props.count}`} />,
@@ -1652,17 +1663,14 @@ describe('ReactHooksWithNoopRenderer', () => {
}, [props.count]);
return <Text text={'Count: ' + count} />;
}
await expect(async () => {
await act(async () => {
ReactNoop.render(<Counter count={0} />, () =>
Scheduler.log('Sync effect'),
);
await waitFor(['Count: (empty)', 'Sync effect']);
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Count: (empty)" />,
);
});
}).toErrorDev('flushSync was called from inside a lifecycle method');
await act(async () => {
ReactNoop.render(<Counter count={0} />, () =>
Scheduler.log('Sync effect'),
);
await waitFor(['Count: (empty)', 'Sync effect']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />);
});
assertLog([`Count: 0`]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
});
@@ -2506,35 +2514,47 @@ describe('ReactHooksWithNoopRenderer', () => {
}
const root1 = ReactNoop.createRoot();
await expect(async () => {
await act(() => {
root1.render(<App return={17} />);
});
}).toErrorDev([
'useEffect must not return anything besides a ' +
'function, which is used for clean-up. You returned: 17',
await act(() => {
root1.render(<App return={17} />);
});
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(<App return={null} />);
});
}).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(<App return={null} />);
});
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(<App return={Promise.resolve()} />);
});
}).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(<App return={Promise.resolve()} />);
});
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(<App return={17} />);
});
}).toErrorDev([
'useInsertionEffect must not return anything besides a ' +
'function, which is used for clean-up. You returned: 17',
await act(() => {
root1.render(<App return={17} />);
});
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(<App return={null} />);
});
}).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(<App return={null} />);
});
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(<App return={Promise.resolve()} />);
});
}).toErrorDev([
await act(() => {
root3.render(<App return={Promise.resolve()} />);
});
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(<App />);
});
}).toErrorDev(['useInsertionEffect must not schedule updates.']);
await act(() => {
root.render(<App />);
});
assertConsoleErrorDev([
'useInsertionEffect must not schedule updates.\n' +
' in App (at **)',
]);
await act(async () => {
root.render(<App throw={true} />);
@@ -2988,11 +3023,13 @@ describe('ReactHooksWithNoopRenderer', () => {
await act(() => {
root.render(<App foo="hello" />);
});
await expect(async () => {
await act(() => {
root.render(<App foo="goodbye" />);
});
}).toErrorDev(['useInsertionEffect must not schedule updates.']);
await act(() => {
root.render(<App foo="goodbye" />);
});
assertConsoleErrorDev([
'useInsertionEffect must not schedule updates.\n' +
' in App (at **)',
]);
await act(async () => {
root.render(<App throw={true} />);
@@ -3036,19 +3073,17 @@ describe('ReactHooksWithNoopRenderer', () => {
);
});
if (gate(flags => flags.enableHiddenSubtreeInsertionEffectCleanup)) {
await expect(async () => {
await act(() => {
root.render(<Activity mode="hidden" />);
});
}).toErrorDev(['useInsertionEffect must not schedule updates.']);
} else {
await expect(async () => {
await act(() => {
root.render(<Activity mode="hidden" />);
});
}).toErrorDev([]);
}
await act(() => {
root.render(<Activity mode="hidden" />);
});
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(<App return={17} />);
});
}).toErrorDev([
await act(() => {
root1.render(<App return={17} />);
});
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(<App return={null} />);
});
}).toErrorDev([
await act(() => {
root2.render(<App return={null} />);
});
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(<App return={Promise.resolve()} />);
});
}).toErrorDev([
await act(() => {
root3.render(<App return={Promise.resolve()} />);
});
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(<App id={1} />);
});
}).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(<App id={1} />);
});
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(<App id={1} />);
});
}).toErrorDev(
await act(() => {
ReactNoop.render(<App id={1} />);
});
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(<App loadC={true} />);
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(<App showMore={true} />);
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 **)',
]);
});
@@ -1798,9 +1798,19 @@ 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 **)' +
(gate('enableOwnerStacks') ? '' : '\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 **)' +
(gate('enableOwnerStacks')
? ''
: '\n in div (at **)' + '\n in Intl (at **)'),
]);
ReactNoop.render(
@@ -1853,8 +1863,18 @@ 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 **)' +
(gate('enableOwnerStacks') ? '' : '\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' +
(gate('enableOwnerStacks')
? ' in Indirection (at **)'
: ' in ShowRoute (at **)\n' +
' in Indirection (at **)\n' +
' in Router (at **)\n' +
' in Intl (at **)'),
]);
});
@@ -1890,8 +1910,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 +1967,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 +2063,27 @@ 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 **)' +
(gate('enableOwnerStacks')
? ''
: '\n in Stateful (at **)' +
'\n in IndirectionClass (at **)' +
'\n in IndirectionFn (at **)' +
' 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 **)' +
(gate('enableOwnerStacks')
? ''
: '\n in Stateful (at **)' +
'\n in IndirectionClass (at **)' +
'\n in IndirectionFn (at **)' +
' in Intl (at **)'),
]);
statefulInst.setState({x: 1});
@@ -2125,9 +2172,28 @@ 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' +
(gate('enableOwnerStacks') ? '' : ' 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 **)' +
(gate('enableOwnerStacks')
? ''
: '\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 **)' +
(gate('enableOwnerStacks')
? ''
: '\n in IndirectionClass (at **)' +
'\n in IndirectionFn (at **)' +
'\n in Intl (at **)' +
'\n in Stateful (at **)'),
]);
statefulInst.setState({locale: 'gr'});
@@ -2187,7 +2253,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 +2305,12 @@ describe('ReactIncremental', () => {
// Init
ReactNoop.render(<Root />);
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 +2318,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 +2369,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 +2506,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 +2573,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 +2656,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 +2751,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 +2872,19 @@ describe('ReactIncremental', () => {
<Boundary />
</React.StrictMode>,
);
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 **)',
]);
}
@@ -1212,12 +1212,18 @@ describe('ReactIncrementalErrorHandling', () => {
</Provider>,
);
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 **)' +
(gate('enableOwnerStacks') ? '' : '\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(<InvalidType />);
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},
);
}
@@ -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;
@@ -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)
@@ -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(<div>AB</div>);
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(<div>AB</div>);
});
@@ -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');
+57 -48
View File
@@ -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(
<Suspense fallback={<Text text="Loading..." />}>
<Counter e={5} />
</Suspense>,
);
});
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(
<Suspense fallback={<Text text="Loading..." />}>
<Counter e={5} />
</Suspense>,
);
});
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(<span prop={15} />);
@@ -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(
<div>
<Outer />
</div>,
);
});
}).toErrorDev([
'Support for defaultProps will be removed from memo component',
await act(() => {
root.render(
<div>
<Outer />
</div>,
);
});
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(<div>111</div>);
@@ -559,14 +571,15 @@ describe('memo', () => {
<MemoComponent />
</p>,
);
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', () => {
<MemoComponent />
</p>,
);
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 <Inner>. 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', () => {
<MemoComponent />
</p>,
);
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 <Inner>. 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', () => {
<MemoComponent />
</p>,
);
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 <Outer>. 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', () => {
<MemoComponent />
</p>,
);
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 <Inner>. 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 **)'),
);
]);
});
}
});
@@ -861,8 +861,11 @@ describe('ReactNewContext', () => {
<Context.Provider anyPropNameOtherThanValue="value could be anything" />,
);
await expect(async () => await waitForAll([])).toErrorDev(
'The `value` prop is required for the `<Context.Provider>`. Did you misspell it or forget to pass it?',
await waitForAll([]);
assertConsoleErrorDev(
[
'The `value` prop is required for the `<Context.Provider>`. 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(<span prop="Child" />);
@@ -1320,9 +1325,16 @@ describe('ReactNewContext', () => {
}
ReactNoop.render(<Cls />);
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(<Foo />);
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.
@@ -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 = (
<React.Suspense fallback={'Waiting'}>
@@ -69,7 +71,8 @@ describe('ReactSuspense', () => {
);
ReactNoop.render(elementMissingCallback);
await expect(async () => await waitForAll([])).toErrorDev([]);
await waitForAll([]);
assertConsoleErrorDev([]);
});
// @gate enableSuspenseCallback
@@ -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(<Foo />);
});
}).toErrorDev([
await act(() => {
ReactNoop.render(<Foo />);
});
assertConsoleErrorDev([
'"something" is not a supported revealOrder on ' +
'<SuspenseList />. Did you mean "together", "forwards" or "backwards"?' +
'\n in SuspenseList (at **)' +
@@ -94,11 +95,10 @@ describe('ReactSuspenseList', () => {
);
}
await expect(async () => {
await act(() => {
ReactNoop.render(<Foo />);
});
}).toErrorDev([
await act(() => {
ReactNoop.render(<Foo />);
});
assertConsoleErrorDev([
'"TOGETHER" is not a valid value for revealOrder on ' +
'<SuspenseList />. Use lowercase "together" instead.' +
'\n in SuspenseList (at **)' +
@@ -116,11 +116,10 @@ describe('ReactSuspenseList', () => {
);
}
await expect(async () => {
await act(() => {
ReactNoop.render(<Foo />);
});
}).toErrorDev([
await act(() => {
ReactNoop.render(<Foo />);
});
assertConsoleErrorDev([
'"forward" is not a valid value for revealOrder on ' +
'<SuspenseList />. 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(
<Foo>
<Suspense fallback="Loading">Child</Suspense>
</Foo>,
);
});
}).toErrorDev([
await act(() => {
ReactNoop.render(
<Foo>
<Suspense fallback="Loading">Child</Suspense>
</Foo>,
);
});
assertConsoleErrorDev([
'A single row was passed to a <SuspenseList revealOrder="forwards" />. ' +
'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(<Foo />);
});
}).toErrorDev([
await act(() => {
ReactNoop.render(<Foo />);
});
assertConsoleErrorDev([
'A single row was passed to a <SuspenseList revealOrder="backwards" />. ' +
'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(<Foo items={['A', 'B']} />);
});
}).toErrorDev([
await act(() => {
ReactNoop.render(<Foo items={['A', 'B']} />);
});
assertConsoleErrorDev([
'A nested array was passed to row #0 in <SuspenseList />. ' +
'Wrap it in an additional SuspenseList to configure its revealOrder: ' +
'<SuspenseList revealOrder=...> ... ' +
@@ -1555,11 +1551,10 @@ describe('ReactSuspenseList', () => {
);
}
await expect(async () => {
await act(() => {
ReactNoop.render(<Foo />);
});
}).toErrorDev([
await act(() => {
ReactNoop.render(<Foo />);
});
assertConsoleErrorDev([
'"collapse" is not a supported value for tail on ' +
'<SuspenseList />. Did you mean "collapsed" or "hidden"?' +
'\n in SuspenseList (at **)' +
@@ -1577,11 +1572,10 @@ describe('ReactSuspenseList', () => {
);
}
await expect(async () => {
await act(() => {
ReactNoop.render(<Foo />);
});
}).toErrorDev([
await act(() => {
ReactNoop.render(<Foo />);
});
assertConsoleErrorDev([
'<SuspenseList tail="collapsed" /> is only valid if ' +
'revealOrder is "forwards" or "backwards". ' +
'Did you mean to specify revealOrder="forwards"?' +
@@ -1979,7 +1979,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<App />);
});
// 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(<App />);
});
// 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(<App />);
});
// TODO: assert toErrorDev() when the warning is implemented again.
// TODO: assertConsoleErrorDev() when the warning is implemented again.
await act(() => {
ReactNoop.flushSync(() => _setShow(true));
});
@@ -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(<App navigate={true} markerName="marker two" />);
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(<App markerName="three" markerKey="new key" />),
{