diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js index 47fca9522fac8..cae91efe6d7fc 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js @@ -26,6 +26,16 @@ describe('ReactDOMInput', () => { node.dispatchEvent(new Event(type, {bubbles: true, cancelable: true})); } + function isValueDirty(node) { + // Return the "dirty value flag" as defined in the HTML spec. Cast to text + // input to sidestep complicated value sanitization behaviors. + const copy = node.cloneNode(); + copy.type = 'text'; + // If modifying the attribute now doesn't change the value, the value was already detached. + copy.defaultValue += Math.random(); + return copy.value === node.value; + } + beforeEach(() => { jest.resetModules(); @@ -128,6 +138,7 @@ describe('ReactDOMInput', () => { }).toErrorDev( 'Warning: You provided a `value` prop to a form field without an `onChange` handler.', ); + expect(isValueDirty(node)).toBe(true); setUntrackedValue.call(node, 'giraffe'); @@ -136,6 +147,7 @@ describe('ReactDOMInput', () => { dispatchEventOnNode(node, 'input'); expect(node.value).toBe('lion'); + expect(isValueDirty(node)).toBe(true); }); it('should control a value in reentrant events', () => { @@ -438,15 +450,22 @@ describe('ReactDOMInput', () => { expect(node.value).toBe('0'); expect(node.defaultValue).toBe('0'); + if (disableInputAttributeSyncing) { + expect(isValueDirty(node)).toBe(false); + } else { + expect(isValueDirty(node)).toBe(true); + } ReactDOM.render(, container); if (disableInputAttributeSyncing) { expect(node.value).toBe('1'); expect(node.defaultValue).toBe('1'); + expect(isValueDirty(node)).toBe(false); } else { expect(node.value).toBe('0'); expect(node.defaultValue).toBe('1'); + expect(isValueDirty(node)).toBe(true); } }); @@ -478,12 +497,14 @@ describe('ReactDOMInput', () => { container, ); expect(node.value).toBe('0'); + expect(isValueDirty(node)).toBe(true); expect(() => ReactDOM.render(, container), ).toErrorDev( 'A component is changing a controlled input to be uncontrolled.', ); expect(node.value).toBe('0'); + expect(isValueDirty(node)).toBe(true); }); it('should render defaultValue for SSR', () => { @@ -794,13 +815,16 @@ describe('ReactDOMInput', () => { , container, ); + const node = container.firstChild; + expect(isValueDirty(node)).toBe(false); + ReactDOM.render( , container, ); - const node = container.firstChild; expect(node.value).toBe('0'); + expect(isValueDirty(node)).toBe(true); if (disableInputAttributeSyncing) { expect(node.hasAttribute('value')).toBe(false); @@ -814,15 +838,17 @@ describe('ReactDOMInput', () => { , container, ); + const node = container.firstChild; + expect(isValueDirty(node)).toBe(true); + ReactDOM.render( , container, ); - const node = container.firstChild; - expect(node.value).toBe(''); expect(node.defaultValue).toBe(''); + expect(isValueDirty(node)).toBe(true); }); it('should properly transition a text input from 0 to an empty 0.0', function () { @@ -911,10 +937,16 @@ describe('ReactDOMInput', () => { container, ); expect(inputRef.current.value).toBe('default1'); + if (disableInputAttributeSyncing) { + expect(isValueDirty(inputRef.current)).toBe(false); + } else { + expect(isValueDirty(inputRef.current)).toBe(true); + } setUntrackedValue.call(inputRef.current, 'changed'); dispatchEventOnNode(inputRef.current, 'input'); expect(inputRef.current.value).toBe('changed'); + expect(isValueDirty(inputRef.current)).toBe(true); ReactDOM.render(