@@ -26,6 +26,16 @@ describe('ReactDOMInput', () => {
26
26
node . dispatchEvent ( new Event ( type , { bubbles : true , cancelable : true } ) ) ;
27
27
}
28
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
+
29
39
beforeEach ( ( ) => {
30
40
jest . resetModules ( ) ;
31
41
@@ -128,6 +138,7 @@ describe('ReactDOMInput', () => {
128
138
} ) . toErrorDev (
129
139
'Warning: You provided a `value` prop to a form field without an `onChange` handler.' ,
130
140
) ;
141
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
131
142
132
143
setUntrackedValue . call ( node , 'giraffe' ) ;
133
144
@@ -136,6 +147,7 @@ describe('ReactDOMInput', () => {
136
147
dispatchEventOnNode ( node , 'input' ) ;
137
148
138
149
expect ( node . value ) . toBe ( 'lion' ) ;
150
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
139
151
} ) ;
140
152
141
153
it ( 'should control a value in reentrant events' , ( ) => {
@@ -438,15 +450,22 @@ describe('ReactDOMInput', () => {
438
450
439
451
expect ( node . value ) . toBe ( '0' ) ;
440
452
expect ( node . defaultValue ) . toBe ( '0' ) ;
453
+ if ( disableInputAttributeSyncing ) {
454
+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
455
+ } else {
456
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
457
+ }
441
458
442
459
ReactDOM . render ( < input type = "text" defaultValue = "1" /> , container ) ;
443
460
444
461
if ( disableInputAttributeSyncing ) {
445
462
expect ( node . value ) . toBe ( '1' ) ;
446
463
expect ( node . defaultValue ) . toBe ( '1' ) ;
464
+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
447
465
} else {
448
466
expect ( node . value ) . toBe ( '0' ) ;
449
467
expect ( node . defaultValue ) . toBe ( '1' ) ;
468
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
450
469
}
451
470
} ) ;
452
471
@@ -478,12 +497,14 @@ describe('ReactDOMInput', () => {
478
497
container ,
479
498
) ;
480
499
expect ( node . value ) . toBe ( '0' ) ;
500
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
481
501
expect ( ( ) =>
482
502
ReactDOM . render ( < input type = "text" defaultValue = "1" /> , container ) ,
483
503
) . toErrorDev (
484
504
'A component is changing a controlled input to be uncontrolled.' ,
485
505
) ;
486
506
expect ( node . value ) . toBe ( '0' ) ;
507
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
487
508
} ) ;
488
509
489
510
it ( 'should render defaultValue for SSR' , ( ) => {
@@ -794,13 +815,16 @@ describe('ReactDOMInput', () => {
794
815
< input type = "text" value = "" onChange = { emptyFunction } /> ,
795
816
container ,
796
817
) ;
818
+ const node = container . firstChild ;
819
+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
820
+
797
821
ReactDOM . render (
798
822
< input type = "text" value = { 0 } onChange = { emptyFunction } /> ,
799
823
container ,
800
824
) ;
801
825
802
- const node = container . firstChild ;
803
826
expect ( node . value ) . toBe ( '0' ) ;
827
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
804
828
805
829
if ( disableInputAttributeSyncing ) {
806
830
expect ( node . hasAttribute ( 'value' ) ) . toBe ( false ) ;
@@ -814,15 +838,17 @@ describe('ReactDOMInput', () => {
814
838
< input type = "text" value = { 0 } onChange = { emptyFunction } /> ,
815
839
container ,
816
840
) ;
841
+ const node = container . firstChild ;
842
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
843
+
817
844
ReactDOM . render (
818
845
< input type = "text" value = "" onChange = { emptyFunction } /> ,
819
846
container ,
820
847
) ;
821
848
822
- const node = container . firstChild ;
823
-
824
849
expect ( node . value ) . toBe ( '' ) ;
825
850
expect ( node . defaultValue ) . toBe ( '' ) ;
851
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
826
852
} ) ;
827
853
828
854
it ( 'should properly transition a text input from 0 to an empty 0.0' , function ( ) {
@@ -911,10 +937,16 @@ describe('ReactDOMInput', () => {
911
937
container ,
912
938
) ;
913
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
+ }
914
945
915
946
setUntrackedValue . call ( inputRef . current , 'changed' ) ;
916
947
dispatchEventOnNode ( inputRef . current , 'input' ) ;
917
948
expect ( inputRef . current . value ) . toBe ( 'changed' ) ;
949
+ expect ( isValueDirty ( inputRef . current ) ) . toBe ( true ) ;
918
950
919
951
ReactDOM . render (
920
952
< form >
@@ -924,12 +956,14 @@ describe('ReactDOMInput', () => {
924
956
container ,
925
957
) ;
926
958
expect ( inputRef . current . value ) . toBe ( 'changed' ) ;
959
+ expect ( isValueDirty ( inputRef . current ) ) . toBe ( true ) ;
927
960
928
961
container . firstChild . reset ( ) ;
929
962
// Note: I don't know if we want to always support this.
930
963
// But it's current behavior so worth being intentional if we break it.
931
964
// https://github.com/facebook/react/issues/4618
932
965
expect ( inputRef . current . value ) . toBe ( 'default2' ) ;
966
+ expect ( isValueDirty ( inputRef . current ) ) . toBe ( false ) ;
933
967
} ) ;
934
968
935
969
it ( 'should not set a value for submit buttons unnecessarily' , ( ) => {
@@ -1300,8 +1334,18 @@ describe('ReactDOMInput', () => {
1300
1334
1301
1335
it ( 'should update defaultValue to empty string' , ( ) => {
1302
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
+ }
1303
1342
ReactDOM . render ( < input type = "text" defaultValue = { '' } /> , container ) ;
1304
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
+ }
1305
1349
} ) ;
1306
1350
1307
1351
it ( 'should warn if value is null' , ( ) => {
@@ -1838,10 +1882,12 @@ describe('ReactDOMInput', () => {
1838
1882
const Input = getTestInput ( ) ;
1839
1883
const stub = ReactDOM . render ( < Input type = "text" /> , container ) ;
1840
1884
const node = ReactDOM . findDOMNode ( stub ) ;
1885
+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
1841
1886
1842
1887
setUntrackedValue . call ( node , '2' ) ;
1843
1888
dispatchEventOnNode ( node , 'input' ) ;
1844
1889
1890
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
1845
1891
if ( disableInputAttributeSyncing ) {
1846
1892
expect ( node . hasAttribute ( 'value' ) ) . toBe ( false ) ;
1847
1893
} else {
@@ -1856,12 +1902,14 @@ describe('ReactDOMInput', () => {
1856
1902
container ,
1857
1903
) ;
1858
1904
const node = ReactDOM . findDOMNode ( stub ) ;
1905
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
1859
1906
1860
1907
node . focus ( ) ;
1861
1908
1862
1909
setUntrackedValue . call ( node , '2' ) ;
1863
1910
dispatchEventOnNode ( node , 'input' ) ;
1864
1911
1912
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
1865
1913
if ( disableInputAttributeSyncing ) {
1866
1914
expect ( node . hasAttribute ( 'value' ) ) . toBe ( false ) ;
1867
1915
} else {
@@ -1876,12 +1924,14 @@ describe('ReactDOMInput', () => {
1876
1924
container ,
1877
1925
) ;
1878
1926
const node = ReactDOM . findDOMNode ( stub ) ;
1927
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
1879
1928
1880
1929
node . focus ( ) ;
1881
1930
setUntrackedValue . call ( node , '2' ) ;
1882
1931
dispatchEventOnNode ( node , 'input' ) ;
1883
1932
node . blur ( ) ;
1884
1933
1934
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
1885
1935
if ( disableInputAttributeSyncing ) {
1886
1936
expect ( node . value ) . toBe ( '2' ) ;
1887
1937
expect ( node . hasAttribute ( 'value' ) ) . toBe ( false ) ;
@@ -1896,12 +1946,18 @@ describe('ReactDOMInput', () => {
1896
1946
< input type = "number" defaultValue = "1" /> ,
1897
1947
container ,
1898
1948
) ;
1949
+ if ( disableInputAttributeSyncing ) {
1950
+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
1951
+ } else {
1952
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
1953
+ }
1899
1954
1900
1955
node . focus ( ) ;
1901
1956
setUntrackedValue . call ( node , 4 ) ;
1902
1957
dispatchEventOnNode ( node , 'input' ) ;
1903
1958
node . blur ( ) ;
1904
1959
1960
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
1905
1961
expect ( node . getAttribute ( 'value' ) ) . toBe ( '1' ) ;
1906
1962
} ) ;
1907
1963
@@ -1910,12 +1966,18 @@ describe('ReactDOMInput', () => {
1910
1966
< input type = "text" defaultValue = "1" /> ,
1911
1967
container ,
1912
1968
) ;
1969
+ if ( disableInputAttributeSyncing ) {
1970
+ expect ( isValueDirty ( node ) ) . toBe ( false ) ;
1971
+ } else {
1972
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
1973
+ }
1913
1974
1914
1975
node . focus ( ) ;
1915
1976
setUntrackedValue . call ( node , 4 ) ;
1916
1977
dispatchEventOnNode ( node , 'input' ) ;
1917
1978
node . blur ( ) ;
1918
1979
1980
+ expect ( isValueDirty ( node ) ) . toBe ( true ) ;
1919
1981
expect ( node . getAttribute ( 'value' ) ) . toBe ( '1' ) ;
1920
1982
} ) ;
1921
1983
} ) ;
0 commit comments