diff --git a/packages/react-dom/src/__tests__/ReactDOMComponentTree-test.js b/packages/react-dom/src/__tests__/ReactDOMComponentTree-test.js index e0b2b741dd6c0..4dec7df2faaf5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponentTree-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponentTree-test.js @@ -14,11 +14,14 @@ describe('ReactDOMComponentTree', () => { let ReactDOMClient; let act; let container; + let assertConsoleErrorDev; beforeEach(() => { React = require('react'); ReactDOMClient = require('react-dom/client'); act = require('internal-test-utils').act; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; container = document.createElement('div'); document.body.appendChild(container); @@ -190,18 +193,18 @@ describe('ReactDOMComponentTree', () => { root.render(); }); - await expect( - async () => - await act(() => { - simulateInput(inputRef.current, finishValue); - }), - ).toErrorDev( + await act(() => { + simulateInput(inputRef.current, finishValue); + }); + assertConsoleErrorDev([ 'A component is changing an uncontrolled input to be controlled. ' + 'This is likely caused by the value changing from undefined to ' + 'a defined value, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: ' + - 'https://react.dev/link/controlled-components', - ); + 'https://react.dev/link/controlled-components\n' + + ' in input (at **)\n' + + ' in Controlled (at **)', + ]); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js index cf0526fd61bf0..9784e4f849897 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js @@ -746,8 +746,13 @@ describe('ReactDOMFiber', () => { root.render(); }); assertConsoleErrorDev([ - 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Parent uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Parent (at **)', + 'Component uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate('enableOwnerStacks') ? '' : ' in Component (at **)\n') + + ' in Parent (at **)', ]); expect(container.innerHTML).toBe(''); expect(portalContainer.innerHTML).toBe('
bar
'); @@ -957,15 +962,14 @@ describe('ReactDOMFiber', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' + ' in div (at **)\n' + ' in Example (at **)', - ); + ]); }); it('should warn with a special message for `false` event listeners', () => { @@ -974,17 +978,16 @@ describe('ReactDOMFiber', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Expected `onClick` listener to be a function, instead got `false`.\n\n' + 'If you used to conditionally omit it with onClick={condition && value}, ' + 'pass onClick={condition ? value : undefined} instead.\n' + ' in div (at **)\n' + ' in Example (at **)', - ); + ]); }); it('should not update event handlers until commit', async () => { diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js index 027099d54707c..47ae48eb767bd 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js @@ -19,6 +19,7 @@ let waitForAll; let waitFor; let waitForMicrotasks; let assertLog; +let assertConsoleErrorDev; const setUntrackedInputValue = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, @@ -34,6 +35,8 @@ describe('ReactDOMFiberAsync', () => { ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); act = require('internal-test-utils').act; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); @@ -176,11 +179,15 @@ describe('ReactDOMFiberAsync', () => { root.render(); }); // Update - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev('flushSync was called from inside a lifecycle method'); + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'flushSync was called from inside a lifecycle method. ' + + 'React cannot flush when React is already rendering. ' + + 'Consider moving this call to a scheduler task or micro task.\n' + + ' in Component (at **)', + ]); }); describe('concurrent mode', () => { diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index 2300f4563c4b6..5b88ac54c794b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js @@ -26,6 +26,7 @@ let useFormStatus; let useOptimistic; let useActionState; let Scheduler; +let assertConsoleErrorDev; describe('ReactDOMFizzForm', () => { beforeEach(() => { @@ -38,6 +39,8 @@ describe('ReactDOMFizzForm', () => { useFormStatus = require('react-dom').useFormStatus; useOptimistic = require('react').useOptimistic; act = require('internal-test-utils').act; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; container = document.createElement('div'); document.body.appendChild(container); // TODO: Test the old api but it warns so needs warnings to be asserted. @@ -195,12 +198,26 @@ describe('ReactDOMFizzForm', () => { ReactDOMServer.renderToReadableStream(), ); await readIntoContainer(stream); - await expect(async () => { - await act(async () => { - ReactDOMClient.hydrateRoot(container, ); - }); - }).toErrorDev( - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + await act(async () => { + ReactDOMClient.hydrateRoot(container, ); + }); + assertConsoleErrorDev( + [ + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " + + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" + + "- A server/client branch `if (typeof window !== 'undefined')`.\n" + + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" + + "- Date formatting in a user's locale which doesn't match the server.\n" + + '- External changing data without sending a snapshot of it along with the HTML.\n' + + '- Invalid HTML tag nesting.\n\n' + + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' + + 'https://react.dev/link/hydration-mismatch\n\n' + + ' \n' + + ' \n', + ], {withoutStack: true}, ); }); @@ -357,23 +374,56 @@ describe('ReactDOMFizzForm', () => { // Specifying the extra form fields are a DEV error, but we expect it // to eventually still be patched up after an update. - await expect(async () => { - const stream = await serverAct(() => - ReactDOMServer.renderToReadableStream(), - ); - await readIntoContainer(stream); - }).toErrorDev([ - 'Cannot specify a encType or method for a form that specifies a function as the action.', - 'Cannot specify a formTarget for a button that specifies a function as a formAction.', + const stream = await serverAct(() => + ReactDOMServer.renderToReadableStream(), + ); + await readIntoContainer(stream); + assertConsoleErrorDev([ + 'Cannot specify a encType or method for a form that specifies a function as the action. ' + + 'React provides those automatically. They will get overridden.\n' + + ' in form (at **)\n' + + ' in App (at **)', + 'Cannot specify a formTarget for a button that specifies a function as a formAction. ' + + 'The function will always be executed in the same window.\n' + + ' in input (at **)\n' + + (gate('enableOwnerStacks') ? '' : ' in form (at **)\n') + + ' in App (at **)', ]); let root; - await expect(async () => { - await act(async () => { - root = ReactDOMClient.hydrateRoot(container, ); - }); - }).toErrorDev( + await act(async () => { + root = ReactDOMClient.hydrateRoot(container, ); + }); + assertConsoleErrorDev( [ - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " + + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" + + "- A server/client branch `if (typeof window !== 'undefined')`.\n" + + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" + + "- Date formatting in a user's locale which doesn't match the server.\n" + + '- External changing data without sending a snapshot of it along with the HTML.\n' + + '- Invalid HTML tag nesting.\n\n' + + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' + + 'https://react.dev/link/hydration-mismatch\n\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n', ], {withoutStack: true}, ); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 777f067a183e8..058473b96c7e1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -1925,8 +1925,18 @@ describe('ReactDOMFizzServer', () => { pipe(writable); }); assertConsoleErrorDev([ - 'TestProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'TestConsumer uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'TestProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in TestProvider (at **)', + 'TestConsumer uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in TestConsumer (at **)' + + (gate('enableOwnerStacks') + ? '' + : '\n in TestProvider (at **)' + + '\n in Suspense (at **)' + + '\n in div (at **)' + + '\n in TestProvider (at **)'), ]); expect(getVisibleChildren(container)).toEqual(
@@ -3506,13 +3516,14 @@ describe('ReactDOMFizzServer', () => {
, { onRecoverableError(error, errorInfo) { - expect(() => { - expect(error.digest).toBe('a digest'); - expect(errorInfo.digest).toBe(undefined); - }).toErrorDev( - 'You are accessing "digest" from the errorInfo object passed to onRecoverableError.' + - ' This property is no longer provided as part of errorInfo but can be accessed as a property' + - ' of the Error instance itself.', + expect(error.digest).toBe('a digest'); + expect(errorInfo.digest).toBe(undefined); + assertConsoleErrorDev( + [ + 'You are accessing "digest" from the errorInfo object passed to onRecoverableError.' + + ' This property is no longer provided as part of errorInfo but can be accessed as a property' + + ' of the Error instance itself.', + ], {withoutStack: true}, ); }, @@ -5777,13 +5788,24 @@ describe('ReactDOMFizzServer', () => { ); } - await expect(async () => { - await act(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); - }).toErrorDev([ - 'React expects the `children` prop of 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}. 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: {`hello ${nameOfUser}`}.', + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); + assertConsoleErrorDev([ + 'React expects the `children` prop of 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}. ' + + '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: ' + + '{`hello ${nameOfUser}`}.\n' + + ' in title (at **)\n' + + (gate('enableOwnerStacks') ? '' : ' in head (at **)\n') + + ' in App (at **)', ]); expect(getVisibleChildren(document.head)).toEqual(); @@ -5814,13 +5836,22 @@ describe('ReactDOMFizzServer', () => { ); } - await expect(async () => { - await act(() => { - const {pipe} = renderToPipeableStream(<App />); - pipe(writable); - }); - }).toErrorDev([ - '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.', + await act(() => { + const {pipe} = renderToPipeableStream(<App />); + pipe(writable); + }); + assertConsoleErrorDev([ + '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.\n' + + ' in title (at **)\n' + + (gate('enableOwnerStacks') ? '' : ' in head (at **)\n') + + ' in App (at **)', ]); // object titles are toStringed when float is on expect(getVisibleChildren(document.head)).toEqual( @@ -5849,13 +5880,22 @@ describe('ReactDOMFizzServer', () => { ); } - await expect(async () => { - await act(() => { - const {pipe} = renderToPipeableStream(<App />); - pipe(writable); - }); - }).toErrorDev([ - '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>.', + await act(() => { + const {pipe} = renderToPipeableStream(<App />); + pipe(writable); + }); + assertConsoleErrorDev([ + '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>.\n' + + ' in title (at **)\n' + + (gate('enableOwnerStacks') ? '' : ' in head (at **)\n') + + ' in App (at **)', ]); // object titles are toStringed when float is on expect(getVisibleChildren(document.head)).toEqual( @@ -8381,12 +8421,11 @@ describe('ReactDOMFizzServer', () => { return <div>{children}</div>; } - await expect(async () => { - await act(() => { - const {pipe} = renderToPipeableStream(<Foo />); - pipe(writable); - }); - }).toErrorDev( + await act(() => { + const {pipe} = renderToPipeableStream(<Foo />); + pipe(writable); + }); + assertConsoleErrorDev([ 'Using Iterators as children is unsupported and will likely yield ' + 'unexpected results because enumerating a generator mutates it. ' + 'You may convert it to an array with `Array.from()` or the ' + @@ -8394,7 +8433,7 @@ describe('ReactDOMFizzServer', () => { 'Iterable that can iterate multiple times over the same items.\n' + ' in div (at **)\n' + ' in Foo (at **)', - ); + ]); expect(document.body.textContent).toBe('HelloWorld'); }); @@ -8420,19 +8459,18 @@ describe('ReactDOMFizzServer', () => { return iterator; } - await expect(async () => { - await act(() => { - const {pipe} = renderToPipeableStream(<Foo />); - pipe(writable); - }); - }).toErrorDev( + await act(() => { + const {pipe} = renderToPipeableStream(<Foo />); + pipe(writable); + }); + assertConsoleErrorDev([ 'Using Iterators as children is unsupported and will likely yield ' + 'unexpected results because enumerating a generator mutates it. ' + 'You may convert it to an array with `Array.from()` or the ' + '`[...spread]` operator before rendering. You can also use an ' + 'Iterable that can iterate multiple times over the same items.\n' + ' in Foo (at **)', - ); + ]); expect(document.body.textContent).toBe('HelloWorld'); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index 7185cc5cad152..9fdfb3acbaaf9 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -361,7 +361,7 @@ describe('ReactDOMForm', () => { expect(actionCalled).toBe(false); }); - it('should only submit the inner of nested forms', async () => { + it('should submit the inner of nested forms', async () => { const ref = React.createRef(); let data; @@ -373,19 +373,18 @@ describe('ReactDOMForm', () => { } const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(async () => { - // This isn't valid HTML but just in case. - root.render( - <form action={outerAction}> - <input type="text" name="data" defaultValue="outer" /> - <form action={innerAction} ref={ref}> - <input type="text" name="data" defaultValue="inner" /> - </form> - </form>, - ); - }); - }).toErrorDev( + await act(async () => { + // This isn't valid HTML but just in case. + root.render( + <form action={outerAction}> + <input type="text" name="data" defaultValue="outer" /> + <form action={innerAction} ref={ref}> + <input type="text" name="data" defaultValue="inner" /> + </form> + </form>, + ); + }); + assertConsoleErrorDev([ 'In HTML, <form> cannot be a descendant of <form>.\n' + 'This will cause a hydration error.\n' + '\n' + @@ -394,14 +393,14 @@ describe('ReactDOMForm', () => { '> <form action={function innerAction} ref={{current:null}}>\n' + '\n in form (at **)' + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in form (at **)'), - ); + ]); await submit(ref.current); expect(data).toBe('innerinner'); }); - it('should only submit once if one root is nested inside the other', async () => { + it('should submit once if one root is nested inside the other', async () => { const ref = React.createRef(); let outerCalled = 0; let innerCalled = 0; @@ -444,7 +443,7 @@ describe('ReactDOMForm', () => { expect(innerCalled).toBe(1); }); - it('should only submit once if a portal is nested inside its own root', async () => { + it('should submit once if a portal is nested inside its own root', async () => { const ref = React.createRef(); let outerCalled = 0; let innerCalled = 0; @@ -565,29 +564,32 @@ describe('ReactDOMForm', () => { } const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(async () => { - root.render( - <form> - <input - type="submit" - name="button" - value="delete" - ref={inputRef} - formAction={action} - /> - <button - name="button" - value="edit" - ref={buttonRef} - formAction={action}> - Edit - </button> - </form>, - ); - }); - }).toErrorDev([ - 'Cannot specify a "name" prop for a button that specifies a function as a formAction.', + await act(async () => { + root.render( + <form> + <input + type="submit" + name="button" + value="delete" + ref={inputRef} + formAction={action} + /> + <button + name="button" + value="edit" + ref={buttonRef} + formAction={action}> + Edit + </button> + </form>, + ); + }); + assertConsoleErrorDev([ + 'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' + + 'React needs it to encode which action should be invoked. ' + + 'It will get overridden.\n' + + ' in input (at **)' + + (gate('enableOwnerStacks') ? '' : '\n in form (at **)'), ]); await submit(inputRef.current); @@ -1492,8 +1494,9 @@ describe('ReactDOMForm', () => { await act(() => dispatch()); assertConsoleErrorDev([ [ - 'An async function was passed to useActionState, but it was ' + - 'dispatched outside of an action context', + 'An async function was passed to useActionState, but it was dispatched outside of an action context. ' + + 'This is likely not what you intended. ' + + 'Either pass the dispatch function to an `action` prop, or dispatch manually inside `startTransition`', {withoutStack: true}, ], ]); @@ -1919,11 +1922,16 @@ describe('ReactDOMForm', () => { expect(inputRef.current.value).toBe(' Updated '); // This triggers a synchronous requestFormReset, and a warning - await expect(async () => { - await act(() => resolveText('Wait 1')); - }).toErrorDev(['requestFormReset was called outside a transition'], { - withoutStack: true, - }); + await act(() => resolveText('Wait 1')); + assertConsoleErrorDev( + [ + 'requestFormReset was called outside a transition or action. ' + + 'To fix, move to an action, or wrap with startTransition.', + ], + { + withoutStack: true, + }, + ); assertLog(['Request form reset']); // The form was reset even though the action didn't finish. @@ -1957,7 +1965,14 @@ describe('ReactDOMForm', () => { // Symbols are coerced to null, so this should fire the form action await act(() => root.render(<App submitterAction={Symbol()} />)); - assertConsoleErrorDev(['Invalid value for prop `formAction`']); + assertConsoleErrorDev([ + 'Invalid value for prop `formAction` on <button> tag. ' + + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' + + 'For details, see https://react.dev/link/attribute-behavior \n' + + ' in button (at **)\n' + + (gate('enableOwnerStacks') ? '' : ' in form (at **)\n') + + ' in App (at **)', + ]); await submit(buttonRef.current); assertLog(['Form action']); @@ -2201,7 +2216,13 @@ describe('ReactDOMForm', () => { // Symbols are coerced to null await act(() => root.render(<Form action={Symbol()} />)); - assertConsoleErrorDev(['Invalid value for prop `action`']); + assertConsoleErrorDev([ + 'Invalid value for prop `action` on <form> tag. ' + + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' + + 'For details, see https://react.dev/link/attribute-behavior \n' + + ' in form (at **)\n' + + ' in Form (at **)', + ]); await submit(formRef.current); assertLog([null]); diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js index 80ded083d085e..5b47095d7c755 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js @@ -26,6 +26,7 @@ describe('ReactDOMInput', () => { let setUntrackedChecked; let container; let root; + let assertConsoleErrorDev; function dispatchEventOnNode(node, type) { node.dispatchEvent(new Event(type, {bubbles: true, cancelable: true})); @@ -96,6 +97,8 @@ describe('ReactDOMInput', () => { ReactDOMServer = require('react-dom/server'); Scheduler = require('scheduler'); act = require('internal-test-utils').act; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; assertLog = require('internal-test-utils').assertLog; container = document.createElement('div'); @@ -109,52 +112,54 @@ describe('ReactDOMInput', () => { }); it('should warn for controlled value of 0 with missing onChange', async () => { - await expect(async () => { - await act(() => { - root.render(<input type="text" value={0} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="text" value={0} />); + }); + assertConsoleErrorDev([ 'You provided a `value` prop to a form ' + 'field without an `onChange` handler. This will render a read-only ' + 'field. If the field should be mutable use `defaultValue`. ' + - 'Otherwise, set either `onChange` or `readOnly`.', - ); + 'Otherwise, set either `onChange` or `readOnly`.\n' + + ' in input (at **)', + ]); }); it('should warn for controlled value of "" with missing onChange', async () => { - await expect(async () => { - await act(() => { - root.render(<input type="text" value="" />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="text" value="" />); + }); + assertConsoleErrorDev([ 'You provided a `value` prop to a form ' + 'field without an `onChange` handler. This will render a read-only ' + 'field. If the field should be mutable use `defaultValue`. ' + - 'Otherwise, set either `onChange` or `readOnly`.', - ); + 'Otherwise, set either `onChange` or `readOnly`.\n' + + ' in input (at **)', + ]); }); it('should warn for controlled value of "0" with missing onChange', async () => { - await expect(async () => { - await act(() => { - root.render(<input type="text" value="0" />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="text" value="0" />); + }); + assertConsoleErrorDev([ 'You provided a `value` prop to a form ' + 'field without an `onChange` handler. This will render a read-only ' + 'field. If the field should be mutable use `defaultValue`. ' + - 'Otherwise, set either `onChange` or `readOnly`.', - ); + 'Otherwise, set either `onChange` or `readOnly`.\n' + + ' in input (at **)', + ]); }); it('should warn for controlled value of false with missing onChange', async () => { - await expect(async () => { - await act(() => { - root.render(<input type="checkbox" checked={false} />); - }); - }).toErrorDev( - 'You provided a `checked` prop to a form field without an `onChange` handler.', - ); + await act(() => { + root.render(<input type="checkbox" checked={false} />); + }); + assertConsoleErrorDev([ + 'You provided a `checked` prop to a form field without an `onChange` handler. ' + + 'This will render a read-only field. If the field should be mutable use `defaultChecked`. ' + + 'Otherwise, set either `onChange` or `readOnly`.\n' + + ' in input (at **)', + ]); }); it('should warn with checked and no onChange handler with readOnly specified', async () => { @@ -164,15 +169,15 @@ describe('ReactDOMInput', () => { root.unmount(); root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(<input type="checkbox" checked={false} readOnly={false} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="checkbox" checked={false} readOnly={false} />); + }); + assertConsoleErrorDev([ 'You provided a `checked` prop to a form field without an `onChange` handler. ' + 'This will render a read-only field. If the field should be mutable use `defaultChecked`. ' + - 'Otherwise, set either `onChange` or `readOnly`.', - ); + 'Otherwise, set either `onChange` or `readOnly`.\n' + + ' in input (at **)', + ]); }); it('should not warn about missing onChange in uncontrolled inputs', async () => { @@ -213,13 +218,15 @@ describe('ReactDOMInput', () => { }); it('should properly control a value even if no event listener exists', async () => { - await expect(async () => { - await act(() => { - root.render(<input type="text" value="lion" />); - }); - }).toErrorDev( - 'You provided a `value` prop to a form field without an `onChange` handler.', - ); + await act(() => { + root.render(<input type="text" value="lion" />); + }); + assertConsoleErrorDev([ + 'You provided a `value` prop to a form field without an `onChange` handler. ' + + 'This will render a read-only field. If the field should be mutable use `defaultValue`. ' + + 'Otherwise, set either `onChange` or `readOnly`.\n' + + ' in input (at **)', + ]); const node = container.firstChild; expect(isValueDirty(node)).toBe(true); @@ -426,14 +433,16 @@ describe('ReactDOMInput', () => { } const ref = React.createRef(); - await expect(async () => { - await act(() => { - root.render(<Stub ref={ref} />); - }); - }).toErrorDev( - 'You provided a `value` prop to a form field ' + - 'without an `onChange` handler.', - ); + await act(() => { + root.render(<Stub ref={ref} />); + }); + assertConsoleErrorDev([ + 'You provided a `value` prop to a form field without an `onChange` handler. ' + + 'This will render a read-only field. If the field should be mutable use `defaultValue`. ' + + 'Otherwise, set either `onChange` or `readOnly`.\n' + + ' in input (at **)\n' + + ' in Stub (at **)', + ]); const node = container.firstChild; await act(() => { ref.current.setState({value: '0.98'}); @@ -499,14 +508,16 @@ describe('ReactDOMInput', () => { } const ref = React.createRef(); - await expect(async () => { - await act(() => { - root.render(<Stub ref={ref} />); - }); - }).toErrorDev( - 'You provided a `value` prop to a form field ' + - 'without an `onChange` handler.', - ); + await act(() => { + root.render(<Stub ref={ref} />); + }); + assertConsoleErrorDev([ + 'You provided a `value` prop to a form field without an `onChange` handler. ' + + 'This will render a read-only field. If the field should be mutable use `defaultValue`. ' + + 'Otherwise, set either `onChange` or `readOnly`.\n' + + ' in input (at **)\n' + + ' in Stub (at **)', + ]); const node = container.firstChild; await act(() => { ref.current.setState({value: '3'}); @@ -636,13 +647,16 @@ describe('ReactDOMInput', () => { const node = container.firstChild; expect(node.value).toBe('0'); expect(isValueDirty(node)).toBe(true); - await expect(async () => { - await act(() => { - root.render(<input type="text" defaultValue="1" />); - }); - }).toErrorDev( - 'A component is changing a controlled input to be uncontrolled.', - ); + await act(() => { + root.render(<input type="text" defaultValue="1" />); + }); + assertConsoleErrorDev([ + 'A component is changing a controlled input to be uncontrolled. ' + + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' + + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' + + 'More info: https://react.dev/link/controlled-components\n' + + ' in input (at **)', + ]); expect(node.value).toBe('0'); expect(isValueDirty(node)).toBe(true); }); @@ -745,15 +759,18 @@ describe('ReactDOMInput', () => { } } await expect(async () => { - await expect(async () => { - await act(() => { - root.render(<input defaultValue={new TemporalLike()} type="date" />); - }); - }).toErrorDev( - 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + - 'strings, not TemporalLike. This value must be coerced to a string before using it here.', - ); + await act(() => { + root.render(<input defaultValue={new TemporalLike()} type="date" />); + }); }).rejects.toThrowError(new TypeError('prod message')); + assertConsoleErrorDev([ + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' + + ' in input (at **)', + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' + + ' in input (at **)', + ]); }); it('should throw for text inputs if `defaultValue` is an object where valueOf() throws', async () => { @@ -768,15 +785,18 @@ describe('ReactDOMInput', () => { } } await expect(async () => { - await expect(async () => { - await act(() => { - root.render(<input defaultValue={new TemporalLike()} type="text" />); - }); - }).toErrorDev( - 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + - 'strings, not TemporalLike. This value must be coerced to a string before using it here.', - ); + await act(() => { + root.render(<input defaultValue={new TemporalLike()} type="text" />); + }); }).rejects.toThrowError(new TypeError('prod message')); + assertConsoleErrorDev([ + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' + + ' in input (at **)', + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' + + ' in input (at **)', + ]); }); it('should throw for date inputs if `value` is an object where valueOf() throws', async () => { @@ -791,21 +811,20 @@ describe('ReactDOMInput', () => { } } await expect(async () => { - await expect(async () => { - await act(() => { - root.render( - <input - value={new TemporalLike()} - type="date" - onChange={() => {}} - />, - ); - }); - }).toErrorDev( - 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + - 'strings, not TemporalLike. This value must be coerced to a string before using it here.', - ); + await act(() => { + root.render( + <input value={new TemporalLike()} type="date" onChange={() => {}} />, + ); + }); }).rejects.toThrowError(new TypeError('prod message')); + assertConsoleErrorDev([ + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' + + ' in input (at **)', + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' + + ' in input (at **)', + ]); }); it('should throw for text inputs if `value` is an object where valueOf() throws', async () => { @@ -820,21 +839,20 @@ describe('ReactDOMInput', () => { } } await expect(async () => { - await expect(async () => { - await act(() => { - root.render( - <input - value={new TemporalLike()} - type="text" - onChange={() => {}} - />, - ); - }); - }).toErrorDev( - 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + - 'strings, not TemporalLike. This value must be coerced to a string before using it here.', - ); + await act(() => { + root.render( + <input value={new TemporalLike()} type="text" onChange={() => {}} />, + ); + }); }).rejects.toThrowError(new TypeError('prod message')); + assertConsoleErrorDev([ + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' + + ' in input (at **)', + 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' + + 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' + + ' in input (at **)', + ]); }); it('should display `value` of number 0', async () => { @@ -1199,15 +1217,18 @@ describe('ReactDOMInput', () => { // Not really relevant to this particular test, but changing to undefined // should nonetheless trigger a warning - await expect(async () => { - await act(() => { - root.render( - <input type="submit" value={undefined} onChange={emptyFunction} />, - ); - }); - }).toErrorDev( - 'A component is changing a controlled input to be uncontrolled.', - ); + await act(() => { + root.render( + <input type="submit" value={undefined} onChange={emptyFunction} />, + ); + }); + assertConsoleErrorDev([ + 'A component is changing a controlled input to be uncontrolled. ' + + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' + + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' + + 'More info: https://react.dev/link/controlled-components\n' + + ' in input (at **)', + ]); const node = container.firstChild; expect(node.getAttribute('value')).toBe(null); @@ -1221,15 +1242,18 @@ describe('ReactDOMInput', () => { // Not really relevant to this particular test, but changing to undefined // should nonetheless trigger a warning - await expect(async () => { - await act(() => { - root.render( - <input type="reset" value={undefined} onChange={emptyFunction} />, - ); - }); - }).toErrorDev( - 'A component is changing a controlled input to be uncontrolled.', - ); + await act(() => { + root.render( + <input type="reset" value={undefined} onChange={emptyFunction} />, + ); + }); + assertConsoleErrorDev([ + 'A component is changing a controlled input to be uncontrolled. ' + + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' + + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' + + 'More info: https://react.dev/link/controlled-components\n' + + ' in input (at **)', + ]); const node = container.firstChild; expect(node.getAttribute('value')).toBe(null); @@ -1281,11 +1305,14 @@ describe('ReactDOMInput', () => { it('should not set a null value on a submit input', async () => { const stub = <input type="submit" value={null} />; - await expect(async () => { - await act(() => { - root.render(stub); - }); - }).toErrorDev('`value` prop on `input` should not be null'); + await act(() => { + root.render(stub); + }); + assertConsoleErrorDev([ + '`value` prop on `input` should not be null. ' + + 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' + + ' in input (at **)', + ]); const node = container.firstChild; // Note: it shouldn't be an empty string @@ -1300,11 +1327,14 @@ describe('ReactDOMInput', () => { it('should not set a null value on a reset input', async () => { const stub = <input type="reset" value={null} />; - await expect(async () => { - await act(() => { - root.render(stub); - }); - }).toErrorDev('`value` prop on `input` should not be null'); + await act(() => { + root.render(stub); + }); + assertConsoleErrorDev([ + '`value` prop on `input` should not be null. ' + + 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' + + ' in input (at **)', + ]); const node = container.firstChild; // Note: it shouldn't be an empty string @@ -1906,17 +1936,16 @@ describe('ReactDOMInput', () => { root.unmount(); root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(<input type="text" value="zoink" readOnly={false} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="text" value="zoink" readOnly={false} />); + }); + assertConsoleErrorDev([ 'You provided a `value` prop to a form ' + 'field without an `onChange` handler. This will render a read-only ' + 'field. If the field should be mutable use `defaultValue`. ' + 'Otherwise, set either `onChange` or `readOnly`.\n' + ' in input (at **)', - ); + ]); }); it('should have a this value of undefined if bind is not used', async () => { @@ -1958,15 +1987,15 @@ describe('ReactDOMInput', () => { }); it('should warn if value is null', async () => { - await expect(async () => { - await act(() => { - root.render(<input type="text" value={null} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="text" value={null} />); + }); + assertConsoleErrorDev([ '`value` prop on `input` should not be null. ' + 'Consider using an empty string to clear the component or `undefined` ' + - 'for uncontrolled components.', - ); + 'for uncontrolled components.\n' + + ' in input (at **)', + ]); root.unmount(); root = ReactDOMClient.createRoot(container); @@ -1976,25 +2005,25 @@ describe('ReactDOMInput', () => { }); it('should warn if checked and defaultChecked props are specified', async () => { - await expect(async () => { - await act(() => { - root.render( - <input - type="radio" - checked={true} - defaultChecked={true} - readOnly={true} - />, - ); - }); - }).toErrorDev( + await act(() => { + root.render( + <input + type="radio" + checked={true} + defaultChecked={true} + readOnly={true} + />, + ); + }); + assertConsoleErrorDev([ 'A component contains an input of type radio with both checked and defaultChecked props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the checked prop, or the defaultChecked prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + - 'https://react.dev/link/controlled-components', - ); + 'https://react.dev/link/controlled-components\n' + + ' in input (at **)', + ]); root.unmount(); root = ReactDOMClient.createRoot(container); @@ -2011,20 +2040,20 @@ describe('ReactDOMInput', () => { }); it('should warn if value and defaultValue props are specified', async () => { - await expect(async () => { - await act(() => { - root.render( - <input type="text" value="foo" defaultValue="bar" readOnly={true} />, - ); - }); - }).toErrorDev( + await act(() => { + root.render( + <input type="text" value="foo" defaultValue="bar" readOnly={true} />, + ); + }); + assertConsoleErrorDev([ 'A component contains an input of type text with both value and defaultValue props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the value prop, or the defaultValue prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + - 'https://react.dev/link/controlled-components', - ); + 'https://react.dev/link/controlled-components\n' + + ' in input (at **)', + ]); await (() => { root.unmount(); }); @@ -2043,18 +2072,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="text" />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="text" />); + }); + assertConsoleErrorDev([ 'A component is changing a controlled input to be uncontrolled. ' + 'This is likely caused by the value changing from a defined to ' + 'undefined, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if controlled input switches to uncontrolled (value is null)', async () => { @@ -2064,13 +2092,13 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="text" value={null} />); - }); - }).toErrorDev([ + await act(() => { + root.render(<input type="text" value={null} />); + }); + assertConsoleErrorDev([ '`value` prop on `input` should not be null. ' + - 'Consider using an empty string to clear the component or `undefined` for uncontrolled components', + 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' + + ' in input (at **)', 'A component is changing a controlled input to be uncontrolled. ' + 'This is likely caused by the value changing from a defined to ' + 'undefined, which should not happen. ' + @@ -2087,18 +2115,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="text" defaultValue="uncontrolled" />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="text" defaultValue="uncontrolled" />); + }); + assertConsoleErrorDev([ 'A component is changing a controlled input to be uncontrolled. ' + 'This is likely caused by the value changing from a defined to ' + 'undefined, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if uncontrolled input (value is undefined) switches to controlled', async () => { @@ -2106,42 +2133,40 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="text" value="controlled" />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="text" value="controlled" />); + }); + assertConsoleErrorDev([ 'A component is changing an uncontrolled input to be controlled. ' + 'This is likely caused by the value changing from undefined to ' + 'a defined value, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if uncontrolled input (value is null) switches to controlled', async () => { const stub = <input type="text" value={null} />; - await expect(async () => { - await act(() => { - root.render(stub); - }); - }).toErrorDev( + await act(() => { + root.render(stub); + }); + assertConsoleErrorDev([ '`value` prop on `input` should not be null. ' + - 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.', - ); - await expect(async () => { - await act(() => { - root.render(<input type="text" value="controlled" />); - }); - }).toErrorDev( + 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' + + ' in input (at **)', + ]); + await act(() => { + root.render(<input type="text" value="controlled" />); + }); + assertConsoleErrorDev([ 'A component is changing an uncontrolled input to be controlled. ' + 'This is likely caused by the value changing from undefined to ' + 'a defined value, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if controlled checkbox switches to uncontrolled (checked is undefined)', async () => { @@ -2151,18 +2176,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="checkbox" />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="checkbox" />); + }); + assertConsoleErrorDev([ 'A component is changing a controlled input to be uncontrolled. ' + 'This is likely caused by the value changing from a defined to ' + 'undefined, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if controlled checkbox switches to uncontrolled (checked is null)', async () => { @@ -2172,18 +2196,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="checkbox" checked={null} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="checkbox" checked={null} />); + }); + assertConsoleErrorDev([ 'A component is changing a controlled input to be uncontrolled. ' + 'This is likely caused by the value changing from a defined to ' + 'undefined, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if controlled checkbox switches to uncontrolled with defaultChecked', async () => { @@ -2193,18 +2216,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="checkbox" defaultChecked={true} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="checkbox" defaultChecked={true} />); + }); + assertConsoleErrorDev([ 'A component is changing a controlled input to be uncontrolled. ' + 'This is likely caused by the value changing from a defined to ' + 'undefined, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if uncontrolled checkbox (checked is undefined) switches to controlled', async () => { @@ -2212,18 +2234,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="checkbox" checked={true} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="checkbox" checked={true} />); + }); + assertConsoleErrorDev([ 'A component is changing an uncontrolled input to be controlled. ' + 'This is likely caused by the value changing from undefined to ' + 'a defined value, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if uncontrolled checkbox (checked is null) switches to controlled', async () => { @@ -2231,18 +2252,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="checkbox" checked={true} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="checkbox" checked={true} />); + }); + assertConsoleErrorDev([ 'A component is changing an uncontrolled input to be controlled. ' + 'This is likely caused by the value changing from undefined to ' + 'a defined value, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if controlled radio switches to uncontrolled (checked is undefined)', async () => { @@ -2250,18 +2270,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="radio" />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="radio" />); + }); + assertConsoleErrorDev([ 'A component is changing a controlled input to be uncontrolled. ' + 'This is likely caused by the value changing from a defined to ' + 'undefined, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if controlled radio switches to uncontrolled (checked is null)', async () => { @@ -2269,18 +2288,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="radio" checked={null} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="radio" checked={null} />); + }); + assertConsoleErrorDev([ 'A component is changing a controlled input to be uncontrolled. ' + 'This is likely caused by the value changing from a defined to ' + 'undefined, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if controlled radio switches to uncontrolled with defaultChecked', async () => { @@ -2288,18 +2306,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="radio" defaultChecked={true} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="radio" defaultChecked={true} />); + }); + assertConsoleErrorDev([ 'A component is changing a controlled input to be uncontrolled. ' + 'This is likely caused by the value changing from a defined to ' + 'undefined, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if uncontrolled radio (checked is undefined) switches to controlled', async () => { @@ -2307,18 +2324,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="radio" checked={true} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="radio" checked={true} />); + }); + assertConsoleErrorDev([ 'A component is changing an uncontrolled input to be controlled. ' + 'This is likely caused by the value changing from undefined to ' + 'a defined value, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should warn if uncontrolled radio (checked is null) switches to controlled', async () => { @@ -2326,18 +2342,17 @@ describe('ReactDOMInput', () => { await act(() => { root.render(stub); }); - await expect(async () => { - await act(() => { - root.render(<input type="radio" checked={true} />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="radio" checked={true} />); + }); + assertConsoleErrorDev([ 'A component is changing an uncontrolled input to be controlled. ' + 'This is likely caused by the value changing from undefined to ' + 'a defined value, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('should not warn if radio value changes but never becomes controlled', async () => { @@ -2390,18 +2405,17 @@ describe('ReactDOMInput', () => { />, ); }); - await expect(async () => { - await act(() => { - root.render(<input type="radio" value="value" />); - }); - }).toErrorDev( + await act(() => { + root.render(<input type="radio" value="value" />); + }); + assertConsoleErrorDev([ 'A component is changing a controlled input to be uncontrolled. ' + 'This is likely caused by the value changing from a defined to ' + 'undefined, which should not happen. ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' + ' in input (at **)', - ); + ]); }); it('sets type, step, min, max before value always', async () => { @@ -2747,9 +2761,15 @@ describe('ReactDOMInput', () => { } it('reverts the value attribute to the initial value', async () => { - await expect(renderInputWithStringThenWithUndefined).toErrorDev( - 'A component is changing a controlled input to be uncontrolled.', - ); + await renderInputWithStringThenWithUndefined(); + assertConsoleErrorDev([ + 'A component is changing a controlled input to be uncontrolled. ' + + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' + + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' + + 'More info: https://react.dev/link/controlled-components\n' + + ' in input (at **)\n' + + ' in Input (at **)', + ]); if (disableInputAttributeSyncing) { expect(input.getAttribute('value')).toBe(null); } else { @@ -2758,9 +2778,15 @@ describe('ReactDOMInput', () => { }); it('preserves the value property', async () => { - await expect(renderInputWithStringThenWithUndefined).toErrorDev( - 'A component is changing a controlled input to be uncontrolled.', - ); + await renderInputWithStringThenWithUndefined(); + assertConsoleErrorDev([ + 'A component is changing a controlled input to be uncontrolled. ' + + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' + + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' + + 'More info: https://react.dev/link/controlled-components\n' + + ' in input (at **)\n' + + ' in Input (at **)', + ]); expect(input.value).toBe('latest'); }); }); @@ -2798,11 +2824,19 @@ describe('ReactDOMInput', () => { } it('reverts the value attribute to the initial value', async () => { - await expect(renderInputWithStringThenWithNull).toErrorDev([ + await renderInputWithStringThenWithNull(); + assertConsoleErrorDev([ '`value` prop on `input` should not be null. ' + 'Consider using an empty string to clear the component ' + - 'or `undefined` for uncontrolled components.', - 'A component is changing a controlled input to be uncontrolled.', + 'or `undefined` for uncontrolled components.\n' + + ' in input (at **)\n' + + ' in Input (at **)', + 'A component is changing a controlled input to be uncontrolled. ' + + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' + + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' + + 'More info: https://react.dev/link/controlled-components\n' + + ' in input (at **)\n' + + ' in Input (at **)', ]); if (disableInputAttributeSyncing) { expect(input.getAttribute('value')).toBe(null); @@ -2812,11 +2846,19 @@ describe('ReactDOMInput', () => { }); it('preserves the value property', async () => { - await expect(renderInputWithStringThenWithNull).toErrorDev([ + await renderInputWithStringThenWithNull(); + assertConsoleErrorDev([ '`value` prop on `input` should not be null. ' + 'Consider using an empty string to clear the component ' + - 'or `undefined` for uncontrolled components.', - 'A component is changing a controlled input to be uncontrolled.', + 'or `undefined` for uncontrolled components.\n' + + ' in input (at **)\n' + + ' in Input (at **)', + 'A component is changing a controlled input to be uncontrolled. ' + + 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' + + 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' + + 'More info: https://react.dev/link/controlled-components\n' + + ' in input (at **)\n' + + ' in Input (at **)', ]); expect(input.value).toBe('latest'); }); @@ -2824,11 +2866,15 @@ describe('ReactDOMInput', () => { describe('When given a Symbol value', function () { it('treats initial Symbol value as an empty string', async () => { - await expect(async () => { - await act(() => { - root.render(<input value={Symbol('foobar')} onChange={() => {}} />); - }); - }).toErrorDev('Invalid value for prop `value`'); + await act(() => { + root.render(<input value={Symbol('foobar')} onChange={() => {}} />); + }); + assertConsoleErrorDev([ + 'Invalid value for prop `value` on <input> tag. ' + + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' + + 'For details, see https://react.dev/link/attribute-behavior \n' + + ' in input (at **)', + ]); const node = container.firstChild; expect(node.value).toBe(''); @@ -2843,11 +2889,15 @@ describe('ReactDOMInput', () => { await act(() => { root.render(<input value="foo" onChange={() => {}} />); }); - await expect(async () => { - await act(() => { - root.render(<input value={Symbol('foobar')} onChange={() => {}} />); - }); - }).toErrorDev('Invalid value for prop `value`'); + await act(() => { + root.render(<input value={Symbol('foobar')} onChange={() => {}} />); + }); + assertConsoleErrorDev([ + 'Invalid value for prop `value` on <input> tag. ' + + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' + + 'For details, see https://react.dev/link/attribute-behavior \n' + + ' in input (at **)', + ]); const node = container.firstChild; expect(node.value).toBe(''); @@ -2890,11 +2940,15 @@ describe('ReactDOMInput', () => { describe('When given a function value', function () { it('treats initial function value as an empty string', async () => { - await expect(async () => { - await act(() => { - root.render(<input value={() => {}} onChange={() => {}} />); - }); - }).toErrorDev('Invalid value for prop `value`'); + await act(() => { + root.render(<input value={() => {}} onChange={() => {}} />); + }); + assertConsoleErrorDev([ + 'Invalid value for prop `value` on <input> tag. ' + + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' + + 'For details, see https://react.dev/link/attribute-behavior \n' + + ' in input (at **)', + ]); const node = container.firstChild; expect(node.value).toBe(''); @@ -2909,11 +2963,15 @@ describe('ReactDOMInput', () => { await act(() => { root.render(<input value="foo" onChange={() => {}} />); }); - await expect(async () => { - await act(() => { - root.render(<input value={() => {}} onChange={() => {}} />); - }); - }).toErrorDev('Invalid value for prop `value`'); + await act(() => { + root.render(<input value={() => {}} onChange={() => {}} />); + }); + assertConsoleErrorDev([ + 'Invalid value for prop `value` on <input> tag. ' + + 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' + + 'For details, see https://react.dev/link/attribute-behavior \n' + + ' in input (at **)', + ]); const node = container.firstChild; expect(node.value).toBe('');