main
js 3,109 lines 99.5 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 // Set by `yarn test-fire`.
13 const {disableInputAttributeSyncing} = require('shared/ReactFeatureFlags');
14
15 function emptyFunction() {}
16
17 describe('ReactDOMInput', () => {
18 let React;
19 let ReactDOM;
20 let ReactDOMClient;
21 let ReactDOMServer;
22 let Scheduler;
23 let act;
24 let assertLog;
25 let setUntrackedValue;
26 let setUntrackedChecked;
27 let container;
28 let root;
29 let assertConsoleErrorDev;
30
31 function dispatchEventOnNode(node, type) {
32 node.dispatchEvent(new Event(type, {bubbles: true, cancelable: true}));
33 }
34
35 function isValueDirty(node) {
36 // Return the "dirty value flag" as defined in the HTML spec. Cast to text
37 // input to sidestep complicated value sanitization behaviors.
38 const copy = node.cloneNode();
39 copy.type = 'text';
40 // If modifying the attribute now doesn't change the value, the value was already detached.
41 copy.defaultValue += Math.random();
42 return copy.value === node.value;
43 }
44
45 function isCheckedDirty(node) {
46 // Return the "dirty checked flag" as defined in the HTML spec.
47 if (node.checked !== node.defaultChecked) {
48 return true;
49 }
50 const copy = node.cloneNode();
51 copy.type = 'checkbox';
52 copy.defaultChecked = !copy.defaultChecked;
53 return copy.checked === node.checked;
54 }
55
56 function getTrackedAndCurrentInputValue(elem: HTMLElement): [mixed, mixed] {
57 const tracker = elem._valueTracker;
58 if (!tracker) {
59 throw new Error('No input tracker');
60 }
61 return [
62 tracker.getValue(),
63 elem.nodeName === 'INPUT' &&
64 (elem.type === 'checkbox' || elem.type === 'radio')
65 ? String(elem.checked)
66 : elem.value,
67 ];
68 }
69
70 function assertInputTrackingIsCurrent(parent) {
71 parent.querySelectorAll('input, textarea, select').forEach(input => {
72 const [trackedValue, currentValue] =
73 getTrackedAndCurrentInputValue(input);
74 if (trackedValue !== currentValue) {
75 throw new Error(
76 `Input ${input.outerHTML} is currently ${currentValue} but tracker thinks it's ${trackedValue}`,
77 );
78 }
79 });
80 }
81
82 beforeEach(() => {
83 jest.resetModules();
84
85 setUntrackedValue = Object.getOwnPropertyDescriptor(
86 HTMLInputElement.prototype,
87 'value',
88 ).set;
89 setUntrackedChecked = Object.getOwnPropertyDescriptor(
90 HTMLInputElement.prototype,
91 'checked',
92 ).set;
93
94 React = require('react');
95 ReactDOM = require('react-dom');
96 ReactDOMClient = require('react-dom/client');
97 ReactDOMServer = require('react-dom/server');
98 Scheduler = require('scheduler');
99 act = require('internal-test-utils').act;
100 assertConsoleErrorDev =
101 require('internal-test-utils').assertConsoleErrorDev;
102 assertLog = require('internal-test-utils').assertLog;
103
104 container = document.createElement('div');
105 document.body.appendChild(container);
106 root = ReactDOMClient.createRoot(container);
107 });
108
109 afterEach(() => {
110 document.body.removeChild(container);
111 jest.restoreAllMocks();
112 });
113
114 it('should warn for controlled value of 0 with missing onChange', async () => {
115 await act(() => {
116 root.render(<input type="text" value={0} />);
117 });
118 assertConsoleErrorDev([
119 'You provided a `value` prop to a form ' +
120 'field without an `onChange` handler. This will render a read-only ' +
121 'field. If the field should be mutable use `defaultValue`. ' +
122 'Otherwise, set either `onChange` or `readOnly`.\n' +
123 ' in input (at **)',
124 ]);
125 });
126
127 it('should warn for controlled value of "" with missing onChange', async () => {
128 await act(() => {
129 root.render(<input type="text" value="" />);
130 });
131 assertConsoleErrorDev([
132 'You provided a `value` prop to a form ' +
133 'field without an `onChange` handler. This will render a read-only ' +
134 'field. If the field should be mutable use `defaultValue`. ' +
135 'Otherwise, set either `onChange` or `readOnly`.\n' +
136 ' in input (at **)',
137 ]);
138 });
139
140 it('should warn for controlled value of "0" with missing onChange', async () => {
141 await act(() => {
142 root.render(<input type="text" value="0" />);
143 });
144 assertConsoleErrorDev([
145 'You provided a `value` prop to a form ' +
146 'field without an `onChange` handler. This will render a read-only ' +
147 'field. If the field should be mutable use `defaultValue`. ' +
148 'Otherwise, set either `onChange` or `readOnly`.\n' +
149 ' in input (at **)',
150 ]);
151 });
152
153 it('should warn for controlled value of false with missing onChange', async () => {
154 await act(() => {
155 root.render(<input type="checkbox" checked={false} />);
156 });
157 assertConsoleErrorDev([
158 'You provided a `checked` prop to a form field without an `onChange` handler. ' +
159 'This will render a read-only field. If the field should be mutable use `defaultChecked`. ' +
160 'Otherwise, set either `onChange` or `readOnly`.\n' +
161 ' in input (at **)',
162 ]);
163 });
164
165 it('should warn with checked and no onChange handler with readOnly specified', async () => {
166 await act(() => {
167 root.render(<input type="checkbox" checked={false} readOnly={true} />);
168 });
169 root.unmount();
170 root = ReactDOMClient.createRoot(container);
171
172 await act(() => {
173 root.render(<input type="checkbox" checked={false} readOnly={false} />);
174 });
175 assertConsoleErrorDev([
176 'You provided a `checked` prop to a form field without an `onChange` handler. ' +
177 'This will render a read-only field. If the field should be mutable use `defaultChecked`. ' +
178 'Otherwise, set either `onChange` or `readOnly`.\n' +
179 ' in input (at **)',
180 ]);
181 });
182
183 it('should not warn about missing onChange in uncontrolled inputs', async () => {
184 await act(() => {
185 root.render(<input />);
186 });
187 root.unmount();
188 root = ReactDOMClient.createRoot(container);
189 await act(() => {
190 root.render(<input value={undefined} />);
191 });
192 root.unmount();
193 root = ReactDOMClient.createRoot(container);
194 await act(() => {
195 root.render(<input type="text" />);
196 });
197 root.unmount();
198 root = ReactDOMClient.createRoot(container);
199 await act(() => {
200 root.render(<input type="text" value={undefined} />);
201 });
202 root.unmount();
203 root = ReactDOMClient.createRoot(container);
204 await act(() => {
205 root.render(<input type="checkbox" />);
206 });
207 root.unmount();
208 root = ReactDOMClient.createRoot(container);
209 await act(() => {
210 root.render(<input type="checkbox" checked={undefined} />);
211 });
212 });
213
214 it('should not warn with value and onInput handler', async () => {
215 await act(() => {
216 root.render(<input value="..." onInput={() => {}} />);
217 });
218 });
219
220 it('should properly control a value even if no event listener exists', async () => {
221 await act(() => {
222 root.render(<input type="text" value="lion" />);
223 });
224 assertConsoleErrorDev([
225 'You provided a `value` prop to a form field without an `onChange` handler. ' +
226 'This will render a read-only field. If the field should be mutable use `defaultValue`. ' +
227 'Otherwise, set either `onChange` or `readOnly`.\n' +
228 ' in input (at **)',
229 ]);
230 const node = container.firstChild;
231 expect(isValueDirty(node)).toBe(true);
232
233 setUntrackedValue.call(node, 'giraffe');
234
235 // This must use the native event dispatching. If we simulate, we will
236 // bypass the lazy event attachment system so we won't actually test this.
237 await act(() => {
238 dispatchEventOnNode(node, 'input');
239 });
240
241 expect(node.value).toBe('lion');
242 expect(isValueDirty(node)).toBe(true);
243 });
244
245 it('should control a value in reentrant events', async () => {
246 class ControlledInputs extends React.Component {
247 state = {value: 'lion'};
248 a = null;
249 b = null;
250 switchedFocus = false;
251 change(newValue) {
252 this.setState({value: newValue});
253 // Calling focus here will blur the text box which causes a native
254 // change event. Ideally we shouldn't have to fire this ourselves.
255 // Don't remove unless you've verified the fix in #8240 is still covered.
256 dispatchEventOnNode(this.a, 'input');
257 this.b.focus();
258 }
259 blur(currentValue) {
260 this.switchedFocus = true;
261 // currentValue should be 'giraffe' here because we should not have
262 // restored it on the target yet.
263 this.setState({value: currentValue});
264 }
265 render() {
266 return (
267 <div>
268 <input
269 type="text"
270 ref={n => (this.a = n)}
271 value={this.state.value}
272 onChange={e => this.change(e.target.value)}
273 onBlur={e => this.blur(e.target.value)}
274 />
275 <input type="text" ref={n => (this.b = n)} />
276 </div>
277 );
278 }
279 }
280
281 const ref = React.createRef();
282 await act(() => {
283 root.render(<ControlledInputs ref={ref} />);
284 });
285 const instance = ref.current;
286
287 // Focus the field so we can later blur it.
288 // Don't remove unless you've verified the fix in #8240 is still covered.
289 await act(() => {
290 instance.a.focus();
291 });
292 setUntrackedValue.call(instance.a, 'giraffe');
293 // This must use the native event dispatching. If we simulate, we will
294 // bypass the lazy event attachment system so we won't actually test this.
295 await act(() => {
296 dispatchEventOnNode(instance.a, 'input');
297 });
298 await act(() => {
299 dispatchEventOnNode(instance.a, 'blur');
300 });
301 await act(() => {
302 dispatchEventOnNode(instance.a, 'focusout');
303 });
304
305 expect(instance.a.value).toBe('giraffe');
306 expect(instance.switchedFocus).toBe(true);
307 });
308
309 it('should control values in reentrant events with different targets', async () => {
310 class ControlledInputs extends React.Component {
311 state = {value: 'lion'};
312 a = null;
313 b = null;
314 change(newValue) {
315 // This click will change the checkbox's value to false. Then it will
316 // invoke an inner change event. When we finally, flush, we need to
317 // reset the checkbox's value to true since that is its controlled
318 // value.
319 this.b.click();
320 }
321 render() {
322 return (
323 <div>
324 <input
325 type="text"
326 ref={n => (this.a = n)}
327 value="lion"
328 onChange={e => this.change(e.target.value)}
329 />
330 <input
331 type="checkbox"
332 ref={n => (this.b = n)}
333 checked={true}
334 onChange={() => {}}
335 />
336 </div>
337 );
338 }
339 }
340
341 const ref = React.createRef();
342 await act(() => {
343 root.render(<ControlledInputs ref={ref} />);
344 });
345 const instance = ref.current;
346
347 setUntrackedValue.call(instance.a, 'giraffe');
348 // This must use the native event dispatching. If we simulate, we will
349 // bypass the lazy event attachment system so we won't actually test this.
350 await act(() => {
351 dispatchEventOnNode(instance.a, 'input');
352 });
353
354 expect(instance.a.value).toBe('lion');
355 expect(instance.b.checked).toBe(true);
356 });
357
358 describe('switching text inputs between numeric and string numbers', () => {
359 it('does change the number 2 to "2.0" with no change handler', async () => {
360 await act(() => {
361 root.render(<input type="text" value={2} onChange={jest.fn()} />);
362 });
363 const node = container.firstChild;
364
365 setUntrackedValue.call(node, '2.0');
366 dispatchEventOnNode(node, 'input');
367
368 expect(node.value).toBe('2');
369 if (disableInputAttributeSyncing) {
370 expect(node.hasAttribute('value')).toBe(false);
371 } else {
372 expect(node.getAttribute('value')).toBe('2');
373 }
374 });
375
376 it('does change the string "2" to "2.0" with no change handler', async () => {
377 await act(() => {
378 root.render(<input type="text" value={'2'} onChange={jest.fn()} />);
379 });
380 const node = container.firstChild;
381
382 setUntrackedValue.call(node, '2.0');
383 dispatchEventOnNode(node, 'input');
384
385 expect(node.value).toBe('2');
386 if (disableInputAttributeSyncing) {
387 expect(node.hasAttribute('value')).toBe(false);
388 } else {
389 expect(node.getAttribute('value')).toBe('2');
390 }
391 });
392
393 it('changes the number 2 to "2.0" using a change handler', async () => {
394 class Stub extends React.Component {
395 state = {
396 value: 2,
397 };
398 onChange = event => {
399 this.setState({value: event.target.value});
400 };
401 render() {
402 const {value} = this.state;
403
404 return <input type="text" value={value} onChange={this.onChange} />;
405 }
406 }
407
408 await act(() => {
409 root.render(<Stub />);
410 });
411 const node = container.firstChild;
412
413 setUntrackedValue.call(node, '2.0');
414 dispatchEventOnNode(node, 'input');
415
416 expect(node.value).toBe('2.0');
417 if (disableInputAttributeSyncing) {
418 expect(node.hasAttribute('value')).toBe(false);
419 } else {
420 expect(node.getAttribute('value')).toBe('2.0');
421 }
422 });
423 });
424
425 it('does change the string ".98" to "0.98" with no change handler', async () => {
426 class Stub extends React.Component {
427 state = {
428 value: '.98',
429 };
430 render() {
431 return <input type="number" value={this.state.value} />;
432 }
433 }
434
435 const ref = React.createRef();
436 await act(() => {
437 root.render(<Stub ref={ref} />);
438 });
439 assertConsoleErrorDev([
440 'You provided a `value` prop to a form field without an `onChange` handler. ' +
441 'This will render a read-only field. If the field should be mutable use `defaultValue`. ' +
442 'Otherwise, set either `onChange` or `readOnly`.\n' +
443 ' in input (at **)\n' +
444 ' in Stub (at **)',
445 ]);
446 const node = container.firstChild;
447 await act(() => {
448 ref.current.setState({value: '0.98'});
449 });
450
451 expect(node.value).toEqual('0.98');
452 });
453
454 it('performs a state change from "" to 0', async () => {
455 class Stub extends React.Component {
456 state = {
457 value: '',
458 };
459 render() {
460 return <input type="number" value={this.state.value} readOnly={true} />;
461 }
462 }
463
464 const ref = React.createRef();
465 await act(() => {
466 root.render(<Stub ref={ref} />);
467 });
468 const node = container.firstChild;
469 await act(() => {
470 ref.current.setState({value: 0});
471 });
472
473 expect(node.value).toEqual('0');
474 });
475
476 it('updates the value on radio buttons from "" to 0', async () => {
477 await act(() => {
478 root.render(<input type="radio" value="" onChange={function () {}} />);
479 });
480 await act(() => {
481 root.render(<input type="radio" value={0} onChange={function () {}} />);
482 });
483 expect(container.firstChild.value).toBe('0');
484 expect(container.firstChild.getAttribute('value')).toBe('0');
485 });
486
487 it('updates the value on checkboxes from "" to 0', async () => {
488 await act(() => {
489 root.render(<input type="checkbox" value="" onChange={function () {}} />);
490 });
491 await act(() => {
492 root.render(
493 <input type="checkbox" value={0} onChange={function () {}} />,
494 );
495 });
496 expect(container.firstChild.value).toBe('0');
497 expect(container.firstChild.getAttribute('value')).toBe('0');
498 });
499
500 it('distinguishes precision for extra zeroes in string number values', async () => {
501 class Stub extends React.Component {
502 state = {
503 value: '3.0000',
504 };
505 render() {
506 return <input type="number" value={this.state.value} />;
507 }
508 }
509
510 const ref = React.createRef();
511 await act(() => {
512 root.render(<Stub ref={ref} />);
513 });
514 assertConsoleErrorDev([
515 'You provided a `value` prop to a form field without an `onChange` handler. ' +
516 'This will render a read-only field. If the field should be mutable use `defaultValue`. ' +
517 'Otherwise, set either `onChange` or `readOnly`.\n' +
518 ' in input (at **)\n' +
519 ' in Stub (at **)',
520 ]);
521 const node = container.firstChild;
522 await act(() => {
523 ref.current.setState({value: '3'});
524 });
525
526 expect(node.value).toEqual('3');
527 });
528
529 it('should display `defaultValue` of number 0', async () => {
530 await act(() => {
531 root.render(<input type="text" defaultValue={0} />);
532 });
533 const node = container.firstChild;
534
535 expect(node.getAttribute('value')).toBe('0');
536 expect(node.value).toBe('0');
537 });
538
539 it('only assigns defaultValue if it changes', async () => {
540 class Test extends React.Component {
541 render() {
542 return <input defaultValue="0" />;
543 }
544 }
545
546 const ref = React.createRef();
547 await act(() => {
548 root.render(<Test ref={ref} />);
549 });
550 const node = container.firstChild;
551
552 Object.defineProperty(node, 'defaultValue', {
553 get() {
554 return '0';
555 },
556 set(value) {
557 throw new Error(
558 `defaultValue was assigned ${value}, but it did not change!`,
559 );
560 },
561 });
562
563 await act(() => {
564 ref.current.forceUpdate();
565 });
566 });
567
568 it('should display "true" for `defaultValue` of `true`', async () => {
569 const stub = <input type="text" defaultValue={true} />;
570 await act(() => {
571 root.render(stub);
572 });
573 const node = container.firstChild;
574
575 expect(node.value).toBe('true');
576 });
577
578 it('should display "false" for `defaultValue` of `false`', async () => {
579 const stub = <input type="text" defaultValue={false} />;
580 await act(() => {
581 root.render(stub);
582 });
583 const node = container.firstChild;
584
585 expect(node.value).toBe('false');
586 });
587
588 it('should update `defaultValue` for uncontrolled input', async () => {
589 await act(() => {
590 root.render(<input type="text" defaultValue="0" />);
591 });
592 const node = container.firstChild;
593
594 expect(node.value).toBe('0');
595 expect(node.defaultValue).toBe('0');
596 if (disableInputAttributeSyncing) {
597 expect(isValueDirty(node)).toBe(false);
598 } else {
599 expect(isValueDirty(node)).toBe(true);
600 }
601
602 await act(() => {
603 root.render(<input type="text" defaultValue="1" />);
604 });
605
606 if (disableInputAttributeSyncing) {
607 expect(node.value).toBe('1');
608 expect(node.defaultValue).toBe('1');
609 expect(isValueDirty(node)).toBe(false);
610 } else {
611 expect(node.value).toBe('0');
612 expect(node.defaultValue).toBe('1');
613 expect(isValueDirty(node)).toBe(true);
614 }
615 });
616
617 it('should update `defaultValue` for uncontrolled date/time input', async () => {
618 await act(() => {
619 root.render(<input type="date" defaultValue="1980-01-01" />);
620 });
621 const node = container.firstChild;
622
623 expect(node.value).toBe('1980-01-01');
624 expect(node.defaultValue).toBe('1980-01-01');
625
626 await act(() => {
627 root.render(<input type="date" defaultValue="2000-01-01" />);
628 });
629
630 if (disableInputAttributeSyncing) {
631 expect(node.value).toBe('2000-01-01');
632 expect(node.defaultValue).toBe('2000-01-01');
633 } else {
634 expect(node.value).toBe('1980-01-01');
635 expect(node.defaultValue).toBe('2000-01-01');
636 }
637
638 await act(() => {
639 root.render(<input type="date" />);
640 });
641 });
642
643 it('should take `defaultValue` when changing to uncontrolled input', async () => {
644 await act(() => {
645 root.render(<input type="text" value="0" readOnly={true} />);
646 });
647 const node = container.firstChild;
648 expect(node.value).toBe('0');
649 expect(isValueDirty(node)).toBe(true);
650 await act(() => {
651 root.render(<input type="text" defaultValue="1" />);
652 });
653 assertConsoleErrorDev([
654 'A component is changing a controlled input to be uncontrolled. ' +
655 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
656 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
657 'More info: https://react.dev/link/controlled-components\n' +
658 ' in input (at **)',
659 ]);
660 expect(node.value).toBe('0');
661 expect(isValueDirty(node)).toBe(true);
662 });
663
664 it('should render defaultValue for SSR', () => {
665 const markup = ReactDOMServer.renderToString(
666 <input type="text" defaultValue="1" />,
667 );
668 const div = document.createElement('div');
669 div.innerHTML = markup;
670 expect(div.firstChild.getAttribute('value')).toBe('1');
671 expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
672 });
673
674 it('should render bigint defaultValue for SSR', () => {
675 const markup = ReactDOMServer.renderToString(
676 <input type="text" defaultValue={5n} />,
677 );
678 const div = document.createElement('div');
679 div.innerHTML = markup;
680 expect(div.firstChild.getAttribute('value')).toBe('5');
681 expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
682 });
683
684 it('should render value for SSR', () => {
685 const element = <input type="text" value="1" onChange={() => {}} />;
686 const markup = ReactDOMServer.renderToString(element);
687 const div = document.createElement('div');
688 div.innerHTML = markup;
689 expect(div.firstChild.getAttribute('value')).toBe('1');
690 expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
691 });
692
693 it('should render bigint value for SSR', () => {
694 const element = <input type="text" value={5n} onChange={() => {}} />;
695 const markup = ReactDOMServer.renderToString(element);
696 const div = document.createElement('div');
697 div.innerHTML = markup;
698 expect(div.firstChild.getAttribute('value')).toBe('5');
699 expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
700 });
701
702 it('should render name attribute if it is supplied', async () => {
703 await act(() => {
704 root.render(<input type="text" name="name" />);
705 });
706 const node = container.firstChild;
707 expect(node.name).toBe('name');
708 expect(container.firstChild.getAttribute('name')).toBe('name');
709 });
710
711 it('should render name attribute if it is supplied for SSR', () => {
712 const element = <input type="text" name="name" />;
713 const markup = ReactDOMServer.renderToString(element);
714 const div = document.createElement('div');
715 div.innerHTML = markup;
716 expect(div.firstChild.getAttribute('name')).toBe('name');
717 });
718
719 it('should not render name attribute if it is not supplied', async () => {
720 await act(() => {
721 root.render(<input type="text" />);
722 });
723 expect(container.firstChild.getAttribute('name')).toBe(null);
724 });
725
726 it('should not render name attribute if it is not supplied for SSR', () => {
727 const element = <input type="text" />;
728 const markup = ReactDOMServer.renderToString(element);
729 const div = document.createElement('div');
730 div.innerHTML = markup;
731 expect(div.firstChild.getAttribute('name')).toBe(null);
732 });
733
734 it('should display "foobar" for `defaultValue` of `objToString`', async () => {
735 const objToString = {
736 toString: function () {
737 return 'foobar';
738 },
739 };
740
741 const stub = <input type="text" defaultValue={objToString} />;
742 await act(() => {
743 root.render(stub);
744 });
745 const node = container.firstChild;
746
747 expect(node.value).toBe('foobar');
748 });
749
750 it('should throw for date inputs if `defaultValue` is an object where valueOf() throws', async () => {
751 class TemporalLike {
752 valueOf() {
753 // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
754 // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
755 throw new TypeError('prod message');
756 }
757 toString() {
758 return '2020-01-01';
759 }
760 }
761 await expect(async () => {
762 await act(() => {
763 root.render(<input defaultValue={new TemporalLike()} type="date" />);
764 });
765 }).rejects.toThrow(new TypeError('prod message'));
766 assertConsoleErrorDev([
767 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
768 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
769 ' in input (at **)',
770 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
771 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
772 ' in input (at **)',
773 ]);
774 });
775
776 it('should throw for text inputs if `defaultValue` is an object where valueOf() throws', async () => {
777 class TemporalLike {
778 valueOf() {
779 // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
780 // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
781 throw new TypeError('prod message');
782 }
783 toString() {
784 return '2020-01-01';
785 }
786 }
787 await expect(async () => {
788 await act(() => {
789 root.render(<input defaultValue={new TemporalLike()} type="text" />);
790 });
791 }).rejects.toThrow(new TypeError('prod message'));
792 assertConsoleErrorDev([
793 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
794 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
795 ' in input (at **)',
796 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
797 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
798 ' in input (at **)',
799 ]);
800 });
801
802 it('should throw for date inputs if `value` is an object where valueOf() throws', async () => {
803 class TemporalLike {
804 valueOf() {
805 // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
806 // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
807 throw new TypeError('prod message');
808 }
809 toString() {
810 return '2020-01-01';
811 }
812 }
813 await expect(async () => {
814 await act(() => {
815 root.render(
816 <input value={new TemporalLike()} type="date" onChange={() => {}} />,
817 );
818 });
819 }).rejects.toThrow(new TypeError('prod message'));
820 assertConsoleErrorDev([
821 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
822 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
823 ' in input (at **)',
824 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
825 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
826 ' in input (at **)',
827 ]);
828 });
829
830 it('should throw for text inputs if `value` is an object where valueOf() throws', async () => {
831 class TemporalLike {
832 valueOf() {
833 // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
834 // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
835 throw new TypeError('prod message');
836 }
837 toString() {
838 return '2020-01-01';
839 }
840 }
841 await expect(async () => {
842 await act(() => {
843 root.render(
844 <input value={new TemporalLike()} type="text" onChange={() => {}} />,
845 );
846 });
847 }).rejects.toThrow(new TypeError('prod message'));
848 assertConsoleErrorDev([
849 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
850 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
851 ' in input (at **)',
852 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
853 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
854 ' in input (at **)',
855 ]);
856 });
857
858 it('should display `value` of number 0', async () => {
859 await act(() => {
860 root.render(<input type="text" value={0} onChange={emptyFunction} />);
861 });
862 const node = container.firstChild;
863
864 expect(node.value).toBe('0');
865 });
866
867 it('should display `value` of bigint 5', async () => {
868 await act(() => {
869 root.render(<input type="text" value={5n} onChange={emptyFunction} />);
870 });
871 const node = container.firstChild;
872
873 expect(node.value).toBe('5');
874 });
875
876 it('should allow setting `value` to `true`', async () => {
877 await act(() => {
878 root.render(<input type="text" value="yolo" onChange={emptyFunction} />);
879 });
880 const node = container.firstChild;
881
882 expect(node.value).toBe('yolo');
883
884 await act(() => {
885 root.render(<input type="text" value={true} onChange={emptyFunction} />);
886 });
887 expect(node.value).toEqual('true');
888 });
889
890 it('should allow setting `value` to `false`', async () => {
891 await act(() => {
892 root.render(<input type="text" value="yolo" onChange={emptyFunction} />);
893 });
894 const node = container.firstChild;
895
896 expect(node.value).toBe('yolo');
897
898 await act(() => {
899 root.render(<input type="text" value={false} onChange={emptyFunction} />);
900 });
901 expect(node.value).toEqual('false');
902 });
903
904 it('should allow setting `value` to `objToString`', async () => {
905 await act(() => {
906 root.render(<input type="text" value="foo" onChange={emptyFunction} />);
907 });
908 const node = container.firstChild;
909
910 expect(node.value).toBe('foo');
911
912 const objToString = {
913 toString: function () {
914 return 'foobar';
915 },
916 };
917 await act(() => {
918 root.render(
919 <input type="text" value={objToString} onChange={emptyFunction} />,
920 );
921 });
922 expect(node.value).toEqual('foobar');
923 });
924
925 it('should not incur unnecessary DOM mutations', async () => {
926 await act(() => {
927 root.render(<input value="a" onChange={() => {}} />);
928 });
929
930 const node = container.firstChild;
931 let nodeValue = 'a';
932 const nodeValueSetter = jest.fn();
933 Object.defineProperty(node, 'value', {
934 get: function () {
935 return nodeValue;
936 },
937 set: nodeValueSetter.mockImplementation(function (newValue) {
938 nodeValue = newValue;
939 }),
940 });
941
942 await act(() => {
943 root.render(<input value="a" onChange={() => {}} />);
944 });
945 expect(nodeValueSetter).toHaveBeenCalledTimes(0);
946
947 await act(() => {
948 root.render(<input value="b" onChange={() => {}} />);
949 });
950 expect(nodeValueSetter).toHaveBeenCalledTimes(1);
951 });
952
953 it('should not incur unnecessary DOM mutations for numeric type conversion', async () => {
954 await act(() => {
955 root.render(<input value="0" onChange={() => {}} />);
956 });
957
958 const node = container.firstChild;
959 let nodeValue = '0';
960 const nodeValueSetter = jest.fn();
961 Object.defineProperty(node, 'value', {
962 get: function () {
963 return nodeValue;
964 },
965 set: nodeValueSetter.mockImplementation(function (newValue) {
966 nodeValue = newValue;
967 }),
968 });
969
970 await act(() => {
971 root.render(<input value={0} onChange={() => {}} />);
972 });
973 expect(nodeValueSetter).toHaveBeenCalledTimes(0);
974 });
975
976 it('should not incur unnecessary DOM mutations for the boolean type conversion', async () => {
977 await act(() => {
978 root.render(<input value="true" onChange={() => {}} />);
979 });
980
981 const node = container.firstChild;
982 let nodeValue = 'true';
983 const nodeValueSetter = jest.fn();
984 Object.defineProperty(node, 'value', {
985 get: function () {
986 return nodeValue;
987 },
988 set: nodeValueSetter.mockImplementation(function (newValue) {
989 nodeValue = newValue;
990 }),
991 });
992
993 await act(() => {
994 root.render(<input value={true} onChange={() => {}} />);
995 });
996 expect(nodeValueSetter).toHaveBeenCalledTimes(0);
997 });
998
999 it('should properly control a value of number `0`', async () => {
1000 await act(() => {
1001 root.render(<input type="text" value={0} onChange={emptyFunction} />);
1002 });
1003 const node = container.firstChild;
1004
1005 setUntrackedValue.call(node, 'giraffe');
1006 dispatchEventOnNode(node, 'input');
1007 expect(node.value).toBe('0');
1008 });
1009
1010 it('should properly control 0.0 for a text input', async () => {
1011 await act(() => {
1012 root.render(<input type="text" value={0} onChange={emptyFunction} />);
1013 });
1014 const node = container.firstChild;
1015
1016 setUntrackedValue.call(node, '0.0');
1017 await act(() => {
1018 dispatchEventOnNode(node, 'input');
1019 });
1020 expect(node.value).toBe('0');
1021 });
1022
1023 it('should properly control 0.0 for a number input', async () => {
1024 await act(() => {
1025 root.render(<input type="number" value={0} onChange={emptyFunction} />);
1026 });
1027 const node = container.firstChild;
1028
1029 setUntrackedValue.call(node, '0.0');
1030 await act(() => {
1031 dispatchEventOnNode(node, 'input');
1032 });
1033
1034 if (disableInputAttributeSyncing) {
1035 expect(node.value).toBe('0.0');
1036 expect(node.hasAttribute('value')).toBe(false);
1037 } else {
1038 expect(node.value).toBe('0.0');
1039 expect(node.getAttribute('value')).toBe('0.0');
1040 }
1041 });
1042
1043 it('should properly transition from an empty value to 0', async () => {
1044 await act(() => {
1045 root.render(<input type="text" value="" onChange={emptyFunction} />);
1046 });
1047 const node = container.firstChild;
1048 expect(isValueDirty(node)).toBe(false);
1049
1050 await act(() => {
1051 root.render(<input type="text" value={0} onChange={emptyFunction} />);
1052 });
1053
1054 expect(node.value).toBe('0');
1055 expect(isValueDirty(node)).toBe(true);
1056
1057 if (disableInputAttributeSyncing) {
1058 expect(node.hasAttribute('value')).toBe(false);
1059 } else {
1060 expect(node.defaultValue).toBe('0');
1061 }
1062 });
1063
1064 it('should properly transition from 0 to an empty value', async () => {
1065 await act(() => {
1066 root.render(<input type="text" value={0} onChange={emptyFunction} />);
1067 });
1068 const node = container.firstChild;
1069 expect(isValueDirty(node)).toBe(true);
1070
1071 await act(() => {
1072 root.render(<input type="text" value="" onChange={emptyFunction} />);
1073 });
1074
1075 expect(node.value).toBe('');
1076 expect(node.defaultValue).toBe('');
1077 expect(isValueDirty(node)).toBe(true);
1078 });
1079
1080 it('should properly transition a text input from 0 to an empty 0.0', async () => {
1081 await act(() => {
1082 root.render(<input type="text" value={0} onChange={emptyFunction} />);
1083 });
1084 await act(() => {
1085 root.render(<input type="text" value="0.0" onChange={emptyFunction} />);
1086 });
1087
1088 const node = container.firstChild;
1089
1090 expect(node.value).toBe('0.0');
1091 if (disableInputAttributeSyncing) {
1092 expect(node.hasAttribute('value')).toBe(false);
1093 } else {
1094 expect(node.defaultValue).toBe('0.0');
1095 }
1096 });
1097
1098 it('should properly transition a number input from "" to 0', async () => {
1099 await act(() => {
1100 root.render(<input type="number" value="" onChange={emptyFunction} />);
1101 });
1102 await act(() => {
1103 root.render(<input type="number" value={0} onChange={emptyFunction} />);
1104 });
1105
1106 const node = container.firstChild;
1107
1108 expect(node.value).toBe('0');
1109 if (disableInputAttributeSyncing) {
1110 expect(node.hasAttribute('value')).toBe(false);
1111 } else {
1112 expect(node.defaultValue).toBe('0');
1113 }
1114 });
1115
1116 it('should properly transition a number input from "" to "0"', async () => {
1117 await act(() => {
1118 root.render(<input type="number" value="" onChange={emptyFunction} />);
1119 });
1120 await act(() => {
1121 root.render(<input type="number" value="0" onChange={emptyFunction} />);
1122 });
1123
1124 const node = container.firstChild;
1125
1126 expect(node.value).toBe('0');
1127 if (disableInputAttributeSyncing) {
1128 expect(node.hasAttribute('value')).toBe(false);
1129 } else {
1130 expect(node.defaultValue).toBe('0');
1131 }
1132 });
1133
1134 it('should have the correct target value', async () => {
1135 let handled = false;
1136 const handler = function (event) {
1137 expect(event.target.nodeName).toBe('INPUT');
1138 handled = true;
1139 };
1140 await act(() => {
1141 root.render(<input type="text" value={0} onChange={handler} />);
1142 });
1143 const node = container.firstChild;
1144
1145 setUntrackedValue.call(node, 'giraffe');
1146
1147 await act(() => {
1148 dispatchEventOnNode(node, 'input');
1149 });
1150
1151 expect(handled).toBe(true);
1152 });
1153
1154 it('should restore uncontrolled inputs to last defaultValue upon reset', async () => {
1155 const inputRef = React.createRef();
1156 await act(() => {
1157 root.render(
1158 <form>
1159 <input defaultValue="default1" ref={inputRef} />
1160 <input type="reset" />
1161 </form>,
1162 );
1163 });
1164 expect(inputRef.current.value).toBe('default1');
1165 if (disableInputAttributeSyncing) {
1166 expect(isValueDirty(inputRef.current)).toBe(false);
1167 } else {
1168 expect(isValueDirty(inputRef.current)).toBe(true);
1169 }
1170
1171 setUntrackedValue.call(inputRef.current, 'changed');
1172 dispatchEventOnNode(inputRef.current, 'input');
1173 expect(inputRef.current.value).toBe('changed');
1174 expect(isValueDirty(inputRef.current)).toBe(true);
1175
1176 await act(() => {
1177 root.render(
1178 <form>
1179 <input defaultValue="default2" ref={inputRef} />
1180 <input type="reset" />
1181 </form>,
1182 );
1183 });
1184 expect(inputRef.current.value).toBe('changed');
1185 expect(isValueDirty(inputRef.current)).toBe(true);
1186
1187 container.firstChild.reset();
1188 // Note: I don't know if we want to always support this.
1189 // But it's current behavior so worth being intentional if we break it.
1190 // https://github.com/facebook/react/issues/4618
1191 expect(inputRef.current.value).toBe('default2');
1192 expect(isValueDirty(inputRef.current)).toBe(false);
1193 });
1194
1195 it('should not set a value for submit buttons unnecessarily', async () => {
1196 const stub = <input type="submit" />;
1197 await act(() => {
1198 root.render(stub);
1199 });
1200 const node = container.firstChild;
1201
1202 // The value shouldn't be '', or else the button will have no text; it
1203 // should have the default "Submit" or "Submit Query" label. Most browsers
1204 // report this as not having a `value` attribute at all; IE reports it as
1205 // the actual label that the user sees.
1206 expect(node.hasAttribute('value')).toBe(false);
1207 });
1208
1209 it('should remove the value attribute on submit inputs when value is updated to undefined', async () => {
1210 const stub = <input type="submit" value="foo" onChange={emptyFunction} />;
1211 await act(() => {
1212 root.render(stub);
1213 });
1214
1215 // Not really relevant to this particular test, but changing to undefined
1216 // should nonetheless trigger a warning
1217 await act(() => {
1218 root.render(
1219 <input type="submit" value={undefined} onChange={emptyFunction} />,
1220 );
1221 });
1222 assertConsoleErrorDev([
1223 'A component is changing a controlled input to be uncontrolled. ' +
1224 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
1225 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
1226 'More info: https://react.dev/link/controlled-components\n' +
1227 ' in input (at **)',
1228 ]);
1229
1230 const node = container.firstChild;
1231 expect(node.getAttribute('value')).toBe(null);
1232 });
1233
1234 it('should remove the value attribute on reset inputs when value is updated to undefined', async () => {
1235 const stub = <input type="reset" value="foo" onChange={emptyFunction} />;
1236 await act(() => {
1237 root.render(stub);
1238 });
1239
1240 // Not really relevant to this particular test, but changing to undefined
1241 // should nonetheless trigger a warning
1242 await act(() => {
1243 root.render(
1244 <input type="reset" value={undefined} onChange={emptyFunction} />,
1245 );
1246 });
1247 assertConsoleErrorDev([
1248 'A component is changing a controlled input to be uncontrolled. ' +
1249 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
1250 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
1251 'More info: https://react.dev/link/controlled-components\n' +
1252 ' in input (at **)',
1253 ]);
1254
1255 const node = container.firstChild;
1256 expect(node.getAttribute('value')).toBe(null);
1257 });
1258
1259 it('should set a value on a submit input', async () => {
1260 const stub = <input type="submit" value="banana" />;
1261 await act(() => {
1262 root.render(stub);
1263 });
1264 const node = container.firstChild;
1265
1266 expect(node.getAttribute('value')).toBe('banana');
1267 });
1268
1269 it('should not set an undefined value on a submit input', async () => {
1270 const stub = <input type="submit" value={undefined} />;
1271 await act(() => {
1272 root.render(stub);
1273 });
1274 const node = container.firstChild;
1275
1276 // Note: it shouldn't be an empty string
1277 // because that would erase the "submit" label.
1278 expect(node.getAttribute('value')).toBe(null);
1279
1280 await act(() => {
1281 root.render(stub);
1282 });
1283 expect(node.getAttribute('value')).toBe(null);
1284 });
1285
1286 it('should not set an undefined value on a reset input', async () => {
1287 const stub = <input type="reset" value={undefined} />;
1288 await act(() => {
1289 root.render(stub);
1290 });
1291 const node = container.firstChild;
1292
1293 // Note: it shouldn't be an empty string
1294 // because that would erase the "reset" label.
1295 expect(node.getAttribute('value')).toBe(null);
1296
1297 await act(() => {
1298 root.render(stub);
1299 });
1300 expect(node.getAttribute('value')).toBe(null);
1301 });
1302
1303 it('should not set a null value on a submit input', async () => {
1304 const stub = <input type="submit" value={null} />;
1305 await act(() => {
1306 root.render(stub);
1307 });
1308 assertConsoleErrorDev([
1309 '`value` prop on `input` should not be null. ' +
1310 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' +
1311 ' in input (at **)',
1312 ]);
1313 const node = container.firstChild;
1314
1315 // Note: it shouldn't be an empty string
1316 // because that would erase the "submit" label.
1317 expect(node.getAttribute('value')).toBe(null);
1318
1319 await act(() => {
1320 root.render(stub);
1321 });
1322 expect(node.getAttribute('value')).toBe(null);
1323 });
1324
1325 it('should not set a null value on a reset input', async () => {
1326 const stub = <input type="reset" value={null} />;
1327 await act(() => {
1328 root.render(stub);
1329 });
1330 assertConsoleErrorDev([
1331 '`value` prop on `input` should not be null. ' +
1332 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' +
1333 ' in input (at **)',
1334 ]);
1335 const node = container.firstChild;
1336
1337 // Note: it shouldn't be an empty string
1338 // because that would erase the "reset" label.
1339 expect(node.getAttribute('value')).toBe(null);
1340
1341 await act(() => {
1342 root.render(stub);
1343 });
1344 expect(node.getAttribute('value')).toBe(null);
1345 });
1346
1347 it('should set a value on a reset input', async () => {
1348 const stub = <input type="reset" value="banana" />;
1349 await act(() => {
1350 root.render(stub);
1351 });
1352 const node = container.firstChild;
1353
1354 expect(node.getAttribute('value')).toBe('banana');
1355 });
1356
1357 it('should set an empty string value on a submit input', async () => {
1358 const stub = <input type="submit" value="" />;
1359 await act(() => {
1360 root.render(stub);
1361 });
1362 const node = container.firstChild;
1363
1364 expect(node.getAttribute('value')).toBe('');
1365 });
1366
1367 it('should set an empty string value on a reset input', async () => {
1368 const stub = <input type="reset" value="" />;
1369 await act(() => {
1370 root.render(stub);
1371 });
1372 const node = container.firstChild;
1373
1374 expect(node.getAttribute('value')).toBe('');
1375 });
1376
1377 it('should control radio buttons', async () => {
1378 class RadioGroup extends React.Component {
1379 aRef = React.createRef();
1380 bRef = React.createRef();
1381 cRef = React.createRef();
1382
1383 render() {
1384 return (
1385 <div>
1386 <input
1387 ref={this.aRef}
1388 type="radio"
1389 name="fruit"
1390 checked={true}
1391 onChange={emptyFunction}
1392 data-which="a"
1393 />
1394 A
1395 <input
1396 ref={this.bRef}
1397 type="radio"
1398 name="fruit"
1399 onChange={emptyFunction}
1400 data-which="b"
1401 />
1402 B
1403 <form>
1404 <input
1405 ref={this.cRef}
1406 type="radio"
1407 name="fruit"
1408 defaultChecked={true}
1409 onChange={emptyFunction}
1410 data-which="c"
1411 />
1412 </form>
1413 </div>
1414 );
1415 }
1416 }
1417
1418 const ref = React.createRef();
1419 await act(() => {
1420 root.render(<RadioGroup ref={ref} />);
1421 });
1422 const stub = ref.current;
1423 const aNode = stub.aRef.current;
1424 const bNode = stub.bRef.current;
1425 const cNode = stub.cRef.current;
1426
1427 expect(aNode.checked).toBe(true);
1428 expect(bNode.checked).toBe(false);
1429 // c is in a separate form and shouldn't be affected at all here
1430 expect(cNode.checked).toBe(true);
1431
1432 if (disableInputAttributeSyncing) {
1433 expect(aNode.hasAttribute('checked')).toBe(false);
1434 expect(bNode.hasAttribute('checked')).toBe(false);
1435 expect(cNode.hasAttribute('checked')).toBe(true);
1436 } else {
1437 expect(aNode.hasAttribute('checked')).toBe(true);
1438 expect(bNode.hasAttribute('checked')).toBe(false);
1439 expect(cNode.hasAttribute('checked')).toBe(true);
1440 }
1441
1442 expect(isCheckedDirty(aNode)).toBe(true);
1443 expect(isCheckedDirty(bNode)).toBe(true);
1444 expect(isCheckedDirty(cNode)).toBe(true);
1445 assertInputTrackingIsCurrent(container);
1446
1447 setUntrackedChecked.call(bNode, true);
1448 expect(aNode.checked).toBe(false);
1449 expect(cNode.checked).toBe(true);
1450
1451 // The original 'checked' attribute should be unchanged
1452 if (disableInputAttributeSyncing) {
1453 expect(aNode.hasAttribute('checked')).toBe(false);
1454 expect(bNode.hasAttribute('checked')).toBe(false);
1455 expect(cNode.hasAttribute('checked')).toBe(true);
1456 } else {
1457 expect(aNode.hasAttribute('checked')).toBe(true);
1458 expect(bNode.hasAttribute('checked')).toBe(false);
1459 expect(cNode.hasAttribute('checked')).toBe(true);
1460 }
1461
1462 // Now let's run the actual ReactDOMInput change event handler
1463 await act(() => {
1464 dispatchEventOnNode(bNode, 'click');
1465 });
1466
1467 // The original state should have been restored
1468 expect(aNode.checked).toBe(true);
1469 expect(cNode.checked).toBe(true);
1470
1471 expect(isCheckedDirty(aNode)).toBe(true);
1472 expect(isCheckedDirty(bNode)).toBe(true);
1473 expect(isCheckedDirty(cNode)).toBe(true);
1474 assertInputTrackingIsCurrent(container);
1475 });
1476
1477 it('should hydrate controlled radio buttons', async () => {
1478 function App() {
1479 const [current, setCurrent] = React.useState('a');
1480 return (
1481 <>
1482 <input
1483 type="radio"
1484 name="fruit"
1485 checked={current === 'a'}
1486 onChange={() => {
1487 Scheduler.log('click a');
1488 setCurrent('a');
1489 }}
1490 />
1491 <input
1492 type="radio"
1493 name="fruit"
1494 checked={current === 'b'}
1495 onChange={() => {
1496 Scheduler.log('click b');
1497 setCurrent('b');
1498 }}
1499 />
1500 <input
1501 type="radio"
1502 name="fruit"
1503 checked={current === 'c'}
1504 onChange={() => {
1505 Scheduler.log('click c');
1506 // Let's say the user can't pick C
1507 }}
1508 />
1509 </>
1510 );
1511 }
1512 const html = ReactDOMServer.renderToString(<App />);
1513 // Create a fresh container, not attached a root yet
1514 container.remove();
1515 container = document.createElement('div');
1516 document.body.appendChild(container);
1517 container.innerHTML = html;
1518 const [a, b, c] = container.querySelectorAll('input');
1519 expect(a.checked).toBe(true);
1520 expect(b.checked).toBe(false);
1521 expect(c.checked).toBe(false);
1522 expect(isCheckedDirty(a)).toBe(false);
1523 expect(isCheckedDirty(b)).toBe(false);
1524 expect(isCheckedDirty(c)).toBe(false);
1525
1526 // Click on B before hydrating
1527 b.checked = true;
1528 expect(isCheckedDirty(a)).toBe(true);
1529 expect(isCheckedDirty(b)).toBe(true);
1530 expect(isCheckedDirty(c)).toBe(false);
1531
1532 await act(async () => {
1533 ReactDOMClient.hydrateRoot(container, <App />);
1534 });
1535
1536 if (gate(flags => flags.enableHydrationChangeEvent)) {
1537 // We replayed the click since the value changed before hydration.
1538 assertLog(['click b']);
1539 } else {
1540 assertLog([]);
1541 // Strangely, we leave `b` checked even though we rendered A with
1542 // checked={true} and B with checked={false}. Arguably this is a bug.
1543 }
1544 expect(a.checked).toBe(false);
1545 expect(b.checked).toBe(true);
1546 expect(c.checked).toBe(false);
1547 expect(isCheckedDirty(a)).toBe(true);
1548 expect(isCheckedDirty(b)).toBe(true);
1549 expect(isCheckedDirty(c)).toBe(true);
1550 assertInputTrackingIsCurrent(container);
1551
1552 // If we click on C now though...
1553 await act(async () => {
1554 setUntrackedChecked.call(c, true);
1555 dispatchEventOnNode(c, 'click');
1556 });
1557
1558 assertLog(['click c']);
1559 if (gate(flags => flags.enableHydrationChangeEvent)) {
1560 // then since C's onClick doesn't set state, B becomes rechecked.
1561 expect(a.checked).toBe(false);
1562 expect(b.checked).toBe(true);
1563 expect(c.checked).toBe(false);
1564 } else {
1565 // then since C's onClick doesn't set state, A becomes rechecked
1566 // since in this branch we didn't replay to select B.
1567 expect(a.checked).toBe(true);
1568 expect(b.checked).toBe(false);
1569 expect(c.checked).toBe(false);
1570 }
1571 expect(isCheckedDirty(a)).toBe(true);
1572 expect(isCheckedDirty(b)).toBe(true);
1573 expect(isCheckedDirty(c)).toBe(true);
1574 assertInputTrackingIsCurrent(container);
1575
1576 await act(async () => {
1577 setUntrackedChecked.call(b, true);
1578 dispatchEventOnNode(b, 'click');
1579 });
1580 if (gate(flags => flags.enableHydrationChangeEvent)) {
1581 // Since we already had this selected, this doesn't trigger a change again.
1582 assertLog([]);
1583 } else {
1584 // And we can also change to B properly after hydration.
1585 assertLog(['click b']);
1586 }
1587 expect(a.checked).toBe(false);
1588 expect(b.checked).toBe(true);
1589 expect(c.checked).toBe(false);
1590 expect(isCheckedDirty(a)).toBe(true);
1591 expect(isCheckedDirty(b)).toBe(true);
1592 expect(isCheckedDirty(c)).toBe(true);
1593 assertInputTrackingIsCurrent(container);
1594 });
1595
1596 it('should hydrate uncontrolled radio buttons', async () => {
1597 function App() {
1598 return (
1599 <>
1600 <input
1601 type="radio"
1602 name="fruit"
1603 defaultChecked={true}
1604 onChange={() => Scheduler.log('click a')}
1605 />
1606 <input
1607 type="radio"
1608 name="fruit"
1609 defaultChecked={false}
1610 onChange={() => Scheduler.log('click b')}
1611 />
1612 <input
1613 type="radio"
1614 name="fruit"
1615 defaultChecked={false}
1616 onChange={() => Scheduler.log('click c')}
1617 />
1618 </>
1619 );
1620 }
1621 const html = ReactDOMServer.renderToString(<App />);
1622 // Create a fresh container, not attached a root yet
1623 container.remove();
1624 container = document.createElement('div');
1625 document.body.appendChild(container);
1626 container.innerHTML = html;
1627 const [a, b, c] = container.querySelectorAll('input');
1628 expect(a.checked).toBe(true);
1629 expect(b.checked).toBe(false);
1630 expect(c.checked).toBe(false);
1631 expect(isCheckedDirty(a)).toBe(false);
1632 expect(isCheckedDirty(b)).toBe(false);
1633 expect(isCheckedDirty(c)).toBe(false);
1634
1635 // Click on B before hydrating
1636 b.checked = true;
1637 expect(isCheckedDirty(a)).toBe(true);
1638 expect(isCheckedDirty(b)).toBe(true);
1639 expect(isCheckedDirty(c)).toBe(false);
1640
1641 await act(async () => {
1642 ReactDOMClient.hydrateRoot(container, <App />);
1643 });
1644
1645 if (gate(flags => flags.enableHydrationChangeEvent)) {
1646 // We replayed the click since the value changed before hydration.
1647 assertLog(['click b']);
1648 } else {
1649 assertLog([]);
1650 }
1651 expect(a.checked).toBe(false);
1652 expect(b.checked).toBe(true);
1653 expect(c.checked).toBe(false);
1654 expect(isCheckedDirty(a)).toBe(true);
1655 expect(isCheckedDirty(b)).toBe(true);
1656 expect(isCheckedDirty(c)).toBe(true);
1657 assertInputTrackingIsCurrent(container);
1658
1659 // Click back to A
1660 await act(async () => {
1661 setUntrackedChecked.call(a, true);
1662 dispatchEventOnNode(a, 'click');
1663 });
1664
1665 assertLog(['click a']);
1666 expect(a.checked).toBe(true);
1667 expect(b.checked).toBe(false);
1668 expect(c.checked).toBe(false);
1669 expect(isCheckedDirty(a)).toBe(true);
1670 expect(isCheckedDirty(b)).toBe(true);
1671 expect(isCheckedDirty(c)).toBe(true);
1672 assertInputTrackingIsCurrent(container);
1673 });
1674
1675 it('should check the correct radio when the selected name moves', async () => {
1676 class App extends React.Component {
1677 state = {
1678 updated: false,
1679 };
1680 onClick = () => {
1681 this.setState({updated: !this.state.updated});
1682 };
1683 render() {
1684 const {updated} = this.state;
1685 const radioName = updated ? 'secondName' : 'firstName';
1686 return (
1687 <div>
1688 <button type="button" onClick={this.onClick} />
1689 <input
1690 type="radio"
1691 name={radioName}
1692 onChange={emptyFunction}
1693 checked={updated === true}
1694 />
1695 <input
1696 type="radio"
1697 name={radioName}
1698 onChange={emptyFunction}
1699 checked={updated === false}
1700 />
1701 </div>
1702 );
1703 }
1704 }
1705
1706 await act(() => {
1707 root.render(<App />);
1708 });
1709 const node = container.firstChild;
1710 const buttonNode = node.childNodes[0];
1711 const firstRadioNode = node.childNodes[1];
1712 expect(isCheckedDirty(firstRadioNode)).toBe(true);
1713 expect(firstRadioNode.checked).toBe(false);
1714 assertInputTrackingIsCurrent(container);
1715 await act(() => {
1716 dispatchEventOnNode(buttonNode, 'click');
1717 });
1718 expect(firstRadioNode.checked).toBe(true);
1719 assertInputTrackingIsCurrent(container);
1720 await act(() => {
1721 dispatchEventOnNode(buttonNode, 'click');
1722 });
1723 expect(firstRadioNode.checked).toBe(false);
1724 assertInputTrackingIsCurrent(container);
1725 });
1726
1727 it("shouldn't get tricked by changing radio names, part 2", async () => {
1728 await act(() => {
1729 root.render(
1730 <div>
1731 <input
1732 type="radio"
1733 name="a"
1734 value="1"
1735 checked={true}
1736 onChange={() => {}}
1737 />
1738 <input
1739 type="radio"
1740 name="a"
1741 value="2"
1742 checked={false}
1743 onChange={() => {}}
1744 />
1745 </div>,
1746 );
1747 });
1748 const one = container.querySelector('input[name="a"][value="1"]');
1749 const two = container.querySelector('input[name="a"][value="2"]');
1750 expect(one.checked).toBe(true);
1751 expect(two.checked).toBe(false);
1752 expect(isCheckedDirty(one)).toBe(true);
1753 expect(isCheckedDirty(two)).toBe(true);
1754 assertInputTrackingIsCurrent(container);
1755
1756 await act(() => {
1757 root.render(
1758 <div>
1759 <input
1760 type="radio"
1761 name="a"
1762 value="1"
1763 checked={true}
1764 onChange={() => {}}
1765 />
1766 <input
1767 type="radio"
1768 name="b"
1769 value="2"
1770 checked={true}
1771 onChange={() => {}}
1772 />
1773 </div>,
1774 );
1775 });
1776 expect(one.checked).toBe(true);
1777 expect(two.checked).toBe(true);
1778 expect(isCheckedDirty(one)).toBe(true);
1779 expect(isCheckedDirty(two)).toBe(true);
1780 assertInputTrackingIsCurrent(container);
1781 });
1782
1783 // @gate !disableLegacyMode
1784 it('should control radio buttons if the tree updates during render in legacy mode', async () => {
1785 container.remove();
1786 container = document.createElement('div');
1787 document.body.appendChild(container);
1788 const sharedParent = container;
1789 const container1 = document.createElement('div');
1790 const container2 = document.createElement('div');
1791
1792 sharedParent.appendChild(container1);
1793
1794 let aNode;
1795 let bNode;
1796 class ComponentA extends React.Component {
1797 state = {changed: false};
1798 handleChange = () => {
1799 this.setState({
1800 changed: true,
1801 });
1802 };
1803 componentDidUpdate() {
1804 sharedParent.appendChild(container2);
1805 }
1806 componentDidMount() {
1807 ReactDOM.render(<ComponentB />, container2);
1808 }
1809 render() {
1810 return (
1811 <div>
1812 <input
1813 ref={n => (aNode = n)}
1814 type="radio"
1815 name="fruit"
1816 checked={false}
1817 onChange={this.handleChange}
1818 />
1819 A
1820 </div>
1821 );
1822 }
1823 }
1824
1825 class ComponentB extends React.Component {
1826 render() {
1827 return (
1828 <div>
1829 <input
1830 ref={n => (bNode = n)}
1831 type="radio"
1832 name="fruit"
1833 checked={true}
1834 onChange={emptyFunction}
1835 />
1836 B
1837 </div>
1838 );
1839 }
1840 }
1841
1842 ReactDOM.render(<ComponentA />, container1);
1843
1844 expect(aNode.checked).toBe(false);
1845 expect(bNode.checked).toBe(true);
1846 expect(isCheckedDirty(aNode)).toBe(true);
1847 expect(isCheckedDirty(bNode)).toBe(true);
1848 assertInputTrackingIsCurrent(container);
1849
1850 setUntrackedChecked.call(aNode, true);
1851 // This next line isn't necessary in a proper browser environment, but
1852 // jsdom doesn't uncheck the others in a group (because they are not yet
1853 // sharing a parent), which makes this whole test a little less effective.
1854 setUntrackedChecked.call(bNode, false);
1855
1856 // Now let's run the actual ReactDOMInput change event handler
1857 dispatchEventOnNode(aNode, 'click');
1858
1859 // The original state should have been restored
1860 expect(aNode.checked).toBe(false);
1861 expect(bNode.checked).toBe(true);
1862 expect(isCheckedDirty(aNode)).toBe(true);
1863 expect(isCheckedDirty(bNode)).toBe(true);
1864 assertInputTrackingIsCurrent(container);
1865 });
1866
1867 it('should control radio buttons if the tree updates during render (case 2; #26876)', async () => {
1868 let thunk = null;
1869 function App() {
1870 const [disabled, setDisabled] = React.useState(false);
1871 const [value, setValue] = React.useState('one');
1872 function handleChange(e) {
1873 setDisabled(true);
1874 // Pretend this is in a setTimeout or something
1875 thunk = () => {
1876 setDisabled(false);
1877 setValue(e.target.value);
1878 };
1879 }
1880 return (
1881 <>
1882 <input
1883 type="radio"
1884 name="fruit"
1885 value="one"
1886 checked={value === 'one'}
1887 onChange={handleChange}
1888 disabled={disabled}
1889 />
1890 <input
1891 type="radio"
1892 name="fruit"
1893 value="two"
1894 checked={value === 'two'}
1895 onChange={handleChange}
1896 disabled={disabled}
1897 />
1898 </>
1899 );
1900 }
1901 await act(() => {
1902 root.render(<App />);
1903 });
1904 const [one, two] = container.querySelectorAll('input');
1905 expect(one.checked).toBe(true);
1906 expect(two.checked).toBe(false);
1907 expect(isCheckedDirty(one)).toBe(true);
1908 expect(isCheckedDirty(two)).toBe(true);
1909 assertInputTrackingIsCurrent(container);
1910
1911 // Click two
1912 setUntrackedChecked.call(two, true);
1913 await act(() => {
1914 dispatchEventOnNode(two, 'click');
1915 });
1916 expect(one.checked).toBe(true);
1917 expect(two.checked).toBe(false);
1918 expect(isCheckedDirty(one)).toBe(true);
1919 expect(isCheckedDirty(two)).toBe(true);
1920 assertInputTrackingIsCurrent(container);
1921
1922 // After a delay...
1923 await act(thunk);
1924 expect(one.checked).toBe(false);
1925 expect(two.checked).toBe(true);
1926 expect(isCheckedDirty(one)).toBe(true);
1927 expect(isCheckedDirty(two)).toBe(true);
1928 assertInputTrackingIsCurrent(container);
1929
1930 // Click back to one
1931 setUntrackedChecked.call(one, true);
1932 await act(() => {
1933 dispatchEventOnNode(one, 'click');
1934 });
1935 expect(one.checked).toBe(false);
1936 expect(two.checked).toBe(true);
1937 expect(isCheckedDirty(one)).toBe(true);
1938 expect(isCheckedDirty(two)).toBe(true);
1939 assertInputTrackingIsCurrent(container);
1940
1941 // After a delay...
1942 await act(thunk);
1943 expect(one.checked).toBe(true);
1944 expect(two.checked).toBe(false);
1945 expect(isCheckedDirty(one)).toBe(true);
1946 expect(isCheckedDirty(two)).toBe(true);
1947 assertInputTrackingIsCurrent(container);
1948 });
1949
1950 it('should warn with value and no onChange handler and readOnly specified', async () => {
1951 await act(() => {
1952 root.render(<input type="text" value="zoink" readOnly={true} />);
1953 });
1954 root.unmount();
1955 root = ReactDOMClient.createRoot(container);
1956
1957 await act(() => {
1958 root.render(<input type="text" value="zoink" readOnly={false} />);
1959 });
1960 assertConsoleErrorDev([
1961 'You provided a `value` prop to a form ' +
1962 'field without an `onChange` handler. This will render a read-only ' +
1963 'field. If the field should be mutable use `defaultValue`. ' +
1964 'Otherwise, set either `onChange` or `readOnly`.\n' +
1965 ' in input (at **)',
1966 ]);
1967 });
1968
1969 it('should have a this value of undefined if bind is not used', async () => {
1970 expect.assertions(1);
1971 const unboundInputOnChange = function () {
1972 expect(this).toBe(undefined);
1973 };
1974
1975 const stub = <input type="text" onChange={unboundInputOnChange} />;
1976 await act(() => {
1977 root.render(stub);
1978 });
1979 const node = container.firstChild;
1980
1981 setUntrackedValue.call(node, 'giraffe');
1982 await act(() => {
1983 dispatchEventOnNode(node, 'input');
1984 });
1985 });
1986
1987 it('should update defaultValue to empty string', async () => {
1988 await act(() => {
1989 root.render(<input type="text" defaultValue={'foo'} />);
1990 });
1991 if (disableInputAttributeSyncing) {
1992 expect(isValueDirty(container.firstChild)).toBe(false);
1993 } else {
1994 expect(isValueDirty(container.firstChild)).toBe(true);
1995 }
1996 await act(() => {
1997 root.render(<input type="text" defaultValue={''} />);
1998 });
1999 expect(container.firstChild.defaultValue).toBe('');
2000 if (disableInputAttributeSyncing) {
2001 expect(isValueDirty(container.firstChild)).toBe(false);
2002 } else {
2003 expect(isValueDirty(container.firstChild)).toBe(true);
2004 }
2005 });
2006
2007 it('should warn if value is null', async () => {
2008 await act(() => {
2009 root.render(<input type="text" value={null} />);
2010 });
2011 assertConsoleErrorDev([
2012 '`value` prop on `input` should not be null. ' +
2013 'Consider using an empty string to clear the component or `undefined` ' +
2014 'for uncontrolled components.\n' +
2015 ' in input (at **)',
2016 ]);
2017 root.unmount();
2018
2019 root = ReactDOMClient.createRoot(container);
2020 await act(() => {
2021 root.render(<input type="text" value={null} />);
2022 });
2023 });
2024
2025 it('should warn if checked and defaultChecked props are specified', async () => {
2026 await act(() => {
2027 root.render(
2028 <input
2029 type="radio"
2030 checked={true}
2031 defaultChecked={true}
2032 readOnly={true}
2033 />,
2034 );
2035 });
2036 assertConsoleErrorDev([
2037 'A component contains an input of type radio with both checked and defaultChecked props. ' +
2038 'Input elements must be either controlled or uncontrolled ' +
2039 '(specify either the checked prop, or the defaultChecked prop, but not ' +
2040 'both). Decide between using a controlled or uncontrolled input ' +
2041 'element and remove one of these props. More info: ' +
2042 'https://react.dev/link/controlled-components\n' +
2043 ' in input (at **)',
2044 ]);
2045 root.unmount();
2046
2047 root = ReactDOMClient.createRoot(container);
2048 await act(() => {
2049 root.render(
2050 <input
2051 type="radio"
2052 checked={true}
2053 defaultChecked={true}
2054 readOnly={true}
2055 />,
2056 );
2057 });
2058 });
2059
2060 it('should warn if value and defaultValue props are specified', async () => {
2061 await act(() => {
2062 root.render(
2063 <input type="text" value="foo" defaultValue="bar" readOnly={true} />,
2064 );
2065 });
2066 assertConsoleErrorDev([
2067 'A component contains an input of type text with both value and defaultValue props. ' +
2068 'Input elements must be either controlled or uncontrolled ' +
2069 '(specify either the value prop, or the defaultValue prop, but not ' +
2070 'both). Decide between using a controlled or uncontrolled input ' +
2071 'element and remove one of these props. More info: ' +
2072 'https://react.dev/link/controlled-components\n' +
2073 ' in input (at **)',
2074 ]);
2075 await (() => {
2076 root.unmount();
2077 });
2078
2079 await act(() => {
2080 root.render(
2081 <input type="text" value="foo" defaultValue="bar" readOnly={true} />,
2082 );
2083 });
2084 });
2085
2086 it('should warn if controlled input switches to uncontrolled (value is undefined)', async () => {
2087 const stub = (
2088 <input type="text" value="controlled" onChange={emptyFunction} />
2089 );
2090 await act(() => {
2091 root.render(stub);
2092 });
2093 await act(() => {
2094 root.render(<input type="text" />);
2095 });
2096 assertConsoleErrorDev([
2097 'A component is changing a controlled input to be uncontrolled. ' +
2098 'This is likely caused by the value changing from a defined to ' +
2099 'undefined, which should not happen. ' +
2100 'Decide between using a controlled or uncontrolled input ' +
2101 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2102 ' in input (at **)',
2103 ]);
2104 });
2105
2106 it('should warn if controlled input switches to uncontrolled (value is null)', async () => {
2107 const stub = (
2108 <input type="text" value="controlled" onChange={emptyFunction} />
2109 );
2110 await act(() => {
2111 root.render(stub);
2112 });
2113 await act(() => {
2114 root.render(<input type="text" value={null} />);
2115 });
2116 assertConsoleErrorDev([
2117 '`value` prop on `input` should not be null. ' +
2118 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' +
2119 ' in input (at **)',
2120 'A component is changing a controlled input to be uncontrolled. ' +
2121 'This is likely caused by the value changing from a defined to ' +
2122 'undefined, which should not happen. ' +
2123 'Decide between using a controlled or uncontrolled input ' +
2124 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2125 ' in input (at **)',
2126 ]);
2127 });
2128
2129 it('should warn if controlled input switches to uncontrolled with defaultValue', async () => {
2130 const stub = (
2131 <input type="text" value="controlled" onChange={emptyFunction} />
2132 );
2133 await act(() => {
2134 root.render(stub);
2135 });
2136 await act(() => {
2137 root.render(<input type="text" defaultValue="uncontrolled" />);
2138 });
2139 assertConsoleErrorDev([
2140 'A component is changing a controlled input to be uncontrolled. ' +
2141 'This is likely caused by the value changing from a defined to ' +
2142 'undefined, which should not happen. ' +
2143 'Decide between using a controlled or uncontrolled input ' +
2144 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2145 ' in input (at **)',
2146 ]);
2147 });
2148
2149 it('should warn if uncontrolled input (value is undefined) switches to controlled', async () => {
2150 const stub = <input type="text" />;
2151 await act(() => {
2152 root.render(stub);
2153 });
2154 await act(() => {
2155 root.render(<input type="text" value="controlled" />);
2156 });
2157 assertConsoleErrorDev([
2158 'A component is changing an uncontrolled input to be controlled. ' +
2159 'This is likely caused by the value changing from undefined to ' +
2160 'a defined value, which should not happen. ' +
2161 'Decide between using a controlled or uncontrolled input ' +
2162 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2163 ' in input (at **)',
2164 ]);
2165 });
2166
2167 it('should warn if uncontrolled input (value is null) switches to controlled', async () => {
2168 const stub = <input type="text" value={null} />;
2169 await act(() => {
2170 root.render(stub);
2171 });
2172 assertConsoleErrorDev([
2173 '`value` prop on `input` should not be null. ' +
2174 'Consider using an empty string to clear the component or `undefined` for uncontrolled components.\n' +
2175 ' in input (at **)',
2176 ]);
2177 await act(() => {
2178 root.render(<input type="text" value="controlled" />);
2179 });
2180 assertConsoleErrorDev([
2181 'A component is changing an uncontrolled input to be controlled. ' +
2182 'This is likely caused by the value changing from undefined to ' +
2183 'a defined value, which should not happen. ' +
2184 'Decide between using a controlled or uncontrolled input ' +
2185 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2186 ' in input (at **)',
2187 ]);
2188 });
2189
2190 it('should warn if controlled checkbox switches to uncontrolled (checked is undefined)', async () => {
2191 const stub = (
2192 <input type="checkbox" checked={true} onChange={emptyFunction} />
2193 );
2194 await act(() => {
2195 root.render(stub);
2196 });
2197 await act(() => {
2198 root.render(<input type="checkbox" />);
2199 });
2200 assertConsoleErrorDev([
2201 'A component is changing a controlled input to be uncontrolled. ' +
2202 'This is likely caused by the value changing from a defined to ' +
2203 'undefined, which should not happen. ' +
2204 'Decide between using a controlled or uncontrolled input ' +
2205 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2206 ' in input (at **)',
2207 ]);
2208 });
2209
2210 it('should warn if controlled checkbox switches to uncontrolled (checked is null)', async () => {
2211 const stub = (
2212 <input type="checkbox" checked={true} onChange={emptyFunction} />
2213 );
2214 await act(() => {
2215 root.render(stub);
2216 });
2217 await act(() => {
2218 root.render(<input type="checkbox" checked={null} />);
2219 });
2220 assertConsoleErrorDev([
2221 'A component is changing a controlled input to be uncontrolled. ' +
2222 'This is likely caused by the value changing from a defined to ' +
2223 'undefined, which should not happen. ' +
2224 'Decide between using a controlled or uncontrolled input ' +
2225 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2226 ' in input (at **)',
2227 ]);
2228 });
2229
2230 it('should warn if controlled checkbox switches to uncontrolled with defaultChecked', async () => {
2231 const stub = (
2232 <input type="checkbox" checked={true} onChange={emptyFunction} />
2233 );
2234 await act(() => {
2235 root.render(stub);
2236 });
2237 await act(() => {
2238 root.render(<input type="checkbox" defaultChecked={true} />);
2239 });
2240 assertConsoleErrorDev([
2241 'A component is changing a controlled input to be uncontrolled. ' +
2242 'This is likely caused by the value changing from a defined to ' +
2243 'undefined, which should not happen. ' +
2244 'Decide between using a controlled or uncontrolled input ' +
2245 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2246 ' in input (at **)',
2247 ]);
2248 });
2249
2250 it('should warn if uncontrolled checkbox (checked is undefined) switches to controlled', async () => {
2251 const stub = <input type="checkbox" />;
2252 await act(() => {
2253 root.render(stub);
2254 });
2255 await act(() => {
2256 root.render(<input type="checkbox" checked={true} />);
2257 });
2258 assertConsoleErrorDev([
2259 'A component is changing an uncontrolled input to be controlled. ' +
2260 'This is likely caused by the value changing from undefined to ' +
2261 'a defined value, which should not happen. ' +
2262 'Decide between using a controlled or uncontrolled input ' +
2263 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2264 ' in input (at **)',
2265 ]);
2266 });
2267
2268 it('should warn if uncontrolled checkbox (checked is null) switches to controlled', async () => {
2269 const stub = <input type="checkbox" checked={null} />;
2270 await act(() => {
2271 root.render(stub);
2272 });
2273 await act(() => {
2274 root.render(<input type="checkbox" checked={true} />);
2275 });
2276 assertConsoleErrorDev([
2277 'A component is changing an uncontrolled input to be controlled. ' +
2278 'This is likely caused by the value changing from undefined to ' +
2279 'a defined value, which should not happen. ' +
2280 'Decide between using a controlled or uncontrolled input ' +
2281 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2282 ' in input (at **)',
2283 ]);
2284 });
2285
2286 it('should warn if controlled radio switches to uncontrolled (checked is undefined)', async () => {
2287 const stub = <input type="radio" checked={true} onChange={emptyFunction} />;
2288 await act(() => {
2289 root.render(stub);
2290 });
2291 await act(() => {
2292 root.render(<input type="radio" />);
2293 });
2294 assertConsoleErrorDev([
2295 'A component is changing a controlled input to be uncontrolled. ' +
2296 'This is likely caused by the value changing from a defined to ' +
2297 'undefined, which should not happen. ' +
2298 'Decide between using a controlled or uncontrolled input ' +
2299 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2300 ' in input (at **)',
2301 ]);
2302 });
2303
2304 it('should warn if controlled radio switches to uncontrolled (checked is null)', async () => {
2305 const stub = <input type="radio" checked={true} onChange={emptyFunction} />;
2306 await act(() => {
2307 root.render(stub);
2308 });
2309 await act(() => {
2310 root.render(<input type="radio" checked={null} />);
2311 });
2312 assertConsoleErrorDev([
2313 'A component is changing a controlled input to be uncontrolled. ' +
2314 'This is likely caused by the value changing from a defined to ' +
2315 'undefined, which should not happen. ' +
2316 'Decide between using a controlled or uncontrolled input ' +
2317 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2318 ' in input (at **)',
2319 ]);
2320 });
2321
2322 it('should warn if controlled radio switches to uncontrolled with defaultChecked', async () => {
2323 const stub = <input type="radio" checked={true} onChange={emptyFunction} />;
2324 await act(() => {
2325 root.render(stub);
2326 });
2327 await act(() => {
2328 root.render(<input type="radio" defaultChecked={true} />);
2329 });
2330 assertConsoleErrorDev([
2331 'A component is changing a controlled input to be uncontrolled. ' +
2332 'This is likely caused by the value changing from a defined to ' +
2333 'undefined, which should not happen. ' +
2334 'Decide between using a controlled or uncontrolled input ' +
2335 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2336 ' in input (at **)',
2337 ]);
2338 });
2339
2340 it('should warn if uncontrolled radio (checked is undefined) switches to controlled', async () => {
2341 const stub = <input type="radio" />;
2342 await act(() => {
2343 root.render(stub);
2344 });
2345 await act(() => {
2346 root.render(<input type="radio" checked={true} />);
2347 });
2348 assertConsoleErrorDev([
2349 'A component is changing an uncontrolled input to be controlled. ' +
2350 'This is likely caused by the value changing from undefined to ' +
2351 'a defined value, which should not happen. ' +
2352 'Decide between using a controlled or uncontrolled input ' +
2353 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2354 ' in input (at **)',
2355 ]);
2356 });
2357
2358 it('should warn if uncontrolled radio (checked is null) switches to controlled', async () => {
2359 const stub = <input type="radio" checked={null} />;
2360 await act(() => {
2361 root.render(stub);
2362 });
2363 await act(() => {
2364 root.render(<input type="radio" checked={true} />);
2365 });
2366 assertConsoleErrorDev([
2367 'A component is changing an uncontrolled input to be controlled. ' +
2368 'This is likely caused by the value changing from undefined to ' +
2369 'a defined value, which should not happen. ' +
2370 'Decide between using a controlled or uncontrolled input ' +
2371 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2372 ' in input (at **)',
2373 ]);
2374 });
2375
2376 it('should not warn if radio value changes but never becomes controlled', async () => {
2377 await act(() => {
2378 root.render(<input type="radio" value="value" />);
2379 });
2380 await act(() => {
2381 root.render(<input type="radio" />);
2382 });
2383 await act(() => {
2384 root.render(<input type="radio" value="value" defaultChecked={true} />);
2385 });
2386 await act(() => {
2387 root.render(<input type="radio" value="value" onChange={() => null} />);
2388 });
2389 await act(() => {
2390 root.render(<input type="radio" />);
2391 });
2392 });
2393
2394 it('should not warn if radio value changes but never becomes uncontrolled', async () => {
2395 await act(() => {
2396 root.render(<input type="radio" checked={false} onChange={() => null} />);
2397 });
2398 const input = container.querySelector('input');
2399 expect(isCheckedDirty(input)).toBe(true);
2400 await act(() => {
2401 root.render(
2402 <input
2403 type="radio"
2404 value="value"
2405 defaultChecked={true}
2406 checked={false}
2407 onChange={() => null}
2408 />,
2409 );
2410 });
2411 expect(isCheckedDirty(input)).toBe(true);
2412 assertInputTrackingIsCurrent(container);
2413 });
2414
2415 it('should warn if radio checked false changes to become uncontrolled', async () => {
2416 await act(() => {
2417 root.render(
2418 <input
2419 type="radio"
2420 value="value"
2421 checked={false}
2422 onChange={() => null}
2423 />,
2424 );
2425 });
2426 await act(() => {
2427 root.render(<input type="radio" value="value" />);
2428 });
2429 assertConsoleErrorDev([
2430 'A component is changing a controlled input to be uncontrolled. ' +
2431 'This is likely caused by the value changing from a defined to ' +
2432 'undefined, which should not happen. ' +
2433 'Decide between using a controlled or uncontrolled input ' +
2434 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components\n' +
2435 ' in input (at **)',
2436 ]);
2437 });
2438
2439 it('sets type, step, min, max before value always', async () => {
2440 const log = [];
2441 const originalCreateElement = document.createElement;
2442 spyOnDevAndProd(document, 'createElement').mockImplementation(
2443 function (type) {
2444 const el = originalCreateElement.apply(this, arguments);
2445 let value = '';
2446 let typeProp = '';
2447
2448 if (type === 'input') {
2449 Object.defineProperty(el, 'type', {
2450 get: function () {
2451 return typeProp;
2452 },
2453 set: function (val) {
2454 typeProp = String(val);
2455 log.push('set property type');
2456 },
2457 });
2458 Object.defineProperty(el, 'value', {
2459 get: function () {
2460 return value;
2461 },
2462 set: function (val) {
2463 value = String(val);
2464 log.push('set property value');
2465 },
2466 });
2467 spyOnDevAndProd(el, 'setAttribute').mockImplementation(
2468 function (name) {
2469 log.push('set attribute ' + name);
2470 },
2471 );
2472 }
2473 return el;
2474 },
2475 );
2476
2477 await act(() => {
2478 root.render(
2479 <input
2480 value="0"
2481 onChange={() => {}}
2482 type="range"
2483 min="0"
2484 max="100"
2485 step="1"
2486 />,
2487 );
2488 });
2489
2490 expect(log).toEqual([
2491 'set attribute min',
2492 'set attribute max',
2493 'set attribute step',
2494 'set property type',
2495 'set property value',
2496 ]);
2497 });
2498
2499 it('sets value properly with type coming later in props', async () => {
2500 await act(() => {
2501 root.render(<input value="hi" type="radio" />);
2502 });
2503 expect(container.firstChild.value).toBe('hi');
2504 });
2505
2506 it('does not raise a validation warning when it switches types', async () => {
2507 class Input extends React.Component {
2508 state = {type: 'number', value: 1000};
2509
2510 render() {
2511 const {value, type} = this.state;
2512 return <input onChange={() => {}} type={type} value={value} />;
2513 }
2514 }
2515
2516 const ref = React.createRef();
2517 await act(() => {
2518 root.render(<Input ref={ref} />);
2519 });
2520 const node = container.firstChild;
2521
2522 // If the value is set before the type, a validation warning will raise and
2523 // the value will not be assigned.
2524 await act(() => {
2525 ref.current.setState({type: 'text', value: 'Test'});
2526 });
2527 expect(node.value).toEqual('Test');
2528 });
2529
2530 it('resets value of date/time input to fix bugs in iOS Safari', async () => {
2531 function strify(x) {
2532 return JSON.stringify(x, null, 2);
2533 }
2534
2535 const log = [];
2536 const originalCreateElement = document.createElement;
2537 spyOnDevAndProd(document, 'createElement').mockImplementation(
2538 function (type) {
2539 const el = originalCreateElement.apply(this, arguments);
2540 const getDefaultValue = Object.getOwnPropertyDescriptor(
2541 HTMLInputElement.prototype,
2542 'defaultValue',
2543 ).get;
2544 const setDefaultValue = Object.getOwnPropertyDescriptor(
2545 HTMLInputElement.prototype,
2546 'defaultValue',
2547 ).set;
2548 const getValue = Object.getOwnPropertyDescriptor(
2549 HTMLInputElement.prototype,
2550 'value',
2551 ).get;
2552 const setValue = Object.getOwnPropertyDescriptor(
2553 HTMLInputElement.prototype,
2554 'value',
2555 ).set;
2556 const getType = Object.getOwnPropertyDescriptor(
2557 HTMLInputElement.prototype,
2558 'type',
2559 ).get;
2560 const setType = Object.getOwnPropertyDescriptor(
2561 HTMLInputElement.prototype,
2562 'type',
2563 ).set;
2564 if (type === 'input') {
2565 Object.defineProperty(el, 'defaultValue', {
2566 get: function () {
2567 return getDefaultValue.call(this);
2568 },
2569 set: function (val) {
2570 log.push(`node.defaultValue = ${strify(val)}`);
2571 setDefaultValue.call(this, val);
2572 },
2573 });
2574 Object.defineProperty(el, 'value', {
2575 get: function () {
2576 return getValue.call(this);
2577 },
2578 set: function (val) {
2579 log.push(`node.value = ${strify(val)}`);
2580 setValue.call(this, val);
2581 },
2582 });
2583 Object.defineProperty(el, 'type', {
2584 get: function () {
2585 return getType.call(this);
2586 },
2587 set: function (val) {
2588 log.push(`node.type = ${strify(val)}`);
2589 setType.call(this, val);
2590 },
2591 });
2592 spyOnDevAndProd(el, 'setAttribute').mockImplementation(
2593 function (name, val) {
2594 log.push(`node.setAttribute(${strify(name)}, ${strify(val)})`);
2595 },
2596 );
2597 }
2598 return el;
2599 },
2600 );
2601
2602 await act(() => {
2603 root.render(<input type="date" defaultValue="1980-01-01" />);
2604 });
2605
2606 if (disableInputAttributeSyncing) {
2607 expect(log).toEqual([
2608 'node.type = "date"',
2609 'node.defaultValue = "1980-01-01"',
2610 // TODO: it's possible this reintroduces the bug because we don't assign `value` at all.
2611 // Need to check this on mobile Safari and Chrome.
2612 ]);
2613 } else {
2614 expect(log).toEqual([
2615 'node.type = "date"',
2616 // value must be assigned before defaultValue. This fixes an issue where the
2617 // visually displayed value of date inputs disappears on mobile Safari and Chrome:
2618 // https://github.com/facebook/react/issues/7233
2619 'node.value = "1980-01-01"',
2620 'node.defaultValue = "1980-01-01"',
2621 ]);
2622 }
2623 });
2624
2625 describe('assigning the value attribute on controlled inputs', function () {
2626 function getTestInput() {
2627 return class extends React.Component {
2628 state = {
2629 value: this.props.value == null ? '' : this.props.value,
2630 };
2631 onChange = event => {
2632 this.setState({value: event.target.value});
2633 };
2634 render() {
2635 const type = this.props.type;
2636 const value = this.state.value;
2637
2638 return <input type={type} value={value} onChange={this.onChange} />;
2639 }
2640 };
2641 }
2642
2643 it('always sets the attribute when values change on text inputs', async () => {
2644 const Input = getTestInput();
2645 await act(() => {
2646 root.render(<Input type="text" />);
2647 });
2648 const node = container.firstChild;
2649 expect(isValueDirty(node)).toBe(false);
2650
2651 setUntrackedValue.call(node, '2');
2652 await act(() => {
2653 dispatchEventOnNode(node, 'input');
2654 });
2655
2656 expect(isValueDirty(node)).toBe(true);
2657 if (disableInputAttributeSyncing) {
2658 expect(node.hasAttribute('value')).toBe(false);
2659 } else {
2660 expect(node.getAttribute('value')).toBe('2');
2661 }
2662 });
2663
2664 it('sets the value attribute on number inputs even when focused', async () => {
2665 const Input = getTestInput();
2666 await act(() => {
2667 root.render(<Input type="number" value="1" />);
2668 });
2669 const node = container.firstChild;
2670 expect(isValueDirty(node)).toBe(true);
2671
2672 node.focus();
2673
2674 setUntrackedValue.call(node, '2');
2675 dispatchEventOnNode(node, 'input');
2676
2677 expect(isValueDirty(node)).toBe(true);
2678 if (disableInputAttributeSyncing) {
2679 expect(node.hasAttribute('value')).toBe(false);
2680 } else {
2681 expect(node.getAttribute('value')).toBe('2');
2682 }
2683 });
2684
2685 it('sets the value attribute on number inputs on blur', async () => {
2686 const Input = getTestInput();
2687 await act(() => {
2688 root.render(<Input type="number" value="1" />);
2689 });
2690 const node = container.firstChild;
2691 expect(isValueDirty(node)).toBe(true);
2692
2693 node.focus();
2694 setUntrackedValue.call(node, '2');
2695 dispatchEventOnNode(node, 'input');
2696 node.blur();
2697
2698 expect(isValueDirty(node)).toBe(true);
2699 if (disableInputAttributeSyncing) {
2700 expect(node.value).toBe('2');
2701 expect(node.hasAttribute('value')).toBe(false);
2702 } else {
2703 expect(node.value).toBe('2');
2704 expect(node.getAttribute('value')).toBe('2');
2705 }
2706 });
2707
2708 it('an uncontrolled number input will not update the value attribute on blur', async () => {
2709 await act(() => {
2710 root.render(<input type="number" defaultValue="1" />);
2711 });
2712 const node = container.firstChild;
2713 if (disableInputAttributeSyncing) {
2714 expect(isValueDirty(node)).toBe(false);
2715 } else {
2716 expect(isValueDirty(node)).toBe(true);
2717 }
2718
2719 node.focus();
2720 setUntrackedValue.call(node, 4);
2721 dispatchEventOnNode(node, 'input');
2722 node.blur();
2723
2724 expect(isValueDirty(node)).toBe(true);
2725 expect(node.getAttribute('value')).toBe('1');
2726 });
2727
2728 it('an uncontrolled text input will not update the value attribute on blur', async () => {
2729 await act(() => {
2730 root.render(<input type="text" defaultValue="1" />);
2731 });
2732 const node = container.firstChild;
2733 if (disableInputAttributeSyncing) {
2734 expect(isValueDirty(node)).toBe(false);
2735 } else {
2736 expect(isValueDirty(node)).toBe(true);
2737 }
2738
2739 node.focus();
2740 setUntrackedValue.call(node, 4);
2741 dispatchEventOnNode(node, 'input');
2742 node.blur();
2743
2744 expect(isValueDirty(node)).toBe(true);
2745 expect(node.getAttribute('value')).toBe('1');
2746 });
2747 });
2748
2749 describe('setting a controlled input to undefined', () => {
2750 let input;
2751
2752 async function renderInputWithStringThenWithUndefined() {
2753 let setValueToUndefined;
2754 class Input extends React.Component {
2755 constructor() {
2756 super();
2757 setValueToUndefined = () => this.setState({value: undefined});
2758 }
2759 state = {value: 'first'};
2760 render() {
2761 return (
2762 <input
2763 onChange={e => this.setState({value: e.target.value})}
2764 value={this.state.value}
2765 />
2766 );
2767 }
2768 }
2769
2770 await act(() => {
2771 root.render(<Input />);
2772 });
2773 input = container.firstChild;
2774 setUntrackedValue.call(input, 'latest');
2775 dispatchEventOnNode(input, 'input');
2776 await act(() => {
2777 setValueToUndefined();
2778 });
2779 }
2780
2781 it('reverts the value attribute to the initial value', async () => {
2782 await renderInputWithStringThenWithUndefined();
2783 assertConsoleErrorDev([
2784 'A component is changing a controlled input to be uncontrolled. ' +
2785 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
2786 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
2787 'More info: https://react.dev/link/controlled-components\n' +
2788 ' in input (at **)\n' +
2789 ' in Input (at **)',
2790 ]);
2791 if (disableInputAttributeSyncing) {
2792 expect(input.getAttribute('value')).toBe(null);
2793 } else {
2794 expect(input.getAttribute('value')).toBe('latest');
2795 }
2796 });
2797
2798 it('preserves the value property', async () => {
2799 await renderInputWithStringThenWithUndefined();
2800 assertConsoleErrorDev([
2801 'A component is changing a controlled input to be uncontrolled. ' +
2802 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
2803 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
2804 'More info: https://react.dev/link/controlled-components\n' +
2805 ' in input (at **)\n' +
2806 ' in Input (at **)',
2807 ]);
2808 expect(input.value).toBe('latest');
2809 });
2810 });
2811
2812 describe('setting a controlled input to null', () => {
2813 let input;
2814
2815 async function renderInputWithStringThenWithNull() {
2816 let setValueToNull;
2817 class Input extends React.Component {
2818 constructor() {
2819 super();
2820 setValueToNull = () => this.setState({value: null});
2821 }
2822 state = {value: 'first'};
2823 render() {
2824 return (
2825 <input
2826 onChange={e => this.setState({value: e.target.value})}
2827 value={this.state.value}
2828 />
2829 );
2830 }
2831 }
2832
2833 await act(() => {
2834 root.render(<Input />);
2835 });
2836 input = container.firstChild;
2837 setUntrackedValue.call(input, 'latest');
2838 dispatchEventOnNode(input, 'input');
2839 await act(() => {
2840 setValueToNull();
2841 });
2842 }
2843
2844 it('reverts the value attribute to the initial value', async () => {
2845 await renderInputWithStringThenWithNull();
2846 assertConsoleErrorDev([
2847 '`value` prop on `input` should not be null. ' +
2848 'Consider using an empty string to clear the component ' +
2849 'or `undefined` for uncontrolled components.\n' +
2850 ' in input (at **)\n' +
2851 ' in Input (at **)',
2852 'A component is changing a controlled input to be uncontrolled. ' +
2853 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
2854 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
2855 'More info: https://react.dev/link/controlled-components\n' +
2856 ' in input (at **)\n' +
2857 ' in Input (at **)',
2858 ]);
2859 if (disableInputAttributeSyncing) {
2860 expect(input.getAttribute('value')).toBe(null);
2861 } else {
2862 expect(input.getAttribute('value')).toBe('latest');
2863 }
2864 });
2865
2866 it('preserves the value property', async () => {
2867 await renderInputWithStringThenWithNull();
2868 assertConsoleErrorDev([
2869 '`value` prop on `input` should not be null. ' +
2870 'Consider using an empty string to clear the component ' +
2871 'or `undefined` for uncontrolled components.\n' +
2872 ' in input (at **)\n' +
2873 ' in Input (at **)',
2874 'A component is changing a controlled input to be uncontrolled. ' +
2875 'This is likely caused by the value changing from a defined to undefined, which should not happen. ' +
2876 'Decide between using a controlled or uncontrolled input element for the lifetime of the component. ' +
2877 'More info: https://react.dev/link/controlled-components\n' +
2878 ' in input (at **)\n' +
2879 ' in Input (at **)',
2880 ]);
2881 expect(input.value).toBe('latest');
2882 });
2883 });
2884
2885 describe('When given a Symbol value', function () {
2886 it('treats initial Symbol value as an empty string', async () => {
2887 await act(() => {
2888 root.render(<input value={Symbol('foobar')} onChange={() => {}} />);
2889 });
2890 assertConsoleErrorDev([
2891 'Invalid value for prop `value` on <input> tag. ' +
2892 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
2893 'For details, see https://react.dev/link/attribute-behavior \n' +
2894 ' in input (at **)',
2895 ]);
2896 const node = container.firstChild;
2897
2898 expect(node.value).toBe('');
2899 if (disableInputAttributeSyncing) {
2900 expect(node.hasAttribute('value')).toBe(false);
2901 } else {
2902 expect(node.getAttribute('value')).toBe('');
2903 }
2904 });
2905
2906 it('treats updated Symbol value as an empty string', async () => {
2907 await act(() => {
2908 root.render(<input value="foo" onChange={() => {}} />);
2909 });
2910 await act(() => {
2911 root.render(<input value={Symbol('foobar')} onChange={() => {}} />);
2912 });
2913 assertConsoleErrorDev([
2914 'Invalid value for prop `value` on <input> tag. ' +
2915 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
2916 'For details, see https://react.dev/link/attribute-behavior \n' +
2917 ' in input (at **)',
2918 ]);
2919 const node = container.firstChild;
2920
2921 expect(node.value).toBe('');
2922 if (disableInputAttributeSyncing) {
2923 expect(node.hasAttribute('value')).toBe(false);
2924 } else {
2925 expect(node.getAttribute('value')).toBe('');
2926 }
2927 });
2928
2929 it('treats initial Symbol defaultValue as an empty string', async () => {
2930 await act(() => {
2931 root.render(<input defaultValue={Symbol('foobar')} />);
2932 });
2933 const node = container.firstChild;
2934
2935 expect(node.value).toBe('');
2936 expect(node.getAttribute('value')).toBe('');
2937 // TODO: we should warn here.
2938 });
2939
2940 it('treats updated Symbol defaultValue as an empty string', async () => {
2941 await act(() => {
2942 root.render(<input defaultValue="foo" />);
2943 });
2944 await act(() => {
2945 root.render(<input defaultValue={Symbol('foobar')} />);
2946 });
2947 const node = container.firstChild;
2948
2949 if (disableInputAttributeSyncing) {
2950 expect(node.value).toBe('');
2951 } else {
2952 expect(node.value).toBe('foo');
2953 }
2954 expect(node.getAttribute('value')).toBe('');
2955 // TODO: we should warn here.
2956 });
2957 });
2958
2959 describe('When given a function value', function () {
2960 it('treats initial function value as an empty string', async () => {
2961 await act(() => {
2962 root.render(<input value={() => {}} onChange={() => {}} />);
2963 });
2964 assertConsoleErrorDev([
2965 'Invalid value for prop `value` on <input> tag. ' +
2966 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
2967 'For details, see https://react.dev/link/attribute-behavior \n' +
2968 ' in input (at **)',
2969 ]);
2970 const node = container.firstChild;
2971
2972 expect(node.value).toBe('');
2973 if (disableInputAttributeSyncing) {
2974 expect(node.hasAttribute('value')).toBe(false);
2975 } else {
2976 expect(node.getAttribute('value')).toBe('');
2977 }
2978 });
2979
2980 it('treats updated function value as an empty string', async () => {
2981 await act(() => {
2982 root.render(<input value="foo" onChange={() => {}} />);
2983 });
2984 await act(() => {
2985 root.render(<input value={() => {}} onChange={() => {}} />);
2986 });
2987 assertConsoleErrorDev([
2988 'Invalid value for prop `value` on <input> tag. ' +
2989 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
2990 'For details, see https://react.dev/link/attribute-behavior \n' +
2991 ' in input (at **)',
2992 ]);
2993 const node = container.firstChild;
2994
2995 expect(node.value).toBe('');
2996 if (disableInputAttributeSyncing) {
2997 expect(node.hasAttribute('value')).toBe(false);
2998 } else {
2999 expect(node.getAttribute('value')).toBe('');
3000 }
3001 });
3002
3003 it('treats initial function defaultValue as an empty string', async () => {
3004 await act(() => {
3005 root.render(<input defaultValue={() => {}} />);
3006 });
3007 const node = container.firstChild;
3008
3009 expect(node.value).toBe('');
3010 expect(node.getAttribute('value')).toBe('');
3011 // TODO: we should warn here.
3012 });
3013
3014 it('treats updated function defaultValue as an empty string', async () => {
3015 await act(() => {
3016 root.render(<input defaultValue="foo" />);
3017 });
3018 await act(() => {
3019 root.render(<input defaultValue={() => {}} />);
3020 });
3021 const node = container.firstChild;
3022
3023 if (disableInputAttributeSyncing) {
3024 expect(node.value).toBe('');
3025 expect(node.getAttribute('value')).toBe('');
3026 } else {
3027 expect(node.value).toBe('foo');
3028 expect(node.getAttribute('value')).toBe('');
3029 }
3030 // TODO: we should warn here.
3031 });
3032 });
3033
3034 describe('checked inputs without a value property', function () {
3035 // In absence of a value, radio and checkboxes report a value of "on".
3036 // Between 16 and 16.2, we assigned a node's value to it's current
3037 // value in order to "dettach" it from defaultValue. This had the unfortunate
3038 // side-effect of assigning value="on" to radio and checkboxes
3039 it('does not add "on" in absence of value on a checkbox', async () => {
3040 await act(() => {
3041 root.render(<input type="checkbox" defaultChecked={true} />);
3042 });
3043 const node = container.firstChild;
3044
3045 expect(node.value).toBe('on');
3046 expect(node.hasAttribute('value')).toBe(false);
3047 });
3048
3049 it('does not add "on" in absence of value on a radio', async () => {
3050 await act(() => {
3051 root.render(<input type="radio" defaultChecked={true} />);
3052 });
3053 const node = container.firstChild;
3054
3055 expect(node.value).toBe('on');
3056 expect(node.hasAttribute('value')).toBe(false);
3057 });
3058 });
3059
3060 it('should remove previous `defaultValue`', async () => {
3061 await act(() => {
3062 root.render(<input type="text" defaultValue="0" />);
3063 });
3064 const node = container.firstChild;
3065
3066 expect(node.value).toBe('0');
3067 expect(node.defaultValue).toBe('0');
3068
3069 await act(() => {
3070 root.render(<input type="text" />);
3071 });
3072 expect(node.defaultValue).toBe('');
3073 });
3074
3075 it('should treat `defaultValue={null}` as missing', async () => {
3076 await act(() => {
3077 root.render(<input type="text" defaultValue="0" />);
3078 });
3079 const node = container.firstChild;
3080
3081 expect(node.value).toBe('0');
3082 expect(node.defaultValue).toBe('0');
3083
3084 await act(() => {
3085 root.render(<input type="text" defaultValue={null} />);
3086 });
3087 expect(node.defaultValue).toBe('');
3088 });
3089
3090 it('should notice input changes when reverting back to original value', async () => {
3091 const log = [];
3092 function onChange(e) {
3093 log.push(e.target.value);
3094 }
3095 await act(() => {
3096 root.render(<input type="text" value="" onChange={onChange} />);
3097 });
3098 await act(() => {
3099 root.render(<input type="text" value="a" onChange={onChange} />);
3100 });
3101
3102 const node = container.firstChild;
3103 setUntrackedValue.call(node, '');
3104 dispatchEventOnNode(node, 'input');
3105
3106 expect(log).toEqual(['']);
3107 expect(node.value).toBe('a');
3108 });
3109 });