36
return copy.value === node.value;
37
}
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
+
76
beforeEach(() => {
77
jest.resetModules();
78
1156
name="fruit"
1157
checked={true}
1158
onChange={emptyFunction}
1159
+ data-which="a"
1160
/>
1161
A
1162
<input
1164
type="radio"
1165
name="fruit"
1166
onChange={emptyFunction}
1167
+ data-which="b"
1168
/>
1169
B
1170
<form>
1174
name="fruit"
1175
defaultChecked={true}
1176
onChange={emptyFunction}
1177
+ data-which="c"
1178
/>
1179
</form>
1180
</div>
1202
expect(cNode.hasAttribute('checked')).toBe(true);
1203
}
1204
1205
+ expect(isCheckedDirty(aNode)).toBe(true);
1206
+ expect(isCheckedDirty(bNode)).toBe(true);
1207
+ expect(isCheckedDirty(cNode)).toBe(true);
1208
+ assertInputTrackingIsCurrent(container);
1209
+
1210
setUntrackedChecked.call(bNode, true);
1211
expect(aNode.checked).toBe(false);
1212
expect(cNode.checked).toBe(true);
1228
// The original state should have been restored
1229
expect(aNode.checked).toBe(true);
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);
1236
});
1237
1238
it('should check the correct radio when the selected name moves', () => {
1269
const stub = ReactDOM.render(<App />, container);
1270
const buttonNode = ReactDOM.findDOMNode(stub).childNodes[0];
1271
const firstRadioNode = ReactDOM.findDOMNode(stub).childNodes[1];
1272
+ expect(isCheckedDirty(firstRadioNode)).toBe(true);
1273
expect(firstRadioNode.checked).toBe(false);
1274
+ assertInputTrackingIsCurrent(container);
1275
dispatchEventOnNode(buttonNode, 'click');
1276
expect(firstRadioNode.checked).toBe(true);
1277
+ assertInputTrackingIsCurrent(container);
1278
dispatchEventOnNode(buttonNode, 'click');
1279
expect(firstRadioNode.checked).toBe(false);
1280
+ assertInputTrackingIsCurrent(container);
1281
});
1282
1283
it("shouldn't get tricked by changing radio names, part 2", () => {
1300
</div>,
1301
container,
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);
1310
1311
ReactDOM.render(
1312
<div>
1327
</div>,
1328
container,
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);
1335
});
1336
1337
it('should control radio buttons if the tree updates during render', () => {
1393
1394
expect(aNode.checked).toBe(false);
1395
expect(bNode.checked).toBe(true);
1396
+ expect(isCheckedDirty(aNode)).toBe(true);
1397
+ expect(isCheckedDirty(bNode)).toBe(true);
1398
+ assertInputTrackingIsCurrent(container);
1399
1400
setUntrackedChecked.call(aNode, true);
1401
// This next line isn't necessary in a proper browser environment, but
1409
// The original state should have been restored
1410
expect(aNode.checked).toBe(false);
1411
expect(bNode.checked).toBe(true);
1412
+ expect(isCheckedDirty(aNode)).toBe(true);
1413
+ expect(isCheckedDirty(bNode)).toBe(true);
1414
+ assertInputTrackingIsCurrent(container);
1415
});
1416
1417
it('should warn with value and no onChange handler and readOnly specified', () => {
1794
<input type="radio" checked={false} onChange={() => null} />,
1795
container,
1796
);
1797
+ const input = container.querySelector('input');
1798
+ expect(isCheckedDirty(input)).toBe(true);
1799
ReactDOM.render(
1800
<input
1801
type="radio"
1806
/>,
1807
container,
1808
);
1809
+ expect(isCheckedDirty(input)).toBe(true);
1810
+ assertInputTrackingIsCurrent(container);
1811
});
1812
1813
it('should warn if radio checked false changes to become uncontrolled', () => {