Files
react/packages/react-dom/src/__tests__/ReactDOMInput-test.js
T
Justin Grant c88fb49d37 Improve DEV errors if string coercion throws (Temporal.*, Symbol, etc.) (#22064)
* Revise ESLint rules for string coercion

Currently, react uses `'' + value` to coerce mixed values to strings.
This code will throw for Temporal objects or symbols.

To make string-coercion safer and to improve user-facing error messages,
This commit adds a new ESLint rule called `safe-string-coercion`.

This rule has two modes: a production mode and a non-production mode.
* If the `isProductionUserAppCode` option is true, then `'' + value`
  coercions are allowed (because they're faster, although they may
  throw) and `String(value)` coercions are disallowed. Exception:
  when building error messages or running DEV-only code in prod
  files, `String()` should be used because it won't throw.
* If the `isProductionUserAppCode` option is false, then `'' + value`
  coercions are disallowed (because they may throw, and in non-prod
  code it's not worth the risk) and `String(value)` are allowed.

Production mode is used for all files which will be bundled with
developers' userland apps. Non-prod mode is used for all other React
code: tests, DEV blocks, devtools extension, etc.

In production mode, in addiiton to flagging `String(value)` calls,
the rule will also flag `'' + value` or `value + ''` coercions that may
throw. The rule is smart enough to silence itself in the following
"will never throw" cases:
* When the coercion is wrapped in a `typeof` test that restricts to safe
  (non-symbol, non-object) types. Example:
    if (typeof value === 'string' || typeof value === 'number') {
      thisWontReport('' + value);
    }
* When what's being coerced is a unary function result, because unary
   functions never return an object or a symbol.
* When the coerced value is a commonly-used numeric identifier:
  `i`, `idx`, or `lineNumber`.
* When the statement immeidately before the coercion is a DEV-only
  call to a function from shared/CheckStringCoercion.js. This call is a
  no-op in production, but in DEV it will show a console error
  explaining the problem, then will throw right after a long explanatory
  code comment so that debugger users will have an idea what's going on.
  The check function call must be in the following format:
    if (__DEV__) {
      checkXxxxxStringCoercion(value);
    };

Manually disabling the rule is usually not necessary because almost all
prod use of the `'' + value` pattern falls into one of the categories
above. But in the rare cases where the rule isn't smart enough to detect
safe usage (e.g. when a coercion is inside a nested ternary operator),
manually disabling the rule will be needed.

The rule should also be manually disabled in prod error handling code
where `String(value)` should be used for coercions, because it'd be
bad to throw while building an error message or stack trace!

The prod and non-prod modes have differentiated error messages to
explain how to do a proper coercion in that mode.

If a production check call is needed but is missing or incorrect
(e.g. not in a DEV block or not immediately before the coercion), then
a context-sensitive error message will be reported so that developers
can figure out what's wrong and how to fix the problem.

Because string coercions are now handled by the `safe-string-coercion`
rule, the `no-primitive-constructor` rule no longer flags `String()`
usage. It still flags `new String(value)` because that usage is almost
always a bug.

* Add DEV-only string coercion check functions

This commit adds DEV-only functions to check whether coercing
values to strings using the `'' + value` pattern will throw. If it will
throw, these functions will:
1. Display a console error with a friendly error message describing
   the problem and the developer can fix it.
2. Perform the coercion, which will throw. Right before the line where
   the throwing happens, there's a long code comment that will help
   debugger users (or others looking at the exception call stack) figure
   out what happened and how to fix the problem.

One of these check functions should be called before all string coercion
of user-provided values, except when the the coercion is guaranteed not
to throw, e.g.
* if inside a typeof check like `if (typeof value === 'string')`
* if coercing the result of a unary function like `+value` or `value++`
* if coercing a variable named in a whitelist of numeric identifiers:
  `i`, `idx`, or `lineNumber`.

The new `safe-string-coercion` internal ESLint rule enforces that
these check functions are called when they are required.

Only use these check functions in production code that will be bundled
with user apps.  For non-prod code (and for production error-handling
code), use `String(value)` instead which may be a little slower but will
never throw.

* Add failing tests for string coercion

Added failing tests to verify:
* That input, select, and textarea elements with value and defaultValue
  set to Temporal-like objects which will throw when coerced to string
  using the `'' + value` pattern.
* That text elements will throw for Temporal-like objects
* That dangerouslySetInnerHTML will *not* throw for Temporal-like
  objects because this value is not cast to a string before passing to
  the DOM.
* That keys that are Temporal-like objects will throw

All tests above validate the friendly error messages thrown.

* Use `String(value)` for coercion in non-prod files

This commit switches non-production code from `'' + value` (which
throws for Temporal objects and symbols) to instead use `String(value)`
which won't throw for these or other future plus-phobic types.

"Non-produciton code" includes anything not bundled into user apps:
* Tests and test utilities. Note that I didn't change legacy React
  test fixtures because I assumed it was good for those files to
  act just like old React, including coercion behavior.
* Build scripts
* Dev tools package - In addition to switching to `String`, I also
  removed special-case code for coercing symbols which is now
  unnecessary.

* Add DEV-only string coercion checks to prod files

This commit adds DEV-only function calls to to check if string coercion
using `'' + value` will throw, which it will if the value is a Temporal
object or a symbol because those types can't be added with `+`.

If it will throw, then in DEV these checks will show a console error
to help the user undertsand what went wrong and how to fix the
problem. After emitting the console error, the check functions will
retry the coercion which will throw with a call stack that's easy (or
at least easier!) to troubleshoot because the exception happens right
after a long comment explaining the issue. So whether the user is in
a debugger, looking at the browser console, or viewing the in-browser
DEV call stack, it should be easy to understand and fix the problem.

In most cases, the safe-string-coercion ESLint rule is smart enough to
detect when a coercion is safe. But in rare cases (e.g. when a coercion
is inside a ternary) this rule will have to be manually disabled.

This commit also switches error-handling code to use `String(value)`
for coercion, because it's bad to crash when you're trying to build
an error message or a call stack!  Because `String()` is usually
disallowed by the `safe-string-coercion` ESLint rule in production
code, the rule must be disabled when `String()` is used.
2021-09-27 10:05:07 -07:00

2170 lines
72 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
// Set by `yarn test-fire`.
const {disableInputAttributeSyncing} = require('shared/ReactFeatureFlags');
function emptyFunction() {}
describe('ReactDOMInput', () => {
let React;
let ReactDOM;
let ReactDOMServer;
let setUntrackedValue;
let setUntrackedChecked;
let container;
function dispatchEventOnNode(node, type) {
node.dispatchEvent(new Event(type, {bubbles: true, cancelable: true}));
}
beforeEach(() => {
jest.resetModules();
setUntrackedValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
).set;
setUntrackedChecked = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'checked',
).set;
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
it('should warn for controlled value of 0 with missing onChange', () => {
expect(() => {
ReactDOM.render(<input type="text" value={0} />, container);
}).toErrorDev(
'Warning: You provided a `value` prop to a form field without an `onChange` handler.',
);
});
it('should warn for controlled value of "" with missing onChange', () => {
expect(() => {
ReactDOM.render(<input type="text" value="" />, container);
}).toErrorDev(
'Warning: You provided a `value` prop to a form field without an `onChange` handler.',
);
});
it('should warn for controlled value of "0" with missing onChange', () => {
expect(() => {
ReactDOM.render(<input type="text" value="0" />, container);
}).toErrorDev(
'Warning: You provided a `value` prop to a form field without an `onChange` handler.',
);
});
it('should warn for controlled value of false with missing onChange', () => {
expect(() =>
ReactDOM.render(<input type="checkbox" checked={false} />, container),
).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(
<input type="checkbox" checked={false} readOnly={true} />,
container,
);
ReactDOM.unmountComponentAtNode(container);
expect(() =>
ReactDOM.render(
<input type="checkbox" checked={false} readOnly={false} />,
container,
),
).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(<input />, container);
ReactDOM.unmountComponentAtNode(container);
ReactDOM.render(<input value={undefined} />, container);
ReactDOM.unmountComponentAtNode(container);
ReactDOM.render(<input type="text" />, container);
ReactDOM.unmountComponentAtNode(container);
ReactDOM.render(<input type="text" value={undefined} />, container);
ReactDOM.unmountComponentAtNode(container);
ReactDOM.render(<input type="checkbox" />, container);
ReactDOM.unmountComponentAtNode(container);
ReactDOM.render(<input type="checkbox" checked={undefined} />, container);
});
it('should not warn with value and onInput handler', () => {
ReactDOM.render(<input value="..." onInput={() => {}} />, container);
});
it('should properly control a value even if no event listener exists', () => {
let node;
expect(() => {
node = ReactDOM.render(<input type="text" value="lion" />, container);
}).toErrorDev(
'Warning: You provided a `value` prop to a form field without an `onChange` handler.',
);
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');
expect(node.value).toBe('lion');
});
it('should control a value in reentrant events', () => {
class ControlledInputs extends React.Component {
state = {value: 'lion'};
a = null;
b = null;
switchedFocus = false;
change(newValue) {
this.setState({value: newValue});
// Calling focus here will blur the text box which causes a native
// change event. Ideally we shouldn't have to fire this ourselves.
// Don't remove unless you've verified the fix in #8240 is still covered.
dispatchEventOnNode(this.a, 'input');
this.b.focus();
}
blur(currentValue) {
this.switchedFocus = true;
// currentValue should be 'giraffe' here because we should not have
// restored it on the target yet.
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 instance = ReactDOM.render(<ControlledInputs />, container);
// 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();
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');
expect(instance.a.value).toBe('giraffe');
expect(instance.switchedFocus).toBe(true);
});
it('should control values in reentrant events with different targets', () => {
class ControlledInputs extends React.Component {
state = {value: 'lion'};
a = null;
b = null;
change(newValue) {
// This click will change the checkbox's value to false. Then it will
// invoke an inner change event. When we finally, flush, we need to
// reset the checkbox's value to true since that is its controlled
// value.
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 instance = ReactDOM.render(<ControlledInputs />, container);
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');
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 = <input type="text" value={2} onChange={jest.fn()} />;
const node = ReactDOM.render(stub, container);
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', () => {
const stub = <input type="text" value={'2'} onChange={jest.fn()} />;
const node = ReactDOM.render(stub, container);
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', () => {
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} />;
}
}
const stub = ReactDOM.render(<Stub />, container);
const node = ReactDOM.findDOMNode(stub);
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', () => {
class Stub extends React.Component {
state = {
value: '.98',
};
render() {
return <input type="number" value={this.state.value} />;
}
}
let stub;
expect(() => {
stub = ReactDOM.render(<Stub />, container);
}).toErrorDev(
'You provided a `value` prop to a form field ' +
'without an `onChange` handler.',
);
const node = ReactDOM.findDOMNode(stub);
stub.setState({value: '0.98'});
expect(node.value).toEqual('0.98');
});
it('performs a state change from "" to 0', () => {
class Stub extends React.Component {
state = {
value: '',
};
render() {
return <input type="number" value={this.state.value} readOnly={true} />;
}
}
const stub = ReactDOM.render(<Stub />, container);
const node = ReactDOM.findDOMNode(stub);
stub.setState({value: 0});
expect(node.value).toEqual('0');
});
it('updates the value on radio buttons from "" to 0', function() {
ReactDOM.render(
<input type="radio" value="" onChange={function() {}} />,
container,
);
ReactDOM.render(
<input type="radio" value={0} onChange={function() {}} />,
container,
);
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(
<input type="checkbox" value="" onChange={function() {}} />,
container,
);
ReactDOM.render(
<input type="checkbox" value={0} onChange={function() {}} />,
container,
);
expect(container.firstChild.value).toBe('0');
expect(container.firstChild.getAttribute('value')).toBe('0');
});
it('distinguishes precision for extra zeroes in string number values', () => {
class Stub extends React.Component {
state = {
value: '3.0000',
};
render() {
return <input type="number" value={this.state.value} />;
}
}
let stub;
expect(() => {
stub = ReactDOM.render(<Stub />, container);
}).toErrorDev(
'You provided a `value` prop to a form field ' +
'without an `onChange` handler.',
);
const node = ReactDOM.findDOMNode(stub);
stub.setState({value: '3'});
expect(node.value).toEqual('3');
});
it('should display `defaultValue` of number 0', () => {
const stub = <input type="text" defaultValue={0} />;
const node = ReactDOM.render(stub, container);
expect(node.getAttribute('value')).toBe('0');
expect(node.value).toBe('0');
});
it('only assigns defaultValue if it changes', () => {
class Test extends React.Component {
render() {
return <input defaultValue="0" />;
}
}
const component = ReactDOM.render(<Test />, container);
const node = ReactDOM.findDOMNode(component);
Object.defineProperty(node, 'defaultValue', {
get() {
return '0';
},
set(value) {
throw new Error(
`defaultValue was assigned ${value}, but it did not change!`,
);
},
});
component.forceUpdate();
});
it('should display "true" for `defaultValue` of `true`', () => {
const stub = <input type="text" defaultValue={true} />;
const node = ReactDOM.render(stub, container);
expect(node.value).toBe('true');
});
it('should display "false" for `defaultValue` of `false`', () => {
const stub = <input type="text" defaultValue={false} />;
const node = ReactDOM.render(stub, container);
expect(node.value).toBe('false');
});
it('should update `defaultValue` for uncontrolled input', () => {
const node = ReactDOM.render(
<input type="text" defaultValue="0" />,
container,
);
expect(node.value).toBe('0');
expect(node.defaultValue).toBe('0');
ReactDOM.render(<input type="text" defaultValue="1" />, container);
if (disableInputAttributeSyncing) {
expect(node.value).toBe('1');
expect(node.defaultValue).toBe('1');
} else {
expect(node.value).toBe('0');
expect(node.defaultValue).toBe('1');
}
});
it('should update `defaultValue` for uncontrolled date/time input', () => {
const node = ReactDOM.render(
<input type="date" defaultValue="1980-01-01" />,
container,
);
expect(node.value).toBe('1980-01-01');
expect(node.defaultValue).toBe('1980-01-01');
ReactDOM.render(<input type="date" defaultValue="2000-01-01" />, container);
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');
}
ReactDOM.render(<input type="date" />, container);
});
it('should take `defaultValue` when changing to uncontrolled input', () => {
const node = ReactDOM.render(
<input type="text" value="0" readOnly={true} />,
container,
);
expect(node.value).toBe('0');
expect(() =>
ReactDOM.render(<input type="text" defaultValue="1" />, container),
).toErrorDev(
'A component is changing a controlled input to be uncontrolled.',
);
expect(node.value).toBe('0');
});
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 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 name attribute if it is supplied', () => {
const node = ReactDOM.render(<input type="text" name="name" />, container);
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', () => {
ReactDOM.render(<input type="text" />, container);
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`', () => {
const objToString = {
toString: function() {
return 'foobar';
},
};
const stub = <input type="text" defaultValue={objToString} />;
const node = ReactDOM.render(stub, container);
expect(node.value).toBe('foobar');
});
it('should throw for date inputs if `defaultValue` is an object where valueOf() throws', () => {
class TemporalLike {
valueOf() {
// Throwing here is the behavior of ECMAScript "Temporal" date/time API.
// See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
throw new TypeError('prod message');
}
toString() {
return '2020-01-01';
}
}
const test = () =>
ReactDOM.render(
<input defaultValue={new TemporalLike()} type="date" />,
container,
);
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 before using it here.',
);
});
it('should throw for text inputs if `defaultValue` is an object where valueOf() throws', () => {
class TemporalLike {
valueOf() {
// Throwing here is the behavior of ECMAScript "Temporal" date/time API.
// See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
throw new TypeError('prod message');
}
toString() {
return '2020-01-01';
}
}
const test = () =>
ReactDOM.render(
<input defaultValue={new TemporalLike()} type="text" />,
container,
);
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 before using it here.',
);
});
it('should throw for date inputs if `value` is an object where valueOf() throws', () => {
class TemporalLike {
valueOf() {
// Throwing here is the behavior of ECMAScript "Temporal" date/time API.
// See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
throw new TypeError('prod message');
}
toString() {
return '2020-01-01';
}
}
const test = () =>
ReactDOM.render(
<input value={new TemporalLike()} type="date" onChange={() => {}} />,
container,
);
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 before using it here.',
);
});
it('should throw for text inputs if `value` is an object where valueOf() throws', () => {
class TemporalLike {
valueOf() {
// Throwing here is the behavior of ECMAScript "Temporal" date/time API.
// See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
throw new TypeError('prod message');
}
toString() {
return '2020-01-01';
}
}
const test = () =>
ReactDOM.render(
<input value={new TemporalLike()} type="text" onChange={() => {}} />,
container,
);
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 before using it here.',
);
});
it('should display `value` of number 0', () => {
const stub = <input type="text" value={0} onChange={emptyFunction} />;
const node = ReactDOM.render(stub, container);
expect(node.value).toBe('0');
});
it('should allow setting `value` to `true`', () => {
let stub = <input type="text" value="yolo" onChange={emptyFunction} />;
const node = ReactDOM.render(stub, container);
expect(node.value).toBe('yolo');
stub = ReactDOM.render(
<input type="text" value={true} onChange={emptyFunction} />,
container,
);
expect(node.value).toEqual('true');
});
it('should allow setting `value` to `false`', () => {
let stub = <input type="text" value="yolo" onChange={emptyFunction} />;
const node = ReactDOM.render(stub, container);
expect(node.value).toBe('yolo');
stub = ReactDOM.render(
<input type="text" value={false} onChange={emptyFunction} />,
container,
);
expect(node.value).toEqual('false');
});
it('should allow setting `value` to `objToString`', () => {
let stub = <input type="text" value="foo" onChange={emptyFunction} />;
const node = ReactDOM.render(stub, container);
expect(node.value).toBe('foo');
const objToString = {
toString: function() {
return 'foobar';
},
};
stub = ReactDOM.render(
<input type="text" value={objToString} onChange={emptyFunction} />,
container,
);
expect(node.value).toEqual('foobar');
});
it('should not incur unnecessary DOM mutations', () => {
ReactDOM.render(<input value="a" onChange={() => {}} />, container);
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;
}),
});
ReactDOM.render(<input value="a" onChange={() => {}} />, container);
expect(nodeValueSetter).toHaveBeenCalledTimes(0);
ReactDOM.render(<input value="b" onChange={() => {}} />, container);
expect(nodeValueSetter).toHaveBeenCalledTimes(1);
});
it('should not incur unnecessary DOM mutations for numeric type conversion', () => {
ReactDOM.render(<input value="0" onChange={() => {}} />, container);
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;
}),
});
ReactDOM.render(<input value={0} onChange={() => {}} />, container);
expect(nodeValueSetter).toHaveBeenCalledTimes(0);
});
it('should not incur unnecessary DOM mutations for the boolean type conversion', () => {
ReactDOM.render(<input value="true" onChange={() => {}} />, container);
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;
}),
});
ReactDOM.render(<input value={true} onChange={() => {}} />, container);
expect(nodeValueSetter).toHaveBeenCalledTimes(0);
});
it('should properly control a value of number `0`', () => {
const stub = <input type="text" value={0} onChange={emptyFunction} />;
const node = ReactDOM.render(stub, container);
setUntrackedValue.call(node, 'giraffe');
dispatchEventOnNode(node, 'input');
expect(node.value).toBe('0');
});
it('should properly control 0.0 for a text input', () => {
const stub = <input type="text" value={0} onChange={emptyFunction} />;
const node = ReactDOM.render(stub, container);
setUntrackedValue.call(node, '0.0');
dispatchEventOnNode(node, 'input');
expect(node.value).toBe('0');
});
it('should properly control 0.0 for a number input', () => {
const stub = <input type="number" value={0} onChange={emptyFunction} />;
const node = ReactDOM.render(stub, container);
setUntrackedValue.call(node, '0.0');
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', function() {
ReactDOM.render(
<input type="text" value="" onChange={emptyFunction} />,
container,
);
ReactDOM.render(
<input type="text" value={0} onChange={emptyFunction} />,
container,
);
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 from 0 to an empty value', function() {
ReactDOM.render(
<input type="text" value={0} onChange={emptyFunction} />,
container,
);
ReactDOM.render(
<input type="text" value="" onChange={emptyFunction} />,
container,
);
const node = container.firstChild;
expect(node.value).toBe('');
expect(node.defaultValue).toBe('');
});
it('should properly transition a text input from 0 to an empty 0.0', function() {
ReactDOM.render(
<input type="text" value={0} onChange={emptyFunction} />,
container,
);
ReactDOM.render(
<input type="text" value="0.0" onChange={emptyFunction} />,
container,
);
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', function() {
ReactDOM.render(
<input type="number" value="" onChange={emptyFunction} />,
container,
);
ReactDOM.render(
<input type="number" value={0} onChange={emptyFunction} />,
container,
);
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"', function() {
ReactDOM.render(
<input type="number" value="" onChange={emptyFunction} />,
container,
);
ReactDOM.render(
<input type="number" value="0" onChange={emptyFunction} />,
container,
);
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', () => {
let handled = false;
const handler = function(event) {
expect(event.target.nodeName).toBe('INPUT');
handled = true;
};
const stub = <input type="text" value={0} onChange={handler} />;
const node = ReactDOM.render(stub, container);
setUntrackedValue.call(node, 'giraffe');
dispatchEventOnNode(node, 'input');
expect(handled).toBe(true);
});
it('should restore uncontrolled inputs to last defaultValue upon reset', () => {
const inputRef = React.createRef();
ReactDOM.render(
<form>
<input defaultValue="default1" ref={inputRef} />
<input type="reset" />
</form>,
container,
);
expect(inputRef.current.value).toBe('default1');
setUntrackedValue.call(inputRef.current, 'changed');
dispatchEventOnNode(inputRef.current, 'input');
expect(inputRef.current.value).toBe('changed');
ReactDOM.render(
<form>
<input defaultValue="default2" ref={inputRef} />
<input type="reset" />
</form>,
container,
);
expect(inputRef.current.value).toBe('changed');
container.firstChild.reset();
// Note: I don't know if we want to always support this.
// But it's current behavior so worth being intentional if we break it.
// https://github.com/facebook/react/issues/4618
expect(inputRef.current.value).toBe('default2');
});
it('should not set a value for submit buttons unnecessarily', () => {
const stub = <input type="submit" />;
ReactDOM.render(stub, container);
const node = container.firstChild;
// The value shouldn't be '', or else the button will have no text; it
// should have the default "Submit" or "Submit Query" label. Most browsers
// report this as not having a `value` attribute at all; IE reports it as
// the actual label that the user sees.
expect(node.hasAttribute('value')).toBe(false);
});
it('should remove the value attribute on submit inputs when value is updated to undefined', () => {
const stub = <input type="submit" value="foo" onChange={emptyFunction} />;
ReactDOM.render(stub, container);
// Not really relevant to this particular test, but changing to undefined
// should nonetheless trigger a warning
expect(() =>
ReactDOM.render(
<input type="submit" value={undefined} onChange={emptyFunction} />,
container,
),
).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', () => {
const stub = <input type="reset" value="foo" onChange={emptyFunction} />;
ReactDOM.render(stub, container);
// Not really relevant to this particular test, but changing to undefined
// should nonetheless trigger a warning
expect(() =>
ReactDOM.render(
<input type="reset" value={undefined} onChange={emptyFunction} />,
container,
),
).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', () => {
const stub = <input type="submit" value="banana" />;
ReactDOM.render(stub, container);
const node = container.firstChild;
expect(node.getAttribute('value')).toBe('banana');
});
it('should not set an undefined value on a submit input', () => {
const stub = <input type="submit" value={undefined} />;
ReactDOM.render(stub, container);
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);
expect(node.getAttribute('value')).toBe(null);
});
it('should not set an undefined value on a reset input', () => {
const stub = <input type="reset" value={undefined} />;
ReactDOM.render(stub, container);
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);
expect(node.getAttribute('value')).toBe(null);
});
it('should not set a null value on a submit input', () => {
const stub = <input type="submit" value={null} />;
expect(() => {
ReactDOM.render(stub, container);
}).toErrorDev('`value` prop on `input` should not be null');
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);
expect(node.getAttribute('value')).toBe(null);
});
it('should not set a null value on a reset input', () => {
const stub = <input type="reset" value={null} />;
expect(() => {
ReactDOM.render(stub, container);
}).toErrorDev('`value` prop on `input` should not be null');
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);
expect(node.getAttribute('value')).toBe(null);
});
it('should set a value on a reset input', () => {
const stub = <input type="reset" value="banana" />;
ReactDOM.render(stub, container);
const node = container.firstChild;
expect(node.getAttribute('value')).toBe('banana');
});
it('should set an empty string value on a submit input', () => {
const stub = <input type="submit" value="" />;
ReactDOM.render(stub, container);
const node = container.firstChild;
expect(node.getAttribute('value')).toBe('');
});
it('should set an empty string value on a reset input', () => {
const stub = <input type="reset" value="" />;
ReactDOM.render(stub, container);
const node = container.firstChild;
expect(node.getAttribute('value')).toBe('');
});
it('should control radio buttons', () => {
class RadioGroup extends React.Component {
render() {
return (
<div>
<input
ref="a"
type="radio"
name="fruit"
checked={true}
onChange={emptyFunction}
/>
A
<input ref="b" type="radio" name="fruit" onChange={emptyFunction} />
B
<form>
<input
ref="c"
type="radio"
name="fruit"
defaultChecked={true}
onChange={emptyFunction}
/>
</form>
</div>
);
}
}
const stub = ReactDOM.render(<RadioGroup />, container);
const aNode = stub.refs.a;
const bNode = stub.refs.b;
const cNode = stub.refs.c;
expect(aNode.checked).toBe(true);
expect(bNode.checked).toBe(false);
// c is in a separate form and shouldn't be affected at all here
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);
}
setUntrackedChecked.call(bNode, true);
expect(aNode.checked).toBe(false);
expect(cNode.checked).toBe(true);
// The original 'checked' attribute should be unchanged
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);
}
// Now let's run the actual ReactDOMInput change event handler
dispatchEventOnNode(bNode, 'click');
// The original state should have been restored
expect(aNode.checked).toBe(true);
expect(cNode.checked).toBe(true);
});
it('should check the correct radio when the selected name moves', () => {
class App extends React.Component {
state = {
updated: false,
};
onClick = () => {
this.setState({updated: true});
};
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>
);
}
}
const stub = ReactDOM.render(<App />, container);
const buttonNode = ReactDOM.findDOMNode(stub).childNodes[0];
const firstRadioNode = ReactDOM.findDOMNode(stub).childNodes[1];
expect(firstRadioNode.checked).toBe(false);
dispatchEventOnNode(buttonNode, 'click');
expect(firstRadioNode.checked).toBe(true);
});
it('should control radio buttons if the tree updates during render', () => {
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);
setUntrackedChecked.call(aNode, true);
// This next line isn't necessary in a proper browser environment, but
// jsdom doesn't uncheck the others in a group (because they are not yet
// sharing a parent), which makes this whole test a little less effective.
setUntrackedChecked.call(bNode, false);
// Now let's run the actual ReactDOMInput change event handler
dispatchEventOnNode(aNode, 'click');
// The original state should have been restored
expect(aNode.checked).toBe(false);
expect(bNode.checked).toBe(true);
});
it('should warn with value and no onChange handler and readOnly specified', () => {
ReactDOM.render(
<input type="text" value="zoink" readOnly={true} />,
container,
);
ReactDOM.unmountComponentAtNode(container);
expect(() =>
ReactDOM.render(
<input type="text" value="zoink" readOnly={false} />,
container,
),
).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`. ' +
'Otherwise, set either `onChange` or `readOnly`.\n' +
' in input (at **)',
);
});
it('should have a this value of undefined if bind is not used', () => {
expect.assertions(1);
const unboundInputOnChange = function() {
expect(this).toBe(undefined);
};
const stub = <input type="text" onChange={unboundInputOnChange} />;
const node = ReactDOM.render(stub, container);
setUntrackedValue.call(node, 'giraffe');
dispatchEventOnNode(node, 'input');
});
it('should update defaultValue to empty string', () => {
ReactDOM.render(<input type="text" defaultValue={'foo'} />, container);
ReactDOM.render(<input type="text" defaultValue={''} />, container);
expect(container.firstChild.defaultValue).toBe('');
});
it('should warn if value is null', () => {
expect(() =>
ReactDOM.render(<input type="text" value={null} />, container),
).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);
ReactDOM.render(<input type="text" value={null} />, container);
});
it('should warn if checked and defaultChecked props are specified', () => {
expect(() =>
ReactDOM.render(
<input
type="radio"
checked={true}
defaultChecked={true}
readOnly={true}
/>,
container,
),
).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://reactjs.org/link/controlled-components',
);
ReactDOM.unmountComponentAtNode(container);
ReactDOM.render(
<input
type="radio"
checked={true}
defaultChecked={true}
readOnly={true}
/>,
container,
);
});
it('should warn if value and defaultValue props are specified', () => {
expect(() =>
ReactDOM.render(
<input type="text" value="foo" defaultValue="bar" readOnly={true} />,
container,
),
).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://reactjs.org/link/controlled-components',
);
ReactDOM.unmountComponentAtNode(container);
ReactDOM.render(
<input type="text" value="foo" defaultValue="bar" readOnly={true} />,
container,
);
});
it('should warn if controlled input switches to uncontrolled (value is undefined)', () => {
const stub = (
<input type="text" value="controlled" onChange={emptyFunction} />
);
ReactDOM.render(stub, container);
expect(() => ReactDOM.render(<input type="text" />, container)).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if controlled input switches to uncontrolled (value is null)', () => {
const stub = (
<input type="text" value="controlled" onChange={emptyFunction} />
);
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(<input type="text" value={null} />, container),
).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. ' +
'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://reactjs.org/link/controlled-components\n' +
' in input (at **)',
]);
});
it('should warn if controlled input switches to uncontrolled with defaultValue', () => {
const stub = (
<input type="text" value="controlled" onChange={emptyFunction} />
);
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(
<input type="text" defaultValue="uncontrolled" />,
container,
),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if uncontrolled input (value is undefined) switches to controlled', () => {
const stub = <input type="text" />;
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(<input type="text" value="controlled" />, container),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if uncontrolled input (value is null) switches to controlled', () => {
const stub = <input type="text" value={null} />;
expect(() => ReactDOM.render(stub, container)).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(<input type="text" value="controlled" />, container),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if controlled checkbox switches to uncontrolled (checked is undefined)', () => {
const stub = (
<input type="checkbox" checked={true} onChange={emptyFunction} />
);
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(<input type="checkbox" />, container),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if controlled checkbox switches to uncontrolled (checked is null)', () => {
const stub = (
<input type="checkbox" checked={true} onChange={emptyFunction} />
);
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(<input type="checkbox" checked={null} />, container),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if controlled checkbox switches to uncontrolled with defaultChecked', () => {
const stub = (
<input type="checkbox" checked={true} onChange={emptyFunction} />
);
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(
<input type="checkbox" defaultChecked={true} />,
container,
),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if uncontrolled checkbox (checked is undefined) switches to controlled', () => {
const stub = <input type="checkbox" />;
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(<input type="checkbox" checked={true} />, container),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if uncontrolled checkbox (checked is null) switches to controlled', () => {
const stub = <input type="checkbox" checked={null} />;
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(<input type="checkbox" checked={true} />, container),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if controlled radio switches to uncontrolled (checked is undefined)', () => {
const stub = <input type="radio" checked={true} onChange={emptyFunction} />;
ReactDOM.render(stub, container);
expect(() => ReactDOM.render(<input type="radio" />, container)).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if controlled radio switches to uncontrolled (checked is null)', () => {
const stub = <input type="radio" checked={true} onChange={emptyFunction} />;
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(<input type="radio" checked={null} />, container),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if controlled radio switches to uncontrolled with defaultChecked', () => {
const stub = <input type="radio" checked={true} onChange={emptyFunction} />;
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(<input type="radio" defaultChecked={true} />, container),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if uncontrolled radio (checked is undefined) switches to controlled', () => {
const stub = <input type="radio" />;
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(<input type="radio" checked={true} />, container),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should warn if uncontrolled radio (checked is null) switches to controlled', () => {
const stub = <input type="radio" checked={null} />;
ReactDOM.render(stub, container);
expect(() =>
ReactDOM.render(<input type="radio" checked={true} />, container),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('should not warn if radio value changes but never becomes controlled', () => {
ReactDOM.render(<input type="radio" value="value" />, container);
ReactDOM.render(<input type="radio" />, container);
ReactDOM.render(
<input type="radio" value="value" defaultChecked={true} />,
container,
);
ReactDOM.render(
<input type="radio" value="value" onChange={() => null} />,
container,
);
ReactDOM.render(<input type="radio" />, container);
});
it('should not warn if radio value changes but never becomes uncontrolled', () => {
ReactDOM.render(
<input type="radio" checked={false} onChange={() => null} />,
container,
);
ReactDOM.render(
<input
type="radio"
value="value"
defaultChecked={true}
checked={false}
onChange={() => null}
/>,
container,
);
});
it('should warn if radio checked false changes to become uncontrolled', () => {
ReactDOM.render(
<input
type="radio"
value="value"
checked={false}
onChange={() => null}
/>,
container,
);
expect(() =>
ReactDOM.render(<input type="radio" value="value" />, container),
).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. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components\n' +
' in input (at **)',
);
});
it('sets type, step, min, max before value always', () => {
const log = [];
const originalCreateElement = document.createElement;
spyOnDevAndProd(document, 'createElement').and.callFake(function(type) {
const el = originalCreateElement.apply(this, arguments);
let value = '';
if (type === 'input') {
Object.defineProperty(el, 'value', {
get: function() {
return value;
},
set: function(val) {
value = String(val);
log.push('set property value');
},
});
spyOnDevAndProd(el, 'setAttribute').and.callFake(function(name) {
log.push('set attribute ' + name);
});
}
return el;
});
ReactDOM.render(
<input
value="0"
onChange={() => {}}
type="range"
min="0"
max="100"
step="1"
/>,
container,
);
expect(log).toEqual([
'set attribute type',
'set attribute min',
'set attribute max',
'set attribute step',
'set property value',
]);
});
it('sets value properly with type coming later in props', () => {
const input = ReactDOM.render(<input value="hi" type="radio" />, container);
expect(input.value).toBe('hi');
});
it('does not raise a validation warning when it switches types', () => {
class Input extends React.Component {
state = {type: 'number', value: 1000};
render() {
const {value, type} = this.state;
return <input onChange={() => {}} type={type} value={value} />;
}
}
const input = ReactDOM.render(<Input />, container);
const node = ReactDOM.findDOMNode(input);
// 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'});
expect(node.value).toEqual('Test');
});
it('resets value of date/time input to fix bugs in iOS Safari', () => {
function strify(x) {
return JSON.stringify(x, null, 2);
}
const log = [];
const originalCreateElement = document.createElement;
spyOnDevAndProd(document, 'createElement').and.callFake(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;
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);
},
});
spyOnDevAndProd(el, 'setAttribute').and.callFake(function(name, val) {
log.push(`node.setAttribute(${strify(name)}, ${strify(val)})`);
});
}
return el;
});
ReactDOM.render(<input type="date" defaultValue="1980-01-01" />, container);
if (disableInputAttributeSyncing) {
expect(log).toEqual([
'node.setAttribute("type", "date")',
'node.defaultValue = "1980-01-01"',
// TODO: it's possible this reintroduces the bug because we don't assign `value` at all.
// Need to check this on mobile Safari and Chrome.
]);
} else {
expect(log).toEqual([
'node.setAttribute("type", "date")',
// value must be assigned before defaultValue. This fixes an issue where the
// visually displayed value of date inputs disappears on mobile Safari and Chrome:
// https://github.com/facebook/react/issues/7233
'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', function() {
const Input = getTestInput();
const stub = ReactDOM.render(<Input type="text" />, container);
const node = ReactDOM.findDOMNode(stub);
setUntrackedValue.call(node, '2');
dispatchEventOnNode(node, 'input');
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', () => {
const Input = getTestInput();
const stub = ReactDOM.render(
<Input type="number" value="1" />,
container,
);
const node = ReactDOM.findDOMNode(stub);
node.focus();
setUntrackedValue.call(node, '2');
dispatchEventOnNode(node, 'input');
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', () => {
const Input = getTestInput();
const stub = ReactDOM.render(
<Input type="number" value="1" />,
container,
);
const node = ReactDOM.findDOMNode(stub);
node.focus();
setUntrackedValue.call(node, '2');
dispatchEventOnNode(node, 'input');
// TODO: it is unclear why blur must be triggered twice,
// manual testing in the fixtures shows that the active element
// is no longer the input, however blur() + a blur event seem to
// be the only way to remove focus in JSDOM
node.blur();
dispatchEventOnNode(node, 'blur');
dispatchEventOnNode(node, 'focusout');
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', () => {
const node = ReactDOM.render(
<input type="number" defaultValue="1" />,
container,
);
node.focus();
setUntrackedValue.call(node, 4);
dispatchEventOnNode(node, 'input');
// TODO: it is unclear why blur must be triggered twice,
// manual testing in the fixtures shows that the active element
// is no longer the input, however blur() + a blur event seem to
// be the only way to remove focus in JSDOM
node.blur();
dispatchEventOnNode(node, 'blur');
dispatchEventOnNode(node, 'focusout');
expect(node.getAttribute('value')).toBe('1');
});
it('an uncontrolled text input will not update the value attribute on blur', () => {
const node = ReactDOM.render(
<input type="text" defaultValue="1" />,
container,
);
node.focus();
setUntrackedValue.call(node, 4);
dispatchEventOnNode(node, 'input');
// TODO: it is unclear why blur must be triggered twice,
// manual testing in the fixtures shows that the active element
// is no longer the input, however blur() + a blur event seem to
// be the only way to remove focus in JSDOM
node.blur();
dispatchEventOnNode(node, 'blur');
dispatchEventOnNode(node, 'focusout');
expect(node.getAttribute('value')).toBe('1');
});
});
describe('setting a controlled input to undefined', () => {
let input;
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}
/>
);
}
}
const stub = ReactDOM.render(<Input />, container);
input = ReactDOM.findDOMNode(stub);
setUntrackedValue.call(input, 'latest');
dispatchEventOnNode(input, 'input');
setValueToUndefined();
}
it('reverts the value attribute to the initial value', () => {
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('first');
}
});
it('preserves the value property', () => {
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;
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}
/>
);
}
}
const stub = ReactDOM.render(<Input />, container);
input = ReactDOM.findDOMNode(stub);
setUntrackedValue.call(input, 'latest');
dispatchEventOnNode(input, 'input');
setValueToNull();
}
it('reverts the value attribute to the initial value', () => {
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.hasAttribute('value')).toBe(false);
} else {
expect(input.getAttribute('value')).toBe('first');
}
});
it('preserves the value property', () => {
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', function() {
expect(() =>
ReactDOM.render(
<input value={Symbol('foobar')} onChange={() => {}} />,
container,
),
).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', function() {
ReactDOM.render(<input value="foo" onChange={() => {}} />, container);
expect(() =>
ReactDOM.render(
<input value={Symbol('foobar')} onChange={() => {}} />,
container,
),
).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', function() {
ReactDOM.render(<input defaultValue={Symbol('foobar')} />, container);
const node = container.firstChild;
expect(node.value).toBe('');
expect(node.getAttribute('value')).toBe('');
// TODO: we should warn here.
});
it('treats updated Symbol defaultValue as an empty string', function() {
ReactDOM.render(<input defaultValue="foo" />, container);
ReactDOM.render(<input defaultValue={Symbol('foobar')} />, container);
const node = container.firstChild;
if (disableInputAttributeSyncing) {
expect(node.value).toBe('');
} else {
expect(node.value).toBe('foo');
}
expect(node.getAttribute('value')).toBe('');
// TODO: we should warn here.
});
});
describe('When given a function value', function() {
it('treats initial function value as an empty string', function() {
expect(() =>
ReactDOM.render(
<input value={() => {}} onChange={() => {}} />,
container,
),
).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', function() {
ReactDOM.render(<input value="foo" onChange={() => {}} />, container);
expect(() =>
ReactDOM.render(
<input value={() => {}} onChange={() => {}} />,
container,
),
).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', function() {
ReactDOM.render(<input defaultValue={() => {}} />, container);
const node = container.firstChild;
expect(node.value).toBe('');
expect(node.getAttribute('value')).toBe('');
// TODO: we should warn here.
});
it('treats updated function defaultValue as an empty string', function() {
ReactDOM.render(<input defaultValue="foo" />, container);
ReactDOM.render(<input defaultValue={() => {}} />, container);
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('');
}
// TODO: we should warn here.
});
});
describe('checked inputs without a value property', function() {
// In absence of a value, radio and checkboxes report a value of "on".
// 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(
<input type="checkbox" defaultChecked={true} />,
container,
);
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(<input type="radio" defaultChecked={true} />, container);
const node = container.firstChild;
expect(node.value).toBe('on');
expect(node.hasAttribute('value')).toBe(false);
});
});
});