main
js 1,134 lines 34.6 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 function emptyFunction() {}
13
14 describe('ReactDOMTextarea', () => {
15 let React;
16 let ReactDOMClient;
17 let ReactDOMServer;
18 let act;
19 let assertConsoleErrorDev;
20
21 let renderTextarea;
22
23 const ReactFeatureFlags = require('shared/ReactFeatureFlags');
24
25 beforeEach(() => {
26 jest.resetModules();
27
28 React = require('react');
29 ReactDOMClient = require('react-dom/client');
30 ReactDOMServer = require('react-dom/server');
31 act = require('internal-test-utils').act;
32 assertConsoleErrorDev =
33 require('internal-test-utils').assertConsoleErrorDev;
34
35 renderTextarea = async function (component, container, root) {
36 await act(() => {
37 root.render(component);
38 });
39
40 const node = container.firstChild;
41
42 // Fixing jsdom's quirky behavior -- in reality, the parser should strip
43 // off the leading newline but we need to do it by hand here.
44 node.defaultValue = node.innerHTML.replace(/^\n/, '');
45 return node;
46 };
47 });
48
49 afterEach(() => {
50 jest.restoreAllMocks();
51 });
52
53 it('should allow setting `defaultValue`', async () => {
54 const container = document.createElement('div');
55 const root = ReactDOMClient.createRoot(container);
56 const node = await renderTextarea(
57 <textarea defaultValue="giraffe" />,
58 container,
59 root,
60 );
61
62 expect(node.value).toBe('giraffe');
63
64 // Changing `defaultValue` should do nothing.
65 await renderTextarea(<textarea defaultValue="gorilla" />, container, root);
66 expect(node.value).toEqual('giraffe');
67
68 node.value = 'cat';
69
70 await renderTextarea(<textarea defaultValue="monkey" />, container, root);
71 expect(node.value).toEqual('cat');
72 });
73
74 it('should display `defaultValue` of number 0', async () => {
75 const container = document.createElement('div');
76 const root = ReactDOMClient.createRoot(container);
77 const node = await renderTextarea(
78 <textarea defaultValue={0} />,
79 container,
80 root,
81 );
82
83 expect(node.value).toBe('0');
84 });
85
86 it('should display `defaultValue` of bigint 0', async () => {
87 const container = document.createElement('div');
88 const root = ReactDOMClient.createRoot(container);
89 const node = await renderTextarea(
90 <textarea defaultValue={0n} />,
91 container,
92 root,
93 );
94
95 expect(node.value).toBe('0');
96 });
97
98 it('should display "false" for `defaultValue` of `false`', async () => {
99 const container = document.createElement('div');
100 const root = ReactDOMClient.createRoot(container);
101 const node = await renderTextarea(
102 <textarea defaultValue={false} />,
103 container,
104 root,
105 );
106
107 expect(node.value).toBe('false');
108 });
109
110 it('should display "foobar" for `defaultValue` of `objToString`', async () => {
111 const container = document.createElement('div');
112 const root = ReactDOMClient.createRoot(container);
113 const objToString = {
114 toString: function () {
115 return 'foobar';
116 },
117 };
118 const node = await renderTextarea(
119 <textarea defaultValue={objToString} />,
120 container,
121 root,
122 );
123
124 expect(node.value).toBe('foobar');
125 });
126
127 it('should set defaultValue', async () => {
128 const container = document.createElement('div');
129 const root = ReactDOMClient.createRoot(container);
130 await act(() => {
131 root.render(<textarea defaultValue="foo" />);
132 });
133 await act(() => {
134 root.render(<textarea defaultValue="bar" />);
135 });
136 await act(() => {
137 root.render(<textarea defaultValue="noise" />);
138 });
139
140 expect(container.firstChild.defaultValue).toBe('noise');
141 });
142
143 it('should not render value as an attribute', async () => {
144 const container = document.createElement('div');
145 const root = ReactDOMClient.createRoot(container);
146 const node = await renderTextarea(
147 <textarea value="giraffe" onChange={emptyFunction} />,
148 container,
149 root,
150 );
151
152 expect(node.getAttribute('value')).toBe(null);
153 });
154
155 it('should display `value` of number 0', async () => {
156 const container = document.createElement('div');
157 const root = ReactDOMClient.createRoot(container);
158 const node = await renderTextarea(
159 <textarea value={0} onChange={emptyFunction} />,
160 container,
161 root,
162 );
163
164 expect(node.value).toBe('0');
165 });
166
167 it('should update defaultValue to empty string', async () => {
168 const container = document.createElement('div');
169 const root = ReactDOMClient.createRoot(container);
170 await act(() => {
171 root.render(<textarea defaultValue={'foo'} />);
172 });
173
174 await act(() => {
175 root.render(<textarea defaultValue={''} />);
176 });
177
178 expect(container.firstChild.defaultValue).toBe('');
179 });
180
181 it('should allow setting `value` to `giraffe`', async () => {
182 const container = document.createElement('div');
183 const root = ReactDOMClient.createRoot(container);
184 const node = await renderTextarea(
185 <textarea value="giraffe" onChange={emptyFunction} />,
186 container,
187 root,
188 );
189
190 expect(node.value).toBe('giraffe');
191
192 await act(() => {
193 root.render(<textarea value="gorilla" onChange={emptyFunction} />);
194 });
195
196 expect(node.value).toEqual('gorilla');
197 });
198
199 it('will not initially assign an empty value (covers case where firefox throws a validation error when required attribute is set)', async () => {
200 const container = document.createElement('div');
201
202 let counter = 0;
203 const originalCreateElement = document.createElement;
204 spyOnDevAndProd(document, 'createElement').mockImplementation(
205 function (type) {
206 const el = originalCreateElement.apply(this, arguments);
207 let value = '';
208 if (type === 'textarea') {
209 Object.defineProperty(el, 'value', {
210 get: function () {
211 return value;
212 },
213 set: function (val) {
214 value = String(val);
215 counter++;
216 },
217 });
218 }
219 return el;
220 },
221 );
222
223 const root = ReactDOMClient.createRoot(container);
224 await act(() => {
225 root.render(<textarea value="" readOnly={true} />);
226 });
227
228 expect(counter).toEqual(0);
229 });
230
231 it('should render defaultValue for SSR', () => {
232 const markup = ReactDOMServer.renderToString(<textarea defaultValue="1" />);
233 const div = document.createElement('div');
234 div.innerHTML = markup;
235 expect(div.firstChild.innerHTML).toBe('1');
236 expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
237 });
238
239 it('should render value for SSR', () => {
240 const element = <textarea value="1" onChange={function () {}} />;
241 const markup = ReactDOMServer.renderToString(element);
242 const div = document.createElement('div');
243 div.innerHTML = markup;
244 expect(div.firstChild.innerHTML).toBe('1');
245 expect(div.firstChild.getAttribute('defaultValue')).toBe(null);
246 });
247
248 it('should allow setting `value` to `true`', async () => {
249 const container = document.createElement('div');
250 const root = ReactDOMClient.createRoot(container);
251 const node = await renderTextarea(
252 <textarea value="giraffe" onChange={emptyFunction} />,
253 container,
254 root,
255 );
256
257 expect(node.value).toBe('giraffe');
258
259 await act(() => {
260 root.render(<textarea value={true} onChange={emptyFunction} />);
261 });
262
263 expect(node.value).toEqual('true');
264 });
265
266 it('should allow setting `value` to `false`', async () => {
267 const container = document.createElement('div');
268 const root = ReactDOMClient.createRoot(container);
269 const node = await renderTextarea(
270 <textarea value="giraffe" onChange={emptyFunction} />,
271 container,
272 root,
273 );
274
275 expect(node.value).toBe('giraffe');
276
277 await act(() => {
278 root.render(<textarea value={false} onChange={emptyFunction} />);
279 });
280
281 expect(node.value).toEqual('false');
282 });
283
284 it('should allow setting `value` to `objToString`', async () => {
285 const container = document.createElement('div');
286 const root = ReactDOMClient.createRoot(container);
287 const node = await renderTextarea(
288 <textarea value="giraffe" onChange={emptyFunction} />,
289 container,
290 root,
291 );
292
293 expect(node.value).toBe('giraffe');
294
295 const objToString = {
296 toString: function () {
297 return 'foo';
298 },
299 };
300
301 await act(() => {
302 root.render(<textarea value={objToString} onChange={emptyFunction} />);
303 });
304
305 expect(node.value).toEqual('foo');
306 });
307
308 it('should throw when value is set to a Temporal-like object', async () => {
309 class TemporalLike {
310 valueOf() {
311 // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
312 // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
313 throw new TypeError('prod message');
314 }
315 toString() {
316 return '2020-01-01';
317 }
318 }
319 const container = document.createElement('div');
320 const root = ReactDOMClient.createRoot(container);
321 const node = await renderTextarea(
322 <textarea value="giraffe" onChange={emptyFunction} />,
323 container,
324 root,
325 );
326
327 expect(node.value).toBe('giraffe');
328
329 const test = async () => {
330 await act(() => {
331 root.render(
332 <textarea value={new TemporalLike()} onChange={emptyFunction} />,
333 );
334 });
335 };
336 await expect(test).rejects.toThrow(new TypeError('prod message'));
337 assertConsoleErrorDev([
338 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
339 'strings, not TemporalLike. This value must be coerced to a string before using it here.\n' +
340 ' in textarea (at **',
341 ]);
342 });
343
344 it('should take updates to `defaultValue` for uncontrolled textarea', async () => {
345 const container = document.createElement('div');
346 const root = ReactDOMClient.createRoot(container);
347 await act(() => {
348 root.render(<textarea defaultValue="0" />);
349 });
350
351 const node = container.firstChild;
352
353 expect(node.value).toBe('0');
354
355 await act(() => {
356 root.render(<textarea defaultValue="1" />);
357 });
358
359 expect(node.value).toBe('0');
360 });
361
362 it('should take updates to children in lieu of `defaultValue` for uncontrolled textarea', async () => {
363 const container = document.createElement('div');
364 const root = ReactDOMClient.createRoot(container);
365 await act(() => {
366 root.render(<textarea defaultValue="0" />);
367 });
368
369 const node = container.firstChild;
370
371 expect(node.value).toBe('0');
372
373 await act(() => {
374 root.render(<textarea>1</textarea>);
375 });
376
377 expect(node.value).toBe('0');
378 });
379
380 it('should not incur unnecessary DOM mutations', async () => {
381 const container = document.createElement('div');
382 const root = ReactDOMClient.createRoot(container);
383 await act(() => {
384 root.render(<textarea value="a" onChange={emptyFunction} />);
385 });
386
387 const node = container.firstChild;
388 let nodeValue = 'a';
389 const nodeValueSetter = jest.fn();
390 Object.defineProperty(node, 'value', {
391 get: function () {
392 return nodeValue;
393 },
394 set: nodeValueSetter.mockImplementation(function (newValue) {
395 nodeValue = newValue;
396 }),
397 });
398
399 await act(() => {
400 root.render(<textarea value="a" onChange={emptyFunction} />);
401 });
402
403 expect(nodeValueSetter).toHaveBeenCalledTimes(0);
404
405 await act(() => {
406 root.render(<textarea value="b" onChange={emptyFunction} />);
407 });
408
409 expect(nodeValueSetter).toHaveBeenCalledTimes(1);
410 });
411
412 it('should properly control a value of number `0`', async () => {
413 const setUntrackedValue = Object.getOwnPropertyDescriptor(
414 HTMLTextAreaElement.prototype,
415 'value',
416 ).set;
417
418 const container = document.createElement('div');
419 const root = ReactDOMClient.createRoot(container);
420 document.body.appendChild(container);
421
422 try {
423 const node = await renderTextarea(
424 <textarea value={0} onChange={emptyFunction} />,
425 container,
426 root,
427 );
428
429 setUntrackedValue.call(node, 'giraffe');
430 node.dispatchEvent(
431 new Event('input', {bubbles: true, cancelable: false}),
432 );
433 expect(node.value).toBe('0');
434 } finally {
435 document.body.removeChild(container);
436 }
437 });
438
439 if (ReactFeatureFlags.disableTextareaChildren) {
440 it('should ignore children content', async () => {
441 const container = document.createElement('div');
442 const root = ReactDOMClient.createRoot(container);
443 const node = await renderTextarea(
444 <textarea>giraffe</textarea>,
445 container,
446 root,
447 );
448 assertConsoleErrorDev([
449 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
450 ' in textarea (at **)',
451 ]);
452 expect(node.value).toBe('');
453
454 await act(() => {
455 root.render(<textarea>gorilla</textarea>);
456 });
457
458 expect(node.value).toEqual('');
459 });
460 }
461
462 if (ReactFeatureFlags.disableTextareaChildren) {
463 it('should receive defaultValue and still ignore children content', async () => {
464 const container = document.createElement('div');
465 const root = ReactDOMClient.createRoot(container);
466
467 const node = await renderTextarea(
468 <textarea defaultValue="dragon">monkey</textarea>,
469 container,
470 root,
471 );
472 assertConsoleErrorDev([
473 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
474 ' in textarea (at **)',
475 ]);
476 expect(node.value).toBe('dragon');
477 });
478 }
479
480 if (!ReactFeatureFlags.disableTextareaChildren) {
481 it('should treat children like `defaultValue`', async () => {
482 const container = document.createElement('div');
483 const root = ReactDOMClient.createRoot(container);
484
485 const node = await renderTextarea(
486 <textarea>giraffe</textarea>,
487 container,
488 root,
489 );
490 assertConsoleErrorDev([
491 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
492 ' in textarea (at **)',
493 ]);
494
495 expect(node.value).toBe('giraffe');
496
497 await act(() => {
498 root.render(<textarea>gorilla</textarea>);
499 });
500
501 expect(node.value).toEqual('giraffe');
502 });
503 }
504
505 it('should keep value when switching to uncontrolled element if not changed', async () => {
506 const container = document.createElement('div');
507 const root = ReactDOMClient.createRoot(container);
508 const node = await renderTextarea(
509 <textarea value="kitten" onChange={emptyFunction} />,
510 container,
511 root,
512 );
513
514 expect(node.value).toBe('kitten');
515
516 await act(() => {
517 root.render(<textarea defaultValue="gorilla" />);
518 });
519
520 expect(node.value).toEqual('kitten');
521 });
522
523 it('should keep value when switching to uncontrolled element if changed', async () => {
524 const container = document.createElement('div');
525 const root = ReactDOMClient.createRoot(container);
526 const node = await renderTextarea(
527 <textarea value="kitten" onChange={emptyFunction} />,
528 container,
529 root,
530 );
531
532 expect(node.value).toBe('kitten');
533
534 await act(() => {
535 root.render(<textarea value="puppies" onChange={emptyFunction} />);
536 });
537
538 expect(node.value).toBe('puppies');
539
540 await act(() => {
541 root.render(<textarea defaultValue="gorilla" />);
542 });
543
544 expect(node.value).toEqual('puppies');
545 });
546
547 if (ReactFeatureFlags.disableTextareaChildren) {
548 it('should ignore numbers as children', async () => {
549 const container = document.createElement('div');
550 const root = ReactDOMClient.createRoot(container);
551 const node = await renderTextarea(
552 <textarea>{17}</textarea>,
553 container,
554 root,
555 );
556 assertConsoleErrorDev([
557 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
558 ' in textarea (at **)',
559 ]);
560 expect(node.value).toBe('');
561 });
562 }
563
564 if (!ReactFeatureFlags.disableTextareaChildren) {
565 it('should allow numbers as children', async () => {
566 const container = document.createElement('div');
567 const root = ReactDOMClient.createRoot(container);
568 const node = await renderTextarea(
569 <textarea>{17}</textarea>,
570 container,
571 root,
572 );
573 assertConsoleErrorDev([
574 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
575 ' in textarea (at **)',
576 ]);
577 expect(node.value).toBe('17');
578 });
579 }
580
581 if (ReactFeatureFlags.disableTextareaChildren) {
582 it('should ignore booleans as children', async () => {
583 const container = document.createElement('div');
584 const root = ReactDOMClient.createRoot(container);
585 const node = await renderTextarea(
586 <textarea>{false}</textarea>,
587 container,
588 root,
589 );
590 assertConsoleErrorDev([
591 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
592 ' in textarea (at **)',
593 ]);
594 expect(node.value).toBe('');
595 });
596 }
597
598 if (!ReactFeatureFlags.disableTextareaChildren) {
599 it('should allow booleans as children', async () => {
600 const container = document.createElement('div');
601 const root = ReactDOMClient.createRoot(container);
602 const node = await renderTextarea(
603 <textarea>{false}</textarea>,
604 container,
605 root,
606 );
607 assertConsoleErrorDev([
608 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
609 ' in textarea (at **)',
610 ]);
611 expect(node.value).toBe('false');
612 });
613 }
614
615 if (ReactFeatureFlags.disableTextareaChildren) {
616 it('should ignore objects as children', async () => {
617 const container = document.createElement('div');
618 const root = ReactDOMClient.createRoot(container);
619 const obj = {
620 toString: function () {
621 return 'sharkswithlasers';
622 },
623 };
624 const node = await renderTextarea(
625 <textarea>{obj}</textarea>,
626 container,
627 root,
628 );
629 assertConsoleErrorDev([
630 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
631 ' in textarea (at **)',
632 ]);
633 expect(node.value).toBe('');
634 });
635 }
636
637 if (!ReactFeatureFlags.disableTextareaChildren) {
638 it('should allow objects as children', async () => {
639 const container = document.createElement('div');
640 const root = ReactDOMClient.createRoot(container);
641 const obj = {
642 toString: function () {
643 return 'sharkswithlasers';
644 },
645 };
646 const node = await renderTextarea(
647 <textarea>{obj}</textarea>,
648 container,
649 root,
650 );
651 assertConsoleErrorDev([
652 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
653 ' in textarea (at **)',
654 ]);
655 expect(node.value).toBe('sharkswithlasers');
656 });
657 }
658
659 if (!ReactFeatureFlags.disableTextareaChildren) {
660 it('should throw with multiple or invalid children', async () => {
661 const container = document.createElement('div');
662 const root = ReactDOMClient.createRoot(container);
663 await expect(async () => {
664 await act(() => {
665 root.render(
666 <textarea>
667 {'hello'}
668 {'there'}
669 </textarea>,
670 );
671 });
672 }).rejects.toThrow('<textarea> can only have at most one child');
673 assertConsoleErrorDev([
674 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
675 ' in textarea (at **)',
676 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
677 ' in textarea (at **)',
678 ]);
679
680 let node;
681 await expect(
682 (async () =>
683 (node = await renderTextarea(
684 <textarea>
685 <strong />
686 </textarea>,
687 container,
688 root,
689 )))(),
690 ).resolves.not.toThrow();
691 assertConsoleErrorDev([
692 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
693 ' in textarea (at **)',
694 ]);
695
696 expect(node.value).toBe('[object Object]');
697 });
698 }
699
700 it('should unmount', async () => {
701 const container = document.createElement('div');
702 const root = ReactDOMClient.createRoot(container);
703 await act(() => {
704 root.render(<textarea />);
705 });
706
707 await act(() => {
708 root.unmount();
709 });
710 });
711
712 it('should warn if value is null', async () => {
713 const container = document.createElement('div');
714 const root = ReactDOMClient.createRoot(container);
715 await act(() => {
716 root.render(<textarea value={null} />);
717 });
718 assertConsoleErrorDev([
719 '`value` prop on `textarea` should not be null. ' +
720 'Consider using an empty string to clear the component or `undefined` ' +
721 'for uncontrolled components.\n' +
722 ' in textarea (at **)',
723 ]);
724
725 await act(() => {
726 root.render(<textarea value={null} />);
727 });
728 });
729
730 it('should warn if value and defaultValue are specified', async () => {
731 const InvalidComponent = () => (
732 <textarea value="foo" defaultValue="bar" readOnly={true} />
733 );
734 let container = document.createElement('div');
735 let root = ReactDOMClient.createRoot(container);
736 await act(() => {
737 root.render(<InvalidComponent />);
738 });
739 assertConsoleErrorDev([
740 'InvalidComponent contains a textarea with both value and defaultValue props. ' +
741 'Textarea elements must be either controlled or uncontrolled ' +
742 '(specify either the value prop, or the defaultValue prop, but not ' +
743 'both). Decide between using a controlled or uncontrolled textarea ' +
744 'and remove one of these props. More info: ' +
745 'https://react.dev/link/controlled-components\n' +
746 ' in textarea (at **)\n' +
747 ' in InvalidComponent (at **)',
748 ]);
749
750 container = document.createElement('div');
751 root = ReactDOMClient.createRoot(container);
752
753 await act(() => {
754 root.render(<InvalidComponent />);
755 });
756 });
757
758 it('should not warn about missing onChange in uncontrolled textareas', async () => {
759 const container = document.createElement('div');
760 let root = ReactDOMClient.createRoot(container);
761
762 await act(() => {
763 root.render(<textarea />);
764 });
765
766 await act(() => {
767 root.unmount();
768 });
769 root = ReactDOMClient.createRoot(container);
770
771 await act(() => {
772 root.render(<textarea value={undefined} />);
773 });
774 });
775
776 it('does not set textContent if value is unchanged', async () => {
777 const container = document.createElement('div');
778 let node;
779 let instance;
780 // Setting defaultValue on a textarea is equivalent to setting textContent,
781 // and is the method we currently use, so we can observe if defaultValue is
782 // is set to determine if textContent is being recreated.
783 // https://html.spec.whatwg.org/#the-textarea-element
784 let defaultValue;
785 const set = jest.fn(value => {
786 defaultValue = value;
787 });
788 const get = jest.fn(value => {
789 return defaultValue;
790 });
791 class App extends React.Component {
792 state = {count: 0, text: 'foo'};
793 componentDidMount() {
794 instance = this;
795 }
796 render() {
797 return (
798 <div>
799 <span>{this.state.count}</span>
800 <textarea
801 ref={n => (node = n)}
802 value="foo"
803 onChange={emptyFunction}
804 data-count={this.state.count}
805 />
806 </div>
807 );
808 }
809 }
810 const root = ReactDOMClient.createRoot(container);
811 await act(() => {
812 root.render(<App />);
813 });
814
815 defaultValue = node.defaultValue;
816 Object.defineProperty(node, 'defaultValue', {get, set});
817 instance.setState({count: 1});
818 expect(set.mock.calls.length).toBe(0);
819 });
820
821 describe('When given a Symbol value', () => {
822 it('treats initial Symbol value as an empty string', async () => {
823 const container = document.createElement('div');
824 const root = ReactDOMClient.createRoot(container);
825 await act(() => {
826 root.render(<textarea value={Symbol('foobar')} onChange={() => {}} />);
827 });
828 assertConsoleErrorDev([
829 'Invalid value for prop `value` on <textarea> tag. ' +
830 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
831 'For details, see https://react.dev/link/attribute-behavior \n' +
832 ' in textarea (at **)',
833 ]);
834 const node = container.firstChild;
835
836 expect(node.value).toBe('');
837 });
838
839 it('treats initial Symbol children as an empty string', async () => {
840 const container = document.createElement('div');
841 const root = ReactDOMClient.createRoot(container);
842 await act(() => {
843 root.render(<textarea onChange={() => {}}>{Symbol('foo')}</textarea>);
844 });
845 assertConsoleErrorDev([
846 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
847 ' in textarea (at **)',
848 ]);
849 const node = container.firstChild;
850
851 expect(node.value).toBe('');
852 });
853
854 it('treats updated Symbol value as an empty string', async () => {
855 const container = document.createElement('div');
856 const root = ReactDOMClient.createRoot(container);
857
858 await act(() => {
859 root.render(<textarea value="foo" onChange={() => {}} />);
860 });
861
862 await act(() => {
863 root.render(<textarea value={Symbol('foo')} onChange={() => {}} />);
864 });
865 assertConsoleErrorDev([
866 'Invalid value for prop `value` on <textarea> tag. ' +
867 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
868 'For details, see https://react.dev/link/attribute-behavior \n' +
869 ' in textarea (at **)',
870 ]);
871 const node = container.firstChild;
872
873 expect(node.value).toBe('');
874 });
875
876 it('treats initial Symbol defaultValue as an empty string', async () => {
877 const container = document.createElement('div');
878 const root = ReactDOMClient.createRoot(container);
879
880 await act(() => {
881 root.render(<textarea defaultValue={Symbol('foobar')} />);
882 });
883
884 const node = container.firstChild;
885
886 // TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
887 expect(node.value).toBe('');
888 });
889
890 it('treats updated Symbol defaultValue as an empty string', async () => {
891 const container = document.createElement('div');
892 const root = ReactDOMClient.createRoot(container);
893 await act(() => {
894 root.render(<textarea defaultValue="foo" />);
895 });
896
897 await act(() => {
898 root.render(<textarea defaultValue={Symbol('foobar')} />);
899 });
900
901 const node = container.firstChild;
902
903 // TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
904 expect(node.value).toBe('foo');
905 });
906 });
907
908 describe('When given a function value', () => {
909 it('treats initial function value as an empty string', async () => {
910 const container = document.createElement('div');
911 const root = ReactDOMClient.createRoot(container);
912
913 await act(() => {
914 root.render(<textarea value={() => {}} onChange={() => {}} />);
915 });
916 assertConsoleErrorDev([
917 'Invalid value for prop `value` on <textarea> tag. ' +
918 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
919 'For details, see https://react.dev/link/attribute-behavior \n' +
920 ' in textarea (at **)',
921 ]);
922 const node = container.firstChild;
923
924 expect(node.value).toBe('');
925 });
926
927 it('treats initial function children as an empty string', async () => {
928 const container = document.createElement('div');
929 const root = ReactDOMClient.createRoot(container);
930
931 await act(() => {
932 root.render(<textarea onChange={() => {}}>{() => {}}</textarea>);
933 });
934 assertConsoleErrorDev([
935 'Use the `defaultValue` or `value` props instead of setting children on <textarea>.\n' +
936 ' in textarea (at **)',
937 ]);
938 const node = container.firstChild;
939
940 expect(node.value).toBe('');
941 });
942
943 it('treats updated function value as an empty string', async () => {
944 const container = document.createElement('div');
945 const root = ReactDOMClient.createRoot(container);
946
947 await act(() => {
948 root.render(<textarea value="foo" onChange={() => {}} />);
949 });
950
951 await act(() => {
952 root.render(<textarea value={() => {}} onChange={() => {}} />);
953 });
954 assertConsoleErrorDev([
955 'Invalid value for prop `value` on <textarea> tag. ' +
956 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
957 'For details, see https://react.dev/link/attribute-behavior \n' +
958 ' in textarea (at **)',
959 ]);
960 const node = container.firstChild;
961
962 expect(node.value).toBe('');
963 });
964
965 it('treats initial function defaultValue as an empty string', async () => {
966 const container = document.createElement('div');
967 const root = ReactDOMClient.createRoot(container);
968 await act(() => {
969 root.render(<textarea defaultValue={() => {}} />);
970 });
971
972 const node = container.firstChild;
973
974 // TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
975 expect(node.value).toBe('');
976 });
977
978 it('treats updated function defaultValue as an empty string', async () => {
979 const container = document.createElement('div');
980 const root = ReactDOMClient.createRoot(container);
981 await act(() => {
982 root.render(<textarea defaultValue="foo" />);
983 });
984
985 await act(() => {
986 root.render(<textarea defaultValue={() => {}} />);
987 });
988
989 const node = container.firstChild;
990
991 // TODO: defaultValue is a reserved prop and is not validated. Check warnings when they are.
992 expect(node.value).toBe('foo');
993 });
994 });
995
996 it('should remove previous `defaultValue`', async () => {
997 const container = document.createElement('div');
998 const root = ReactDOMClient.createRoot(container);
999 await act(() => {
1000 root.render(<textarea defaultValue="0" />);
1001 });
1002
1003 const node = container.firstChild;
1004
1005 expect(node.value).toBe('0');
1006 expect(node.defaultValue).toBe('0');
1007
1008 await act(() => {
1009 root.render(<textarea />);
1010 });
1011
1012 expect(node.defaultValue).toBe('');
1013 });
1014
1015 it('should treat `defaultValue={null}` as missing', async () => {
1016 const container = document.createElement('div');
1017 const root = ReactDOMClient.createRoot(container);
1018 await act(() => {
1019 root.render(<textarea defaultValue="0" />);
1020 });
1021
1022 const node = container.firstChild;
1023
1024 expect(node.value).toBe('0');
1025 expect(node.defaultValue).toBe('0');
1026
1027 await act(() => {
1028 root.render(<textarea defaultValue={null} />);
1029 });
1030
1031 expect(node.defaultValue).toBe('');
1032 });
1033
1034 it('should not warn about missing onChange if value is undefined', async () => {
1035 const container = document.createElement('div');
1036 const root = ReactDOMClient.createRoot(container);
1037 await expect(
1038 act(() => {
1039 root.render(<textarea value={undefined} />);
1040 }),
1041 ).resolves.not.toThrow();
1042 });
1043
1044 it('should not warn about missing onChange if onChange is set', async () => {
1045 const change = jest.fn();
1046 const container = document.createElement('div');
1047 const root = ReactDOMClient.createRoot(container);
1048 await expect(
1049 act(() => {
1050 root.render(<textarea value="something" onChange={change} />);
1051 }),
1052 ).resolves.not.toThrow();
1053 });
1054
1055 it('should not warn about missing onChange if disabled is true', async () => {
1056 const container = document.createElement('div');
1057 const root = ReactDOMClient.createRoot(container);
1058 await expect(
1059 act(() => {
1060 root.render(<textarea value="something" disabled={true} />);
1061 }),
1062 ).resolves.not.toThrow();
1063 });
1064
1065 it('should not warn about missing onChange if value is not set', async () => {
1066 const container = document.createElement('div');
1067 const root = ReactDOMClient.createRoot(container);
1068 await expect(
1069 act(() => {
1070 root.render(<textarea value="something" readOnly={true} />);
1071 }),
1072 ).resolves.not.toThrow();
1073 });
1074
1075 it('should warn about missing onChange if value is false', async () => {
1076 const container = document.createElement('div');
1077 const root = ReactDOMClient.createRoot(container);
1078 await act(() => {
1079 root.render(<textarea value={false} />);
1080 });
1081 assertConsoleErrorDev([
1082 'You provided a `value` prop to a form ' +
1083 'field without an `onChange` handler. This will render a read-only ' +
1084 'field. If the field should be mutable use `defaultValue`. ' +
1085 'Otherwise, set either `onChange` or `readOnly`.\n' +
1086 ' in textarea (at **)',
1087 ]);
1088 });
1089
1090 it('should warn about missing onChange if value is 0', async () => {
1091 const container = document.createElement('div');
1092 const root = ReactDOMClient.createRoot(container);
1093 await act(() => {
1094 root.render(<textarea value={0} />);
1095 });
1096 assertConsoleErrorDev([
1097 'You provided a `value` prop to a form ' +
1098 'field without an `onChange` handler. This will render a read-only ' +
1099 'field. If the field should be mutable use `defaultValue`. ' +
1100 'Otherwise, set either `onChange` or `readOnly`.\n' +
1101 ' in textarea (at **)',
1102 ]);
1103 });
1104
1105 it('should warn about missing onChange if value is "0"', async () => {
1106 const container = document.createElement('div');
1107 const root = ReactDOMClient.createRoot(container);
1108 await act(() => {
1109 root.render(<textarea value="0" />);
1110 });
1111 assertConsoleErrorDev([
1112 'You provided a `value` prop to a form ' +
1113 'field without an `onChange` handler. This will render a read-only ' +
1114 'field. If the field should be mutable use `defaultValue`. ' +
1115 'Otherwise, set either `onChange` or `readOnly`.\n' +
1116 ' in textarea (at **)',
1117 ]);
1118 });
1119
1120 it('should warn about missing onChange if value is ""', async () => {
1121 const container = document.createElement('div');
1122 const root = ReactDOMClient.createRoot(container);
1123 await act(() => {
1124 root.render(<textarea value="" />);
1125 });
1126 assertConsoleErrorDev([
1127 'You provided a `value` prop to a form ' +
1128 'field without an `onChange` handler. This will render a read-only ' +
1129 'field. If the field should be mutable use `defaultValue`. ' +
1130 'Otherwise, set either `onChange` or `readOnly`.\n' +
1131 ' in textarea (at **)',
1132 ]);
1133 });
1134 });