main
js 3,907 lines 127 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 describe('ReactDOMComponent', () => {
13 let React;
14 let ReactDOM;
15 let ReactDOMClient;
16 let ReactDOMServer;
17 const ReactFeatureFlags = require('shared/ReactFeatureFlags');
18
19 let act;
20 let assertLog;
21 let Scheduler;
22 let assertConsoleErrorDev;
23
24 beforeEach(() => {
25 jest.resetModules();
26 React = require('react');
27 ReactDOM = require('react-dom');
28 ReactDOMClient = require('react-dom/client');
29 ReactDOMServer = require('react-dom/server');
30 Scheduler = require('scheduler');
31 act = require('internal-test-utils').act;
32 assertConsoleErrorDev =
33 require('internal-test-utils').assertConsoleErrorDev;
34 assertLog = require('internal-test-utils').assertLog;
35 });
36
37 afterEach(() => {
38 jest.restoreAllMocks();
39 });
40
41 describe('updateDOM', () => {
42 it('should handle className', async () => {
43 const container = document.createElement('div');
44 const root = ReactDOMClient.createRoot(container);
45 await act(() => {
46 root.render(<div style={{}} />);
47 });
48
49 await act(() => {
50 root.render(<div className={'foo'} />);
51 });
52 expect(container.firstChild.className).toEqual('foo');
53 await act(() => {
54 root.render(<div className={'bar'} />);
55 });
56 expect(container.firstChild.className).toEqual('bar');
57 await act(() => {
58 root.render(<div className={null} />);
59 });
60 expect(container.firstChild.className).toEqual('');
61 });
62
63 it('should gracefully handle various style value types', async () => {
64 const container = document.createElement('div');
65 const root = ReactDOMClient.createRoot(container);
66 await act(() => {
67 root.render(<div style={{}} />);
68 });
69 const stubStyle = container.firstChild.style;
70
71 // set initial style
72 const setup = {
73 display: 'block',
74 left: '1px',
75 top: 2,
76 fontFamily: 'Arial',
77 };
78 await act(() => {
79 root.render(<div style={setup} />);
80 });
81 expect(stubStyle.display).toEqual('block');
82 expect(stubStyle.left).toEqual('1px');
83 expect(stubStyle.top).toEqual('2px');
84 expect(stubStyle.fontFamily).toEqual('Arial');
85
86 // reset the style to their default state
87 const reset = {display: '', left: null, top: false, fontFamily: true};
88 await act(() => {
89 root.render(<div style={reset} />);
90 });
91 expect(stubStyle.display).toEqual('');
92 expect(stubStyle.left).toEqual('');
93 expect(stubStyle.top).toEqual('');
94 expect(stubStyle.fontFamily).toEqual('');
95 });
96
97 it('should not update styles when mutating a proxy style object', async () => {
98 const styleStore = {
99 display: 'none',
100 fontFamily: 'Arial',
101 lineHeight: 1.2,
102 };
103 // We use a proxy style object so that we can mutate it even if it is
104 // frozen in DEV.
105 const styles = {
106 get display() {
107 return styleStore.display;
108 },
109 set display(v) {
110 styleStore.display = v;
111 },
112 get fontFamily() {
113 return styleStore.fontFamily;
114 },
115 set fontFamily(v) {
116 styleStore.fontFamily = v;
117 },
118 get lineHeight() {
119 return styleStore.lineHeight;
120 },
121 set lineHeight(v) {
122 styleStore.lineHeight = v;
123 },
124 };
125 const container = document.createElement('div');
126 const root = ReactDOMClient.createRoot(container);
127 await act(() => {
128 root.render(<div style={styles} />);
129 });
130
131 const stubStyle = container.firstChild.style;
132 stubStyle.display = styles.display;
133 stubStyle.fontFamily = styles.fontFamily;
134
135 styles.display = 'block';
136
137 await act(() => {
138 root.render(<div style={styles} />);
139 });
140 expect(stubStyle.display).toEqual('none');
141 expect(stubStyle.fontFamily).toEqual('Arial');
142 expect(stubStyle.lineHeight).toEqual('1.2');
143
144 styles.fontFamily = 'Helvetica';
145
146 await act(() => {
147 root.render(<div style={styles} />);
148 });
149 expect(stubStyle.display).toEqual('none');
150 expect(stubStyle.fontFamily).toEqual('Arial');
151 expect(stubStyle.lineHeight).toEqual('1.2');
152
153 styles.lineHeight = 0.5;
154
155 await act(() => {
156 root.render(<div style={styles} />);
157 });
158 expect(stubStyle.display).toEqual('none');
159 expect(stubStyle.fontFamily).toEqual('Arial');
160 expect(stubStyle.lineHeight).toEqual('1.2');
161
162 await act(() => {
163 root.render(<div style={undefined} />);
164 });
165 expect(stubStyle.display).toBe('');
166 expect(stubStyle.fontFamily).toBe('');
167 expect(stubStyle.lineHeight).toBe('');
168 });
169
170 it('should throw when mutating style objects', async () => {
171 const style = {border: '1px solid black'};
172
173 class App extends React.Component {
174 state = {style: style};
175
176 render() {
177 return <div style={this.state.style}>asd</div>;
178 }
179 }
180
181 const container = document.createElement('div');
182 const root = ReactDOMClient.createRoot(container);
183 await act(() => {
184 root.render(<App />);
185 });
186
187 if (__DEV__) {
188 expect(() => (style.position = 'absolute')).toThrow();
189 }
190 });
191
192 it('should warn for unknown prop', async () => {
193 const container = document.createElement('div');
194 const root = ReactDOMClient.createRoot(container);
195 await act(() => {
196 root.render(<div foo={() => {}} />);
197 });
198 assertConsoleErrorDev([
199 'Invalid value for prop `foo` on <div> tag. Either remove it ' +
200 'from the element, or pass a string or number value to keep ' +
201 'it in the DOM. For details, see https://react.dev/link/attribute-behavior ' +
202 '\n in div (at **)',
203 ]);
204 });
205
206 it('should group multiple unknown prop warnings together', async () => {
207 const container = document.createElement('div');
208 const root = ReactDOMClient.createRoot(container);
209 await act(() => {
210 root.render(<div foo={() => {}} baz={() => {}} />);
211 });
212 assertConsoleErrorDev([
213 'Invalid values for props `foo`, `baz` on <div> tag. Either remove ' +
214 'them from the element, or pass a string or number value to keep ' +
215 'them in the DOM. For details, see https://react.dev/link/attribute-behavior ' +
216 '\n in div (at **)',
217 ]);
218 });
219
220 it('should warn for onDblClick prop', async () => {
221 const container = document.createElement('div');
222 const root = ReactDOMClient.createRoot(container);
223 await act(() => {
224 root.render(<div onDblClick={() => {}} />);
225 });
226 assertConsoleErrorDev([
227 'Invalid event handler property `onDblClick`. Did you mean `onDoubleClick`?\n' +
228 ' in div (at **)',
229 ]);
230 });
231
232 it('should warn for unknown string event handlers', async () => {
233 const container = document.createElement('div');
234 const root = ReactDOMClient.createRoot(container);
235 await act(() => {
236 root.render(<div onUnknown='alert("hack")' />);
237 });
238 assertConsoleErrorDev([
239 'Unknown event handler property `onUnknown`. It will be ignored.\n' +
240 ' in div (at **)',
241 ]);
242 expect(container.firstChild.hasAttribute('onUnknown')).toBe(false);
243 expect(container.firstChild.onUnknown).toBe(undefined);
244 await act(() => {
245 root.render(<div onunknown='alert("hack")' />);
246 });
247 assertConsoleErrorDev([
248 'Unknown event handler property `onunknown`. It will be ignored.\n' +
249 ' in div (at **)',
250 ]);
251 expect(container.firstChild.hasAttribute('onunknown')).toBe(false);
252 expect(container.firstChild.onunknown).toBe(undefined);
253
254 await act(() => {
255 root.render(<div on-unknown='alert("hack")' />);
256 });
257 assertConsoleErrorDev([
258 'Unknown event handler property `on-unknown`. It will be ignored.\n' +
259 ' in div (at **)',
260 ]);
261 expect(container.firstChild.hasAttribute('on-unknown')).toBe(false);
262 expect(container.firstChild['on-unknown']).toBe(undefined);
263 });
264
265 it('should warn for unknown function event handlers', async () => {
266 const container = document.createElement('div');
267 const root = ReactDOMClient.createRoot(container);
268 await act(() => {
269 root.render(<div onUnknown={function () {}} />);
270 });
271 assertConsoleErrorDev([
272 'Unknown event handler property `onUnknown`. It will be ignored.\n' +
273 ' in div (at **)',
274 ]);
275 expect(container.firstChild.hasAttribute('onUnknown')).toBe(false);
276 expect(container.firstChild.onUnknown).toBe(undefined);
277 await act(() => {
278 root.render(<div onunknown={function () {}} />);
279 });
280 assertConsoleErrorDev([
281 'Unknown event handler property `onunknown`. It will be ignored.\n' +
282 ' in div (at **)',
283 ]);
284 expect(container.firstChild.hasAttribute('onunknown')).toBe(false);
285 expect(container.firstChild.onunknown).toBe(undefined);
286 await act(() => {
287 root.render(<div on-unknown={function () {}} />);
288 });
289 assertConsoleErrorDev([
290 'Unknown event handler property `on-unknown`. It will be ignored.\n' +
291 ' in div (at **)',
292 ]);
293 expect(container.firstChild.hasAttribute('on-unknown')).toBe(false);
294 expect(container.firstChild['on-unknown']).toBe(undefined);
295 });
296
297 it('should warn for badly cased React attributes', async () => {
298 const container = document.createElement('div');
299 const root = ReactDOMClient.createRoot(container);
300 await act(() => {
301 root.render(<div CHILDREN="5" />);
302 });
303 assertConsoleErrorDev([
304 'Invalid DOM property `CHILDREN`. Did you mean `children`?\n' +
305 ' in div (at **)',
306 ]);
307 expect(container.firstChild.getAttribute('CHILDREN')).toBe('5');
308 });
309
310 it('should not warn for "0" as a unitless style value', async () => {
311 class Component extends React.Component {
312 render() {
313 return <div style={{margin: '0'}} />;
314 }
315 }
316
317 const container = document.createElement('div');
318 const root = ReactDOMClient.createRoot(container);
319 await act(() => {
320 root.render(<Component />);
321 });
322 });
323
324 it('should warn nicely about NaN in style', async () => {
325 const style = {fontSize: NaN};
326 const div = document.createElement('div');
327 const root = ReactDOMClient.createRoot(div);
328 await act(() => {
329 root.render(<span style={style} />);
330 });
331 assertConsoleErrorDev([
332 '`NaN` is an invalid value for the `fontSize` css style property.\n' +
333 ' in span (at **)',
334 ]);
335 await act(() => {
336 root.render(<span style={style} />);
337 });
338 });
339
340 it('throws with Temporal-like objects as style values', async () => {
341 class TemporalLike {
342 valueOf() {
343 // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
344 // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
345 throw new TypeError('prod message');
346 }
347 toString() {
348 return '2020-01-01';
349 }
350 }
351 const style = {fontSize: new TemporalLike()};
352 const root = ReactDOMClient.createRoot(document.createElement('div'));
353 await expect(async () => {
354 await act(() => {
355 root.render(<span style={style} />);
356 });
357 }).rejects.toThrow(new TypeError('prod message'));
358 assertConsoleErrorDev([
359 'The provided `fontSize` CSS property is an unsupported type TemporalLike.' +
360 ' This value must be coerced to a string before using it here.\n' +
361 ' in span (at **)',
362 'The provided `fontSize` CSS property is an unsupported type TemporalLike.' +
363 ' This value must be coerced to a string before using it here.\n' +
364 ' in span (at **)',
365 ]);
366 });
367
368 it('should update styles if initially null', async () => {
369 let styles = null;
370 const container = document.createElement('div');
371 const root = ReactDOMClient.createRoot(container);
372 await act(() => {
373 root.render(<div style={styles} />);
374 });
375
376 const stubStyle = container.firstChild.style;
377
378 styles = {display: 'block'};
379
380 await act(() => {
381 root.render(<div style={styles} />);
382 });
383 expect(stubStyle.display).toEqual('block');
384 });
385
386 it('should update styles if updated to null multiple times', async () => {
387 let styles = null;
388 const container = document.createElement('div');
389 const root = ReactDOMClient.createRoot(container);
390 await act(() => {
391 root.render(<div style={styles} />);
392 });
393
394 styles = {display: 'block'};
395 const stubStyle = container.firstChild.style;
396
397 await act(() => {
398 root.render(<div style={styles} />);
399 });
400 expect(stubStyle.display).toEqual('block');
401
402 await act(() => {
403 root.render(<div style={null} />);
404 });
405 expect(stubStyle.display).toEqual('');
406
407 await act(() => {
408 root.render(<div style={styles} />);
409 });
410 expect(stubStyle.display).toEqual('block');
411
412 await act(() => {
413 root.render(<div style={null} />);
414 });
415 expect(stubStyle.display).toEqual('');
416 });
417
418 it('should allow named slot projection on both web components and regular DOM elements', async () => {
419 const container = document.createElement('div');
420 const root = ReactDOMClient.createRoot(container);
421
422 await act(() => {
423 root.render(
424 <my-component>
425 <my-second-component slot="first" />
426 <button slot="second">Hello</button>
427 </my-component>,
428 );
429 });
430
431 const lightDOM = container.firstChild.childNodes;
432
433 expect(lightDOM[0].getAttribute('slot')).toBe('first');
434 expect(lightDOM[1].getAttribute('slot')).toBe('second');
435 });
436
437 it('should skip reserved props on web components', async () => {
438 const container = document.createElement('div');
439 const root = ReactDOMClient.createRoot(container);
440
441 await act(() => {
442 root.render(
443 <my-component
444 children={['foo']}
445 suppressContentEditableWarning={true}
446 suppressHydrationWarning={true}
447 />,
448 );
449 });
450 expect(container.firstChild.hasAttribute('children')).toBe(false);
451 expect(
452 container.firstChild.hasAttribute('suppressContentEditableWarning'),
453 ).toBe(false);
454 expect(
455 container.firstChild.hasAttribute('suppressHydrationWarning'),
456 ).toBe(false);
457
458 await act(() => {
459 root.render(
460 <my-component
461 children={['bar']}
462 suppressContentEditableWarning={false}
463 suppressHydrationWarning={false}
464 />,
465 );
466 });
467 expect(container.firstChild.hasAttribute('children')).toBe(false);
468 expect(
469 container.firstChild.hasAttribute('suppressContentEditableWarning'),
470 ).toBe(false);
471 expect(
472 container.firstChild.hasAttribute('suppressHydrationWarning'),
473 ).toBe(false);
474 });
475
476 it('should skip dangerouslySetInnerHTML on web components', async () => {
477 const container = document.createElement('div');
478 const root = ReactDOMClient.createRoot(container);
479
480 await act(() => {
481 root.render(<my-component dangerouslySetInnerHTML={{__html: 'hi'}} />);
482 });
483 expect(container.firstChild.hasAttribute('dangerouslySetInnerHTML')).toBe(
484 false,
485 );
486
487 await act(() => {
488 root.render(<my-component dangerouslySetInnerHTML={{__html: 'bye'}} />);
489 });
490 expect(container.firstChild.hasAttribute('dangerouslySetInnerHTML')).toBe(
491 false,
492 );
493 });
494
495 it('should render null and undefined as empty but print other falsy values', async () => {
496 const container = document.createElement('div');
497 const root = ReactDOMClient.createRoot(container);
498
499 await act(() => {
500 root.render(<div dangerouslySetInnerHTML={{__html: 'textContent'}} />);
501 });
502 expect(container.textContent).toEqual('textContent');
503
504 await act(() => {
505 root.render(<div dangerouslySetInnerHTML={{__html: 0}} />);
506 });
507 expect(container.textContent).toEqual('0');
508
509 await act(() => {
510 root.render(<div dangerouslySetInnerHTML={{__html: false}} />);
511 });
512 expect(container.textContent).toEqual('false');
513
514 await act(() => {
515 root.render(<div dangerouslySetInnerHTML={{__html: ''}} />);
516 });
517 expect(container.textContent).toEqual('');
518
519 await act(() => {
520 root.render(<div dangerouslySetInnerHTML={{__html: null}} />);
521 });
522 expect(container.textContent).toEqual('');
523
524 await act(() => {
525 root.render(<div dangerouslySetInnerHTML={{__html: undefined}} />);
526 });
527 expect(container.textContent).toEqual('');
528 });
529
530 it('should remove attributes', async () => {
531 const container = document.createElement('div');
532 const root = ReactDOMClient.createRoot(container);
533 await act(() => {
534 root.render(<img height="17" />);
535 });
536
537 expect(container.firstChild.hasAttribute('height')).toBe(true);
538 await act(() => {
539 root.render(<img />);
540 });
541 expect(container.firstChild.hasAttribute('height')).toBe(false);
542 });
543
544 it('should remove properties', async () => {
545 const container = document.createElement('div');
546 const root = ReactDOMClient.createRoot(container);
547 await act(() => {
548 root.render(<div className="monkey" />);
549 });
550
551 expect(container.firstChild.className).toEqual('monkey');
552 await act(() => {
553 root.render(<div />);
554 });
555 expect(container.firstChild.className).toEqual('');
556 });
557
558 it('should not set null/undefined attributes', async () => {
559 const container = document.createElement('div');
560 const root = ReactDOMClient.createRoot(container);
561 // Initial render.
562 await act(() => {
563 root.render(<img src={null} data-foo={undefined} />);
564 });
565 const node = container.firstChild;
566 expect(node.hasAttribute('src')).toBe(false);
567 expect(node.hasAttribute('data-foo')).toBe(false);
568 // Update in one direction.
569 await act(() => {
570 root.render(<img src={undefined} data-foo={null} />);
571 });
572 expect(node.hasAttribute('src')).toBe(false);
573 expect(node.hasAttribute('data-foo')).toBe(false);
574 // Update in another direction.
575 await act(() => {
576 root.render(<img src={null} data-foo={undefined} />);
577 });
578 expect(node.hasAttribute('src')).toBe(false);
579 expect(node.hasAttribute('data-foo')).toBe(false);
580 // Removal.
581 await act(() => {
582 root.render(<img />);
583 });
584 expect(node.hasAttribute('src')).toBe(false);
585 expect(node.hasAttribute('data-foo')).toBe(false);
586 // Addition.
587 await act(() => {
588 root.render(<img src={undefined} data-foo={null} />);
589 });
590 expect(node.hasAttribute('src')).toBe(false);
591 expect(node.hasAttribute('data-foo')).toBe(false);
592 });
593
594 it('should not add an empty src attribute', async () => {
595 const container = document.createElement('div');
596 const root = ReactDOMClient.createRoot(container);
597 await act(() => {
598 root.render(<img src="" />);
599 });
600 assertConsoleErrorDev([
601 'An empty string ("") was passed to the src attribute. ' +
602 'This may cause the browser to download the whole page again over the network. ' +
603 'To fix this, either do not render the element at all ' +
604 'or pass null to src instead of an empty string.\n' +
605 ' in img (at **)',
606 ]);
607 const node = container.firstChild;
608 expect(node.hasAttribute('src')).toBe(false);
609
610 await act(() => {
611 root.render(<img src="abc" />);
612 });
613 expect(node.hasAttribute('src')).toBe(true);
614
615 await act(() => {
616 root.render(<img src="" />);
617 });
618 assertConsoleErrorDev([
619 'An empty string ("") was passed to the src attribute. ' +
620 'This may cause the browser to download the whole page again over the network. ' +
621 'To fix this, either do not render the element at all ' +
622 'or pass null to src instead of an empty string.\n' +
623 ' in img (at **)',
624 ]);
625 expect(node.hasAttribute('src')).toBe(false);
626 });
627
628 it('should not add an empty href attribute', async () => {
629 const container = document.createElement('div');
630 const root = ReactDOMClient.createRoot(container);
631 await act(() => {
632 root.render(<link href="" />);
633 });
634 assertConsoleErrorDev([
635 'An empty string ("") was passed to the href attribute. ' +
636 'To fix this, either do not render the element at all ' +
637 'or pass null to href instead of an empty string.\n' +
638 ' in link (at **)',
639 ]);
640 const node = container.firstChild;
641 expect(node.hasAttribute('href')).toBe(false);
642
643 await act(() => {
644 root.render(<link href="abc" />);
645 });
646 expect(node.hasAttribute('href')).toBe(true);
647
648 await act(() => {
649 root.render(<link href="" />);
650 });
651 assertConsoleErrorDev([
652 'An empty string ("") was passed to the href attribute. ' +
653 'To fix this, either do not render the element at all ' +
654 'or pass null to href instead of an empty string.\n' +
655 ' in link (at **)',
656 ]);
657 expect(node.hasAttribute('href')).toBe(false);
658 });
659
660 it('should allow an empty href attribute on anchors', async () => {
661 const container = document.createElement('div');
662 const root = ReactDOMClient.createRoot(container);
663 await act(() => {
664 root.render(<a href="" />);
665 });
666 const node = container.firstChild;
667 expect(node.getAttribute('href')).toBe('');
668 });
669
670 it('should allow an empty action attribute', async () => {
671 const container = document.createElement('div');
672 const root = ReactDOMClient.createRoot(container);
673 await act(() => {
674 root.render(<form action="" />);
675 });
676 const node = container.firstChild;
677 expect(node.getAttribute('action')).toBe('');
678
679 await act(() => {
680 root.render(<form action="abc" />);
681 });
682 expect(node.hasAttribute('action')).toBe(true);
683
684 await act(() => {
685 root.render(<form action="" />);
686 });
687 expect(node.getAttribute('action')).toBe('');
688 });
689
690 it('allows empty string of a formAction to override the default of a parent', async () => {
691 const container = document.createElement('div');
692 const root = ReactDOMClient.createRoot(container);
693 await act(() => {
694 root.render(
695 <form action="hello">
696 <button formAction="" />,
697 </form>,
698 );
699 });
700 const node = container.firstChild.firstChild;
701 expect(node.hasAttribute('formaction')).toBe(true);
702 expect(node.getAttribute('formaction')).toBe('');
703 });
704
705 it('should not filter attributes for custom elements', async () => {
706 const container = document.createElement('div');
707 const root = ReactDOMClient.createRoot(container);
708 await act(() => {
709 root.render(
710 <some-custom-element action="" formAction="" href="" src="" />,
711 );
712 });
713 const node = container.firstChild;
714 expect(node.hasAttribute('action')).toBe(true);
715 expect(node.hasAttribute('formAction')).toBe(true);
716 expect(node.hasAttribute('href')).toBe(true);
717 expect(node.hasAttribute('src')).toBe(true);
718 });
719
720 it('should apply React-specific aliases to HTML elements', async () => {
721 const container = document.createElement('div');
722 const root = ReactDOMClient.createRoot(container);
723 await act(() => {
724 root.render(<form acceptCharset="foo" />);
725 });
726 const node = container.firstChild;
727 // Test attribute initialization.
728 expect(node.getAttribute('accept-charset')).toBe('foo');
729 expect(node.hasAttribute('acceptCharset')).toBe(false);
730 // Test attribute update.
731 await act(() => {
732 root.render(<form acceptCharset="boo" />);
733 });
734 expect(node.getAttribute('accept-charset')).toBe('boo');
735 expect(node.hasAttribute('acceptCharset')).toBe(false);
736 // Test attribute removal by setting to null.
737 await act(() => {
738 root.render(<form acceptCharset={null} />);
739 });
740 expect(node.hasAttribute('accept-charset')).toBe(false);
741 expect(node.hasAttribute('acceptCharset')).toBe(false);
742 // Restore.
743 await act(() => {
744 root.render(<form acceptCharset="foo" />);
745 });
746 expect(node.getAttribute('accept-charset')).toBe('foo');
747 expect(node.hasAttribute('acceptCharset')).toBe(false);
748 // Test attribute removal by setting to undefined.
749 await act(() => {
750 root.render(<form acceptCharset={undefined} />);
751 });
752 expect(node.hasAttribute('accept-charset')).toBe(false);
753 expect(node.hasAttribute('acceptCharset')).toBe(false);
754 // Restore.
755 await act(() => {
756 root.render(<form acceptCharset="foo" />);
757 });
758 expect(node.getAttribute('accept-charset')).toBe('foo');
759 expect(node.hasAttribute('acceptCharset')).toBe(false);
760 // Test attribute removal.
761 await act(() => {
762 root.render(<form />);
763 });
764 expect(node.hasAttribute('accept-charset')).toBe(false);
765 expect(node.hasAttribute('acceptCharset')).toBe(false);
766 });
767
768 it('should apply React-specific aliases to SVG elements', async () => {
769 const container = document.createElement('div');
770 const root = ReactDOMClient.createRoot(container);
771 await act(() => {
772 root.render(<svg arabicForm="foo" />);
773 });
774 const node = container.firstChild;
775 // Test attribute initialization.
776 expect(node.getAttribute('arabic-form')).toBe('foo');
777 expect(node.hasAttribute('arabicForm')).toBe(false);
778 // Test attribute update.
779 await act(() => {
780 root.render(<svg arabicForm="boo" />);
781 });
782 expect(node.getAttribute('arabic-form')).toBe('boo');
783 expect(node.hasAttribute('arabicForm')).toBe(false);
784 // Test attribute removal by setting to null.
785 await act(() => {
786 root.render(<svg arabicForm={null} />);
787 });
788 expect(node.hasAttribute('arabic-form')).toBe(false);
789 expect(node.hasAttribute('arabicForm')).toBe(false);
790 // Restore.
791 await act(() => {
792 root.render(<svg arabicForm="foo" />);
793 });
794 expect(node.getAttribute('arabic-form')).toBe('foo');
795 expect(node.hasAttribute('arabicForm')).toBe(false);
796 // Test attribute removal by setting to undefined.
797 await act(() => {
798 root.render(<svg arabicForm={undefined} />);
799 });
800 expect(node.hasAttribute('arabic-form')).toBe(false);
801 expect(node.hasAttribute('arabicForm')).toBe(false);
802 // Restore.
803 await act(() => {
804 root.render(<svg arabicForm="foo" />);
805 });
806 expect(node.getAttribute('arabic-form')).toBe('foo');
807 expect(node.hasAttribute('arabicForm')).toBe(false);
808 // Test attribute removal.
809 await act(() => {
810 root.render(<svg />);
811 });
812 expect(node.hasAttribute('arabic-form')).toBe(false);
813 expect(node.hasAttribute('arabicForm')).toBe(false);
814 });
815
816 it('should properly update custom attributes on custom elements', async () => {
817 const container = document.createElement('div');
818 const root = ReactDOMClient.createRoot(container);
819 await act(() => {
820 root.render(<some-custom-element foo="bar" />);
821 });
822 expect(container.firstChild.getAttribute('foo')).toBe('bar');
823 await act(() => {
824 root.render(<some-custom-element bar="buzz" />);
825 });
826 expect(container.firstChild.hasAttribute('foo')).toBe(false);
827 expect(container.firstChild.getAttribute('bar')).toBe('buzz');
828 const node = container.firstChild;
829 expect(node.hasAttribute('foo')).toBe(false);
830 expect(node.getAttribute('bar')).toBe('buzz');
831 });
832
833 it('should not apply React-specific aliases to custom elements', async () => {
834 const container = document.createElement('div');
835 const root = ReactDOMClient.createRoot(container);
836 await act(() => {
837 root.render(<some-custom-element arabicForm="foo" />);
838 });
839 const node = container.firstChild;
840 // Should not get transformed to arabic-form as SVG would be.
841 expect(node.getAttribute('arabicForm')).toBe('foo');
842 expect(node.hasAttribute('arabic-form')).toBe(false);
843 // Test attribute update.
844 await act(() => {
845 root.render(<some-custom-element arabicForm="boo" />);
846 });
847 expect(node.getAttribute('arabicForm')).toBe('boo');
848 // Test attribute removal and addition.
849 await act(() => {
850 root.render(<some-custom-element acceptCharset="buzz" />);
851 });
852 // Verify the previous attribute was removed.
853 expect(node.hasAttribute('arabicForm')).toBe(false);
854 // Should not get transformed to accept-charset as HTML would be.
855 expect(node.getAttribute('acceptCharset')).toBe('buzz');
856 expect(node.hasAttribute('accept-charset')).toBe(false);
857 });
858
859 it('should clear a single style prop when changing `style`', async () => {
860 let styles = {display: 'none', color: 'red'};
861 const container = document.createElement('div');
862 const root = ReactDOMClient.createRoot(container);
863 await act(() => {
864 root.render(<div style={styles} />);
865 });
866
867 const stubStyle = container.firstChild.style;
868
869 styles = {color: 'green'};
870 await act(() => {
871 root.render(<div style={styles} />);
872 });
873 expect(stubStyle.display).toEqual('');
874 expect(stubStyle.color).toEqual('green');
875 });
876
877 it('should reject attribute key injection attack on markup for regular DOM (SSR)', () => {
878 for (let i = 0; i < 3; i++) {
879 const element1 = React.createElement(
880 'div',
881 {'blah" onclick="beevil" noise="hi': 'selected'},
882 null,
883 );
884 const element2 = React.createElement(
885 'div',
886 {'></div><script>alert("hi")</script>': 'selected'},
887 null,
888 );
889 const result1 = ReactDOMServer.renderToString(element1);
890 const result2 = ReactDOMServer.renderToString(element2);
891 expect(result1.toLowerCase()).not.toContain('onclick');
892 expect(result2.toLowerCase()).not.toContain('script');
893 }
894 assertConsoleErrorDev([
895 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' +
896 ' in div (at **)',
897 'Invalid attribute name: `></div><script>alert("hi")</script>`\n' +
898 ' in div (at **)',
899 ]);
900 });
901
902 it('should reject attribute key injection attack on markup for custom elements (SSR)', () => {
903 for (let i = 0; i < 3; i++) {
904 const element1 = React.createElement(
905 'x-foo-component',
906 {'blah" onclick="beevil" noise="hi': 'selected'},
907 null,
908 );
909 const element2 = React.createElement(
910 'x-foo-component',
911 {'></x-foo-component><script>alert("hi")</script>': 'selected'},
912 null,
913 );
914 const result1 = ReactDOMServer.renderToString(element1);
915 const result2 = ReactDOMServer.renderToString(element2);
916 expect(result1.toLowerCase()).not.toContain('onclick');
917 expect(result2.toLowerCase()).not.toContain('script');
918 }
919 assertConsoleErrorDev([
920 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' +
921 ' in x-foo-component (at **)',
922 'Invalid attribute name: `></x-foo-component><script>alert("hi")</script>`\n' +
923 ' in x-foo-component (at **)',
924 ]);
925 });
926
927 it('should reject attribute key injection attack on mount for regular DOM', async () => {
928 for (let i = 0; i < 3; i++) {
929 const container = document.createElement('div');
930 let root = ReactDOMClient.createRoot(container);
931 await act(() => {
932 root.render(
933 React.createElement(
934 'div',
935 {'blah" onclick="beevil" noise="hi': 'selected'},
936 null,
937 ),
938 );
939 });
940
941 expect(container.firstChild.attributes.length).toBe(0);
942 if (i === 0) {
943 assertConsoleErrorDev([
944 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' +
945 ' in div (at **)',
946 ]);
947 }
948 await act(() => {
949 root.unmount();
950 });
951 root = ReactDOMClient.createRoot(container);
952 await act(() => {
953 root.render(
954 React.createElement(
955 'div',
956 {'></div><script>alert("hi")</script>': 'selected'},
957 null,
958 ),
959 );
960 });
961 if (i === 0) {
962 assertConsoleErrorDev([
963 'Invalid attribute name: `></div><script>alert("hi")</script>`\n' +
964 ' in div (at **)',
965 ]);
966 }
967
968 expect(container.firstChild.attributes.length).toBe(0);
969 }
970 });
971
972 it('should reject attribute key injection attack on mount for custom elements', async () => {
973 for (let i = 0; i < 3; i++) {
974 const container = document.createElement('div');
975 let root = ReactDOMClient.createRoot(container);
976
977 await act(() => {
978 root.render(
979 React.createElement(
980 'x-foo-component',
981 {'blah" onclick="beevil" noise="hi': 'selected'},
982 null,
983 ),
984 );
985 });
986
987 if (i === 0) {
988 assertConsoleErrorDev([
989 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' +
990 ' in x-foo-component (at **)',
991 ]);
992 }
993 expect(container.firstChild.attributes.length).toBe(0);
994 await act(() => {
995 root.unmount();
996 });
997
998 root = ReactDOMClient.createRoot(container);
999 await act(() => {
1000 root.render(
1001 React.createElement(
1002 'x-foo-component',
1003 {'></x-foo-component><script>alert("hi")</script>': 'selected'},
1004 null,
1005 ),
1006 );
1007 });
1008
1009 if (i === 0) {
1010 assertConsoleErrorDev([
1011 'Invalid attribute name: `></x-foo-component><script>alert("hi")</script>`\n' +
1012 ' in x-foo-component (at **)',
1013 ]);
1014 }
1015 expect(container.firstChild.attributes.length).toBe(0);
1016 }
1017 });
1018
1019 it('should reject attribute key injection attack on update for regular DOM', async () => {
1020 for (let i = 0; i < 3; i++) {
1021 const container = document.createElement('div');
1022 const beforeUpdate = React.createElement('div', {}, null);
1023 const root = ReactDOMClient.createRoot(container);
1024 await act(() => {
1025 root.render(beforeUpdate);
1026 });
1027 await act(() => {
1028 root.render(
1029 React.createElement(
1030 'div',
1031 {'blah" onclick="beevil" noise="hi': 'selected'},
1032 null,
1033 ),
1034 );
1035 });
1036
1037 if (i === 0) {
1038 assertConsoleErrorDev([
1039 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' +
1040 ' in div (at **)',
1041 ]);
1042 }
1043 expect(container.firstChild.attributes.length).toBe(0);
1044 await act(() => {
1045 root.render(
1046 React.createElement(
1047 'div',
1048 {'></div><script>alert("hi")</script>': 'selected'},
1049 null,
1050 ),
1051 );
1052 });
1053 if (i === 0) {
1054 assertConsoleErrorDev([
1055 'Invalid attribute name: `></div><script>alert("hi")</script>`\n' +
1056 ' in div (at **)',
1057 ]);
1058 }
1059
1060 expect(container.firstChild.attributes.length).toBe(0);
1061 }
1062 });
1063
1064 it('should reject attribute key injection attack on update for custom elements', async () => {
1065 for (let i = 0; i < 3; i++) {
1066 const container = document.createElement('div');
1067 const beforeUpdate = React.createElement('x-foo-component', {}, null);
1068 const root = ReactDOMClient.createRoot(container);
1069 await act(() => {
1070 root.render(beforeUpdate);
1071 });
1072 await act(() => {
1073 root.render(
1074 React.createElement(
1075 'x-foo-component',
1076 {'blah" onclick="beevil" noise="hi': 'selected'},
1077 null,
1078 ),
1079 );
1080 });
1081
1082 if (i === 0) {
1083 assertConsoleErrorDev([
1084 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' +
1085 ' in x-foo-component (at **)',
1086 ]);
1087 }
1088 expect(container.firstChild.attributes.length).toBe(0);
1089 await act(() => {
1090 root.render(
1091 React.createElement(
1092 'x-foo-component',
1093 {'></x-foo-component><script>alert("hi")</script>': 'selected'},
1094 null,
1095 ),
1096 );
1097 });
1098
1099 if (i === 0) {
1100 assertConsoleErrorDev([
1101 'Invalid attribute name: `></x-foo-component><script>alert("hi")</script>`\n' +
1102 ' in x-foo-component (at **)',
1103 ]);
1104 }
1105 expect(container.firstChild.attributes.length).toBe(0);
1106 }
1107 });
1108
1109 it('should update arbitrary attributes for tags containing dashes', async () => {
1110 const container = document.createElement('div');
1111 const root = ReactDOMClient.createRoot(container);
1112
1113 const beforeUpdate = React.createElement('x-foo-component', {}, null);
1114 await act(() => {
1115 root.render(beforeUpdate);
1116 });
1117
1118 const afterUpdate = <x-foo-component myattr="myval" />;
1119 await act(() => {
1120 root.render(afterUpdate);
1121 });
1122
1123 expect(container.childNodes[0].getAttribute('myattr')).toBe('myval');
1124 });
1125
1126 it('should clear all the styles when removing `style`', async () => {
1127 const styles = {display: 'none', color: 'red'};
1128 const container = document.createElement('div');
1129 const root = ReactDOMClient.createRoot(container);
1130 await act(() => {
1131 root.render(<div style={styles} />);
1132 });
1133
1134 const stubStyle = container.firstChild.style;
1135
1136 await act(() => {
1137 root.render(<div />);
1138 });
1139 expect(stubStyle.display).toEqual('');
1140 expect(stubStyle.color).toEqual('');
1141 });
1142
1143 it('should update styles when `style` changes from null to object', async () => {
1144 const container = document.createElement('div');
1145 const root = ReactDOMClient.createRoot(container);
1146 const styles = {color: 'red'};
1147 await act(() => {
1148 root.render(<div style={styles} />);
1149 });
1150 const stubStyle = container.firstChild.style;
1151 expect(stubStyle.color).toBe('red');
1152 await act(() => {
1153 root.render(<div />);
1154 });
1155 expect(stubStyle.color).toBe('');
1156 await act(() => {
1157 root.render(<div style={styles} />);
1158 });
1159
1160 expect(stubStyle.color).toBe('red');
1161 });
1162
1163 it('should not reset innerHTML for when children is null', async () => {
1164 const container = document.createElement('div');
1165 const root = ReactDOMClient.createRoot(container);
1166 await act(() => {
1167 root.render(<div />);
1168 });
1169 container.firstChild.innerHTML = 'bonjour';
1170 expect(container.firstChild.innerHTML).toEqual('bonjour');
1171
1172 await act(() => {
1173 root.render(<div />);
1174 });
1175 expect(container.firstChild.innerHTML).toEqual('bonjour');
1176 });
1177
1178 it('should reset innerHTML when switching from a direct text child to an empty child', async () => {
1179 const transitionToValues = [null, undefined, false];
1180 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
1181 for (const transitionToValue of transitionToValues) {
1182 const container = document.createElement('div');
1183 const root = ReactDOMClient.createRoot(container);
1184 await act(() => {
1185 root.render(<div>bonjour</div>);
1186 });
1187 expect(container.firstChild.innerHTML).toEqual('bonjour');
1188
1189 await act(() => {
1190 root.render(<div>{transitionToValue}</div>);
1191 });
1192 expect(container.firstChild.innerHTML).toEqual('');
1193 }
1194 });
1195
1196 it('should empty element when removing innerHTML', async () => {
1197 const container = document.createElement('div');
1198 const root = ReactDOMClient.createRoot(container);
1199 await act(() => {
1200 root.render(<div dangerouslySetInnerHTML={{__html: ':)'}} />);
1201 });
1202
1203 expect(container.firstChild.innerHTML).toEqual(':)');
1204 await act(() => {
1205 root.render(<div />);
1206 });
1207 expect(container.firstChild.innerHTML).toEqual('');
1208 });
1209
1210 it('should transition from string content to innerHTML', async () => {
1211 const container = document.createElement('div');
1212 const root = ReactDOMClient.createRoot(container);
1213 await act(() => {
1214 root.render(<div>hello</div>);
1215 });
1216
1217 expect(container.firstChild.innerHTML).toEqual('hello');
1218 await act(() => {
1219 root.render(<div dangerouslySetInnerHTML={{__html: 'goodbye'}} />);
1220 });
1221 expect(container.firstChild.innerHTML).toEqual('goodbye');
1222 });
1223
1224 it('should transition from innerHTML to string content', async () => {
1225 const container = document.createElement('div');
1226 const root = ReactDOMClient.createRoot(container);
1227 await act(() => {
1228 root.render(<div dangerouslySetInnerHTML={{__html: 'bonjour'}} />);
1229 });
1230
1231 expect(container.firstChild.innerHTML).toEqual('bonjour');
1232 await act(() => {
1233 root.render(<div>adieu</div>);
1234 });
1235 expect(container.firstChild.innerHTML).toEqual('adieu');
1236 });
1237
1238 it('should transition from innerHTML to children in nested el', async () => {
1239 const container = document.createElement('div');
1240 const root = ReactDOMClient.createRoot(container);
1241 await act(() => {
1242 root.render(
1243 <div>
1244 <div dangerouslySetInnerHTML={{__html: 'bonjour'}} />
1245 </div>,
1246 );
1247 });
1248
1249 expect(container.textContent).toEqual('bonjour');
1250 await act(() => {
1251 root.render(
1252 <div>
1253 <div>
1254 <span>adieu</span>
1255 </div>
1256 </div>,
1257 );
1258 });
1259 expect(container.textContent).toEqual('adieu');
1260 });
1261
1262 it('should transition from children to innerHTML in nested el', async () => {
1263 const container = document.createElement('div');
1264 const root = ReactDOMClient.createRoot(container);
1265 await act(() => {
1266 root.render(
1267 <div>
1268 <div>
1269 <span>adieu</span>
1270 </div>
1271 </div>,
1272 );
1273 });
1274
1275 expect(container.textContent).toEqual('adieu');
1276 await act(() => {
1277 root.render(
1278 <div>
1279 <div dangerouslySetInnerHTML={{__html: 'bonjour'}} />
1280 </div>,
1281 );
1282 });
1283 expect(container.textContent).toEqual('bonjour');
1284 });
1285
1286 it('should not incur unnecessary DOM mutations for equal innerHTML', async () => {
1287 // Regression test for https://github.com/facebook/react/issues/30994.
1288 // Reassigning equal innerHTML destroys and recreates the child nodes,
1289 // which breaks in-progress gestures (e.g. swallows an in-flight click
1290 // when a re-render commits between focus and click) and discards state
1291 // like text selection.
1292 const container = document.createElement('div');
1293 const root = ReactDOMClient.createRoot(container);
1294 await act(() => {
1295 root.render(
1296 <div dangerouslySetInnerHTML={{__html: '<span>hi</span>'}} />,
1297 );
1298 });
1299
1300 const node = container.firstChild;
1301 const child = node.firstChild;
1302
1303 // A new object with an equal __html string must not touch the DOM.
1304 await act(() => {
1305 root.render(
1306 <div dangerouslySetInnerHTML={{__html: '<span>hi</span>'}} />,
1307 );
1308 });
1309 expect(node.firstChild).toBe(child);
1310
1311 // A different __html string still updates.
1312 await act(() => {
1313 root.render(
1314 <div dangerouslySetInnerHTML={{__html: '<span>bye</span>'}} />,
1315 );
1316 });
1317 expect(node.firstChild).not.toBe(child);
1318 expect(node.innerHTML).toEqual('<span>bye</span>');
1319 });
1320
1321 it('should not incur unnecessary DOM mutations for attributes', async () => {
1322 const container = document.createElement('div');
1323 const root = ReactDOMClient.createRoot(container);
1324 await act(() => {
1325 root.render(<div id="" />);
1326 });
1327
1328 const node = container.firstChild;
1329 const nodeSetAttribute = node.setAttribute;
1330 node.setAttribute = jest.fn();
1331 node.setAttribute.mockImplementation(nodeSetAttribute);
1332
1333 const nodeRemoveAttribute = node.removeAttribute;
1334 node.removeAttribute = jest.fn();
1335 node.removeAttribute.mockImplementation(nodeRemoveAttribute);
1336
1337 await act(() => {
1338 root.render(<div id="" />);
1339 });
1340 expect(node.setAttribute).toHaveBeenCalledTimes(0);
1341 expect(node.removeAttribute).toHaveBeenCalledTimes(0);
1342
1343 await act(() => {
1344 root.render(<div id="foo" />);
1345 });
1346 expect(node.setAttribute).toHaveBeenCalledTimes(1);
1347 expect(node.removeAttribute).toHaveBeenCalledTimes(0);
1348
1349 await act(() => {
1350 root.render(<div id="foo" />);
1351 });
1352 expect(node.setAttribute).toHaveBeenCalledTimes(1);
1353 expect(node.removeAttribute).toHaveBeenCalledTimes(0);
1354
1355 await act(() => {
1356 root.render(<div />);
1357 });
1358 expect(node.setAttribute).toHaveBeenCalledTimes(1);
1359 expect(node.removeAttribute).toHaveBeenCalledTimes(1);
1360
1361 await act(() => {
1362 root.render(<div id="" />);
1363 });
1364 expect(node.setAttribute).toHaveBeenCalledTimes(2);
1365 expect(node.removeAttribute).toHaveBeenCalledTimes(1);
1366
1367 await act(() => {
1368 root.render(<div />);
1369 });
1370 expect(node.setAttribute).toHaveBeenCalledTimes(2);
1371 expect(node.removeAttribute).toHaveBeenCalledTimes(2);
1372 });
1373
1374 it('should not incur unnecessary DOM mutations for string properties', async () => {
1375 const container = document.createElement('div');
1376 const root = ReactDOMClient.createRoot(container);
1377 await act(() => {
1378 root.render(<div value="" />);
1379 });
1380
1381 const node = container.firstChild;
1382
1383 const nodeValueSetter = jest.fn();
1384
1385 const oldSetAttribute = node.setAttribute.bind(node);
1386 node.setAttribute = function (key, value) {
1387 oldSetAttribute(key, value);
1388 nodeValueSetter(key, value);
1389 };
1390
1391 await act(() => {
1392 root.render(<div value="foo" />);
1393 });
1394 expect(nodeValueSetter).toHaveBeenCalledTimes(1);
1395
1396 await act(() => {
1397 root.render(<div value="foo" />);
1398 });
1399 expect(nodeValueSetter).toHaveBeenCalledTimes(1);
1400
1401 await act(() => {
1402 root.render(<div />);
1403 });
1404 expect(nodeValueSetter).toHaveBeenCalledTimes(1);
1405
1406 await act(() => {
1407 root.render(<div value={null} />);
1408 });
1409 expect(nodeValueSetter).toHaveBeenCalledTimes(1);
1410
1411 await act(() => {
1412 root.render(<div value="" />);
1413 });
1414 expect(nodeValueSetter).toHaveBeenCalledTimes(2);
1415
1416 await act(() => {
1417 root.render(<div />);
1418 });
1419 expect(nodeValueSetter).toHaveBeenCalledTimes(2);
1420 });
1421
1422 it('should not incur unnecessary DOM mutations for controlled string properties', async () => {
1423 function onChange() {}
1424 const container = document.createElement('div');
1425 const root = ReactDOMClient.createRoot(container);
1426 await act(() => {
1427 root.render(<input value="" onChange={onChange} />);
1428 });
1429
1430 const node = container.firstChild;
1431
1432 let nodeValue = '';
1433 const nodeValueSetter = jest.fn();
1434 Object.defineProperty(node, 'value', {
1435 get: function () {
1436 return nodeValue;
1437 },
1438 set: nodeValueSetter.mockImplementation(function (newValue) {
1439 nodeValue = newValue;
1440 }),
1441 });
1442
1443 await act(() => {
1444 root.render(<input value="foo" onChange={onChange} />);
1445 });
1446 expect(nodeValueSetter).toHaveBeenCalledTimes(1);
1447
1448 await act(() => {
1449 root.render(
1450 <input value="foo" data-unrelated={true} onChange={onChange} />,
1451 );
1452 });
1453 expect(nodeValueSetter).toHaveBeenCalledTimes(1);
1454
1455 await act(() => {
1456 root.render(<input onChange={onChange} />);
1457 });
1458 assertConsoleErrorDev([
1459 'A component is changing a controlled input to be uncontrolled. This is likely caused by ' +
1460 'the value changing from a defined to undefined, which should not happen. Decide between ' +
1461 'using a controlled or uncontrolled input element for the lifetime of the component. ' +
1462 'More info: https://react.dev/link/controlled-components\n' +
1463 ' in input (at **)',
1464 ]);
1465 expect(nodeValueSetter).toHaveBeenCalledTimes(1);
1466
1467 await act(() => {
1468 root.render(<input value={null} onChange={onChange} />);
1469 });
1470 assertConsoleErrorDev([
1471 '`value` prop on `input` should not be null. Consider using an empty string to clear the ' +
1472 'component or `undefined` for uncontrolled components.\n' +
1473 ' in input (at **)',
1474 ]);
1475 expect(nodeValueSetter).toHaveBeenCalledTimes(1);
1476
1477 await act(() => {
1478 root.render(<input value="" onChange={onChange} />);
1479 });
1480 assertConsoleErrorDev([
1481 'A component is changing an uncontrolled input to be controlled. This is likely caused by ' +
1482 'the value changing from undefined to a defined value, which should not happen. Decide between ' +
1483 'using a controlled or uncontrolled input element for the lifetime of the component. ' +
1484 'More info: https://react.dev/link/controlled-components\n' +
1485 ' in input (at **)',
1486 ]);
1487 expect(nodeValueSetter).toHaveBeenCalledTimes(2);
1488
1489 await act(() => {
1490 root.render(<input onChange={onChange} />);
1491 });
1492 expect(nodeValueSetter).toHaveBeenCalledTimes(2);
1493 });
1494
1495 it('should not incur unnecessary DOM mutations for boolean properties', async () => {
1496 const container = document.createElement('div');
1497 const root = ReactDOMClient.createRoot(container);
1498 await act(() => {
1499 root.render(<audio muted={true} />);
1500 });
1501
1502 const node = container.firstChild;
1503 let nodeValue = true;
1504 const nodeValueSetter = jest.fn();
1505 Object.defineProperty(node, 'muted', {
1506 get: function () {
1507 return nodeValue;
1508 },
1509 set: nodeValueSetter.mockImplementation(function (newValue) {
1510 nodeValue = newValue;
1511 }),
1512 });
1513
1514 await act(() => {
1515 root.render(<audio muted={true} data-unrelated="yes" />);
1516 });
1517 expect(nodeValueSetter).toHaveBeenCalledTimes(0);
1518
1519 await act(() => {
1520 root.render(<audio muted={false} data-unrelated="ok" />);
1521 });
1522 expect(nodeValueSetter).toHaveBeenCalledTimes(1);
1523 });
1524
1525 it('should ignore attribute list for elements with the "is" attribute', async () => {
1526 const container = document.createElement('div');
1527 const root = ReactDOMClient.createRoot(container);
1528 await act(() => {
1529 root.render(<button is="test" cowabunga="chevynova" />);
1530 });
1531 expect(container.firstChild.hasAttribute('cowabunga')).toBe(true);
1532 });
1533
1534 it('should warn about non-string "is" attribute', async () => {
1535 const container = document.createElement('div');
1536 const root = ReactDOMClient.createRoot(container);
1537 await act(() => {
1538 root.render(<button is={function () {}} />);
1539 });
1540 assertConsoleErrorDev([
1541 'Received a `function` for a string attribute `is`. If this is expected, cast ' +
1542 'the value to a string.\n' +
1543 ' in button (at **)',
1544 ]);
1545 });
1546
1547 it('should not update when switching between null/undefined', async () => {
1548 const container = document.createElement('div');
1549 const root = ReactDOMClient.createRoot(container);
1550 await act(() => {
1551 root.render(<div />);
1552 });
1553
1554 const setter = jest.fn();
1555 container.firstChild.setAttribute = setter;
1556
1557 await act(() => {
1558 root.render(<div dir={null} />);
1559 });
1560 await act(() => {
1561 root.render(<div dir={undefined} />);
1562 });
1563 await act(() => {
1564 root.render(<div />);
1565 });
1566 expect(setter).toHaveBeenCalledTimes(0);
1567 await act(() => {
1568 root.render(<div dir="ltr" />);
1569 });
1570 expect(setter).toHaveBeenCalledTimes(1);
1571 });
1572
1573 it('handles multiple child updates without interference', async () => {
1574 // This test might look like it's just testing ReactMultiChild but the
1575 // last bug in this was actually in DOMChildrenOperations so this test
1576 // needs to be in some DOM-specific test file.
1577 const container = document.createElement('div');
1578 const root = ReactDOMClient.createRoot(container);
1579
1580 // ABCD
1581 await act(() => {
1582 root.render(
1583 <div>
1584 <div key="one">
1585 <div key="A">A</div>
1586 <div key="B">B</div>
1587 </div>
1588 <div key="two">
1589 <div key="C">C</div>
1590 <div key="D">D</div>
1591 </div>
1592 </div>,
1593 );
1594 });
1595 // BADC
1596 await act(() => {
1597 root.render(
1598 <div>
1599 <div key="one">
1600 <div key="B">B</div>
1601 <div key="A">A</div>
1602 </div>
1603 <div key="two">
1604 <div key="D">D</div>
1605 <div key="C">C</div>
1606 </div>
1607 </div>,
1608 );
1609 });
1610
1611 expect(container.textContent).toBe('BADC');
1612 });
1613 });
1614
1615 describe('createOpenTagMarkup', () => {
1616 function quoteRegexp(str) {
1617 return String(str).replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1');
1618 }
1619
1620 function expectToHaveAttribute(actual, expected) {
1621 const [attr, value] = expected;
1622 let re = '(?:^|\\s)' + attr + '=[\\\'"]';
1623 if (typeof value !== 'undefined') {
1624 re += quoteRegexp(value) + '[\\\'"]';
1625 }
1626 expect(actual).toMatch(new RegExp(re));
1627 }
1628
1629 function genMarkup(props) {
1630 return ReactDOMServer.renderToString(<div {...props} />);
1631 }
1632
1633 it('should generate the correct markup with className', () => {
1634 expectToHaveAttribute(genMarkup({className: 'a'}), ['class', 'a']);
1635 expectToHaveAttribute(genMarkup({className: 'a b'}), ['class', 'a b']);
1636 expectToHaveAttribute(genMarkup({className: ''}), ['class', '']);
1637 });
1638
1639 it('should escape style names and values', () => {
1640 expectToHaveAttribute(
1641 genMarkup({
1642 style: {'b&ckground': '<3'},
1643 }),
1644 ['style', 'b&amp;ckground:&lt;3'],
1645 );
1646 });
1647 });
1648
1649 describe('createContentMarkup', () => {
1650 function quoteRegexp(str) {
1651 return String(str).replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1');
1652 }
1653
1654 function genMarkup(props) {
1655 return ReactDOMServer.renderToString(<div {...props} />);
1656 }
1657
1658 function toHaveInnerhtml(actual, expected) {
1659 const re = quoteRegexp(expected);
1660 return new RegExp(re).test(actual);
1661 }
1662
1663 it('should handle dangerouslySetInnerHTML', () => {
1664 const innerHTML = {__html: 'testContent'};
1665 expect(
1666 toHaveInnerhtml(
1667 genMarkup({dangerouslySetInnerHTML: innerHTML}),
1668 'testContent',
1669 ),
1670 ).toBe(true);
1671 });
1672 });
1673
1674 describe('mountComponent', () => {
1675 let mountComponent;
1676
1677 beforeEach(() => {
1678 mountComponent = async props => {
1679 const container = document.createElement('div');
1680 const root = ReactDOMClient.createRoot(container);
1681 await act(() => {
1682 root.render(<div {...props} />);
1683 });
1684 };
1685 });
1686
1687 it('should work error event on <source> element', async () => {
1688 const container = document.createElement('div');
1689 const root = ReactDOMClient.createRoot(container);
1690 await act(() => {
1691 root.render(
1692 <video>
1693 <source
1694 src="http://example.org/video"
1695 type="video/mp4"
1696 onError={e => Scheduler.log('onError called')}
1697 />
1698 </video>,
1699 );
1700 });
1701
1702 const errorEvent = document.createEvent('Event');
1703 errorEvent.initEvent('error', false, false);
1704 container.getElementsByTagName('source')[0].dispatchEvent(errorEvent);
1705
1706 if (__DEV__) {
1707 assertLog(['onError called']);
1708 }
1709 });
1710
1711 it('should warn for uppercased selfclosing tags', () => {
1712 class Container extends React.Component {
1713 render() {
1714 return React.createElement('BR', null);
1715 }
1716 }
1717
1718 const returnedValue = ReactDOMServer.renderToString(<Container />);
1719 assertConsoleErrorDev([
1720 '<BR /> is using incorrect casing. ' +
1721 'Use PascalCase for React components, ' +
1722 'or lowercase for HTML elements.\n' +
1723 ' in BR (at **)\n' +
1724 ' in Container (at **)',
1725 ]);
1726 // This includes a duplicate tag because we didn't treat this as self-closing.
1727 expect(returnedValue).toContain('</BR>');
1728 });
1729
1730 it('should warn on upper case HTML tags, not SVG nor custom tags', async () => {
1731 let container = document.createElement('div');
1732 let root = ReactDOMClient.createRoot(container);
1733 await act(() => {
1734 root.render(
1735 React.createElement('svg', null, React.createElement('PATH')),
1736 );
1737 });
1738
1739 container = document.createElement('div');
1740 root = ReactDOMClient.createRoot(container);
1741 await act(() => {
1742 root.render(React.createElement('CUSTOM-TAG'));
1743 });
1744
1745 container = document.createElement('div');
1746 root = ReactDOMClient.createRoot(container);
1747
1748 await act(() => {
1749 root.render(React.createElement('IMG'));
1750 });
1751 assertConsoleErrorDev([
1752 '<IMG /> is using incorrect casing. ' +
1753 'Use PascalCase for React components, ' +
1754 'or lowercase for HTML elements.\n' +
1755 ' in IMG (at **)',
1756 ]);
1757 });
1758
1759 it('should warn on props reserved for future use', async () => {
1760 const container = document.createElement('div');
1761 const root = ReactDOMClient.createRoot(container);
1762
1763 await act(() => {
1764 root.render(<div aria="hello" />);
1765 });
1766 assertConsoleErrorDev([
1767 'The `aria` attribute is reserved for future use in React. ' +
1768 'Pass individual `aria-` attributes instead.\n' +
1769 ' in div (at **)',
1770 ]);
1771 });
1772
1773 it('should warn if the tag is unrecognized', async () => {
1774 let realToString;
1775 try {
1776 realToString = Object.prototype.toString;
1777 const wrappedToString = function () {
1778 // Emulate browser behavior which is missing in jsdom
1779 if (this instanceof window.HTMLUnknownElement) {
1780 return '[object HTMLUnknownElement]';
1781 }
1782 return realToString.apply(this, arguments);
1783 };
1784 Object.prototype.toString = wrappedToString; // eslint-disable-line no-extend-native
1785
1786 const root = ReactDOMClient.createRoot(document.createElement('div'));
1787
1788 await act(() => {
1789 root.render(<bar />);
1790 });
1791 assertConsoleErrorDev([
1792 'The tag <bar> is unrecognized in this browser. ' +
1793 'If you meant to render a React component, start its name with an uppercase letter.\n' +
1794 ' in bar (at **)',
1795 ]);
1796 // Test deduplication
1797 await act(() => {
1798 root.render(<foo />);
1799 });
1800 assertConsoleErrorDev([
1801 'The tag <foo> is unrecognized in this browser. ' +
1802 'If you meant to render a React component, start its name with an uppercase letter.\n' +
1803 ' in foo (at **)',
1804 ]);
1805 await act(() => {
1806 root.render(<foo />);
1807 });
1808 await act(() => {
1809 root.render(<time />);
1810 });
1811
1812 // Corner case. Make sure out deduplication logic doesn't break with weird tag.
1813 await act(() => {
1814 root.render(<hasOwnProperty />);
1815 });
1816 assertConsoleErrorDev([
1817 '<hasOwnProperty /> is using incorrect casing. ' +
1818 'Use PascalCase for React components, or lowercase for HTML elements.\n' +
1819 ' in hasOwnProperty (at **)',
1820 'The tag <hasOwnProperty> is unrecognized in this browser. ' +
1821 'If you meant to render a React component, start its name with an uppercase letter.\n' +
1822 ' in hasOwnProperty (at **)',
1823 ]);
1824 } finally {
1825 Object.prototype.toString = realToString; // eslint-disable-line no-extend-native
1826 }
1827 });
1828
1829 it('should throw on children for void elements', async () => {
1830 const container = document.createElement('div');
1831 const root = ReactDOMClient.createRoot(container);
1832 await expect(async () => {
1833 await act(() => {
1834 root.render(<input>children</input>);
1835 });
1836 }).rejects.toThrow(
1837 'input is a void element tag and must neither have `children` nor ' +
1838 'use `dangerouslySetInnerHTML`.',
1839 );
1840 });
1841
1842 it('should throw on dangerouslySetInnerHTML for void elements', async () => {
1843 const container = document.createElement('div');
1844 const root = ReactDOMClient.createRoot(container);
1845 await expect(async () => {
1846 await act(() => {
1847 root.render(<input dangerouslySetInnerHTML={{__html: 'content'}} />);
1848 });
1849 }).rejects.toThrow(
1850 'input is a void element tag and must neither have `children` nor ' +
1851 'use `dangerouslySetInnerHTML`.',
1852 );
1853 });
1854
1855 it('should treat menuitem as a void element but still create the closing tag', async () => {
1856 // menuitem is not implemented in jsdom, so this triggers the unknown warning error
1857 const container = document.createElement('div');
1858 const root = ReactDOMClient.createRoot(container);
1859
1860 const returnedValue = ReactDOMServer.renderToString(
1861 <menu>
1862 <menuitem />
1863 </menu>,
1864 );
1865
1866 expect(returnedValue).toContain('</menuitem>');
1867
1868 await expect(async () => {
1869 await act(() => {
1870 root.render(
1871 <menu>
1872 <menuitem>children</menuitem>
1873 </menu>,
1874 );
1875 });
1876 }).rejects.toThrow(
1877 'menuitem is a void element tag and must neither have `children` nor use ' +
1878 '`dangerouslySetInnerHTML`.',
1879 );
1880 assertConsoleErrorDev([
1881 'The tag <menuitem> is unrecognized in this browser. ' +
1882 'If you meant to render a React component, start its name with an uppercase letter.\n' +
1883 ' in menuitem (at **)',
1884 ]);
1885 });
1886
1887 it('should validate against multiple children props', async () => {
1888 await expect(async () => {
1889 await mountComponent({children: '', dangerouslySetInnerHTML: ''});
1890 }).rejects.toThrow(
1891 '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
1892 'Please visit https://react.dev/link/dangerously-set-inner-html for more information.',
1893 );
1894 });
1895
1896 it('should validate against use of innerHTML', async () => {
1897 await mountComponent({innerHTML: '<span>Hi Jim!</span>'});
1898 assertConsoleErrorDev([
1899 'Directly setting property `innerHTML` is not permitted. ' +
1900 'For more information, lookup documentation on `dangerouslySetInnerHTML`.\n' +
1901 ' in div (at **)',
1902 ]);
1903 });
1904
1905 it('should validate against use of innerHTML without case sensitivity', async () => {
1906 await mountComponent({innerhtml: '<span>Hi Jim!</span>'});
1907 assertConsoleErrorDev([
1908 'Directly setting property `innerHTML` is not permitted. ' +
1909 'For more information, lookup documentation on `dangerouslySetInnerHTML`.\n' +
1910 ' in div (at **)',
1911 ]);
1912 });
1913
1914 it('should validate use of dangerouslySetInnerHTM with JSX', async () => {
1915 await expect(async () => {
1916 await mountComponent({dangerouslySetInnerHTML: '<span>Hi Jim!</span>'});
1917 }).rejects.toThrow(
1918 '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
1919 'Please visit https://react.dev/link/dangerously-set-inner-html for more information.',
1920 );
1921 });
1922
1923 it('should validate use of dangerouslySetInnerHTML with object', async () => {
1924 await expect(async () => {
1925 await mountComponent({dangerouslySetInnerHTML: {foo: 'bar'}});
1926 }).rejects.toThrow(
1927 '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
1928 'Please visit https://react.dev/link/dangerously-set-inner-html for more information.',
1929 );
1930 });
1931
1932 it('should allow {__html: null}', async () => {
1933 await expect(async () => {
1934 await mountComponent({dangerouslySetInnerHTML: {__html: null}});
1935 }).not.toThrow();
1936 });
1937
1938 it('should warn about contentEditable and children', async () => {
1939 await mountComponent({contentEditable: true, children: ''});
1940 assertConsoleErrorDev([
1941 'A component is `contentEditable` and contains `children` ' +
1942 'managed by React. It is now your responsibility to guarantee that ' +
1943 'none of those nodes are unexpectedly modified or duplicated. This ' +
1944 'is probably not intentional.\n' +
1945 ' in div (at **)',
1946 ]);
1947 });
1948
1949 it('should respect suppressContentEditableWarning', async () => {
1950 await mountComponent({
1951 contentEditable: true,
1952 children: '',
1953 suppressContentEditableWarning: true,
1954 });
1955 });
1956
1957 it('should validate against invalid styles', async () => {
1958 await expect(async () => {
1959 await mountComponent({style: 'display: none'});
1960 }).rejects.toThrow(
1961 'The `style` prop expects a mapping from style properties to values, ' +
1962 "not a string. For example, style={{marginRight: spacing + 'em'}} " +
1963 'when using JSX.',
1964 );
1965 });
1966
1967 it('should throw for children on void elements', async () => {
1968 class X extends React.Component {
1969 render() {
1970 return <input>moo</input>;
1971 }
1972 }
1973
1974 const container = document.createElement('div');
1975 const root = ReactDOMClient.createRoot(container);
1976 await expect(async () => {
1977 await act(() => {
1978 root.render(<X />);
1979 });
1980 }).rejects.toThrow(
1981 'input is a void element tag and must neither have `children` ' +
1982 'nor use `dangerouslySetInnerHTML`.',
1983 );
1984 });
1985
1986 it('should support custom elements which extend native elements', async () => {
1987 const container = document.createElement('div');
1988 const root = ReactDOMClient.createRoot(container);
1989 spyOnDevAndProd(document, 'createElement');
1990 await act(() => {
1991 root.render(<div is="custom-div" />);
1992 });
1993 expect(document.createElement).toHaveBeenCalledWith('div', {
1994 is: 'custom-div',
1995 });
1996 });
1997
1998 it('should work load and error events on <image> element in SVG', async () => {
1999 const container = document.createElement('div');
2000 const root = ReactDOMClient.createRoot(container);
2001 await act(() => {
2002 root.render(
2003 <svg>
2004 <image
2005 xlinkHref="http://example.org/image"
2006 onError={e => Scheduler.log('onError called')}
2007 onLoad={e => Scheduler.log('onLoad called')}
2008 />
2009 </svg>,
2010 );
2011 });
2012
2013 const loadEvent = document.createEvent('Event');
2014 const errorEvent = document.createEvent('Event');
2015
2016 loadEvent.initEvent('load', false, false);
2017 errorEvent.initEvent('error', false, false);
2018
2019 container.getElementsByTagName('image')[0].dispatchEvent(errorEvent);
2020 container.getElementsByTagName('image')[0].dispatchEvent(loadEvent);
2021
2022 if (__DEV__) {
2023 assertLog(['onError called', 'onLoad called']);
2024 }
2025 });
2026
2027 it('should receive a load event on <link> elements', async () => {
2028 const container = document.createElement('div');
2029 const root = ReactDOMClient.createRoot(container);
2030 const onLoad = jest.fn();
2031
2032 await act(() => {
2033 root.render(<link href="http://example.org/link" onLoad={onLoad} />);
2034 });
2035
2036 const loadEvent = document.createEvent('Event');
2037 const link = container.getElementsByTagName('link')[0];
2038
2039 loadEvent.initEvent('load', false, false);
2040 link.dispatchEvent(loadEvent);
2041
2042 expect(onLoad).toHaveBeenCalledTimes(1);
2043 });
2044
2045 it('should receive an error event on <link> elements', async () => {
2046 const container = document.createElement('div');
2047 const root = ReactDOMClient.createRoot(container);
2048 const onError = jest.fn();
2049
2050 await act(() => {
2051 root.render(<link href="http://example.org/link" onError={onError} />);
2052 });
2053
2054 const errorEvent = document.createEvent('Event');
2055 const link = container.getElementsByTagName('link')[0];
2056
2057 errorEvent.initEvent('error', false, false);
2058 link.dispatchEvent(errorEvent);
2059
2060 expect(onError).toHaveBeenCalledTimes(1);
2061 });
2062 });
2063
2064 describe('updateComponent', () => {
2065 let container;
2066 let root;
2067
2068 beforeEach(() => {
2069 container = document.createElement('div');
2070 root = ReactDOMClient.createRoot(container);
2071 });
2072
2073 it('should warn against children for void elements', async () => {
2074 await act(() => {
2075 root.render(<input />);
2076 });
2077
2078 await expect(async () => {
2079 await act(() => {
2080 root.render(<input>children</input>);
2081 });
2082 }).rejects.toThrow(
2083 'input is a void element tag and must neither have `children` nor use ' +
2084 '`dangerouslySetInnerHTML`.',
2085 );
2086 });
2087
2088 it('should warn against dangerouslySetInnerHTML for void elements', async () => {
2089 await act(() => {
2090 root.render(<input />);
2091 });
2092
2093 await expect(async () => {
2094 await act(() => {
2095 root.render(<input dangerouslySetInnerHTML={{__html: 'content'}} />);
2096 });
2097 }).rejects.toThrow(
2098 'input is a void element tag and must neither have `children` nor use ' +
2099 '`dangerouslySetInnerHTML`.',
2100 );
2101 });
2102
2103 it('should validate against multiple children props', async () => {
2104 await act(() => {
2105 root.render(<div />);
2106 });
2107
2108 await expect(async () => {
2109 await act(() => {
2110 root.render(
2111 <div children="" dangerouslySetInnerHTML={{__html: ''}} />,
2112 );
2113 });
2114 }).rejects.toThrow(
2115 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
2116 );
2117 });
2118
2119 it('should warn about contentEditable and children', async () => {
2120 await act(() => {
2121 root.render(
2122 <div contentEditable={true}>
2123 <div />
2124 </div>,
2125 );
2126 });
2127 assertConsoleErrorDev([
2128 'A component is `contentEditable` and contains `children` managed by React. ' +
2129 'It is now your responsibility to guarantee that none of those nodes are unexpectedly modified or duplicated. ' +
2130 'This is probably not intentional.\n' +
2131 ' in div (at **)',
2132 ]);
2133 });
2134
2135 it('should validate against invalid styles', async () => {
2136 await act(() => {
2137 root.render(<div />);
2138 });
2139
2140 await expect(async () => {
2141 await act(() => {
2142 root.render(<div style={1} />);
2143 });
2144 }).rejects.toThrow(
2145 'The `style` prop expects a mapping from style properties to values, ' +
2146 "not a string. For example, style={{marginRight: spacing + 'em'}} " +
2147 'when using JSX.',
2148 );
2149 });
2150
2151 it('should report component containing invalid styles', async () => {
2152 class Animal extends React.Component {
2153 render() {
2154 return <div style={1} />;
2155 }
2156 }
2157
2158 await expect(async () => {
2159 await act(() => {
2160 root.render(<Animal />);
2161 });
2162 }).rejects.toThrow(
2163 'The `style` prop expects a mapping from style properties to values, ' +
2164 "not a string. For example, style={{marginRight: spacing + 'em'}} " +
2165 'when using JSX.',
2166 );
2167 });
2168
2169 it('should properly escape text content and attributes values', () => {
2170 expect(
2171 ReactDOMServer.renderToStaticMarkup(
2172 React.createElement(
2173 'div',
2174 {
2175 title: '\'"<>&',
2176 style: {
2177 textAlign: '\'"<>&',
2178 },
2179 },
2180 '\'"<>&',
2181 ),
2182 ),
2183 ).toBe(
2184 '<div title="&#x27;&quot;&lt;&gt;&amp;" style="text-align:&#x27;&quot;&lt;&gt;&amp;">' +
2185 '&#x27;&quot;&lt;&gt;&amp;' +
2186 '</div>',
2187 );
2188 });
2189 });
2190
2191 describe('unmountComponent', () => {
2192 it('unmounts children before unsetting DOM node info', async () => {
2193 class Inner extends React.Component {
2194 render() {
2195 return <span />;
2196 }
2197
2198 componentWillUnmount() {
2199 // Should not throw
2200 expect(
2201 ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode(
2202 this,
2203 ).nodeName,
2204 ).toBe('SPAN');
2205 }
2206 }
2207
2208 const root = ReactDOMClient.createRoot(document.createElement('div'));
2209 await act(() => {
2210 root.render(
2211 <div>
2212 <Inner />
2213 </div>,
2214 );
2215 });
2216 await act(() => {
2217 root.unmount();
2218 });
2219 });
2220 });
2221
2222 describe('tag sanitization', () => {
2223 it('should throw when an invalid tag name is used server-side', () => {
2224 const hackzor = React.createElement('script tag');
2225 expect(() => ReactDOMServer.renderToString(hackzor)).toThrow(
2226 'Invalid tag: script tag',
2227 );
2228 });
2229
2230 it('should throw when an attack vector is used server-side', () => {
2231 const hackzor = React.createElement('div><img /><div');
2232 expect(() => ReactDOMServer.renderToString(hackzor)).toThrow(
2233 'Invalid tag: div><img /><div',
2234 );
2235 });
2236
2237 it('should throw when an invalid tag name is used', async () => {
2238 const hackzor = React.createElement('script tag');
2239 const container = document.createElement('div');
2240 const root = ReactDOMClient.createRoot(container);
2241 await expect(
2242 act(() => {
2243 root.render(hackzor);
2244 }),
2245 ).rejects.toThrow();
2246 });
2247
2248 it('should throw when an attack vector is used', async () => {
2249 const hackzor = React.createElement('div><img /><div');
2250 const container = document.createElement('div');
2251 const root = ReactDOMClient.createRoot(container);
2252 await expect(
2253 act(() => {
2254 root.render(hackzor);
2255 }),
2256 ).rejects.toThrow();
2257 });
2258 });
2259
2260 describe('nesting validation', () => {
2261 it('warns on invalid nesting', async () => {
2262 const container = document.createElement('div');
2263 const root = ReactDOMClient.createRoot(container);
2264 await act(() => {
2265 root.render(
2266 <div>
2267 <tr />
2268 <tr />
2269 </div>,
2270 );
2271 });
2272 assertConsoleErrorDev([
2273 'In HTML, <tr> cannot be a child of <div>.\n' +
2274 'This will cause a hydration error.\n' +
2275 '\n' +
2276 '> <div>\n' +
2277 '> <tr>\n' +
2278 ' ...\n' +
2279 '\n in tr (at **)',
2280 ]);
2281 });
2282
2283 it('warns on invalid nesting at root', async () => {
2284 const p = document.createElement('p');
2285 const root = ReactDOMClient.createRoot(p);
2286
2287 await act(() => {
2288 root.render(
2289 <span>
2290 <p />
2291 </span>,
2292 );
2293 });
2294 assertConsoleErrorDev([
2295 'In HTML, <p> cannot be a descendant of <p>.\n' +
2296 'This will cause a hydration error.' +
2297 // There is no outer `p` here because root container is not part of the stack.
2298 '\n in p (at **)',
2299 ]);
2300 });
2301
2302 it('warns nicely for table rows', async () => {
2303 class Row extends React.Component {
2304 render() {
2305 return <tr>x</tr>;
2306 }
2307 }
2308
2309 class Foo extends React.Component {
2310 render() {
2311 return (
2312 <table>
2313 <Row />{' '}
2314 </table>
2315 );
2316 }
2317 }
2318
2319 const container = document.createElement('div');
2320 const root = ReactDOMClient.createRoot(container);
2321
2322 await act(() => {
2323 root.render(<Foo />);
2324 });
2325 assertConsoleErrorDev([
2326 'In HTML, <tr> cannot be a child of ' +
2327 '<table>. Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated ' +
2328 'by the browser.\n' +
2329 'This will cause a hydration error.\n' +
2330 '\n' +
2331 ' <Foo>\n' +
2332 '> <table>\n' +
2333 ' <Row>\n' +
2334 '> <tr>\n' +
2335 ' ...\n' +
2336 '\n in tr (at **)' +
2337 '\n in Row (at **)' +
2338 '\n in Foo (at **)',
2339 '<table> cannot contain a nested <tr>.\nSee this log for the ancestor stack trace.' +
2340 '\n in table (at **)' +
2341 '\n in Foo (at **)',
2342 'In HTML, text nodes cannot be a ' +
2343 'child of <tr>.\n' +
2344 'This will cause a hydration error.\n' +
2345 '\n' +
2346 ' <Foo>\n' +
2347 ' <table>\n' +
2348 ' <Row>\n' +
2349 ' <tr>\n' +
2350 '> x\n' +
2351 ' ...\n' +
2352 '\n in tr (at **)' +
2353 '\n in Row (at **)' +
2354 '\n in Foo (at **)',
2355 'In HTML, whitespace text nodes cannot ' +
2356 "be a child of <table>. Make sure you don't have any extra " +
2357 'whitespace between tags on each line of your source code.\n' +
2358 'This will cause a hydration error.\n' +
2359 '\n' +
2360 ' <Foo>\n' +
2361 '> <table>\n' +
2362 ' <Row>\n' +
2363 '> {" "}\n' +
2364 '\n in table (at **)' +
2365 '\n in Foo (at **)',
2366 ]);
2367 });
2368
2369 it('warns nicely for updating table rows to use text', async () => {
2370 const root = ReactDOMClient.createRoot(document.createElement('div'));
2371
2372 function Row({children}) {
2373 return <tr>{children}</tr>;
2374 }
2375
2376 function Foo({children}) {
2377 return <table>{children}</table>;
2378 }
2379
2380 // First is fine.
2381 await act(() => {
2382 root.render(<Foo />);
2383 });
2384
2385 await act(() => {
2386 root.render(<Foo> </Foo>);
2387 });
2388 assertConsoleErrorDev([
2389 'In HTML, whitespace text nodes cannot ' +
2390 "be a child of <table>. Make sure you don't have any extra " +
2391 'whitespace between tags on each line of your source code.\n' +
2392 'This will cause a hydration error.\n' +
2393 '\n' +
2394 ' <Foo>\n' +
2395 ' <table>\n' +
2396 '> {" "}\n' +
2397 '\n in table (at **)' +
2398 '\n in Foo (at **)',
2399 ]);
2400
2401 await act(() => {
2402 root.render(
2403 <Foo>
2404 <tbody>
2405 <Row />
2406 </tbody>
2407 </Foo>,
2408 );
2409 });
2410
2411 await act(() => {
2412 root.render(
2413 <Foo>
2414 <tbody>
2415 <Row>text</Row>
2416 </tbody>
2417 </Foo>,
2418 );
2419 });
2420 assertConsoleErrorDev([
2421 'In HTML, text nodes cannot be a ' +
2422 'child of <tr>.\n' +
2423 'This will cause a hydration error.\n' +
2424 '\n' +
2425 ' <Foo>\n' +
2426 ' <table>\n' +
2427 ' <tbody>\n' +
2428 ' <Row>\n' +
2429 ' <tr>\n' +
2430 '> text\n' +
2431 '\n in tr (at **)' +
2432 '\n in Row (at **)',
2433 ]);
2434 });
2435
2436 it('gives useful context in warnings', async () => {
2437 function Row() {
2438 return <tr />;
2439 }
2440 function FancyRow() {
2441 return <Row />;
2442 }
2443
2444 function Viz1() {
2445 return (
2446 <table>
2447 <FancyRow />
2448 </table>
2449 );
2450 }
2451 function App1() {
2452 return <Viz1 />;
2453 }
2454 const container = document.createElement('div');
2455 const root = ReactDOMClient.createRoot(container);
2456 await act(() => {
2457 root.render(<App1 />);
2458 });
2459 assertConsoleErrorDev([
2460 'In HTML, <tr> cannot be a child of <table>. ' +
2461 'Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by the browser.\n' +
2462 'This will cause a hydration error.\n' +
2463 '\n' +
2464 ' <App1>\n' +
2465 ' <Viz1>\n' +
2466 '> <table>\n' +
2467 ' <FancyRow>\n' +
2468 ' <Row>\n' +
2469 '> <tr>\n' +
2470 '\n in tr (at **)' +
2471 '\n in Row (at **)' +
2472 '\n in FancyRow (at **)' +
2473 '\n in Viz1 (at **)' +
2474 '\n in App1 (at **)',
2475 '<table> cannot contain a nested <tr>.\n' +
2476 'See this log for the ancestor stack trace.\n' +
2477 ' in table (at **)\n' +
2478 ' in Viz1 (at **)\n' +
2479 ' in App1 (at **)',
2480 ]);
2481 });
2482
2483 it('gives useful context in warnings 2', async () => {
2484 function Row() {
2485 return <tr />;
2486 }
2487 function FancyRow() {
2488 return <Row />;
2489 }
2490
2491 class Table extends React.Component {
2492 render() {
2493 return <table>{this.props.children}</table>;
2494 }
2495 }
2496
2497 class FancyTable extends React.Component {
2498 render() {
2499 return <Table>{this.props.children}</Table>;
2500 }
2501 }
2502
2503 function Viz2() {
2504 return (
2505 <FancyTable>
2506 <FancyRow />
2507 </FancyTable>
2508 );
2509 }
2510 function App2() {
2511 return <Viz2 />;
2512 }
2513 const container = document.createElement('div');
2514 const root = ReactDOMClient.createRoot(container);
2515
2516 await act(() => {
2517 root.render(<App2 />);
2518 });
2519 assertConsoleErrorDev([
2520 'In HTML, <tr> cannot be a child of <table>. ' +
2521 'Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by the browser.\n' +
2522 'This will cause a hydration error.\n' +
2523 '\n' +
2524 ' <App2>\n' +
2525 ' <Viz2>\n' +
2526 ' <FancyTable>\n' +
2527 ' <Table>\n' +
2528 '> <table>\n' +
2529 ' <FancyRow>\n' +
2530 ' <Row>\n' +
2531 '> <tr>\n' +
2532 '\n in tr (at **)' +
2533 '\n in Row (at **)' +
2534 '\n in FancyRow (at **)' +
2535 '\n in Viz2 (at **)' +
2536 '\n in App2 (at **)',
2537 '<table> cannot contain a nested <tr>.\n' +
2538 'See this log for the ancestor stack trace.\n' +
2539 ' in table (at **)\n' +
2540 ' in Table (at **)\n' +
2541 ' in FancyTable (at **)\n' +
2542 ' in Viz2 (at **)\n' +
2543 ' in App2 (at **)',
2544 ]);
2545 });
2546
2547 it('gives useful context in warnings 3', async () => {
2548 function Row() {
2549 return <tr />;
2550 }
2551 function FancyRow() {
2552 return <Row />;
2553 }
2554
2555 class Table extends React.Component {
2556 render() {
2557 return <table>{this.props.children}</table>;
2558 }
2559 }
2560
2561 class FancyTable extends React.Component {
2562 render() {
2563 return <Table>{this.props.children}</Table>;
2564 }
2565 }
2566 const container = document.createElement('div');
2567 const root = ReactDOMClient.createRoot(container);
2568
2569 await act(() => {
2570 root.render(
2571 <FancyTable>
2572 <FancyRow />
2573 </FancyTable>,
2574 );
2575 });
2576 assertConsoleErrorDev([
2577 'In HTML, <tr> cannot be a child of <table>. ' +
2578 'Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by the browser.\n' +
2579 'This will cause a hydration error.\n' +
2580 '\n' +
2581 ' <FancyTable>\n' +
2582 ' <Table>\n' +
2583 '> <table>\n' +
2584 ' <FancyRow>\n' +
2585 ' <Row>\n' +
2586 '> <tr>\n' +
2587 '\n in tr (at **)' +
2588 '\n in Row (at **)' +
2589 '\n in FancyRow (at **)',
2590 '<table> cannot contain a nested <tr>.\n' +
2591 'See this log for the ancestor stack trace.' +
2592 '\n in table (at **)' +
2593 '\n in Table (at **)' +
2594 '\n in FancyTable (at **)',
2595 ]);
2596 });
2597
2598 it('gives useful context in warnings 4', async () => {
2599 function Row() {
2600 return <tr />;
2601 }
2602 function FancyRow() {
2603 return <Row />;
2604 }
2605
2606 const container = document.createElement('div');
2607 const root = ReactDOMClient.createRoot(container);
2608
2609 await act(() => {
2610 root.render(
2611 <table>
2612 <FancyRow />
2613 </table>,
2614 );
2615 });
2616 assertConsoleErrorDev([
2617 'In HTML, <tr> cannot be a child of <table>. ' +
2618 'Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by the browser.\n' +
2619 'This will cause a hydration error.\n' +
2620 '\n' +
2621 '> <table>\n' +
2622 ' <FancyRow>\n' +
2623 ' <Row>\n' +
2624 '> <tr>\n' +
2625 '\n in tr (at **)' +
2626 '\n in Row (at **)' +
2627 '\n in FancyRow (at **)',
2628 '<table> cannot contain a nested <tr>.\n' +
2629 'See this log for the ancestor stack trace.' +
2630 '\n in table (at **)',
2631 ]);
2632 });
2633
2634 it('gives useful context in warnings 5', async () => {
2635 class Table extends React.Component {
2636 render() {
2637 return <table>{this.props.children}</table>;
2638 }
2639 }
2640
2641 class FancyTable extends React.Component {
2642 render() {
2643 return <Table>{this.props.children}</Table>;
2644 }
2645 }
2646
2647 const container = document.createElement('div');
2648 const root = ReactDOMClient.createRoot(container);
2649 await act(() => {
2650 root.render(
2651 <FancyTable>
2652 <tr />
2653 </FancyTable>,
2654 );
2655 });
2656 assertConsoleErrorDev([
2657 'In HTML, <tr> cannot be a child of <table>. ' +
2658 'Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by the browser.\n' +
2659 'This will cause a hydration error.\n' +
2660 '\n' +
2661 ' <FancyTable>\n' +
2662 ' <Table>\n' +
2663 '> <table>\n' +
2664 '> <tr>\n' +
2665 '\n in tr (at **)',
2666 '<table> cannot contain a nested <tr>.\n' +
2667 'See this log for the ancestor stack trace.' +
2668 '\n in table (at **)' +
2669 '\n in Table (at **)' +
2670 '\n in FancyTable (at **)',
2671 ]);
2672
2673 class Link extends React.Component {
2674 render() {
2675 return <a>{this.props.children}</a>;
2676 }
2677 }
2678
2679 await act(() => {
2680 root.render(
2681 <Link>
2682 <div>
2683 <Link />
2684 </div>
2685 </Link>,
2686 );
2687 });
2688 assertConsoleErrorDev([
2689 'In HTML, <a> cannot be a descendant of <a>.\n' +
2690 'This will cause a hydration error.\n' +
2691 '\n' +
2692 ' <Link>\n' +
2693 '> <a>\n' +
2694 ' <div>\n' +
2695 ' <Link>\n' +
2696 '> <a>\n' +
2697 '\n in a (at **)' +
2698 '\n in Link (at **)',
2699 '<a> cannot contain a nested <a>.\n' +
2700 'See this log for the ancestor stack trace.' +
2701 '\n in a (at **)' +
2702 '\n in Link (at **)',
2703 ]);
2704 });
2705
2706 it('should warn about incorrect casing on properties (ssr)', () => {
2707 ReactDOMServer.renderToString(
2708 React.createElement('input', {type: 'text', tabindex: '1'}),
2709 );
2710 assertConsoleErrorDev([
2711 'Invalid DOM property `tabindex`. Did you mean `tabIndex`?\n' +
2712 ' in input (at **)',
2713 ]);
2714 });
2715
2716 it('should warn about incorrect casing on the credentialless property (ssr)', () => {
2717 ReactDOMServer.renderToString(
2718 React.createElement('iframe', {Credentialless: true}),
2719 );
2720 assertConsoleErrorDev([
2721 'Invalid DOM property `Credentialless`. Did you mean `credentialless`?\n' +
2722 ' in iframe (at **)',
2723 ]);
2724 });
2725
2726 it('should warn about incorrect casing on event handlers (ssr)', () => {
2727 ReactDOMServer.renderToString(
2728 React.createElement('input', {type: 'text', oninput: '1'}),
2729 );
2730 assertConsoleErrorDev([
2731 'Invalid event handler property `oninput`. ' +
2732 'React events use the camelCase naming convention, ' +
2733 // Note: we don't know the right event name so we
2734 // use a generic one (onClick) as a suggestion.
2735 // This is because we don't bundle the event system
2736 // on the server.
2737 'for example `onClick`.\n' +
2738 ' in input (at **)',
2739 ]);
2740 ReactDOMServer.renderToString(
2741 React.createElement('input', {type: 'text', onKeydown: '1'}),
2742 );
2743 // We can't warn for `onKeydown` on the server because
2744 // there is no way tell if this is a valid event or not
2745 // without access to the event system (which we don't bundle).
2746 });
2747
2748 it('should warn about incorrect casing on properties', async () => {
2749 const container = document.createElement('div');
2750 const root = ReactDOMClient.createRoot(container);
2751 await act(() => {
2752 root.render(
2753 React.createElement('input', {type: 'text', tabindex: '1'}),
2754 );
2755 });
2756 assertConsoleErrorDev([
2757 'Invalid DOM property `tabindex`. Did you mean `tabIndex`?\n' +
2758 ' in input (at **)',
2759 ]);
2760 });
2761
2762 it('should warn about incorrect casing on event handlers', async () => {
2763 const container = document.createElement('div');
2764 const root = ReactDOMClient.createRoot(container);
2765
2766 await act(() => {
2767 root.render(React.createElement('input', {type: 'text', oninput: '1'}));
2768 });
2769 assertConsoleErrorDev([
2770 'Invalid event handler property `oninput`. Did you mean `onInput`?\n' +
2771 ' in input (at **)',
2772 ]);
2773
2774 await act(() => {
2775 root.render(
2776 React.createElement('input', {type: 'text', onKeydown: '1'}),
2777 );
2778 });
2779 assertConsoleErrorDev([
2780 'Invalid event handler property `onKeydown`. Did you mean `onKeyDown`?\n' +
2781 ' in input (at **)',
2782 ]);
2783 });
2784
2785 it('should warn about class', async () => {
2786 const container = document.createElement('div');
2787 const root = ReactDOMClient.createRoot(container);
2788 await act(() => {
2789 root.render(React.createElement('div', {class: 'muffins'}));
2790 });
2791 assertConsoleErrorDev([
2792 'Invalid DOM property `class`. Did you mean `className`?\n' +
2793 ' in div (at **)',
2794 ]);
2795 });
2796
2797 it('should warn about class (ssr)', () => {
2798 ReactDOMServer.renderToString(
2799 React.createElement('div', {class: 'muffins'}),
2800 );
2801 assertConsoleErrorDev([
2802 'Invalid DOM property `class`. Did you mean `className`?\n' +
2803 ' in div (at **)',
2804 ]);
2805 });
2806
2807 it('should warn about props that are no longer supported', async () => {
2808 let container = document.createElement('div');
2809 let root = ReactDOMClient.createRoot(container);
2810 await act(() => {
2811 root.render(<div />);
2812 });
2813
2814 container = document.createElement('div');
2815 root = ReactDOMClient.createRoot(container);
2816
2817 await act(() => {
2818 root.render(<div onFocusIn={() => {}} />);
2819 });
2820 assertConsoleErrorDev([
2821 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
2822 'All React events are normalized to bubble, so onFocusIn and onFocusOut are not needed/supported by React.\n' +
2823 ' in div (at **)',
2824 ]);
2825 container = document.createElement('div');
2826 root = ReactDOMClient.createRoot(container);
2827 await act(() => {
2828 root.render(<div onFocusOut={() => {}} />);
2829 });
2830 assertConsoleErrorDev([
2831 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
2832 'All React events are normalized to bubble, so onFocusIn and onFocusOut are not needed/supported by React.\n' +
2833 ' in div (at **)',
2834 ]);
2835 });
2836
2837 it('should warn about props that are no longer supported without case sensitivity', async () => {
2838 let container = document.createElement('div');
2839 let root = ReactDOMClient.createRoot(container);
2840 await act(() => {
2841 root.render(<div />);
2842 });
2843
2844 container = document.createElement('div');
2845 root = ReactDOMClient.createRoot(container);
2846 await act(() => {
2847 root.render(<div onfocusin={() => {}} />);
2848 });
2849 assertConsoleErrorDev([
2850 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
2851 'All React events are normalized to bubble, so onFocusIn and onFocusOut are not needed/supported by React.\n' +
2852 ' in div (at **)',
2853 ]);
2854 container = document.createElement('div');
2855 root = ReactDOMClient.createRoot(container);
2856 await act(() => {
2857 root.render(<div onfocusout={() => {}} />);
2858 });
2859 assertConsoleErrorDev([
2860 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
2861 'All React events are normalized to bubble, so onFocusIn and onFocusOut are not needed/supported by React.\n' +
2862 ' in div (at **)',
2863 ]);
2864 });
2865
2866 it('should warn about props that are no longer supported (ssr)', () => {
2867 ReactDOMServer.renderToString(<div />);
2868 ReactDOMServer.renderToString(<div onFocusIn={() => {}} />);
2869 assertConsoleErrorDev([
2870 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
2871 'All React events are normalized to bubble, so onFocusIn and onFocusOut are not needed/supported by React.\n' +
2872 ' in div (at **)',
2873 ]);
2874 ReactDOMServer.renderToString(<div onFocusOut={() => {}} />);
2875 assertConsoleErrorDev([
2876 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
2877 'All React events are normalized to bubble, so onFocusIn and onFocusOut are not needed/supported by React.\n' +
2878 ' in div (at **)',
2879 ]);
2880 });
2881
2882 it('should warn about props that are no longer supported without case sensitivity (ssr)', () => {
2883 ReactDOMServer.renderToString(<div />);
2884 ReactDOMServer.renderToString(<div onfocusin={() => {}} />);
2885 assertConsoleErrorDev([
2886 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
2887 'All React events are normalized to bubble, so onFocusIn and onFocusOut are not needed/supported by React.\n' +
2888 ' in div (at **)',
2889 ]);
2890 ReactDOMServer.renderToString(<div onfocusout={() => {}} />);
2891 assertConsoleErrorDev([
2892 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
2893 'All React events are normalized to bubble, so onFocusIn and onFocusOut are not needed/supported by React.\n' +
2894 ' in div (at **)',
2895 ]);
2896 });
2897
2898 it('gives source code refs for unknown prop warning', async () => {
2899 let container = document.createElement('div');
2900 let root = ReactDOMClient.createRoot(container);
2901 await act(() => {
2902 root.render(<div class="paladin" />);
2903 });
2904 assertConsoleErrorDev([
2905 'Invalid DOM property `class`. Did you mean `className`?\n' +
2906 ' in div (at **)',
2907 ]);
2908 container = document.createElement('div');
2909 root = ReactDOMClient.createRoot(container);
2910 await act(() => {
2911 root.render(<input type="text" onclick="1" />);
2912 });
2913 assertConsoleErrorDev([
2914 'Invalid event handler property `onclick`. Did you mean ' +
2915 '`onClick`?\n' +
2916 ' in input (at **)',
2917 ]);
2918 });
2919
2920 it('gives source code refs for unknown prop warning (ssr)', () => {
2921 ReactDOMServer.renderToString(<div class="paladin" />);
2922 assertConsoleErrorDev([
2923 'Invalid DOM property `class`. Did you mean `className`?\n' +
2924 ' in div (at **)',
2925 ]);
2926 ReactDOMServer.renderToString(<input type="text" oninput="1" />);
2927 assertConsoleErrorDev([
2928 'Invalid event handler property `oninput`. ' +
2929 // Note: we don't know the right event name so we
2930 // use a generic one (onClick) as a suggestion.
2931 // This is because we don't bundle the event system
2932 // on the server.
2933 'React events use the camelCase naming convention, for example `onClick`.\n' +
2934 ' in input (at **)',
2935 ]);
2936 });
2937
2938 it('gives source code refs for unknown prop warning for update render', async () => {
2939 let container = document.createElement('div');
2940 let root = ReactDOMClient.createRoot(container);
2941 await act(() => {
2942 root.render(<div className="paladin" />);
2943 });
2944
2945 container = document.createElement('div');
2946 root = ReactDOMClient.createRoot(container);
2947 await act(() => {
2948 root.render(<div class="paladin" />);
2949 });
2950 assertConsoleErrorDev([
2951 'Invalid DOM property `class`. Did you mean `className`?\n' +
2952 ' in div (at **)',
2953 ]);
2954 });
2955
2956 it('gives source code refs for unknown prop warning for exact elements', async () => {
2957 const container = document.createElement('div');
2958 const root = ReactDOMClient.createRoot(container);
2959 await act(() => {
2960 root.render(
2961 <div className="foo1">
2962 <span class="foo2" />
2963 <div onClick={() => {}} />
2964 <strong onclick={() => {}} />
2965 <div className="foo5" />
2966 <div className="foo6" />
2967 </div>,
2968 );
2969 });
2970 assertConsoleErrorDev([
2971 'Invalid DOM property `class`. Did you mean `className`?\n' +
2972 ' in span (at **)',
2973 'Invalid event handler property `onclick`. Did you mean `onClick`?\n' +
2974 ' in strong (at **)',
2975 ]);
2976 });
2977
2978 it('gives source code refs for unknown prop warning for exact elements (ssr)', () => {
2979 ReactDOMServer.renderToString(
2980 <div className="foo1">
2981 <span class="foo2" />
2982 <div onClick="foo3" />
2983 <strong onclick="foo4" />
2984 <div className="foo5" />
2985 <div className="foo6" />
2986 </div>,
2987 );
2988 assertConsoleErrorDev([
2989 'Invalid DOM property `class`. Did you mean `className`?\n' +
2990 ' in span (at **)',
2991 'Invalid event handler property `onclick`. ' +
2992 'React events use the camelCase naming convention, for example `onClick`.\n' +
2993 ' in strong (at **)',
2994 ]);
2995 });
2996
2997 it('gives source code refs for unknown prop warning for exact elements in composition', async () => {
2998 class Parent extends React.Component {
2999 render() {
3000 return (
3001 <div>
3002 <Child1 />
3003 <Child2 />
3004 <Child3 />
3005 <Child4 />
3006 </div>
3007 );
3008 }
3009 }
3010
3011 class Child1 extends React.Component {
3012 render() {
3013 return <span class="paladin">Child1</span>;
3014 }
3015 }
3016
3017 class Child2 extends React.Component {
3018 render() {
3019 return <div>Child2</div>;
3020 }
3021 }
3022
3023 class Child3 extends React.Component {
3024 render() {
3025 return <strong onclick="1">Child3</strong>;
3026 }
3027 }
3028
3029 class Child4 extends React.Component {
3030 render() {
3031 return <div>Child4</div>;
3032 }
3033 }
3034
3035 const container = document.createElement('div');
3036 const root = ReactDOMClient.createRoot(container);
3037 await act(() => {
3038 root.render(<Parent />);
3039 });
3040 assertConsoleErrorDev([
3041 'Invalid DOM property `class`. Did you mean `className`?\n' +
3042 ' in span (at **)\n' +
3043 ' in Child1 (at **)\n' +
3044 ' in Parent (at **)',
3045 'Invalid event handler property `onclick`. Did you mean `onClick`?\n' +
3046 ' in strong (at **)\n' +
3047 ' in Child3 (at **)\n' +
3048 ' in Parent (at **)',
3049 ]);
3050 });
3051
3052 it('gives source code refs for unknown prop warning for exact elements in composition (ssr)', () => {
3053 const container = document.createElement('div');
3054
3055 class Parent extends React.Component {
3056 render() {
3057 return (
3058 <div>
3059 <Child1 />
3060 <Child2 />
3061 <Child3 />
3062 <Child4 />
3063 </div>
3064 );
3065 }
3066 }
3067
3068 class Child1 extends React.Component {
3069 render() {
3070 return <span class="paladin">Child1</span>;
3071 }
3072 }
3073
3074 class Child2 extends React.Component {
3075 render() {
3076 return <div>Child2</div>;
3077 }
3078 }
3079
3080 class Child3 extends React.Component {
3081 render() {
3082 return <strong onclick="1">Child3</strong>;
3083 }
3084 }
3085
3086 class Child4 extends React.Component {
3087 render() {
3088 return <div>Child4</div>;
3089 }
3090 }
3091
3092 ReactDOMServer.renderToString(<Parent />, container);
3093 assertConsoleErrorDev([
3094 'Invalid DOM property `class`. Did you mean `className`?\n' +
3095 ' in span (at **)\n' +
3096 ' in Child1 (at **)\n' +
3097 ' in Parent (at **)',
3098 'Invalid event handler property `onclick`. ' +
3099 'React events use the camelCase naming convention, for example `onClick`.\n' +
3100 ' in strong (at **)\n' +
3101 ' in Child3 (at **)\n' +
3102 ' in Parent (at **)',
3103 ]);
3104 });
3105
3106 it('should suggest property name if available', async () => {
3107 let container = document.createElement('div');
3108 let root = ReactDOMClient.createRoot(container);
3109 await act(() => {
3110 root.render(React.createElement('label', {for: 'test'}));
3111 });
3112 assertConsoleErrorDev([
3113 'Invalid DOM property `for`. Did you mean `htmlFor`?\n' +
3114 ' in label',
3115 ]);
3116
3117 container = document.createElement('div');
3118 root = ReactDOMClient.createRoot(container);
3119 await act(() => {
3120 root.render(
3121 React.createElement('input', {type: 'text', autofocus: true}),
3122 );
3123 });
3124 assertConsoleErrorDev([
3125 'Invalid DOM property `autofocus`. Did you mean `autoFocus`?\n in input',
3126 ]);
3127 });
3128
3129 it('should suggest property name if available (ssr)', () => {
3130 ReactDOMServer.renderToString(
3131 React.createElement('label', {for: 'test'}),
3132 );
3133 assertConsoleErrorDev([
3134 'Invalid DOM property `for`. Did you mean `htmlFor`?\n' +
3135 ' in label',
3136 ]);
3137 ReactDOMServer.renderToString(
3138 React.createElement('input', {type: 'text', autofocus: true}),
3139 );
3140 assertConsoleErrorDev([
3141 'Invalid DOM property `autofocus`. Did you mean `autoFocus`?\n' +
3142 ' in input',
3143 ]);
3144 });
3145 });
3146
3147 describe('whitespace', () => {
3148 it('renders innerHTML and preserves whitespace', async () => {
3149 const container = document.createElement('div');
3150 const root = ReactDOMClient.createRoot(container);
3151
3152 const html = '\n \t <span> \n testContent \t </span> \n \t';
3153 const elem = <div dangerouslySetInnerHTML={{__html: html}} />;
3154
3155 await act(() => {
3156 root.render(elem);
3157 });
3158 expect(container.firstChild.innerHTML).toBe(html);
3159 });
3160
3161 it('render and then updates innerHTML and preserves whitespace', async () => {
3162 const container = document.createElement('div');
3163 const root = ReactDOMClient.createRoot(container);
3164 const html = '\n \t <span> \n testContent1 \t </span> \n \t';
3165 const elem = <div dangerouslySetInnerHTML={{__html: html}} />;
3166 await act(() => {
3167 root.render(elem);
3168 });
3169
3170 const html2 = '\n \t <div> \n testContent2 \t </div> \n \t';
3171 const elem2 = <div dangerouslySetInnerHTML={{__html: html2}} />;
3172 await act(() => {
3173 root.render(elem2);
3174 });
3175
3176 expect(container.firstChild.innerHTML).toBe(html2);
3177 });
3178 });
3179
3180 describe('Attributes with aliases', function () {
3181 it('sets aliased attributes on HTML attributes', async function () {
3182 let el;
3183 const container = document.createElement('div');
3184 const root = ReactDOMClient.createRoot(container);
3185
3186 await act(() => {
3187 root.render(<div class="test" ref={current => (el = current)} />);
3188 });
3189 assertConsoleErrorDev([
3190 'Invalid DOM property `class`. Did you mean `className`?\n' +
3191 ' in div (at **)',
3192 ]);
3193
3194 expect(el.className).toBe('test');
3195 });
3196
3197 it('sets incorrectly cased aliased attributes on HTML attributes with a warning', async function () {
3198 let el;
3199 const container = document.createElement('div');
3200 const root = ReactDOMClient.createRoot(container);
3201
3202 await act(() => {
3203 root.render(<div cLASS="test" ref={current => (el = current)} />);
3204 });
3205 assertConsoleErrorDev([
3206 'Invalid DOM property `cLASS`. Did you mean `className`?\n' +
3207 ' in div (at **)',
3208 ]);
3209
3210 expect(el.className).toBe('test');
3211 });
3212
3213 it('sets aliased attributes on SVG elements with a warning', async function () {
3214 let el;
3215 const container = document.createElement('div');
3216 const root = ReactDOMClient.createRoot(container);
3217
3218 await act(() => {
3219 root.render(
3220 <svg ref={current => (el = current)}>
3221 <text arabic-form="initial" />
3222 </svg>,
3223 );
3224 });
3225 assertConsoleErrorDev([
3226 'Invalid DOM property `arabic-form`. Did you mean `arabicForm`?\n' +
3227 ' in text (at **)',
3228 ]);
3229 const text = el.querySelector('text');
3230
3231 expect(text.hasAttribute('arabic-form')).toBe(true);
3232 });
3233
3234 it('sets aliased attributes on custom elements', async function () {
3235 const container = document.createElement('div');
3236 const root = ReactDOMClient.createRoot(container);
3237 await act(() => {
3238 root.render(<div is="custom-element" class="test" />);
3239 });
3240
3241 const el = container.firstChild;
3242 expect(el.getAttribute('class')).toBe('test');
3243 });
3244
3245 it('aliased attributes on custom elements with bad casing', async function () {
3246 const container = document.createElement('div');
3247 const root = ReactDOMClient.createRoot(container);
3248
3249 await act(() => {
3250 root.render(<div is="custom-element" claSS="test" />);
3251 });
3252
3253 const el = container.firstChild;
3254
3255 expect(el.getAttribute('class')).toBe('test');
3256 });
3257
3258 it('updates aliased attributes on custom elements', async () => {
3259 const container = document.createElement('div');
3260 const root = ReactDOMClient.createRoot(container);
3261 await act(() => {
3262 root.render(<div is="custom-element" class="foo" />);
3263 });
3264 await act(() => {
3265 root.render(<div is="custom-element" class="bar" />);
3266 });
3267
3268 expect(container.firstChild.getAttribute('class')).toBe('bar');
3269 });
3270 });
3271
3272 describe('Custom attributes', function () {
3273 it('allows assignment of custom attributes with string values', async () => {
3274 const container = document.createElement('div');
3275 const root = ReactDOMClient.createRoot(container);
3276
3277 await act(() => {
3278 root.render(<div whatever="30" />);
3279 });
3280
3281 const el = container.firstChild;
3282
3283 expect(el.getAttribute('whatever')).toBe('30');
3284 });
3285
3286 it('removes custom attributes', async () => {
3287 const container = document.createElement('div');
3288 const root = ReactDOMClient.createRoot(container);
3289 await act(() => {
3290 root.render(<div whatever="30" />);
3291 });
3292
3293 expect(container.firstChild.getAttribute('whatever')).toBe('30');
3294
3295 await act(() => {
3296 root.render(<div whatever={null} />);
3297 });
3298
3299 expect(container.firstChild.hasAttribute('whatever')).toBe(false);
3300 });
3301
3302 it('does not assign a boolean custom attributes as a string', async function () {
3303 let el;
3304 const container = document.createElement('div');
3305 const root = ReactDOMClient.createRoot(container);
3306
3307 await act(() => {
3308 root.render(<div whatever={true} ref={current => (el = current)} />);
3309 });
3310 assertConsoleErrorDev([
3311 'Received `true` for a non-boolean attribute `whatever`.\n\n' +
3312 'If you want to write it to the DOM, pass a string instead: ' +
3313 'whatever="true" or whatever={value.toString()}.\n' +
3314 ' in div (at **)',
3315 ]);
3316
3317 expect(el.hasAttribute('whatever')).toBe(false);
3318 });
3319
3320 it('does not assign an implicit boolean custom attributes', async function () {
3321 let el;
3322 const container = document.createElement('div');
3323 const root = ReactDOMClient.createRoot(container);
3324
3325 await act(() => {
3326 root.render(
3327 // eslint-disable-next-line react/jsx-boolean-value
3328 <div whatever ref={current => (el = current)} />,
3329 );
3330 });
3331 assertConsoleErrorDev([
3332 'Received `true` for a non-boolean attribute `whatever`.\n\n' +
3333 'If you want to write it to the DOM, pass a string instead: ' +
3334 'whatever="true" or whatever={value.toString()}.\n' +
3335 ' in div (at **)',
3336 ]);
3337
3338 expect(el.hasAttribute('whatever')).toBe(false);
3339 });
3340
3341 it('assigns a numeric custom attributes as a string', async function () {
3342 const container = document.createElement('div');
3343 const root = ReactDOMClient.createRoot(container);
3344
3345 await act(() => {
3346 root.render(<div whatever={3} />);
3347 });
3348
3349 const el = container.firstChild;
3350
3351 expect(el.getAttribute('whatever')).toBe('3');
3352 });
3353
3354 it('will not assign a function custom attributes', async function () {
3355 let el;
3356 const container = document.createElement('div');
3357 const root = ReactDOMClient.createRoot(container);
3358
3359 await act(() => {
3360 root.render(
3361 <div whatever={() => {}} ref={current => (el = current)} />,
3362 );
3363 });
3364 assertConsoleErrorDev([
3365 'Invalid value for prop `whatever` on <div> tag. ' +
3366 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
3367 'For details, see https://react.dev/link/attribute-behavior \n' +
3368 ' in div (at **)',
3369 ]);
3370
3371 expect(el.hasAttribute('whatever')).toBe(false);
3372 });
3373
3374 it('will assign an object custom attributes', async function () {
3375 const container = document.createElement('div');
3376 const root = ReactDOMClient.createRoot(container);
3377
3378 await act(() => {
3379 root.render(<div whatever={{}} />);
3380 });
3381
3382 const el = container.firstChild;
3383 expect(el.getAttribute('whatever')).toBe('[object Object]');
3384 });
3385
3386 it('allows Temporal-like objects as HTML (they are not coerced to strings first)', async () => {
3387 class TemporalLike {
3388 valueOf() {
3389 // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
3390 // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
3391 throw new TypeError('prod message');
3392 }
3393 toString() {
3394 return '2020-01-01';
3395 }
3396 }
3397
3398 // `dangerouslySetInnerHTML` is never coerced to a string, so won't throw
3399 // even with a Temporal-like object.
3400 const container = document.createElement('div');
3401 const root = ReactDOMClient.createRoot(container);
3402 await act(() => {
3403 root.render(
3404 <div dangerouslySetInnerHTML={{__html: new TemporalLike()}} />,
3405 );
3406 });
3407 expect(container.firstChild.innerHTML).toEqual('2020-01-01');
3408 });
3409
3410 it('allows cased data attributes', async () => {
3411 let el;
3412 const container = document.createElement('div');
3413 const root = ReactDOMClient.createRoot(container);
3414
3415 await act(() => {
3416 root.render(<div data-fooBar="true" ref={current => (el = current)} />);
3417 });
3418 assertConsoleErrorDev([
3419 'React does not recognize the `data-fooBar` prop on a DOM element. ' +
3420 'If you intentionally want it to appear in the DOM as a custom ' +
3421 'attribute, spell it as lowercase `data-foobar` instead. ' +
3422 'If you accidentally passed it from a parent component, remove ' +
3423 'it from the DOM element.\n' +
3424 ' in div (at **)',
3425 ]);
3426 expect(el.getAttribute('data-foobar')).toBe('true');
3427 });
3428
3429 it('allows cased custom attributes', async () => {
3430 let el;
3431 const container = document.createElement('div');
3432 const root = ReactDOMClient.createRoot(container);
3433
3434 await act(() => {
3435 root.render(<div fooBar="true" ref={current => (el = current)} />);
3436 });
3437 assertConsoleErrorDev([
3438 'React does not recognize the `fooBar` prop on a DOM element. ' +
3439 'If you intentionally want it to appear in the DOM as a custom ' +
3440 'attribute, spell it as lowercase `foobar` instead. ' +
3441 'If you accidentally passed it from a parent component, remove ' +
3442 'it from the DOM element.\n' +
3443 ' in div (at **)',
3444 ]);
3445 expect(el.getAttribute('foobar')).toBe('true');
3446 });
3447
3448 it('warns on NaN attributes', async () => {
3449 let el;
3450 const container = document.createElement('div');
3451 const root = ReactDOMClient.createRoot(container);
3452
3453 await act(() => {
3454 root.render(<div whatever={NaN} ref={current => (el = current)} />);
3455 });
3456 assertConsoleErrorDev([
3457 'Received NaN for the `whatever` attribute. If this is ' +
3458 'expected, cast the value to a string.\n' +
3459 ' in div',
3460 ]);
3461
3462 expect(el.getAttribute('whatever')).toBe('NaN');
3463 });
3464
3465 it('removes a property when it becomes invalid', async () => {
3466 const container = document.createElement('div');
3467 const root = ReactDOMClient.createRoot(container);
3468 await act(() => {
3469 root.render(<div whatever={0} />);
3470 });
3471 await act(() => {
3472 root.render(<div whatever={() => {}} />);
3473 });
3474 assertConsoleErrorDev([
3475 'Invalid value for prop `whatever` on <div> tag. ' +
3476 'Either remove it from the element, or pass a string or number value to keep it in the DOM. ' +
3477 'For details, see https://react.dev/link/attribute-behavior \n' +
3478 ' in div (at **)',
3479 ]);
3480 const el = container.firstChild;
3481 expect(el.hasAttribute('whatever')).toBe(false);
3482 });
3483
3484 it('warns on bad casing of known HTML attributes', async function () {
3485 let el;
3486 const container = document.createElement('div');
3487 const root = ReactDOMClient.createRoot(container);
3488
3489 await act(() => {
3490 root.render(<div SiZe="30" ref={current => (el = current)} />);
3491 });
3492 assertConsoleErrorDev([
3493 'Invalid DOM property `SiZe`. Did you mean `size`?\n' +
3494 ' in div (at **)',
3495 ]);
3496
3497 expect(el.getAttribute('size')).toBe('30');
3498 });
3499 });
3500
3501 describe('Object stringification', function () {
3502 it('allows objects on known properties', async function () {
3503 const container = document.createElement('div');
3504 const root = ReactDOMClient.createRoot(container);
3505
3506 await act(() => {
3507 root.render(<div acceptCharset={{}} />);
3508 });
3509
3510 const el = container.firstChild;
3511 expect(el.getAttribute('accept-charset')).toBe('[object Object]');
3512 });
3513
3514 it('should pass objects as attributes if they define toString', async () => {
3515 const obj = {
3516 toString() {
3517 return 'hello';
3518 },
3519 };
3520 const container = document.createElement('div');
3521 const root = ReactDOMClient.createRoot(container);
3522
3523 await act(() => {
3524 root.render(<img src={obj} />);
3525 });
3526 expect(container.firstChild.src).toBe('http://localhost/hello');
3527
3528 await act(() => {
3529 root.render(<svg arabicForm={obj} />);
3530 });
3531 expect(container.firstChild.getAttribute('arabic-form')).toBe('hello');
3532
3533 await act(() => {
3534 root.render(<div unknown={obj} />);
3535 });
3536 expect(container.firstChild.getAttribute('unknown')).toBe('hello');
3537 });
3538
3539 it('passes objects on known SVG attributes if they do not define toString', async () => {
3540 const obj = {};
3541 const container = document.createElement('div');
3542 const root = ReactDOMClient.createRoot(container);
3543
3544 await act(() => {
3545 root.render(<svg arabicForm={obj} />);
3546 });
3547 expect(container.firstChild.getAttribute('arabic-form')).toBe(
3548 '[object Object]',
3549 );
3550 });
3551
3552 it('passes objects on custom attributes if they do not define toString', async () => {
3553 const obj = {};
3554 const container = document.createElement('div');
3555 const root = ReactDOMClient.createRoot(container);
3556
3557 await act(() => {
3558 root.render(<div unknown={obj} />);
3559 });
3560 expect(container.firstChild.getAttribute('unknown')).toBe(
3561 '[object Object]',
3562 );
3563 });
3564
3565 it('allows objects that inherit a custom toString method', async function () {
3566 const parent = {toString: () => 'hello.jpg'};
3567 const child = Object.create(parent);
3568 const container = document.createElement('div');
3569 const root = ReactDOMClient.createRoot(container);
3570
3571 await act(() => {
3572 root.render(<img src={child} />);
3573 });
3574
3575 const el = container.firstChild;
3576
3577 expect(el.src).toBe('http://localhost/hello.jpg');
3578 });
3579
3580 it('assigns ajaxify (an important internal FB attribute)', async function () {
3581 const options = {toString: () => 'ajaxy'};
3582 const container = document.createElement('div');
3583 const root = ReactDOMClient.createRoot(container);
3584
3585 await act(() => {
3586 root.render(<div ajaxify={options} />);
3587 });
3588
3589 const el = container.firstChild;
3590
3591 expect(el.getAttribute('ajaxify')).toBe('ajaxy');
3592 });
3593 });
3594
3595 describe('String boolean attributes', function () {
3596 it('does not assign string boolean attributes for custom attributes', async function () {
3597 let el;
3598 const container = document.createElement('div');
3599 const root = ReactDOMClient.createRoot(container);
3600
3601 await act(() => {
3602 root.render(<div whatever={true} ref={current => (el = current)} />);
3603 });
3604 assertConsoleErrorDev([
3605 'Received `true` for a non-boolean attribute `whatever`.\n\n' +
3606 'If you want to write it to the DOM, pass a string instead: ' +
3607 'whatever="true" or whatever={value.toString()}.\n' +
3608 ' in div (at **)',
3609 ]);
3610
3611 expect(el.hasAttribute('whatever')).toBe(false);
3612 });
3613
3614 it('stringifies the boolean true for allowed attributes', async function () {
3615 const container = document.createElement('div');
3616 const root = ReactDOMClient.createRoot(container);
3617
3618 await act(() => {
3619 root.render(<div spellCheck={true} />);
3620 });
3621
3622 const el = container.firstChild;
3623
3624 expect(el.getAttribute('spellCheck')).toBe('true');
3625 });
3626
3627 it('stringifies the boolean false for allowed attributes', async function () {
3628 const container = document.createElement('div');
3629 const root = ReactDOMClient.createRoot(container);
3630
3631 await act(() => {
3632 root.render(<div spellCheck={false} />);
3633 });
3634
3635 const el = container.firstChild;
3636
3637 expect(el.getAttribute('spellCheck')).toBe('false');
3638 });
3639
3640 it('stringifies implicit booleans for allowed attributes', async function () {
3641 const container = document.createElement('div');
3642 const root = ReactDOMClient.createRoot(container);
3643
3644 await act(() => {
3645 // eslint-disable-next-line react/jsx-boolean-value
3646 root.render(<div spellCheck />);
3647 });
3648
3649 const el = container.firstChild;
3650
3651 expect(el.getAttribute('spellCheck')).toBe('true');
3652 });
3653 });
3654
3655 describe('Boolean attributes', function () {
3656 it('warns on the ambiguous string value "false"', async function () {
3657 let el;
3658 const container = document.createElement('div');
3659 const root = ReactDOMClient.createRoot(container);
3660
3661 await act(() => {
3662 root.render(<div hidden="false" ref={current => (el = current)} />);
3663 });
3664 assertConsoleErrorDev([
3665 'Received the string `false` for the boolean attribute `hidden`. ' +
3666 'The browser will interpret it as a truthy value. ' +
3667 'Did you mean hidden={false}?\n' +
3668 ' in div (at **)',
3669 ]);
3670
3671 expect(el.getAttribute('hidden')).toBe('');
3672 });
3673
3674 it('warns on the potentially-ambiguous string value "true"', async function () {
3675 let el;
3676 const container = document.createElement('div');
3677 const root = ReactDOMClient.createRoot(container);
3678
3679 await act(() => {
3680 root.render(<div hidden="true" ref={current => (el = current)} />);
3681 });
3682 assertConsoleErrorDev([
3683 'Received the string `true` for the boolean attribute `hidden`. ' +
3684 'Although this works, it will not work as expected if you pass the string "false". ' +
3685 'Did you mean hidden={true}?\n' +
3686 ' in div (at **)',
3687 ]);
3688
3689 expect(el.getAttribute('hidden')).toBe('');
3690 });
3691 });
3692
3693 describe('Hyphenated SVG elements', function () {
3694 it('the font-face element is not a custom element', async function () {
3695 let el;
3696 const container = document.createElement('div');
3697 const root = ReactDOMClient.createRoot(container);
3698
3699 await act(() => {
3700 root.render(
3701 <svg ref={current => (el = current)}>
3702 <font-face x-height={false} />
3703 </svg>,
3704 );
3705 });
3706 assertConsoleErrorDev([
3707 'Invalid DOM property `x-height`. Did you mean `xHeight`?\n' +
3708 ' in font-face (at **)',
3709 ]);
3710
3711 expect(el.querySelector('font-face').hasAttribute('x-height')).toBe(
3712 false,
3713 );
3714 });
3715
3716 it('the font-face element does not allow unknown boolean values', async function () {
3717 let el;
3718 const container = document.createElement('div');
3719 const root = ReactDOMClient.createRoot(container);
3720
3721 await act(() => {
3722 root.render(
3723 <svg ref={current => (el = current)}>
3724 <font-face whatever={false} />
3725 </svg>,
3726 );
3727 });
3728 assertConsoleErrorDev([
3729 'Received `false` for a non-boolean attribute `whatever`.\n\n' +
3730 'If you want to write it to the DOM, pass a string instead: ' +
3731 'whatever="false" or whatever={value.toString()}.\n\n' +
3732 'If you used to conditionally omit it with whatever={condition && value}, ' +
3733 'pass whatever={condition ? value : undefined} instead.\n' +
3734 ' in font-face (at **)',
3735 ]);
3736
3737 expect(el.querySelector('font-face').hasAttribute('whatever')).toBe(
3738 false,
3739 );
3740 });
3741 });
3742
3743 // These tests mostly verify the existing behavior.
3744 // It may not always makes sense but we can't change it in minors.
3745 describe('Custom elements', () => {
3746 it('does not strip unknown boolean attributes', async () => {
3747 const container = document.createElement('div');
3748 const root = ReactDOMClient.createRoot(container);
3749 await act(() => {
3750 root.render(<some-custom-element foo={true} />);
3751 });
3752 const node = container.firstChild;
3753 expect(node.getAttribute('foo')).toBe('');
3754 await act(() => {
3755 root.render(<some-custom-element foo={false} />);
3756 });
3757 expect(node.getAttribute('foo')).toBe(null);
3758 await act(() => {
3759 root.render(<some-custom-element />);
3760 });
3761 expect(node.hasAttribute('foo')).toBe(false);
3762 await act(() => {
3763 root.render(<some-custom-element foo={true} />);
3764 });
3765 expect(node.hasAttribute('foo')).toBe(true);
3766 });
3767
3768 it('does not strip the on* attributes', async () => {
3769 const container = document.createElement('div');
3770 const root = ReactDOMClient.createRoot(container);
3771 await act(() => {
3772 root.render(<some-custom-element onx="bar" />);
3773 });
3774 const node = container.firstChild;
3775 expect(node.getAttribute('onx')).toBe('bar');
3776 await act(() => {
3777 root.render(<some-custom-element onx="buzz" />);
3778 });
3779 expect(node.getAttribute('onx')).toBe('buzz');
3780 await act(() => {
3781 root.render(<some-custom-element />);
3782 });
3783 expect(node.hasAttribute('onx')).toBe(false);
3784 await act(() => {
3785 root.render(<some-custom-element onx="bar" />);
3786 });
3787 expect(node.getAttribute('onx')).toBe('bar');
3788 });
3789 });
3790
3791 it('receives events in specific order', async () => {
3792 const eventOrder = [];
3793 const track = tag => () => eventOrder.push(tag);
3794 const outerRef = React.createRef();
3795 const innerRef = React.createRef();
3796
3797 function OuterReactApp() {
3798 return (
3799 <div
3800 ref={outerRef}
3801 onClick={track('outer bubble')}
3802 onClickCapture={track('outer capture')}
3803 />
3804 );
3805 }
3806
3807 function InnerReactApp() {
3808 return (
3809 <div
3810 ref={innerRef}
3811 onClick={track('inner bubble')}
3812 onClickCapture={track('inner capture')}
3813 />
3814 );
3815 }
3816
3817 const container = document.createElement('div');
3818 const root = ReactDOMClient.createRoot(container);
3819 document.body.appendChild(container);
3820
3821 try {
3822 await act(() => {
3823 root.render(<OuterReactApp />);
3824 });
3825 const innerRoot = ReactDOMClient.createRoot(outerRef.current);
3826 await act(() => {
3827 innerRoot.render(<InnerReactApp />);
3828 });
3829
3830 document.addEventListener('click', track('document bubble'));
3831 document.addEventListener('click', track('document capture'), true);
3832
3833 innerRef.current.click();
3834
3835 if (ReactFeatureFlags.enableLegacyFBSupport) {
3836 // The order will change here, as the legacy FB support adds
3837 // the event listener onto the document after the one above has.
3838 expect(eventOrder).toEqual([
3839 'document capture',
3840 'outer capture',
3841 'inner capture',
3842 'document bubble',
3843 'inner bubble',
3844 'outer bubble',
3845 ]);
3846 } else {
3847 expect(eventOrder).toEqual([
3848 'document capture',
3849 'outer capture',
3850 'inner capture',
3851 'inner bubble',
3852 'outer bubble',
3853 'document bubble',
3854 ]);
3855 }
3856 } finally {
3857 document.body.removeChild(container);
3858 }
3859 });
3860
3861 describe('iOS Tap Highlight', () => {
3862 it('adds onclick handler to elements with onClick prop', async () => {
3863 const container = document.createElement('div');
3864 const root = ReactDOMClient.createRoot(container);
3865
3866 const elementRef = React.createRef();
3867 function Component() {
3868 return <div ref={elementRef} onClick={() => {}} />;
3869 }
3870
3871 await act(() => {
3872 root.render(<Component />);
3873 });
3874 expect(typeof elementRef.current.onclick).toBe('function');
3875 });
3876
3877 it('adds onclick handler to a portal root', async () => {
3878 const container = document.createElement('div');
3879 const root = ReactDOMClient.createRoot(container);
3880 const portalContainer = document.createElement('div');
3881
3882 function Component() {
3883 return ReactDOM.createPortal(
3884 <div onClick={() => {}} />,
3885 portalContainer,
3886 );
3887 }
3888
3889 await act(() => {
3890 root.render(<Component />);
3891 });
3892 expect(typeof portalContainer.onclick).toBe('function');
3893 });
3894
3895 // @gate !disableLegacyMode
3896 it('does not add onclick handler to the React root in legacy mode', () => {
3897 const container = document.createElement('div');
3898
3899 function Component() {
3900 return <div onClick={() => {}} />;
3901 }
3902
3903 ReactDOM.render(<Component />, container);
3904 expect(typeof container.onclick).not.toBe('function');
3905 });
3906 });
3907 });