diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js index 6b8baaddbe023..2ccd1c043661a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js @@ -25,6 +25,7 @@ describe('ReactDOMInput', () => { let setUntrackedValue; let setUntrackedChecked; let container; + let root; function dispatchEventOnNode(node, type) { node.dispatchEvent(new Event(type, {bubbles: true, cancelable: true})); @@ -99,6 +100,7 @@ describe('ReactDOMInput', () => { container = document.createElement('div'); document.body.appendChild(container); + root = ReactDOMClient.createRoot(container); }); afterEach(() => { @@ -106,9 +108,11 @@ describe('ReactDOMInput', () => { jest.restoreAllMocks(); }); - it('should warn for controlled value of 0 with missing onChange', () => { - expect(() => { - ReactDOM.render(, container); + it('should warn for controlled value of 0 with missing onChange', async () => { + await expect(async () => { + await act(() => { + root.render(); + }); }).toErrorDev( 'Warning: You provided a `value` prop to a form ' + 'field without an `onChange` handler. This will render a read-only ' + @@ -117,9 +121,11 @@ describe('ReactDOMInput', () => { ); }); - it('should warn for controlled value of "" with missing onChange', () => { - expect(() => { - ReactDOM.render(, container); + it('should warn for controlled value of "" with missing onChange', async () => { + await expect(async () => { + await act(() => { + root.render(); + }); }).toErrorDev( 'Warning: You provided a `value` prop to a form ' + 'field without an `onChange` handler. This will render a read-only ' + @@ -128,9 +134,11 @@ describe('ReactDOMInput', () => { ); }); - it('should warn for controlled value of "0" with missing onChange', () => { - expect(() => { - ReactDOM.render(, container); + it('should warn for controlled value of "0" with missing onChange', async () => { + await expect(async () => { + await act(() => { + root.render(); + }); }).toErrorDev( 'Warning: You provided a `value` prop to a form ' + 'field without an `onChange` handler. This will render a read-only ' + @@ -139,72 +147,95 @@ describe('ReactDOMInput', () => { ); }); - it('should warn for controlled value of false with missing onChange', () => { - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + it('should warn for controlled value of false with missing onChange', async () => { + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: You provided a `checked` prop to a form field without an `onChange` handler.', ); }); - it('should warn with checked and no onChange handler with readOnly specified', () => { - ReactDOM.render( - , - container, - ); - ReactDOM.unmountComponentAtNode(container); + it('should warn with checked and no onChange handler with readOnly specified', async () => { + await act(() => { + root.render(); + }); + root.unmount(); + root = ReactDOMClient.createRoot(container); - expect(() => - ReactDOM.render( - , - container, - ), - ).toErrorDev( + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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`.', ); }); - it('should not warn about missing onChange in uncontrolled inputs', () => { - ReactDOM.render(, container); - ReactDOM.unmountComponentAtNode(container); - ReactDOM.render(, container); - ReactDOM.unmountComponentAtNode(container); - ReactDOM.render(, container); - ReactDOM.unmountComponentAtNode(container); - ReactDOM.render(, container); - ReactDOM.unmountComponentAtNode(container); - ReactDOM.render(, container); - ReactDOM.unmountComponentAtNode(container); - ReactDOM.render(, container); + it('should not warn about missing onChange in uncontrolled inputs', async () => { + await act(() => { + root.render(); + }); + root.unmount(); + root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + root.unmount(); + root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + root.unmount(); + root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + root.unmount(); + root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + root.unmount(); + root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); }); - it('should not warn with value and onInput handler', () => { - ReactDOM.render( {}} />, container); + it('should not warn with value and onInput handler', async () => { + await act(() => { + root.render( {}} />); + }); }); - it('should properly control a value even if no event listener exists', () => { - let node; - - expect(() => { - node = ReactDOM.render(, container); + it('should properly control a value even if no event listener exists', async () => { + await expect(async () => { + await act(() => { + root.render(); + }); }).toErrorDev( 'Warning: You provided a `value` prop to a form field without an `onChange` handler.', ); + const node = container.firstChild; expect(isValueDirty(node)).toBe(true); setUntrackedValue.call(node, 'giraffe'); // This must use the native event dispatching. If we simulate, we will // bypass the lazy event attachment system so we won't actually test this. - dispatchEventOnNode(node, 'input'); + await act(() => { + dispatchEventOnNode(node, 'input'); + }); expect(node.value).toBe('lion'); expect(isValueDirty(node)).toBe(true); }); - it('should control a value in reentrant events', () => { + it('should control a value in reentrant events', async () => { class ControlledInputs extends React.Component { state = {value: 'lion'}; a = null; @@ -240,23 +271,35 @@ describe('ReactDOMInput', () => { } } - const instance = ReactDOM.render(, container); + const ref = React.createRef(); + await act(() => { + root.render(); + }); + const instance = ref.current; // Focus the field so we can later blur it. // Don't remove unless you've verified the fix in #8240 is still covered. - instance.a.focus(); + await act(() => { + instance.a.focus(); + }); setUntrackedValue.call(instance.a, 'giraffe'); // This must use the native event dispatching. If we simulate, we will // bypass the lazy event attachment system so we won't actually test this. - dispatchEventOnNode(instance.a, 'input'); - dispatchEventOnNode(instance.a, 'blur'); - dispatchEventOnNode(instance.a, 'focusout'); + await act(() => { + dispatchEventOnNode(instance.a, 'input'); + }); + await act(() => { + dispatchEventOnNode(instance.a, 'blur'); + }); + await act(() => { + dispatchEventOnNode(instance.a, 'focusout'); + }); expect(instance.a.value).toBe('giraffe'); expect(instance.switchedFocus).toBe(true); }); - it('should control values in reentrant events with different targets', () => { + it('should control values in reentrant events with different targets', async () => { class ControlledInputs extends React.Component { state = {value: 'lion'}; a = null; @@ -288,21 +331,29 @@ describe('ReactDOMInput', () => { } } - const instance = ReactDOM.render(, container); + const ref = React.createRef(); + await act(() => { + root.render(); + }); + const instance = ref.current; setUntrackedValue.call(instance.a, 'giraffe'); // This must use the native event dispatching. If we simulate, we will // bypass the lazy event attachment system so we won't actually test this. - dispatchEventOnNode(instance.a, 'input'); + await act(() => { + dispatchEventOnNode(instance.a, 'input'); + }); expect(instance.a.value).toBe('lion'); expect(instance.b.checked).toBe(true); }); describe('switching text inputs between numeric and string numbers', () => { - it('does change the number 2 to "2.0" with no change handler', () => { - const stub = ; - const node = ReactDOM.render(stub, container); + it('does change the number 2 to "2.0" with no change handler', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; setUntrackedValue.call(node, '2.0'); dispatchEventOnNode(node, 'input'); @@ -315,9 +366,11 @@ describe('ReactDOMInput', () => { } }); - it('does change the string "2" to "2.0" with no change handler', () => { - const stub = ; - const node = ReactDOM.render(stub, container); + it('does change the string "2" to "2.0" with no change handler', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; setUntrackedValue.call(node, '2.0'); dispatchEventOnNode(node, 'input'); @@ -330,7 +383,7 @@ describe('ReactDOMInput', () => { } }); - it('changes the number 2 to "2.0" using a change handler', () => { + it('changes the number 2 to "2.0" using a change handler', async () => { class Stub extends React.Component { state = { value: 2, @@ -345,8 +398,10 @@ describe('ReactDOMInput', () => { } } - const stub = ReactDOM.render(, container); - const node = ReactDOM.findDOMNode(stub); + await act(() => { + root.render(); + }); + const node = container.firstChild; setUntrackedValue.call(node, '2.0'); dispatchEventOnNode(node, 'input'); @@ -360,7 +415,7 @@ describe('ReactDOMInput', () => { }); }); - it('does change the string ".98" to "0.98" with no change handler', () => { + it('does change the string ".98" to "0.98" with no change handler', async () => { class Stub extends React.Component { state = { value: '.98', @@ -370,20 +425,24 @@ describe('ReactDOMInput', () => { } } - let stub; - expect(() => { - stub = ReactDOM.render(, container); + const ref = React.createRef(); + await expect(async () => { + await act(() => { + root.render(); + }); }).toErrorDev( 'You provided a `value` prop to a form field ' + 'without an `onChange` handler.', ); - const node = ReactDOM.findDOMNode(stub); - stub.setState({value: '0.98'}); + const node = container.firstChild; + await act(() => { + ref.current.setState({value: '0.98'}); + }); expect(node.value).toEqual('0.98'); }); - it('performs a state change from "" to 0', () => { + it('performs a state change from "" to 0', async () => { class Stub extends React.Component { state = { value: '', @@ -393,40 +452,43 @@ describe('ReactDOMInput', () => { } } - const stub = ReactDOM.render(, container); - const node = ReactDOM.findDOMNode(stub); - stub.setState({value: 0}); + const ref = React.createRef(); + await act(() => { + root.render(); + }); + const node = container.firstChild; + await act(() => { + ref.current.setState({value: 0}); + }); expect(node.value).toEqual('0'); }); - it('updates the value on radio buttons from "" to 0', function () { - ReactDOM.render( - , - container, - ); - ReactDOM.render( - , - container, - ); + it('updates the value on radio buttons from "" to 0', async () => { + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); expect(container.firstChild.value).toBe('0'); expect(container.firstChild.getAttribute('value')).toBe('0'); }); - it('updates the value on checkboxes from "" to 0', function () { - ReactDOM.render( - , - container, - ); - ReactDOM.render( - , - container, - ); + it('updates the value on checkboxes from "" to 0', async () => { + await act(() => { + root.render(); + }); + await act(() => { + root.render( + , + ); + }); expect(container.firstChild.value).toBe('0'); expect(container.firstChild.getAttribute('value')).toBe('0'); }); - it('distinguishes precision for extra zeroes in string number values', () => { + it('distinguishes precision for extra zeroes in string number values', async () => { class Stub extends React.Component { state = { value: '3.0000', @@ -436,37 +498,45 @@ describe('ReactDOMInput', () => { } } - let stub; - - expect(() => { - stub = ReactDOM.render(, container); + const ref = React.createRef(); + await expect(async () => { + await act(() => { + root.render(); + }); }).toErrorDev( 'You provided a `value` prop to a form field ' + 'without an `onChange` handler.', ); - const node = ReactDOM.findDOMNode(stub); - stub.setState({value: '3'}); + const node = container.firstChild; + await act(() => { + ref.current.setState({value: '3'}); + }); expect(node.value).toEqual('3'); }); - it('should display `defaultValue` of number 0', () => { - const stub = ; - const node = ReactDOM.render(stub, container); + it('should display `defaultValue` of number 0', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(node.getAttribute('value')).toBe('0'); expect(node.value).toBe('0'); }); - it('only assigns defaultValue if it changes', () => { + it('only assigns defaultValue if it changes', async () => { class Test extends React.Component { render() { return ; } } - const component = ReactDOM.render(, container); - const node = ReactDOM.findDOMNode(component); + const ref = React.createRef(); + await act(() => { + root.render(); + }); + const node = container.firstChild; Object.defineProperty(node, 'defaultValue', { get() { @@ -479,28 +549,36 @@ describe('ReactDOMInput', () => { }, }); - component.forceUpdate(); + await act(() => { + ref.current.forceUpdate(); + }); }); - it('should display "true" for `defaultValue` of `true`', () => { + it('should display "true" for `defaultValue` of `true`', async () => { const stub = ; - const node = ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); + const node = container.firstChild; expect(node.value).toBe('true'); }); - it('should display "false" for `defaultValue` of `false`', () => { + it('should display "false" for `defaultValue` of `false`', async () => { const stub = ; - const node = ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); + const node = container.firstChild; expect(node.value).toBe('false'); }); - it('should update `defaultValue` for uncontrolled input', () => { - const node = ReactDOM.render( - , - container, - ); + it('should update `defaultValue` for uncontrolled input', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(node.value).toBe('0'); expect(node.defaultValue).toBe('0'); @@ -510,7 +588,9 @@ describe('ReactDOMInput', () => { expect(isValueDirty(node)).toBe(true); } - ReactDOM.render(, container); + await act(() => { + root.render(); + }); if (disableInputAttributeSyncing) { expect(node.value).toBe('1'); @@ -523,16 +603,18 @@ describe('ReactDOMInput', () => { } }); - it('should update `defaultValue` for uncontrolled date/time input', () => { - const node = ReactDOM.render( - , - container, - ); + it('should update `defaultValue` for uncontrolled date/time input', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(node.value).toBe('1980-01-01'); expect(node.defaultValue).toBe('1980-01-01'); - ReactDOM.render(, container); + await act(() => { + root.render(); + }); if (disableInputAttributeSyncing) { expect(node.value).toBe('2000-01-01'); @@ -542,19 +624,23 @@ describe('ReactDOMInput', () => { expect(node.defaultValue).toBe('2000-01-01'); } - ReactDOM.render(, container); + await act(() => { + root.render(); + }); }); - it('should take `defaultValue` when changing to uncontrolled input', () => { - const node = ReactDOM.render( - , - container, - ); + it('should take `defaultValue` when changing to uncontrolled input', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(node.value).toBe('0'); expect(isValueDirty(node)).toBe(true); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'A component is changing a controlled input to be uncontrolled.', ); expect(node.value).toBe('0'); @@ -580,8 +666,11 @@ describe('ReactDOMInput', () => { expect(div.firstChild.getAttribute('defaultValue')).toBe(null); }); - it('should render name attribute if it is supplied', () => { - const node = ReactDOM.render(, container); + it('should render name attribute if it is supplied', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(node.name).toBe('name'); expect(container.firstChild.getAttribute('name')).toBe('name'); }); @@ -594,8 +683,10 @@ describe('ReactDOMInput', () => { expect(div.firstChild.getAttribute('name')).toBe('name'); }); - it('should not render name attribute if it is not supplied', () => { - ReactDOM.render(, container); + it('should not render name attribute if it is not supplied', async () => { + await act(() => { + root.render(); + }); expect(container.firstChild.getAttribute('name')).toBe(null); }); @@ -607,7 +698,7 @@ describe('ReactDOMInput', () => { expect(div.firstChild.getAttribute('name')).toBe(null); }); - it('should display "foobar" for `defaultValue` of `objToString`', () => { + it('should display "foobar" for `defaultValue` of `objToString`', async () => { const objToString = { toString: function () { return 'foobar'; @@ -615,7 +706,10 @@ describe('ReactDOMInput', () => { }; const stub = ; - const node = ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); + const node = container.firstChild; expect(node.value).toBe('foobar'); }); @@ -631,10 +725,12 @@ describe('ReactDOMInput', () => { return '2020-01-01'; } } + const legacyContainer = document.createElement('div'); + document.body.appendChild(legacyContainer); const test = () => ReactDOM.render( , - container, + legacyContainer, ); expect(() => expect(test).toThrowError(new TypeError('prod message')), @@ -644,7 +740,7 @@ describe('ReactDOMInput', () => { ); }); - it('should throw for text inputs if `defaultValue` is an object where valueOf() throws', () => { + it('should throw for text inputs if `defaultValue` is an object where valueOf() throws', async () => { class TemporalLike { valueOf() { // Throwing here is the behavior of ECMAScript "Temporal" date/time API. @@ -655,20 +751,19 @@ describe('ReactDOMInput', () => { return '2020-01-01'; } } - const test = () => - ReactDOM.render( - , - container, + await expect(async () => { + await expect(async () => { + await act(() => { + root.render(); + }); + }).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.', ); - expect(() => - expect(test).toThrowError(new TypeError('prod message')), - ).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.', - ); + }).rejects.toThrowError(new TypeError('prod message')); }); - it('should throw for date inputs if `value` is an object where valueOf() throws', () => { + it('should throw for date inputs if `value` is an object where valueOf() throws', async () => { class TemporalLike { valueOf() { // Throwing here is the behavior of ECMAScript "Temporal" date/time API. @@ -679,20 +774,25 @@ describe('ReactDOMInput', () => { return '2020-01-01'; } } - const test = () => - ReactDOM.render( - {}} />, - container, + await expect(async () => { + await expect(async () => { + await act(() => { + root.render( + {}} + />, + ); + }); + }).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.', ); - expect(() => - expect(test).toThrowError(new TypeError('prod message')), - ).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.', - ); + }).rejects.toThrowError(new TypeError('prod message')); }); - it('should throw for text inputs if `value` is an object where valueOf() throws', () => { + it('should throw for text inputs if `value` is an object where valueOf() throws', async () => { class TemporalLike { valueOf() { // Throwing here is the behavior of ECMAScript "Temporal" date/time API. @@ -703,55 +803,66 @@ describe('ReactDOMInput', () => { return '2020-01-01'; } } - const test = () => - ReactDOM.render( - {}} />, - container, + await expect(async () => { + await expect(async () => { + await act(() => { + root.render( + {}} + />, + ); + }); + }).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.', ); - expect(() => - expect(test).toThrowError(new TypeError('prod message')), - ).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.', - ); + }).rejects.toThrowError(new TypeError('prod message')); }); - it('should display `value` of number 0', () => { - const stub = ; - const node = ReactDOM.render(stub, container); + it('should display `value` of number 0', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(node.value).toBe('0'); }); - it('should allow setting `value` to `true`', () => { - let stub = ; - const node = ReactDOM.render(stub, container); + it('should allow setting `value` to `true`', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(node.value).toBe('yolo'); - stub = ReactDOM.render( - , - container, - ); + await act(() => { + root.render(); + }); expect(node.value).toEqual('true'); }); - it('should allow setting `value` to `false`', () => { - let stub = ; - const node = ReactDOM.render(stub, container); + it('should allow setting `value` to `false`', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(node.value).toBe('yolo'); - stub = ReactDOM.render( - , - container, - ); + await act(() => { + root.render(); + }); expect(node.value).toEqual('false'); }); - it('should allow setting `value` to `objToString`', () => { - let stub = ; - const node = ReactDOM.render(stub, container); + it('should allow setting `value` to `objToString`', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(node.value).toBe('foo'); @@ -760,15 +871,18 @@ describe('ReactDOMInput', () => { return 'foobar'; }, }; - stub = ReactDOM.render( - , - container, - ); + await act(() => { + root.render( + , + ); + }); expect(node.value).toEqual('foobar'); }); - it('should not incur unnecessary DOM mutations', () => { - ReactDOM.render( {}} />, container); + it('should not incur unnecessary DOM mutations', async () => { + await act(() => { + root.render( {}} />); + }); const node = container.firstChild; let nodeValue = 'a'; @@ -782,15 +896,21 @@ describe('ReactDOMInput', () => { }), }); - ReactDOM.render( {}} />, container); + await act(() => { + root.render( {}} />); + }); expect(nodeValueSetter).toHaveBeenCalledTimes(0); - ReactDOM.render( {}} />, container); + await act(() => { + root.render( {}} />); + }); expect(nodeValueSetter).toHaveBeenCalledTimes(1); }); - it('should not incur unnecessary DOM mutations for numeric type conversion', () => { - ReactDOM.render( {}} />, container); + it('should not incur unnecessary DOM mutations for numeric type conversion', async () => { + await act(() => { + root.render( {}} />); + }); const node = container.firstChild; let nodeValue = '0'; @@ -804,12 +924,16 @@ describe('ReactDOMInput', () => { }), }); - ReactDOM.render( {}} />, container); + await act(() => { + root.render( {}} />); + }); expect(nodeValueSetter).toHaveBeenCalledTimes(0); }); - it('should not incur unnecessary DOM mutations for the boolean type conversion', () => { - ReactDOM.render( {}} />, container); + it('should not incur unnecessary DOM mutations for the boolean type conversion', async () => { + await act(() => { + root.render( {}} />); + }); const node = container.firstChild; let nodeValue = 'true'; @@ -823,34 +947,46 @@ describe('ReactDOMInput', () => { }), }); - ReactDOM.render( {}} />, container); + await act(() => { + root.render( {}} />); + }); expect(nodeValueSetter).toHaveBeenCalledTimes(0); }); - it('should properly control a value of number `0`', () => { - const stub = ; - const node = ReactDOM.render(stub, container); + it('should properly control a value of number `0`', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; setUntrackedValue.call(node, 'giraffe'); dispatchEventOnNode(node, 'input'); expect(node.value).toBe('0'); }); - it('should properly control 0.0 for a text input', () => { - const stub = ; - const node = ReactDOM.render(stub, container); + it('should properly control 0.0 for a text input', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; setUntrackedValue.call(node, '0.0'); - dispatchEventOnNode(node, 'input'); + await act(() => { + dispatchEventOnNode(node, 'input'); + }); expect(node.value).toBe('0'); }); - it('should properly control 0.0 for a number input', () => { - const stub = ; - const node = ReactDOM.render(stub, container); + it('should properly control 0.0 for a number input', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; setUntrackedValue.call(node, '0.0'); - dispatchEventOnNode(node, 'input'); + await act(() => { + dispatchEventOnNode(node, 'input'); + }); if (disableInputAttributeSyncing) { expect(node.value).toBe('0.0'); @@ -864,18 +1000,16 @@ describe('ReactDOMInput', () => { } }); - it('should properly transition from an empty value to 0', function () { - ReactDOM.render( - , - container, - ); + it('should properly transition from an empty value to 0', async () => { + await act(() => { + root.render(); + }); const node = container.firstChild; expect(isValueDirty(node)).toBe(false); - ReactDOM.render( - , - container, - ); + await act(() => { + root.render(); + }); expect(node.value).toBe('0'); expect(isValueDirty(node)).toBe(true); @@ -887,33 +1021,29 @@ describe('ReactDOMInput', () => { } }); - it('should properly transition from 0 to an empty value', function () { - ReactDOM.render( - , - container, - ); + it('should properly transition from 0 to an empty value', async () => { + await act(() => { + root.render(); + }); const node = container.firstChild; expect(isValueDirty(node)).toBe(true); - ReactDOM.render( - , - container, - ); + await act(() => { + root.render(); + }); expect(node.value).toBe(''); expect(node.defaultValue).toBe(''); expect(isValueDirty(node)).toBe(true); }); - it('should properly transition a text input from 0 to an empty 0.0', function () { - ReactDOM.render( - , - container, - ); - ReactDOM.render( - , - container, - ); + it('should properly transition a text input from 0 to an empty 0.0', async () => { + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); const node = container.firstChild; @@ -925,15 +1055,13 @@ describe('ReactDOMInput', () => { } }); - it('should properly transition a number input from "" to 0', function () { - ReactDOM.render( - , - container, - ); - ReactDOM.render( - , - container, - ); + it('should properly transition a number input from "" to 0', async () => { + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); const node = container.firstChild; @@ -945,15 +1073,13 @@ describe('ReactDOMInput', () => { } }); - it('should properly transition a number input from "" to "0"', function () { - ReactDOM.render( - , - container, - ); - ReactDOM.render( - , - container, - ); + it('should properly transition a number input from "" to "0"', async () => { + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); const node = container.firstChild; @@ -965,31 +1091,36 @@ describe('ReactDOMInput', () => { } }); - it('should have the correct target value', () => { + it('should have the correct target value', async () => { let handled = false; const handler = function (event) { expect(event.target.nodeName).toBe('INPUT'); handled = true; }; - const stub = ; - const node = ReactDOM.render(stub, container); + await act(() => { + root.render(); + }); + const node = container.firstChild; setUntrackedValue.call(node, 'giraffe'); - dispatchEventOnNode(node, 'input'); + await act(() => { + dispatchEventOnNode(node, 'input'); + }); expect(handled).toBe(true); }); - it('should restore uncontrolled inputs to last defaultValue upon reset', () => { + it('should restore uncontrolled inputs to last defaultValue upon reset', async () => { const inputRef = React.createRef(); - ReactDOM.render( -
- - -
, - container, - ); + await act(() => { + root.render( +
+ + +
, + ); + }); expect(inputRef.current.value).toBe('default1'); if (disableInputAttributeSyncing) { expect(isValueDirty(inputRef.current)).toBe(false); @@ -1002,13 +1133,14 @@ describe('ReactDOMInput', () => { expect(inputRef.current.value).toBe('changed'); expect(isValueDirty(inputRef.current)).toBe(true); - ReactDOM.render( -
- - -
, - container, - ); + await act(() => { + root.render( +
+ + +
, + ); + }); expect(inputRef.current.value).toBe('changed'); expect(isValueDirty(inputRef.current)).toBe(true); @@ -1020,9 +1152,11 @@ describe('ReactDOMInput', () => { expect(isValueDirty(inputRef.current)).toBe(false); }); - it('should not set a value for submit buttons unnecessarily', () => { + it('should not set a value for submit buttons unnecessarily', async () => { const stub = ; - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); const node = container.firstChild; // The value shouldn't be '', or else the button will have no text; it @@ -1032,18 +1166,21 @@ describe('ReactDOMInput', () => { expect(node.hasAttribute('value')).toBe(false); }); - it('should remove the value attribute on submit inputs when value is updated to undefined', () => { + it('should remove the value attribute on submit inputs when value is updated to undefined', async () => { const stub = ; - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); // Not really relevant to this particular test, but changing to undefined // should nonetheless trigger a warning - expect(() => - ReactDOM.render( - , - container, - ), - ).toErrorDev( + await expect(async () => { + await act(() => { + root.render( + , + ); + }); + }).toErrorDev( 'A component is changing a controlled input to be uncontrolled.', ); @@ -1051,18 +1188,21 @@ describe('ReactDOMInput', () => { expect(node.getAttribute('value')).toBe(null); }); - it('should remove the value attribute on reset inputs when value is updated to undefined', () => { + it('should remove the value attribute on reset inputs when value is updated to undefined', async () => { const stub = ; - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); // Not really relevant to this particular test, but changing to undefined // should nonetheless trigger a warning - expect(() => - ReactDOM.render( - , - container, - ), - ).toErrorDev( + await expect(async () => { + await act(() => { + root.render( + , + ); + }); + }).toErrorDev( 'A component is changing a controlled input to be uncontrolled.', ); @@ -1070,44 +1210,56 @@ describe('ReactDOMInput', () => { expect(node.getAttribute('value')).toBe(null); }); - it('should set a value on a submit input', () => { + it('should set a value on a submit input', async () => { const stub = ; - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); const node = container.firstChild; expect(node.getAttribute('value')).toBe('banana'); }); - it('should not set an undefined value on a submit input', () => { + it('should not set an undefined value on a submit input', async () => { const stub = ; - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); const node = container.firstChild; // Note: it shouldn't be an empty string // because that would erase the "submit" label. expect(node.getAttribute('value')).toBe(null); - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); expect(node.getAttribute('value')).toBe(null); }); - it('should not set an undefined value on a reset input', () => { + it('should not set an undefined value on a reset input', async () => { const stub = ; - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); const node = container.firstChild; // Note: it shouldn't be an empty string // because that would erase the "reset" label. expect(node.getAttribute('value')).toBe(null); - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); expect(node.getAttribute('value')).toBe(null); }); - it('should not set a null value on a submit input', () => { + it('should not set a null value on a submit input', async () => { const stub = ; - expect(() => { - ReactDOM.render(stub, container); + await expect(async () => { + await act(() => { + root.render(stub); + }); }).toErrorDev('`value` prop on `input` should not be null'); const node = container.firstChild; @@ -1115,14 +1267,18 @@ describe('ReactDOMInput', () => { // because that would erase the "submit" label. expect(node.getAttribute('value')).toBe(null); - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); expect(node.getAttribute('value')).toBe(null); }); - it('should not set a null value on a reset input', () => { + it('should not set a null value on a reset input', async () => { const stub = ; - expect(() => { - ReactDOM.render(stub, container); + await expect(async () => { + await act(() => { + root.render(stub); + }); }).toErrorDev('`value` prop on `input` should not be null'); const node = container.firstChild; @@ -1130,35 +1286,43 @@ describe('ReactDOMInput', () => { // because that would erase the "reset" label. expect(node.getAttribute('value')).toBe(null); - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); expect(node.getAttribute('value')).toBe(null); }); - it('should set a value on a reset input', () => { + it('should set a value on a reset input', async () => { const stub = ; - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); const node = container.firstChild; expect(node.getAttribute('value')).toBe('banana'); }); - it('should set an empty string value on a submit input', () => { + it('should set an empty string value on a submit input', async () => { const stub = ; - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); const node = container.firstChild; expect(node.getAttribute('value')).toBe(''); }); - it('should set an empty string value on a reset input', () => { + it('should set an empty string value on a reset input', async () => { const stub = ; - ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); const node = container.firstChild; expect(node.getAttribute('value')).toBe(''); }); - it('should control radio buttons', () => { + it('should control radio buttons', async () => { class RadioGroup extends React.Component { aRef = React.createRef(); bRef = React.createRef(); @@ -1199,7 +1363,11 @@ describe('ReactDOMInput', () => { } } - const stub = ReactDOM.render(, container); + const ref = React.createRef(); + await act(() => { + root.render(); + }); + const stub = ref.current; const aNode = stub.aRef.current; const bNode = stub.bRef.current; const cNode = stub.cRef.current; @@ -1240,7 +1408,9 @@ describe('ReactDOMInput', () => { } // Now let's run the actual ReactDOMInput change event handler - dispatchEventOnNode(bNode, 'click'); + await act(() => { + dispatchEventOnNode(bNode, 'click'); + }); // The original state should have been restored expect(aNode.checked).toBe(true); @@ -1288,6 +1458,10 @@ describe('ReactDOMInput', () => { ); } const html = ReactDOMServer.renderToString(); + // Create a fresh container, not attached a root yet + container.remove(); + container = document.createElement('div'); + document.body.appendChild(container); container.innerHTML = html; const [a, b, c] = container.querySelectorAll('input'); expect(a.checked).toBe(true); @@ -1376,6 +1550,10 @@ describe('ReactDOMInput', () => { ); } const html = ReactDOMServer.renderToString(); + // Create a fresh container, not attached a root yet + container.remove(); + container = document.createElement('div'); + document.body.appendChild(container); container.innerHTML = html; const [a, b, c] = container.querySelectorAll('input'); expect(a.checked).toBe(true); @@ -1421,7 +1599,7 @@ describe('ReactDOMInput', () => { assertInputTrackingIsCurrent(container); }); - it('should check the correct radio when the selected name moves', () => { + it('should check the correct radio when the selected name moves', async () => { class App extends React.Component { state = { updated: false, @@ -1452,40 +1630,48 @@ describe('ReactDOMInput', () => { } } - const stub = ReactDOM.render(, container); - const buttonNode = ReactDOM.findDOMNode(stub).childNodes[0]; - const firstRadioNode = ReactDOM.findDOMNode(stub).childNodes[1]; + await act(() => { + root.render(); + }); + const node = container.firstChild; + const buttonNode = node.childNodes[0]; + const firstRadioNode = node.childNodes[1]; expect(isCheckedDirty(firstRadioNode)).toBe(true); expect(firstRadioNode.checked).toBe(false); assertInputTrackingIsCurrent(container); - dispatchEventOnNode(buttonNode, 'click'); + await act(() => { + dispatchEventOnNode(buttonNode, 'click'); + }); expect(firstRadioNode.checked).toBe(true); assertInputTrackingIsCurrent(container); - dispatchEventOnNode(buttonNode, 'click'); + await act(() => { + dispatchEventOnNode(buttonNode, 'click'); + }); expect(firstRadioNode.checked).toBe(false); assertInputTrackingIsCurrent(container); }); - it("shouldn't get tricked by changing radio names, part 2", () => { - ReactDOM.render( -
- {}} - /> - {}} - /> -
, - container, - ); + it("shouldn't get tricked by changing radio names, part 2", async () => { + await act(() => { + root.render( +
+ {}} + /> + {}} + /> +
, + ); + }); const one = container.querySelector('input[name="a"][value="1"]'); const two = container.querySelector('input[name="a"][value="2"]'); expect(one.checked).toBe(true); @@ -1494,25 +1680,26 @@ describe('ReactDOMInput', () => { expect(isCheckedDirty(two)).toBe(true); assertInputTrackingIsCurrent(container); - ReactDOM.render( -
- {}} - /> - {}} - /> -
, - container, - ); + await act(() => { + root.render( +
+ {}} + /> + {}} + /> +
, + ); + }); expect(one.checked).toBe(true); expect(two.checked).toBe(true); expect(isCheckedDirty(one)).toBe(true); @@ -1521,6 +1708,9 @@ describe('ReactDOMInput', () => { }); it('should control radio buttons if the tree updates during render', () => { + container.remove(); + container = document.createElement('div'); + document.body.appendChild(container); const sharedParent = container; const container1 = document.createElement('div'); const container2 = document.createElement('div'); @@ -1600,7 +1790,7 @@ describe('ReactDOMInput', () => { assertInputTrackingIsCurrent(container); }); - it('should control radio buttons if the tree updates during render (case 2; #26876)', () => { + it('should control radio buttons if the tree updates during render (case 2; #26876)', async () => { let thunk = null; function App() { const [disabled, setDisabled] = React.useState(false); @@ -1634,7 +1824,9 @@ describe('ReactDOMInput', () => { ); } - ReactDOM.render(, container); + await act(() => { + root.render(); + }); const [one, two] = container.querySelectorAll('input'); expect(one.checked).toBe(true); expect(two.checked).toBe(false); @@ -1644,7 +1836,9 @@ describe('ReactDOMInput', () => { // Click two setUntrackedChecked.call(two, true); - dispatchEventOnNode(two, 'click'); + await act(() => { + dispatchEventOnNode(two, 'click'); + }); expect(one.checked).toBe(true); expect(two.checked).toBe(false); expect(isCheckedDirty(one)).toBe(true); @@ -1652,7 +1846,7 @@ describe('ReactDOMInput', () => { assertInputTrackingIsCurrent(container); // After a delay... - ReactDOM.unstable_batchedUpdates(thunk); + await act(thunk); expect(one.checked).toBe(false); expect(two.checked).toBe(true); expect(isCheckedDirty(one)).toBe(true); @@ -1661,7 +1855,9 @@ describe('ReactDOMInput', () => { // Click back to one setUntrackedChecked.call(one, true); - dispatchEventOnNode(one, 'click'); + await act(() => { + dispatchEventOnNode(one, 'click'); + }); expect(one.checked).toBe(false); expect(two.checked).toBe(true); expect(isCheckedDirty(one)).toBe(true); @@ -1669,7 +1865,7 @@ describe('ReactDOMInput', () => { assertInputTrackingIsCurrent(container); // After a delay... - ReactDOM.unstable_batchedUpdates(thunk); + await act(thunk); expect(one.checked).toBe(true); expect(two.checked).toBe(false); expect(isCheckedDirty(one)).toBe(true); @@ -1677,19 +1873,18 @@ describe('ReactDOMInput', () => { assertInputTrackingIsCurrent(container); }); - it('should warn with value and no onChange handler and readOnly specified', () => { - ReactDOM.render( - , - container, - ); - ReactDOM.unmountComponentAtNode(container); + it('should warn with value and no onChange handler and readOnly specified', async () => { + await act(() => { + root.render(); + }); + root.unmount(); + root = ReactDOMClient.createRoot(container); - expect(() => - ReactDOM.render( - , - container, - ), - ).toErrorDev( + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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`. ' + @@ -1698,27 +1893,36 @@ describe('ReactDOMInput', () => { ); }); - it('should have a this value of undefined if bind is not used', () => { + it('should have a this value of undefined if bind is not used', async () => { expect.assertions(1); const unboundInputOnChange = function () { expect(this).toBe(undefined); }; const stub = ; - const node = ReactDOM.render(stub, container); + await act(() => { + root.render(stub); + }); + const node = container.firstChild; setUntrackedValue.call(node, 'giraffe'); - dispatchEventOnNode(node, 'input'); + await act(() => { + dispatchEventOnNode(node, 'input'); + }); }); - it('should update defaultValue to empty string', () => { - ReactDOM.render(, container); + it('should update defaultValue to empty string', async () => { + await act(() => { + root.render(); + }); if (disableInputAttributeSyncing) { expect(isValueDirty(container.firstChild)).toBe(false); } else { expect(isValueDirty(container.firstChild)).toBe(true); } - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(container.firstChild.defaultValue).toBe(''); if (disableInputAttributeSyncing) { expect(isValueDirty(container.firstChild)).toBe(false); @@ -1727,31 +1931,37 @@ describe('ReactDOMInput', () => { } }); - it('should warn if value is null', () => { - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + it('should warn if value is null', async () => { + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( '`value` prop on `input` should not be null. ' + 'Consider using an empty string to clear the component or `undefined` ' + 'for uncontrolled components.', ); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); - ReactDOM.render(, container); + root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); }); - it('should warn if checked and defaultChecked props are specified', () => { - expect(() => - ReactDOM.render( - , - container, - ), - ).toErrorDev( + it('should warn if checked and defaultChecked props are specified', async () => { + await expect(async () => { + await act(() => { + root.render( + , + ); + }); + }).toErrorDev( '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 ' + @@ -1759,26 +1969,29 @@ describe('ReactDOMInput', () => { 'element and remove one of these props. More info: ' + 'https://reactjs.org/link/controlled-components', ); - ReactDOM.unmountComponentAtNode(container); - - ReactDOM.render( - , - container, - ); + root.unmount(); + + root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); }); - it('should warn if value and defaultValue props are specified', () => { - expect(() => - ReactDOM.render( - , - container, - ), - ).toErrorDev( + it('should warn if value and defaultValue props are specified', async () => { + await expect(async () => { + await act(() => { + root.render( + , + ); + }); + }).toErrorDev( '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 ' + @@ -1786,20 +1999,29 @@ describe('ReactDOMInput', () => { 'element and remove one of these props. More info: ' + 'https://reactjs.org/link/controlled-components', ); - ReactDOM.unmountComponentAtNode(container); + await (() => { + root.unmount(); + }); - ReactDOM.render( - , - container, - ); + await act(() => { + root.render( + , + ); + }); }); - it('should warn if controlled input switches to uncontrolled (value is undefined)', () => { + it('should warn if controlled input switches to uncontrolled (value is undefined)', async () => { const stub = ( ); - ReactDOM.render(stub, container); - expect(() => ReactDOM.render(, container)).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -1809,14 +2031,18 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if controlled input switches to uncontrolled (value is null)', () => { + it('should warn if controlled input switches to uncontrolled (value is null)', async () => { const stub = ( ); - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render(, container), - ).toErrorDev([ + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev([ '`value` prop on `input` should not be null. ' + 'Consider using an empty string to clear the component or `undefined` for uncontrolled components', 'Warning: A component is changing a controlled input to be uncontrolled. ' + @@ -1828,17 +2054,18 @@ describe('ReactDOMInput', () => { ]); }); - it('should warn if controlled input switches to uncontrolled with defaultValue', () => { + it('should warn if controlled input switches to uncontrolled with defaultValue', async () => { const stub = ( ); - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render( - , - container, - ), - ).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -1848,12 +2075,16 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if uncontrolled input (value is undefined) switches to controlled', () => { + it('should warn if uncontrolled input (value is undefined) switches to controlled', async () => { const stub = ; - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -1863,15 +2094,21 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if uncontrolled input (value is null) switches to controlled', () => { + it('should warn if uncontrolled input (value is null) switches to controlled', async () => { const stub = ; - expect(() => ReactDOM.render(stub, container)).toErrorDev( + await expect(async () => { + await act(() => { + root.render(stub); + }); + }).toErrorDev( '`value` prop on `input` should not be null. ' + 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.', ); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -1881,14 +2118,18 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if controlled checkbox switches to uncontrolled (checked is undefined)', () => { + it('should warn if controlled checkbox switches to uncontrolled (checked is undefined)', async () => { const stub = ( ); - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -1898,14 +2139,18 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if controlled checkbox switches to uncontrolled (checked is null)', () => { + it('should warn if controlled checkbox switches to uncontrolled (checked is null)', async () => { const stub = ( ); - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -1915,17 +2160,18 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if controlled checkbox switches to uncontrolled with defaultChecked', () => { + it('should warn if controlled checkbox switches to uncontrolled with defaultChecked', async () => { const stub = ( ); - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render( - , - container, - ), - ).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -1935,12 +2181,16 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if uncontrolled checkbox (checked is undefined) switches to controlled', () => { + it('should warn if uncontrolled checkbox (checked is undefined) switches to controlled', async () => { const stub = ; - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -1950,12 +2200,16 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if uncontrolled checkbox (checked is null) switches to controlled', () => { + it('should warn if uncontrolled checkbox (checked is null) switches to controlled', async () => { const stub = ; - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -1965,10 +2219,16 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if controlled radio switches to uncontrolled (checked is undefined)', () => { + it('should warn if controlled radio switches to uncontrolled (checked is undefined)', async () => { const stub = ; - ReactDOM.render(stub, container); - expect(() => ReactDOM.render(, container)).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -1978,12 +2238,16 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if controlled radio switches to uncontrolled (checked is null)', () => { + it('should warn if controlled radio switches to uncontrolled (checked is null)', async () => { const stub = ; - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -1993,12 +2257,16 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if controlled radio switches to uncontrolled with defaultChecked', () => { + it('should warn if controlled radio switches to uncontrolled with defaultChecked', async () => { const stub = ; - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -2008,12 +2276,16 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if uncontrolled radio (checked is undefined) switches to controlled', () => { + it('should warn if uncontrolled radio (checked is undefined) switches to controlled', async () => { const stub = ; - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -2023,12 +2295,16 @@ describe('ReactDOMInput', () => { ); }); - it('should warn if uncontrolled radio (checked is null) switches to controlled', () => { + it('should warn if uncontrolled radio (checked is null) switches to controlled', async () => { const stub = ; - ReactDOM.render(stub, container); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + await act(() => { + root.render(stub); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -2038,54 +2314,61 @@ describe('ReactDOMInput', () => { ); }); - it('should not warn if radio value changes but never becomes controlled', () => { - ReactDOM.render(, container); - ReactDOM.render(, container); - ReactDOM.render( - , - container, - ); - ReactDOM.render( - null} />, - container, - ); - ReactDOM.render(, container); + it('should not warn if radio value changes but never becomes controlled', async () => { + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); + await act(() => { + root.render( null} />); + }); + await act(() => { + root.render(); + }); }); - it('should not warn if radio value changes but never becomes uncontrolled', () => { - ReactDOM.render( - null} />, - container, - ); + it('should not warn if radio value changes but never becomes uncontrolled', async () => { + await act(() => { + root.render( null} />); + }); const input = container.querySelector('input'); expect(isCheckedDirty(input)).toBe(true); - ReactDOM.render( - null} - />, - container, - ); + await act(() => { + root.render( + null} + />, + ); + }); expect(isCheckedDirty(input)).toBe(true); assertInputTrackingIsCurrent(container); }); - it('should warn if radio checked false changes to become uncontrolled', () => { - ReactDOM.render( - null} - />, - container, - ); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + it('should warn if radio checked false changes to become uncontrolled', async () => { + await act(() => { + root.render( + null} + />, + ); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: 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. ' + @@ -2095,7 +2378,7 @@ describe('ReactDOMInput', () => { ); }); - it('sets type, step, min, max before value always', () => { + it('sets type, step, min, max before value always', async () => { const log = []; const originalCreateElement = document.createElement; spyOnDevAndProd(document, 'createElement').mockImplementation( @@ -2133,17 +2416,18 @@ describe('ReactDOMInput', () => { }, ); - ReactDOM.render( - {}} - type="range" - min="0" - max="100" - step="1" - />, - container, - ); + await act(() => { + root.render( + {}} + type="range" + min="0" + max="100" + step="1" + />, + ); + }); expect(log).toEqual([ 'set attribute min', @@ -2154,12 +2438,14 @@ describe('ReactDOMInput', () => { ]); }); - it('sets value properly with type coming later in props', () => { - const input = ReactDOM.render(, container); - expect(input.value).toBe('hi'); + it('sets value properly with type coming later in props', async () => { + await act(() => { + root.render(); + }); + expect(container.firstChild.value).toBe('hi'); }); - it('does not raise a validation warning when it switches types', () => { + it('does not raise a validation warning when it switches types', async () => { class Input extends React.Component { state = {type: 'number', value: 1000}; @@ -2169,16 +2455,21 @@ describe('ReactDOMInput', () => { } } - const input = ReactDOM.render(, container); - const node = ReactDOM.findDOMNode(input); + const ref = React.createRef(); + await act(() => { + root.render(); + }); + const node = container.firstChild; // If the value is set before the type, a validation warning will raise and // the value will not be assigned. - input.setState({type: 'text', value: 'Test'}); + await act(() => { + ref.current.setState({type: 'text', value: 'Test'}); + }); expect(node.value).toEqual('Test'); }); - it('resets value of date/time input to fix bugs in iOS Safari', () => { + it('resets value of date/time input to fix bugs in iOS Safari', async () => { function strify(x) { return JSON.stringify(x, null, 2); } @@ -2250,7 +2541,9 @@ describe('ReactDOMInput', () => { }, ); - ReactDOM.render(, container); + await act(() => { + root.render(); + }); if (disableInputAttributeSyncing) { expect(log).toEqual([ @@ -2289,14 +2582,18 @@ describe('ReactDOMInput', () => { }; } - it('always sets the attribute when values change on text inputs', function () { + it('always sets the attribute when values change on text inputs', async () => { const Input = getTestInput(); - const stub = ReactDOM.render(, container); - const node = ReactDOM.findDOMNode(stub); + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(isValueDirty(node)).toBe(false); setUntrackedValue.call(node, '2'); - dispatchEventOnNode(node, 'input'); + await act(() => { + dispatchEventOnNode(node, 'input'); + }); expect(isValueDirty(node)).toBe(true); if (disableInputAttributeSyncing) { @@ -2306,13 +2603,12 @@ describe('ReactDOMInput', () => { } }); - it('does not set the value attribute on number inputs if focused', () => { + it('does not set the value attribute on number inputs if focused', async () => { const Input = getTestInput(); - const stub = ReactDOM.render( - , - container, - ); - const node = ReactDOM.findDOMNode(stub); + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(isValueDirty(node)).toBe(true); node.focus(); @@ -2328,13 +2624,12 @@ describe('ReactDOMInput', () => { } }); - it('sets the value attribute on number inputs on blur', () => { + it('sets the value attribute on number inputs on blur', async () => { const Input = getTestInput(); - const stub = ReactDOM.render( - , - container, - ); - const node = ReactDOM.findDOMNode(stub); + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(isValueDirty(node)).toBe(true); node.focus(); @@ -2352,11 +2647,11 @@ describe('ReactDOMInput', () => { } }); - it('an uncontrolled number input will not update the value attribute on blur', () => { - const node = ReactDOM.render( - , - container, - ); + it('an uncontrolled number input will not update the value attribute on blur', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; if (disableInputAttributeSyncing) { expect(isValueDirty(node)).toBe(false); } else { @@ -2372,11 +2667,11 @@ describe('ReactDOMInput', () => { expect(node.getAttribute('value')).toBe('1'); }); - it('an uncontrolled text input will not update the value attribute on blur', () => { - const node = ReactDOM.render( - , - container, - ); + it('an uncontrolled text input will not update the value attribute on blur', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; if (disableInputAttributeSyncing) { expect(isValueDirty(node)).toBe(false); } else { @@ -2396,7 +2691,7 @@ describe('ReactDOMInput', () => { describe('setting a controlled input to undefined', () => { let input; - function renderInputWithStringThenWithUndefined() { + async function renderInputWithStringThenWithUndefined() { let setValueToUndefined; class Input extends React.Component { constructor() { @@ -2414,15 +2709,19 @@ describe('ReactDOMInput', () => { } } - const stub = ReactDOM.render(, container); - input = ReactDOM.findDOMNode(stub); + await act(() => { + root.render(); + }); + input = container.firstChild; setUntrackedValue.call(input, 'latest'); dispatchEventOnNode(input, 'input'); - setValueToUndefined(); + await act(() => { + setValueToUndefined(); + }); } - it('reverts the value attribute to the initial value', () => { - expect(renderInputWithStringThenWithUndefined).toErrorDev( + it('reverts the value attribute to the initial value', async () => { + await expect(renderInputWithStringThenWithUndefined).toErrorDev( 'A component is changing a controlled input to be uncontrolled.', ); if (disableInputAttributeSyncing) { @@ -2432,8 +2731,8 @@ describe('ReactDOMInput', () => { } }); - it('preserves the value property', () => { - expect(renderInputWithStringThenWithUndefined).toErrorDev( + it('preserves the value property', async () => { + await expect(renderInputWithStringThenWithUndefined).toErrorDev( 'A component is changing a controlled input to be uncontrolled.', ); expect(input.value).toBe('latest'); @@ -2443,7 +2742,7 @@ describe('ReactDOMInput', () => { describe('setting a controlled input to null', () => { let input; - function renderInputWithStringThenWithNull() { + async function renderInputWithStringThenWithNull() { let setValueToNull; class Input extends React.Component { constructor() { @@ -2461,15 +2760,19 @@ describe('ReactDOMInput', () => { } } - const stub = ReactDOM.render(, container); - input = ReactDOM.findDOMNode(stub); + await act(() => { + root.render(); + }); + input = container.firstChild; setUntrackedValue.call(input, 'latest'); dispatchEventOnNode(input, 'input'); - setValueToNull(); + await act(() => { + setValueToNull(); + }); } - it('reverts the value attribute to the initial value', () => { - expect(renderInputWithStringThenWithNull).toErrorDev([ + it('reverts the value attribute to the initial value', async () => { + await expect(renderInputWithStringThenWithNull).toErrorDev([ '`value` prop on `input` should not be null. ' + 'Consider using an empty string to clear the component ' + 'or `undefined` for uncontrolled components.', @@ -2482,8 +2785,8 @@ describe('ReactDOMInput', () => { } }); - it('preserves the value property', () => { - expect(renderInputWithStringThenWithNull).toErrorDev([ + it('preserves the value property', async () => { + await expect(renderInputWithStringThenWithNull).toErrorDev([ '`value` prop on `input` should not be null. ' + 'Consider using an empty string to clear the component ' + 'or `undefined` for uncontrolled components.', @@ -2494,13 +2797,12 @@ describe('ReactDOMInput', () => { }); describe('When given a Symbol value', function () { - it('treats initial Symbol value as an empty string', function () { - expect(() => - ReactDOM.render( - {}} />, - container, - ), - ).toErrorDev('Invalid value for prop `value`'); + it('treats initial Symbol value as an empty string', async () => { + await expect(async () => { + await act(() => { + root.render( {}} />); + }); + }).toErrorDev('Invalid value for prop `value`'); const node = container.firstChild; expect(node.value).toBe(''); @@ -2511,14 +2813,15 @@ describe('ReactDOMInput', () => { } }); - it('treats updated Symbol value as an empty string', function () { - ReactDOM.render( {}} />, container); - expect(() => - ReactDOM.render( - {}} />, - container, - ), - ).toErrorDev('Invalid value for prop `value`'); + it('treats updated Symbol value as an empty string', async () => { + await act(() => { + root.render( {}} />); + }); + await expect(async () => { + await act(() => { + root.render( {}} />); + }); + }).toErrorDev('Invalid value for prop `value`'); const node = container.firstChild; expect(node.value).toBe(''); @@ -2529,8 +2832,10 @@ describe('ReactDOMInput', () => { } }); - it('treats initial Symbol defaultValue as an empty string', function () { - ReactDOM.render(, container); + it('treats initial Symbol defaultValue as an empty string', async () => { + await act(() => { + root.render(); + }); const node = container.firstChild; expect(node.value).toBe(''); @@ -2538,9 +2843,13 @@ describe('ReactDOMInput', () => { // TODO: we should warn here. }); - it('treats updated Symbol defaultValue as an empty string', function () { - ReactDOM.render(, container); - ReactDOM.render(, container); + it('treats updated Symbol defaultValue as an empty string', async () => { + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); const node = container.firstChild; if (disableInputAttributeSyncing) { @@ -2554,13 +2863,12 @@ describe('ReactDOMInput', () => { }); describe('When given a function value', function () { - it('treats initial function value as an empty string', function () { - expect(() => - ReactDOM.render( - {}} onChange={() => {}} />, - container, - ), - ).toErrorDev('Invalid value for prop `value`'); + it('treats initial function value as an empty string', async () => { + await expect(async () => { + await act(() => { + root.render( {}} onChange={() => {}} />); + }); + }).toErrorDev('Invalid value for prop `value`'); const node = container.firstChild; expect(node.value).toBe(''); @@ -2571,14 +2879,15 @@ describe('ReactDOMInput', () => { } }); - it('treats updated function value as an empty string', function () { - ReactDOM.render( {}} />, container); - expect(() => - ReactDOM.render( - {}} onChange={() => {}} />, - container, - ), - ).toErrorDev('Invalid value for prop `value`'); + it('treats updated function value as an empty string', async () => { + await act(() => { + root.render( {}} />); + }); + await expect(async () => { + await act(() => { + root.render( {}} onChange={() => {}} />); + }); + }).toErrorDev('Invalid value for prop `value`'); const node = container.firstChild; expect(node.value).toBe(''); @@ -2589,8 +2898,10 @@ describe('ReactDOMInput', () => { } }); - it('treats initial function defaultValue as an empty string', function () { - ReactDOM.render( {}} />, container); + it('treats initial function defaultValue as an empty string', async () => { + await act(() => { + root.render( {}} />); + }); const node = container.firstChild; expect(node.value).toBe(''); @@ -2598,9 +2909,13 @@ describe('ReactDOMInput', () => { // TODO: we should warn here. }); - it('treats updated function defaultValue as an empty string', function () { - ReactDOM.render(, container); - ReactDOM.render( {}} />, container); + it('treats updated function defaultValue as an empty string', async () => { + await act(() => { + root.render(); + }); + await act(() => { + root.render( {}} />); + }); const node = container.firstChild; if (disableInputAttributeSyncing) { @@ -2619,19 +2934,20 @@ describe('ReactDOMInput', () => { // Between 16 and 16.2, we assigned a node's value to it's current // value in order to "dettach" it from defaultValue. This had the unfortunate // side-effect of assigning value="on" to radio and checkboxes - it('does not add "on" in absence of value on a checkbox', function () { - ReactDOM.render( - , - container, - ); + it('does not add "on" in absence of value on a checkbox', async () => { + await act(() => { + root.render(); + }); const node = container.firstChild; expect(node.value).toBe('on'); expect(node.hasAttribute('value')).toBe(false); }); - it('does not add "on" in absence of value on a radio', function () { - ReactDOM.render(, container); + it('does not add "on" in absence of value on a radio', async () => { + await act(() => { + root.render(); + }); const node = container.firstChild; expect(node.value).toBe('on'); @@ -2639,45 +2955,47 @@ describe('ReactDOMInput', () => { }); }); - it('should remove previous `defaultValue`', () => { - const node = ReactDOM.render( - , - container, - ); + it('should remove previous `defaultValue`', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(node.value).toBe('0'); expect(node.defaultValue).toBe('0'); - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(node.defaultValue).toBe(''); }); - it('should treat `defaultValue={null}` as missing', () => { - const node = ReactDOM.render( - , - container, - ); + it('should treat `defaultValue={null}` as missing', async () => { + await act(() => { + root.render(); + }); + const node = container.firstChild; expect(node.value).toBe('0'); expect(node.defaultValue).toBe('0'); - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(node.defaultValue).toBe(''); }); - it('should notice input changes when reverting back to original value', () => { + it('should notice input changes when reverting back to original value', async () => { const log = []; function onChange(e) { log.push(e.target.value); } - ReactDOM.render( - , - container, - ); - ReactDOM.render( - , - container, - ); + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); const node = container.firstChild; setUntrackedValue.call(node, '');