main
js 1,808 lines 59.2 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 // Fix JSDOM. setAttribute is supposed to throw on things that can't be implicitly toStringed.
13 const setAttribute = Element.prototype.setAttribute;
14 Element.prototype.setAttribute = function (name, value) {
15 // eslint-disable-next-line react-internal/safe-string-coercion
16 return setAttribute.call(this, name, '' + value);
17 };
18
19 describe('ReactDOMSelect', () => {
20 let React;
21 let ReactDOM;
22 let ReactDOMClient;
23 let ReactDOMServer;
24 let act;
25 let assertConsoleErrorDev;
26
27 const noop = function () {};
28
29 beforeEach(() => {
30 jest.resetModules();
31 React = require('react');
32 ReactDOM = require('react-dom');
33 ReactDOMClient = require('react-dom/client');
34 ReactDOMServer = require('react-dom/server');
35 act = require('internal-test-utils').act;
36 assertConsoleErrorDev =
37 require('internal-test-utils').assertConsoleErrorDev;
38 });
39
40 it('should allow setting `defaultValue`', async () => {
41 const stub = (
42 <select defaultValue="giraffe">
43 <option value="monkey">A monkey!</option>
44 <option value="giraffe">A giraffe!</option>
45 <option value="gorilla">A gorilla!</option>
46 </select>
47 );
48 const options = stub.props.children;
49 const container = document.createElement('div');
50 const root = ReactDOMClient.createRoot(container);
51 await act(() => {
52 root.render(stub);
53 });
54
55 const node = container.firstChild;
56
57 expect(node.value).toBe('giraffe');
58
59 await act(() => {
60 root.render(<select defaultValue="gorilla">{options}</select>);
61 });
62
63 expect(node.value).toEqual('giraffe');
64 });
65
66 it('should not throw with `defaultValue` and without children', () => {
67 const stub = <select defaultValue="dummy" />;
68
69 expect(async () => {
70 const container = document.createElement('div');
71 const root = ReactDOMClient.createRoot(container);
72
73 await act(() => {
74 root.render(stub);
75 });
76 }).not.toThrow();
77 });
78
79 it('should not control when using `defaultValue`', async () => {
80 const el = (
81 <select defaultValue="giraffe">
82 <option value="monkey">A monkey!</option>
83 <option value="giraffe">A giraffe!</option>
84 <option value="gorilla">A gorilla!</option>
85 </select>
86 );
87 const container = document.createElement('div');
88 const root = ReactDOMClient.createRoot(container);
89
90 await act(() => {
91 root.render(el);
92 });
93
94 const node = container.firstChild;
95
96 expect(node.value).toBe('giraffe');
97
98 node.value = 'monkey';
99 await act(() => {
100 root.render(el);
101 });
102
103 // Uncontrolled selects shouldn't change the value after first mounting
104 expect(node.value).toEqual('monkey');
105 });
106
107 it('should allow setting `defaultValue` with multiple', async () => {
108 const stub = (
109 <select multiple={true} defaultValue={['giraffe', 'gorilla']}>
110 <option value="monkey">A monkey!</option>
111 <option value="giraffe">A giraffe!</option>
112 <option value="gorilla">A gorilla!</option>
113 </select>
114 );
115 const options = stub.props.children;
116 const container = document.createElement('div');
117 const root = ReactDOMClient.createRoot(container);
118
119 await act(() => {
120 root.render(stub);
121 });
122
123 const node = container.firstChild;
124
125 expect(node.options[0].selected).toBe(false); // monkey
126 expect(node.options[1].selected).toBe(true); // giraffe
127 expect(node.options[2].selected).toBe(true); // gorilla
128
129 await act(() => {
130 root.render(
131 <select multiple={true} defaultValue={['monkey']}>
132 {options}
133 </select>,
134 );
135 });
136
137 expect(node.options[0].selected).toBe(false); // monkey
138 expect(node.options[1].selected).toBe(true); // giraffe
139 expect(node.options[2].selected).toBe(true); // gorilla
140 });
141
142 it('should allow setting `value`', async () => {
143 const stub = (
144 <select value="giraffe" onChange={noop}>
145 <option value="monkey">A monkey!</option>
146 <option value="giraffe">A giraffe!</option>
147 <option value="gorilla">A gorilla!</option>
148 </select>
149 );
150 const options = stub.props.children;
151 const container = document.createElement('div');
152 const root = ReactDOMClient.createRoot(container);
153
154 await act(() => {
155 root.render(stub);
156 });
157
158 const node = container.firstChild;
159
160 expect(node.value).toBe('giraffe');
161
162 await act(() => {
163 root.render(
164 <select value="gorilla" onChange={noop}>
165 {options}
166 </select>,
167 );
168 });
169
170 expect(node.value).toEqual('gorilla');
171 });
172
173 it('should default to the first non-disabled option', async () => {
174 const stub = (
175 <select defaultValue="">
176 <option disabled={true}>Disabled</option>
177 <option disabled={true}>Still Disabled</option>
178 <option>0</option>
179 <option disabled={true}>Also Disabled</option>
180 </select>
181 );
182 const container = document.createElement('div');
183 const root = ReactDOMClient.createRoot(container);
184
185 await act(() => {
186 root.render(stub);
187 });
188
189 const node = container.firstChild;
190 expect(node.options[0].selected).toBe(false);
191 expect(node.options[2].selected).toBe(true);
192 });
193
194 it('should allow setting `value` to __proto__', async () => {
195 const stub = (
196 <select value="__proto__" onChange={noop}>
197 <option value="monkey">A monkey!</option>
198 <option value="__proto__">A giraffe!</option>
199 <option value="gorilla">A gorilla!</option>
200 </select>
201 );
202 const options = stub.props.children;
203 const container = document.createElement('div');
204 const root = ReactDOMClient.createRoot(container);
205 await act(() => {
206 root.render(stub);
207 });
208
209 const node = container.firstChild;
210
211 expect(node.value).toBe('__proto__');
212
213 await act(() => {
214 root.render(
215 <select value="gorilla" onChange={noop}>
216 {options}
217 </select>,
218 );
219 });
220
221 expect(node.value).toEqual('gorilla');
222 });
223
224 it('should not throw with `value` and without children', () => {
225 const stub = <select value="dummy" onChange={noop} />;
226
227 expect(async () => {
228 const container = document.createElement('div');
229 const root = ReactDOMClient.createRoot(container);
230
231 await act(() => {
232 root.render(stub);
233 });
234 }).not.toThrow();
235 });
236
237 it('should allow setting `value` with multiple', async () => {
238 const stub = (
239 <select multiple={true} value={['giraffe', 'gorilla']} onChange={noop}>
240 <option value="monkey">A monkey!</option>
241 <option value="giraffe">A giraffe!</option>
242 <option value="gorilla">A gorilla!</option>
243 </select>
244 );
245 const options = stub.props.children;
246 const container = document.createElement('div');
247 const root = ReactDOMClient.createRoot(container);
248
249 await act(() => {
250 root.render(stub);
251 });
252
253 const node = container.firstChild;
254
255 expect(node.options[0].selected).toBe(false); // monkey
256 expect(node.options[1].selected).toBe(true); // giraffe
257 expect(node.options[2].selected).toBe(true); // gorilla
258
259 await act(() => {
260 root.render(
261 <select multiple={true} value={['monkey']} onChange={noop}>
262 {options}
263 </select>,
264 );
265 });
266
267 expect(node.options[0].selected).toBe(true); // monkey
268 expect(node.options[1].selected).toBe(false); // giraffe
269 expect(node.options[2].selected).toBe(false); // gorilla
270 });
271
272 it('should allow setting `value` to __proto__ with multiple', async () => {
273 const stub = (
274 <select multiple={true} value={['__proto__', 'gorilla']} onChange={noop}>
275 <option value="monkey">A monkey!</option>
276 <option value="__proto__">A __proto__!</option>
277 <option value="gorilla">A gorilla!</option>
278 </select>
279 );
280 const options = stub.props.children;
281 const container = document.createElement('div');
282 const root = ReactDOMClient.createRoot(container);
283 await act(() => {
284 root.render(stub);
285 });
286
287 const node = container.firstChild;
288
289 expect(node.options[0].selected).toBe(false); // monkey
290 expect(node.options[1].selected).toBe(true); // __proto__
291 expect(node.options[2].selected).toBe(true); // gorilla
292
293 await act(() => {
294 root.render(
295 <select multiple={true} value={['monkey']} onChange={noop}>
296 {options}
297 </select>,
298 );
299 });
300
301 expect(node.options[0].selected).toBe(true); // monkey
302 expect(node.options[1].selected).toBe(false); // __proto__
303 expect(node.options[2].selected).toBe(false); // gorilla
304 });
305
306 it('should not select other options automatically', async () => {
307 const stub = (
308 <select multiple={true} value={['12']} onChange={noop}>
309 <option value="1">one</option>
310 <option value="2">two</option>
311 <option value="12">twelve</option>
312 </select>
313 );
314 const container = document.createElement('div');
315 const root = ReactDOMClient.createRoot(container);
316
317 await act(() => {
318 root.render(stub);
319 });
320
321 const node = container.firstChild;
322
323 expect(node.options[0].selected).toBe(false); // one
324 expect(node.options[1].selected).toBe(false); // two
325 expect(node.options[2].selected).toBe(true); // twelve
326 });
327
328 it('should reset child options selected when they are changed and `value` is set', async () => {
329 const stub = <select multiple={true} value={['a', 'b']} onChange={noop} />;
330 const container = document.createElement('div');
331 const root = ReactDOMClient.createRoot(container);
332 await act(() => {
333 root.render(stub);
334 });
335
336 const node = container.firstChild;
337
338 await act(() => {
339 root.render(
340 <select multiple={true} value={['a', 'b']} onChange={noop}>
341 <option value="a">a</option>
342 <option value="b">b</option>
343 <option value="c">c</option>
344 </select>,
345 );
346 });
347
348 expect(node.options[0].selected).toBe(true); // a
349 expect(node.options[1].selected).toBe(true); // b
350 expect(node.options[2].selected).toBe(false); // c
351 });
352
353 it('should allow setting `value` with `objectToString`', async () => {
354 const objectToString = {
355 animal: 'giraffe',
356 toString: function () {
357 return this.animal;
358 },
359 };
360
361 const el = (
362 <select multiple={true} value={[objectToString]} onChange={noop}>
363 <option value="monkey">A monkey!</option>
364 <option value="giraffe">A giraffe!</option>
365 <option value="gorilla">A gorilla!</option>
366 </select>
367 );
368 const container = document.createElement('div');
369 const root = ReactDOMClient.createRoot(container);
370 await act(() => {
371 root.render(el);
372 });
373
374 const node = container.firstChild;
375
376 expect(node.options[0].selected).toBe(false); // monkey
377 expect(node.options[1].selected).toBe(true); // giraffe
378 expect(node.options[2].selected).toBe(false); // gorilla
379
380 // Changing the `value` prop should change the selected options.
381 objectToString.animal = 'monkey';
382
383 const el2 = (
384 <select multiple={true} value={[objectToString]}>
385 <option value="monkey">A monkey!</option>
386 <option value="giraffe">A giraffe!</option>
387 <option value="gorilla">A gorilla!</option>
388 </select>
389 );
390
391 await act(() => {
392 root.render(el2);
393 });
394
395 expect(node.options[0].selected).toBe(true); // monkey
396 expect(node.options[1].selected).toBe(false); // giraffe
397 expect(node.options[2].selected).toBe(false); // gorilla
398 });
399
400 it('should allow switching to multiple', async () => {
401 const stub = (
402 <select defaultValue="giraffe">
403 <option value="monkey">A monkey!</option>
404 <option value="giraffe">A giraffe!</option>
405 <option value="gorilla">A gorilla!</option>
406 </select>
407 );
408 const options = stub.props.children;
409 const container = document.createElement('div');
410 const root = ReactDOMClient.createRoot(container);
411 await act(() => {
412 root.render(stub);
413 });
414
415 const node = container.firstChild;
416
417 expect(node.options[0].selected).toBe(false); // monkey
418 expect(node.options[1].selected).toBe(true); // giraffe
419 expect(node.options[2].selected).toBe(false); // gorilla
420
421 await act(() => {
422 root.render(
423 <select multiple={true} defaultValue={['giraffe', 'gorilla']}>
424 {options}
425 </select>,
426 );
427 });
428
429 expect(node.options[0].selected).toBe(false); // monkey
430 expect(node.options[1].selected).toBe(true); // giraffe
431 expect(node.options[2].selected).toBe(true); // gorilla
432 });
433
434 it('should allow switching from multiple', async () => {
435 const stub = (
436 <select multiple={true} defaultValue={['giraffe', 'gorilla']}>
437 <option value="monkey">A monkey!</option>
438 <option value="giraffe">A giraffe!</option>
439 <option value="gorilla">A gorilla!</option>
440 </select>
441 );
442 const options = stub.props.children;
443 const container = document.createElement('div');
444 const root = ReactDOMClient.createRoot(container);
445 await act(() => {
446 root.render(stub);
447 });
448
449 const node = container.firstChild;
450
451 expect(node.options[0].selected).toBe(false); // monkey
452 expect(node.options[1].selected).toBe(true); // giraffe
453 expect(node.options[2].selected).toBe(true); // gorilla
454
455 await act(() => {
456 root.render(<select defaultValue="gorilla">{options}</select>);
457 });
458
459 expect(node.options[0].selected).toBe(false); // monkey
460 expect(node.options[1].selected).toBe(false); // giraffe
461 expect(node.options[2].selected).toBe(true); // gorilla
462 });
463
464 it('does not select an item when size is initially set to greater than 1', async () => {
465 const stub = (
466 <select size="2">
467 <option value="monkey">A monkey!</option>
468 <option value="giraffe">A giraffe!</option>
469 <option value="gorilla">A gorilla!</option>
470 </select>
471 );
472 const container = document.createElement('div');
473 const root = ReactDOMClient.createRoot(container);
474
475 await act(() => {
476 root.render(stub);
477 });
478
479 const select = container.firstChild;
480
481 expect(select.options[0].selected).toBe(false);
482 expect(select.options[1].selected).toBe(false);
483 expect(select.options[2].selected).toBe(false);
484
485 expect(select.value).toBe('');
486 expect(select.selectedIndex).toBe(-1);
487 });
488
489 it('should remember value when switching to uncontrolled', async () => {
490 const stub = (
491 <select value={'giraffe'} onChange={noop}>
492 <option value="monkey">A monkey!</option>
493 <option value="giraffe">A giraffe!</option>
494 <option value="gorilla">A gorilla!</option>
495 </select>
496 );
497 const options = stub.props.children;
498 const container = document.createElement('div');
499 const root = ReactDOMClient.createRoot(container);
500 await act(() => {
501 root.render(stub);
502 });
503
504 const node = container.firstChild;
505
506 expect(node.options[0].selected).toBe(false); // monkey
507 expect(node.options[1].selected).toBe(true); // giraffe
508 expect(node.options[2].selected).toBe(false); // gorilla
509
510 await act(() => {
511 root.render(<select>{options}</select>);
512 });
513
514 expect(node.options[0].selected).toBe(false); // monkey
515 expect(node.options[1].selected).toBe(true); // giraffe
516 expect(node.options[2].selected).toBe(false); // gorilla
517 });
518
519 it('should remember updated value when switching to uncontrolled', async () => {
520 const stub = (
521 <select value={'giraffe'} onChange={noop}>
522 <option value="monkey">A monkey!</option>
523 <option value="giraffe">A giraffe!</option>
524 <option value="gorilla">A gorilla!</option>
525 </select>
526 );
527 const options = stub.props.children;
528 const container = document.createElement('div');
529 const root = ReactDOMClient.createRoot(container);
530 await act(() => {
531 root.render(stub);
532 });
533
534 const node = container.firstChild;
535 await act(() => {
536 root.render(
537 <select value="gorilla" onChange={noop}>
538 {options}
539 </select>,
540 );
541 });
542
543 expect(node.options[0].selected).toBe(false); // monkey
544 expect(node.options[1].selected).toBe(false); // giraffe
545 expect(node.options[2].selected).toBe(true); // gorilla
546
547 await act(() => {
548 root.render(<select>{options}</select>);
549 });
550
551 expect(node.options[0].selected).toBe(false); // monkey
552 expect(node.options[1].selected).toBe(false); // giraffe
553 expect(node.options[2].selected).toBe(true); // gorilla
554 });
555
556 it('should support server-side rendering', () => {
557 const stub = (
558 <select value="giraffe" onChange={noop}>
559 <option value="monkey">A monkey!</option>
560 <option value="giraffe">A giraffe!</option>
561 <option value="gorilla">A gorilla!</option>
562 </select>
563 );
564 const container = document.createElement('div');
565 container.innerHTML = ReactDOMServer.renderToString(stub);
566 const options = container.firstChild.options;
567 expect(options[0].value).toBe('monkey');
568 expect(options[0].selected).toBe(false);
569 expect(options[1].value).toBe('giraffe');
570 expect(options[1].selected).toBe(true);
571 expect(options[2].value).toBe('gorilla');
572 expect(options[2].selected).toBe(false);
573 });
574
575 it('should support server-side rendering with defaultValue', () => {
576 const stub = (
577 <select defaultValue="giraffe">
578 <option value="monkey">A monkey!</option>
579 <option value="giraffe">A giraffe!</option>
580 <option value="gorilla">A gorilla!</option>
581 </select>
582 );
583 const container = document.createElement('div');
584 container.innerHTML = ReactDOMServer.renderToString(stub);
585 const options = container.firstChild.options;
586 expect(options[0].value).toBe('monkey');
587 expect(options[0].selected).toBe(false);
588 expect(options[1].value).toBe('giraffe');
589 expect(options[1].selected).toBe(true);
590 expect(options[2].value).toBe('gorilla');
591 expect(options[2].selected).toBe(false);
592 });
593
594 it('should support server-side rendering with dangerouslySetInnerHTML', () => {
595 const stub = (
596 <select defaultValue="giraffe">
597 <option
598 value="monkey"
599 dangerouslySetInnerHTML={{
600 __html: 'A monkey!',
601 }}>
602 {undefined}
603 </option>
604 <option
605 value="giraffe"
606 dangerouslySetInnerHTML={{
607 __html: 'A giraffe!',
608 }}>
609 {null}
610 </option>
611 <option
612 value="gorilla"
613 dangerouslySetInnerHTML={{
614 __html: 'A gorilla!',
615 }}
616 />
617 </select>
618 );
619 const container = document.createElement('div');
620 container.innerHTML = ReactDOMServer.renderToString(stub);
621 const options = container.firstChild.options;
622 expect(options[0].value).toBe('monkey');
623 expect(options[0].selected).toBe(false);
624 expect(options[1].value).toBe('giraffe');
625 expect(options[1].selected).toBe(true);
626 expect(options[2].value).toBe('gorilla');
627 expect(options[2].selected).toBe(false);
628 });
629
630 it('should support server-side rendering with multiple', () => {
631 const stub = (
632 <select multiple={true} value={['giraffe', 'gorilla']} onChange={noop}>
633 <option value="monkey">A monkey!</option>
634 <option value="giraffe">A giraffe!</option>
635 <option value="gorilla">A gorilla!</option>
636 </select>
637 );
638 const container = document.createElement('div');
639 container.innerHTML = ReactDOMServer.renderToString(stub);
640 const options = container.firstChild.options;
641 expect(options[0].value).toBe('monkey');
642 expect(options[0].selected).toBe(false);
643 expect(options[1].value).toBe('giraffe');
644 expect(options[1].selected).toBe(true);
645 expect(options[2].value).toBe('gorilla');
646 expect(options[2].selected).toBe(true);
647 });
648
649 it('should not control defaultValue if re-adding options', async () => {
650 const container = document.createElement('div');
651
652 const root = ReactDOMClient.createRoot(container);
653
654 await act(() => {
655 root.render(
656 <select multiple={true} defaultValue={['giraffe']}>
657 <option key="monkey" value="monkey">
658 A monkey!
659 </option>
660 <option key="giraffe" value="giraffe">
661 A giraffe!
662 </option>
663 <option key="gorilla" value="gorilla">
664 A gorilla!
665 </option>
666 </select>,
667 );
668 });
669
670 const node = container.firstChild;
671
672 expect(node.options[0].selected).toBe(false); // monkey
673 expect(node.options[1].selected).toBe(true); // giraffe
674 expect(node.options[2].selected).toBe(false); // gorilla
675
676 await act(() => {
677 root.render(
678 <select multiple={true} defaultValue={['giraffe']}>
679 <option key="monkey" value="monkey">
680 A monkey!
681 </option>
682 <option key="gorilla" value="gorilla">
683 A gorilla!
684 </option>
685 </select>,
686 );
687 });
688
689 expect(node.options[0].selected).toBe(false); // monkey
690 expect(node.options[1].selected).toBe(false); // gorilla
691
692 await act(() => {
693 root.render(
694 <select multiple={true} defaultValue={['giraffe']}>
695 <option key="monkey" value="monkey">
696 A monkey!
697 </option>
698 <option key="giraffe" value="giraffe">
699 A giraffe!
700 </option>
701 <option key="gorilla" value="gorilla">
702 A gorilla!
703 </option>
704 </select>,
705 );
706 });
707
708 expect(node.options[0].selected).toBe(false); // monkey
709 expect(node.options[1].selected).toBe(false); // giraffe
710 expect(node.options[2].selected).toBe(false); // gorilla
711 });
712
713 it('should support options with dynamic children', async () => {
714 const container = document.createElement('div');
715
716 let node;
717
718 function App({value}) {
719 return (
720 <select value={value} ref={n => (node = n)} onChange={noop}>
721 <option key="monkey" value="monkey">
722 A monkey {value === 'monkey' ? 'is chosen' : null}!
723 </option>
724 <option key="giraffe" value="giraffe">
725 A giraffe {value === 'giraffe' && 'is chosen'}!
726 </option>
727 <option key="gorilla" value="gorilla">
728 A gorilla {value === 'gorilla' && 'is chosen'}!
729 </option>
730 </select>
731 );
732 }
733
734 const root = ReactDOMClient.createRoot(container);
735 await act(() => {
736 root.render(<App value="monkey" />);
737 });
738
739 expect(node.options[0].selected).toBe(true); // monkey
740 expect(node.options[1].selected).toBe(false); // giraffe
741 expect(node.options[2].selected).toBe(false); // gorilla
742
743 await act(() => {
744 root.render(<App value="giraffe" />);
745 });
746
747 expect(node.options[0].selected).toBe(false); // monkey
748 expect(node.options[1].selected).toBe(true); // giraffe
749 expect(node.options[2].selected).toBe(false); // gorilla
750 });
751
752 it('should warn if value is null', async () => {
753 const container = document.createElement('div');
754 const root = ReactDOMClient.createRoot(container);
755 await act(() => {
756 root.render(
757 <select value={null}>
758 <option value="test" />
759 </select>,
760 );
761 });
762 assertConsoleErrorDev([
763 '`value` prop on `select` should not be null. ' +
764 'Consider using an empty string to clear the component or `undefined` ' +
765 'for uncontrolled components.\n' +
766 ' in select (at **)',
767 ]);
768
769 await act(() => {
770 root.render(
771 <select value={null}>
772 <option value="test" />
773 </select>,
774 );
775 });
776 });
777
778 it('should warn if selected is set on <option>', async () => {
779 function App() {
780 return (
781 <select>
782 <option selected={true} />
783 <option selected={true} />
784 </select>
785 );
786 }
787
788 const container = document.createElement('div');
789 const root = ReactDOMClient.createRoot(container);
790 await act(() => {
791 root.render(<App />);
792 });
793 assertConsoleErrorDev([
794 'Use the `defaultValue` or `value` props on <select> instead of ' +
795 'setting `selected` on <option>.\n' +
796 ' in option (at **)\n' +
797 ' in App (at **)',
798 ]);
799
800 await act(() => {
801 root.render(<App />);
802 });
803 });
804
805 it('should warn if value is null and multiple is true', async () => {
806 const container = document.createElement('div');
807 const root = ReactDOMClient.createRoot(container);
808 await act(() => {
809 root.render(
810 <select value={null} multiple={true}>
811 <option value="test" />
812 </select>,
813 );
814 });
815 assertConsoleErrorDev([
816 '`value` prop on `select` should not be null. ' +
817 'Consider using an empty array when `multiple` is ' +
818 'set to `true` to clear the component or `undefined` ' +
819 'for uncontrolled components.\n' +
820 ' in select (at **)',
821 ]);
822
823 await act(() => {
824 root.render(
825 <select value={null} multiple={true}>
826 <option value="test" />
827 </select>,
828 );
829 });
830 });
831
832 it('should refresh state on change', async () => {
833 const stub = (
834 <select value="giraffe" onChange={noop}>
835 <option value="monkey">A monkey!</option>
836 <option value="giraffe">A giraffe!</option>
837 <option value="gorilla">A gorilla!</option>
838 </select>
839 );
840 const container = document.createElement('div');
841 document.body.appendChild(container);
842
843 try {
844 const root = ReactDOMClient.createRoot(container);
845
846 await act(() => {
847 root.render(stub);
848 });
849
850 const node = container.firstChild;
851
852 await act(() => {
853 node.dispatchEvent(
854 new Event('change', {bubbles: true, cancelable: false}),
855 );
856 });
857
858 expect(node.value).toBe('giraffe');
859 } finally {
860 document.body.removeChild(container);
861 }
862 });
863
864 it('should warn if value and defaultValue props are specified', async () => {
865 const container = document.createElement('div');
866 const root = ReactDOMClient.createRoot(container);
867 await act(() => {
868 root.render(
869 <select value="giraffe" defaultValue="giraffe" readOnly={true}>
870 <option value="monkey">A monkey!</option>
871 <option value="giraffe">A giraffe!</option>
872 <option value="gorilla">A gorilla!</option>
873 </select>,
874 );
875 });
876 assertConsoleErrorDev([
877 'Select elements must be either controlled or uncontrolled ' +
878 '(specify either the value prop, or the defaultValue prop, but not ' +
879 'both). Decide between using a controlled or uncontrolled select ' +
880 'element and remove one of these props. More info: ' +
881 'https://react.dev/link/controlled-components\n' +
882 ' in select (at **)',
883 ]);
884
885 await act(() => {
886 root.render(
887 <select value="giraffe" defaultValue="giraffe" readOnly={true}>
888 <option value="monkey">A monkey!</option>
889 <option value="giraffe">A giraffe!</option>
890 <option value="gorilla">A gorilla!</option>
891 </select>,
892 );
893 });
894 });
895
896 it('should not warn about missing onChange in uncontrolled textareas', async () => {
897 const container = document.createElement('div');
898 let root = ReactDOMClient.createRoot(container);
899
900 await act(() => {
901 root.render(<select />);
902 });
903
904 await act(() => {
905 root.unmount();
906 });
907 root = ReactDOMClient.createRoot(container);
908
909 await act(() => {
910 root.render(<select value={undefined} />);
911 });
912 });
913
914 it('should be able to safely remove select onChange', async () => {
915 const container = document.createElement('div');
916 const root = ReactDOMClient.createRoot(container);
917 async function changeView() {
918 root.unmount();
919 }
920
921 const stub = (
922 <select value="giraffe" onChange={changeView}>
923 <option value="monkey">A monkey!</option>
924 <option value="giraffe">A giraffe!</option>
925 <option value="gorilla">A gorilla!</option>
926 </select>
927 );
928
929 await act(() => {
930 root.render(stub);
931 });
932
933 const node = container.firstChild;
934
935 await expect(
936 act(() => {
937 node.dispatchEvent(
938 new Event('change', {bubbles: true, cancelable: false}),
939 );
940 }),
941 ).resolves.not.toThrow();
942
943 expect(container.firstChild).toBe(null);
944 });
945
946 it('should select grandchild options nested inside an optgroup', async () => {
947 const stub = (
948 <select value="b" onChange={noop}>
949 <optgroup label="group">
950 <option value="a">a</option>
951 <option value="b">b</option>
952 <option value="c">c</option>
953 </optgroup>
954 </select>
955 );
956 const container = document.createElement('div');
957 const root = ReactDOMClient.createRoot(container);
958
959 await act(() => {
960 root.render(stub);
961 });
962
963 const node = container.firstChild;
964
965 expect(node.options[0].selected).toBe(false); // a
966 expect(node.options[1].selected).toBe(true); // b
967 expect(node.options[2].selected).toBe(false); // c
968 });
969
970 // @gate !disableLegacyMode
971 it('should allow controlling `value` in a nested legacy render', async () => {
972 let selectNode;
973
974 class Parent extends React.Component {
975 state = {
976 value: 'giraffe',
977 };
978
979 componentDidMount() {
980 this._renderNested();
981 }
982
983 componentDidUpdate() {
984 this._renderNested();
985 }
986
987 _handleChange(event) {
988 this.setState({value: event.target.value});
989 }
990
991 _renderNested() {
992 ReactDOM.render(
993 <select
994 onChange={this._handleChange.bind(this)}
995 ref={n => (selectNode = n)}
996 value={this.state.value}>
997 <option value="monkey">A monkey!</option>
998 <option value="giraffe">A giraffe!</option>
999 <option value="gorilla">A gorilla!</option>
1000 </select>,
1001 this._nestingContainer,
1002 );
1003 }
1004
1005 render() {
1006 return <div ref={n => (this._nestingContainer = n)} />;
1007 }
1008 }
1009
1010 const container = document.createElement('div');
1011
1012 document.body.appendChild(container);
1013
1014 ReactDOM.render(<Parent />, container);
1015
1016 expect(selectNode.value).toBe('giraffe');
1017
1018 selectNode.value = 'gorilla';
1019
1020 let nativeEvent = document.createEvent('Event');
1021 nativeEvent.initEvent('input', true, true);
1022 selectNode.dispatchEvent(nativeEvent);
1023
1024 expect(selectNode.value).toEqual('gorilla');
1025
1026 nativeEvent = document.createEvent('Event');
1027 nativeEvent.initEvent('change', true, true);
1028 selectNode.dispatchEvent(nativeEvent);
1029
1030 expect(selectNode.value).toEqual('gorilla');
1031
1032 document.body.removeChild(container);
1033 });
1034
1035 it('should not select first option by default when multiple is set and no defaultValue is set', async () => {
1036 const stub = (
1037 <select multiple={true} onChange={noop}>
1038 <option value="a">a</option>
1039 <option value="b">b</option>
1040 <option value="c">c</option>
1041 </select>
1042 );
1043 const container = document.createElement('div');
1044 const root = ReactDOMClient.createRoot(container);
1045
1046 await act(() => {
1047 root.render(stub);
1048 });
1049
1050 const node = container.firstChild;
1051
1052 expect(node.options[0].selected).toBe(false); // a
1053 expect(node.options[1].selected).toBe(false); // b
1054 expect(node.options[2].selected).toBe(false); // c
1055 });
1056
1057 describe('When given a Symbol value', () => {
1058 it('treats initial Symbol value as missing', async () => {
1059 const container = document.createElement('div');
1060 const root = ReactDOMClient.createRoot(container);
1061 await act(() => {
1062 root.render(
1063 <select onChange={noop} value={Symbol('foobar')}>
1064 <option value={Symbol('foobar')}>A Symbol!</option>
1065 <option value="monkey">A monkey!</option>
1066 <option value="giraffe">A giraffe!</option>
1067 </select>,
1068 );
1069 });
1070 assertConsoleErrorDev([
1071 'Invalid value for prop `value` on <option> tag. ' +
1072 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1073 'For details, see https://react.dev/link/attribute-behavior \n' +
1074 ' in option (at **)',
1075 ]);
1076
1077 const node = container.firstChild;
1078 expect(node.value).toBe('A Symbol!');
1079 });
1080
1081 it('treats updated Symbol value as missing', async () => {
1082 const container = document.createElement('div');
1083 const root = ReactDOMClient.createRoot(container);
1084 await act(() => {
1085 root.render(
1086 <select onChange={noop} value="monkey">
1087 <option value={Symbol('foobar')}>A Symbol!</option>
1088 <option value="monkey">A monkey!</option>
1089 <option value="giraffe">A giraffe!</option>
1090 </select>,
1091 );
1092 });
1093 assertConsoleErrorDev([
1094 'Invalid value for prop `value` on <option> tag. ' +
1095 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1096 'For details, see https://react.dev/link/attribute-behavior \n' +
1097 ' in option (at **)',
1098 ]);
1099
1100 let node = container.firstChild;
1101 expect(node.value).toBe('monkey');
1102
1103 await act(() => {
1104 root.render(
1105 <select onChange={noop} value={Symbol('foobar')}>
1106 <option value={Symbol('foobar')}>A Symbol!</option>
1107 <option value="monkey">A monkey!</option>
1108 <option value="giraffe">A giraffe!</option>
1109 </select>,
1110 );
1111 });
1112
1113 node = container.firstChild;
1114
1115 expect(node.value).toBe('A Symbol!');
1116 });
1117
1118 it('treats initial Symbol defaultValue as an empty string', async () => {
1119 const container = document.createElement('div');
1120 const root = ReactDOMClient.createRoot(container);
1121 await act(() => {
1122 root.render(
1123 <select defaultValue={Symbol('foobar')}>
1124 <option value={Symbol('foobar')}>A Symbol!</option>
1125 <option value="monkey">A monkey!</option>
1126 <option value="giraffe">A giraffe!</option>
1127 </select>,
1128 );
1129 });
1130 assertConsoleErrorDev([
1131 'Invalid value for prop `value` on <option> tag. ' +
1132 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1133 'For details, see https://react.dev/link/attribute-behavior \n' +
1134 ' in option (at **)',
1135 ]);
1136
1137 const node = container.firstChild;
1138 expect(node.value).toBe('A Symbol!');
1139 });
1140
1141 it('treats updated Symbol defaultValue as an empty string', async () => {
1142 let container = document.createElement('div');
1143 let root = ReactDOMClient.createRoot(container);
1144 await act(() => {
1145 root.render(
1146 <select defaultValue="monkey">
1147 <option value={Symbol('foobar')}>A Symbol!</option>
1148 <option value="monkey">A monkey!</option>
1149 <option value="giraffe">A giraffe!</option>
1150 </select>,
1151 );
1152 });
1153 assertConsoleErrorDev([
1154 'Invalid value for prop `value` on <option> tag. ' +
1155 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1156 'For details, see https://react.dev/link/attribute-behavior \n' +
1157 ' in option (at **)',
1158 ]);
1159
1160 let node = container.firstChild;
1161 expect(node.value).toBe('monkey');
1162
1163 container = document.createElement('div');
1164 root = ReactDOMClient.createRoot(container);
1165 await act(() => {
1166 root.render(
1167 <select defaultValue={Symbol('foobar')}>
1168 <option value={Symbol('foobar')}>A Symbol!</option>
1169 <option value="monkey">A monkey!</option>
1170 <option value="giraffe">A giraffe!</option>
1171 </select>,
1172 );
1173 });
1174
1175 node = container.firstChild;
1176 expect(node.value).toBe('A Symbol!');
1177 });
1178 });
1179
1180 describe('When given a function value', () => {
1181 it('treats initial function value as missing', async () => {
1182 const container = document.createElement('div');
1183 const root = ReactDOMClient.createRoot(container);
1184 await act(() => {
1185 root.render(
1186 <select onChange={noop} value={() => {}}>
1187 <option value={() => {}}>A function!</option>
1188 <option value="monkey">A monkey!</option>
1189 <option value="giraffe">A giraffe!</option>
1190 </select>,
1191 );
1192 });
1193 assertConsoleErrorDev([
1194 'Invalid value for prop `value` on <option> tag. ' +
1195 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1196 'For details, see https://react.dev/link/attribute-behavior \n' +
1197 ' in option (at **)',
1198 ]);
1199
1200 const node = container.firstChild;
1201 expect(node.value).toBe('A function!');
1202 });
1203
1204 it('treats initial function defaultValue as an empty string', async () => {
1205 const container = document.createElement('div');
1206 const root = ReactDOMClient.createRoot(container);
1207 await act(() => {
1208 root.render(
1209 <select defaultValue={() => {}}>
1210 <option value={() => {}}>A function!</option>
1211 <option value="monkey">A monkey!</option>
1212 <option value="giraffe">A giraffe!</option>
1213 </select>,
1214 );
1215 });
1216 assertConsoleErrorDev([
1217 'Invalid value for prop `value` on <option> tag. ' +
1218 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1219 'For details, see https://react.dev/link/attribute-behavior \n' +
1220 ' in option (at **)',
1221 ]);
1222
1223 const node = container.firstChild;
1224 expect(node.value).toBe('A function!');
1225 });
1226
1227 it('treats updated function value as an empty string', async () => {
1228 const container = document.createElement('div');
1229 const root = ReactDOMClient.createRoot(container);
1230 await act(() => {
1231 root.render(
1232 <select onChange={noop} value="monkey">
1233 <option value={() => {}}>A function!</option>
1234 <option value="monkey">A monkey!</option>
1235 <option value="giraffe">A giraffe!</option>
1236 </select>,
1237 );
1238 });
1239 assertConsoleErrorDev([
1240 'Invalid value for prop `value` on <option> tag. ' +
1241 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1242 'For details, see https://react.dev/link/attribute-behavior \n' +
1243 ' in option (at **)',
1244 ]);
1245
1246 let node = container.firstChild;
1247 expect(node.value).toBe('monkey');
1248
1249 await act(() => {
1250 root.render(
1251 <select onChange={noop} value={() => {}}>
1252 <option value={() => {}}>A function!</option>
1253 <option value="monkey">A monkey!</option>
1254 <option value="giraffe">A giraffe!</option>
1255 </select>,
1256 );
1257 });
1258
1259 node = container.firstChild;
1260 expect(node.value).toBe('A function!');
1261 });
1262
1263 it('treats updated function defaultValue as an empty string', async () => {
1264 let container = document.createElement('div');
1265 let root = ReactDOMClient.createRoot(container);
1266 await act(() => {
1267 root.render(
1268 <select defaultValue="monkey">
1269 <option value={() => {}}>A function!</option>
1270 <option value="monkey">A monkey!</option>
1271 <option value="giraffe">A giraffe!</option>
1272 </select>,
1273 );
1274 });
1275 assertConsoleErrorDev([
1276 'Invalid value for prop `value` on <option> tag. ' +
1277 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
1278 'For details, see https://react.dev/link/attribute-behavior \n' +
1279 ' in option (at **)',
1280 ]);
1281
1282 let node = container.firstChild;
1283 expect(node.value).toBe('monkey');
1284
1285 container = document.createElement('div');
1286 root = ReactDOMClient.createRoot(container);
1287 await act(() => {
1288 root.render(
1289 <select defaultValue={() => {}}>
1290 <option value={() => {}}>A function!</option>
1291 <option value="monkey">A monkey!</option>
1292 <option value="giraffe">A giraffe!</option>
1293 </select>,
1294 );
1295 });
1296
1297 node = container.firstChild;
1298
1299 expect(node.value).toBe('A function!');
1300 });
1301 });
1302
1303 describe('When given a Temporal.PlainDate-like value', () => {
1304 class TemporalLike {
1305 valueOf() {
1306 // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
1307 // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
1308 throw new TypeError('prod message');
1309 }
1310 toString() {
1311 return '2020-01-01';
1312 }
1313 }
1314
1315 it('throws when given a Temporal.PlainDate-like value (select)', async () => {
1316 const container = document.createElement('div');
1317 const root = ReactDOMClient.createRoot(container);
1318 await expect(
1319 act(() => {
1320 root.render(
1321 <select onChange={noop} value={new TemporalLike()}>
1322 <option value="2020-01-01">like a Temporal.PlainDate</option>
1323 <option value="monkey">A monkey!</option>
1324 <option value="giraffe">A giraffe!</option>
1325 </select>,
1326 );
1327 }),
1328 ).rejects.toThrow(new TypeError('prod message'));
1329 assertConsoleErrorDev([
1330 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1331 ' must be strings, not TemporalLike. ' +
1332 'This value must be coerced to a string before using it here.\n' +
1333 ' in select (at **)',
1334 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1335 ' must be strings, not TemporalLike. ' +
1336 'This value must be coerced to a string before using it here.\n' +
1337 ' in select (at **)',
1338 ]);
1339 });
1340
1341 it('throws when given a Temporal.PlainDate-like value (option)', async () => {
1342 const container = document.createElement('div');
1343 const root = ReactDOMClient.createRoot(container);
1344 await expect(
1345 act(() => {
1346 root.render(
1347 <select onChange={noop} value="2020-01-01">
1348 <option value={new TemporalLike()}>
1349 like a Temporal.PlainDate
1350 </option>
1351 <option value="monkey">A monkey!</option>
1352 <option value="giraffe">A giraffe!</option>
1353 </select>,
1354 );
1355 }),
1356 ).rejects.toThrow(new TypeError('prod message'));
1357 assertConsoleErrorDev([
1358 'The provided `value` attribute is an unsupported type TemporalLike.' +
1359 ' This value must be coerced to a string before using it here.\n' +
1360 ' in option (at **)',
1361 'The provided `value` attribute is an unsupported type TemporalLike.' +
1362 ' This value must be coerced to a string before using it here.\n' +
1363 ' in option (at **)',
1364 ]);
1365 });
1366
1367 it('throws when given a Temporal.PlainDate-like value (both)', async () => {
1368 const container = document.createElement('div');
1369 const root = ReactDOMClient.createRoot(container);
1370
1371 await expect(
1372 act(() => {
1373 root.render(
1374 <select onChange={noop} value={new TemporalLike()}>
1375 <option value={new TemporalLike()}>
1376 like a Temporal.PlainDate
1377 </option>
1378 <option value="monkey">A monkey!</option>
1379 <option value="giraffe">A giraffe!</option>
1380 </select>,
1381 );
1382 }),
1383 ).rejects.toThrow(new TypeError('prod message'));
1384 assertConsoleErrorDev([
1385 'The provided `value` attribute is an unsupported type TemporalLike.' +
1386 ' This value must be coerced to a string before using it here.\n' +
1387 ' in option (at **)',
1388 'The provided `value` attribute is an unsupported type TemporalLike.' +
1389 ' This value must be coerced to a string before using it here.\n' +
1390 ' in option (at **)',
1391 ]);
1392 });
1393
1394 it('throws with updated Temporal.PlainDate-like value (select)', async () => {
1395 const container = document.createElement('div');
1396 const root = ReactDOMClient.createRoot(container);
1397
1398 await act(() => {
1399 root.render(
1400 <select onChange={noop} value="monkey">
1401 <option value="2020-01-01">like a Temporal.PlainDate</option>
1402 <option value="monkey">A monkey!</option>
1403 <option value="giraffe">A giraffe!</option>
1404 </select>,
1405 );
1406 });
1407
1408 await expect(
1409 act(() => {
1410 root.render(
1411 <select onChange={noop} value={new TemporalLike()}>
1412 <option value="2020-01-01">like a Temporal.PlainDate</option>
1413 <option value="monkey">A monkey!</option>
1414 <option value="giraffe">A giraffe!</option>
1415 </select>,
1416 );
1417 }),
1418 ).rejects.toThrow(new TypeError('prod message'));
1419 assertConsoleErrorDev([
1420 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1421 ' must be strings, not TemporalLike. ' +
1422 'This value must be coerced to a string before using it here.\n' +
1423 ' in select (at **)',
1424 ]);
1425 });
1426
1427 it('throws with updated Temporal.PlainDate-like value (option)', async () => {
1428 const container = document.createElement('div');
1429 const root = ReactDOMClient.createRoot(container);
1430
1431 await act(() => {
1432 root.render(
1433 <select onChange={noop} value="2020-01-01">
1434 <option value="donkey">like a Temporal.PlainDate</option>
1435 <option value="monkey">A monkey!</option>
1436 <option value="giraffe">A giraffe!</option>
1437 </select>,
1438 );
1439 });
1440
1441 await expect(
1442 act(() => {
1443 root.render(
1444 <select onChange={noop} value="2020-01-01">
1445 <option value={new TemporalLike()}>
1446 like a Temporal.PlainDate
1447 </option>
1448 <option value="monkey">A monkey!</option>
1449 <option value="giraffe">A giraffe!</option>
1450 </select>,
1451 );
1452 }),
1453 ).rejects.toThrow(new TypeError('prod message'));
1454 assertConsoleErrorDev([
1455 'The provided `value` attribute is an unsupported type TemporalLike.' +
1456 ' This value must be coerced to a string before using it here.\n' +
1457 ' in option (at **)',
1458 ]);
1459 });
1460
1461 it('throws with updated Temporal.PlainDate-like value (both)', async () => {
1462 const container = document.createElement('div');
1463 const root = ReactDOMClient.createRoot(container);
1464
1465 await act(() => {
1466 root.render(
1467 <select onChange={noop} value="donkey">
1468 <option value="donkey">like a Temporal.PlainDate</option>
1469 <option value="monkey">A monkey!</option>
1470 <option value="giraffe">A giraffe!</option>
1471 </select>,
1472 );
1473 });
1474
1475 await expect(
1476 act(() => {
1477 root.render(
1478 <select onChange={noop} value={new TemporalLike()}>
1479 <option value={new TemporalLike()}>
1480 like a Temporal.PlainDate
1481 </option>
1482 <option value="monkey">A monkey!</option>
1483 <option value="giraffe">A giraffe!</option>
1484 </select>,
1485 );
1486 }),
1487 ).rejects.toThrow(
1488 new AggregateError([
1489 new TypeError('prod message'),
1490 new TypeError('prod message'),
1491 ]),
1492 );
1493 assertConsoleErrorDev([
1494 'The provided `value` attribute is an unsupported type TemporalLike.' +
1495 ' This value must be coerced to a string before using it here.\n' +
1496 ' in option (at **)',
1497 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1498 ' must be strings, not TemporalLike. ' +
1499 'This value must be coerced to a string before using it here.\n' +
1500 ' in select (at **)',
1501 ]);
1502 });
1503
1504 it('throws when given a Temporal.PlainDate-like defaultValue (select)', async () => {
1505 const container = document.createElement('div');
1506 const root = ReactDOMClient.createRoot(container);
1507 await expect(
1508 act(() => {
1509 root.render(
1510 <select onChange={noop} defaultValue={new TemporalLike()}>
1511 <option value="2020-01-01">like a Temporal.PlainDate</option>
1512 <option value="monkey">A monkey!</option>
1513 <option value="giraffe">A giraffe!</option>
1514 </select>,
1515 );
1516 }),
1517 ).rejects.toThrow(new TypeError('prod message'));
1518 assertConsoleErrorDev([
1519 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1520 ' must be strings, not TemporalLike. ' +
1521 'This value must be coerced to a string before using it here.\n' +
1522 ' in select (at **)',
1523 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1524 ' must be strings, not TemporalLike. ' +
1525 'This value must be coerced to a string before using it here.\n' +
1526 ' in select (at **)',
1527 ]);
1528 });
1529
1530 it('throws when given a Temporal.PlainDate-like defaultValue (option)', async () => {
1531 const container = document.createElement('div');
1532 const root = ReactDOMClient.createRoot(container);
1533
1534 await expect(
1535 act(() => {
1536 root.render(
1537 <select onChange={noop} defaultValue="2020-01-01">
1538 <option value={new TemporalLike()}>
1539 like a Temporal.PlainDate
1540 </option>
1541 <option value="monkey">A monkey!</option>
1542 <option value="giraffe">A giraffe!</option>
1543 </select>,
1544 );
1545 }),
1546 ).rejects.toThrow(new TypeError('prod message'));
1547 assertConsoleErrorDev([
1548 'The provided `value` attribute is an unsupported type TemporalLike.' +
1549 ' This value must be coerced to a string before using it here.\n' +
1550 ' in option (at **)',
1551 'The provided `value` attribute is an unsupported type TemporalLike.' +
1552 ' This value must be coerced to a string before using it here.\n' +
1553 ' in option (at **)',
1554 ]);
1555 });
1556
1557 it('throws when given a Temporal.PlainDate-like defaultValue (both)', async () => {
1558 const container = document.createElement('div');
1559 const root = ReactDOMClient.createRoot(container);
1560 await expect(
1561 act(() => {
1562 root.render(
1563 <select onChange={noop} defaultValue={new TemporalLike()}>
1564 <option value={new TemporalLike()}>
1565 like a Temporal.PlainDate
1566 </option>
1567 <option value="monkey">A monkey!</option>
1568 <option value="giraffe">A giraffe!</option>
1569 </select>,
1570 );
1571 }),
1572 ).rejects.toThrow(new TypeError('prod message'));
1573 assertConsoleErrorDev([
1574 'The provided `value` attribute is an unsupported type TemporalLike.' +
1575 ' This value must be coerced to a string before using it here.\n' +
1576 ' in option (at **)',
1577 'The provided `value` attribute is an unsupported type TemporalLike.' +
1578 ' This value must be coerced to a string before using it here.\n' +
1579 ' in option (at **)',
1580 ]);
1581 });
1582
1583 it('throws with updated Temporal.PlainDate-like defaultValue (select)', async () => {
1584 let container = document.createElement('div');
1585 let root = ReactDOMClient.createRoot(container);
1586 await act(() => {
1587 root.render(
1588 <select onChange={noop} defaultValue="monkey">
1589 <option value="2020-01-01">like a Temporal.PlainDate</option>
1590 <option value="monkey">A monkey!</option>
1591 <option value="giraffe">A giraffe!</option>
1592 </select>,
1593 );
1594 });
1595
1596 container = document.createElement('div');
1597 root = ReactDOMClient.createRoot(container);
1598 await expect(
1599 act(() => {
1600 root.render(
1601 <select onChange={noop} defaultValue={new TemporalLike()}>
1602 <option value="2020-01-01">like a Temporal.PlainDate</option>
1603 <option value="monkey">A monkey!</option>
1604 <option value="giraffe">A giraffe!</option>
1605 </select>,
1606 );
1607 }),
1608 ).rejects.toThrow(new TypeError('prod message'));
1609 assertConsoleErrorDev([
1610 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1611 ' must be strings, not TemporalLike. ' +
1612 'This value must be coerced to a string before using it here.\n' +
1613 ' in select (at **)',
1614 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
1615 ' must be strings, not TemporalLike. ' +
1616 'This value must be coerced to a string before using it here.\n' +
1617 ' in select (at **)',
1618 ]);
1619 });
1620
1621 it('throws with updated Temporal.PlainDate-like defaultValue (both)', async () => {
1622 let container = document.createElement('div');
1623 let root = ReactDOMClient.createRoot(container);
1624
1625 await act(() => {
1626 root.render(
1627 <select onChange={noop} defaultValue="monkey">
1628 <option value="donkey">like a Temporal.PlainDate</option>
1629 <option value="monkey">A monkey!</option>
1630 <option value="giraffe">A giraffe!</option>
1631 </select>,
1632 );
1633 });
1634
1635 container = document.createElement('div');
1636 root = ReactDOMClient.createRoot(container);
1637 await expect(
1638 act(() => {
1639 root.render(
1640 <select onChange={noop} value={new TemporalLike()}>
1641 <option value={new TemporalLike()}>
1642 like a Temporal.PlainDate
1643 </option>
1644 <option value="monkey">A monkey!</option>
1645 <option value="giraffe">A giraffe!</option>
1646 </select>,
1647 );
1648 }),
1649 ).rejects.toThrow(new TypeError('prod message'));
1650 assertConsoleErrorDev([
1651 'The provided `value` attribute is an unsupported type TemporalLike.' +
1652 ' This value must be coerced to a string before using it here.\n' +
1653 ' in option (at **)',
1654 'The provided `value` attribute is an unsupported type TemporalLike.' +
1655 ' This value must be coerced to a string before using it here.\n' +
1656 ' in option (at **)',
1657 ]);
1658 });
1659
1660 it('should not warn about missing onChange if value is not set', async () => {
1661 const container = document.createElement('div');
1662 const root = ReactDOMClient.createRoot(container);
1663 await expect(
1664 act(() => {
1665 root.render(
1666 <select>
1667 <option value="monkey">A monkey!</option>
1668 <option value="giraffe">A giraffe!</option>
1669 <option value="gorilla">A gorilla!</option>
1670 </select>,
1671 );
1672 }),
1673 ).resolves.not.toThrow();
1674 });
1675
1676 it('should not throw an error about missing onChange if value is undefined', async () => {
1677 const container = document.createElement('div');
1678 const root = ReactDOMClient.createRoot(container);
1679 await expect(
1680 act(() => {
1681 root.render(
1682 <select value={undefined}>
1683 <option value="monkey">A monkey!</option>
1684 <option value="giraffe">A giraffe!</option>
1685 <option value="gorilla">A gorilla!</option>
1686 </select>,
1687 );
1688 }),
1689 ).resolves.not.toThrow();
1690 });
1691
1692 it('should not warn about missing onChange if onChange is set', async () => {
1693 const change = jest.fn();
1694 const container = document.createElement('div');
1695 const root = ReactDOMClient.createRoot(container);
1696 await expect(
1697 act(() => {
1698 root.render(
1699 <select value="monkey" onChange={change}>
1700 <option value="monkey">A monkey!</option>
1701 <option value="giraffe">A giraffe!</option>
1702 <option value="gorilla">A gorilla!</option>
1703 </select>,
1704 );
1705 }),
1706 ).resolves.not.toThrow();
1707 });
1708
1709 it('should not warn about missing onChange if disabled is true', async () => {
1710 const container = document.createElement('div');
1711 const root = ReactDOMClient.createRoot(container);
1712 await expect(
1713 act(() => {
1714 root.render(
1715 <select value="monkey" disabled={true}>
1716 <option value="monkey">A monkey!</option>
1717 <option value="giraffe">A giraffe!</option>
1718 <option value="gorilla">A gorilla!</option>
1719 </select>,
1720 );
1721 }),
1722 ).resolves.not.toThrow();
1723 });
1724
1725 it('should warn about missing onChange if value is false', async () => {
1726 const container = document.createElement('div');
1727 const root = ReactDOMClient.createRoot(container);
1728 await act(() => {
1729 root.render(
1730 <select value={false}>
1731 <option value="monkey">A monkey!</option>
1732 <option value="giraffe">A giraffe!</option>
1733 <option value="gorilla">A gorilla!</option>
1734 </select>,
1735 );
1736 });
1737 assertConsoleErrorDev([
1738 'You provided a `value` prop to a form ' +
1739 'field without an `onChange` handler. This will render a read-only ' +
1740 'field. If the field should be mutable use `defaultValue`. ' +
1741 'Otherwise, set `onChange`.\n in select (at **)',
1742 ]);
1743 });
1744
1745 it('should warn about missing onChange if value is 0', async () => {
1746 const container = document.createElement('div');
1747 const root = ReactDOMClient.createRoot(container);
1748 await act(() => {
1749 root.render(
1750 <select value={0}>
1751 <option value="monkey">A monkey!</option>
1752 <option value="giraffe">A giraffe!</option>
1753 <option value="gorilla">A gorilla!</option>
1754 </select>,
1755 );
1756 });
1757 assertConsoleErrorDev([
1758 'You provided a `value` prop to a form ' +
1759 'field without an `onChange` handler. This will render a read-only ' +
1760 'field. If the field should be mutable use `defaultValue`. ' +
1761 'Otherwise, set `onChange`.\n' +
1762 ' in select (at **)',
1763 ]);
1764 });
1765
1766 it('should warn about missing onChange if value is "0"', async () => {
1767 const container = document.createElement('div');
1768 const root = ReactDOMClient.createRoot(container);
1769 await act(() => {
1770 root.render(
1771 <select value="0">
1772 <option value="monkey">A monkey!</option>
1773 <option value="giraffe">A giraffe!</option>
1774 <option value="gorilla">A gorilla!</option>
1775 </select>,
1776 );
1777 });
1778 assertConsoleErrorDev([
1779 'You provided a `value` prop to a form ' +
1780 'field without an `onChange` handler. This will render a read-only ' +
1781 'field. If the field should be mutable use `defaultValue`. ' +
1782 'Otherwise, set `onChange`.\n' +
1783 ' in select (at **)',
1784 ]);
1785 });
1786
1787 it('should warn about missing onChange if value is ""', async () => {
1788 const container = document.createElement('div');
1789 const root = ReactDOMClient.createRoot(container);
1790 await act(() => {
1791 root.render(
1792 <select value="">
1793 <option value="monkey">A monkey!</option>
1794 <option value="giraffe">A giraffe!</option>
1795 <option value="gorilla">A gorilla!</option>
1796 </select>,
1797 );
1798 });
1799 assertConsoleErrorDev([
1800 'You provided a `value` prop to a form ' +
1801 'field without an `onChange` handler. This will render a read-only ' +
1802 'field. If the field should be mutable use `defaultValue`. ' +
1803 'Otherwise, set `onChange`.\n' +
1804 ' in select (at **)',
1805 ]);
1806 });
1807 });
1808 });