@@ -26,6 +26,16 @@ describe('ReactDOMInput', () => {
2626 node . dispatchEvent ( new Event ( type , { bubbles : true , cancelable : true } ) ) ;
2727 }
2828
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+
2939 beforeEach ( ( ) => {
3040 jest . resetModules ( ) ;
3141
@@ -128,6 +138,7 @@ describe('ReactDOMInput', () => {
128138 } ) . toErrorDev (
129139 'Warning: You provided a `value` prop to a form field without an `onChange` handler.' ,
130140 ) ;
141+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
131142
132143 setUntrackedValue . call ( node , 'giraffe' ) ;
133144
@@ -136,6 +147,7 @@ describe('ReactDOMInput', () => {
136147 dispatchEventOnNode ( node , 'input' ) ;
137148
138149 expect ( node . value ) . toBe ( 'lion' ) ;
150+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
139151 } ) ;
140152
141153 it ( 'should control a value in reentrant events' , ( ) => {
@@ -438,15 +450,22 @@ describe('ReactDOMInput', () => {
438450
439451 expect ( node . value ) . toBe ( '0' ) ;
440452 expect ( node . defaultValue ) . toBe ( '0' ) ;
453+ if ( disableInputAttributeSyncing ) {
454+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
455+ } else {
456+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
457+ }
441458
442459 ReactDOM . render ( < input type = "text" defaultValue = "1" /> , container ) ;
443460
444461 if ( disableInputAttributeSyncing ) {
445462 expect ( node . value ) . toBe ( '1' ) ;
446463 expect ( node . defaultValue ) . toBe ( '1' ) ;
464+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
447465 } else {
448466 expect ( node . value ) . toBe ( '0' ) ;
449467 expect ( node . defaultValue ) . toBe ( '1' ) ;
468+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
450469 }
451470 } ) ;
452471
@@ -478,12 +497,14 @@ describe('ReactDOMInput', () => {
478497 container ,
479498 ) ;
480499 expect ( node . value ) . toBe ( '0' ) ;
500+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
481501 expect ( ( ) =>
482502 ReactDOM . render ( < input type = "text" defaultValue = "1" /> , container ) ,
483503 ) . toErrorDev (
484504 'A component is changing a controlled input to be uncontrolled.' ,
485505 ) ;
486506 expect ( node . value ) . toBe ( '0' ) ;
507+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
487508 } ) ;
488509
489510 it ( 'should render defaultValue for SSR' , ( ) => {
@@ -794,13 +815,16 @@ describe('ReactDOMInput', () => {
794815 < input type = "text" value = "" onChange = { emptyFunction } /> ,
795816 container ,
796817 ) ;
818+ const node = container . firstChild ;
819+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
820+
797821 ReactDOM . render (
798822 < input type = "text" value = { 0 } onChange = { emptyFunction } /> ,
799823 container ,
800824 ) ;
801825
802- const node = container . firstChild ;
803826 expect ( node . value ) . toBe ( '0' ) ;
827+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
804828
805829 if ( disableInputAttributeSyncing ) {
806830 expect ( node . hasAttribute ( 'value' ) ) . toBe ( false ) ;
@@ -814,15 +838,17 @@ describe('ReactDOMInput', () => {
814838 < input type = "text" value = { 0 } onChange = { emptyFunction } /> ,
815839 container ,
816840 ) ;
841+ const node = container . firstChild ;
842+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
843+
817844 ReactDOM . render (
818845 < input type = "text" value = "" onChange = { emptyFunction } /> ,
819846 container ,
820847 ) ;
821848
822- const node = container . firstChild ;
823-
824849 expect ( node . value ) . toBe ( '' ) ;
825850 expect ( node . defaultValue ) . toBe ( '' ) ;
851+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
826852 } ) ;
827853
828854 it ( 'should properly transition a text input from 0 to an empty 0.0' , function ( ) {
@@ -911,10 +937,16 @@ describe('ReactDOMInput', () => {
911937 container ,
912938 ) ;
913939 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+ }
914945
915946 setUntrackedValue . call ( inputRef . current , 'changed' ) ;
916947 dispatchEventOnNode ( inputRef . current , 'input' ) ;
917948 expect ( inputRef . current . value ) . toBe ( 'changed' ) ;
949+ expect ( isValueDirty ( inputRef . current ) ) . toBe ( true ) ;
918950
919951 ReactDOM . render (
920952 < form >
@@ -924,12 +956,14 @@ describe('ReactDOMInput', () => {
924956 container ,
925957 ) ;
926958 expect ( inputRef . current . value ) . toBe ( 'changed' ) ;
959+ expect ( isValueDirty ( inputRef . current ) ) . toBe ( true ) ;
927960
928961 container . firstChild . reset ( ) ;
929962 // Note: I don't know if we want to always support this.
930963 // But it's current behavior so worth being intentional if we break it.
931964 // https://github.com/facebook/react/issues/4618
932965 expect ( inputRef . current . value ) . toBe ( 'default2' ) ;
966+ expect ( isValueDirty ( inputRef . current ) ) . toBe ( false ) ;
933967 } ) ;
934968
935969 it ( 'should not set a value for submit buttons unnecessarily' , ( ) => {
@@ -1300,8 +1334,18 @@ describe('ReactDOMInput', () => {
13001334
13011335 it ( 'should update defaultValue to empty string' , ( ) => {
13021336 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+ }
13031342 ReactDOM . render ( < input type = "text" defaultValue = { '' } /> , container ) ;
13041343 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+ }
13051349 } ) ;
13061350
13071351 it ( 'should warn if value is null' , ( ) => {
@@ -1838,10 +1882,12 @@ describe('ReactDOMInput', () => {
18381882 const Input = getTestInput ( ) ;
18391883 const stub = ReactDOM . render ( < Input type = "text" /> , container ) ;
18401884 const node = ReactDOM . findDOMNode ( stub ) ;
1885+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
18411886
18421887 setUntrackedValue . call ( node , '2' ) ;
18431888 dispatchEventOnNode ( node , 'input' ) ;
18441889
1890+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
18451891 if ( disableInputAttributeSyncing ) {
18461892 expect ( node . hasAttribute ( 'value' ) ) . toBe ( false ) ;
18471893 } else {
@@ -1856,12 +1902,14 @@ describe('ReactDOMInput', () => {
18561902 container ,
18571903 ) ;
18581904 const node = ReactDOM . findDOMNode ( stub ) ;
1905+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
18591906
18601907 node . focus ( ) ;
18611908
18621909 setUntrackedValue . call ( node , '2' ) ;
18631910 dispatchEventOnNode ( node , 'input' ) ;
18641911
1912+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
18651913 if ( disableInputAttributeSyncing ) {
18661914 expect ( node . hasAttribute ( 'value' ) ) . toBe ( false ) ;
18671915 } else {
@@ -1876,12 +1924,14 @@ describe('ReactDOMInput', () => {
18761924 container ,
18771925 ) ;
18781926 const node = ReactDOM . findDOMNode ( stub ) ;
1927+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
18791928
18801929 node . focus ( ) ;
18811930 setUntrackedValue . call ( node , '2' ) ;
18821931 dispatchEventOnNode ( node , 'input' ) ;
18831932 node . blur ( ) ;
18841933
1934+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
18851935 if ( disableInputAttributeSyncing ) {
18861936 expect ( node . value ) . toBe ( '2' ) ;
18871937 expect ( node . hasAttribute ( 'value' ) ) . toBe ( false ) ;
@@ -1896,12 +1946,18 @@ describe('ReactDOMInput', () => {
18961946 < input type = "number" defaultValue = "1" /> ,
18971947 container ,
18981948 ) ;
1949+ if ( disableInputAttributeSyncing ) {
1950+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
1951+ } else {
1952+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
1953+ }
18991954
19001955 node . focus ( ) ;
19011956 setUntrackedValue . call ( node , 4 ) ;
19021957 dispatchEventOnNode ( node , 'input' ) ;
19031958 node . blur ( ) ;
19041959
1960+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
19051961 expect ( node . getAttribute ( 'value' ) ) . toBe ( '1' ) ;
19061962 } ) ;
19071963
@@ -1910,12 +1966,18 @@ describe('ReactDOMInput', () => {
19101966 < input type = "text" defaultValue = "1" /> ,
19111967 container ,
19121968 ) ;
1969+ if ( disableInputAttributeSyncing ) {
1970+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
1971+ } else {
1972+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
1973+ }
19131974
19141975 node . focus ( ) ;
19151976 setUntrackedValue . call ( node , 4 ) ;
19161977 dispatchEventOnNode ( node , 'input' ) ;
19171978 node . blur ( ) ;
19181979
1980+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
19191981 expect ( node . getAttribute ( 'value' ) ) . toBe ( '1' ) ;
19201982 } ) ;
19211983 } ) ;
0 commit comments