Skip to content

Commit 0715051

Browse files
committed
Update tests, add some caveats for SSR mismatches
1 parent 14e23cb commit 0715051

File tree

4 files changed

+163
-55
lines changed

4 files changed

+163
-55
lines changed

packages/react-dom/src/__tests__/DOMPropertyOperations-test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe('DOMPropertyOperations', () => {
8080
it('should not remove empty attributes for special input properties', () => {
8181
const container = document.createElement('div');
8282
ReactDOM.render(<input value="" onChange={() => {}} />, container);
83-
expect(container.firstChild.getAttribute('value')).toBe('');
83+
expect(container.firstChild.hasAttribute('value')).toBe(false);
8484
expect(container.firstChild.value).toBe('');
8585
});
8686

@@ -165,7 +165,7 @@ describe('DOMPropertyOperations', () => {
165165
<input type="text" value="foo" onChange={function() {}} />,
166166
container,
167167
);
168-
expect(container.firstChild.getAttribute('value')).toBe('foo');
168+
expect(container.firstChild.hasAttribute('value')).toBe(false);
169169
expect(container.firstChild.value).toBe('foo');
170170
expect(() =>
171171
ReactDOM.render(
@@ -175,7 +175,7 @@ describe('DOMPropertyOperations', () => {
175175
).toWarnDev(
176176
'A component is changing a controlled input of type text to be uncontrolled',
177177
);
178-
expect(container.firstChild.getAttribute('value')).toBe('foo');
178+
expect(container.firstChild.hasAttribute('value')).toBe(false);
179179
expect(container.firstChild.value).toBe('foo');
180180
});
181181

packages/react-dom/src/__tests__/ReactDOMInput-test.js

Lines changed: 89 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ describe('ReactDOMInput', () => {
245245

246246
dispatchEventOnNode(node, 'input');
247247

248-
expect(node.hasAttribute('value')).toBe(false);;
248+
expect(node.hasAttribute('value')).toBe(false);
249249
expect(node.value).toBe('2');
250250
});
251251

@@ -685,7 +685,7 @@ describe('ReactDOMInput', () => {
685685
const node = container.firstChild;
686686

687687
expect(node.value).toBe('');
688-
expect(node.hasAttribute('value')).toBe(false)
688+
expect(node.hasAttribute('value')).toBe(false);
689689
});
690690

691691
it('should properly transition a text input from 0 to an empty 0.0', function() {
@@ -701,7 +701,7 @@ describe('ReactDOMInput', () => {
701701
const node = container.firstChild;
702702

703703
expect(node.value).toBe('0.0');
704-
expect(node.hasAttribute('value')).toBe(false)
704+
expect(node.hasAttribute('value')).toBe(false);
705705
});
706706

707707
it('should properly transition a number input from "" to 0', function() {
@@ -1145,7 +1145,7 @@ describe('ReactDOMInput', () => {
11451145
ReactDOM.render(<input type="text" value={null} />, container);
11461146
});
11471147

1148-
it.skip('should warn if checked and defaultChecked props are specified', () => {
1148+
it('should warn if checked and defaultChecked props are specified', () => {
11491149
expect(() =>
11501150
ReactDOM.render(
11511151
<input
@@ -1177,7 +1177,7 @@ describe('ReactDOMInput', () => {
11771177
);
11781178
});
11791179

1180-
it.skip('should warn if value and defaultValue props are specified', () => {
1180+
it('should warn if value and defaultValue props are specified', () => {
11811181
expect(() =>
11821182
ReactDOM.render(
11831183
<input type="text" value="foo" defaultValue="bar" readOnly={true} />,
@@ -1520,7 +1520,7 @@ describe('ReactDOMInput', () => {
15201520
'set attribute min',
15211521
'set attribute max',
15221522
'set attribute step',
1523-
'set property value'
1523+
'set property value',
15241524
]);
15251525
});
15261526

@@ -1639,44 +1639,99 @@ describe('ReactDOMInput', () => {
16391639
});
16401640

16411641
describe('setting a controlled input to undefined', () => {
1642-
it('preserves the value property', () => {
1643-
expect(() => {
1644-
ReactDOM.render(
1645-
<input type="number" value="1" readOnly={true} />,
1646-
container,
1647-
);
1648-
ReactDOM.render(
1649-
<input type="number" value={undefined} readOnly={true} />,
1650-
container,
1651-
);
1652-
}).toWarnDev(
1642+
let input;
1643+
1644+
function renderInputWithStringThenWithUndefined() {
1645+
let setValueToUndefined;
1646+
class Input extends React.Component {
1647+
constructor() {
1648+
super();
1649+
setValueToUndefined = () => this.setState({value: undefined});
1650+
}
1651+
state = {value: 'first'};
1652+
render() {
1653+
return (
1654+
<input
1655+
onChange={e => this.setState({value: e.target.value})}
1656+
value={this.state.value}
1657+
/>
1658+
);
1659+
}
1660+
}
1661+
1662+
const stub = ReactDOM.render(<Input />, container);
1663+
input = ReactDOM.findDOMNode(stub);
1664+
setUntrackedValue.call(input, 'latest');
1665+
dispatchEventOnNode(input, 'input');
1666+
setValueToUndefined();
1667+
}
1668+
1669+
it('does not set the value attribute', () => {
1670+
expect(renderInputWithStringThenWithUndefined).toWarnDev(
16531671
'Input elements should not switch from controlled to ' +
16541672
'uncontrolled (or vice versa).',
16551673
);
1656-
1657-
const input = container.firstChild;
1658-
expect(input.value).toBe('1');
16591674
expect(input.hasAttribute('value')).toBe(false);
16601675
});
1661-
});
16621676

1663-
describe.skip('setting a controlled input to null', () => {
16641677
it('preserves the value property', () => {
1665-
expect(() => {
1666-
ReactDOM.render(
1667-
<input type="number" value="1" readOnly={true} />,
1668-
container,
1669-
);
1670-
ReactDOM.render(
1671-
<input type="number" value={null} readOnly={true} />,
1672-
container,
1673-
);
1674-
}).toWarnDev('`value` prop on `input` should not be null');
1678+
expect(renderInputWithStringThenWithUndefined).toWarnDev(
1679+
'Input elements should not switch from controlled to ' +
1680+
'uncontrolled (or vice versa).',
1681+
);
1682+
expect(input.value).toBe('latest');
1683+
});
1684+
});
16751685

1676-
const input = container.firstChild;
1677-
expect(input.value).toBe('1');
1686+
describe('setting a controlled input to null', () => {
1687+
let input;
1688+
1689+
function renderInputWithStringThenWithNull() {
1690+
let setValueToNull;
1691+
class Input extends React.Component {
1692+
constructor() {
1693+
super();
1694+
setValueToNull = () => this.setState({value: null});
1695+
}
1696+
state = {value: 'first'};
1697+
render() {
1698+
return (
1699+
<input
1700+
onChange={e => this.setState({value: e.target.value})}
1701+
value={this.state.value}
1702+
/>
1703+
);
1704+
}
1705+
}
1706+
1707+
const stub = ReactDOM.render(<Input />, container);
1708+
input = ReactDOM.findDOMNode(stub);
1709+
setUntrackedValue.call(input, 'latest');
1710+
dispatchEventOnNode(input, 'input');
1711+
setValueToNull();
1712+
}
1713+
1714+
it('reverts the value attribute to the initial value', () => {
1715+
expect(renderInputWithStringThenWithNull).toWarnDev([
1716+
'`value` prop on `input` should not be null. ' +
1717+
'Consider using an empty string to clear the component ' +
1718+
'or `undefined` for uncontrolled components.',
1719+
'Input elements should not switch from controlled ' +
1720+
'to uncontrolled (or vice versa).',
1721+
]);
16781722
expect(input.hasAttribute('value')).toBe(false);
16791723
});
1724+
1725+
it('preserves the value property', () => {
1726+
expect(renderInputWithStringThenWithNull).toWarnDev([
1727+
'`value` prop on `input` should not be null. ' +
1728+
'Consider using an empty string to clear the component ' +
1729+
'or `undefined` for uncontrolled components.',
1730+
'Input elements should not switch from controlled ' +
1731+
'to uncontrolled (or vice versa).',
1732+
]);
1733+
expect(input.value).toBe('latest');
1734+
});
16801735
});
16811736

16821737
describe('When given a Symbol value', function() {

packages/react-dom/src/__tests__/ReactDOMServerIntegrationForms-test.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const {
3838
serverRender,
3939
} = ReactDOMServerIntegrationUtils(initModules);
4040

41-
describe('ReactDOMServerIntegration', () => {
41+
describe('ReactDOMServerIntegrationForms', () => {
4242
beforeEach(() => {
4343
resetModules();
4444
});
@@ -62,7 +62,10 @@ describe('ReactDOMServerIntegration', () => {
6262
// onChange or readOnly is a mistake.
6363
const e = await render(<input value="foo" />, 1);
6464
expect(e.value).toBe('foo');
65-
expect(e.getAttribute('value')).toBe('foo');
65+
// TODO: The value attribute has a mismatch. Server rendering
66+
// conveys the value property through the attribute (standard HTML),
67+
// however the client does not. What do we do in this case?
68+
// expect(e.getAttribute('value')).toBe('false');
6669
},
6770
);
6871

@@ -79,7 +82,10 @@ describe('ReactDOMServerIntegration', () => {
7982
1,
8083
);
8184
expect(e.value).toBe('foo');
82-
expect(e.getAttribute('value')).toBe('foo');
85+
// TODO: The value attribute has a mismatch. Server rendering
86+
// conveys the value property through the attribute (standard HTML),
87+
// however the client does not. What do we do in this case?
88+
// expect(e.getAttribute('value')).toBe('bar');
8389
expect(e.getAttribute('defaultValue')).toBe(null);
8490
});
8591

@@ -91,7 +97,10 @@ describe('ReactDOMServerIntegration', () => {
9197
1,
9298
);
9399
expect(e.value).toBe('foo');
94-
expect(e.getAttribute('value')).toBe('foo');
100+
// TODO: The value attribute has a mismatch. Server rendering
101+
// conveys the value property through the attribute (standard HTML),
102+
// however the client does not. What do we do in this case?
103+
// expect(e.getAttribute('value')).toBe('bar');
95104
expect(e.getAttribute('defaultValue')).toBe(null);
96105
},
97106
);

packages/react-dom/src/client/ReactDOMInput.js

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type InputWithWrapperState = HTMLInputElement & {
2727
};
2828

2929
let didWarnValueDefaultValue = false;
30+
let didWarnCheckedDefaultChecked = false;
3031
let didWarnControlledToUncontrolled = false;
3132
let didWarnUncontrolledToControlled = false;
3233

@@ -64,6 +65,43 @@ export function getHostProps(element: Element, props: Object) {
6465
export function initWrapperState(element: Element, props: Object) {
6566
if (__DEV__) {
6667
ReactControlledValuePropTypes.checkPropTypes('input', props);
68+
69+
if (
70+
props.checked !== undefined &&
71+
props.defaultChecked !== undefined &&
72+
!didWarnCheckedDefaultChecked
73+
) {
74+
warning(
75+
false,
76+
'%s contains an input of type %s with both checked and defaultChecked props. ' +
77+
'Input elements must be either controlled or uncontrolled ' +
78+
'(specify either the checked prop, or the defaultChecked prop, but not ' +
79+
'both). Decide between using a controlled or uncontrolled input ' +
80+
'element and remove one of these props. More info: ' +
81+
'https://fb.me/react-controlled-components',
82+
getCurrentFiberOwnerNameInDevOrNull() || 'A component',
83+
props.type,
84+
);
85+
didWarnCheckedDefaultChecked = true;
86+
}
87+
if (
88+
props.value !== undefined &&
89+
props.defaultValue !== undefined &&
90+
!didWarnValueDefaultValue
91+
) {
92+
warning(
93+
false,
94+
'%s contains an input of type %s with both value and defaultValue props. ' +
95+
'Input elements must be either controlled or uncontrolled ' +
96+
'(specify either the value prop, or the defaultValue prop, but not ' +
97+
'both). Decide between using a controlled or uncontrolled input ' +
98+
'element and remove one of these props. More info: ' +
99+
'https://fb.me/react-controlled-components',
100+
getCurrentFiberOwnerNameInDevOrNull() || 'A component',
101+
props.type,
102+
);
103+
didWarnValueDefaultValue = true;
104+
}
67105
}
68106

69107
const node = ((element: any): InputWithWrapperState);
@@ -167,20 +205,23 @@ export function postMountWrapper(
167205
props: Object,
168206
isHydrating: boolean,
169207
) {
208+
if (isHydrating) {
209+
return;
210+
}
211+
170212
const node: HTMLInputElement = element;
171213

172214
// Do not assign value if it is already set. This prevents user text input
173215
// from being lost during SSR hydration.
174-
if (!isHydrating && props.hasOwnProperty('value')) {
175-
const initialValue = getToStringValue(props.value);
176-
const currentValue = node.value;
216+
if (props.hasOwnProperty('value')) {
217+
const value = getToStringValue(props.value);
177218
const type = props.type;
178219

179220
// Avoid setting value attribute on submit/reset inputs as it overrides the
180221
// default value provided by the browser. See: #12872
181222
if (type === 'submit' || type === 'reset') {
182-
if (initialValue !== undefined && initialValue !== null) {
183-
node.setAttribute('value', initialValue);
223+
if (value !== undefined && value !== null) {
224+
node.setAttribute('value', value);
184225
}
185226

186227
return;
@@ -189,8 +230,8 @@ export function postMountWrapper(
189230
// Do not re-assign the value property if is empty. This
190231
// potentially avoids a DOM write and prevents Firefox (~60.0.1) from
191232
// prematurely marking required inputs as invalid
192-
if (initialValue !== '') {
193-
node.value = initialValue;
233+
if (value !== '') {
234+
node.value = value;
194235
}
195236
}
196237

@@ -201,7 +242,10 @@ export function postMountWrapper(
201242
node.defaultValue = getToStringValue(props.defaultValue);
202243
}
203244

204-
if (props.hasOwnProperty('defaultChecked')) {
245+
if (
246+
props.hasOwnProperty('checked') ||
247+
props.hasOwnProperty('defaultChecked')
248+
) {
205249
// Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug
206250
// this is needed to work around a chrome bug where setting defaultChecked
207251
// will sometimes influence the value of checked (even after detachment).
@@ -212,18 +256,18 @@ export function postMountWrapper(
212256
node.name = '';
213257
}
214258

215-
// Do not assign checked value if it is already set. This prevents user text input
216-
// from being lost during SSR hydration.
217-
if (!isHydrating && props.hasOwnProperty('checked')) {
218-
node.checked = !!props.checked;
219-
}
220-
221259
node.defaultChecked = !node.defaultChecked;
222260
node.defaultChecked = !!props.defaultChecked;
261+
262+
updateChecked(node, props);
263+
223264
if (name !== '') {
224265
node.name = name;
225266
}
226267
}
268+
269+
// TODO: This is necessary because assignment to checked/value is mitigated, but why?
270+
inputValueTracking.updateValueIfChanged(node);
227271
}
228272

229273
export function restoreControlledState(element: Element, props: Object) {

0 commit comments

Comments
 (0)