From 57c4abe9b68a2e71d162c5b6f6b160ae94fa40d5 Mon Sep 17 00:00:00 2001 From: Sophie Alpert Date: Tue, 18 Apr 2023 11:03:36 -0700 Subject: [PATCH] Add assertions about 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. --- .../src/__tests__/ReactDOMInput-test.js | 68 ++++++++++++++++++- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js index 39fabad88b501..0c736174e4545 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(
@@ -924,12 +956,14 @@ describe('ReactDOMInput', () => { container, ); expect(inputRef.current.value).toBe('changed'); + expect(isValueDirty(inputRef.current)).toBe(true); container.firstChild.reset(); // Note: I don't know if we want to always support this. // But it's current behavior so worth being intentional if we break it. // https://github.com/facebook/react/issues/4618 expect(inputRef.current.value).toBe('default2'); + expect(isValueDirty(inputRef.current)).toBe(false); }); it('should not set a value for submit buttons unnecessarily', () => { @@ -1300,8 +1334,18 @@ describe('ReactDOMInput', () => { it('should update defaultValue to empty string', () => { ReactDOM.render(, container); + if (disableInputAttributeSyncing) { + expect(isValueDirty(container.firstChild)).toBe(false); + } else { + expect(isValueDirty(container.firstChild)).toBe(true); + } ReactDOM.render(, container); expect(container.firstChild.defaultValue).toBe(''); + if (disableInputAttributeSyncing) { + expect(isValueDirty(container.firstChild)).toBe(false); + } else { + expect(isValueDirty(container.firstChild)).toBe(true); + } }); it('should warn if value is null', () => { @@ -1838,10 +1882,12 @@ describe('ReactDOMInput', () => { const Input = getTestInput(); const stub = ReactDOM.render(, container); const node = ReactDOM.findDOMNode(stub); + expect(isValueDirty(node)).toBe(false); setUntrackedValue.call(node, '2'); dispatchEventOnNode(node, 'input'); + expect(isValueDirty(node)).toBe(true); if (disableInputAttributeSyncing) { expect(node.hasAttribute('value')).toBe(false); } else { @@ -1856,12 +1902,14 @@ describe('ReactDOMInput', () => { container, ); const node = ReactDOM.findDOMNode(stub); + expect(isValueDirty(node)).toBe(true); node.focus(); setUntrackedValue.call(node, '2'); dispatchEventOnNode(node, 'input'); + expect(isValueDirty(node)).toBe(true); if (disableInputAttributeSyncing) { expect(node.hasAttribute('value')).toBe(false); } else { @@ -1876,12 +1924,14 @@ describe('ReactDOMInput', () => { container, ); const node = ReactDOM.findDOMNode(stub); + expect(isValueDirty(node)).toBe(true); node.focus(); setUntrackedValue.call(node, '2'); dispatchEventOnNode(node, 'input'); node.blur(); + expect(isValueDirty(node)).toBe(true); if (disableInputAttributeSyncing) { expect(node.value).toBe('2'); expect(node.hasAttribute('value')).toBe(false); @@ -1896,12 +1946,18 @@ describe('ReactDOMInput', () => { , container, ); + if (disableInputAttributeSyncing) { + expect(isValueDirty(node)).toBe(false); + } else { + expect(isValueDirty(node)).toBe(true); + } node.focus(); setUntrackedValue.call(node, 4); dispatchEventOnNode(node, 'input'); node.blur(); + expect(isValueDirty(node)).toBe(true); expect(node.getAttribute('value')).toBe('1'); }); @@ -1910,12 +1966,18 @@ describe('ReactDOMInput', () => { , container, ); + if (disableInputAttributeSyncing) { + expect(isValueDirty(node)).toBe(false); + } else { + expect(isValueDirty(node)).toBe(true); + } node.focus(); setUntrackedValue.call(node, 4); dispatchEventOnNode(node, 'input'); node.blur(); + expect(isValueDirty(node)).toBe(true); expect(node.getAttribute('value')).toBe('1'); }); });