mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
* 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.
734 lines
23 KiB
JavaScript
734 lines
23 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';
|
|
|
|
function emptyFunction() {}
|
|
|
|
describe('ReactDOMTextarea', () => {
|
|
let React;
|
|
let ReactDOM;
|
|
let ReactDOMServer;
|
|
let ReactTestUtils;
|
|
|
|
let renderTextarea;
|
|
|
|
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
|
|
React = require('react');
|
|
ReactDOM = require('react-dom');
|
|
ReactDOMServer = require('react-dom/server');
|
|
ReactTestUtils = require('react-dom/test-utils');
|
|
|
|
renderTextarea = function(component, container) {
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
}
|
|
const node = ReactDOM.render(component, container);
|
|
|
|
// Fixing jsdom's quirky behavior -- in reality, the parser should strip
|
|
// off the leading newline but we need to do it by hand here.
|
|
node.defaultValue = node.innerHTML.replace(/^\n/, '');
|
|
return node;
|
|
};
|
|
});
|
|
|
|
it('should allow setting `defaultValue`', () => {
|
|
const container = document.createElement('div');
|
|
const node = renderTextarea(<textarea defaultValue="giraffe" />, container);
|
|
|
|
expect(node.value).toBe('giraffe');
|
|
|
|
// Changing `defaultValue` should do nothing.
|
|
renderTextarea(<textarea defaultValue="gorilla" />, container);
|
|
expect(node.value).toEqual('giraffe');
|
|
|
|
node.value = 'cat';
|
|
|
|
renderTextarea(<textarea defaultValue="monkey" />, container);
|
|
expect(node.value).toEqual('cat');
|
|
});
|
|
|
|
it('should display `defaultValue` of number 0', () => {
|
|
const stub = <textarea defaultValue={0} />;
|
|
const node = renderTextarea(stub);
|
|
|
|
expect(node.value).toBe('0');
|
|
});
|
|
|
|
it('should display "false" for `defaultValue` of `false`', () => {
|
|
const stub = <textarea defaultValue={false} />;
|
|
const node = renderTextarea(stub);
|
|
|
|
expect(node.value).toBe('false');
|
|
});
|
|
|
|
it('should display "foobar" for `defaultValue` of `objToString`', () => {
|
|
const objToString = {
|
|
toString: function() {
|
|
return 'foobar';
|
|
},
|
|
};
|
|
|
|
const stub = <textarea defaultValue={objToString} />;
|
|
const node = renderTextarea(stub);
|
|
|
|
expect(node.value).toBe('foobar');
|
|
});
|
|
|
|
it('should set defaultValue', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<textarea defaultValue="foo" />, container);
|
|
ReactDOM.render(<textarea defaultValue="bar" />, container);
|
|
ReactDOM.render(<textarea defaultValue="noise" />, container);
|
|
expect(container.firstChild.defaultValue).toBe('noise');
|
|
});
|
|
|
|
it('should not render value as an attribute', () => {
|
|
const stub = <textarea value="giraffe" onChange={emptyFunction} />;
|
|
const node = renderTextarea(stub);
|
|
|
|
expect(node.getAttribute('value')).toBe(null);
|
|
});
|
|
|
|
it('should display `value` of number 0', () => {
|
|
const stub = <textarea value={0} onChange={emptyFunction} />;
|
|
const node = renderTextarea(stub);
|
|
|
|
expect(node.value).toBe('0');
|
|
});
|
|
|
|
it('should update defaultValue to empty string', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<textarea defaultValue={'foo'} />, container);
|
|
ReactDOM.render(<textarea defaultValue={''} />, container);
|
|
expect(container.firstChild.defaultValue).toBe('');
|
|
});
|
|
|
|
it('should allow setting `value` to `giraffe`', () => {
|
|
const container = document.createElement('div');
|
|
let stub = <textarea value="giraffe" onChange={emptyFunction} />;
|
|
const node = renderTextarea(stub, container);
|
|
|
|
expect(node.value).toBe('giraffe');
|
|
|
|
stub = ReactDOM.render(
|
|
<textarea value="gorilla" onChange={emptyFunction} />,
|
|
container,
|
|
);
|
|
expect(node.value).toEqual('gorilla');
|
|
});
|
|
|
|
it('will not initially assign an empty value (covers case where firefox throws a validation error when required attribute is set)', () => {
|
|
const container = document.createElement('div');
|
|
|
|
let counter = 0;
|
|
const originalCreateElement = document.createElement;
|
|
spyOnDevAndProd(document, 'createElement').and.callFake(function(type) {
|
|
const el = originalCreateElement.apply(this, arguments);
|
|
let value = '';
|
|
if (type === 'textarea') {
|
|
Object.defineProperty(el, 'value', {
|
|
get: function() {
|
|
return value;
|
|
},
|
|
set: function(val) {
|
|
value = String(val);
|
|
counter++;
|
|
},
|
|
});
|
|
}
|
|
return el;
|
|
});
|
|
|
|
ReactDOM.render(<textarea value="" readOnly={true} />, container);
|
|
|
|
expect(counter).toEqual(0);
|
|
});
|
|
|
|
it('should render defaultValue for SSR', () => {
|
|
const markup = ReactDOMServer.renderToString(<textarea defaultValue="1" />);
|
|
const div = document.createElement('div');
|
|
div.innerHTML = markup;
|
|
expect(div.firstChild.innerHTML).toBe('1');
|
|
expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
|
|
});
|
|
|
|
it('should render value for SSR', () => {
|
|
const element = <textarea value="1" onChange={function() {}} />;
|
|
const markup = ReactDOMServer.renderToString(element);
|
|
const div = document.createElement('div');
|
|
div.innerHTML = markup;
|
|
expect(div.firstChild.innerHTML).toBe('1');
|
|
expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
|
|
});
|
|
|
|
it('should allow setting `value` to `true`', () => {
|
|
const container = document.createElement('div');
|
|
let stub = <textarea value="giraffe" onChange={emptyFunction} />;
|
|
const node = renderTextarea(stub, container);
|
|
|
|
expect(node.value).toBe('giraffe');
|
|
|
|
stub = ReactDOM.render(
|
|
<textarea value={true} onChange={emptyFunction} />,
|
|
container,
|
|
);
|
|
expect(node.value).toEqual('true');
|
|
});
|
|
|
|
it('should allow setting `value` to `false`', () => {
|
|
const container = document.createElement('div');
|
|
let stub = <textarea value="giraffe" onChange={emptyFunction} />;
|
|
const node = renderTextarea(stub, container);
|
|
|
|
expect(node.value).toBe('giraffe');
|
|
|
|
stub = ReactDOM.render(
|
|
<textarea value={false} onChange={emptyFunction} />,
|
|
container,
|
|
);
|
|
expect(node.value).toEqual('false');
|
|
});
|
|
|
|
it('should allow setting `value` to `objToString`', () => {
|
|
const container = document.createElement('div');
|
|
let stub = <textarea value="giraffe" onChange={emptyFunction} />;
|
|
const node = renderTextarea(stub, container);
|
|
|
|
expect(node.value).toBe('giraffe');
|
|
|
|
const objToString = {
|
|
toString: function() {
|
|
return 'foo';
|
|
},
|
|
};
|
|
stub = ReactDOM.render(
|
|
<textarea value={objToString} onChange={emptyFunction} />,
|
|
container,
|
|
);
|
|
expect(node.value).toEqual('foo');
|
|
});
|
|
|
|
it('should throw when value is set to a Temporal-like object', () => {
|
|
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 container = document.createElement('div');
|
|
const stub = <textarea value="giraffe" onChange={emptyFunction} />;
|
|
const node = renderTextarea(stub, container);
|
|
|
|
expect(node.value).toBe('giraffe');
|
|
|
|
const test = () =>
|
|
ReactDOM.render(
|
|
<textarea value={new TemporalLike()} onChange={emptyFunction} />,
|
|
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 take updates to `defaultValue` for uncontrolled textarea', () => {
|
|
const container = document.createElement('div');
|
|
|
|
const node = ReactDOM.render(<textarea defaultValue="0" />, container);
|
|
|
|
expect(node.value).toBe('0');
|
|
|
|
ReactDOM.render(<textarea defaultValue="1" />, container);
|
|
|
|
expect(node.value).toBe('0');
|
|
});
|
|
|
|
it('should take updates to children in lieu of `defaultValue` for uncontrolled textarea', () => {
|
|
const container = document.createElement('div');
|
|
|
|
const node = ReactDOM.render(<textarea defaultValue="0" />, container);
|
|
|
|
expect(node.value).toBe('0');
|
|
|
|
ReactDOM.render(<textarea>1</textarea>, container);
|
|
|
|
expect(node.value).toBe('0');
|
|
});
|
|
|
|
it('should not incur unnecessary DOM mutations', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<textarea value="a" onChange={emptyFunction} />, 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(<textarea value="a" onChange={emptyFunction} />, container);
|
|
expect(nodeValueSetter).toHaveBeenCalledTimes(0);
|
|
|
|
ReactDOM.render(<textarea value="b" onChange={emptyFunction} />, container);
|
|
expect(nodeValueSetter).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should properly control a value of number `0`', () => {
|
|
const stub = <textarea value={0} onChange={emptyFunction} />;
|
|
const setUntrackedValue = Object.getOwnPropertyDescriptor(
|
|
HTMLTextAreaElement.prototype,
|
|
'value',
|
|
).set;
|
|
|
|
const container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
|
|
try {
|
|
const node = renderTextarea(stub, container);
|
|
|
|
setUntrackedValue.call(node, 'giraffe');
|
|
node.dispatchEvent(
|
|
new Event('input', {bubbles: true, cancelable: false}),
|
|
);
|
|
expect(node.value).toBe('0');
|
|
} finally {
|
|
document.body.removeChild(container);
|
|
}
|
|
});
|
|
|
|
if (ReactFeatureFlags.disableTextareaChildren) {
|
|
it('should ignore children content', () => {
|
|
const container = document.createElement('div');
|
|
let stub = <textarea>giraffe</textarea>;
|
|
let node;
|
|
|
|
expect(() => {
|
|
node = renderTextarea(stub, container);
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
|
|
);
|
|
expect(node.value).toBe('');
|
|
// Changing children should do nothing, it functions like `defaultValue`.
|
|
stub = ReactDOM.render(<textarea>gorilla</textarea>, container);
|
|
expect(node.value).toEqual('');
|
|
});
|
|
}
|
|
|
|
if (ReactFeatureFlags.disableTextareaChildren) {
|
|
it('should receive defaultValue and still ignore children content', () => {
|
|
let node;
|
|
|
|
expect(() => {
|
|
node = renderTextarea(
|
|
<textarea defaultValue="dragon">monkey</textarea>,
|
|
);
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
|
|
);
|
|
expect(node.value).toBe('dragon');
|
|
});
|
|
}
|
|
|
|
if (!ReactFeatureFlags.disableTextareaChildren) {
|
|
it('should treat children like `defaultValue`', () => {
|
|
const container = document.createElement('div');
|
|
let stub = <textarea>giraffe</textarea>;
|
|
let node;
|
|
|
|
expect(() => {
|
|
node = renderTextarea(stub, container);
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
|
|
);
|
|
|
|
expect(node.value).toBe('giraffe');
|
|
|
|
// Changing children should do nothing, it functions like `defaultValue`.
|
|
stub = ReactDOM.render(<textarea>gorilla</textarea>, container);
|
|
expect(node.value).toEqual('giraffe');
|
|
});
|
|
}
|
|
|
|
it('should keep value when switching to uncontrolled element if not changed', () => {
|
|
const container = document.createElement('div');
|
|
|
|
const node = renderTextarea(
|
|
<textarea value="kitten" onChange={emptyFunction} />,
|
|
container,
|
|
);
|
|
|
|
expect(node.value).toBe('kitten');
|
|
|
|
ReactDOM.render(<textarea defaultValue="gorilla" />, container);
|
|
|
|
expect(node.value).toEqual('kitten');
|
|
});
|
|
|
|
it('should keep value when switching to uncontrolled element if changed', () => {
|
|
const container = document.createElement('div');
|
|
|
|
const node = renderTextarea(
|
|
<textarea value="kitten" onChange={emptyFunction} />,
|
|
container,
|
|
);
|
|
|
|
expect(node.value).toBe('kitten');
|
|
|
|
ReactDOM.render(
|
|
<textarea value="puppies" onChange={emptyFunction} />,
|
|
container,
|
|
);
|
|
|
|
expect(node.value).toBe('puppies');
|
|
|
|
ReactDOM.render(<textarea defaultValue="gorilla" />, container);
|
|
|
|
expect(node.value).toEqual('puppies');
|
|
});
|
|
|
|
if (ReactFeatureFlags.disableTextareaChildren) {
|
|
it('should ignore numbers as children', () => {
|
|
let node;
|
|
expect(() => {
|
|
node = renderTextarea(<textarea>{17}</textarea>);
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
|
|
);
|
|
expect(node.value).toBe('');
|
|
});
|
|
}
|
|
|
|
if (!ReactFeatureFlags.disableTextareaChildren) {
|
|
it('should allow numbers as children', () => {
|
|
let node;
|
|
expect(() => {
|
|
node = renderTextarea(<textarea>{17}</textarea>);
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
|
|
);
|
|
expect(node.value).toBe('17');
|
|
});
|
|
}
|
|
|
|
if (ReactFeatureFlags.disableTextareaChildren) {
|
|
it('should ignore booleans as children', () => {
|
|
let node;
|
|
expect(() => {
|
|
node = renderTextarea(<textarea>{false}</textarea>);
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
|
|
);
|
|
expect(node.value).toBe('');
|
|
});
|
|
}
|
|
|
|
if (!ReactFeatureFlags.disableTextareaChildren) {
|
|
it('should allow booleans as children', () => {
|
|
let node;
|
|
expect(() => {
|
|
node = renderTextarea(<textarea>{false}</textarea>);
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
|
|
);
|
|
expect(node.value).toBe('false');
|
|
});
|
|
}
|
|
|
|
if (ReactFeatureFlags.disableTextareaChildren) {
|
|
it('should ignore objects as children', () => {
|
|
const obj = {
|
|
toString: function() {
|
|
return 'sharkswithlasers';
|
|
},
|
|
};
|
|
let node;
|
|
expect(() => {
|
|
node = renderTextarea(<textarea>{obj}</textarea>);
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
|
|
);
|
|
expect(node.value).toBe('');
|
|
});
|
|
}
|
|
|
|
if (!ReactFeatureFlags.disableTextareaChildren) {
|
|
it('should allow objects as children', () => {
|
|
const obj = {
|
|
toString: function() {
|
|
return 'sharkswithlasers';
|
|
},
|
|
};
|
|
let node;
|
|
expect(() => {
|
|
node = renderTextarea(<textarea>{obj}</textarea>);
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
|
|
);
|
|
expect(node.value).toBe('sharkswithlasers');
|
|
});
|
|
}
|
|
|
|
if (!ReactFeatureFlags.disableTextareaChildren) {
|
|
it('should throw with multiple or invalid children', () => {
|
|
expect(() => {
|
|
expect(() =>
|
|
ReactTestUtils.renderIntoDocument(
|
|
<textarea>
|
|
{'hello'}
|
|
{'there'}
|
|
</textarea>,
|
|
),
|
|
).toThrow('<textarea> can only have at most one child');
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
|
|
);
|
|
|
|
let node;
|
|
expect(() => {
|
|
expect(
|
|
() =>
|
|
(node = renderTextarea(
|
|
<textarea>
|
|
<strong />
|
|
</textarea>,
|
|
)),
|
|
).not.toThrow();
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props instead of setting children on <textarea>.',
|
|
);
|
|
|
|
expect(node.value).toBe('[object Object]');
|
|
});
|
|
}
|
|
|
|
it('should unmount', () => {
|
|
const container = document.createElement('div');
|
|
renderTextarea(<textarea />, container);
|
|
ReactDOM.unmountComponentAtNode(container);
|
|
});
|
|
|
|
it('should warn if value is null', () => {
|
|
expect(() =>
|
|
ReactTestUtils.renderIntoDocument(<textarea value={null} />),
|
|
).toErrorDev(
|
|
'`value` prop on `textarea` should not be null. ' +
|
|
'Consider using an empty string to clear the component or `undefined` ' +
|
|
'for uncontrolled components.',
|
|
);
|
|
|
|
// No additional warnings are expected
|
|
ReactTestUtils.renderIntoDocument(<textarea value={null} />);
|
|
});
|
|
|
|
it('should warn if value and defaultValue are specified', () => {
|
|
const InvalidComponent = () => (
|
|
<textarea value="foo" defaultValue="bar" readOnly={true} />
|
|
);
|
|
expect(() =>
|
|
ReactTestUtils.renderIntoDocument(<InvalidComponent />),
|
|
).toErrorDev(
|
|
'InvalidComponent contains a textarea with both value and defaultValue props. ' +
|
|
'Textarea 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 textarea ' +
|
|
'and remove one of these props. More info: ' +
|
|
'https://reactjs.org/link/controlled-components',
|
|
);
|
|
|
|
// No additional warnings are expected
|
|
ReactTestUtils.renderIntoDocument(<InvalidComponent />);
|
|
});
|
|
|
|
it('should not warn about missing onChange in uncontrolled textareas', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<textarea />, container);
|
|
ReactDOM.unmountComponentAtNode(container);
|
|
ReactDOM.render(<textarea value={undefined} />, container);
|
|
});
|
|
|
|
it('does not set textContent if value is unchanged', () => {
|
|
const container = document.createElement('div');
|
|
let node;
|
|
let instance;
|
|
// Setting defaultValue on a textarea is equivalent to setting textContent,
|
|
// and is the method we currently use, so we can observe if defaultValue is
|
|
// is set to determine if textContent is being recreated.
|
|
// https://html.spec.whatwg.org/#the-textarea-element
|
|
let defaultValue;
|
|
const set = jest.fn(value => {
|
|
defaultValue = value;
|
|
});
|
|
const get = jest.fn(value => {
|
|
return defaultValue;
|
|
});
|
|
class App extends React.Component {
|
|
state = {count: 0, text: 'foo'};
|
|
componentDidMount() {
|
|
instance = this;
|
|
}
|
|
render() {
|
|
return (
|
|
<div>
|
|
<span>{this.state.count}</span>
|
|
<textarea
|
|
ref={n => (node = n)}
|
|
value="foo"
|
|
onChange={emptyFunction}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
ReactDOM.render(<App />, container);
|
|
defaultValue = node.defaultValue;
|
|
Object.defineProperty(node, 'defaultValue', {get, set});
|
|
instance.setState({count: 1});
|
|
expect(set.mock.calls.length).toBe(0);
|
|
});
|
|
|
|
describe('When given a Symbol value', () => {
|
|
it('treats initial Symbol value as an empty string', () => {
|
|
const container = document.createElement('div');
|
|
expect(() =>
|
|
ReactDOM.render(
|
|
<textarea value={Symbol('foobar')} onChange={() => {}} />,
|
|
container,
|
|
),
|
|
).toErrorDev('Invalid value for prop `value`');
|
|
const node = container.firstChild;
|
|
|
|
expect(node.value).toBe('');
|
|
});
|
|
|
|
it('treats initial Symbol children as an empty string', () => {
|
|
const container = document.createElement('div');
|
|
expect(() =>
|
|
ReactDOM.render(
|
|
<textarea onChange={() => {}}>{Symbol('foo')}</textarea>,
|
|
container,
|
|
),
|
|
).toErrorDev('Use the `defaultValue` or `value` props');
|
|
const node = container.firstChild;
|
|
|
|
expect(node.value).toBe('');
|
|
});
|
|
|
|
it('treats updated Symbol value as an empty string', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<textarea value="foo" onChange={() => {}} />, container);
|
|
expect(() =>
|
|
ReactDOM.render(
|
|
<textarea value={Symbol('foo')} onChange={() => {}} />,
|
|
container,
|
|
),
|
|
).toErrorDev('Invalid value for prop `value`');
|
|
const node = container.firstChild;
|
|
|
|
expect(node.value).toBe('');
|
|
});
|
|
|
|
it('treats initial Symbol defaultValue as an empty string', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<textarea defaultValue={Symbol('foobar')} />, container);
|
|
const node = container.firstChild;
|
|
|
|
// TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
|
|
expect(node.value).toBe('');
|
|
});
|
|
|
|
it('treats updated Symbol defaultValue as an empty string', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<textarea defaultValue="foo" />, container);
|
|
ReactDOM.render(<textarea defaultValue={Symbol('foobar')} />, container);
|
|
const node = container.firstChild;
|
|
|
|
// TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
|
|
expect(node.value).toBe('foo');
|
|
});
|
|
});
|
|
|
|
describe('When given a function value', () => {
|
|
it('treats initial function value as an empty string', () => {
|
|
const container = document.createElement('div');
|
|
expect(() =>
|
|
ReactDOM.render(
|
|
<textarea value={() => {}} onChange={() => {}} />,
|
|
container,
|
|
),
|
|
).toErrorDev('Invalid value for prop `value`');
|
|
const node = container.firstChild;
|
|
|
|
expect(node.value).toBe('');
|
|
});
|
|
|
|
it('treats initial function children as an empty string', () => {
|
|
const container = document.createElement('div');
|
|
expect(() =>
|
|
ReactDOM.render(
|
|
<textarea onChange={() => {}}>{() => {}}</textarea>,
|
|
container,
|
|
),
|
|
).toErrorDev('Use the `defaultValue` or `value` props');
|
|
const node = container.firstChild;
|
|
|
|
expect(node.value).toBe('');
|
|
});
|
|
|
|
it('treats updated function value as an empty string', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<textarea value="foo" onChange={() => {}} />, container);
|
|
expect(() =>
|
|
ReactDOM.render(
|
|
<textarea value={() => {}} onChange={() => {}} />,
|
|
container,
|
|
),
|
|
).toErrorDev('Invalid value for prop `value`');
|
|
const node = container.firstChild;
|
|
|
|
expect(node.value).toBe('');
|
|
});
|
|
|
|
it('treats initial function defaultValue as an empty string', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<textarea defaultValue={() => {}} />, container);
|
|
const node = container.firstChild;
|
|
|
|
// TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
|
|
expect(node.value).toBe('');
|
|
});
|
|
|
|
it('treats updated function defaultValue as an empty string', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<textarea defaultValue="foo" />, container);
|
|
ReactDOM.render(<textarea defaultValue={() => {}} />, container);
|
|
const node = container.firstChild;
|
|
|
|
// TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
|
|
expect(node.value).toBe('foo');
|
|
});
|
|
});
|
|
});
|