Add assertions about <input> value dirty state (#26626)
Since this is an observable behavior and is hard to think about, seems good to have tests for this. The expected value included in each test is the behavior that existed prior to #26546.
Sophie Alpert committed
Apr 18, 2023 at 11:03 UTC
1b4a0daba8aba71f6fbf0c3b88633c51207a6ec7
1 file changed
+65
-3
packages/react-dom/src/__tests__/ReactDOMInput-test.js
+65
-3
@@ -26,6 +26,16 @@ describe('ReactDOMInput', () => {
26
node.dispatchEvent(new Event(type, {bubbles: true, cancelable: true}));
27
}
28
29
+ function isValueDirty(node) {
30
+ // Return the "dirty value flag" as defined in the HTML spec. Cast to text
31
+ // input to sidestep complicated value sanitization behaviors.
32
+ const copy = node.cloneNode();
33
+ copy.type = 'text';
34
+ // If modifying the attribute now doesn't change the value, the value was already detached.
35
+ copy.defaultValue += Math.random();
36
+ return copy.value === node.value;
37
+ }
38
+
39
beforeEach(() => {
40
jest.resetModules();
41
@@ -128,6 +138,7 @@ describe('ReactDOMInput', () => {
138
}).toErrorDev(
139
'Warning: You provided a `value` prop to a form field without an `onChange` handler.',
140
);
141
+ expect(isValueDirty(node)).toBe(true);
142
143
setUntrackedValue.call(node, 'giraffe');
144
@@ -136,6 +147,7 @@ describe('ReactDOMInput', () => {
147
dispatchEventOnNode(node, 'input');
148
149
expect(node.value).toBe('lion');
150
+ expect(isValueDirty(node)).toBe(true);
151
});
152
153
it('should control a value in reentrant events', () => {
@@ -438,15 +450,22 @@ describe('ReactDOMInput', () => {
450
451
expect(node.value).toBe('0');
452
expect(node.defaultValue).toBe('0');
453
+ if (disableInputAttributeSyncing) {
454
+ expect(isValueDirty(node)).toBe(false);
455
+ } else {
456
+ expect(isValueDirty(node)).toBe(true);
457
+ }
458
459
ReactDOM.render(<input type="text" defaultValue="1" />, container);
460
461
if (disableInputAttributeSyncing) {
462
expect(node.value).toBe('1');
463
expect(node.defaultValue).toBe('1');
464
+ expect(isValueDirty(node)).toBe(false);
465
} else {
466
expect(node.value).toBe('0');
467
expect(node.defaultValue).toBe('1');
468
+ expect(isValueDirty(node)).toBe(true);
469
}
470
});
471
@@ -478,12 +497,14 @@ describe('ReactDOMInput', () => {
497
container,
498
);
499
expect(node.value).toBe('0');
500
+ expect(isValueDirty(node)).toBe(true);
501
expect(() =>
502
ReactDOM.render(<input type="text" defaultValue="1" />, container),
503
).toErrorDev(
504
'A component is changing a controlled input to be uncontrolled.',
505
);
506
expect(node.value).toBe('0');
507
+ expect(isValueDirty(node)).toBe(true);
508
});
509
510
it('should render defaultValue for SSR', () => {
@@ -794,13 +815,16 @@ describe('ReactDOMInput', () => {
815
<input type="text" value="" onChange={emptyFunction} />,
816
container,
817
);
818
+ const node = container.firstChild;
819
+ expect(isValueDirty(node)).toBe(false);
820
+
821
ReactDOM.render(
822
<input type="text" value={0} onChange={emptyFunction} />,
823
container,
824
);
825
802
- const node = container.firstChild;
826
expect(node.value).toBe('0');
827
+ expect(isValueDirty(node)).toBe(true);
828
829
if (disableInputAttributeSyncing) {
830
expect(node.hasAttribute('value')).toBe(false);
@@ -814,15 +838,17 @@ describe('ReactDOMInput', () => {
838
<input type="text" value={0} onChange={emptyFunction} />,
839
container,
840
);
841
+ const node = container.firstChild;
842
+ expect(isValueDirty(node)).toBe(true);
843
+
844
ReactDOM.render(
845
<input type="text" value="" onChange={emptyFunction} />,
846
container,
847
);
848
822
- const node = container.firstChild;
823
-
849
expect(node.value).toBe('');
850
expect(node.defaultValue).toBe('');
851
+ expect(isValueDirty(node)).toBe(true);
852
});
853
854
it('should properly transition a text input from 0 to an empty 0.0', function () {
@@ -911,10 +937,16 @@ describe('ReactDOMInput', () => {
937
container,
938
);
939
expect(inputRef.current.value).toBe('default1');
940
+ if (disableInputAttributeSyncing) {
941
+ expect(isValueDirty(inputRef.current)).toBe(false);
942
+ } else {
943
+ expect(isValueDirty(inputRef.current)).toBe(true);
944
+ }
945
946
setUntrackedValue.call(inputRef.current, 'changed');
947
dispatchEventOnNode(inputRef.current, 'input');
948
expect(inputRef.current.value).toBe('changed');
949
+ expect(isValueDirty(inputRef.current)).toBe(true);
950
951
ReactDOM.render(
952
<form>
@@ -924,12 +956,14 @@ describe('ReactDOMInput', () => {
956
container,
957
);
958
expect(inputRef.current.value).toBe('changed');
959
+ expect(isValueDirty(inputRef.current)).toBe(true);
960
961
container.firstChild.reset();
962
// Note: I don't know if we want to always support this.
963
// But it's current behavior so worth being intentional if we break it.
964
// https://github.com/facebook/react/issues/4618
965
expect(inputRef.current.value).toBe('default2');
966
+ expect(isValueDirty(inputRef.current)).toBe(false);
967
});
968
969
it('should not set a value for submit buttons unnecessarily', () => {
@@ -1300,8 +1334,18 @@ describe('ReactDOMInput', () => {
1334
1335
it('should update defaultValue to empty string', () => {
1336
ReactDOM.render(<input type="text" defaultValue={'foo'} />, container);
1337
+ if (disableInputAttributeSyncing) {
1338
+ expect(isValueDirty(container.firstChild)).toBe(false);
1339
+ } else {
1340
+ expect(isValueDirty(container.firstChild)).toBe(true);
1341
+ }
1342
ReactDOM.render(<input type="text" defaultValue={''} />, container);
1343
expect(container.firstChild.defaultValue).toBe('');
1344
+ if (disableInputAttributeSyncing) {
1345
+ expect(isValueDirty(container.firstChild)).toBe(false);
1346
+ } else {
1347
+ expect(isValueDirty(container.firstChild)).toBe(true);
1348
+ }
1349
});
1350
1351
it('should warn if value is null', () => {
@@ -1838,10 +1882,12 @@ describe('ReactDOMInput', () => {
1882
const Input = getTestInput();
1883
const stub = ReactDOM.render(<Input type="text" />, container);
1884
const node = ReactDOM.findDOMNode(stub);
1885
+ expect(isValueDirty(node)).toBe(false);
1886
1887
setUntrackedValue.call(node, '2');
1888
dispatchEventOnNode(node, 'input');
1889
1890
+ expect(isValueDirty(node)).toBe(true);
1891
if (disableInputAttributeSyncing) {
1892
expect(node.hasAttribute('value')).toBe(false);
1893
} else {
@@ -1856,12 +1902,14 @@ describe('ReactDOMInput', () => {
1902
container,
1903
);
1904
const node = ReactDOM.findDOMNode(stub);
1905
+ expect(isValueDirty(node)).toBe(true);
1906
1907
node.focus();
1908
1909
setUntrackedValue.call(node, '2');
1910
dispatchEventOnNode(node, 'input');
1911
1912
+ expect(isValueDirty(node)).toBe(true);
1913
if (disableInputAttributeSyncing) {
1914
expect(node.hasAttribute('value')).toBe(false);
1915
} else {
@@ -1876,12 +1924,14 @@ describe('ReactDOMInput', () => {
1924
container,
1925
);
1926
const node = ReactDOM.findDOMNode(stub);
1927
+ expect(isValueDirty(node)).toBe(true);
1928
1929
node.focus();
1930
setUntrackedValue.call(node, '2');
1931
dispatchEventOnNode(node, 'input');
1932
node.blur();
1933
1934
+ expect(isValueDirty(node)).toBe(true);
1935
if (disableInputAttributeSyncing) {
1936
expect(node.value).toBe('2');
1937
expect(node.hasAttribute('value')).toBe(false);
@@ -1896,12 +1946,18 @@ describe('ReactDOMInput', () => {
1946
<input type="number" defaultValue="1" />,
1947
container,
1948
);
1949
+ if (disableInputAttributeSyncing) {
1950
+ expect(isValueDirty(node)).toBe(false);
1951
+ } else {
1952
+ expect(isValueDirty(node)).toBe(true);
1953
+ }
1954
1955
node.focus();
1956
setUntrackedValue.call(node, 4);
1957
dispatchEventOnNode(node, 'input');
1958
node.blur();
1959
1960
+ expect(isValueDirty(node)).toBe(true);
1961
expect(node.getAttribute('value')).toBe('1');
1962
});
1963
@@ -1910,12 +1966,18 @@ describe('ReactDOMInput', () => {
1966
<input type="text" defaultValue="1" />,
1967
container,
1968
);
1969
+ if (disableInputAttributeSyncing) {
1970
+ expect(isValueDirty(node)).toBe(false);
1971
+ } else {
1972
+ expect(isValueDirty(node)).toBe(true);
1973
+ }
1974
1975
node.focus();
1976
setUntrackedValue.call(node, 4);
1977
dispatchEventOnNode(node, 'input');
1978
node.blur();
1979
1980
+ expect(isValueDirty(node)).toBe(true);
1981
expect(node.getAttribute('value')).toBe('1');
1982
});
1983
});