@samitouri / QOS-React-1 / commits / 6b865330f4

[assert helpers] react-reconciler (#31986)

Based off: https://github.com/facebook/react/pull/31984

Ricky committed Jan 6, 2025 at 14:12 UTC 6b865330f4bc6c87dcd2c8cdf665895c8a190fc1
17 files changed +1101 -638
packages/react-reconciler/src/__tests__/Activity-test.js
+10 -5
@@ -14,6 +14,7 @@ let startTransition;
14 let waitForPaint;
15 let waitFor;
16 let assertLog;
17 +let assertConsoleErrorDev;
18
19 describe('Activity', () => {
20 beforeEach(() => {
@@ -37,6 +38,7 @@ describe('Activity', () => {
38 waitForPaint = InternalTestUtils.waitForPaint;
39 waitFor = InternalTestUtils.waitFor;
40 assertLog = InternalTestUtils.assertLog;
41 + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
42 });
43
44 function Text(props) {
@@ -784,11 +786,14 @@ describe('Activity', () => {
786 // would be null because it was nulled out when it was deleted, but there
787 // was no null check before we accessed it. A weird edge case but we must
788 // account for it.
787 - expect(() => {
788 - setState('Updated');
789 - }).toErrorDev(
790 - "Can't perform a React state update on a component that hasn't mounted yet",
791 - );
789 + setState('Updated');
790 + assertConsoleErrorDev([
791 + "Can't perform a React state update on a component that hasn't mounted yet. " +
792 + 'This indicates that you have a side-effect in your render function that ' +
793 + 'asynchronously later calls tries to update the component. ' +
794 + 'Move this work to useEffect instead.\n' +
795 + ' in Child (at **)',
796 + ]);
797 });
798 expect(root).toMatchRenderedOutput(null);
799 });
packages/react-reconciler/src/__tests__/ReactActWarnings-test.js
+120 -33
@@ -18,6 +18,7 @@ let Suspense;
18 let startTransition;
19 let getCacheForType;
20 let caches;
21 +let assertConsoleErrorDev;
22
23 // These tests are mostly concerned with concurrent roots. The legacy root
24 // behavior is covered by other older test suites and is unchanged from
@@ -38,6 +39,7 @@ describe('act warnings', () => {
39 const InternalTestUtils = require('internal-test-utils');
40 waitForAll = InternalTestUtils.waitForAll;
41 assertLog = InternalTestUtils.assertLog;
42 + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
43 });
44
45 function createTextCache() {
@@ -171,9 +173,21 @@ describe('act warnings', () => {
173
174 // Flag is true. Warn.
175 await withActEnvironment(true, async () => {
174 - expect(() => setState(2)).toErrorDev(
175 - 'An update to App inside a test was not wrapped in act',
176 - );
176 + setState(2);
177 + assertConsoleErrorDev([
178 + 'An update to App inside a test was not wrapped in act(...).\n' +
179 + '\n' +
180 + 'When testing, code that causes React state updates should be wrapped into act(...):\n' +
181 + '\n' +
182 + 'act(() => {\n' +
183 + ' /* fire events that update state */\n' +
184 + '});\n' +
185 + '/* assert on the output */\n' +
186 + '\n' +
187 + "This ensures that you're testing the behavior the user would see in the browser. " +
188 + 'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
189 + ' in App (at **)',
190 + ]);
191 await waitForAll([2]);
192 expect(root).toMatchRenderedOutput('2');
193 });
@@ -202,12 +216,11 @@ describe('act warnings', () => {
216
217 // Default behavior. Flag is undefined. Warn.
218 expect(global.IS_REACT_ACT_ENVIRONMENT).toBe(undefined);
205 - expect(() => {
206 - act(() => {
207 - setState(1);
208 - });
209 - }).toErrorDev(
210 - 'The current testing environment is not configured to support act(...)',
219 + act(() => {
220 + setState(1);
221 + });
222 + assertConsoleErrorDev(
223 + ['The current testing environment is not configured to support act(...)'],
224 {withoutStack: true},
225 );
226 assertLog([1]);
@@ -224,12 +237,13 @@ describe('act warnings', () => {
237
238 // Flag is false. Warn.
239 await withActEnvironment(false, () => {
227 - expect(() => {
228 - act(() => {
229 - setState(1);
230 - });
231 - }).toErrorDev(
232 - 'The current testing environment is not configured to support act(...)',
240 + act(() => {
241 + setState(1);
242 + });
243 + assertConsoleErrorDev(
244 + [
245 + 'The current testing environment is not configured to support act(...)',
246 + ],
247 {withoutStack: true},
248 );
249 assertLog([1]);
@@ -240,10 +254,23 @@ describe('act warnings', () => {
254 it('warns if root update is not wrapped', async () => {
255 await withActEnvironment(true, () => {
256 const root = ReactNoop.createRoot();
243 - expect(() => root.render('Hi')).toErrorDev(
244 - // TODO: Better error message that doesn't make it look like "Root" is
245 - // the name of a custom component
246 - 'An update to Root inside a test was not wrapped in act(...)',
257 + root.render('Hi');
258 + assertConsoleErrorDev(
259 + [
260 + // TODO: Better error message that doesn't make it look like "Root" is
261 + // the name of a custom component
262 + 'An update to Root inside a test was not wrapped in act(...).\n' +
263 + '\n' +
264 + 'When testing, code that causes React state updates should be wrapped into act(...):\n' +
265 + '\n' +
266 + 'act(() => {\n' +
267 + ' /* fire events that update state */\n' +
268 + '});\n' +
269 + '/* assert on the output */\n' +
270 + '\n' +
271 + "This ensures that you're testing the behavior the user would see in the browser. " +
272 + 'Learn more at https://react.dev/link/wrap-tests-with-act',
273 + ],
274 {withoutStack: true},
275 );
276 });
@@ -265,9 +292,21 @@ describe('act warnings', () => {
292 act(() => {
293 root.render(<App />);
294 });
268 - expect(() => app.setState({count: 1})).toErrorDev(
269 - 'An update to App inside a test was not wrapped in act(...)',
270 - );
295 + app.setState({count: 1});
296 + assertConsoleErrorDev([
297 + 'An update to App inside a test was not wrapped in act(...).\n' +
298 + '\n' +
299 + 'When testing, code that causes React state updates should be wrapped into act(...):\n' +
300 + '\n' +
301 + 'act(() => {\n' +
302 + ' /* fire events that update state */\n' +
303 + '});\n' +
304 + '/* assert on the output */\n' +
305 + '\n' +
306 + "This ensures that you're testing the behavior the user would see in the browser. " +
307 + 'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
308 + ' in App (at **)',
309 + ]);
310 });
311 });
312
@@ -288,9 +327,21 @@ describe('act warnings', () => {
327
328 // Even though this update is synchronous, we should still fire a warning,
329 // because it could have spawned additional asynchronous work
291 - expect(() => ReactNoop.flushSync(() => setState(1))).toErrorDev(
292 - 'An update to App inside a test was not wrapped in act(...)',
293 - );
330 + ReactNoop.flushSync(() => setState(1));
331 + assertConsoleErrorDev([
332 + 'An update to App inside a test was not wrapped in act(...).\n' +
333 + '\n' +
334 + 'When testing, code that causes React state updates should be wrapped into act(...):\n' +
335 + '\n' +
336 + 'act(() => {\n' +
337 + ' /* fire events that update state */\n' +
338 + '});\n' +
339 + '/* assert on the output */\n' +
340 + '\n' +
341 + "This ensures that you're testing the behavior the user would see in the browser. " +
342 + 'Learn more at https://react.dev/link/wrap-tests-with-act\n' +
343 + ' in App (at **)',
344 + ]);
345
346 assertLog([1]);
347 expect(root).toMatchRenderedOutput('1');
@@ -322,12 +373,36 @@ describe('act warnings', () => {
373 expect(root).toMatchRenderedOutput('Loading...');
374
375 // This is a retry, not a ping, because we already showed a fallback.
325 - expect(() => resolveText('Async')).toErrorDev(
376 + resolveText('Async');
377 + assertConsoleErrorDev(
378 [
327 - 'A suspended resource finished loading inside a test, but the event ' +
328 - 'was not wrapped in act(...)',
329 -
330 - ...(gate('enableSiblingPrerendering') ? ['not wrapped in act'] : []),
379 + 'A suspended resource finished loading inside a test, but the event was not wrapped in act(...).\n' +
380 + '\n' +
381 + 'When testing, code that resolves suspended data should be wrapped into act(...):\n' +
382 + '\n' +
383 + 'act(() => {\n' +
384 + ' /* finish loading suspended data */\n' +
385 + '});\n' +
386 + '/* assert on the output */\n' +
387 + '\n' +
388 + "This ensures that you're testing the behavior the user would see in the browser. " +
389 + 'Learn more at https://react.dev/link/wrap-tests-with-act',
390 +
391 + ...(gate('enableSiblingPrerendering')
392 + ? [
393 + 'A suspended resource finished loading inside a test, but the event was not wrapped in act(...).\n' +
394 + '\n' +
395 + 'When testing, code that resolves suspended data should be wrapped into act(...):\n' +
396 + '\n' +
397 + 'act(() => {\n' +
398 + ' /* finish loading suspended data */\n' +
399 + '});\n' +
400 + '/* assert on the output */\n' +
401 + '\n' +
402 + "This ensures that you're testing the behavior the user would see in the browser. " +
403 + 'Learn more at https://react.dev/link/wrap-tests-with-act',
404 + ]
405 + : []),
406 ],
407
408 {withoutStack: true},
@@ -363,9 +438,21 @@ describe('act warnings', () => {
438 expect(root).toMatchRenderedOutput('(empty)');
439
440 // This is a ping, not a retry, because no fallback is showing.
366 - expect(() => resolveText('Async')).toErrorDev(
367 - 'A suspended resource finished loading inside a test, but the event ' +
368 - 'was not wrapped in act(...)',
441 + resolveText('Async');
442 + assertConsoleErrorDev(
443 + [
444 + 'A suspended resource finished loading inside a test, but the event was not wrapped in act(...).\n' +
445 + '\n' +
446 + 'When testing, code that resolves suspended data should be wrapped into act(...):\n' +
447 + '\n' +
448 + 'act(() => {\n' +
449 + ' /* finish loading suspended data */\n' +
450 + '});\n' +
451 + '/* assert on the output */\n' +
452 + '\n' +
453 + "This ensures that you're testing the behavior the user would see in the browser. " +
454 + 'Learn more at https://react.dev/link/wrap-tests-with-act',
455 + ],
456 {withoutStack: true},
457 );
458 });
packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js
+13 -9
@@ -7,6 +7,7 @@ let useTransition;
7 let useState;
8 let useOptimistic;
9 let textCache;
10 +let assertConsoleErrorDev;
11
12 describe('ReactAsyncActions', () => {
13 beforeEach(() => {
@@ -21,6 +22,8 @@ describe('ReactAsyncActions', () => {
22 Scheduler = require('scheduler');
23 act = require('internal-test-utils').act;
24 assertLog = require('internal-test-utils').assertLog;
25 + assertConsoleErrorDev =
26 + require('internal-test-utils').assertConsoleErrorDev;
27 useTransition = React.useTransition;
28 useState = React.useState;
29 useOptimistic = React.useOptimistic;
@@ -1231,15 +1234,16 @@ describe('ReactAsyncActions', () => {
1234 assertLog(['A']);
1235 expect(root).toMatchRenderedOutput(<div>A</div>);
1236
1234 - await expect(async () => {
1235 - await act(() => {
1236 - setLoadingProgress('25%');
1237 - startTransition(() => setText('B'));
1238 - });
1239 - }).toErrorDev(
1240 - 'An optimistic state update occurred outside a transition or ' +
1241 - 'action. To fix, move the update to an action, or wrap ' +
1242 - 'with startTransition.',
1237 + await act(() => {
1238 + setLoadingProgress('25%');
1239 + startTransition(() => setText('B'));
1240 + });
1241 + assertConsoleErrorDev(
1242 + [
1243 + 'An optimistic state update occurred outside a transition or ' +
1244 + 'action. To fix, move the update to an action, or wrap ' +
1245 + 'with startTransition.',
1246 + ],
1247 {withoutStack: true},
1248 );
1249 assertLog(['Loading... (25%)', 'A', 'B']);
packages/react-reconciler/src/__tests__/ReactFragment-test.js
+28 -6
@@ -12,6 +12,7 @@
12 let React;
13 let ReactNoop;
14 let waitForAll;
15 +let assertConsoleErrorDev;
16
17 describe('ReactFragment', () => {
18 beforeEach(function () {
@@ -22,6 +23,7 @@ describe('ReactFragment', () => {
23
24 const InternalTestUtils = require('internal-test-utils');
25 waitForAll = InternalTestUtils.waitForAll;
26 + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
27 });
28
29 it('should render a single child via noop renderer', async () => {
@@ -740,9 +742,22 @@ describe('ReactFragment', () => {
742 await waitForAll([]);
743
744 ReactNoop.render(<Foo condition={false} />);
743 - await expect(async () => await waitForAll([])).toErrorDev(
744 - 'Each child in a list should have a unique "key" prop.',
745 - );
745 + await waitForAll([]);
746 + assertConsoleErrorDev([
747 + gate('enableOwnerStacks')
748 + ? 'Each child in a list should have a unique "key" prop.\n' +
749 + '\n' +
750 + 'Check the render method of `div`. ' +
751 + 'It was passed a child from Foo. ' +
752 + 'See https://react.dev/link/warning-keys for more information.\n' +
753 + ' in Foo (at **)'
754 + : 'Each child in a list should have a unique "key" prop.\n' +
755 + '\n' +
756 + 'Check the render method of `Foo`. ' +
757 + 'See https://react.dev/link/warning-keys for more information.\n' +
758 + ' in Stateful (at **)\n' +
759 + ' in Foo (at **)',
760 + ]);
761
762 expect(ops).toEqual([]);
763 expect(ReactNoop).toMatchRenderedOutput(
@@ -937,9 +952,16 @@ describe('ReactFragment', () => {
952 }
953
954 ReactNoop.render(<Foo condition={true} />);
940 - await expect(async () => await waitForAll([])).toErrorDev(
941 - 'Each child in a list should have a unique "key" prop.',
942 - );
955 + await waitForAll([]);
956 + assertConsoleErrorDev([
957 + 'Each child in a list should have a unique "key" prop.\n' +
958 + '\n' +
959 + 'Check the top-level render call using <Foo>. ' +
960 + 'It was passed a child from Foo. ' +
961 + 'See https://react.dev/link/warning-keys for more information.\n' +
962 + ' in span (at **)\n' +
963 + ' in Foo (at **)',
964 + ]);
965
966 ReactNoop.render(<Foo condition={false} />);
967 // The key warning gets deduped because it's in the same component.
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+305 -217
@@ -239,17 +239,18 @@ describe('ReactHooks', () => {
239 await waitForAll(['Count: 0']);
240 expect(root).toMatchRenderedOutput('0');
241
242 - await expect(async () => {
243 - await act(() =>
244 - setCounter(1, () => {
245 - throw new Error('Expected to ignore the callback.');
246 - }),
247 - );
248 - }).toErrorDev(
249 - 'State updates from the useState() and useReducer() Hooks ' +
250 - "don't support the second callback argument. " +
251 - 'To execute a side effect after rendering, ' +
252 - 'declare it in the component body with useEffect().',
242 + await act(() =>
243 + setCounter(1, () => {
244 + throw new Error('Expected to ignore the callback.');
245 + }),
246 + );
247 + assertConsoleErrorDev(
248 + [
249 + 'State updates from the useState() and useReducer() Hooks ' +
250 + "don't support the second callback argument. " +
251 + 'To execute a side effect after rendering, ' +
252 + 'declare it in the component body with useEffect().',
253 + ],
254 {withoutStack: true},
255 );
256 assertLog(['Count: 1']);
@@ -273,17 +274,18 @@ describe('ReactHooks', () => {
274 await waitForAll(['Count: 0']);
275 expect(root).toMatchRenderedOutput('0');
276
276 - await expect(async () => {
277 - await act(() =>
278 - dispatch(1, () => {
279 - throw new Error('Expected to ignore the callback.');
280 - }),
281 - );
282 - }).toErrorDev(
283 - 'State updates from the useState() and useReducer() Hooks ' +
284 - "don't support the second callback argument. " +
285 - 'To execute a side effect after rendering, ' +
286 - 'declare it in the component body with useEffect().',
277 + await act(() =>
278 + dispatch(1, () => {
279 + throw new Error('Expected to ignore the callback.');
280 + }),
281 + );
282 + assertConsoleErrorDev(
283 + [
284 + 'State updates from the useState() and useReducer() Hooks ' +
285 + "don't support the second callback argument. " +
286 + 'To execute a side effect after rendering, ' +
287 + 'declare it in the component body with useEffect().',
288 + ],
289 {withoutStack: true},
290 );
291 assertLog(['Count: 1']);
@@ -581,16 +583,17 @@ describe('ReactHooks', () => {
583 });
584 });
585 assertLog(['Did commit: A']);
584 - await expect(async () => {
585 - await act(() => {
586 - root.update(<App dependencies={['A', 'B']} />);
587 - });
588 - }).toErrorDev([
586 + await act(() => {
587 + root.update(<App dependencies={['A', 'B']} />);
588 + });
589 + assertConsoleErrorDev([
590 'The final argument passed to useLayoutEffect changed size ' +
591 'between renders. The order and size of this array must remain ' +
591 - 'constant.\n\n' +
592 + 'constant.\n' +
593 + '\n' +
594 'Previous: [A]\n' +
593 - 'Incoming: [A, B]\n',
595 + 'Incoming: [A, B]\n' +
596 + ' in App (at **)',
597 ]);
598 });
599
@@ -617,14 +620,14 @@ describe('ReactHooks', () => {
620 assertLog(['Compute']);
621 expect(root).toMatchRenderedOutput('HELLO');
622
620 - await expect(async () => {
621 - await act(() => {
622 - root.update(<App text="Hello" hasDeps={false} />);
623 - });
624 - }).toErrorDev([
623 + await act(() => {
624 + root.update(<App text="Hello" hasDeps={false} />);
625 + });
626 + assertConsoleErrorDev([
627 'useMemo received a final argument during this render, but ' +
628 'not during the previous render. Even though the final argument is ' +
627 - 'optional, its type cannot change between renders.',
629 + 'optional, its type cannot change between renders.\n' +
630 + ' in App (at **)',
631 ]);
632 });
633
@@ -639,53 +642,62 @@ describe('ReactHooks', () => {
642 return null;
643 }
644
642 - await expect(async () => {
643 - await act(() => {
644 - ReactTestRenderer.create(<App deps={'hello'} />, {
645 - unstable_isConcurrent: true,
646 - });
645 + await act(() => {
646 + ReactTestRenderer.create(<App deps={'hello'} />, {
647 + unstable_isConcurrent: true,
648 });
648 - }).toErrorDev([
649 + });
650 + assertConsoleErrorDev([
651 'useEffect received a final argument that is not an array (instead, received `string`). ' +
650 - 'When specified, the final argument must be an array.',
652 + 'When specified, the final argument must be an array.\n' +
653 + ' in App (at **)',
654 'useLayoutEffect received a final argument that is not an array (instead, received `string`). ' +
652 - 'When specified, the final argument must be an array.',
655 + 'When specified, the final argument must be an array.\n' +
656 + ' in App (at **)',
657 'useMemo received a final argument that is not an array (instead, received `string`). ' +
654 - 'When specified, the final argument must be an array.',
658 + 'When specified, the final argument must be an array.\n' +
659 + ' in App (at **)',
660 'useCallback received a final argument that is not an array (instead, received `string`). ' +
656 - 'When specified, the final argument must be an array.',
661 + 'When specified, the final argument must be an array.\n' +
662 + ' in App (at **)',
663 ]);
658 - await expect(async () => {
659 - await act(() => {
660 - ReactTestRenderer.create(<App deps={100500} />, {
661 - unstable_isConcurrent: true,
662 - });
664 + await act(() => {
665 + ReactTestRenderer.create(<App deps={100500} />, {
666 + unstable_isConcurrent: true,
667 });
664 - }).toErrorDev([
668 + });
669 + assertConsoleErrorDev([
670 'useEffect received a final argument that is not an array (instead, received `number`). ' +
666 - 'When specified, the final argument must be an array.',
671 + 'When specified, the final argument must be an array.\n' +
672 + ' in App (at **)',
673 'useLayoutEffect received a final argument that is not an array (instead, received `number`). ' +
668 - 'When specified, the final argument must be an array.',
674 + 'When specified, the final argument must be an array.\n' +
675 + ' in App (at **)',
676 'useMemo received a final argument that is not an array (instead, received `number`). ' +
670 - 'When specified, the final argument must be an array.',
677 + 'When specified, the final argument must be an array.\n' +
678 + ' in App (at **)',
679 'useCallback received a final argument that is not an array (instead, received `number`). ' +
672 - 'When specified, the final argument must be an array.',
680 + 'When specified, the final argument must be an array.\n' +
681 + ' in App (at **)',
682 ]);
674 - await expect(async () => {
675 - await act(() => {
676 - ReactTestRenderer.create(<App deps={{}} />, {
677 - unstable_isConcurrent: true,
678 - });
683 + await act(() => {
684 + ReactTestRenderer.create(<App deps={{}} />, {
685 + unstable_isConcurrent: true,
686 });
680 - }).toErrorDev([
687 + });
688 + assertConsoleErrorDev([
689 'useEffect received a final argument that is not an array (instead, received `object`). ' +
682 - 'When specified, the final argument must be an array.',
690 + 'When specified, the final argument must be an array.\n' +
691 + ' in App (at **)',
692 'useLayoutEffect received a final argument that is not an array (instead, received `object`). ' +
684 - 'When specified, the final argument must be an array.',
693 + 'When specified, the final argument must be an array.\n' +
694 + ' in App (at **)',
695 'useMemo received a final argument that is not an array (instead, received `object`). ' +
686 - 'When specified, the final argument must be an array.',
696 + 'When specified, the final argument must be an array.\n' +
697 + ' in App (at **)',
698 'useCallback received a final argument that is not an array (instead, received `object`). ' +
688 - 'When specified, the final argument must be an array.',
699 + 'When specified, the final argument must be an array.\n' +
700 + ' in App (at **)',
701 ]);
702
703 await act(() => {
@@ -710,15 +722,15 @@ describe('ReactHooks', () => {
722 });
723 App.displayName = 'App';
724
713 - await expect(async () => {
714 - await act(() => {
715 - ReactTestRenderer.create(<App deps={'hello'} />, {
716 - unstable_isConcurrent: true,
717 - });
725 + await act(() => {
726 + ReactTestRenderer.create(<App deps={'hello'} />, {
727 + unstable_isConcurrent: true,
728 });
719 - }).toErrorDev([
729 + });
730 + assertConsoleErrorDev([
731 'useImperativeHandle received a final argument that is not an array (instead, received `string`). ' +
721 - 'When specified, the final argument must be an array.',
732 + 'When specified, the final argument must be an array.\n' +
733 + ' in App (at **)',
734 ]);
735 await act(() => {
736 ReactTestRenderer.create(<App deps={null} />, {
@@ -817,17 +829,18 @@ describe('ReactHooks', () => {
829 }
830
831 await expect(async () => {
820 - await expect(async () => {
821 - await act(() => {
822 - ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
823 - });
824 - }).rejects.toThrow('create is not a function');
825 - }).toErrorDev([
832 + await act(() => {
833 + ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
834 + });
835 + }).rejects.toThrow('create is not a function');
836 + assertConsoleErrorDev([
837 + 'Expected useImperativeHandle() second argument to be a function ' +
838 + 'that creates a handle. Instead received: undefined.\n' +
839 + ' in App (at **)',
840 'Expected useImperativeHandle() first argument to either be a ' +
841 'ref callback or React.createRef() object. ' +
828 - 'Instead received: an object with keys {focus}.',
829 - 'Expected useImperativeHandle() second argument to be a function ' +
830 - 'that creates a handle. Instead received: undefined.',
842 + 'Instead received: an object with keys {focus}.\n' +
843 + ' in App (at **)',
844 ]);
845 });
846
@@ -841,13 +854,13 @@ describe('ReactHooks', () => {
854 });
855 App.displayName = 'App';
856
844 - await expect(async () => {
845 - await act(() => {
846 - ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
847 - });
848 - }).toErrorDev([
857 + await act(() => {
858 + ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
859 + });
860 + assertConsoleErrorDev([
861 'Expected useImperativeHandle() second argument to be a function ' +
850 - 'that creates a handle. Instead received: object.',
862 + 'that creates a handle. Instead received: object.\n' +
863 + ' in App (at **)',
864 ]);
865 });
866
@@ -933,13 +946,15 @@ describe('ReactHooks', () => {
946 });
947 return null;
948 }
936 - await expect(async () => {
937 - await act(() => {
938 - ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
939 - });
940 - }).toErrorDev(
941 - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.',
942 - );
949 + await act(() => {
950 + ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
951 + });
952 + assertConsoleErrorDev([
953 + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
954 + 'You can only call Hooks at the top level of your React function. ' +
955 + 'For more information, see https://react.dev/link/rules-of-hooks\n' +
956 + ' in App (at **)',
957 + ]);
958 });
959
960 it('warns when reading context inside useMemo', async () => {
@@ -954,11 +969,16 @@ describe('ReactHooks', () => {
969 }, []);
970 }
971
957 - await expect(async () => {
958 - await act(() => {
959 - ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
960 - });
961 - }).toErrorDev('Context can only be read while React is rendering');
972 + await act(() => {
973 + ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
974 + });
975 + assertConsoleErrorDev([
976 + 'Context can only be read while React is rendering. ' +
977 + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
978 + 'In function components, you can read it directly in the function body, ' +
979 + 'but not inside Hooks like useReducer() or useMemo().\n' +
980 + ' in App (at **)',
981 + ]);
982 });
983
984 it('warns when reading context inside useMemo after reading outside it', async () => {
@@ -977,11 +997,16 @@ describe('ReactHooks', () => {
997 }, []);
998 }
999
980 - await expect(async () => {
981 - await act(() => {
982 - ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
983 - });
984 - }).toErrorDev('Context can only be read while React is rendering');
1000 + await act(() => {
1001 + ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1002 + });
1003 + assertConsoleErrorDev([
1004 + 'Context can only be read while React is rendering. ' +
1005 + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1006 + 'In function components, you can read it directly in the function body, ' +
1007 + 'but not inside Hooks like useReducer() or useMemo().\n' +
1008 + ' in App (at **)',
1009 + ]);
1010 expect(firstRead).toBe('light');
1011 expect(secondRead).toBe('light');
1012 });
@@ -1048,11 +1073,16 @@ describe('ReactHooks', () => {
1073 return null;
1074 }
1075
1051 - await expect(async () => {
1052 - await act(() => {
1053 - ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1054 - });
1055 - }).toErrorDev(['Context can only be read while React is rendering']);
1076 + await act(() => {
1077 + ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1078 + });
1079 + assertConsoleErrorDev([
1080 + 'Context can only be read while React is rendering. ' +
1081 + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1082 + 'In function components, you can read it directly in the function body, ' +
1083 + 'but not inside Hooks like useReducer() or useMemo().\n' +
1084 + ' in App (at **)',
1085 + ]);
1086 });
1087
1088 // Edge case.
@@ -1078,19 +1108,25 @@ describe('ReactHooks', () => {
1108 }
1109 }
1110
1081 - await expect(async () => {
1082 - await act(() => {
1083 - ReactTestRenderer.create(
1084 - <>
1085 - <Fn />
1086 - <Cls />
1087 - </>,
1088 - {unstable_isConcurrent: true},
1089 - );
1090 - });
1091 - }).toErrorDev([
1092 - 'Context can only be read while React is rendering',
1093 - 'Cannot update a component (`Fn`) while rendering a different component (`Cls`).',
1111 + await act(() => {
1112 + ReactTestRenderer.create(
1113 + <>
1114 + <Fn />
1115 + <Cls />
1116 + </>,
1117 + {unstable_isConcurrent: true},
1118 + );
1119 + });
1120 + assertConsoleErrorDev([
1121 + 'Context can only be read while React is rendering. ' +
1122 + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1123 + 'In function components, you can read it directly in the function body, ' +
1124 + 'but not inside Hooks like useReducer() or useMemo().\n' +
1125 + ' in Cls (at **)',
1126 + 'Cannot update a component (`Fn`) while rendering a different component (`Cls`). ' +
1127 + 'To locate the bad setState() call inside `Cls`, ' +
1128 + 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
1129 + ' in Cls (at **)',
1130 ]);
1131 });
1132
@@ -1110,24 +1146,32 @@ describe('ReactHooks', () => {
1146 }
1147
1148 await expect(async () => {
1113 - await expect(async () => {
1114 - await act(() => {
1115 - ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1116 - });
1117 - }).rejects.toThrow(
1118 - 'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
1119 - );
1120 - }).toErrorDev([
1121 - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1149 + await act(() => {
1150 + ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1151 + });
1152 + }).rejects.toThrow(
1153 + 'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
1154 + );
1155 + assertConsoleErrorDev([
1156 + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1157 + 'You can only call Hooks at the top level of your React function. ' +
1158 + 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1159 + ' in App (at **)',
1160 'React has detected a change in the order of Hooks called by App. ' +
1161 'This will lead to bugs and errors if not fixed. For more information, ' +
1124 - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' +
1162 + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
1163 + '\n' +
1164 ' Previous render Next render\n' +
1165 ' ------------------------------------------------------\n' +
1166 '1. useReducer useReducer\n' +
1167 '2. useState useRef\n' +
1129 - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
1130 - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1168 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
1169 + '\n' +
1170 + ' in App (at **)',
1171 + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1172 + 'You can only call Hooks at the top level of your React function. ' +
1173 + 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1174 + ' in App (at **)',
1175 ]);
1176 });
1177
@@ -1140,13 +1184,15 @@ describe('ReactHooks', () => {
1184 });
1185 return null;
1186 }
1143 - await expect(async () => {
1144 - await act(() => {
1145 - ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1146 - });
1147 - }).toErrorDev(
1148 - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.',
1149 - );
1187 + await act(() => {
1188 + ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1189 + });
1190 + assertConsoleErrorDev([
1191 + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1192 + 'You can only call Hooks at the top level of your React function. ' +
1193 + 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1194 + ' in App (at **)',
1195 + ]);
1196 });
1197
1198 it('resets warning internal state when interrupted by an error', async () => {
@@ -1177,21 +1223,37 @@ describe('ReactHooks', () => {
1223 }
1224 }
1225
1180 - await expect(async () => {
1181 - await act(() => {
1182 - ReactTestRenderer.create(
1183 - <Boundary>
1184 - <App />
1185 - </Boundary>,
1186 - {unstable_isConcurrent: true},
1187 - );
1188 - });
1189 - }).toErrorDev([
1190 - 'Context can only be read while React is rendering',
1191 - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1192 -
1193 - 'Context can only be read while React is rendering',
1194 - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1226 + await act(() => {
1227 + ReactTestRenderer.create(
1228 + <Boundary>
1229 + <App />
1230 + </Boundary>,
1231 + {unstable_isConcurrent: true},
1232 + );
1233 + });
1234 + assertConsoleErrorDev([
1235 + 'Context can only be read while React is rendering. ' +
1236 + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1237 + 'In function components, you can read it directly in the function body, ' +
1238 + 'but not inside Hooks like useReducer() or useMemo().\n' +
1239 + ' in App (at **)' +
1240 + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'),
1241 + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1242 + 'You can only call Hooks at the top level of your React function. ' +
1243 + 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1244 + ' in App (at **)' +
1245 + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'),
1246 + 'Context can only be read while React is rendering. ' +
1247 + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1248 + 'In function components, you can read it directly in the function body, ' +
1249 + 'but not inside Hooks like useReducer() or useMemo().\n' +
1250 + ' in App (at **)' +
1251 + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'),
1252 + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1253 + 'You can only call Hooks at the top level of your React function. ' +
1254 + 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1255 + ' in App (at **)' +
1256 + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'),
1257 ]);
1258
1259 function Valid() {
@@ -1218,21 +1280,37 @@ describe('ReactHooks', () => {
1280 });
1281
1282 // Verify warnings don't get permanently disabled.
1221 - await expect(async () => {
1222 - await act(() => {
1223 - ReactTestRenderer.create(
1224 - <Boundary>
1225 - <App />
1226 - </Boundary>,
1227 - {unstable_isConcurrent: true},
1228 - );
1229 - });
1230 - }).toErrorDev([
1231 - 'Context can only be read while React is rendering',
1232 - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1233 -
1234 - 'Context can only be read while React is rendering',
1235 - 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',
1283 + await act(() => {
1284 + ReactTestRenderer.create(
1285 + <Boundary>
1286 + <App />
1287 + </Boundary>,
1288 + {unstable_isConcurrent: true},
1289 + );
1290 + });
1291 + assertConsoleErrorDev([
1292 + 'Context can only be read while React is rendering. ' +
1293 + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1294 + 'In function components, you can read it directly in the function body, ' +
1295 + 'but not inside Hooks like useReducer() or useMemo().\n' +
1296 + ' in App (at **)' +
1297 + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'),
1298 + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1299 + 'You can only call Hooks at the top level of your React function. ' +
1300 + 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1301 + ' in App (at **)' +
1302 + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'),
1303 + 'Context can only be read while React is rendering. ' +
1304 + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1305 + 'In function components, you can read it directly in the function body, ' +
1306 + 'but not inside Hooks like useReducer() or useMemo().\n' +
1307 + ' in App (at **)' +
1308 + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'),
1309 + 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1310 + 'You can only call Hooks at the top level of your React function. ' +
1311 + 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1312 + ' in App (at **)' +
1313 + (gate('enableOwnerStacks') ? '' : '\n in Boundary (at **)'),
1314 ]);
1315 });
1316
@@ -1569,24 +1647,26 @@ describe('ReactHooks', () => {
1647 unstable_isConcurrent: true,
1648 });
1649 });
1572 - await expect(async () => {
1573 - try {
1574 - await act(() => {
1575 - root.update(<App update={true} />);
1576 - });
1577 - } catch (error) {
1578 - // Swapping certain types of hooks will cause runtime errors.
1579 - // This is okay as far as this test is concerned.
1580 - // We just want to verify that warnings are always logged.
1581 - }
1582 - }).toErrorDev([
1650 + try {
1651 + await act(() => {
1652 + root.update(<App update={true} />);
1653 + });
1654 + } catch (error) {
1655 + // Swapping certain types of hooks will cause runtime errors.
1656 + // This is okay as far as this test is concerned.
1657 + // We just want to verify that warnings are always logged.
1658 + }
1659 + assertConsoleErrorDev([
1660 'React has detected a change in the order of Hooks called by App. ' +
1661 'This will lead to bugs and errors if not fixed. For more information, ' +
1585 - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' +
1662 + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
1663 + '\n' +
1664 ' Previous render Next render\n' +
1665 ' ------------------------------------------------------\n' +
1666 `1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameB)}\n` +
1589 - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
1667 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
1668 + '\n' +
1669 + ' in App (at **)',
1670 ]);
1671
1672 // further warnings for this component are silenced
@@ -1618,25 +1698,27 @@ describe('ReactHooks', () => {
1698 });
1699 });
1700
1621 - await expect(async () => {
1622 - try {
1623 - await act(() => {
1624 - root.update(<App update={true} />);
1625 - });
1626 - } catch (error) {
1627 - // Swapping certain types of hooks will cause runtime errors.
1628 - // This is okay as far as this test is concerned.
1629 - // We just want to verify that warnings are always logged.
1630 - }
1631 - }).toErrorDev([
1701 + try {
1702 + await act(() => {
1703 + root.update(<App update={true} />);
1704 + });
1705 + } catch (error) {
1706 + // Swapping certain types of hooks will cause runtime errors.
1707 + // This is okay as far as this test is concerned.
1708 + // We just want to verify that warnings are always logged.
1709 + }
1710 + assertConsoleErrorDev([
1711 'React has detected a change in the order of Hooks called by App. ' +
1712 'This will lead to bugs and errors if not fixed. For more information, ' +
1634 - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' +
1713 + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
1714 + '\n' +
1715 ' Previous render Next render\n' +
1716 ' ------------------------------------------------------\n' +
1717 `1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameA)}\n` +
1718 `2. undefined use${hookNameB}\n` +
1639 - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
1719 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
1720 + '\n' +
1721 + ' in App (at **)',
1722 ]);
1723 });
1724 });
@@ -1701,24 +1783,26 @@ describe('ReactHooks', () => {
1783 unstable_isConcurrent: true,
1784 });
1785 });
1704 - await expect(async () => {
1705 - await act(() => {
1706 - root.update(<App update={true} />);
1707 - }).catch(e => {});
1708 - // Swapping certain types of hooks will cause runtime errors.
1709 - // This is okay as far as this test is concerned.
1710 - // We just want to verify that warnings are always logged.
1711 - }).toErrorDev([
1786 + await act(() => {
1787 + root.update(<App update={true} />);
1788 + }).catch(e => {});
1789 + // Swapping certain types of hooks will cause runtime errors.
1790 + // This is okay as far as this test is concerned.
1791 + // We just want to verify that warnings are always logged.
1792 + assertConsoleErrorDev([
1793 'React has detected a change in the order of Hooks called by App. ' +
1794 'This will lead to bugs and errors if not fixed. For more information, ' +
1714 - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' +
1795 + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
1796 + '\n' +
1797 ' Previous render Next render\n' +
1798 ' ------------------------------------------------------\n' +
1799 `1. ${formatHookNamesToMatchErrorMessage(
1800 'ImperativeHandle',
1801 'Memo',
1802 )}\n` +
1721 - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
1803 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
1804 + '\n' +
1805 + ' in App (at **)',
1806 ]);
1807
1808 // further warnings for this component are silenced
@@ -1751,19 +1835,21 @@ describe('ReactHooks', () => {
1835 });
1836 });
1837 await expect(async () => {
1754 - await expect(async () => {
1755 - await act(() => {
1756 - root.update(<App update={true} />);
1757 - });
1758 - }).rejects.toThrow('custom error');
1759 - }).toErrorDev([
1838 + await act(() => {
1839 + root.update(<App update={true} />);
1840 + });
1841 + }).rejects.toThrow('custom error');
1842 + assertConsoleErrorDev([
1843 'React has detected a change in the order of Hooks called by App. ' +
1844 'This will lead to bugs and errors if not fixed. For more information, ' +
1762 - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' +
1845 + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
1846 + '\n' +
1847 ' Previous render Next render\n' +
1848 ' ------------------------------------------------------\n' +
1849 '1. useReducer useState\n' +
1766 - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
1850 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
1851 + '\n' +
1852 + ' in App (at **)',
1853 ]);
1854 });
1855 });
@@ -1798,8 +1884,10 @@ describe('ReactHooks', () => {
1884 });
1885 }).rejects.toThrow('Hello');
1886 assertConsoleErrorDev([
1801 - 'Cannot update a component (`A`) while rendering ' +
1802 - 'a different component (`B`).',
1887 + 'Cannot update a component (`A`) while rendering a different component (`B`). ' +
1888 + 'To locate the bad setState() call inside `B`, ' +
1889 + 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
1890 + ' in B (at **)',
1891 ]);
1892 });
1893
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+195 -143
@@ -42,6 +42,7 @@ let waitForThrow;
42 let waitForPaint;
43 let assertLog;
44 let useResourceEffect;
45 +let assertConsoleErrorDev;
46
47 describe('ReactHooksWithNoopRenderer', () => {
48 beforeEach(() => {
@@ -52,6 +53,8 @@ describe('ReactHooksWithNoopRenderer', () => {
53 ReactNoop = require('react-noop-renderer');
54 Scheduler = require('scheduler');
55 act = require('internal-test-utils').act;
56 + assertConsoleErrorDev =
57 + require('internal-test-utils').assertConsoleErrorDev;
58 useState = React.useState;
59 useReducer = React.useReducer;
60 useEffect = React.useEffect;
@@ -232,17 +235,18 @@ describe('ReactHooksWithNoopRenderer', () => {
235 });
236
237 it('throws when called outside the render phase', async () => {
235 - expect(() => {
236 - expect(() => useState(0)).toThrow(
237 - "Cannot read property 'useState' of null",
238 - );
239 - }).toErrorDev(
240 - 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
241 - ' one of the following reasons:\n' +
242 - '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
243 - '2. You might be breaking the Rules of Hooks\n' +
244 - '3. You might have more than one copy of React in the same app\n' +
245 - 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
238 + expect(() => useState(0)).toThrow(
239 + "Cannot read property 'useState' of null",
240 + );
241 + assertConsoleErrorDev(
242 + [
243 + 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
244 + ' one of the following reasons:\n' +
245 + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
246 + '2. You might be breaking the Rules of Hooks\n' +
247 + '3. You might have more than one copy of React in the same app\n' +
248 + 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
249 + ],
250 {withoutStack: true},
251 );
252 });
@@ -459,11 +463,12 @@ describe('ReactHooksWithNoopRenderer', () => {
463 <Bar triggerUpdate={true} />
464 </>,
465 );
462 - await expect(
463 - async () => await waitForAll(['Foo [0]', 'Bar', 'Foo [1]']),
464 - ).toErrorDev([
465 - 'Cannot update a component (`Foo`) while rendering a ' +
466 - 'different component (`Bar`). To locate the bad setState() call inside `Bar`',
466 + await waitForAll(['Foo [0]', 'Bar', 'Foo [1]']);
467 + assertConsoleErrorDev([
468 + 'Cannot update a component (`Foo`) while rendering a different component (`Bar`). ' +
469 + 'To locate the bad setState() call inside `Bar`, ' +
470 + 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
471 + ' in Bar (at **)',
472 ]);
473
474 // It should not warn again (deduplication).
@@ -1645,6 +1650,12 @@ describe('ReactHooksWithNoopRenderer', () => {
1650 updateCount(props.count);
1651 });
1652 assertLog([`Schedule update [${props.count}]`]);
1653 + assertConsoleErrorDev([
1654 + 'flushSync was called from inside a lifecycle method. ' +
1655 + 'React cannot flush when React is already rendering. ' +
1656 + 'Consider moving this call to a scheduler task or micro task.\n' +
1657 + ' in Counter (at **)',
1658 + ]);
1659 // This shouldn't flush synchronously.
1660 expect(ReactNoop).not.toMatchRenderedOutput(
1661 <span prop={`Count: ${props.count}`} />,
@@ -1652,17 +1663,14 @@ describe('ReactHooksWithNoopRenderer', () => {
1663 }, [props.count]);
1664 return <Text text={'Count: ' + count} />;
1665 }
1655 - await expect(async () => {
1656 - await act(async () => {
1657 - ReactNoop.render(<Counter count={0} />, () =>
1658 - Scheduler.log('Sync effect'),
1659 - );
1660 - await waitFor(['Count: (empty)', 'Sync effect']);
1661 - expect(ReactNoop).toMatchRenderedOutput(
1662 - <span prop="Count: (empty)" />,
1663 - );
1664 - });
1665 - }).toErrorDev('flushSync was called from inside a lifecycle method');
1666 + await act(async () => {
1667 + ReactNoop.render(<Counter count={0} />, () =>
1668 + Scheduler.log('Sync effect'),
1669 + );
1670 + await waitFor(['Count: (empty)', 'Sync effect']);
1671 + expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />);
1672 + });
1673 +
1674 assertLog([`Count: 0`]);
1675 expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
1676 });
@@ -2506,35 +2514,47 @@ describe('ReactHooksWithNoopRenderer', () => {
2514 }
2515
2516 const root1 = ReactNoop.createRoot();
2509 - await expect(async () => {
2510 - await act(() => {
2511 - root1.render(<App return={17} />);
2512 - });
2513 - }).toErrorDev([
2514 - 'useEffect must not return anything besides a ' +
2515 - 'function, which is used for clean-up. You returned: 17',
2517 + await act(() => {
2518 + root1.render(<App return={17} />);
2519 + });
2520 + assertConsoleErrorDev([
2521 + 'useEffect must not return anything besides a function, ' +
2522 + 'which is used for clean-up. You returned: 17\n' +
2523 + ' in App (at **)',
2524 ]);
2525
2526 const root2 = ReactNoop.createRoot();
2519 - await expect(async () => {
2520 - await act(() => {
2521 - root2.render(<App return={null} />);
2522 - });
2523 - }).toErrorDev([
2524 - 'useEffect must not return anything besides a ' +
2525 - 'function, which is used for clean-up. You returned null. If your ' +
2526 - 'effect does not require clean up, return undefined (or nothing).',
2527 + await act(() => {
2528 + root2.render(<App return={null} />);
2529 + });
2530 + assertConsoleErrorDev([
2531 + 'useEffect must not return anything besides a function, ' +
2532 + 'which is used for clean-up. You returned null. ' +
2533 + 'If your effect does not require clean up, return undefined (or nothing).\n' +
2534 + ' in App (at **)',
2535 ]);
2536
2537 const root3 = ReactNoop.createRoot();
2530 - await expect(async () => {
2531 - await act(() => {
2532 - root3.render(<App return={Promise.resolve()} />);
2533 - });
2534 - }).toErrorDev([
2535 - 'useEffect must not return anything besides a ' +
2536 - 'function, which is used for clean-up.\n\n' +
2537 - 'It looks like you wrote useEffect(async () => ...) or returned a Promise.',
2538 + await act(() => {
2539 + root3.render(<App return={Promise.resolve()} />);
2540 + });
2541 + assertConsoleErrorDev([
2542 + 'useEffect must not return anything besides a function, which is used for clean-up.\n' +
2543 + '\n' +
2544 + 'It looks like you wrote useEffect(async () => ...) or returned a Promise. ' +
2545 + 'Instead, write the async function inside your effect and call it immediately:\n' +
2546 + '\n' +
2547 + 'useEffect(() => {\n' +
2548 + ' async function fetchData() {\n' +
2549 + ' // You can await here\n' +
2550 + ' const response = await MyAPI.getData(someId);\n' +
2551 + ' // ...\n' +
2552 + ' }\n' +
2553 + ' fetchData();\n' +
2554 + "}, [someId]); // Or [] if effect doesn't need props or state\n" +
2555 + '\n' +
2556 + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching\n' +
2557 + ' in App (at **)',
2558 ]);
2559
2560 // Error on unmount because React assumes the value is a function
@@ -2895,35 +2915,48 @@ describe('ReactHooksWithNoopRenderer', () => {
2915 }
2916
2917 const root1 = ReactNoop.createRoot();
2898 - await expect(async () => {
2899 - await act(() => {
2900 - root1.render(<App return={17} />);
2901 - });
2902 - }).toErrorDev([
2903 - 'useInsertionEffect must not return anything besides a ' +
2904 - 'function, which is used for clean-up. You returned: 17',
2918 + await act(() => {
2919 + root1.render(<App return={17} />);
2920 + });
2921 + assertConsoleErrorDev([
2922 + 'useInsertionEffect must not return anything besides a function, ' +
2923 + 'which is used for clean-up. You returned: 17\n' +
2924 + ' in App (at **)',
2925 ]);
2926
2927 const root2 = ReactNoop.createRoot();
2908 - await expect(async () => {
2909 - await act(() => {
2910 - root2.render(<App return={null} />);
2911 - });
2912 - }).toErrorDev([
2913 - 'useInsertionEffect must not return anything besides a ' +
2914 - 'function, which is used for clean-up. You returned null. If your ' +
2915 - 'effect does not require clean up, return undefined (or nothing).',
2928 + await act(() => {
2929 + root2.render(<App return={null} />);
2930 + });
2931 + assertConsoleErrorDev([
2932 + 'useInsertionEffect must not return anything besides a function, ' +
2933 + 'which is used for clean-up. You returned null. ' +
2934 + 'If your effect does not require clean up, return undefined (or nothing).\n' +
2935 + ' in App (at **)',
2936 ]);
2937
2938 const root3 = ReactNoop.createRoot();
2919 - await expect(async () => {
2920 - await act(() => {
2921 - root3.render(<App return={Promise.resolve()} />);
2922 - });
2923 - }).toErrorDev([
2939 + await act(() => {
2940 + root3.render(<App return={Promise.resolve()} />);
2941 + });
2942 + assertConsoleErrorDev([
2943 'useInsertionEffect must not return anything besides a ' +
2925 - 'function, which is used for clean-up.\n\n' +
2926 - 'It looks like you wrote useInsertionEffect(async () => ...) or returned a Promise.',
2944 + 'function, which is used for clean-up.\n' +
2945 + '\n' +
2946 + 'It looks like you wrote useInsertionEffect(async () => ...) or returned a Promise. ' +
2947 + 'Instead, write the async function inside your effect and call it immediately:\n' +
2948 + '\n' +
2949 + 'useInsertionEffect(() => {\n' +
2950 + ' async function fetchData() {\n' +
2951 + ' // You can await here\n' +
2952 + ' const response = await MyAPI.getData(someId);\n' +
2953 + ' // ...\n' +
2954 + ' }\n' +
2955 + ' fetchData();\n' +
2956 + "}, [someId]); // Or [] if effect doesn't need props or state\n" +
2957 + '\n' +
2958 + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching\n' +
2959 + ' in App (at **)',
2960 ]);
2961
2962 // Error on unmount because React assumes the value is a function
@@ -2946,11 +2979,13 @@ describe('ReactHooksWithNoopRenderer', () => {
2979 }
2980
2981 const root = ReactNoop.createRoot();
2949 - await expect(async () => {
2950 - await act(() => {
2951 - root.render(<App />);
2952 - });
2953 - }).toErrorDev(['useInsertionEffect must not schedule updates.']);
2982 + await act(() => {
2983 + root.render(<App />);
2984 + });
2985 + assertConsoleErrorDev([
2986 + 'useInsertionEffect must not schedule updates.\n' +
2987 + ' in App (at **)',
2988 + ]);
2989
2990 await act(async () => {
2991 root.render(<App throw={true} />);
@@ -2988,11 +3023,13 @@ describe('ReactHooksWithNoopRenderer', () => {
3023 await act(() => {
3024 root.render(<App foo="hello" />);
3025 });
2991 - await expect(async () => {
2992 - await act(() => {
2993 - root.render(<App foo="goodbye" />);
2994 - });
2995 - }).toErrorDev(['useInsertionEffect must not schedule updates.']);
3026 + await act(() => {
3027 + root.render(<App foo="goodbye" />);
3028 + });
3029 + assertConsoleErrorDev([
3030 + 'useInsertionEffect must not schedule updates.\n' +
3031 + ' in App (at **)',
3032 + ]);
3033
3034 await act(async () => {
3035 root.render(<App throw={true} />);
@@ -3036,19 +3073,17 @@ describe('ReactHooksWithNoopRenderer', () => {
3073 );
3074 });
3075
3039 - if (gate(flags => flags.enableHiddenSubtreeInsertionEffectCleanup)) {
3040 - await expect(async () => {
3041 - await act(() => {
3042 - root.render(<Activity mode="hidden" />);
3043 - });
3044 - }).toErrorDev(['useInsertionEffect must not schedule updates.']);
3045 - } else {
3046 - await expect(async () => {
3047 - await act(() => {
3048 - root.render(<Activity mode="hidden" />);
3049 - });
3050 - }).toErrorDev([]);
3051 - }
3076 + await act(() => {
3077 + root.render(<Activity mode="hidden" />);
3078 + });
3079 + assertConsoleErrorDev(
3080 + gate('enableHiddenSubtreeInsertionEffectCleanup')
3081 + ? [
3082 + 'useInsertionEffect must not schedule updates.\n' +
3083 + ' in App (at **)',
3084 + ]
3085 + : [],
3086 + );
3087
3088 // Should not warn for regular effects after throw.
3089 function NotInsertion() {
@@ -3225,35 +3260,47 @@ describe('ReactHooksWithNoopRenderer', () => {
3260 }
3261
3262 const root1 = ReactNoop.createRoot();
3228 - await expect(async () => {
3229 - await act(() => {
3230 - root1.render(<App return={17} />);
3231 - });
3232 - }).toErrorDev([
3263 + await act(() => {
3264 + root1.render(<App return={17} />);
3265 + });
3266 + assertConsoleErrorDev([
3267 'useLayoutEffect must not return anything besides a ' +
3234 - 'function, which is used for clean-up. You returned: 17',
3268 + 'function, which is used for clean-up. You returned: 17\n' +
3269 + ' in App (at **)',
3270 ]);
3271
3272 const root2 = ReactNoop.createRoot();
3238 - await expect(async () => {
3239 - await act(() => {
3240 - root2.render(<App return={null} />);
3241 - });
3242 - }).toErrorDev([
3273 + await act(() => {
3274 + root2.render(<App return={null} />);
3275 + });
3276 + assertConsoleErrorDev([
3277 'useLayoutEffect must not return anything besides a ' +
3278 'function, which is used for clean-up. You returned null. If your ' +
3245 - 'effect does not require clean up, return undefined (or nothing).',
3279 + 'effect does not require clean up, return undefined (or nothing).\n' +
3280 + ' in App (at **)',
3281 ]);
3282
3283 const root3 = ReactNoop.createRoot();
3249 - await expect(async () => {
3250 - await act(() => {
3251 - root3.render(<App return={Promise.resolve()} />);
3252 - });
3253 - }).toErrorDev([
3284 + await act(() => {
3285 + root3.render(<App return={Promise.resolve()} />);
3286 + });
3287 + assertConsoleErrorDev([
3288 'useLayoutEffect must not return anything besides a ' +
3289 'function, which is used for clean-up.\n\n' +
3256 - 'It looks like you wrote useLayoutEffect(async () => ...) or returned a Promise.',
3290 + 'It looks like you wrote useLayoutEffect(async () => ...) or returned a Promise. ' +
3291 + 'Instead, write the async function inside your effect and call it immediately:\n' +
3292 + '\n' +
3293 + 'useLayoutEffect(() => {\n' +
3294 + ' async function fetchData() {\n' +
3295 + ' // You can await here\n' +
3296 + ' const response = await MyAPI.getData(someId);\n' +
3297 + ' // ...\n' +
3298 + ' }\n' +
3299 + ' fetchData();\n' +
3300 + "}, [someId]); // Or [] if effect doesn't need props or state\n" +
3301 + '\n' +
3302 + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching\n' +
3303 + ' in App (at **)',
3304 ]);
3305
3306 // Error on unmount because React assumes the value is a function
@@ -3300,13 +3347,14 @@ describe('ReactHooksWithNoopRenderer', () => {
3347 return null;
3348 }
3349
3303 - await expect(async () => {
3304 - await act(() => {
3305 - ReactNoop.render(<App id={1} />);
3306 - });
3307 - }).toErrorDev(
3308 - 'useResourceEffect must provide a callback which returns a resource. ' +
3309 - 'If a managed resource is not needed here, use useEffect. Received undefined',
3350 + await act(() => {
3351 + ReactNoop.render(<App id={1} />);
3352 + });
3353 + assertConsoleErrorDev(
3354 + [
3355 + 'useResourceEffect must provide a callback which returns a resource. ' +
3356 + 'If a managed resource is not needed here, use useEffect. Received undefined',
3357 + ],
3358 {withoutStack: true},
3359 );
3360 });
@@ -3328,14 +3376,14 @@ describe('ReactHooksWithNoopRenderer', () => {
3376 return null;
3377 }
3378
3331 - await expect(async () => {
3332 - await act(() => {
3333 - ReactNoop.render(<App id={1} />);
3334 - });
3335 - }).toErrorDev(
3379 + await act(() => {
3380 + ReactNoop.render(<App id={1} />);
3381 + });
3382 + assertConsoleErrorDev([
3383 'useResourceEffect received a dependency array with no dependencies. ' +
3337 - 'When specified, the dependency array must have at least one dependency.',
3338 - );
3384 + 'When specified, the dependency array must have at least one dependency.\n' +
3385 + ' in App (at **)',
3386 + ]);
3387 });
3388
3389 // @gate enableUseResourceEffectHook
@@ -4472,21 +4520,23 @@ describe('ReactHooksWithNoopRenderer', () => {
4520 );
4521
4522 ReactNoop.render(<App loadC={true} />);
4475 - await expect(async () => {
4476 - await waitForThrow(
4477 - 'Rendered more hooks than during the previous render.',
4478 - );
4479 - assertLog([]);
4480 - }).toErrorDev([
4523 + await waitForThrow(
4524 + 'Rendered more hooks than during the previous render.',
4525 + );
4526 + assertLog([]);
4527 + assertConsoleErrorDev([
4528 'React has detected a change in the order of Hooks called by App. ' +
4529 'This will lead to bugs and errors if not fixed. For more information, ' +
4483 - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' +
4530 + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
4531 + '\n' +
4532 ' Previous render Next render\n' +
4533 ' ------------------------------------------------------\n' +
4534 '1. useState useState\n' +
4535 '2. useState useState\n' +
4536 '3. undefined useState\n' +
4489 - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
4537 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
4538 + '\n' +
4539 + ' in App (at **)',
4540 ]);
4541
4542 // Uncomment if/when we support this again
@@ -4569,20 +4619,22 @@ describe('ReactHooksWithNoopRenderer', () => {
4619
4620 await act(async () => {
4621 ReactNoop.render(<App showMore={true} />);
4572 - await expect(async () => {
4573 - await waitForThrow(
4574 - 'Rendered more hooks than during the previous render.',
4575 - );
4576 - assertLog(['Unmount A']);
4577 - }).toErrorDev([
4622 + await waitForThrow(
4623 + 'Rendered more hooks than during the previous render.',
4624 + );
4625 + assertLog(['Unmount A']);
4626 + assertConsoleErrorDev([
4627 'React has detected a change in the order of Hooks called by App. ' +
4628 'This will lead to bugs and errors if not fixed. For more information, ' +
4580 - 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' +
4629 + 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
4630 + '\n' +
4631 ' Previous render Next render\n' +
4632 ' ------------------------------------------------------\n' +
4633 '1. useEffect useEffect\n' +
4634 '2. undefined useEffect\n' +
4585 - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
4635 + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
4636 + '\n' +
4637 + ' in App (at **)',
4638 ]);
4639 });
4640
packages/react-reconciler/src/__tests__/ReactIncremental-test.js
+175 -38
@@ -1798,9 +1798,19 @@ describe('ReactIncremental', () => {
1798 'ShowBoth {"locale":"fr"}',
1799 ]);
1800 assertConsoleErrorDev([
1801 - 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
1802 - 'ShowLocale uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
1803 - 'ShowBoth uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.',
1801 + 'Intl uses the legacy childContextTypes API which will soon be removed. ' +
1802 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1803 + ' in Intl (at **)',
1804 + 'ShowLocale uses the legacy contextTypes API which will soon be removed. ' +
1805 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
1806 + ' in ShowLocale (at **)' +
1807 + (gate('enableOwnerStacks') ? '' : '\n in Intl (at **)'),
1808 + 'ShowBoth uses the legacy contextTypes API which will be removed soon. ' +
1809 + 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
1810 + ' in ShowBoth (at **)' +
1811 + (gate('enableOwnerStacks')
1812 + ? ''
1813 + : '\n in div (at **)' + '\n in Intl (at **)'),
1814 ]);
1815
1816 ReactNoop.render(
@@ -1853,8 +1863,18 @@ describe('ReactIncremental', () => {
1863 'ShowBoth {"locale":"en"}',
1864 ]);
1865 assertConsoleErrorDev([
1856 - 'Router uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
1857 - 'ShowRoute uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
1866 + 'Router uses the legacy childContextTypes API which will soon be removed. ' +
1867 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1868 + ' in Router (at **)' +
1869 + (gate('enableOwnerStacks') ? '' : '\n in Intl (at **)'),
1870 + 'ShowRoute uses the legacy contextTypes API which will soon be removed. ' +
1871 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
1872 + (gate('enableOwnerStacks')
1873 + ? ' in Indirection (at **)'
1874 + : ' in ShowRoute (at **)\n' +
1875 + ' in Indirection (at **)\n' +
1876 + ' in Router (at **)\n' +
1877 + ' in Intl (at **)'),
1878 ]);
1879 });
1880
@@ -1890,8 +1910,12 @@ describe('ReactIncremental', () => {
1910 'Recurse {"n":0}',
1911 ]);
1912 assertConsoleErrorDev([
1893 - 'Recurse uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
1894 - 'Recurse uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
1913 + 'Recurse uses the legacy childContextTypes API which will soon be removed. ' +
1914 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1915 + ' in Recurse (at **)',
1916 + 'Recurse uses the legacy contextTypes API which will soon be removed. ' +
1917 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
1918 + ' in Recurse (at **)',
1919 ]);
1920 });
1921
@@ -1943,8 +1967,13 @@ describe('ReactIncremental', () => {
1967 'ShowLocale {"locale":"fr"}',
1968 ]);
1969 assertConsoleErrorDev([
1946 - 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
1947 - 'ShowLocale uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
1970 + 'Intl uses the legacy childContextTypes API which will soon be removed. ' +
1971 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1972 + ' in Intl (at **)',
1973 + 'ShowLocale uses the legacy contextTypes API which will soon be removed. ' +
1974 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
1975 + ' in ShowLocale (at **)' +
1976 + (gate('enableOwnerStacks') ? '' : '\n in Intl (at **)'),
1977 ]);
1978
1979 await waitForAll([
@@ -2034,9 +2063,27 @@ describe('ReactIncremental', () => {
2063 'ShowLocaleFn:read {"locale":"fr"}',
2064 ]);
2065 assertConsoleErrorDev([
2037 - 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
2038 - 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
2039 - 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.',
2066 + 'Intl uses the legacy childContextTypes API which will soon be removed. ' +
2067 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2068 + ' in Intl (at **)',
2069 + 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. ' +
2070 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2071 + ' in ShowLocaleClass (at **)' +
2072 + (gate('enableOwnerStacks')
2073 + ? ''
2074 + : '\n in Stateful (at **)' +
2075 + '\n in IndirectionClass (at **)' +
2076 + '\n in IndirectionFn (at **)' +
2077 + ' in Intl (at **)'),
2078 + 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. ' +
2079 + 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
2080 + ' in ShowLocaleFn (at **)' +
2081 + (gate('enableOwnerStacks')
2082 + ? ''
2083 + : '\n in Stateful (at **)' +
2084 + '\n in IndirectionClass (at **)' +
2085 + '\n in IndirectionFn (at **)' +
2086 + ' in Intl (at **)'),
2087 ]);
2088
2089 statefulInst.setState({x: 1});
@@ -2125,9 +2172,28 @@ describe('ReactIncremental', () => {
2172 ]);
2173
2174 assertConsoleErrorDev([
2128 - 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
2129 - 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
2130 - 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.',
2175 + 'Intl uses the legacy childContextTypes API which will soon be removed. ' +
2176 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2177 + (gate('enableOwnerStacks') ? '' : ' in Intl (at **)\n') +
2178 + ' in Stateful (at **)',
2179 + 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. ' +
2180 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2181 + ' in ShowLocaleClass (at **)' +
2182 + (gate('enableOwnerStacks')
2183 + ? ''
2184 + : '\n in IndirectionClass (at **)' +
2185 + '\n in IndirectionFn (at **)' +
2186 + '\n in Intl (at **)' +
2187 + '\n in Stateful (at **)'),
2188 + 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. ' +
2189 + 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
2190 + ' in ShowLocaleFn (at **)' +
2191 + (gate('enableOwnerStacks')
2192 + ? ''
2193 + : '\n in IndirectionClass (at **)' +
2194 + '\n in IndirectionFn (at **)' +
2195 + '\n in Intl (at **)' +
2196 + '\n in Stateful (at **)'),
2197 ]);
2198
2199 statefulInst.setState({locale: 'gr'});
@@ -2187,7 +2253,11 @@ describe('ReactIncremental', () => {
2253 await waitForAll([]);
2254
2255 assertConsoleErrorDev([
2190 - 'Child uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
2256 + 'Child uses the legacy childContextTypes API which will soon be removed. ' +
2257 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2258 + (gate('enableOwnerStacks') ? '' : ' in Child (at **)\n') +
2259 + ' in Middle (at **)\n' +
2260 + ' in Root (at **)',
2261 ]);
2262
2263 // Trigger an update in the middle of the tree
@@ -2235,8 +2305,12 @@ describe('ReactIncremental', () => {
2305
2306 // Init
2307 ReactNoop.render(<Root />);
2238 - await expect(async () => await waitForAll([])).toErrorDev([
2239 - 'ContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
2308 + await waitForAll([]);
2309 + assertConsoleErrorDev([
2310 + 'ContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2311 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2312 + (gate('enableOwnerStacks') ? '' : ' in ContextProvider (at **)\n') +
2313 + ' in Root (at **)',
2314 ]);
2315
2316 // Trigger an update in the middle of the tree
@@ -2244,9 +2318,12 @@ describe('ReactIncremental', () => {
2318 instance.setState({
2319 throwError: true,
2320 });
2247 - await expect(async () => await waitForAll([])).toErrorDev(
2248 - 'Error boundaries should implement getDerivedStateFromError()',
2249 - );
2321 + await waitForAll([]);
2322 + assertConsoleErrorDev([
2323 + 'Root: Error boundaries should implement getDerivedStateFromError(). ' +
2324 + 'In that method, return a state update to display an error message or fallback UI.\n' +
2325 + ' in Root (at **)',
2326 + ]);
2327 });
2328
2329 // @gate !disableLegacyContext || !__DEV__
@@ -2292,7 +2369,9 @@ describe('ReactIncremental', () => {
2369 ]);
2370
2371 assertConsoleErrorDev([
2295 - 'MyComponent uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
2372 + 'MyComponent uses the legacy contextTypes API which will soon be removed. ' +
2373 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2374 + ' in MyComponent (at **)',
2375 ]);
2376 });
2377
@@ -2427,8 +2506,15 @@ describe('ReactIncremental', () => {
2506
2507 await waitForAll(['count:0']);
2508 assertConsoleErrorDev([
2430 - 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
2431 - 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
2509 + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2510 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2511 + ' in TopContextProvider (at **)',
2512 + 'Child uses the legacy contextTypes API which will soon be removed. ' +
2513 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2514 + ' in Child (at **)' +
2515 + (gate('enableOwnerStacks')
2516 + ? ''
2517 + : '\n in Middle (at **)' + '\n in TopContextProvider (at **)'),
2518 ]);
2519 instance.updateCount();
2520 await waitForAll(['count:1']);
@@ -2487,9 +2573,22 @@ describe('ReactIncremental', () => {
2573
2574 await waitForAll(['count:0']);
2575 assertConsoleErrorDev([
2490 - 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
2491 - 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
2492 - 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
2576 + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2577 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2578 + ' in TopContextProvider (at **)',
2579 + 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2580 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2581 + ' in MiddleContextProvider (at **)' +
2582 + (gate('enableOwnerStacks')
2583 + ? ''
2584 + : '\n in TopContextProvider (at **)'),
2585 + 'Child uses the legacy contextTypes API which will soon be removed. ' +
2586 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2587 + ' in Child (at **)' +
2588 + (gate('enableOwnerStacks')
2589 + ? ''
2590 + : '\n in MiddleContextProvider (at **)' +
2591 + '\n in TopContextProvider (at **)'),
2592 ]);
2593 instance.updateCount();
2594 await waitForAll(['count:1']);
@@ -2557,9 +2656,24 @@ describe('ReactIncremental', () => {
2656
2657 await waitForAll(['count:0']);
2658 assertConsoleErrorDev([
2560 - 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
2561 - 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
2562 - 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
2659 + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2660 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2661 + ' in TopContextProvider (at **)',
2662 + 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2663 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2664 + ' in MiddleContextProvider (at **)' +
2665 + (gate('enableOwnerStacks')
2666 + ? ''
2667 + : '\n in MiddleScu (at **)' +
2668 + '\n in TopContextProvider (at **)'),
2669 + 'Child uses the legacy contextTypes API which will soon be removed. ' +
2670 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2671 + ' in Child (at **)' +
2672 + (gate('enableOwnerStacks')
2673 + ? ''
2674 + : '\n in MiddleContextProvider (at **)' +
2675 + '\n in MiddleScu (at **)' +
2676 + '\n in TopContextProvider (at **)'),
2677 ]);
2678 instance.updateCount();
2679 await waitForAll([]);
@@ -2637,9 +2751,24 @@ describe('ReactIncremental', () => {
2751
2752 await waitForAll(['count:0, name:brian']);
2753 assertConsoleErrorDev([
2640 - 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
2641 - 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
2642 - 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
2754 + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2755 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2756 + ' in TopContextProvider (at **)',
2757 + 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2758 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2759 + ' in MiddleContextProvider (at **)' +
2760 + (gate('enableOwnerStacks')
2761 + ? ''
2762 + : '\n in MiddleScu (at **)' +
2763 + '\n in TopContextProvider (at **)'),
2764 + 'Child uses the legacy contextTypes API which will soon be removed. ' +
2765 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2766 + ' in Child (at **)' +
2767 + (gate('enableOwnerStacks')
2768 + ? ''
2769 + : '\n in MiddleContextProvider (at **)' +
2770 + '\n in MiddleScu (at **)' +
2771 + '\n in TopContextProvider (at **)'),
2772 ]);
2773 topInstance.updateCount();
2774 await waitForAll([]);
@@ -2743,11 +2872,19 @@ describe('ReactIncremental', () => {
2872 <Boundary />
2873 </React.StrictMode>,
2874 );
2746 - await expect(async () => {
2747 - await waitForAll([]);
2748 - }).toErrorDev([
2749 - 'Boundary uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
2750 - 'Legacy context API has been detected within a strict-mode tree',
2875 + await waitForAll([]);
2876 + assertConsoleErrorDev([
2877 + 'Boundary uses the legacy contextTypes API which will soon be removed. ' +
2878 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2879 + ' in Boundary (at **)',
2880 + 'Legacy context API has been detected within a strict-mode tree.\n' +
2881 + '\n' +
2882 + 'The old API will be supported in all 16.x releases, but applications using it should migrate to the new version.\n' +
2883 + '\n' +
2884 + 'Please update the following components: Boundary\n' +
2885 + '\n' +
2886 + 'Learn more about this warning here: https://react.dev/link/legacy-context\n' +
2887 + ' in Boundary (at **)',
2888 ]);
2889 }
2890
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+42 -11
@@ -1212,12 +1212,18 @@ describe('ReactIncrementalErrorHandling', () => {
1212 </Provider>,
1213 );
1214
1215 - await expect(async () => {
1216 - await waitForAll([]);
1217 - }).toErrorDev([
1218 - 'Provider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
1219 - 'Provider uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
1220 - 'Connector uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.',
1215 + await waitForAll([]);
1216 + assertConsoleErrorDev([
1217 + 'Provider uses the legacy childContextTypes API which will soon be removed. ' +
1218 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1219 + ' in Provider (at **)',
1220 + 'Provider uses the legacy contextTypes API which will soon be removed. ' +
1221 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
1222 + ' in Provider (at **)',
1223 + 'Connector uses the legacy contextTypes API which will be removed soon. ' +
1224 + 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
1225 + ' in Connector (at **)' +
1226 + (gate('enableOwnerStacks') ? '' : '\n in Provider (at **)'),
1227 ]);
1228
1229 // If the context stack does not unwind, span will get 'abcde'
@@ -1250,9 +1256,19 @@ describe('ReactIncrementalErrorHandling', () => {
1256 await waitForAll([]);
1257 if (gate(flags => !flags.enableOwnerStacks)) {
1258 assertConsoleErrorDev([
1253 - 'React.jsx: type is invalid -- expected a string',
1259 + 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
1260 + 'or a class/function (for composite components) but got: undefined. ' +
1261 + "You likely forgot to export your component from the file it's defined in, " +
1262 + 'or you might have mixed up default and named imports.\n' +
1263 + ' in BrokenRender (at **)\n' +
1264 + ' in ErrorBoundary (at **)',
1265 // React retries once on error
1255 - 'React.jsx: type is invalid -- expected a string',
1266 + 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
1267 + 'or a class/function (for composite components) but got: undefined. ' +
1268 + "You likely forgot to export your component from the file it's defined in, " +
1269 + 'or you might have mixed up default and named imports.\n' +
1270 + ' in BrokenRender (at **)\n' +
1271 + ' in ErrorBoundary (at **)',
1272 ]);
1273 }
1274
@@ -1305,9 +1321,19 @@ describe('ReactIncrementalErrorHandling', () => {
1321 await waitForAll([]);
1322 if (gate(flags => !flags.enableOwnerStacks)) {
1323 assertConsoleErrorDev([
1308 - 'React.jsx: type is invalid -- expected a string',
1324 + 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
1325 + 'or a class/function (for composite components) but got: undefined. ' +
1326 + "You likely forgot to export your component from the file it's defined in, " +
1327 + 'or you might have mixed up default and named imports.\n' +
1328 + ' in BrokenRender (at **)\n' +
1329 + ' in ErrorBoundary (at **)',
1330 // React retries once on error
1310 - 'React.jsx: type is invalid -- expected a string',
1331 + 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
1332 + 'or a class/function (for composite components) but got: undefined. ' +
1333 + "You likely forgot to export your component from the file it's defined in, " +
1334 + 'or you might have mixed up default and named imports.\n' +
1335 + ' in BrokenRender (at **)\n' +
1336 + ' in ErrorBoundary (at **)',
1337 ]);
1338 }
1339 expect(ReactNoop).toMatchRenderedOutput(
@@ -1330,7 +1356,12 @@ describe('ReactIncrementalErrorHandling', () => {
1356 ReactNoop.render(<InvalidType />);
1357 if (gate(flags => !flags.enableOwnerStacks)) {
1358 assertConsoleErrorDev(
1333 - ['React.jsx: type is invalid -- expected a string'],
1359 + [
1360 + 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
1361 + 'or a class/function (for composite components) but got: undefined. ' +
1362 + "You likely forgot to export your component from the file it's defined in, " +
1363 + 'or you might have mixed up default and named imports.',
1364 + ],
1365 {withoutStack: true},
1366 );
1367 }
packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js
+1 -1
@@ -33,7 +33,7 @@ describe('ReactIncrementalErrorLogging', () => {
33 waitForAll = InternalTestUtils.waitForAll;
34 });
35
36 - // Note: in this test file we won't be using toErrorDev() matchers
36 + // Note: in this test file we won't be using assertConsoleDev() matchers
37 // because they filter out precisely the messages we want to test for.
38 let oldConsoleWarn;
39 let oldConsoleError;
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+14 -11
@@ -18,6 +18,7 @@ let act;
18 let waitForAll;
19 let waitFor;
20 let assertLog;
21 +let assertConsoleErrorDev;
22
23 describe('ReactIncrementalUpdates', () => {
24 beforeEach(() => {
@@ -34,6 +35,7 @@ describe('ReactIncrementalUpdates', () => {
35 waitForAll = InternalTestUtils.waitForAll;
36 waitFor = InternalTestUtils.waitFor;
37 assertLog = InternalTestUtils.assertLog;
38 + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
39 });
40
41 function Text({text}) {
@@ -366,20 +368,21 @@ describe('ReactIncrementalUpdates', () => {
368 return {a: 'a'};
369 });
370
369 - await expect(
370 - async () =>
371 - await waitForAll([
372 - 'setState updater',
373 - // Updates in the render phase receive the currently rendering
374 - // lane, so the update flushes immediately in the same render.
375 - 'render',
376 - ]),
377 - ).toErrorDev(
371 + await waitForAll([
372 + 'setState updater',
373 + // Updates in the render phase receive the currently rendering
374 + // lane, so the update flushes immediately in the same render.
375 + 'render',
376 + ]);
377 + assertConsoleErrorDev([
378 'An update (setState, replaceState, or forceUpdate) was scheduled ' +
379 'from inside an update function. Update functions should be pure, ' +
380 'with zero side-effects. Consider using componentDidUpdate or a ' +
381 - 'callback.\n\nPlease update the following component: Foo',
382 - );
381 + 'callback.\n' +
382 + '\n' +
383 + 'Please update the following component: Foo\n' +
384 + ' in Foo (at **)',
385 + ]);
386 expect(instance.state).toEqual({a: 'a', b: 'b'});
387
388 // Test deduplication (no additional warnings expected)
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+48 -34
@@ -721,12 +721,13 @@ describe('ReactLazy', () => {
721 }
722 T.defaultProps = {inner: 'Hi'};
723 const LazyText = lazy(() => fakeImport(T));
724 - expect(() => {
725 - LazyText.defaultProps = {outer: 'Bye'};
726 - }).toErrorDev(
727 - 'It is not supported to assign `defaultProps` to ' +
728 - 'a lazy component import. Either specify them where the component ' +
729 - 'is defined, or create a wrapping component around it.',
724 + LazyText.defaultProps = {outer: 'Bye'};
725 + assertConsoleErrorDev(
726 + [
727 + 'It is not supported to assign `defaultProps` to ' +
728 + 'a lazy component import. Either specify them where the component ' +
729 + 'is defined, or create a wrapping component around it.',
730 + ],
731 {withoutStack: true},
732 );
733
@@ -742,14 +743,15 @@ describe('ReactLazy', () => {
743 await waitForAll(['Loading...']);
744 expect(root).not.toMatchRenderedOutput('Hi Bye');
745
745 - await expect(async () => {
746 - await act(() => resolveFakeImport(T));
747 - assertLog(['Hi Bye']);
748 - }).toErrorDev(
746 + await act(() => resolveFakeImport(T));
747 + assertLog(['Hi Bye']);
748 + assertConsoleErrorDev([
749 'T: Support for defaultProps ' +
750 'will be removed from function components in a future major ' +
751 - 'release. Use JavaScript default parameters instead.',
752 - );
751 + 'release. Use JavaScript default parameters instead.\n' +
752 + ' in T (at **)\n' +
753 + ' in Suspense (at **)',
754 + ]);
755
756 expect(root).toMatchRenderedOutput('Hi Bye');
757
@@ -1026,11 +1028,13 @@ describe('ReactLazy', () => {
1028 expect(root).not.toMatchRenderedOutput('Inner default text');
1029
1030 // Mount
1029 - await expect(async () => {
1030 - await act(() => resolveFakeImport(T));
1031 - assertLog(['Inner default text']);
1032 - }).toErrorDev([
1033 - 'T: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.',
1031 + await act(() => resolveFakeImport(T));
1032 + assertLog(['Inner default text']);
1033 + assertConsoleErrorDev([
1034 + 'T: Support for defaultProps will be removed from function components in a future major release. ' +
1035 + 'Use JavaScript default parameters instead.\n' +
1036 + ' in T (at **)\n' +
1037 + ' in Suspense (at **)',
1038 ]);
1039 expect(root).toMatchRenderedOutput('Inner default text');
1040
@@ -1063,13 +1067,19 @@ describe('ReactLazy', () => {
1067 await waitForAll(['Started loading', 'Loading...']);
1068 expect(root).not.toMatchRenderedOutput(<div>AB</div>);
1069
1066 - await expect(async () => {
1067 - await act(() => resolveFakeImport(Foo));
1068 - assertLog(['A', 'B']);
1069 - }).toErrorDev(
1070 - (gate(flags => flags.enableOwnerStacks) ? '' : ' in Text (at **)\n') +
1071 - ' in Foo (at **)',
1072 - );
1070 + await act(() => resolveFakeImport(Foo));
1071 + assertLog(['A', 'B']);
1072 + assertConsoleErrorDev([
1073 + 'Each child in a list should have a unique "key" prop.\n' +
1074 + '\n' +
1075 + 'Check the render method of `Foo`. ' +
1076 + 'See https://react.dev/link/warning-keys for more information.\n' +
1077 + (gate(flags => flags.enableOwnerStacks)
1078 + ? ' in Foo (at **)'
1079 + : ' in Text (at **)\n' +
1080 + ' in Foo (at **)\n' +
1081 + ' in Suspense (at **)'),
1082 + ]);
1083 expect(root).toMatchRenderedOutput(<div>AB</div>);
1084 });
1085
@@ -1143,11 +1153,12 @@ describe('ReactLazy', () => {
1153 expect(root).not.toMatchRenderedOutput('4');
1154
1155 // Mount
1146 - await expect(async () => {
1147 - await act(() => resolveFakeImport(Add));
1148 - }).toErrorDev(
1149 - 'Unknown: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
1150 - );
1156 + await act(() => resolveFakeImport(Add));
1157 + assertConsoleErrorDev([
1158 + 'Unknown: Support for defaultProps will be removed from memo components in a future major release. ' +
1159 + 'Use JavaScript default parameters instead.\n' +
1160 + ' in Suspense (at **)',
1161 + ]);
1162 expect(root).toMatchRenderedOutput('4');
1163
1164 // Update (shallowly equal)
@@ -1231,11 +1242,14 @@ describe('ReactLazy', () => {
1242 expect(root).not.toMatchRenderedOutput('4');
1243
1244 // Mount
1234 - await expect(async () => {
1235 - await act(() => resolveFakeImport(Add));
1236 - }).toErrorDev([
1237 - 'Memo: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
1238 - 'Unknown: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
1245 + await act(() => resolveFakeImport(Add));
1246 + assertConsoleErrorDev([
1247 + 'Memo: Support for defaultProps will be removed from memo components in a future major release. ' +
1248 + 'Use JavaScript default parameters instead.\n' +
1249 + ' in Suspense (at **)',
1250 + 'Unknown: Support for defaultProps will be removed from memo components in a future major release. ' +
1251 + 'Use JavaScript default parameters instead.\n' +
1252 + ' in Suspense (at **)',
1253 ]);
1254 expect(root).toMatchRenderedOutput('4');
1255
packages/react-reconciler/src/__tests__/ReactMemo-test.js
+57 -48
@@ -19,6 +19,7 @@ let Scheduler;
19 let act;
20 let waitForAll;
21 let assertLog;
22 +let assertConsoleErrorDev;
23
24 describe('memo', () => {
25 beforeEach(() => {
@@ -33,6 +34,7 @@ describe('memo', () => {
34 const InternalTestUtils = require('internal-test-utils');
35 waitForAll = InternalTestUtils.waitForAll;
36 assertLog = InternalTestUtils.assertLog;
37 + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
38 });
39
40 function Text(props) {
@@ -397,17 +399,19 @@ describe('memo', () => {
399 // The final layer uses memo() from test fixture (which might be lazy).
400 Counter = memo(Counter);
401
400 - await expect(async () => {
401 - await act(() => {
402 - ReactNoop.render(
403 - <Suspense fallback={<Text text="Loading..." />}>
404 - <Counter e={5} />
405 - </Suspense>,
406 - );
407 - });
408 - assertLog(['Loading...', 15]);
409 - }).toErrorDev([
410 - 'Counter: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
402 + await act(() => {
403 + ReactNoop.render(
404 + <Suspense fallback={<Text text="Loading..." />}>
405 + <Counter e={5} />
406 + </Suspense>,
407 + );
408 + });
409 + assertLog(['Loading...', 15]);
410 + assertConsoleErrorDev([
411 + 'Counter: Support for defaultProps will be removed from memo components in a future major release. ' +
412 + 'Use JavaScript default parameters instead.\n' +
413 + (label === 'lazy' ? '' : ' in Indirection (at **)\n') +
414 + ' in Suspense (at **)',
415 ]);
416 expect(ReactNoop).toMatchRenderedOutput(<span prop={15} />);
417
@@ -431,17 +435,23 @@ describe('memo', () => {
435 });
436
437 it('warns if the first argument is undefined', () => {
434 - expect(() => memo()).toErrorDev(
435 - 'memo: The first argument must be a component. Instead ' +
436 - 'received: undefined',
438 + memo();
439 + assertConsoleErrorDev(
440 + [
441 + 'memo: The first argument must be a component. Instead ' +
442 + 'received: undefined',
443 + ],
444 {withoutStack: true},
445 );
446 });
447
448 it('warns if the first argument is null', () => {
442 - expect(() => memo(null)).toErrorDev(
443 - 'memo: The first argument must be a component. Instead ' +
444 - 'received: null',
449 + memo(null);
450 + assertConsoleErrorDev(
451 + [
452 + 'memo: The first argument must be a component. Instead ' +
453 + 'received: null',
454 + ],
455 {withoutStack: true},
456 );
457 });
@@ -458,16 +468,18 @@ describe('memo', () => {
468 Outer.defaultProps = {outer: 100};
469
470 const root = ReactNoop.createRoot();
461 - await expect(async () => {
462 - await act(() => {
463 - root.render(
464 - <div>
465 - <Outer />
466 - </div>,
467 - );
468 - });
469 - }).toErrorDev([
470 - 'Support for defaultProps will be removed from memo component',
471 + await act(() => {
472 + root.render(
473 + <div>
474 + <Outer />
475 + </div>,
476 + );
477 + });
478 + assertConsoleErrorDev([
479 + 'Inner: ' +
480 + 'Support for defaultProps will be removed from memo components in a future major release. ' +
481 + 'Use JavaScript default parameters instead.\n' +
482 + ' in div (at **)',
483 ]);
484 expect(root).toMatchRenderedOutput(<div>111</div>);
485
@@ -559,14 +571,15 @@ describe('memo', () => {
571 <MemoComponent />
572 </p>,
573 );
562 - await expect(async () => {
563 - await waitForAll([]);
564 - }).toErrorDev(
574 + await waitForAll([]);
575 + assertConsoleErrorDev([
576 'Each child in a list should have a unique "key" prop. ' +
577 'See https://react.dev/link/warning-keys for more information.\n' +
578 ' in span (at **)\n' +
568 - ' in ',
569 - );
579 + (gate('enableOwnerStacks')
580 + ? ' in **/ReactMemo-test.js:**:** (at **)'
581 + : ' in p (at **)'),
582 + ]);
583 });
584
585 it('should use the inner function name for the stack', async () => {
@@ -578,16 +591,15 @@ describe('memo', () => {
591 <MemoComponent />
592 </p>,
593 );
581 - await expect(async () => {
582 - await waitForAll([]);
583 - }).toErrorDev(
594 + await waitForAll([]);
595 + assertConsoleErrorDev([
596 'Each child in a list should have a unique "key" prop.' +
597 '\n\nCheck the top-level render call using <Inner>. It was passed a child from Inner. ' +
598 'See https://react.dev/link/warning-keys for more information.\n' +
599 ' in span (at **)\n' +
600 ' in Inner (at **)' +
601 (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
590 - );
602 + ]);
603 });
604
605 it('should use the inner name in the stack', async () => {
@@ -601,16 +613,15 @@ describe('memo', () => {
613 <MemoComponent />
614 </p>,
615 );
604 - await expect(async () => {
605 - await waitForAll([]);
606 - }).toErrorDev(
616 + await waitForAll([]);
617 + assertConsoleErrorDev([
618 'Each child in a list should have a unique "key" prop.' +
619 '\n\nCheck the top-level render call using <Inner>. It was passed a child from Inner. ' +
620 'See https://react.dev/link/warning-keys for more information.\n' +
621 ' in span (at **)\n' +
622 ' in Inner (at **)' +
623 (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
613 - );
624 + ]);
625 });
626
627 it('can use the outer displayName in the stack', async () => {
@@ -623,16 +634,15 @@ describe('memo', () => {
634 <MemoComponent />
635 </p>,
636 );
626 - await expect(async () => {
627 - await waitForAll([]);
628 - }).toErrorDev(
637 + await waitForAll([]);
638 + assertConsoleErrorDev([
639 'Each child in a list should have a unique "key" prop.' +
640 '\n\nCheck the top-level render call using <Outer>. It was passed a child from Outer. ' +
641 'See https://react.dev/link/warning-keys for more information.\n' +
642 ' in span (at **)\n' +
643 ' in Outer (at **)' +
644 (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
635 - );
645 + ]);
646 });
647
648 it('should prefer the inner to the outer displayName in the stack', async () => {
@@ -647,16 +657,15 @@ describe('memo', () => {
657 <MemoComponent />
658 </p>,
659 );
650 - await expect(async () => {
651 - await waitForAll([]);
652 - }).toErrorDev(
660 + await waitForAll([]);
661 + assertConsoleErrorDev([
662 'Each child in a list should have a unique "key" prop.' +
663 '\n\nCheck the top-level render call using <Inner>. It was passed a child from Inner. ' +
664 'See https://react.dev/link/warning-keys for more information.\n' +
665 ' in span (at **)\n' +
666 ' in Inner (at **)' +
667 (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
659 - );
668 + ]);
669 });
670 }
671 });
packages/react-reconciler/src/__tests__/ReactNewContext-test.js
+23 -9
@@ -861,8 +861,11 @@ describe('ReactNewContext', () => {
861 <Context.Provider anyPropNameOtherThanValue="value could be anything" />,
862 );
863
864 - await expect(async () => await waitForAll([])).toErrorDev(
865 - 'The `value` prop is required for the `<Context.Provider>`. Did you misspell it or forget to pass it?',
864 + await waitForAll([]);
865 + assertConsoleErrorDev(
866 + [
867 + 'The `value` prop is required for the `<Context.Provider>`. Did you misspell it or forget to pass it?',
868 + ],
869 {
870 withoutStack: true,
871 },
@@ -1036,7 +1039,9 @@ describe('ReactNewContext', () => {
1039 );
1040 await waitForAll(['LegacyProvider', 'App', 'Child']);
1041 assertConsoleErrorDev([
1039 - 'LegacyProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
1042 + 'LegacyProvider uses the legacy childContextTypes API which will soon be removed. ' +
1043 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1044 + ' in LegacyProvider (at **)',
1045 ]);
1046 expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
1047
@@ -1320,9 +1325,16 @@ describe('ReactNewContext', () => {
1325 }
1326
1327 ReactNoop.render(<Cls />);
1323 - await expect(async () => await waitForAll([])).toErrorDev([
1324 - 'Context can only be read while React is rendering',
1325 - 'Cannot update during an existing state transition',
1328 + await waitForAll([]);
1329 + assertConsoleErrorDev([
1330 + 'Cannot update during an existing state transition (such as within `render`). ' +
1331 + 'Render methods should be a pure function of props and state.\n' +
1332 + ' in Cls (at **)',
1333 + 'Context can only be read while React is rendering. ' +
1334 + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1335 + 'In function components, you can read it directly in the function body, ' +
1336 + 'but not inside Hooks like useReducer() or useMemo().\n' +
1337 + ' in Cls (at **)',
1338 ]);
1339 });
1340 });
@@ -1353,10 +1365,12 @@ describe('ReactNewContext', () => {
1365 return useContext(Context.Consumer);
1366 }
1367 ReactNoop.render(<Foo />);
1356 - await expect(async () => await waitForAll([])).toErrorDev(
1368 + await waitForAll([]);
1369 + assertConsoleErrorDev([
1370 'Calling useContext(Context.Consumer) is not supported and will cause bugs. ' +
1358 - 'Did you mean to call useContext(Context) instead?',
1359 - );
1371 + 'Did you mean to call useContext(Context) instead?\n' +
1372 + ' in Foo (at **)',
1373 + ]);
1374 });
1375
1376 // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js
+8 -5
@@ -13,6 +13,7 @@ let React;
13 let ReactNoop;
14 let waitForAll;
15 let act;
16 +let assertConsoleErrorDev;
17
18 describe('ReactSuspense', () => {
19 beforeEach(() => {
@@ -24,6 +25,7 @@ describe('ReactSuspense', () => {
25 const InternalTestUtils = require('internal-test-utils');
26 waitForAll = InternalTestUtils.waitForAll;
27 act = InternalTestUtils.act;
28 + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
29 });
30
31 function createThenable() {
@@ -57,10 +59,10 @@ describe('ReactSuspense', () => {
59 );
60
61 ReactNoop.render(elementBadType);
60 - await expect(async () => await waitForAll([])).toErrorDev(
61 - ['Unexpected type for suspenseCallback.'],
62 - {withoutStack: true},
63 - );
62 + await waitForAll([]);
63 + assertConsoleErrorDev(['Unexpected type for suspenseCallback.'], {
64 + withoutStack: true,
65 + });
66
67 const elementMissingCallback = (
68 <React.Suspense fallback={'Waiting'}>
@@ -69,7 +71,8 @@ describe('ReactSuspense', () => {
71 );
72
73 ReactNoop.render(elementMissingCallback);
72 - await expect(async () => await waitForAll([])).toErrorDev([]);
74 + await waitForAll([]);
75 + assertConsoleErrorDev([]);
76 });
77
78 // @gate enableSuspenseCallback
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+38 -44
@@ -18,6 +18,7 @@ let SuspenseList;
18 let waitForAll;
19 let assertLog;
20 let waitFor;
21 +let assertConsoleErrorDev;
22
23 describe('ReactSuspenseList', () => {
24 beforeEach(() => {
@@ -37,6 +38,7 @@ describe('ReactSuspenseList', () => {
38 assertLog = InternalTestUtils.assertLog;
39 waitFor = InternalTestUtils.waitFor;
40 act = InternalTestUtils.act;
41 + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
42 });
43
44 function Text(props) {
@@ -72,11 +74,10 @@ describe('ReactSuspenseList', () => {
74 );
75 }
76
75 - await expect(async () => {
76 - await act(() => {
77 - ReactNoop.render(<Foo />);
78 - });
79 - }).toErrorDev([
77 + await act(() => {
78 + ReactNoop.render(<Foo />);
79 + });
80 + assertConsoleErrorDev([
81 '"something" is not a supported revealOrder on ' +
82 '<SuspenseList />. Did you mean "together", "forwards" or "backwards"?' +
83 '\n in SuspenseList (at **)' +
@@ -94,11 +95,10 @@ describe('ReactSuspenseList', () => {
95 );
96 }
97
97 - await expect(async () => {
98 - await act(() => {
99 - ReactNoop.render(<Foo />);
100 - });
101 - }).toErrorDev([
98 + await act(() => {
99 + ReactNoop.render(<Foo />);
100 + });
101 + assertConsoleErrorDev([
102 '"TOGETHER" is not a valid value for revealOrder on ' +
103 '<SuspenseList />. Use lowercase "together" instead.' +
104 '\n in SuspenseList (at **)' +
@@ -116,11 +116,10 @@ describe('ReactSuspenseList', () => {
116 );
117 }
118
119 - await expect(async () => {
120 - await act(() => {
121 - ReactNoop.render(<Foo />);
122 - });
123 - }).toErrorDev([
119 + await act(() => {
120 + ReactNoop.render(<Foo />);
121 + });
122 + assertConsoleErrorDev([
123 '"forward" is not a valid value for revealOrder on ' +
124 '<SuspenseList />. React uses the -s suffix in the spelling. ' +
125 'Use "forwards" instead.' +
@@ -147,15 +146,14 @@ describe('ReactSuspenseList', () => {
146 // No warning
147 await waitForAll([]);
148
150 - await expect(async () => {
151 - await act(() => {
152 - ReactNoop.render(
153 - <Foo>
154 - <Suspense fallback="Loading">Child</Suspense>
155 - </Foo>,
156 - );
157 - });
158 - }).toErrorDev([
149 + await act(() => {
150 + ReactNoop.render(
151 + <Foo>
152 + <Suspense fallback="Loading">Child</Suspense>
153 + </Foo>,
154 + );
155 + });
156 + assertConsoleErrorDev([
157 'A single row was passed to a <SuspenseList revealOrder="forwards" />. ' +
158 'This is not useful since it needs multiple rows. ' +
159 'Did you mean to pass multiple children or an array?' +
@@ -174,11 +172,10 @@ describe('ReactSuspenseList', () => {
172 );
173 }
174
177 - await expect(async () => {
178 - await act(() => {
179 - ReactNoop.render(<Foo />);
180 - });
181 - }).toErrorDev([
175 + await act(() => {
176 + ReactNoop.render(<Foo />);
177 + });
178 + assertConsoleErrorDev([
179 'A single row was passed to a <SuspenseList revealOrder="backwards" />. ' +
180 'This is not useful since it needs multiple rows. ' +
181 'Did you mean to pass multiple children or an array?' +
@@ -202,11 +199,10 @@ describe('ReactSuspenseList', () => {
199 );
200 }
201
205 - await expect(async () => {
206 - await act(() => {
207 - ReactNoop.render(<Foo items={['A', 'B']} />);
208 - });
209 - }).toErrorDev([
202 + await act(() => {
203 + ReactNoop.render(<Foo items={['A', 'B']} />);
204 + });
205 + assertConsoleErrorDev([
206 'A nested array was passed to row #0 in <SuspenseList />. ' +
207 'Wrap it in an additional SuspenseList to configure its revealOrder: ' +
208 '<SuspenseList revealOrder=...> ... ' +
@@ -1555,11 +1551,10 @@ describe('ReactSuspenseList', () => {
1551 );
1552 }
1553
1558 - await expect(async () => {
1559 - await act(() => {
1560 - ReactNoop.render(<Foo />);
1561 - });
1562 - }).toErrorDev([
1554 + await act(() => {
1555 + ReactNoop.render(<Foo />);
1556 + });
1557 + assertConsoleErrorDev([
1558 '"collapse" is not a supported value for tail on ' +
1559 '<SuspenseList />. Did you mean "collapsed" or "hidden"?' +
1560 '\n in SuspenseList (at **)' +
@@ -1577,11 +1572,10 @@ describe('ReactSuspenseList', () => {
1572 );
1573 }
1574
1580 - await expect(async () => {
1581 - await act(() => {
1582 - ReactNoop.render(<Foo />);
1583 - });
1584 - }).toErrorDev([
1575 + await act(() => {
1576 + ReactNoop.render(<Foo />);
1577 + });
1578 + assertConsoleErrorDev([
1579 '<SuspenseList tail="collapsed" /> is only valid if ' +
1580 'revealOrder is "forwards" or "backwards". ' +
1581 'Did you mean to specify revealOrder="forwards"?' +
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+3 -3
@@ -1979,7 +1979,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1979 ReactNoop.render(<App />);
1980 });
1981
1982 - // TODO: assert toErrorDev() when the warning is implemented again.
1982 + // TODO: assertConsoleErrorDev() when the warning is implemented again.
1983 await act(() => {
1984 ReactNoop.flushSync(() => _setShow(true));
1985 });
@@ -2006,7 +2006,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2006 ReactNoop.render(<App />);
2007 });
2008
2009 - // TODO: assert toErrorDev() when the warning is implemented again.
2009 + // TODO: assertConsoleErrorDev() when the warning is implemented again.
2010 await act(() => {
2011 ReactNoop.flushSync(() => show());
2012 });
@@ -2076,7 +2076,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2076 ReactNoop.render(<App />);
2077 });
2078
2079 - // TODO: assert toErrorDev() when the warning is implemented again.
2079 + // TODO: assertConsoleErrorDev() when the warning is implemented again.
2080 await act(() => {
2081 ReactNoop.flushSync(() => _setShow(true));
2082 });
packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
+21 -21
@@ -14,6 +14,7 @@ let Scheduler;
14 let act;
15 let waitForAll;
16 let assertLog;
17 +let assertConsoleErrorDev;
18
19 let getCacheForType;
20 let useState;
@@ -43,11 +44,11 @@ describe('ReactInteractionTracing', () => {
44 ReactNoop = require('react-noop-renderer');
45 Scheduler = require('scheduler');
46
46 - act = require('internal-test-utils').act;
47 -
47 const InternalTestUtils = require('internal-test-utils');
48 + act = InternalTestUtils.act;
49 waitForAll = InternalTestUtils.waitForAll;
50 assertLog = InternalTestUtils.assertLog;
51 + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
52
53 useState = React.useState;
54 startTransition = React.startTransition;
@@ -1412,14 +1413,12 @@ describe('ReactInteractionTracing', () => {
1413 root.render(<App navigate={true} markerName="marker two" />);
1414 ReactNoop.expire(1000);
1415 await advanceTimers(1000);
1415 - await expect(
1416 - async () =>
1417 - await waitForAll([
1418 - 'Suspend [Page Two]',
1419 - 'Loading...',
1420 - 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, newName: marker two, type: marker}])',
1421 - ]),
1422 - ).toErrorDev('');
1416 +
1417 + await waitForAll([
1418 + 'Suspend [Page Two]',
1419 + 'Loading...',
1420 + 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, newName: marker two, type: marker}])',
1421 + ]);
1422
1423 resolveText('Page Two');
1424 ReactNoop.expire(1000);
@@ -2220,17 +2219,18 @@ describe('ReactInteractionTracing', () => {
2219 );
2220 ReactNoop.expire(1000);
2221 await advanceTimers(1000);
2223 - await expect(async () => {
2224 - // onMarkerComplete shouldn't be called for transitions with
2225 - // new keys
2226 - await waitForAll([
2227 - 'two',
2228 - 'onTransitionStart(transition two, 1000)',
2229 - 'onTransitionComplete(transition two, 1000, 2000)',
2230 - ]);
2231 - }).toErrorDev(
2232 - 'Changing the name of a tracing marker after mount is not supported.',
2233 - );
2222 + // onMarkerComplete shouldn't be called for transitions with
2223 + // new keys
2224 + await waitForAll([
2225 + 'two',
2226 + 'onTransitionStart(transition two, 1000)',
2227 + 'onTransitionComplete(transition two, 1000, 2000)',
2228 + ]);
2229 + assertConsoleErrorDev([
2230 + 'Changing the name of a tracing marker after mount is not supported. ' +
2231 + 'To remount the tracing marker, pass it a new key.\n' +
2232 + ' in App (at **)',
2233 + ]);
2234 startTransition(
2235 () => root.render(<App markerName="three" markerKey="new key" />),
2236 {