'use strict';
const {disableInputAttributeSyncing} = require('shared/ReactFeatureFlags');
function emptyFunction() {}
describe('ReactDOMInput', () => {
let React;
let ReactDOM;
let ReactDOMClient;
let ReactDOMServer;
let Scheduler;
let act;
let assertLog;
let setUntrackedValue;
let setUntrackedChecked;
let container;
let root;
function dispatchEventOnNode(node, type) {
node.dispatchEvent(new Event(type, {bubbles: true, cancelable: true}));
}
function isValueDirty(node) {
const copy = node.cloneNode();
copy.type = 'text';
copy.defaultValue += Math.random();
return copy.value === node.value;
}
function isCheckedDirty(node) {
if (node.checked !== node.defaultChecked) {
return true;
}
const copy = node.cloneNode();
copy.type = 'checkbox';
copy.defaultChecked = !copy.defaultChecked;
return copy.checked === node.checked;
}
function getTrackedAndCurrentInputValue(elem: HTMLElement): [mixed, mixed] {
const tracker = elem._valueTracker;
if (!tracker) {
throw new Error('No input tracker');
}
return [
tracker.getValue(),
elem.nodeName === 'INPUT' &&
(elem.type === 'checkbox' || elem.type === 'radio')
? String(elem.checked)
: elem.value,
];
}
function assertInputTrackingIsCurrent(parent) {
parent.querySelectorAll('input, textarea, select').forEach(input => {
const [trackedValue, currentValue] =
getTrackedAndCurrentInputValue(input);
if (trackedValue !== currentValue) {
throw new Error(
`Input ${input.outerHTML} is currently ${currentValue} but tracker thinks it's ${trackedValue}`,
);
}
});
}
beforeEach(() => {
jest.resetModules();
setUntrackedValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
).set;
setUntrackedChecked = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'checked',
).set;
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
assertLog = require('internal-test-utils').assertLog;
container = document.createElement('div');
document.body.appendChild(container);
root = ReactDOMClient.createRoot(container);
});
afterEach(() => {
document.body.removeChild(container);
jest.restoreAllMocks();
});
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(
'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`.',
);
});
it('should warn for controlled value of "" with missing onChange', async () => {
await expect(async () => {
await act(() => {
root.render(<input type="text" value="" />);
});
}).toErrorDev(
'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`.',
);
});
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(
'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`.',
);
});
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.',
);
});
it('should warn with checked and no onChange handler with readOnly specified', async () => {
await act(() => {
root.render(<input type="checkbox" checked={false} readOnly={true} />);
});
root.unmount();
root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<input type="checkbox" checked={false} readOnly={false} />);
});
}).toErrorDev(
'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', async () => {
await act(() => {
root.render(<input />);
});
root.unmount();
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<input value={undefined} />);
});
root.unmount();
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<input type="text" />);
});
root.unmount();
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<input type="text" value={undefined} />);
});
root.unmount();
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<input type="checkbox" />);
});
root.unmount();
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<input type="checkbox" checked={undefined} />);
});
});
it('should not warn with value and onInput handler', async () => {
await act(() => {
root.render(<input value="..." onInput={() => {}} />);
});
});
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.',
);
const node = container.firstChild;
expect(isValueDirty(node)).toBe(true);
setUntrackedValue.call(node, 'giraffe');
await act(() => {
dispatchEventOnNode(node, 'input');
});
expect(node.value).toBe('lion');
expect(isValueDirty(node)).toBe(true);
});
it('should control a value in reentrant events', async () => {
class ControlledInputs extends React.Component {
state = {value: 'lion'};
a = null;
b = null;
switchedFocus = false;
change(newValue) {
this.setState({value: newValue});
dispatchEventOnNode(this.a, 'input');
this.b.focus();
}
blur(currentValue) {
this.switchedFocus = true;
this.setState({value: currentValue});
}
render() {
return (
<div>
<input
type="text"
ref={n => (this.a = n)}
value={this.state.value}
onChange={e => this.change(e.target.value)}
onBlur={e => this.blur(e.target.value)}
/>
<input type="text" ref={n => (this.b = n)} />
</div>
);
}
}
const ref = React.createRef();
await act(() => {
root.render(<ControlledInputs ref={ref} />);
});
const instance = ref.current;
await act(() => {
instance.a.focus();
});
setUntrackedValue.call(instance.a, 'giraffe');
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', async () => {
class ControlledInputs extends React.Component {
state = {value: 'lion'};
a = null;
b = null;
change(newValue) {
this.b.click();
}
render() {
return (
<div>
<input
type="text"
ref={n => (this.a = n)}
value="lion"
onChange={e => this.change(e.target.value)}
/>
<input
type="checkbox"
ref={n => (this.b = n)}
checked={true}
onChange={() => {}}
/>
</div>
);
}
}
const ref = React.createRef();
await act(() => {
root.render(<ControlledInputs ref={ref} />);
});
const instance = ref.current;
setUntrackedValue.call(instance.a, 'giraffe');
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', async () => {
await act(() => {
root.render(<input type="text" value={2} onChange={jest.fn()} />);
});
const node = container.firstChild;
setUntrackedValue.call(node, '2.0');
dispatchEventOnNode(node, 'input');
expect(node.value).toBe('2');
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.getAttribute('value')).toBe('2');
}
});
it('does change the string "2" to "2.0" with no change handler', async () => {
await act(() => {
root.render(<input type="text" value={'2'} onChange={jest.fn()} />);
});
const node = container.firstChild;
setUntrackedValue.call(node, '2.0');
dispatchEventOnNode(node, 'input');
expect(node.value).toBe('2');
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.getAttribute('value')).toBe('2');
}
});
it('changes the number 2 to "2.0" using a change handler', async () => {
class Stub extends React.Component {
state = {
value: 2,
};
onChange = event => {
this.setState({value: event.target.value});
};
render() {
const {value} = this.state;
return <input type="text" value={value} onChange={this.onChange} />;
}
}
await act(() => {
root.render(<Stub />);
});
const node = container.firstChild;
setUntrackedValue.call(node, '2.0');
dispatchEventOnNode(node, 'input');
expect(node.value).toBe('2.0');
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.getAttribute('value')).toBe('2.0');
}
});
});
it('does change the string ".98" to "0.98" with no change handler', async () => {
class Stub extends React.Component {
state = {
value: '.98',
};
render() {
return <input type="number" value={this.state.value} />;
}
}
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.',
);
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', async () => {
class Stub extends React.Component {
state = {
value: '',
};
render() {
return <input type="number" value={this.state.value} readOnly={true} />;
}
}
const ref = React.createRef();
await act(() => {
root.render(<Stub ref={ref} />);
});
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', async () => {
await act(() => {
root.render(<input type="radio" value="" onChange={function () {}} />);
});
await act(() => {
root.render(<input type="radio" value={0} onChange={function () {}} />);
});
expect(container.firstChild.value).toBe('0');
expect(container.firstChild.getAttribute('value')).toBe('0');
});
it('updates the value on checkboxes from "" to 0', async () => {
await act(() => {
root.render(<input type="checkbox" value="" onChange={function () {}} />);
});
await act(() => {
root.render(
<input type="checkbox" value={0} onChange={function () {}} />,
);
});
expect(container.firstChild.value).toBe('0');
expect(container.firstChild.getAttribute('value')).toBe('0');
});
it('distinguishes precision for extra zeroes in string number values', async () => {
class Stub extends React.Component {
state = {
value: '3.0000',
};
render() {
return <input type="number" value={this.state.value} />;
}
}
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.',
);
const node = container.firstChild;
await act(() => {
ref.current.setState({value: '3'});
});
expect(node.value).toEqual('3');
});
it('should display `defaultValue` of number 0', async () => {
await act(() => {
root.render(<input type="text" defaultValue={0} />);
});
const node = container.firstChild;
expect(node.getAttribute('value')).toBe('0');
expect(node.value).toBe('0');
});
it('only assigns defaultValue if it changes', async () => {
class Test extends React.Component {
render() {
return <input defaultValue="0" />;
}
}
const ref = React.createRef();
await act(() => {
root.render(<Test ref={ref} />);
});
const node = container.firstChild;
Object.defineProperty(node, 'defaultValue', {
get() {
return '0';
},
set(value) {
throw new Error(
`defaultValue was assigned ${value}, but it did not change!`,
);
},
});
await act(() => {
ref.current.forceUpdate();
});
});
it('should display "true" for `defaultValue` of `true`', async () => {
const stub = <input type="text" defaultValue={true} />;
await act(() => {
root.render(stub);
});
const node = container.firstChild;
expect(node.value).toBe('true');
});
it('should display "false" for `defaultValue` of `false`', async () => {
const stub = <input type="text" defaultValue={false} />;
await act(() => {
root.render(stub);
});
const node = container.firstChild;
expect(node.value).toBe('false');
});
it('should update `defaultValue` for uncontrolled input', async () => {
await act(() => {
root.render(<input type="text" defaultValue="0" />);
});
const node = container.firstChild;
expect(node.value).toBe('0');
expect(node.defaultValue).toBe('0');
if (disableInputAttributeSyncing) {
expect(isValueDirty(node)).toBe(false);
} else {
expect(isValueDirty(node)).toBe(true);
}
await act(() => {
root.render(<input type="text" defaultValue="1" />);
});
if (disableInputAttributeSyncing) {
expect(node.value).toBe('1');
expect(node.defaultValue).toBe('1');
expect(isValueDirty(node)).toBe(false);
} else {
expect(node.value).toBe('0');
expect(node.defaultValue).toBe('1');
expect(isValueDirty(node)).toBe(true);
}
});
it('should update `defaultValue` for uncontrolled date/time input', async () => {
await act(() => {
root.render(<input type="date" defaultValue="1980-01-01" />);
});
const node = container.firstChild;
expect(node.value).toBe('1980-01-01');
expect(node.defaultValue).toBe('1980-01-01');
await act(() => {
root.render(<input type="date" defaultValue="2000-01-01" />);
});
if (disableInputAttributeSyncing) {
expect(node.value).toBe('2000-01-01');
expect(node.defaultValue).toBe('2000-01-01');
} else {
expect(node.value).toBe('1980-01-01');
expect(node.defaultValue).toBe('2000-01-01');
}
await act(() => {
root.render(<input type="date" />);
});
});
it('should take `defaultValue` when changing to uncontrolled input', async () => {
await act(() => {
root.render(<input type="text" value="0" readOnly={true} />);
});
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.',
);
expect(node.value).toBe('0');
expect(isValueDirty(node)).toBe(true);
});
it('should render defaultValue for SSR', () => {
const markup = ReactDOMServer.renderToString(
<input type="text" defaultValue="1" />,
);
const div = document.createElement('div');
div.innerHTML = markup;
expect(div.firstChild.getAttribute('value')).toBe('1');
expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
});
it('should render bigint defaultValue for SSR', () => {
const markup = ReactDOMServer.renderToString(
<input type="text" defaultValue={5n} />,
);
const div = document.createElement('div');
div.innerHTML = markup;
expect(div.firstChild.getAttribute('value')).toBe('5');
expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
});
it('should render value for SSR', () => {
const element = <input type="text" value="1" onChange={() => {}} />;
const markup = ReactDOMServer.renderToString(element);
const div = document.createElement('div');
div.innerHTML = markup;
expect(div.firstChild.getAttribute('value')).toBe('1');
expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
});
it('should render bigint value for SSR', () => {
const element = <input type="text" value={5n} onChange={() => {}} />;
const markup = ReactDOMServer.renderToString(element);
const div = document.createElement('div');
div.innerHTML = markup;
expect(div.firstChild.getAttribute('value')).toBe('5');
expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
});
it('should render name attribute if it is supplied', async () => {
await act(() => {
root.render(<input type="text" name="name" />);
});
const node = container.firstChild;
expect(node.name).toBe('name');
expect(container.firstChild.getAttribute('name')).toBe('name');
});
it('should render name attribute if it is supplied for SSR', () => {
const element = <input type="text" name="name" />;
const markup = ReactDOMServer.renderToString(element);
const div = document.createElement('div');
div.innerHTML = markup;
expect(div.firstChild.getAttribute('name')).toBe('name');
});
it('should not render name attribute if it is not supplied', async () => {
await act(() => {
root.render(<input type="text" />);
});
expect(container.firstChild.getAttribute('name')).toBe(null);
});
it('should not render name attribute if it is not supplied for SSR', () => {
const element = <input type="text" />;
const markup = ReactDOMServer.renderToString(element);
const div = document.createElement('div');
div.innerHTML = markup;
expect(div.firstChild.getAttribute('name')).toBe(null);
});
it('should display "foobar" for `defaultValue` of `objToString`', async () => {
const objToString = {
toString: function () {
return 'foobar';
},
};
const stub = <input type="text" defaultValue={objToString} />;
await act(() => {
root.render(stub);
});
const node = container.firstChild;
expect(node.value).toBe('foobar');
});
it('should throw for date inputs if `defaultValue` is an object where valueOf() throws', async () => {
class TemporalLike {
valueOf() {
throw new TypeError('prod message');
}
toString() {
return '2020-01-01';
}
}
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.',
);
}).rejects.toThrowError(new TypeError('prod message'));
});
it('should throw for text inputs if `defaultValue` is an object where valueOf() throws', async () => {
class TemporalLike {
valueOf() {
throw new TypeError('prod message');
}
toString() {
return '2020-01-01';
}
}
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.',
);
}).rejects.toThrowError(new TypeError('prod message'));
});
it('should throw for date inputs if `value` is an object where valueOf() throws', async () => {
class TemporalLike {
valueOf() {
throw new TypeError('prod message');
}
toString() {
return '2020-01-01';
}
}
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.',
);
}).rejects.toThrowError(new TypeError('prod message'));
});
it('should throw for text inputs if `value` is an object where valueOf() throws', async () => {
class TemporalLike {
valueOf() {
throw new TypeError('prod message');
}
toString() {
return '2020-01-01';
}
}
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.',
);
}).rejects.toThrowError(new TypeError('prod message'));
});
it('should display `value` of number 0', async () => {
await act(() => {
root.render(<input type="text" value={0} onChange={emptyFunction} />);
});
const node = container.firstChild;
expect(node.value).toBe('0');
});
it('should display `value` of bigint 5', async () => {
await act(() => {
root.render(<input type="text" value={5n} onChange={emptyFunction} />);
});
const node = container.firstChild;
expect(node.value).toBe('5');
});
it('should allow setting `value` to `true`', async () => {
await act(() => {
root.render(<input type="text" value="yolo" onChange={emptyFunction} />);
});
const node = container.firstChild;
expect(node.value).toBe('yolo');
await act(() => {
root.render(<input type="text" value={true} onChange={emptyFunction} />);
});
expect(node.value).toEqual('true');
});
it('should allow setting `value` to `false`', async () => {
await act(() => {
root.render(<input type="text" value="yolo" onChange={emptyFunction} />);
});
const node = container.firstChild;
expect(node.value).toBe('yolo');
await act(() => {
root.render(<input type="text" value={false} onChange={emptyFunction} />);
});
expect(node.value).toEqual('false');
});
it('should allow setting `value` to `objToString`', async () => {
await act(() => {
root.render(<input type="text" value="foo" onChange={emptyFunction} />);
});
const node = container.firstChild;
expect(node.value).toBe('foo');
const objToString = {
toString: function () {
return 'foobar';
},
};
await act(() => {
root.render(
<input type="text" value={objToString} onChange={emptyFunction} />,
);
});
expect(node.value).toEqual('foobar');
});
it('should not incur unnecessary DOM mutations', async () => {
await act(() => {
root.render(<input value="a" onChange={() => {}} />);
});
const node = container.firstChild;
let nodeValue = 'a';
const nodeValueSetter = jest.fn();
Object.defineProperty(node, 'value', {
get: function () {
return nodeValue;
},
set: nodeValueSetter.mockImplementation(function (newValue) {
nodeValue = newValue;
}),
});
await act(() => {
root.render(<input value="a" onChange={() => {}} />);
});
expect(nodeValueSetter).toHaveBeenCalledTimes(0);
await act(() => {
root.render(<input value="b" onChange={() => {}} />);
});
expect(nodeValueSetter).toHaveBeenCalledTimes(1);
});
it('should not incur unnecessary DOM mutations for numeric type conversion', async () => {
await act(() => {
root.render(<input value="0" onChange={() => {}} />);
});
const node = container.firstChild;
let nodeValue = '0';
const nodeValueSetter = jest.fn();
Object.defineProperty(node, 'value', {
get: function () {
return nodeValue;
},
set: nodeValueSetter.mockImplementation(function (newValue) {
nodeValue = newValue;
}),
});
await act(() => {
root.render(<input value={0} onChange={() => {}} />);
});
expect(nodeValueSetter).toHaveBeenCalledTimes(0);
});
it('should not incur unnecessary DOM mutations for the boolean type conversion', async () => {
await act(() => {
root.render(<input value="true" onChange={() => {}} />);
});
const node = container.firstChild;
let nodeValue = 'true';
const nodeValueSetter = jest.fn();
Object.defineProperty(node, 'value', {
get: function () {
return nodeValue;
},
set: nodeValueSetter.mockImplementation(function (newValue) {
nodeValue = newValue;
}),
});
await act(() => {
root.render(<input value={true} onChange={() => {}} />);
});
expect(nodeValueSetter).toHaveBeenCalledTimes(0);
});
it('should properly control a value of number `0`', async () => {
await act(() => {
root.render(<input type="text" value={0} onChange={emptyFunction} />);
});
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', async () => {
await act(() => {
root.render(<input type="text" value={0} onChange={emptyFunction} />);
});
const node = container.firstChild;
setUntrackedValue.call(node, '0.0');
await act(() => {
dispatchEventOnNode(node, 'input');
});
expect(node.value).toBe('0');
});
it('should properly control 0.0 for a number input', async () => {
await act(() => {
root.render(<input type="number" value={0} onChange={emptyFunction} />);
});
const node = container.firstChild;
setUntrackedValue.call(node, '0.0');
await act(() => {
dispatchEventOnNode(node, 'input');
});
if (disableInputAttributeSyncing) {
expect(node.value).toBe('0.0');
expect(node.hasAttribute('value')).toBe(false);
} else {
dispatchEventOnNode(node, 'blur');
dispatchEventOnNode(node, 'focusout');
expect(node.value).toBe('0.0');
expect(node.getAttribute('value')).toBe('0.0');
}
});
it('should properly transition from an empty value to 0', async () => {
await act(() => {
root.render(<input type="text" value="" onChange={emptyFunction} />);
});
const node = container.firstChild;
expect(isValueDirty(node)).toBe(false);
await act(() => {
root.render(<input type="text" value={0} onChange={emptyFunction} />);
});
expect(node.value).toBe('0');
expect(isValueDirty(node)).toBe(true);
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.defaultValue).toBe('0');
}
});
it('should properly transition from 0 to an empty value', async () => {
await act(() => {
root.render(<input type="text" value={0} onChange={emptyFunction} />);
});
const node = container.firstChild;
expect(isValueDirty(node)).toBe(true);
await act(() => {
root.render(<input type="text" value="" onChange={emptyFunction} />);
});
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', async () => {
await act(() => {
root.render(<input type="text" value={0} onChange={emptyFunction} />);
});
await act(() => {
root.render(<input type="text" value="0.0" onChange={emptyFunction} />);
});
const node = container.firstChild;
expect(node.value).toBe('0.0');
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.defaultValue).toBe('0.0');
}
});
it('should properly transition a number input from "" to 0', async () => {
await act(() => {
root.render(<input type="number" value="" onChange={emptyFunction} />);
});
await act(() => {
root.render(<input type="number" value={0} onChange={emptyFunction} />);
});
const node = container.firstChild;
expect(node.value).toBe('0');
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.defaultValue).toBe('0');
}
});
it('should properly transition a number input from "" to "0"', async () => {
await act(() => {
root.render(<input type="number" value="" onChange={emptyFunction} />);
});
await act(() => {
root.render(<input type="number" value="0" onChange={emptyFunction} />);
});
const node = container.firstChild;
expect(node.value).toBe('0');
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.defaultValue).toBe('0');
}
});
it('should have the correct target value', async () => {
let handled = false;
const handler = function (event) {
expect(event.target.nodeName).toBe('INPUT');
handled = true;
};
await act(() => {
root.render(<input type="text" value={0} onChange={handler} />);
});
const node = container.firstChild;
setUntrackedValue.call(node, 'giraffe');
await act(() => {
dispatchEventOnNode(node, 'input');
});
expect(handled).toBe(true);
});
it('should restore uncontrolled inputs to last defaultValue upon reset', async () => {
const inputRef = React.createRef();
await act(() => {
root.render(
<form>
<input defaultValue="default1" ref={inputRef} />
<input type="reset" />
</form>,
);
});
expect(inputRef.current.value).toBe('default1');
if (disableInputAttributeSyncing) {
expect(isValueDirty(inputRef.current)).toBe(false);
} else {
expect(isValueDirty(inputRef.current)).toBe(true);
}
setUntrackedValue.call(inputRef.current, 'changed');
dispatchEventOnNode(inputRef.current, 'input');
expect(inputRef.current.value).toBe('changed');
expect(isValueDirty(inputRef.current)).toBe(true);
await act(() => {
root.render(
<form>
<input defaultValue="default2" ref={inputRef} />
<input type="reset" />
</form>,
);
});
expect(inputRef.current.value).toBe('changed');
expect(isValueDirty(inputRef.current)).toBe(true);
container.firstChild.reset();
expect(inputRef.current.value).toBe('default2');
expect(isValueDirty(inputRef.current)).toBe(false);
});
it('should not set a value for submit buttons unnecessarily', async () => {
const stub = <input type="submit" />;
await act(() => {
root.render(stub);
});
const node = container.firstChild;
expect(node.hasAttribute('value')).toBe(false);
});
it('should remove the value attribute on submit inputs when value is updated to undefined', async () => {
const stub = <input type="submit" value="foo" onChange={emptyFunction} />;
await act(() => {
root.render(stub);
});
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.',
);
const node = container.firstChild;
expect(node.getAttribute('value')).toBe(null);
});
it('should remove the value attribute on reset inputs when value is updated to undefined', async () => {
const stub = <input type="reset" value="foo" onChange={emptyFunction} />;
await act(() => {
root.render(stub);
});
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.',
);
const node = container.firstChild;
expect(node.getAttribute('value')).toBe(null);
});
it('should set a value on a submit input', async () => {
const stub = <input type="submit" value="banana" />;
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', async () => {
const stub = <input type="submit" value={undefined} />;
await act(() => {
root.render(stub);
});
const node = container.firstChild;
expect(node.getAttribute('value')).toBe(null);
await act(() => {
root.render(stub);
});
expect(node.getAttribute('value')).toBe(null);
});
it('should not set an undefined value on a reset input', async () => {
const stub = <input type="reset" value={undefined} />;
await act(() => {
root.render(stub);
});
const node = container.firstChild;
expect(node.getAttribute('value')).toBe(null);
await act(() => {
root.render(stub);
});
expect(node.getAttribute('value')).toBe(null);
});
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');
const node = container.firstChild;
expect(node.getAttribute('value')).toBe(null);
await act(() => {
root.render(stub);
});
expect(node.getAttribute('value')).toBe(null);
});
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');
const node = container.firstChild;
expect(node.getAttribute('value')).toBe(null);
await act(() => {
root.render(stub);
});
expect(node.getAttribute('value')).toBe(null);
});
it('should set a value on a reset input', async () => {
const stub = <input type="reset" value="banana" />;
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', async () => {
const stub = <input type="submit" value="" />;
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', async () => {
const stub = <input type="reset" value="" />;
await act(() => {
root.render(stub);
});
const node = container.firstChild;
expect(node.getAttribute('value')).toBe('');
});
it('should control radio buttons', async () => {
class RadioGroup extends React.Component {
aRef = React.createRef();
bRef = React.createRef();
cRef = React.createRef();
render() {
return (
<div>
<input
ref={this.aRef}
type="radio"
name="fruit"
checked={true}
onChange={emptyFunction}
data-which="a"
/>
A
<input
ref={this.bRef}
type="radio"
name="fruit"
onChange={emptyFunction}
data-which="b"
/>
B
<form>
<input
ref={this.cRef}
type="radio"
name="fruit"
defaultChecked={true}
onChange={emptyFunction}
data-which="c"
/>
</form>
</div>
);
}
}
const ref = React.createRef();
await act(() => {
root.render(<RadioGroup ref={ref} />);
});
const stub = ref.current;
const aNode = stub.aRef.current;
const bNode = stub.bRef.current;
const cNode = stub.cRef.current;
expect(aNode.checked).toBe(true);
expect(bNode.checked).toBe(false);
expect(cNode.checked).toBe(true);
if (disableInputAttributeSyncing) {
expect(aNode.hasAttribute('checked')).toBe(false);
expect(bNode.hasAttribute('checked')).toBe(false);
expect(cNode.hasAttribute('checked')).toBe(true);
} else {
expect(aNode.hasAttribute('checked')).toBe(true);
expect(bNode.hasAttribute('checked')).toBe(false);
expect(cNode.hasAttribute('checked')).toBe(true);
}
expect(isCheckedDirty(aNode)).toBe(true);
expect(isCheckedDirty(bNode)).toBe(true);
expect(isCheckedDirty(cNode)).toBe(true);
assertInputTrackingIsCurrent(container);
setUntrackedChecked.call(bNode, true);
expect(aNode.checked).toBe(false);
expect(cNode.checked).toBe(true);
if (disableInputAttributeSyncing) {
expect(aNode.hasAttribute('checked')).toBe(false);
expect(bNode.hasAttribute('checked')).toBe(false);
expect(cNode.hasAttribute('checked')).toBe(true);
} else {
expect(aNode.hasAttribute('checked')).toBe(true);
expect(bNode.hasAttribute('checked')).toBe(false);
expect(cNode.hasAttribute('checked')).toBe(true);
}
await act(() => {
dispatchEventOnNode(bNode, 'click');
});
expect(aNode.checked).toBe(true);
expect(cNode.checked).toBe(true);
expect(isCheckedDirty(aNode)).toBe(true);
expect(isCheckedDirty(bNode)).toBe(true);
expect(isCheckedDirty(cNode)).toBe(true);
assertInputTrackingIsCurrent(container);
});
it('should hydrate controlled radio buttons', async () => {
function App() {
const [current, setCurrent] = React.useState('a');
return (
<>
<input
type="radio"
name="fruit"
checked={current === 'a'}
onChange={() => {
Scheduler.log('click a');
setCurrent('a');
}}
/>
<input
type="radio"
name="fruit"
checked={current === 'b'}
onChange={() => {
Scheduler.log('click b');
setCurrent('b');
}}
/>
<input
type="radio"
name="fruit"
checked={current === 'c'}
onChange={() => {
Scheduler.log('click c');
// Let's say the user can't pick C
}}
/>
</>
);
}
const html = ReactDOMServer.renderToString(<App />);
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);
expect(b.checked).toBe(false);
expect(c.checked).toBe(false);
expect(isCheckedDirty(a)).toBe(false);
expect(isCheckedDirty(b)).toBe(false);
expect(isCheckedDirty(c)).toBe(false);
b.checked = true;
expect(isCheckedDirty(a)).toBe(true);
expect(isCheckedDirty(b)).toBe(true);
expect(isCheckedDirty(c)).toBe(false);
await act(async () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
assertLog([]);
expect(a.checked).toBe(false);
expect(b.checked).toBe(true);
expect(c.checked).toBe(false);
expect(isCheckedDirty(a)).toBe(true);
expect(isCheckedDirty(b)).toBe(true);
expect(isCheckedDirty(c)).toBe(true);
assertInputTrackingIsCurrent(container);
await act(async () => {
setUntrackedChecked.call(c, true);
dispatchEventOnNode(c, 'click');
});
assertLog(['click c']);
expect(a.checked).toBe(true);
expect(b.checked).toBe(false);
expect(c.checked).toBe(false);
expect(isCheckedDirty(a)).toBe(true);
expect(isCheckedDirty(b)).toBe(true);
expect(isCheckedDirty(c)).toBe(true);
assertInputTrackingIsCurrent(container);
await act(async () => {
setUntrackedChecked.call(b, true);
dispatchEventOnNode(b, 'click');
});
assertLog(['click b']);
expect(a.checked).toBe(false);
expect(b.checked).toBe(true);
expect(c.checked).toBe(false);
expect(isCheckedDirty(a)).toBe(true);
expect(isCheckedDirty(b)).toBe(true);
expect(isCheckedDirty(c)).toBe(true);
assertInputTrackingIsCurrent(container);
});
it('should hydrate uncontrolled radio buttons', async () => {
function App() {
return (
<>
<input
type="radio"
name="fruit"
defaultChecked={true}
onChange={() => Scheduler.log('click a')}
/>
<input
type="radio"
name="fruit"
defaultChecked={false}
onChange={() => Scheduler.log('click b')}
/>
<input
type="radio"
name="fruit"
defaultChecked={false}
onChange={() => Scheduler.log('click c')}
/>
</>
);
}
const html = ReactDOMServer.renderToString(<App />);
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);
expect(b.checked).toBe(false);
expect(c.checked).toBe(false);
expect(isCheckedDirty(a)).toBe(false);
expect(isCheckedDirty(b)).toBe(false);
expect(isCheckedDirty(c)).toBe(false);
b.checked = true;
expect(isCheckedDirty(a)).toBe(true);
expect(isCheckedDirty(b)).toBe(true);
expect(isCheckedDirty(c)).toBe(false);
await act(async () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
assertLog([]);
expect(a.checked).toBe(false);
expect(b.checked).toBe(true);
expect(c.checked).toBe(false);
expect(isCheckedDirty(a)).toBe(true);
expect(isCheckedDirty(b)).toBe(true);
expect(isCheckedDirty(c)).toBe(true);
assertInputTrackingIsCurrent(container);
await act(async () => {
setUntrackedChecked.call(a, true);
dispatchEventOnNode(a, 'click');
});
assertLog(['click a']);
expect(a.checked).toBe(true);
expect(b.checked).toBe(false);
expect(c.checked).toBe(false);
expect(isCheckedDirty(a)).toBe(true);
expect(isCheckedDirty(b)).toBe(true);
expect(isCheckedDirty(c)).toBe(true);
assertInputTrackingIsCurrent(container);
});
it('should check the correct radio when the selected name moves', async () => {
class App extends React.Component {
state = {
updated: false,
};
onClick = () => {
this.setState({updated: !this.state.updated});
};
render() {
const {updated} = this.state;
const radioName = updated ? 'secondName' : 'firstName';
return (
<div>
<button type="button" onClick={this.onClick} />
<input
type="radio"
name={radioName}
onChange={emptyFunction}
checked={updated === true}
/>
<input
type="radio"
name={radioName}
onChange={emptyFunction}
checked={updated === false}
/>
</div>
);
}
}
await act(() => {
root.render(<App />);
});
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);
await act(() => {
dispatchEventOnNode(buttonNode, 'click');
});
expect(firstRadioNode.checked).toBe(true);
assertInputTrackingIsCurrent(container);
await act(() => {
dispatchEventOnNode(buttonNode, 'click');
});
expect(firstRadioNode.checked).toBe(false);
assertInputTrackingIsCurrent(container);
});
it("shouldn't get tricked by changing radio names, part 2", async () => {
await act(() => {
root.render(
<div>
<input
type="radio"
name="a"
value="1"
checked={true}
onChange={() => {}}
/>
<input
type="radio"
name="a"
value="2"
checked={false}
onChange={() => {}}
/>
</div>,
);
});
const one = container.querySelector('input[name="a"][value="1"]');
const two = container.querySelector('input[name="a"][value="2"]');
expect(one.checked).toBe(true);
expect(two.checked).toBe(false);
expect(isCheckedDirty(one)).toBe(true);
expect(isCheckedDirty(two)).toBe(true);
assertInputTrackingIsCurrent(container);
await act(() => {
root.render(
<div>
<input
type="radio"
name="a"
value="1"
checked={true}
onChange={() => {}}
/>
<input
type="radio"
name="b"
value="2"
checked={true}
onChange={() => {}}
/>
</div>,
);
});
expect(one.checked).toBe(true);
expect(two.checked).toBe(true);
expect(isCheckedDirty(one)).toBe(true);
expect(isCheckedDirty(two)).toBe(true);
assertInputTrackingIsCurrent(container);
});
it('should control radio buttons if the tree updates during render in legacy mode', async () => {
container.remove();
container = document.createElement('div');
document.body.appendChild(container);
const sharedParent = container;
const container1 = document.createElement('div');
const container2 = document.createElement('div');
sharedParent.appendChild(container1);
let aNode;
let bNode;
class ComponentA extends React.Component {
state = {changed: false};
handleChange = () => {
this.setState({
changed: true,
});
};
componentDidUpdate() {
sharedParent.appendChild(container2);
}
componentDidMount() {
ReactDOM.render(<ComponentB />, container2);
}
render() {
return (
<div>
<input
ref={n => (aNode = n)}
type="radio"
name="fruit"
checked={false}
onChange={this.handleChange}
/>
A
</div>
);
}
}
class ComponentB extends React.Component {
render() {
return (
<div>
<input
ref={n => (bNode = n)}
type="radio"
name="fruit"
checked={true}
onChange={emptyFunction}
/>
B
</div>
);
}
}
ReactDOM.render(<ComponentA />, container1);
expect(aNode.checked).toBe(false);
expect(bNode.checked).toBe(true);
expect(isCheckedDirty(aNode)).toBe(true);
expect(isCheckedDirty(bNode)).toBe(true);
assertInputTrackingIsCurrent(container);
setUntrackedChecked.call(aNode, true);
setUntrackedChecked.call(bNode, false);
dispatchEventOnNode(aNode, 'click');
expect(aNode.checked).toBe(false);
expect(bNode.checked).toBe(true);
expect(isCheckedDirty(aNode)).toBe(true);
expect(isCheckedDirty(bNode)).toBe(true);
assertInputTrackingIsCurrent(container);
});
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);
const [value, setValue] = React.useState('one');
function handleChange(e) {
setDisabled(true);
thunk = () => {
setDisabled(false);
setValue(e.target.value);
};
}
return (
<>
<input
type="radio"
name="fruit"
value="one"
checked={value === 'one'}
onChange={handleChange}
disabled={disabled}
/>
<input
type="radio"
name="fruit"
value="two"
checked={value === 'two'}
onChange={handleChange}
disabled={disabled}
/>
</>
);
}
await act(() => {
root.render(<App />);
});
const [one, two] = container.querySelectorAll('input');
expect(one.checked).toBe(true);
expect(two.checked).toBe(false);
expect(isCheckedDirty(one)).toBe(true);
expect(isCheckedDirty(two)).toBe(true);
assertInputTrackingIsCurrent(container);
setUntrackedChecked.call(two, true);
await act(() => {
dispatchEventOnNode(two, 'click');
});
expect(one.checked).toBe(true);
expect(two.checked).toBe(false);
expect(isCheckedDirty(one)).toBe(true);
expect(isCheckedDirty(two)).toBe(true);
assertInputTrackingIsCurrent(container);
await act(thunk);
expect(one.checked).toBe(false);
expect(two.checked).toBe(true);
expect(isCheckedDirty(one)).toBe(true);
expect(isCheckedDirty(two)).toBe(true);
assertInputTrackingIsCurrent(container);
setUntrackedChecked.call(one, true);
await act(() => {
dispatchEventOnNode(one, 'click');
});
expect(one.checked).toBe(false);
expect(two.checked).toBe(true);
expect(isCheckedDirty(one)).toBe(true);
expect(isCheckedDirty(two)).toBe(true);
assertInputTrackingIsCurrent(container);
await act(thunk);
expect(one.checked).toBe(true);
expect(two.checked).toBe(false);
expect(isCheckedDirty(one)).toBe(true);
expect(isCheckedDirty(two)).toBe(true);
assertInputTrackingIsCurrent(container);
});
it('should warn with value and no onChange handler and readOnly specified', async () => {
await act(() => {
root.render(<input type="text" value="zoink" readOnly={true} />);
});
root.unmount();
root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<input type="text" value="zoink" readOnly={false} />);
});
}).toErrorDev(
'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 () => {
expect.assertions(1);
const unboundInputOnChange = function () {
expect(this).toBe(undefined);
};
const stub = <input type="text" onChange={unboundInputOnChange} />;
await act(() => {
root.render(stub);
});
const node = container.firstChild;
setUntrackedValue.call(node, 'giraffe');
await act(() => {
dispatchEventOnNode(node, 'input');
});
});
it('should update defaultValue to empty string', async () => {
await act(() => {
root.render(<input type="text" defaultValue={'foo'} />);
});
if (disableInputAttributeSyncing) {
expect(isValueDirty(container.firstChild)).toBe(false);
} else {
expect(isValueDirty(container.firstChild)).toBe(true);
}
await act(() => {
root.render(<input type="text" defaultValue={''} />);
});
expect(container.firstChild.defaultValue).toBe('');
if (disableInputAttributeSyncing) {
expect(isValueDirty(container.firstChild)).toBe(false);
} else {
expect(isValueDirty(container.firstChild)).toBe(true);
}
});
it('should warn if value is null', async () => {
await expect(async () => {
await act(() => {
root.render(<input type="text" value={null} />);
});
}).toErrorDev(
'`value` prop on `input` should not be null. ' +
'Consider using an empty string to clear the component or `undefined` ' +
'for uncontrolled components.',
);
root.unmount();
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<input type="text" value={null} />);
});
});
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(
'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',
);
root.unmount();
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<input
type="radio"
checked={true}
defaultChecked={true}
readOnly={true}
/>,
);
});
});
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(
'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',
);
await (() => {
root.unmount();
});
await act(() => {
root.render(
<input type="text" value="foo" defaultValue="bar" readOnly={true} />,
);
});
});
it('should warn if controlled input switches to uncontrolled (value is undefined)', async () => {
const stub = (
<input type="text" value="controlled" onChange={emptyFunction} />
);
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="text" />);
});
}).toErrorDev(
'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 () => {
const stub = (
<input type="text" value="controlled" onChange={emptyFunction} />
);
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="text" value={null} />);
});
}).toErrorDev([
'`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. ' +
'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 with defaultValue', async () => {
const stub = (
<input type="text" value="controlled" onChange={emptyFunction} />
);
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="text" defaultValue="uncontrolled" />);
});
}).toErrorDev(
'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 () => {
const stub = <input type="text" />;
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="text" value="controlled" />);
});
}).toErrorDev(
'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(
'`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(
'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 () => {
const stub = (
<input type="checkbox" checked={true} onChange={emptyFunction} />
);
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="checkbox" />);
});
}).toErrorDev(
'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 () => {
const stub = (
<input type="checkbox" checked={true} onChange={emptyFunction} />
);
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="checkbox" checked={null} />);
});
}).toErrorDev(
'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 () => {
const stub = (
<input type="checkbox" checked={true} onChange={emptyFunction} />
);
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="checkbox" defaultChecked={true} />);
});
}).toErrorDev(
'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 () => {
const stub = <input type="checkbox" />;
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="checkbox" checked={true} />);
});
}).toErrorDev(
'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 () => {
const stub = <input type="checkbox" checked={null} />;
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="checkbox" checked={true} />);
});
}).toErrorDev(
'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 () => {
const stub = <input type="radio" checked={true} onChange={emptyFunction} />;
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="radio" />);
});
}).toErrorDev(
'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 () => {
const stub = <input type="radio" checked={true} onChange={emptyFunction} />;
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="radio" checked={null} />);
});
}).toErrorDev(
'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 () => {
const stub = <input type="radio" checked={true} onChange={emptyFunction} />;
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="radio" defaultChecked={true} />);
});
}).toErrorDev(
'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 () => {
const stub = <input type="radio" />;
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="radio" checked={true} />);
});
}).toErrorDev(
'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 () => {
const stub = <input type="radio" checked={null} />;
await act(() => {
root.render(stub);
});
await expect(async () => {
await act(() => {
root.render(<input type="radio" checked={true} />);
});
}).toErrorDev(
'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 () => {
await act(() => {
root.render(<input type="radio" value="value" />);
});
await act(() => {
root.render(<input type="radio" />);
});
await act(() => {
root.render(<input type="radio" value="value" defaultChecked={true} />);
});
await act(() => {
root.render(<input type="radio" value="value" onChange={() => null} />);
});
await act(() => {
root.render(<input type="radio" />);
});
});
it('should not warn if radio value changes but never becomes uncontrolled', async () => {
await act(() => {
root.render(<input type="radio" checked={false} onChange={() => null} />);
});
const input = container.querySelector('input');
expect(isCheckedDirty(input)).toBe(true);
await act(() => {
root.render(
<input
type="radio"
value="value"
defaultChecked={true}
checked={false}
onChange={() => null}
/>,
);
});
expect(isCheckedDirty(input)).toBe(true);
assertInputTrackingIsCurrent(container);
});
it('should warn if radio checked false changes to become uncontrolled', async () => {
await act(() => {
root.render(
<input
type="radio"
value="value"
checked={false}
onChange={() => null}
/>,
);
});
await expect(async () => {
await act(() => {
root.render(<input type="radio" value="value" />);
});
}).toErrorDev(
'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 () => {
const log = [];
const originalCreateElement = document.createElement;
spyOnDevAndProd(document, 'createElement').mockImplementation(
function (type) {
const el = originalCreateElement.apply(this, arguments);
let value = '';
let typeProp = '';
if (type === 'input') {
Object.defineProperty(el, 'type', {
get: function () {
return typeProp;
},
set: function (val) {
typeProp = String(val);
log.push('set property type');
},
});
Object.defineProperty(el, 'value', {
get: function () {
return value;
},
set: function (val) {
value = String(val);
log.push('set property value');
},
});
spyOnDevAndProd(el, 'setAttribute').mockImplementation(
function (name) {
log.push('set attribute ' + name);
},
);
}
return el;
},
);
await act(() => {
root.render(
<input
value="0"
onChange={() => {}}
type="range"
min="0"
max="100"
step="1"
/>,
);
});
expect(log).toEqual([
'set attribute min',
'set attribute max',
'set attribute step',
'set property type',
'set property value',
]);
});
it('sets value properly with type coming later in props', async () => {
await act(() => {
root.render(<input value="hi" type="radio" />);
});
expect(container.firstChild.value).toBe('hi');
});
it('does not raise a validation warning when it switches types', async () => {
class Input extends React.Component {
state = {type: 'number', value: 1000};
render() {
const {value, type} = this.state;
return <input onChange={() => {}} type={type} value={value} />;
}
}
const ref = React.createRef();
await act(() => {
root.render(<Input ref={ref} />);
});
const node = container.firstChild;
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', async () => {
function strify(x) {
return JSON.stringify(x, null, 2);
}
const log = [];
const originalCreateElement = document.createElement;
spyOnDevAndProd(document, 'createElement').mockImplementation(
function (type) {
const el = originalCreateElement.apply(this, arguments);
const getDefaultValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'defaultValue',
).get;
const setDefaultValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'defaultValue',
).set;
const getValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
).get;
const setValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
).set;
const getType = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'type',
).get;
const setType = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'type',
).set;
if (type === 'input') {
Object.defineProperty(el, 'defaultValue', {
get: function () {
return getDefaultValue.call(this);
},
set: function (val) {
log.push(`node.defaultValue = ${strify(val)}`);
setDefaultValue.call(this, val);
},
});
Object.defineProperty(el, 'value', {
get: function () {
return getValue.call(this);
},
set: function (val) {
log.push(`node.value = ${strify(val)}`);
setValue.call(this, val);
},
});
Object.defineProperty(el, 'type', {
get: function () {
return getType.call(this);
},
set: function (val) {
log.push(`node.type = ${strify(val)}`);
setType.call(this, val);
},
});
spyOnDevAndProd(el, 'setAttribute').mockImplementation(
function (name, val) {
log.push(`node.setAttribute(${strify(name)}, ${strify(val)})`);
},
);
}
return el;
},
);
await act(() => {
root.render(<input type="date" defaultValue="1980-01-01" />);
});
if (disableInputAttributeSyncing) {
expect(log).toEqual([
'node.type = "date"',
'node.defaultValue = "1980-01-01"',
]);
} else {
expect(log).toEqual([
'node.type = "date"',
'node.value = "1980-01-01"',
'node.defaultValue = "1980-01-01"',
]);
}
});
describe('assigning the value attribute on controlled inputs', function () {
function getTestInput() {
return class extends React.Component {
state = {
value: this.props.value == null ? '' : this.props.value,
};
onChange = event => {
this.setState({value: event.target.value});
};
render() {
const type = this.props.type;
const value = this.state.value;
return <input type={type} value={value} onChange={this.onChange} />;
}
};
}
it('always sets the attribute when values change on text inputs', async () => {
const Input = getTestInput();
await act(() => {
root.render(<Input type="text" />);
});
const node = container.firstChild;
expect(isValueDirty(node)).toBe(false);
setUntrackedValue.call(node, '2');
await act(() => {
dispatchEventOnNode(node, 'input');
});
expect(isValueDirty(node)).toBe(true);
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.getAttribute('value')).toBe('2');
}
});
it('does not set the value attribute on number inputs if focused', async () => {
const Input = getTestInput();
await act(() => {
root.render(<Input type="number" value="1" />);
});
const node = container.firstChild;
expect(isValueDirty(node)).toBe(true);
node.focus();
setUntrackedValue.call(node, '2');
dispatchEventOnNode(node, 'input');
expect(isValueDirty(node)).toBe(true);
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.getAttribute('value')).toBe('1');
}
});
it('sets the value attribute on number inputs on blur', async () => {
const Input = getTestInput();
await act(() => {
root.render(<Input type="number" value="1" />);
});
const node = container.firstChild;
expect(isValueDirty(node)).toBe(true);
node.focus();
setUntrackedValue.call(node, '2');
dispatchEventOnNode(node, 'input');
node.blur();
expect(isValueDirty(node)).toBe(true);
if (disableInputAttributeSyncing) {
expect(node.value).toBe('2');
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.value).toBe('2');
expect(node.getAttribute('value')).toBe('2');
}
});
it('an uncontrolled number input will not update the value attribute on blur', async () => {
await act(() => {
root.render(<input type="number" defaultValue="1" />);
});
const node = container.firstChild;
if (disableInputAttributeSyncing) {
expect(isValueDirty(node)).toBe(false);
} else {
expect(isValueDirty(node)).toBe(true);
}
node.focus();
setUntrackedValue.call(node, 4);
dispatchEventOnNode(node, 'input');
node.blur();
expect(isValueDirty(node)).toBe(true);
expect(node.getAttribute('value')).toBe('1');
});
it('an uncontrolled text input will not update the value attribute on blur', async () => {
await act(() => {
root.render(<input type="text" defaultValue="1" />);
});
const node = container.firstChild;
if (disableInputAttributeSyncing) {
expect(isValueDirty(node)).toBe(false);
} else {
expect(isValueDirty(node)).toBe(true);
}
node.focus();
setUntrackedValue.call(node, 4);
dispatchEventOnNode(node, 'input');
node.blur();
expect(isValueDirty(node)).toBe(true);
expect(node.getAttribute('value')).toBe('1');
});
});
describe('setting a controlled input to undefined', () => {
let input;
async function renderInputWithStringThenWithUndefined() {
let setValueToUndefined;
class Input extends React.Component {
constructor() {
super();
setValueToUndefined = () => this.setState({value: undefined});
}
state = {value: 'first'};
render() {
return (
<input
onChange={e => this.setState({value: e.target.value})}
value={this.state.value}
/>
);
}
}
await act(() => {
root.render(<Input />);
});
input = container.firstChild;
setUntrackedValue.call(input, 'latest');
dispatchEventOnNode(input, 'input');
await act(() => {
setValueToUndefined();
});
}
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) {
expect(input.getAttribute('value')).toBe(null);
} else {
expect(input.getAttribute('value')).toBe('latest');
}
});
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');
});
});
describe('setting a controlled input to null', () => {
let input;
async function renderInputWithStringThenWithNull() {
let setValueToNull;
class Input extends React.Component {
constructor() {
super();
setValueToNull = () => this.setState({value: null});
}
state = {value: 'first'};
render() {
return (
<input
onChange={e => this.setState({value: e.target.value})}
value={this.state.value}
/>
);
}
}
await act(() => {
root.render(<Input />);
});
input = container.firstChild;
setUntrackedValue.call(input, 'latest');
dispatchEventOnNode(input, 'input');
await act(() => {
setValueToNull();
});
}
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.',
'A component is changing a controlled input to be uncontrolled.',
]);
if (disableInputAttributeSyncing) {
expect(input.getAttribute('value')).toBe(null);
} else {
expect(input.getAttribute('value')).toBe('latest');
}
});
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.',
'A component is changing a controlled input to be uncontrolled.',
]);
expect(input.value).toBe('latest');
});
});
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`');
const node = container.firstChild;
expect(node.value).toBe('');
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.getAttribute('value')).toBe('');
}
});
it('treats updated Symbol value as an empty string', async () => {
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`');
const node = container.firstChild;
expect(node.value).toBe('');
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.getAttribute('value')).toBe('');
}
});
it('treats initial Symbol defaultValue as an empty string', async () => {
await act(() => {
root.render(<input defaultValue={Symbol('foobar')} />);
});
const node = container.firstChild;
expect(node.value).toBe('');
expect(node.getAttribute('value')).toBe('');
});
it('treats updated Symbol defaultValue as an empty string', async () => {
await act(() => {
root.render(<input defaultValue="foo" />);
});
await act(() => {
root.render(<input defaultValue={Symbol('foobar')} />);
});
const node = container.firstChild;
if (disableInputAttributeSyncing) {
expect(node.value).toBe('');
} else {
expect(node.value).toBe('foo');
}
expect(node.getAttribute('value')).toBe('');
});
});
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`');
const node = container.firstChild;
expect(node.value).toBe('');
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.getAttribute('value')).toBe('');
}
});
it('treats updated function value as an empty string', async () => {
await act(() => {
root.render(<input value="foo" onChange={() => {}} />);
});
await expect(async () => {
await act(() => {
root.render(<input value={() => {}} onChange={() => {}} />);
});
}).toErrorDev('Invalid value for prop `value`');
const node = container.firstChild;
expect(node.value).toBe('');
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
expect(node.getAttribute('value')).toBe('');
}
});
it('treats initial function defaultValue as an empty string', async () => {
await act(() => {
root.render(<input defaultValue={() => {}} />);
});
const node = container.firstChild;
expect(node.value).toBe('');
expect(node.getAttribute('value')).toBe('');
});
it('treats updated function defaultValue as an empty string', async () => {
await act(() => {
root.render(<input defaultValue="foo" />);
});
await act(() => {
root.render(<input defaultValue={() => {}} />);
});
const node = container.firstChild;
if (disableInputAttributeSyncing) {
expect(node.value).toBe('');
expect(node.getAttribute('value')).toBe('');
} else {
expect(node.value).toBe('foo');
expect(node.getAttribute('value')).toBe('');
}
});
});
describe('checked inputs without a value property', function () {
it('does not add "on" in absence of value on a checkbox', async () => {
await act(() => {
root.render(<input type="checkbox" defaultChecked={true} />);
});
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', async () => {
await act(() => {
root.render(<input type="radio" defaultChecked={true} />);
});
const node = container.firstChild;
expect(node.value).toBe('on');
expect(node.hasAttribute('value')).toBe(false);
});
});
it('should remove previous `defaultValue`', async () => {
await act(() => {
root.render(<input type="text" defaultValue="0" />);
});
const node = container.firstChild;
expect(node.value).toBe('0');
expect(node.defaultValue).toBe('0');
await act(() => {
root.render(<input type="text" />);
});
expect(node.defaultValue).toBe('');
});
it('should treat `defaultValue={null}` as missing', async () => {
await act(() => {
root.render(<input type="text" defaultValue="0" />);
});
const node = container.firstChild;
expect(node.value).toBe('0');
expect(node.defaultValue).toBe('0');
await act(() => {
root.render(<input type="text" defaultValue={null} />);
});
expect(node.defaultValue).toBe('');
});
it('should notice input changes when reverting back to original value', async () => {
const log = [];
function onChange(e) {
log.push(e.target.value);
}
await act(() => {
root.render(<input type="text" value="" onChange={onChange} />);
});
await act(() => {
root.render(<input type="text" value="a" onChange={onChange} />);
});
const node = container.firstChild;
setUntrackedValue.call(node, '');
dispatchEventOnNode(node, 'input');
expect(log).toEqual(['']);
expect(node.value).toBe('a');
});
});