@@ -36,6 +36,43 @@ describe('ReactDOMInput', () => {
36
36
return copy . value === node . value ;
37
37
}
38
38
39
+ function isCheckedDirty ( node ) {
40
+ // Return the "dirty checked flag" as defined in the HTML spec.
41
+ if ( node . checked !== node . defaultChecked ) {
42
+ return true ;
43
+ }
44
+ const copy = node . cloneNode ( ) ;
45
+ copy . type = 'checkbox' ;
46
+ copy . defaultChecked = ! copy . defaultChecked ;
47
+ return copy . checked === node . checked ;
48
+ }
49
+
50
+ function getTrackedAndCurrentInputValue ( elem : HTMLElement ) : [ mixed , mixed ] {
51
+ const tracker = elem . _valueTracker ;
52
+ if ( ! tracker ) {
53
+ throw new Error ( 'No input tracker' ) ;
54
+ }
55
+ return [
56
+ tracker . getValue ( ) ,
57
+ elem . nodeName === 'INPUT' &&
58
+ ( elem . type === 'checkbox' || elem . type === 'radio' )
59
+ ? String ( elem . checked )
60
+ : elem . value ,
61
+ ] ;
62
+ }
63
+
64
+ function assertInputTrackingIsCurrent ( parent ) {
65
+ parent . querySelectorAll ( 'input, textarea, select' ) . forEach ( input => {
66
+ const [ trackedValue , currentValue ] =
67
+ getTrackedAndCurrentInputValue ( input ) ;
68
+ if ( trackedValue !== currentValue ) {
69
+ throw new Error (
70
+ `Input ${ input . outerHTML } is currently ${ currentValue } but tracker thinks it's ${ trackedValue } ` ,
71
+ ) ;
72
+ }
73
+ } ) ;
74
+ }
75
+
39
76
beforeEach ( ( ) => {
40
77
jest . resetModules ( ) ;
41
78
@@ -1119,13 +1156,15 @@ describe('ReactDOMInput', () => {
1119
1156
name = "fruit"
1120
1157
checked = { true }
1121
1158
onChange = { emptyFunction }
1159
+ data-which = "a"
1122
1160
/>
1123
1161
A
1124
1162
< input
1125
1163
ref = { this . bRef }
1126
1164
type = "radio"
1127
1165
name = "fruit"
1128
1166
onChange = { emptyFunction }
1167
+ data-which = "b"
1129
1168
/>
1130
1169
B
1131
1170
< form >
@@ -1135,6 +1174,7 @@ describe('ReactDOMInput', () => {
1135
1174
name = "fruit"
1136
1175
defaultChecked = { true }
1137
1176
onChange = { emptyFunction }
1177
+ data-which = "c"
1138
1178
/>
1139
1179
</ form >
1140
1180
</ div >
@@ -1162,6 +1202,11 @@ describe('ReactDOMInput', () => {
1162
1202
expect ( cNode . hasAttribute ( 'checked' ) ) . toBe ( true ) ;
1163
1203
}
1164
1204
1205
+ expect ( isCheckedDirty ( aNode ) ) . toBe ( true ) ;
1206
+ expect ( isCheckedDirty ( bNode ) ) . toBe ( true ) ;
1207
+ expect ( isCheckedDirty ( cNode ) ) . toBe ( true ) ;
1208
+ assertInputTrackingIsCurrent ( container ) ;
1209
+
1165
1210
setUntrackedChecked . call ( bNode , true ) ;
1166
1211
expect ( aNode . checked ) . toBe ( false ) ;
1167
1212
expect ( cNode . checked ) . toBe ( true ) ;
@@ -1183,6 +1228,11 @@ describe('ReactDOMInput', () => {
1183
1228
// The original state should have been restored
1184
1229
expect ( aNode . checked ) . toBe ( true ) ;
1185
1230
expect ( cNode . checked ) . toBe ( true ) ;
1231
+
1232
+ expect ( isCheckedDirty ( aNode ) ) . toBe ( true ) ;
1233
+ expect ( isCheckedDirty ( bNode ) ) . toBe ( true ) ;
1234
+ expect ( isCheckedDirty ( cNode ) ) . toBe ( true ) ;
1235
+ assertInputTrackingIsCurrent ( container ) ;
1186
1236
} ) ;
1187
1237
1188
1238
it ( 'should check the correct radio when the selected name moves' , ( ) => {
@@ -1219,11 +1269,15 @@ describe('ReactDOMInput', () => {
1219
1269
const stub = ReactDOM . render ( < App /> , container ) ;
1220
1270
const buttonNode = ReactDOM . findDOMNode ( stub ) . childNodes [ 0 ] ;
1221
1271
const firstRadioNode = ReactDOM . findDOMNode ( stub ) . childNodes [ 1 ] ;
1272
+ expect ( isCheckedDirty ( firstRadioNode ) ) . toBe ( true ) ;
1222
1273
expect ( firstRadioNode . checked ) . toBe ( false ) ;
1274
+ assertInputTrackingIsCurrent ( container ) ;
1223
1275
dispatchEventOnNode ( buttonNode , 'click' ) ;
1224
1276
expect ( firstRadioNode . checked ) . toBe ( true ) ;
1277
+ assertInputTrackingIsCurrent ( container ) ;
1225
1278
dispatchEventOnNode ( buttonNode , 'click' ) ;
1226
1279
expect ( firstRadioNode . checked ) . toBe ( false ) ;
1280
+ assertInputTrackingIsCurrent ( container ) ;
1227
1281
} ) ;
1228
1282
1229
1283
it ( "shouldn't get tricked by changing radio names, part 2" , ( ) => {
@@ -1246,12 +1300,13 @@ describe('ReactDOMInput', () => {
1246
1300
</ div > ,
1247
1301
container ,
1248
1302
) ;
1249
- expect ( container . querySelector ( 'input[name="a"][value="1"]' ) . checked ) . toBe (
1250
- true ,
1251
- ) ;
1252
- expect ( container . querySelector ( 'input[name="a"][value="2"]' ) . checked ) . toBe (
1253
- false ,
1254
- ) ;
1303
+ const one = container . querySelector ( 'input[name="a"][value="1"]' ) ;
1304
+ const two = container . querySelector ( 'input[name="a"][value="2"]' ) ;
1305
+ expect ( one . checked ) . toBe ( true ) ;
1306
+ expect ( two . checked ) . toBe ( false ) ;
1307
+ expect ( isCheckedDirty ( one ) ) . toBe ( true ) ;
1308
+ expect ( isCheckedDirty ( two ) ) . toBe ( true ) ;
1309
+ assertInputTrackingIsCurrent ( container ) ;
1255
1310
1256
1311
ReactDOM . render (
1257
1312
< div >
@@ -1272,12 +1327,11 @@ describe('ReactDOMInput', () => {
1272
1327
</ div > ,
1273
1328
container ,
1274
1329
) ;
1275
- expect ( container . querySelector ( 'input[name="a"][value="1"]' ) . checked ) . toBe (
1276
- true ,
1277
- ) ;
1278
- expect ( container . querySelector ( 'input[name="b"][value="2"]' ) . checked ) . toBe (
1279
- true ,
1280
- ) ;
1330
+ expect ( one . checked ) . toBe ( true ) ;
1331
+ expect ( two . checked ) . toBe ( true ) ;
1332
+ expect ( isCheckedDirty ( one ) ) . toBe ( true ) ;
1333
+ expect ( isCheckedDirty ( two ) ) . toBe ( true ) ;
1334
+ assertInputTrackingIsCurrent ( container ) ;
1281
1335
} ) ;
1282
1336
1283
1337
it ( 'should control radio buttons if the tree updates during render' , ( ) => {
@@ -1339,6 +1393,9 @@ describe('ReactDOMInput', () => {
1339
1393
1340
1394
expect ( aNode . checked ) . toBe ( false ) ;
1341
1395
expect ( bNode . checked ) . toBe ( true ) ;
1396
+ expect ( isCheckedDirty ( aNode ) ) . toBe ( true ) ;
1397
+ expect ( isCheckedDirty ( bNode ) ) . toBe ( true ) ;
1398
+ assertInputTrackingIsCurrent ( container ) ;
1342
1399
1343
1400
setUntrackedChecked . call ( aNode , true ) ;
1344
1401
// This next line isn't necessary in a proper browser environment, but
@@ -1352,6 +1409,9 @@ describe('ReactDOMInput', () => {
1352
1409
// The original state should have been restored
1353
1410
expect ( aNode . checked ) . toBe ( false ) ;
1354
1411
expect ( bNode . checked ) . toBe ( true ) ;
1412
+ expect ( isCheckedDirty ( aNode ) ) . toBe ( true ) ;
1413
+ expect ( isCheckedDirty ( bNode ) ) . toBe ( true ) ;
1414
+ assertInputTrackingIsCurrent ( container ) ;
1355
1415
} ) ;
1356
1416
1357
1417
it ( 'should warn with value and no onChange handler and readOnly specified' , ( ) => {
@@ -1734,6 +1794,8 @@ describe('ReactDOMInput', () => {
1734
1794
< input type = "radio" checked = { false } onChange = { ( ) => null } /> ,
1735
1795
container ,
1736
1796
) ;
1797
+ const input = container . querySelector ( 'input' ) ;
1798
+ expect ( isCheckedDirty ( input ) ) . toBe ( true ) ;
1737
1799
ReactDOM . render (
1738
1800
< input
1739
1801
type = "radio"
@@ -1744,6 +1806,8 @@ describe('ReactDOMInput', () => {
1744
1806
/> ,
1745
1807
container ,
1746
1808
) ;
1809
+ expect ( isCheckedDirty ( input ) ) . toBe ( true ) ;
1810
+ assertInputTrackingIsCurrent ( container ) ;
1747
1811
} ) ;
1748
1812
1749
1813
it ( 'should warn if radio checked false changes to become uncontrolled' , ( ) => {
0 commit comments