@samitouri / QOS-React-2 / commits / 1b4a0daba8

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 });