@samitouri / QOS-React-2 / commits / 7c11aad374

[assert helpers] react-dom (pt2) (#31902)

Converts more react-dom tests

Ricky committed Jan 2, 2025 at 15:53 UTC 7c11aad3746d7aa89435d12322e79a72896d5a9b
7 files changed +628 -448
packages/react-dom/src/__tests__/ReactDOMComponentTree-test.js
+11 -8
@@ -14,11 +14,14 @@ describe('ReactDOMComponentTree', () => {
14 let ReactDOMClient;
15 let act;
16 let container;
17 + let assertConsoleErrorDev;
18
19 beforeEach(() => {
20 React = require('react');
21 ReactDOMClient = require('react-dom/client');
22 act = require('internal-test-utils').act;
23 + assertConsoleErrorDev =
24 + require('internal-test-utils').assertConsoleErrorDev;
25
26 container = document.createElement('div');
27 document.body.appendChild(container);
@@ -190,18 +193,18 @@ describe('ReactDOMComponentTree', () => {
193 root.render(<Controlled />);
194 });
195
193 - await expect(
194 - async () =>
195 - await act(() => {
196 - simulateInput(inputRef.current, finishValue);
197 - }),
198 - ).toErrorDev(
196 + await act(() => {
197 + simulateInput(inputRef.current, finishValue);
198 + });
199 + assertConsoleErrorDev([
200 'A component is changing an uncontrolled input to be controlled. ' +
201 'This is likely caused by the value changing from undefined to ' +
202 'a defined value, which should not happen. ' +
203 'Decide between using a controlled or uncontrolled input ' +
204 'element for the lifetime of the component. More info: ' +
204 - 'https://react.dev/link/controlled-components',
205 - );
205 + 'https://react.dev/link/controlled-components\n' +
206 + ' in input (at **)\n' +
207 + ' in Controlled (at **)',
208 + ]);
209 });
210 });
packages/react-dom/src/__tests__/ReactDOMFiber-test.js
+17 -14
@@ -746,8 +746,13 @@ describe('ReactDOMFiber', () => {
746 root.render(<Parent />);
747 });
748 assertConsoleErrorDev([
749 - 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
750 - 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
749 + 'Parent uses the legacy childContextTypes API which will soon be removed. ' +
750 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
751 + ' in Parent (at **)',
752 + 'Component uses the legacy contextTypes API which will soon be removed. ' +
753 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
754 + (gate('enableOwnerStacks') ? '' : ' in Component (at **)\n') +
755 + ' in Parent (at **)',
756 ]);
757 expect(container.innerHTML).toBe('');
758 expect(portalContainer.innerHTML).toBe('<div>bar</div>');
@@ -957,15 +962,14 @@ describe('ReactDOMFiber', () => {
962 return <div onClick="woops" />;
963 }
964 }
960 - expect(() => {
961 - ReactDOM.flushSync(() => {
962 - root.render(<Example />);
963 - });
964 - }).toErrorDev(
965 + ReactDOM.flushSync(() => {
966 + root.render(<Example />);
967 + });
968 + assertConsoleErrorDev([
969 'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' +
970 ' in div (at **)\n' +
971 ' in Example (at **)',
968 - );
972 + ]);
973 });
974
975 it('should warn with a special message for `false` event listeners', () => {
@@ -974,17 +978,16 @@ describe('ReactDOMFiber', () => {
978 return <div onClick={false} />;
979 }
980 }
977 - expect(() => {
978 - ReactDOM.flushSync(() => {
979 - root.render(<Example />);
980 - });
981 - }).toErrorDev(
981 + ReactDOM.flushSync(() => {
982 + root.render(<Example />);
983 + });
984 + assertConsoleErrorDev([
985 'Expected `onClick` listener to be a function, instead got `false`.\n\n' +
986 'If you used to conditionally omit it with onClick={condition && value}, ' +
987 'pass onClick={condition ? value : undefined} instead.\n' +
988 ' in div (at **)\n' +
989 ' in Example (at **)',
987 - );
990 + ]);
991 });
992
993 it('should not update event handlers until commit', async () => {
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
+12 -5
@@ -19,6 +19,7 @@ let waitForAll;
19 let waitFor;
20 let waitForMicrotasks;
21 let assertLog;
22 +let assertConsoleErrorDev;
23
24 const setUntrackedInputValue = Object.getOwnPropertyDescriptor(
25 HTMLInputElement.prototype,
@@ -34,6 +35,8 @@ describe('ReactDOMFiberAsync', () => {
35 ReactDOM = require('react-dom');
36 ReactDOMClient = require('react-dom/client');
37 act = require('internal-test-utils').act;
38 + assertConsoleErrorDev =
39 + require('internal-test-utils').assertConsoleErrorDev;
40 Scheduler = require('scheduler');
41
42 const InternalTestUtils = require('internal-test-utils');
@@ -176,11 +179,15 @@ describe('ReactDOMFiberAsync', () => {
179 root.render(<Component />);
180 });
181 // Update
179 - expect(() => {
180 - ReactDOM.flushSync(() => {
181 - root.render(<Component />);
182 - });
183 - }).toErrorDev('flushSync was called from inside a lifecycle method');
182 + ReactDOM.flushSync(() => {
183 + root.render(<Component />);
184 + });
185 + assertConsoleErrorDev([
186 + 'flushSync was called from inside a lifecycle method. ' +
187 + 'React cannot flush when React is already rendering. ' +
188 + 'Consider moving this call to a scheduler task or micro task.\n' +
189 + ' in Component (at **)',
190 + ]);
191 });
192
193 describe('concurrent mode', () => {
packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js
+70 -20
@@ -26,6 +26,7 @@ let useFormStatus;
26 let useOptimistic;
27 let useActionState;
28 let Scheduler;
29 +let assertConsoleErrorDev;
30
31 describe('ReactDOMFizzForm', () => {
32 beforeEach(() => {
@@ -38,6 +39,8 @@ describe('ReactDOMFizzForm', () => {
39 useFormStatus = require('react-dom').useFormStatus;
40 useOptimistic = require('react').useOptimistic;
41 act = require('internal-test-utils').act;
42 + assertConsoleErrorDev =
43 + require('internal-test-utils').assertConsoleErrorDev;
44 container = document.createElement('div');
45 document.body.appendChild(container);
46 // TODO: Test the old api but it warns so needs warnings to be asserted.
@@ -195,12 +198,26 @@ describe('ReactDOMFizzForm', () => {
198 ReactDOMServer.renderToReadableStream(<App />),
199 );
200 await readIntoContainer(stream);
198 - await expect(async () => {
199 - await act(async () => {
200 - ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
201 - });
202 - }).toErrorDev(
203 - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
201 + await act(async () => {
202 + ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
203 + });
204 + assertConsoleErrorDev(
205 + [
206 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
207 + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" +
208 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
209 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
210 + "- Date formatting in a user's locale which doesn't match the server.\n" +
211 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
212 + '- Invalid HTML tag nesting.\n\n' +
213 + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' +
214 + 'https://react.dev/link/hydration-mismatch\n\n' +
215 + ' <App isClient={true}>\n' +
216 + ' <form\n' +
217 + '+ action="action"\n' +
218 + '- action="function"\n' +
219 + ' >\n',
220 + ],
221 {withoutStack: true},
222 );
223 });
@@ -357,23 +374,56 @@ describe('ReactDOMFizzForm', () => {
374
375 // Specifying the extra form fields are a DEV error, but we expect it
376 // to eventually still be patched up after an update.
360 - await expect(async () => {
361 - const stream = await serverAct(() =>
362 - ReactDOMServer.renderToReadableStream(<App />),
363 - );
364 - await readIntoContainer(stream);
365 - }).toErrorDev([
366 - 'Cannot specify a encType or method for a form that specifies a function as the action.',
367 - 'Cannot specify a formTarget for a button that specifies a function as a formAction.',
377 + const stream = await serverAct(() =>
378 + ReactDOMServer.renderToReadableStream(<App />),
379 + );
380 + await readIntoContainer(stream);
381 + assertConsoleErrorDev([
382 + 'Cannot specify a encType or method for a form that specifies a function as the action. ' +
383 + 'React provides those automatically. They will get overridden.\n' +
384 + ' in form (at **)\n' +
385 + ' in App (at **)',
386 + 'Cannot specify a formTarget for a button that specifies a function as a formAction. ' +
387 + 'The function will always be executed in the same window.\n' +
388 + ' in input (at **)\n' +
389 + (gate('enableOwnerStacks') ? '' : ' in form (at **)\n') +
390 + ' in App (at **)',
391 ]);
392 let root;
370 - await expect(async () => {
371 - await act(async () => {
372 - root = ReactDOMClient.hydrateRoot(container, <App />);
373 - });
374 - }).toErrorDev(
393 + await act(async () => {
394 + root = ReactDOMClient.hydrateRoot(container, <App />);
395 + });
396 + assertConsoleErrorDev(
397 [
376 - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
398 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
399 + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" +
400 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
401 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
402 + "- Date formatting in a user's locale which doesn't match the server.\n" +
403 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
404 + '- Invalid HTML tag nesting.\n\n' +
405 + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' +
406 + 'https://react.dev/link/hydration-mismatch\n\n' +
407 + ' <App>\n' +
408 + ' <form\n' +
409 + ' action={function action}\n' +
410 + ' ref={{current:null}}\n' +
411 + '+ method="DELETE"\n' +
412 + '- method={null}\n' +
413 + ' >\n' +
414 + ' <input\n' +
415 + ' type="submit"\n' +
416 + ' formAction={function action}\n' +
417 + ' ref={{current:null}}\n' +
418 + '+ formTarget="elsewhere"\n' +
419 + '- formTarget={null}\n' +
420 + ' >\n' +
421 + ' <button\n' +
422 + ' formAction={function action}\n' +
423 + ' ref={{current:null}}\n' +
424 + '+ formEncType="text/plain"\n' +
425 + '- formEncType={null}\n' +
426 + ' >\n',
427 ],
428 {withoutStack: true},
429 );
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+82 -44
@@ -1925,8 +1925,18 @@ describe('ReactDOMFizzServer', () => {
1925 pipe(writable);
1926 });
1927 assertConsoleErrorDev([
1928 - 'TestProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
1929 - 'TestConsumer uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
1928 + 'TestProvider uses the legacy childContextTypes API which will soon be removed. ' +
1929 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1930 + ' in TestProvider (at **)',
1931 + 'TestConsumer uses the legacy contextTypes API which will soon be removed. ' +
1932 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
1933 + ' in TestConsumer (at **)' +
1934 + (gate('enableOwnerStacks')
1935 + ? ''
1936 + : '\n in TestProvider (at **)' +
1937 + '\n in Suspense (at **)' +
1938 + '\n in div (at **)' +
1939 + '\n in TestProvider (at **)'),
1940 ]);
1941 expect(getVisibleChildren(container)).toEqual(
1942 <div>
@@ -3506,13 +3516,14 @@ describe('ReactDOMFizzServer', () => {
3516 </div>,
3517 {
3518 onRecoverableError(error, errorInfo) {
3509 - expect(() => {
3510 - expect(error.digest).toBe('a digest');
3511 - expect(errorInfo.digest).toBe(undefined);
3512 - }).toErrorDev(
3513 - 'You are accessing "digest" from the errorInfo object passed to onRecoverableError.' +
3514 - ' This property is no longer provided as part of errorInfo but can be accessed as a property' +
3515 - ' of the Error instance itself.',
3519 + expect(error.digest).toBe('a digest');
3520 + expect(errorInfo.digest).toBe(undefined);
3521 + assertConsoleErrorDev(
3522 + [
3523 + 'You are accessing "digest" from the errorInfo object passed to onRecoverableError.' +
3524 + ' This property is no longer provided as part of errorInfo but can be accessed as a property' +
3525 + ' of the Error instance itself.',
3526 + ],
3527 {withoutStack: true},
3528 );
3529 },
@@ -5777,13 +5788,24 @@ describe('ReactDOMFizzServer', () => {
5788 );
5789 }
5790
5780 - await expect(async () => {
5781 - await act(() => {
5782 - const {pipe} = renderToPipeableStream(<App />);
5783 - pipe(writable);
5784 - });
5785 - }).toErrorDev([
5786 - 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an Array with length 2 instead. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value which is why Arrays of length greater than 1 are not supported. When using JSX it can be common to combine text nodes and value nodes. For example: <title>hello {nameOfUser}</title>. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop is using this form try rewriting it using a template string: <title>{`hello ${nameOfUser}`}</title>.',
5791 + await act(() => {
5792 + const {pipe} = renderToPipeableStream(<App />);
5793 + pipe(writable);
5794 + });
5795 + assertConsoleErrorDev([
5796 + 'React expects the `children` prop of <title> tags to be a string, number, bigint, ' +
5797 + 'or object with a novel `toString` method but found an Array with length 2 instead. ' +
5798 + 'Browsers treat all child Nodes of <title> tags as Text content and React expects ' +
5799 + 'to be able to convert `children` of <title> tags to a single string value which is why ' +
5800 + 'Arrays of length greater than 1 are not supported. ' +
5801 + 'When using JSX it can be common to combine text nodes and value nodes. ' +
5802 + 'For example: <title>hello {nameOfUser}</title>. ' +
5803 + 'While not immediately apparent, `children` in this case is an Array with length 2. ' +
5804 + 'If your `children` prop is using this form try rewriting it using a template string: ' +
5805 + '<title>{`hello ${nameOfUser}`}</title>.\n' +
5806 + ' in title (at **)\n' +
5807 + (gate('enableOwnerStacks') ? '' : ' in head (at **)\n') +
5808 + ' in App (at **)',
5809 ]);
5810
5811 expect(getVisibleChildren(document.head)).toEqual(<title />);
@@ -5814,13 +5836,22 @@ describe('ReactDOMFizzServer', () => {
5836 );
5837 }
5838
5817 - await expect(async () => {
5818 - await act(() => {
5819 - const {pipe} = renderToPipeableStream(<App />);
5820 - pipe(writable);
5821 - });
5822 - }).toErrorDev([
5823 - 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an object that appears to be a React element which never implements a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags to a single string value which is why rendering React elements is not supported. If the `children` of <title> is a React Component try moving the <title> tag into that component. If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.',
5839 + await act(() => {
5840 + const {pipe} = renderToPipeableStream(<App />);
5841 + pipe(writable);
5842 + });
5843 + assertConsoleErrorDev([
5844 + 'React expects the `children` prop of <title> tags to be a string, number, bigint, ' +
5845 + 'or object with a novel `toString` method but found an object that appears to be a ' +
5846 + 'React element which never implements a suitable `toString` method. ' +
5847 + 'Browsers treat all child Nodes of <title> tags as Text content and React expects ' +
5848 + 'to be able to convert children of <title> tags to a single string value which is ' +
5849 + 'why rendering React elements is not supported. If the `children` of <title> is a ' +
5850 + 'React Component try moving the <title> tag into that component. ' +
5851 + 'If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.\n' +
5852 + ' in title (at **)\n' +
5853 + (gate('enableOwnerStacks') ? '' : ' in head (at **)\n') +
5854 + ' in App (at **)',
5855 ]);
5856 // object titles are toStringed when float is on
5857 expect(getVisibleChildren(document.head)).toEqual(
@@ -5849,13 +5880,22 @@ describe('ReactDOMFizzServer', () => {
5880 );
5881 }
5882
5852 - await expect(async () => {
5853 - await act(() => {
5854 - const {pipe} = renderToPipeableStream(<App />);
5855 - pipe(writable);
5856 - });
5857 - }).toErrorDev([
5858 - 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an object that does not implement a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags to a single string value. Using the default `toString` method available on every object is almost certainly an error. Consider whether the `children` of this <title> is an object in error and change it to a string or number value if so. Otherwise implement a `toString` method that React can use to produce a valid <title>.',
5883 + await act(() => {
5884 + const {pipe} = renderToPipeableStream(<App />);
5885 + pipe(writable);
5886 + });
5887 + assertConsoleErrorDev([
5888 + 'React expects the `children` prop of <title> tags to be a string, number, bigint, ' +
5889 + 'or object with a novel `toString` method but found an object that does not implement a ' +
5890 + 'suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text ' +
5891 + 'content and React expects to be able to convert children of <title> tags to a single string value. ' +
5892 + 'Using the default `toString` method available on every object is almost certainly an error. ' +
5893 + 'Consider whether the `children` of this <title> is an object in error and change it to a ' +
5894 + 'string or number value if so. Otherwise implement a `toString` method that React can ' +
5895 + 'use to produce a valid <title>.\n' +
5896 + ' in title (at **)\n' +
5897 + (gate('enableOwnerStacks') ? '' : ' in head (at **)\n') +
5898 + ' in App (at **)',
5899 ]);
5900 // object titles are toStringed when float is on
5901 expect(getVisibleChildren(document.head)).toEqual(
@@ -8381,12 +8421,11 @@ describe('ReactDOMFizzServer', () => {
8421 return <div>{children}</div>;
8422 }
8423
8384 - await expect(async () => {
8385 - await act(() => {
8386 - const {pipe} = renderToPipeableStream(<Foo />);
8387 - pipe(writable);
8388 - });
8389 - }).toErrorDev(
8424 + await act(() => {
8425 + const {pipe} = renderToPipeableStream(<Foo />);
8426 + pipe(writable);
8427 + });
8428 + assertConsoleErrorDev([
8429 'Using Iterators as children is unsupported and will likely yield ' +
8430 'unexpected results because enumerating a generator mutates it. ' +
8431 'You may convert it to an array with `Array.from()` or the ' +
@@ -8394,7 +8433,7 @@ describe('ReactDOMFizzServer', () => {
8433 'Iterable that can iterate multiple times over the same items.\n' +
8434 ' in div (at **)\n' +
8435 ' in Foo (at **)',
8397 - );
8436 + ]);
8437
8438 expect(document.body.textContent).toBe('HelloWorld');
8439 });
@@ -8420,19 +8459,18 @@ describe('ReactDOMFizzServer', () => {
8459 return iterator;
8460 }
8461
8423 - await expect(async () => {
8424 - await act(() => {
8425 - const {pipe} = renderToPipeableStream(<Foo />);
8426 - pipe(writable);
8427 - });
8428 - }).toErrorDev(
8462 + await act(() => {
8463 + const {pipe} = renderToPipeableStream(<Foo />);
8464 + pipe(writable);
8465 + });
8466 + assertConsoleErrorDev([
8467 'Using Iterators as children is unsupported and will likely yield ' +
8468 'unexpected results because enumerating a generator mutates it. ' +
8469 'You may convert it to an array with `Array.from()` or the ' +
8470 '`[...spread]` operator before rendering. You can also use an ' +
8471 'Iterable that can iterate multiple times over the same items.\n' +
8472 ' in Foo (at **)',
8435 - );
8473 + ]);
8474
8475 expect(document.body.textContent).toBe('HelloWorld');
8476 });
packages/react-dom/src/__tests__/ReactDOMForm-test.js
+70 -49
@@ -361,7 +361,7 @@ describe('ReactDOMForm', () => {
361 expect(actionCalled).toBe(false);
362 });
363
364 - it('should only submit the inner of nested forms', async () => {
364 + it('should submit the inner of nested forms', async () => {
365 const ref = React.createRef();
366 let data;
367
@@ -373,19 +373,18 @@ describe('ReactDOMForm', () => {
373 }
374
375 const root = ReactDOMClient.createRoot(container);
376 - await expect(async () => {
377 - await act(async () => {
378 - // This isn't valid HTML but just in case.
379 - root.render(
380 - <form action={outerAction}>
381 - <input type="text" name="data" defaultValue="outer" />
382 - <form action={innerAction} ref={ref}>
383 - <input type="text" name="data" defaultValue="inner" />
384 - </form>
385 - </form>,
386 - );
387 - });
388 - }).toErrorDev(
376 + await act(async () => {
377 + // This isn't valid HTML but just in case.
378 + root.render(
379 + <form action={outerAction}>
380 + <input type="text" name="data" defaultValue="outer" />
381 + <form action={innerAction} ref={ref}>
382 + <input type="text" name="data" defaultValue="inner" />
383 + </form>
384 + </form>,
385 + );
386 + });
387 + assertConsoleErrorDev([
388 'In HTML, <form> cannot be a descendant of <form>.\n' +
389 'This will cause a hydration error.\n' +
390 '\n' +
@@ -394,14 +393,14 @@ describe('ReactDOMForm', () => {
393 '> <form action={function innerAction} ref={{current:null}}>\n' +
394 '\n in form (at **)' +
395 (gate(flags => flags.enableOwnerStacks) ? '' : '\n in form (at **)'),
397 - );
396 + ]);
397
398 await submit(ref.current);
399
400 expect(data).toBe('innerinner');
401 });
402
404 - it('should only submit once if one root is nested inside the other', async () => {
403 + it('should submit once if one root is nested inside the other', async () => {
404 const ref = React.createRef();
405 let outerCalled = 0;
406 let innerCalled = 0;
@@ -444,7 +443,7 @@ describe('ReactDOMForm', () => {
443 expect(innerCalled).toBe(1);
444 });
445
447 - it('should only submit once if a portal is nested inside its own root', async () => {
446 + it('should submit once if a portal is nested inside its own root', async () => {
447 const ref = React.createRef();
448 let outerCalled = 0;
449 let innerCalled = 0;
@@ -565,29 +564,32 @@ describe('ReactDOMForm', () => {
564 }
565
566 const root = ReactDOMClient.createRoot(container);
568 - await expect(async () => {
569 - await act(async () => {
570 - root.render(
571 - <form>
572 - <input
573 - type="submit"
574 - name="button"
575 - value="delete"
576 - ref={inputRef}
577 - formAction={action}
578 - />
579 - <button
580 - name="button"
581 - value="edit"
582 - ref={buttonRef}
583 - formAction={action}>
584 - Edit
585 - </button>
586 - </form>,
587 - );
588 - });
589 - }).toErrorDev([
590 - 'Cannot specify a "name" prop for a button that specifies a function as a formAction.',
567 + await act(async () => {
568 + root.render(
569 + <form>
570 + <input
571 + type="submit"
572 + name="button"
573 + value="delete"
574 + ref={inputRef}
575 + formAction={action}
576 + />
577 + <button
578 + name="button"
579 + value="edit"
580 + ref={buttonRef}
581 + formAction={action}>
582 + Edit
583 + </button>
584 + </form>,
585 + );
586 + });
587 + assertConsoleErrorDev([
588 + 'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' +
589 + 'React needs it to encode which action should be invoked. ' +
590 + 'It will get overridden.\n' +
591 + ' in input (at **)' +
592 + (gate('enableOwnerStacks') ? '' : '\n in form (at **)'),
593 ]);
594
595 await submit(inputRef.current);
@@ -1492,8 +1494,9 @@ describe('ReactDOMForm', () => {
1494 await act(() => dispatch());
1495 assertConsoleErrorDev([
1496 [
1495 - 'An async function was passed to useActionState, but it was ' +
1496 - 'dispatched outside of an action context',
1497 + 'An async function was passed to useActionState, but it was dispatched outside of an action context. ' +
1498 + 'This is likely not what you intended. ' +
1499 + 'Either pass the dispatch function to an `action` prop, or dispatch manually inside `startTransition`',
1500 {withoutStack: true},
1501 ],
1502 ]);
@@ -1919,11 +1922,16 @@ describe('ReactDOMForm', () => {
1922 expect(inputRef.current.value).toBe(' Updated ');
1923
1924 // This triggers a synchronous requestFormReset, and a warning
1922 - await expect(async () => {
1923 - await act(() => resolveText('Wait 1'));
1924 - }).toErrorDev(['requestFormReset was called outside a transition'], {
1925 - withoutStack: true,
1926 - });
1925 + await act(() => resolveText('Wait 1'));
1926 + assertConsoleErrorDev(
1927 + [
1928 + 'requestFormReset was called outside a transition or action. ' +
1929 + 'To fix, move to an action, or wrap with startTransition.',
1930 + ],
1931 + {
1932 + withoutStack: true,
1933 + },
1934 + );
1935 assertLog(['Request form reset']);
1936
1937 // The form was reset even though the action didn't finish.
@@ -1957,7 +1965,14 @@ describe('ReactDOMForm', () => {
1965
1966 // Symbols are coerced to null, so this should fire the form action
1967 await act(() => root.render(<App submitterAction={Symbol()} />));
1960 - assertConsoleErrorDev(['Invalid value for prop `formAction`']);
1968 + assertConsoleErrorDev([
1969 + 'Invalid value for prop `formAction` on <button> tag. ' +
1970 + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1971 + 'For details, see https://react.dev/link/attribute-behavior \n' +
1972 + ' in button (at **)\n' +
1973 + (gate('enableOwnerStacks') ? '' : ' in form (at **)\n') +
1974 + ' in App (at **)',
1975 + ]);
1976 await submit(buttonRef.current);
1977 assertLog(['Form action']);
1978
@@ -2201,7 +2216,13 @@ describe('ReactDOMForm', () => {
2216
2217 // Symbols are coerced to null
2218 await act(() => root.render(<Form action={Symbol()} />));
2204 - assertConsoleErrorDev(['Invalid value for prop `action`']);
2219 + assertConsoleErrorDev([
2220 + 'Invalid value for prop `action` on <form> tag. ' +
2221 + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
2222 + 'For details, see https://react.dev/link/attribute-behavior \n' +
2223 + ' in form (at **)\n' +
2224 + ' in Form (at **)',
2225 + ]);
2226 await submit(formRef.current);
2227 assertLog([null]);
2228
packages/react-dom/src/__tests__/ReactDOMInput-test.js
+366 -308
@@ -26,6 +26,7 @@ describe('ReactDOMInput', () => {
26 let setUntrackedChecked;
27 let container;
28 let root;
29 + let assertConsoleErrorDev;
30
31 function dispatchEventOnNode(node, type) {
32 node.dispatchEvent(new Event(type, {bubbles: true, cancelable: true}));
@@ -96,6 +97,8 @@ describe('ReactDOMInput', () => {
97 ReactDOMServer = require('react-dom/server');
98 Scheduler = require('scheduler');
99 act = require('internal-test-utils').act;
100 + assertConsoleErrorDev =
101 + require('internal-test-utils').assertConsoleErrorDev;
102 assertLog = require('internal-test-utils').assertLog;
103
104 container = document.createElement('div');
@@ -109,52 +112,54 @@ describe('ReactDOMInput', () => {
112 });
113
114 it('should warn for controlled value of 0 with missing onChange', async () => {
112 - await expect(async () => {
113 - await act(() => {
114 - root.render(<input type="text" value={0} />);
115 - });
116 - }).toErrorDev(
115 + await act(() => {
116 + root.render(<input type="text" value={0} />);
117 + });
118 + assertConsoleErrorDev([
119 'You provided a `value` prop to a form ' +
120 'field without an `onChange` handler. This will render a read-only ' +
121 'field. If the field should be mutable use `defaultValue`. ' +
120 - 'Otherwise, set either `onChange` or `readOnly`.',
121 - );
122 + 'Otherwise, set either `onChange` or `readOnly`.\n' +
123 + ' in input (at **)',
124 + ]);
125 });
126
127 it('should warn for controlled value of "" with missing onChange', async () => {
125 - await expect(async () => {
126 - await act(() => {
127 - root.render(<input type="text" value="" />);
128 - });
129 - }).toErrorDev(
128 + await act(() => {
129 + root.render(<input type="text" value="" />);
130 + });
131 + assertConsoleErrorDev([
132 'You provided a `value` prop to a form ' +
133 'field without an `onChange` handler. This will render a read-only ' +
134 'field. If the field should be mutable use `defaultValue`. ' +
133 - 'Otherwise, set either `onChange` or `readOnly`.',
134 - );
135 + 'Otherwise, set either `onChange` or `readOnly`.\n' +
136 + ' in input (at **)',
137 + ]);
138 });
139
140 it('should warn for controlled value of "0" with missing onChange', async () => {
138 - await expect(async () => {
139 - await act(() => {
140 - root.render(<input type="text" value="0" />);
141 - });
142 - }).toErrorDev(
141 + await act(() => {
142 + root.render(<input type="text" value="0" />);
143 + });
144 + assertConsoleErrorDev([
145 'You provided a `value` prop to a form ' +
146 'field without an `onChange` handler. This will render a read-only ' +
147 'field. If the field should be mutable use `defaultValue`. ' +
146 - 'Otherwise, set either `onChange` or `readOnly`.',
147 - );
148 + 'Otherwise, set either `onChange` or `readOnly`.\n' +
149 + ' in input (at **)',
150 + ]);
151 });
152
153 it('should warn for controlled value of false with missing onChange', async () => {
151 - await expect(async () => {
152 - await act(() => {
153 - root.render(<input type="checkbox" checked={false} />);
154 - });
155 - }).toErrorDev(
156 - 'You provided a `checked` prop to a form field without an `onChange` handler.',
157 - );
154 + await act(() => {
155 + root.render(<input type="checkbox" checked={false} />);
156 + });
157 + assertConsoleErrorDev([
158 + 'You provided a `checked` prop to a form field without an `onChange` handler. ' +
159 + 'This will render a read-only field. If the field should be mutable use `defaultChecked`. ' +
160 + 'Otherwise, set either `onChange` or `readOnly`.\n' +
161 + ' in input (at **)',
162 + ]);
163 });
164
165 it('should warn with checked and no onChange handler with readOnly specified', async () => {
@@ -164,15 +169,15 @@ describe('ReactDOMInput', () => {
169 root.unmount();
170 root = ReactDOMClient.createRoot(container);
171
167 - await expect(async () => {
168 - await act(() => {
169 - root.render(<input type="checkbox" checked={false} readOnly={false} />);
170 - });
171 - }).toErrorDev(
172 + await act(() => {
173 + root.render(<input type="checkbox" checked={false} readOnly={false} />);
174 + });
175 + assertConsoleErrorDev([
176 'You provided a `checked` prop to a form field without an `onChange` handler. ' +
177 'This will render a read-only field. If the field should be mutable use `defaultChecked`. ' +
174 - 'Otherwise, set either `onChange` or `readOnly`.',
175 - );
178 + 'Otherwise, set either `onChange` or `readOnly`.\n' +
179 + ' in input (at **)',
180 + ]);
181 });
182
183 it('should not warn about missing onChange in uncontrolled inputs', async () => {
@@ -213,13 +218,15 @@ describe('ReactDOMInput', () => {
218 });
219
220 it('should properly control a value even if no event listener exists', async () => {
216 - await expect(async () => {
217 - await act(() => {
218 - root.render(<input type="text" value="lion" />);
219 - });
220 - }).toErrorDev(
221 - 'You provided a `value` prop to a form field without an `onChange` handler.',
222 - );
221 + await act(() => {
222 + root.render(<input type="text" value="lion" />);
223 + });
224 + assertConsoleErrorDev([
225 + 'You provided a `value` prop to a form field without an `onChange` handler. ' +
226 + 'This will render a read-only field. If the field should be mutable use `defaultValue`. ' +
227 + 'Otherwise, set either `onChange` or `readOnly`.\n' +
228 + ' in input (at **)',
229 + ]);
230 const node = container.firstChild;
231 expect(isValueDirty(node)).toBe(true);
232
@@ -426,14 +433,16 @@ describe('ReactDOMInput', () => {
433 }
434
435 const ref = React.createRef();
429 - await expect(async () => {
430 - await act(() => {
431 - root.render(<Stub ref={ref} />);
432 - });
433 - }).toErrorDev(
434 - 'You provided a `value` prop to a form field ' +
435 - 'without an `onChange` handler.',
436 - );
436 + await act(() => {
437 + root.render(<Stub ref={ref} />);
438 + });
439 + assertConsoleErrorDev([
440 + 'You provided a `value` prop to a form field without an `onChange` handler. ' +
441 + 'This will render a read-only field. If the field should be mutable use `defaultValue`. ' +
442 + 'Otherwise, set either `onChange` or `readOnly`.\n' +
443 + ' in input (at **)\n' +
444 + ' in Stub (at **)',
445 + ]);
446 const node = container.firstChild;
447 await act(() => {
448 ref.current.setState({value: '0.98'});
@@ -499,14 +508,16 @@ describe('ReactDOMInput', () => {
508 }
509
510 const ref = React.createRef();
502 - await expect(async () => {
503 - await act(() => {
504 - root.render(<Stub ref={ref} />);
505 - });
506 - }).toErrorDev(
507 - 'You provided a `value` prop to a form field ' +
508 - 'without an `onChange` handler.',
509 - );
511 + await act(() => {
512 + root.render(<Stub ref={ref} />);
513 + });
514 + assertConsoleErrorDev([
515 + 'You provided a `value` prop to a form field without an `onChange` handler. ' +
516 + 'This will render a read-only field. If the field should be mutable use `defaultValue`. ' +
517 + 'Otherwise, set either `onChange` or `readOnly`.\n' +
518 + ' in input (at **)\n' +
519 + ' in Stub (at **)',
520 + ]);
521 const node = container.firstChild;
522 await act(() => {
523 ref.current.setState({value: '3'});
@@ -636,13 +647,16 @@ describe('ReactDOMInput', () => {
647 const node = container.firstChild;
648 expect(node.value).toBe('0');
649 expect(isValueDirty(node)).toBe(true);
639 - await expect(async () => {
640 - await act(() => {
641 - root.render(<input type="text" defaultValue="1" />);
642 - });
643 - }).toErrorDev(
644 - 'A component is changing a controlled input to be uncontrolled.',
645 - );
650 + await act(() => {
651 + root.render(<input type="text" defaultValue="1" />);
652 + });
653 + assertConsoleErrorDev([
654 + 'A component is changing a controlled input to be uncontrolled. ' +
655 + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
656 + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
657 + 'More info: https://react.dev/link/controlled-components\n' +
658 + ' in input (at **)',
659 + ]);
660 expect(node.value).toBe('0');
661 expect(isValueDirty(node)).toBe(true);
662 });
@@ -745,15 +759,18 @@ describe('ReactDOMInput', () => {
759 }
760 }
761 await expect(async () => {
748 - await expect(async () => {
749 - await act(() => {
750 - root.render(<input defaultValue={new TemporalLike()} type="date" />);
751 - });
752 - }).toErrorDev(
753 - 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
754 - 'strings, not TemporalLike. This value must be coerced to a string before using it here.',
755 - );
762 + await act(() => {
763 + root.render(<input defaultValue={new TemporalLike()} type="date" />);
764 + });
765 }).rejects.toThrowError(new TypeError('prod message'));
766 + assertConsoleErrorDev([
767 + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
768 + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
769 + ' in input (at **)',
770 + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
771 + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
772 + ' in input (at **)',
773 + ]);
774 });
775
776 it('should throw for text inputs if `defaultValue` is an object where valueOf() throws', async () => {
@@ -768,15 +785,18 @@ describe('ReactDOMInput', () => {
785 }
786 }
787 await expect(async () => {
771 - await expect(async () => {
772 - await act(() => {
773 - root.render(<input defaultValue={new TemporalLike()} type="text" />);
774 - });
775 - }).toErrorDev(
776 - 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
777 - 'strings, not TemporalLike. This value must be coerced to a string before using it here.',
778 - );
788 + await act(() => {
789 + root.render(<input defaultValue={new TemporalLike()} type="text" />);
790 + });
791 }).rejects.toThrowError(new TypeError('prod message'));
792 + assertConsoleErrorDev([
793 + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
794 + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
795 + ' in input (at **)',
796 + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
797 + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
798 + ' in input (at **)',
799 + ]);
800 });
801
802 it('should throw for date inputs if `value` is an object where valueOf() throws', async () => {
@@ -791,21 +811,20 @@ describe('ReactDOMInput', () => {
811 }
812 }
813 await expect(async () => {
794 - await expect(async () => {
795 - await act(() => {
796 - root.render(
797 - <input
798 - value={new TemporalLike()}
799 - type="date"
800 - onChange={() => {}}
801 - />,
802 - );
803 - });
804 - }).toErrorDev(
805 - 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
806 - 'strings, not TemporalLike. This value must be coerced to a string before using it here.',
807 - );
814 + await act(() => {
815 + root.render(
816 + <input value={new TemporalLike()} type="date" onChange={() => {}} />,
817 + );
818 + });
819 }).rejects.toThrowError(new TypeError('prod message'));
820 + assertConsoleErrorDev([
821 + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
822 + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
823 + ' in input (at **)',
824 + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
825 + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
826 + ' in input (at **)',
827 + ]);
828 });
829
830 it('should throw for text inputs if `value` is an object where valueOf() throws', async () => {
@@ -820,21 +839,20 @@ describe('ReactDOMInput', () => {
839 }
840 }
841 await expect(async () => {
823 - await expect(async () => {
824 - await act(() => {
825 - root.render(
826 - <input
827 - value={new TemporalLike()}
828 - type="text"
829 - onChange={() => {}}
830 - />,
831 - );
832 - });
833 - }).toErrorDev(
834 - 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
835 - 'strings, not TemporalLike. This value must be coerced to a string before using it here.',
836 - );
842 + await act(() => {
843 + root.render(
844 + <input value={new TemporalLike()} type="text" onChange={() => {}} />,
845 + );
846 + });
847 }).rejects.toThrowError(new TypeError('prod message'));
848 + assertConsoleErrorDev([
849 + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
850 + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
851 + ' in input (at **)',
852 + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
853 + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
854 + ' in input (at **)',
855 + ]);
856 });
857
858 it('should display `value` of number 0', async () => {
@@ -1199,15 +1217,18 @@ describe('ReactDOMInput', () => {
1217
1218 // Not really relevant to this particular test, but changing to undefined
1219 // should nonetheless trigger a warning
1202 - await expect(async () => {
1203 - await act(() => {
1204 - root.render(
1205 - <input type="submit" value={undefined} onChange={emptyFunction} />,
1206 - );
1207 - });
1208 - }).toErrorDev(
1209 - 'A component is changing a controlled input to be uncontrolled.',
1210 - );
1220 + await act(() => {
1221 + root.render(
1222 + <input type="submit" value={undefined} onChange={emptyFunction} />,
1223 + );
1224 + });
1225 + assertConsoleErrorDev([
1226 + 'A component is changing a controlled input to be uncontrolled. ' +
1227 + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
1228 + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
1229 + 'More info: https://react.dev/link/controlled-components\n' +
1230 + ' in input (at **)',
1231 + ]);
1232
1233 const node = container.firstChild;
1234 expect(node.getAttribute('value')).toBe(null);
@@ -1221,15 +1242,18 @@ describe('ReactDOMInput', () => {
1242
1243 // Not really relevant to this particular test, but changing to undefined
1244 // should nonetheless trigger a warning
1224 - await expect(async () => {
1225 - await act(() => {
1226 - root.render(
1227 - <input type="reset" value={undefined} onChange={emptyFunction} />,
1228 - );
1229 - });
1230 - }).toErrorDev(
1231 - 'A component is changing a controlled input to be uncontrolled.',
1232 - );
1245 + await act(() => {
1246 + root.render(
1247 + <input type="reset" value={undefined} onChange={emptyFunction} />,
1248 + );
1249 + });
1250 + assertConsoleErrorDev([
1251 + 'A component is changing a controlled input to be uncontrolled. ' +
1252 + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
1253 + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
1254 + 'More info: https://react.dev/link/controlled-components\n' +
1255 + ' in input (at **)',
1256 + ]);
1257
1258 const node = container.firstChild;
1259 expect(node.getAttribute('value')).toBe(null);
@@ -1281,11 +1305,14 @@ describe('ReactDOMInput', () => {
1305
1306 it('should not set a null value on a submit input', async () => {
1307 const stub = <input type="submit" value={null} />;
1284 - await expect(async () => {
1285 - await act(() => {
1286 - root.render(stub);
1287 - });
1288 - }).toErrorDev('`value` prop on `input` should not be null');
1308 + await act(() => {
1309 + root.render(stub);
1310 + });
1311 + assertConsoleErrorDev([
1312 + '`value` prop on `input` should not be null. ' +
1313 + 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' +
1314 + ' in input (at **)',
1315 + ]);
1316 const node = container.firstChild;
1317
1318 // Note: it shouldn't be an empty string
@@ -1300,11 +1327,14 @@ describe('ReactDOMInput', () => {
1327
1328 it('should not set a null value on a reset input', async () => {
1329 const stub = <input type="reset" value={null} />;
1303 - await expect(async () => {
1304 - await act(() => {
1305 - root.render(stub);
1306 - });
1307 - }).toErrorDev('`value` prop on `input` should not be null');
1330 + await act(() => {
1331 + root.render(stub);
1332 + });
1333 + assertConsoleErrorDev([
1334 + '`value` prop on `input` should not be null. ' +
1335 + 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' +
1336 + ' in input (at **)',
1337 + ]);
1338 const node = container.firstChild;
1339
1340 // Note: it shouldn't be an empty string
@@ -1906,17 +1936,16 @@ describe('ReactDOMInput', () => {
1936 root.unmount();
1937 root = ReactDOMClient.createRoot(container);
1938
1909 - await expect(async () => {
1910 - await act(() => {
1911 - root.render(<input type="text" value="zoink" readOnly={false} />);
1912 - });
1913 - }).toErrorDev(
1939 + await act(() => {
1940 + root.render(<input type="text" value="zoink" readOnly={false} />);
1941 + });
1942 + assertConsoleErrorDev([
1943 'You provided a `value` prop to a form ' +
1944 'field without an `onChange` handler. This will render a read-only ' +
1945 'field. If the field should be mutable use `defaultValue`. ' +
1946 'Otherwise, set either `onChange` or `readOnly`.\n' +
1947 ' in input (at **)',
1919 - );
1948 + ]);
1949 });
1950
1951 it('should have a this value of undefined if bind is not used', async () => {
@@ -1958,15 +1987,15 @@ describe('ReactDOMInput', () => {
1987 });
1988
1989 it('should warn if value is null', async () => {
1961 - await expect(async () => {
1962 - await act(() => {
1963 - root.render(<input type="text" value={null} />);
1964 - });
1965 - }).toErrorDev(
1990 + await act(() => {
1991 + root.render(<input type="text" value={null} />);
1992 + });
1993 + assertConsoleErrorDev([
1994 '`value` prop on `input` should not be null. ' +
1995 'Consider using an empty string to clear the component or `undefined` ' +
1968 - 'for uncontrolled components.',
1969 - );
1996 + 'for uncontrolled components.\n' +
1997 + ' in input (at **)',
1998 + ]);
1999 root.unmount();
2000
2001 root = ReactDOMClient.createRoot(container);
@@ -1976,25 +2005,25 @@ describe('ReactDOMInput', () => {
2005 });
2006
2007 it('should warn if checked and defaultChecked props are specified', async () => {
1979 - await expect(async () => {
1980 - await act(() => {
1981 - root.render(
1982 - <input
1983 - type="radio"
1984 - checked={true}
1985 - defaultChecked={true}
1986 - readOnly={true}
1987 - />,
1988 - );
1989 - });
1990 - }).toErrorDev(
2008 + await act(() => {
2009 + root.render(
2010 + <input
2011 + type="radio"
2012 + checked={true}
2013 + defaultChecked={true}
2014 + readOnly={true}
2015 + />,
2016 + );
2017 + });
2018 + assertConsoleErrorDev([
2019 'A component contains an input of type radio with both checked and defaultChecked props. ' +
2020 'Input elements must be either controlled or uncontrolled ' +
2021 '(specify either the checked prop, or the defaultChecked prop, but not ' +
2022 'both). Decide between using a controlled or uncontrolled input ' +
2023 'element and remove one of these props. More info: ' +
1996 - 'https://react.dev/link/controlled-components',
1997 - );
2024 + 'https://react.dev/link/controlled-components\n' +
2025 + ' in input (at **)',
2026 + ]);
2027 root.unmount();
2028
2029 root = ReactDOMClient.createRoot(container);
@@ -2011,20 +2040,20 @@ describe('ReactDOMInput', () => {
2040 });
2041
2042 it('should warn if value and defaultValue props are specified', async () => {
2014 - await expect(async () => {
2015 - await act(() => {
2016 - root.render(
2017 - <input type="text" value="foo" defaultValue="bar" readOnly={true} />,
2018 - );
2019 - });
2020 - }).toErrorDev(
2043 + await act(() => {
2044 + root.render(
2045 + <input type="text" value="foo" defaultValue="bar" readOnly={true} />,
2046 + );
2047 + });
2048 + assertConsoleErrorDev([
2049 'A component contains an input of type text with both value and defaultValue props. ' +
2050 'Input elements must be either controlled or uncontrolled ' +
2051 '(specify either the value prop, or the defaultValue prop, but not ' +
2052 'both). Decide between using a controlled or uncontrolled input ' +
2053 'element and remove one of these props. More info: ' +
2026 - 'https://react.dev/link/controlled-components',
2027 - );
2054 + 'https://react.dev/link/controlled-components\n' +
2055 + ' in input (at **)',
2056 + ]);
2057 await (() => {
2058 root.unmount();
2059 });
@@ -2043,18 +2072,17 @@ describe('ReactDOMInput', () => {
2072 await act(() => {
2073 root.render(stub);
2074 });
2046 - await expect(async () => {
2047 - await act(() => {
2048 - root.render(<input type="text" />);
2049 - });
2050 - }).toErrorDev(
2075 + await act(() => {
2076 + root.render(<input type="text" />);
2077 + });
2078 + assertConsoleErrorDev([
2079 'A component is changing a controlled input to be uncontrolled. ' +
2080 'This is likely caused by the value changing from a defined to ' +
2081 'undefined, which should not happen. ' +
2082 'Decide between using a controlled or uncontrolled input ' +
2083 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2084 ' in input (at **)',
2057 - );
2085 + ]);
2086 });
2087
2088 it('should warn if controlled input switches to uncontrolled (value is null)', async () => {
@@ -2064,13 +2092,13 @@ describe('ReactDOMInput', () => {
2092 await act(() => {
2093 root.render(stub);
2094 });
2067 - await expect(async () => {
2068 - await act(() => {
2069 - root.render(<input type="text" value={null} />);
2070 - });
2071 - }).toErrorDev([
2095 + await act(() => {
2096 + root.render(<input type="text" value={null} />);
2097 + });
2098 + assertConsoleErrorDev([
2099 '`value` prop on `input` should not be null. ' +
2073 - 'Consider using an empty string to clear the component or `undefined` for uncontrolled components',
2100 + 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' +
2101 + ' in input (at **)',
2102 'A component is changing a controlled input to be uncontrolled. ' +
2103 'This is likely caused by the value changing from a defined to ' +
2104 'undefined, which should not happen. ' +
@@ -2087,18 +2115,17 @@ describe('ReactDOMInput', () => {
2115 await act(() => {
2116 root.render(stub);
2117 });
2090 - await expect(async () => {
2091 - await act(() => {
2092 - root.render(<input type="text" defaultValue="uncontrolled" />);
2093 - });
2094 - }).toErrorDev(
2118 + await act(() => {
2119 + root.render(<input type="text" defaultValue="uncontrolled" />);
2120 + });
2121 + assertConsoleErrorDev([
2122 'A component is changing a controlled input to be uncontrolled. ' +
2123 'This is likely caused by the value changing from a defined to ' +
2124 'undefined, which should not happen. ' +
2125 'Decide between using a controlled or uncontrolled input ' +
2126 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2127 ' in input (at **)',
2101 - );
2128 + ]);
2129 });
2130
2131 it('should warn if uncontrolled input (value is undefined) switches to controlled', async () => {
@@ -2106,42 +2133,40 @@ describe('ReactDOMInput', () => {
2133 await act(() => {
2134 root.render(stub);
2135 });
2109 - await expect(async () => {
2110 - await act(() => {
2111 - root.render(<input type="text" value="controlled" />);
2112 - });
2113 - }).toErrorDev(
2136 + await act(() => {
2137 + root.render(<input type="text" value="controlled" />);
2138 + });
2139 + assertConsoleErrorDev([
2140 'A component is changing an uncontrolled input to be controlled. ' +
2141 'This is likely caused by the value changing from undefined to ' +
2142 'a defined value, which should not happen. ' +
2143 'Decide between using a controlled or uncontrolled input ' +
2144 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2145 ' in input (at **)',
2120 - );
2146 + ]);
2147 });
2148
2149 it('should warn if uncontrolled input (value is null) switches to controlled', async () => {
2150 const stub = <input type="text" value={null} />;
2125 - await expect(async () => {
2126 - await act(() => {
2127 - root.render(stub);
2128 - });
2129 - }).toErrorDev(
2151 + await act(() => {
2152 + root.render(stub);
2153 + });
2154 + assertConsoleErrorDev([
2155 '`value` prop on `input` should not be null. ' +
2131 - 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.',
2132 - );
2133 - await expect(async () => {
2134 - await act(() => {
2135 - root.render(<input type="text" value="controlled" />);
2136 - });
2137 - }).toErrorDev(
2156 + 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' +
2157 + ' in input (at **)',
2158 + ]);
2159 + await act(() => {
2160 + root.render(<input type="text" value="controlled" />);
2161 + });
2162 + assertConsoleErrorDev([
2163 'A component is changing an uncontrolled input to be controlled. ' +
2164 'This is likely caused by the value changing from undefined to ' +
2165 'a defined value, which should not happen. ' +
2166 'Decide between using a controlled or uncontrolled input ' +
2167 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2168 ' in input (at **)',
2144 - );
2169 + ]);
2170 });
2171
2172 it('should warn if controlled checkbox switches to uncontrolled (checked is undefined)', async () => {
@@ -2151,18 +2176,17 @@ describe('ReactDOMInput', () => {
2176 await act(() => {
2177 root.render(stub);
2178 });
2154 - await expect(async () => {
2155 - await act(() => {
2156 - root.render(<input type="checkbox" />);
2157 - });
2158 - }).toErrorDev(
2179 + await act(() => {
2180 + root.render(<input type="checkbox" />);
2181 + });
2182 + assertConsoleErrorDev([
2183 'A component is changing a controlled input to be uncontrolled. ' +
2184 'This is likely caused by the value changing from a defined to ' +
2185 'undefined, which should not happen. ' +
2186 'Decide between using a controlled or uncontrolled input ' +
2187 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2188 ' in input (at **)',
2165 - );
2189 + ]);
2190 });
2191
2192 it('should warn if controlled checkbox switches to uncontrolled (checked is null)', async () => {
@@ -2172,18 +2196,17 @@ describe('ReactDOMInput', () => {
2196 await act(() => {
2197 root.render(stub);
2198 });
2175 - await expect(async () => {
2176 - await act(() => {
2177 - root.render(<input type="checkbox" checked={null} />);
2178 - });
2179 - }).toErrorDev(
2199 + await act(() => {
2200 + root.render(<input type="checkbox" checked={null} />);
2201 + });
2202 + assertConsoleErrorDev([
2203 'A component is changing a controlled input to be uncontrolled. ' +
2204 'This is likely caused by the value changing from a defined to ' +
2205 'undefined, which should not happen. ' +
2206 'Decide between using a controlled or uncontrolled input ' +
2207 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2208 ' in input (at **)',
2186 - );
2209 + ]);
2210 });
2211
2212 it('should warn if controlled checkbox switches to uncontrolled with defaultChecked', async () => {
@@ -2193,18 +2216,17 @@ describe('ReactDOMInput', () => {
2216 await act(() => {
2217 root.render(stub);
2218 });
2196 - await expect(async () => {
2197 - await act(() => {
2198 - root.render(<input type="checkbox" defaultChecked={true} />);
2199 - });
2200 - }).toErrorDev(
2219 + await act(() => {
2220 + root.render(<input type="checkbox" defaultChecked={true} />);
2221 + });
2222 + assertConsoleErrorDev([
2223 'A component is changing a controlled input to be uncontrolled. ' +
2224 'This is likely caused by the value changing from a defined to ' +
2225 'undefined, which should not happen. ' +
2226 'Decide between using a controlled or uncontrolled input ' +
2227 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2228 ' in input (at **)',
2207 - );
2229 + ]);
2230 });
2231
2232 it('should warn if uncontrolled checkbox (checked is undefined) switches to controlled', async () => {
@@ -2212,18 +2234,17 @@ describe('ReactDOMInput', () => {
2234 await act(() => {
2235 root.render(stub);
2236 });
2215 - await expect(async () => {
2216 - await act(() => {
2217 - root.render(<input type="checkbox" checked={true} />);
2218 - });
2219 - }).toErrorDev(
2237 + await act(() => {
2238 + root.render(<input type="checkbox" checked={true} />);
2239 + });
2240 + assertConsoleErrorDev([
2241 'A component is changing an uncontrolled input to be controlled. ' +
2242 'This is likely caused by the value changing from undefined to ' +
2243 'a defined value, which should not happen. ' +
2244 'Decide between using a controlled or uncontrolled input ' +
2245 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2246 ' in input (at **)',
2226 - );
2247 + ]);
2248 });
2249
2250 it('should warn if uncontrolled checkbox (checked is null) switches to controlled', async () => {
@@ -2231,18 +2252,17 @@ describe('ReactDOMInput', () => {
2252 await act(() => {
2253 root.render(stub);
2254 });
2234 - await expect(async () => {
2235 - await act(() => {
2236 - root.render(<input type="checkbox" checked={true} />);
2237 - });
2238 - }).toErrorDev(
2255 + await act(() => {
2256 + root.render(<input type="checkbox" checked={true} />);
2257 + });
2258 + assertConsoleErrorDev([
2259 'A component is changing an uncontrolled input to be controlled. ' +
2260 'This is likely caused by the value changing from undefined to ' +
2261 'a defined value, which should not happen. ' +
2262 'Decide between using a controlled or uncontrolled input ' +
2263 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2264 ' in input (at **)',
2245 - );
2265 + ]);
2266 });
2267
2268 it('should warn if controlled radio switches to uncontrolled (checked is undefined)', async () => {
@@ -2250,18 +2270,17 @@ describe('ReactDOMInput', () => {
2270 await act(() => {
2271 root.render(stub);
2272 });
2253 - await expect(async () => {
2254 - await act(() => {
2255 - root.render(<input type="radio" />);
2256 - });
2257 - }).toErrorDev(
2273 + await act(() => {
2274 + root.render(<input type="radio" />);
2275 + });
2276 + assertConsoleErrorDev([
2277 'A component is changing a controlled input to be uncontrolled. ' +
2278 'This is likely caused by the value changing from a defined to ' +
2279 'undefined, which should not happen. ' +
2280 'Decide between using a controlled or uncontrolled input ' +
2281 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2282 ' in input (at **)',
2264 - );
2283 + ]);
2284 });
2285
2286 it('should warn if controlled radio switches to uncontrolled (checked is null)', async () => {
@@ -2269,18 +2288,17 @@ describe('ReactDOMInput', () => {
2288 await act(() => {
2289 root.render(stub);
2290 });
2272 - await expect(async () => {
2273 - await act(() => {
2274 - root.render(<input type="radio" checked={null} />);
2275 - });
2276 - }).toErrorDev(
2291 + await act(() => {
2292 + root.render(<input type="radio" checked={null} />);
2293 + });
2294 + assertConsoleErrorDev([
2295 'A component is changing a controlled input to be uncontrolled. ' +
2296 'This is likely caused by the value changing from a defined to ' +
2297 'undefined, which should not happen. ' +
2298 'Decide between using a controlled or uncontrolled input ' +
2299 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2300 ' in input (at **)',
2283 - );
2301 + ]);
2302 });
2303
2304 it('should warn if controlled radio switches to uncontrolled with defaultChecked', async () => {
@@ -2288,18 +2306,17 @@ describe('ReactDOMInput', () => {
2306 await act(() => {
2307 root.render(stub);
2308 });
2291 - await expect(async () => {
2292 - await act(() => {
2293 - root.render(<input type="radio" defaultChecked={true} />);
2294 - });
2295 - }).toErrorDev(
2309 + await act(() => {
2310 + root.render(<input type="radio" defaultChecked={true} />);
2311 + });
2312 + assertConsoleErrorDev([
2313 'A component is changing a controlled input to be uncontrolled. ' +
2314 'This is likely caused by the value changing from a defined to ' +
2315 'undefined, which should not happen. ' +
2316 'Decide between using a controlled or uncontrolled input ' +
2317 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2318 ' in input (at **)',
2302 - );
2319 + ]);
2320 });
2321
2322 it('should warn if uncontrolled radio (checked is undefined) switches to controlled', async () => {
@@ -2307,18 +2324,17 @@ describe('ReactDOMInput', () => {
2324 await act(() => {
2325 root.render(stub);
2326 });
2310 - await expect(async () => {
2311 - await act(() => {
2312 - root.render(<input type="radio" checked={true} />);
2313 - });
2314 - }).toErrorDev(
2327 + await act(() => {
2328 + root.render(<input type="radio" checked={true} />);
2329 + });
2330 + assertConsoleErrorDev([
2331 'A component is changing an uncontrolled input to be controlled. ' +
2332 'This is likely caused by the value changing from undefined to ' +
2333 'a defined value, which should not happen. ' +
2334 'Decide between using a controlled or uncontrolled input ' +
2335 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2336 ' in input (at **)',
2321 - );
2337 + ]);
2338 });
2339
2340 it('should warn if uncontrolled radio (checked is null) switches to controlled', async () => {
@@ -2326,18 +2342,17 @@ describe('ReactDOMInput', () => {
2342 await act(() => {
2343 root.render(stub);
2344 });
2329 - await expect(async () => {
2330 - await act(() => {
2331 - root.render(<input type="radio" checked={true} />);
2332 - });
2333 - }).toErrorDev(
2345 + await act(() => {
2346 + root.render(<input type="radio" checked={true} />);
2347 + });
2348 + assertConsoleErrorDev([
2349 'A component is changing an uncontrolled input to be controlled. ' +
2350 'This is likely caused by the value changing from undefined to ' +
2351 'a defined value, which should not happen. ' +
2352 'Decide between using a controlled or uncontrolled input ' +
2353 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2354 ' in input (at **)',
2340 - );
2355 + ]);
2356 });
2357
2358 it('should not warn if radio value changes but never becomes controlled', async () => {
@@ -2390,18 +2405,17 @@ describe('ReactDOMInput', () => {
2405 />,
2406 );
2407 });
2393 - await expect(async () => {
2394 - await act(() => {
2395 - root.render(<input type="radio" value="value" />);
2396 - });
2397 - }).toErrorDev(
2408 + await act(() => {
2409 + root.render(<input type="radio" value="value" />);
2410 + });
2411 + assertConsoleErrorDev([
2412 'A component is changing a controlled input to be uncontrolled. ' +
2413 'This is likely caused by the value changing from a defined to ' +
2414 'undefined, which should not happen. ' +
2415 'Decide between using a controlled or uncontrolled input ' +
2416 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2417 ' in input (at **)',
2404 - );
2418 + ]);
2419 });
2420
2421 it('sets type, step, min, max before value always', async () => {
@@ -2747,9 +2761,15 @@ describe('ReactDOMInput', () => {
2761 }
2762
2763 it('reverts the value attribute to the initial value', async () => {
2750 - await expect(renderInputWithStringThenWithUndefined).toErrorDev(
2751 - 'A component is changing a controlled input to be uncontrolled.',
2752 - );
2764 + await renderInputWithStringThenWithUndefined();
2765 + assertConsoleErrorDev([
2766 + 'A component is changing a controlled input to be uncontrolled. ' +
2767 + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
2768 + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
2769 + 'More info: https://react.dev/link/controlled-components\n' +
2770 + ' in input (at **)\n' +
2771 + ' in Input (at **)',
2772 + ]);
2773 if (disableInputAttributeSyncing) {
2774 expect(input.getAttribute('value')).toBe(null);
2775 } else {
@@ -2758,9 +2778,15 @@ describe('ReactDOMInput', () => {
2778 });
2779
2780 it('preserves the value property', async () => {
2761 - await expect(renderInputWithStringThenWithUndefined).toErrorDev(
2762 - 'A component is changing a controlled input to be uncontrolled.',
2763 - );
2781 + await renderInputWithStringThenWithUndefined();
2782 + assertConsoleErrorDev([
2783 + 'A component is changing a controlled input to be uncontrolled. ' +
2784 + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
2785 + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
2786 + 'More info: https://react.dev/link/controlled-components\n' +
2787 + ' in input (at **)\n' +
2788 + ' in Input (at **)',
2789 + ]);
2790 expect(input.value).toBe('latest');
2791 });
2792 });
@@ -2798,11 +2824,19 @@ describe('ReactDOMInput', () => {
2824 }
2825
2826 it('reverts the value attribute to the initial value', async () => {
2801 - await expect(renderInputWithStringThenWithNull).toErrorDev([
2827 + await renderInputWithStringThenWithNull();
2828 + assertConsoleErrorDev([
2829 '`value` prop on `input` should not be null. ' +
2830 'Consider using an empty string to clear the component ' +
2804 - 'or `undefined` for uncontrolled components.',
2805 - 'A component is changing a controlled input to be uncontrolled.',
2831 + 'or `undefined` for uncontrolled components.\n' +
2832 + ' in input (at **)\n' +
2833 + ' in Input (at **)',
2834 + 'A component is changing a controlled input to be uncontrolled. ' +
2835 + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
2836 + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
2837 + 'More info: https://react.dev/link/controlled-components\n' +
2838 + ' in input (at **)\n' +
2839 + ' in Input (at **)',
2840 ]);
2841 if (disableInputAttributeSyncing) {
2842 expect(input.getAttribute('value')).toBe(null);
@@ -2812,11 +2846,19 @@ describe('ReactDOMInput', () => {
2846 });
2847
2848 it('preserves the value property', async () => {
2815 - await expect(renderInputWithStringThenWithNull).toErrorDev([
2849 + await renderInputWithStringThenWithNull();
2850 + assertConsoleErrorDev([
2851 '`value` prop on `input` should not be null. ' +
2852 'Consider using an empty string to clear the component ' +
2818 - 'or `undefined` for uncontrolled components.',
2819 - 'A component is changing a controlled input to be uncontrolled.',
2853 + 'or `undefined` for uncontrolled components.\n' +
2854 + ' in input (at **)\n' +
2855 + ' in Input (at **)',
2856 + 'A component is changing a controlled input to be uncontrolled. ' +
2857 + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
2858 + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
2859 + 'More info: https://react.dev/link/controlled-components\n' +
2860 + ' in input (at **)\n' +
2861 + ' in Input (at **)',
2862 ]);
2863 expect(input.value).toBe('latest');
2864 });
@@ -2824,11 +2866,15 @@ describe('ReactDOMInput', () => {
2866
2867 describe('When given a Symbol value', function () {
2868 it('treats initial Symbol value as an empty string', async () => {
2827 - await expect(async () => {
2828 - await act(() => {
2829 - root.render(<input value={Symbol('foobar')} onChange={() => {}} />);
2830 - });
2831 - }).toErrorDev('Invalid value for prop `value`');
2869 + await act(() => {
2870 + root.render(<input value={Symbol('foobar')} onChange={() => {}} />);
2871 + });
2872 + assertConsoleErrorDev([
2873 + 'Invalid value for prop `value` on <input> tag. ' +
2874 + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
2875 + 'For details, see https://react.dev/link/attribute-behavior \n' +
2876 + ' in input (at **)',
2877 + ]);
2878 const node = container.firstChild;
2879
2880 expect(node.value).toBe('');
@@ -2843,11 +2889,15 @@ describe('ReactDOMInput', () => {
2889 await act(() => {
2890 root.render(<input value="foo" onChange={() => {}} />);
2891 });
2846 - await expect(async () => {
2847 - await act(() => {
2848 - root.render(<input value={Symbol('foobar')} onChange={() => {}} />);
2849 - });
2850 - }).toErrorDev('Invalid value for prop `value`');
2892 + await act(() => {
2893 + root.render(<input value={Symbol('foobar')} onChange={() => {}} />);
2894 + });
2895 + assertConsoleErrorDev([
2896 + 'Invalid value for prop `value` on <input> tag. ' +
2897 + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
2898 + 'For details, see https://react.dev/link/attribute-behavior \n' +
2899 + ' in input (at **)',
2900 + ]);
2901 const node = container.firstChild;
2902
2903 expect(node.value).toBe('');
@@ -2890,11 +2940,15 @@ describe('ReactDOMInput', () => {
2940
2941 describe('When given a function value', function () {
2942 it('treats initial function value as an empty string', async () => {
2893 - await expect(async () => {
2894 - await act(() => {
2895 - root.render(<input value={() => {}} onChange={() => {}} />);
2896 - });
2897 - }).toErrorDev('Invalid value for prop `value`');
2943 + await act(() => {
2944 + root.render(<input value={() => {}} onChange={() => {}} />);
2945 + });
2946 + assertConsoleErrorDev([
2947 + 'Invalid value for prop `value` on <input> tag. ' +
2948 + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
2949 + 'For details, see https://react.dev/link/attribute-behavior \n' +
2950 + ' in input (at **)',
2951 + ]);
2952 const node = container.firstChild;
2953
2954 expect(node.value).toBe('');
@@ -2909,11 +2963,15 @@ describe('ReactDOMInput', () => {
2963 await act(() => {
2964 root.render(<input value="foo" onChange={() => {}} />);
2965 });
2912 - await expect(async () => {
2913 - await act(() => {
2914 - root.render(<input value={() => {}} onChange={() => {}} />);
2915 - });
2916 - }).toErrorDev('Invalid value for prop `value`');
2966 + await act(() => {
2967 + root.render(<input value={() => {}} onChange={() => {}} />);
2968 + });
2969 + assertConsoleErrorDev([
2970 + 'Invalid value for prop `value` on <input> tag. ' +
2971 + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
2972 + 'For details, see https://react.dev/link/attribute-behavior \n' +
2973 + ' in input (at **)',
2974 + ]);
2975 const node = container.firstChild;
2976
2977 expect(node.value).toBe('');