main
js 1,455 lines 41.6 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 const React = require('react');
13 const ReactDOM = require('react-dom');
14 const PropTypes = require('prop-types');
15 let act;
16 let assertConsoleErrorDev;
17 describe('ReactDOMLegacyFiber', () => {
18 let container;
19
20 beforeEach(() => {
21 act = require('internal-test-utils').act;
22 assertConsoleErrorDev =
23 require('internal-test-utils').assertConsoleErrorDev;
24 container = document.createElement('div');
25 document.body.appendChild(container);
26 });
27
28 afterEach(() => {
29 document.body.removeChild(container);
30 container = null;
31 jest.restoreAllMocks();
32 });
33
34 // @gate !disableLegacyMode
35 it('should render strings as children', () => {
36 const Box = ({value}) => <div>{value}</div>;
37
38 ReactDOM.render(<Box value="foo" />, container);
39 expect(container.textContent).toEqual('foo');
40 });
41
42 // @gate !disableLegacyMode
43 it('should render numbers as children', () => {
44 const Box = ({value}) => <div>{value}</div>;
45
46 ReactDOM.render(<Box value={10} />, container);
47
48 expect(container.textContent).toEqual('10');
49 });
50
51 // @gate !disableLegacyMode
52 it('should be called a callback argument', () => {
53 // mounting phase
54 let called = false;
55 ReactDOM.render(<div>Foo</div>, container, () => (called = true));
56 expect(called).toEqual(true);
57
58 // updating phase
59 called = false;
60 ReactDOM.render(<div>Foo</div>, container, () => (called = true));
61 expect(called).toEqual(true);
62 });
63
64 // @gate !disableLegacyMode
65 it('should call a callback argument when the same element is re-rendered', () => {
66 class Foo extends React.Component {
67 render() {
68 return <div>Foo</div>;
69 }
70 }
71 const element = <Foo />;
72
73 // mounting phase
74 let called = false;
75 ReactDOM.render(element, container, () => (called = true));
76 expect(called).toEqual(true);
77
78 // updating phase
79 called = false;
80 ReactDOM.unstable_batchedUpdates(() => {
81 ReactDOM.render(element, container, () => (called = true));
82 });
83 expect(called).toEqual(true);
84 });
85
86 // @gate !disableLegacyMode
87 it('should render a component returning strings directly from render', () => {
88 const Text = ({value}) => value;
89
90 ReactDOM.render(<Text value="foo" />, container);
91 expect(container.textContent).toEqual('foo');
92 });
93
94 // @gate !disableLegacyMode
95 it('should render a component returning numbers directly from render', () => {
96 const Text = ({value}) => value;
97
98 ReactDOM.render(<Text value={10} />, container);
99
100 expect(container.textContent).toEqual('10');
101 });
102
103 // @gate !disableLegacyMode
104 it('finds the DOM Text node of a string child', () => {
105 class Text extends React.Component {
106 render() {
107 return this.props.value;
108 }
109 }
110
111 let instance = null;
112 ReactDOM.render(
113 <Text value="foo" ref={ref => (instance = ref)} />,
114 container,
115 );
116
117 const textNode =
118 ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode(
119 instance,
120 );
121 expect(textNode).toBe(container.firstChild);
122 expect(textNode.nodeType).toBe(3);
123 expect(textNode.nodeValue).toBe('foo');
124 });
125
126 // @gate !disableLegacyMode
127 it('finds the first child when a component returns a fragment', () => {
128 class Fragment extends React.Component {
129 render() {
130 return [<div key="a" />, <span key="b" />];
131 }
132 }
133
134 let instance = null;
135 ReactDOM.render(<Fragment ref={ref => (instance = ref)} />, container);
136
137 expect(container.childNodes.length).toBe(2);
138
139 const firstNode =
140 ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode(
141 instance,
142 );
143 expect(firstNode).toBe(container.firstChild);
144 expect(firstNode.tagName).toBe('DIV');
145 });
146
147 // @gate !disableLegacyMode
148 it('finds the first child even when fragment is nested', () => {
149 class Wrapper extends React.Component {
150 render() {
151 return this.props.children;
152 }
153 }
154
155 class Fragment extends React.Component {
156 render() {
157 return [
158 <Wrapper key="a">
159 <div />
160 </Wrapper>,
161 <span key="b" />,
162 ];
163 }
164 }
165
166 let instance = null;
167 ReactDOM.render(<Fragment ref={ref => (instance = ref)} />, container);
168
169 expect(container.childNodes.length).toBe(2);
170
171 const firstNode =
172 ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode(
173 instance,
174 );
175 expect(firstNode).toBe(container.firstChild);
176 expect(firstNode.tagName).toBe('DIV');
177 });
178
179 // @gate !disableLegacyMode
180 it('finds the first child even when first child renders null', () => {
181 class NullComponent extends React.Component {
182 render() {
183 return null;
184 }
185 }
186
187 class Fragment extends React.Component {
188 render() {
189 return [<NullComponent key="a" />, <div key="b" />, <span key="c" />];
190 }
191 }
192
193 let instance = null;
194 ReactDOM.render(<Fragment ref={ref => (instance = ref)} />, container);
195
196 expect(container.childNodes.length).toBe(2);
197
198 const firstNode =
199 ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode(
200 instance,
201 );
202 expect(firstNode).toBe(container.firstChild);
203 expect(firstNode.tagName).toBe('DIV');
204 });
205
206 // @gate !disableLegacyMode
207 it('renders an empty fragment', () => {
208 const Div = () => <div />;
209 const EmptyFragment = () => <></>;
210 const NonEmptyFragment = () => (
211 <>
212 <Div />
213 </>
214 );
215
216 ReactDOM.render(<EmptyFragment />, container);
217 expect(container.firstChild).toBe(null);
218
219 ReactDOM.render(<NonEmptyFragment />, container);
220 expect(container.firstChild.tagName).toBe('DIV');
221
222 ReactDOM.render(<EmptyFragment />, container);
223 expect(container.firstChild).toBe(null);
224
225 ReactDOM.render(<Div />, container);
226 expect(container.firstChild.tagName).toBe('DIV');
227
228 ReactDOM.render(<EmptyFragment />, container);
229 expect(container.firstChild).toBe(null);
230 });
231
232 let svgEls, htmlEls, mathEls;
233 const expectSVG = {ref: el => svgEls.push(el)};
234 const expectHTML = {ref: el => htmlEls.push(el)};
235 const expectMath = {ref: el => mathEls.push(el)};
236
237 const usePortal = function (tree) {
238 return ReactDOM.createPortal(tree, document.createElement('div'));
239 };
240
241 const assertNamespacesMatch = function (tree) {
242 const testContainer = document.createElement('div');
243 svgEls = [];
244 htmlEls = [];
245 mathEls = [];
246
247 ReactDOM.render(tree, testContainer);
248 svgEls.forEach(el => {
249 expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg');
250 });
251 htmlEls.forEach(el => {
252 expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
253 });
254 mathEls.forEach(el => {
255 expect(el.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
256 });
257
258 ReactDOM.unmountComponentAtNode(testContainer);
259 expect(testContainer.innerHTML).toBe('');
260 };
261
262 // @gate !disableLegacyMode
263 it('should render one portal', () => {
264 const portalContainer = document.createElement('div');
265
266 ReactDOM.render(
267 <div>{ReactDOM.createPortal(<div>portal</div>, portalContainer)}</div>,
268 container,
269 );
270 expect(portalContainer.innerHTML).toBe('<div>portal</div>');
271 expect(container.innerHTML).toBe('<div></div>');
272
273 ReactDOM.unmountComponentAtNode(container);
274 expect(portalContainer.innerHTML).toBe('');
275 expect(container.innerHTML).toBe('');
276 });
277
278 // @gate !disableLegacyMode
279 it('should render many portals', () => {
280 const portalContainer1 = document.createElement('div');
281 const portalContainer2 = document.createElement('div');
282
283 const ops = [];
284 class Child extends React.Component {
285 componentDidMount() {
286 ops.push(`${this.props.name} componentDidMount`);
287 }
288 componentDidUpdate() {
289 ops.push(`${this.props.name} componentDidUpdate`);
290 }
291 componentWillUnmount() {
292 ops.push(`${this.props.name} componentWillUnmount`);
293 }
294 render() {
295 return <div>{this.props.name}</div>;
296 }
297 }
298
299 class Parent extends React.Component {
300 componentDidMount() {
301 ops.push(`Parent:${this.props.step} componentDidMount`);
302 }
303 componentDidUpdate() {
304 ops.push(`Parent:${this.props.step} componentDidUpdate`);
305 }
306 componentWillUnmount() {
307 ops.push(`Parent:${this.props.step} componentWillUnmount`);
308 }
309 render() {
310 const {step} = this.props;
311 return [
312 <Child key="a" name={`normal[0]:${step}`} />,
313 ReactDOM.createPortal(
314 <Child key="b" name={`portal1[0]:${step}`} />,
315 portalContainer1,
316 ),
317 <Child key="c" name={`normal[1]:${step}`} />,
318 ReactDOM.createPortal(
319 [
320 <Child key="d" name={`portal2[0]:${step}`} />,
321 <Child key="e" name={`portal2[1]:${step}`} />,
322 ],
323 portalContainer2,
324 ),
325 ];
326 }
327 }
328
329 ReactDOM.render(<Parent step="a" />, container);
330 expect(portalContainer1.innerHTML).toBe('<div>portal1[0]:a</div>');
331 expect(portalContainer2.innerHTML).toBe(
332 '<div>portal2[0]:a</div><div>portal2[1]:a</div>',
333 );
334 expect(container.innerHTML).toBe(
335 '<div>normal[0]:a</div><div>normal[1]:a</div>',
336 );
337 expect(ops).toEqual([
338 'normal[0]:a componentDidMount',
339 'portal1[0]:a componentDidMount',
340 'normal[1]:a componentDidMount',
341 'portal2[0]:a componentDidMount',
342 'portal2[1]:a componentDidMount',
343 'Parent:a componentDidMount',
344 ]);
345
346 ops.length = 0;
347 ReactDOM.render(<Parent step="b" />, container);
348 expect(portalContainer1.innerHTML).toBe('<div>portal1[0]:b</div>');
349 expect(portalContainer2.innerHTML).toBe(
350 '<div>portal2[0]:b</div><div>portal2[1]:b</div>',
351 );
352 expect(container.innerHTML).toBe(
353 '<div>normal[0]:b</div><div>normal[1]:b</div>',
354 );
355 expect(ops).toEqual([
356 'normal[0]:b componentDidUpdate',
357 'portal1[0]:b componentDidUpdate',
358 'normal[1]:b componentDidUpdate',
359 'portal2[0]:b componentDidUpdate',
360 'portal2[1]:b componentDidUpdate',
361 'Parent:b componentDidUpdate',
362 ]);
363
364 ops.length = 0;
365 ReactDOM.unmountComponentAtNode(container);
366 expect(portalContainer1.innerHTML).toBe('');
367 expect(portalContainer2.innerHTML).toBe('');
368 expect(container.innerHTML).toBe('');
369 expect(ops).toEqual([
370 'Parent:b componentWillUnmount',
371 'normal[0]:b componentWillUnmount',
372 'portal1[0]:b componentWillUnmount',
373 'normal[1]:b componentWillUnmount',
374 'portal2[0]:b componentWillUnmount',
375 'portal2[1]:b componentWillUnmount',
376 ]);
377 });
378
379 // @gate !disableLegacyMode
380 it('should render nested portals', () => {
381 const portalContainer1 = document.createElement('div');
382 const portalContainer2 = document.createElement('div');
383 const portalContainer3 = document.createElement('div');
384
385 ReactDOM.render(
386 [
387 <div key="a">normal[0]</div>,
388 ReactDOM.createPortal(
389 [
390 <div key="b">portal1[0]</div>,
391 ReactDOM.createPortal(
392 <div key="c">portal2[0]</div>,
393 portalContainer2,
394 ),
395 ReactDOM.createPortal(
396 <div key="d">portal3[0]</div>,
397 portalContainer3,
398 ),
399 <div key="e">portal1[1]</div>,
400 ],
401 portalContainer1,
402 ),
403 <div key="f">normal[1]</div>,
404 ],
405 container,
406 );
407 expect(portalContainer1.innerHTML).toBe(
408 '<div>portal1[0]</div><div>portal1[1]</div>',
409 );
410 expect(portalContainer2.innerHTML).toBe('<div>portal2[0]</div>');
411 expect(portalContainer3.innerHTML).toBe('<div>portal3[0]</div>');
412 expect(container.innerHTML).toBe(
413 '<div>normal[0]</div><div>normal[1]</div>',
414 );
415
416 ReactDOM.unmountComponentAtNode(container);
417 expect(portalContainer1.innerHTML).toBe('');
418 expect(portalContainer2.innerHTML).toBe('');
419 expect(portalContainer3.innerHTML).toBe('');
420 expect(container.innerHTML).toBe('');
421 });
422
423 // @gate !disableLegacyMode
424 it('should reconcile portal children', () => {
425 const portalContainer = document.createElement('div');
426
427 ReactDOM.render(
428 <div>{ReactDOM.createPortal(<div>portal:1</div>, portalContainer)}</div>,
429 container,
430 );
431 expect(portalContainer.innerHTML).toBe('<div>portal:1</div>');
432 expect(container.innerHTML).toBe('<div></div>');
433
434 ReactDOM.render(
435 <div>{ReactDOM.createPortal(<div>portal:2</div>, portalContainer)}</div>,
436 container,
437 );
438 expect(portalContainer.innerHTML).toBe('<div>portal:2</div>');
439 expect(container.innerHTML).toBe('<div></div>');
440
441 ReactDOM.render(
442 <div>{ReactDOM.createPortal(<p>portal:3</p>, portalContainer)}</div>,
443 container,
444 );
445 expect(portalContainer.innerHTML).toBe('<p>portal:3</p>');
446 expect(container.innerHTML).toBe('<div></div>');
447
448 ReactDOM.render(
449 <div>{ReactDOM.createPortal(['Hi', 'Bye'], portalContainer)}</div>,
450 container,
451 );
452 expect(portalContainer.innerHTML).toBe('HiBye');
453 expect(container.innerHTML).toBe('<div></div>');
454
455 ReactDOM.render(
456 <div>{ReactDOM.createPortal(['Bye', 'Hi'], portalContainer)}</div>,
457 container,
458 );
459 expect(portalContainer.innerHTML).toBe('ByeHi');
460 expect(container.innerHTML).toBe('<div></div>');
461
462 ReactDOM.render(
463 <div>{ReactDOM.createPortal(null, portalContainer)}</div>,
464 container,
465 );
466 expect(portalContainer.innerHTML).toBe('');
467 expect(container.innerHTML).toBe('<div></div>');
468 });
469
470 // @gate !disableLegacyMode
471 it('should unmount empty portal component wherever it appears', () => {
472 const portalContainer = document.createElement('div');
473
474 class Wrapper extends React.Component {
475 constructor(props) {
476 super(props);
477 this.state = {
478 show: true,
479 };
480 }
481 render() {
482 return (
483 <div>
484 {this.state.show && (
485 <>
486 {ReactDOM.createPortal(null, portalContainer)}
487 <div>child</div>
488 </>
489 )}
490 <div>parent</div>
491 </div>
492 );
493 }
494 }
495
496 const instance = ReactDOM.render(<Wrapper />, container);
497 expect(container.innerHTML).toBe(
498 '<div><div>child</div><div>parent</div></div>',
499 );
500 instance.setState({show: false});
501 expect(instance.state.show).toBe(false);
502 expect(container.innerHTML).toBe('<div><div>parent</div></div>');
503 });
504
505 // @gate !disableLegacyMode
506 it('should keep track of namespace across portals (simple)', () => {
507 assertNamespacesMatch(
508 <svg {...expectSVG}>
509 <image {...expectSVG} />
510 {usePortal(<div {...expectHTML} />)}
511 <image {...expectSVG} />
512 </svg>,
513 );
514 assertNamespacesMatch(
515 <math {...expectMath}>
516 <mi {...expectMath} />
517 {usePortal(<div {...expectHTML} />)}
518 <mi {...expectMath} />
519 </math>,
520 );
521 assertNamespacesMatch(
522 <div {...expectHTML}>
523 <p {...expectHTML} />
524 {usePortal(
525 <svg {...expectSVG}>
526 <image {...expectSVG} />
527 </svg>,
528 )}
529 <p {...expectHTML} />
530 </div>,
531 );
532 });
533
534 // @gate !disableLegacyMode
535 it('should keep track of namespace across portals (medium)', () => {
536 assertNamespacesMatch(
537 <svg {...expectSVG}>
538 <image {...expectSVG} />
539 {usePortal(<div {...expectHTML} />)}
540 <image {...expectSVG} />
541 {usePortal(<div {...expectHTML} />)}
542 <image {...expectSVG} />
543 </svg>,
544 );
545 assertNamespacesMatch(
546 <div {...expectHTML}>
547 <math {...expectMath}>
548 <mi {...expectMath} />
549 {usePortal(
550 <svg {...expectSVG}>
551 <image {...expectSVG} />
552 </svg>,
553 )}
554 </math>
555 <p {...expectHTML} />
556 </div>,
557 );
558 assertNamespacesMatch(
559 <math {...expectMath}>
560 <mi {...expectMath} />
561 {usePortal(
562 <svg {...expectSVG}>
563 <image {...expectSVG} />
564 <foreignObject {...expectSVG}>
565 <p {...expectHTML} />
566 <math {...expectMath}>
567 <mi {...expectMath} />
568 </math>
569 <p {...expectHTML} />
570 </foreignObject>
571 <image {...expectSVG} />
572 </svg>,
573 )}
574 <mi {...expectMath} />
575 </math>,
576 );
577 assertNamespacesMatch(
578 <div {...expectHTML}>
579 {usePortal(
580 <svg {...expectSVG}>
581 {usePortal(<div {...expectHTML} />)}
582 <image {...expectSVG} />
583 </svg>,
584 )}
585 <p {...expectHTML} />
586 </div>,
587 );
588 assertNamespacesMatch(
589 <svg {...expectSVG}>
590 <svg {...expectSVG}>
591 {usePortal(<div {...expectHTML} />)}
592 <image {...expectSVG} />
593 </svg>
594 <image {...expectSVG} />
595 </svg>,
596 );
597 });
598
599 // @gate !disableLegacyMode
600 it('should keep track of namespace across portals (complex)', () => {
601 assertNamespacesMatch(
602 <div {...expectHTML}>
603 {usePortal(
604 <svg {...expectSVG}>
605 <image {...expectSVG} />
606 </svg>,
607 )}
608 <p {...expectHTML} />
609 <svg {...expectSVG}>
610 <image {...expectSVG} />
611 </svg>
612 <svg {...expectSVG}>
613 <svg {...expectSVG}>
614 <image {...expectSVG} />
615 </svg>
616 <image {...expectSVG} />
617 </svg>
618 <p {...expectHTML} />
619 </div>,
620 );
621 assertNamespacesMatch(
622 <div {...expectHTML}>
623 <svg {...expectSVG}>
624 <svg {...expectSVG}>
625 <image {...expectSVG} />
626 {usePortal(
627 <svg {...expectSVG}>
628 <image {...expectSVG} />
629 <svg {...expectSVG}>
630 <image {...expectSVG} />
631 </svg>
632 <image {...expectSVG} />
633 </svg>,
634 )}
635 <image {...expectSVG} />
636 <foreignObject {...expectSVG}>
637 <p {...expectHTML} />
638 {usePortal(<p {...expectHTML} />)}
639 <p {...expectHTML} />
640 </foreignObject>
641 </svg>
642 <image {...expectSVG} />
643 </svg>
644 <p {...expectHTML} />
645 </div>,
646 );
647 assertNamespacesMatch(
648 <div {...expectHTML}>
649 <svg {...expectSVG}>
650 <foreignObject {...expectSVG}>
651 <p {...expectHTML} />
652 {usePortal(
653 <svg {...expectSVG}>
654 <image {...expectSVG} />
655 <svg {...expectSVG}>
656 <image {...expectSVG} />
657 <foreignObject {...expectSVG}>
658 <p {...expectHTML} />
659 </foreignObject>
660 {usePortal(<p {...expectHTML} />)}
661 </svg>
662 <image {...expectSVG} />
663 </svg>,
664 )}
665 <p {...expectHTML} />
666 </foreignObject>
667 <image {...expectSVG} />
668 </svg>
669 <p {...expectHTML} />
670 </div>,
671 );
672 });
673
674 // @gate !disableLegacyMode
675 it('should unwind namespaces on uncaught errors', async () => {
676 function BrokenRender() {
677 throw new Error('Hello');
678 }
679
680 await expect(async () => {
681 await act(() => {
682 assertNamespacesMatch(
683 <svg {...expectSVG}>
684 <BrokenRender />
685 </svg>,
686 );
687 });
688 }).rejects.toThrow('Hello');
689 assertNamespacesMatch(<div {...expectHTML} />);
690 });
691
692 // @gate !disableLegacyMode
693 it('should unwind namespaces on caught errors', () => {
694 function BrokenRender() {
695 throw new Error('Hello');
696 }
697
698 class ErrorBoundary extends React.Component {
699 state = {error: null};
700 componentDidCatch(error) {
701 this.setState({error});
702 }
703 render() {
704 if (this.state.error) {
705 return <p {...expectHTML} />;
706 }
707 return this.props.children;
708 }
709 }
710
711 assertNamespacesMatch(
712 <svg {...expectSVG}>
713 <foreignObject {...expectSVG}>
714 <ErrorBoundary>
715 <math {...expectMath}>
716 <BrokenRender />
717 </math>
718 </ErrorBoundary>
719 </foreignObject>
720 <image {...expectSVG} />
721 </svg>,
722 );
723 assertNamespacesMatch(<div {...expectHTML} />);
724 });
725
726 // @gate !disableLegacyMode
727 it('should unwind namespaces on caught errors in a portal', () => {
728 function BrokenRender() {
729 throw new Error('Hello');
730 }
731
732 class ErrorBoundary extends React.Component {
733 state = {error: null};
734 componentDidCatch(error) {
735 this.setState({error});
736 }
737 render() {
738 if (this.state.error) {
739 return <image {...expectSVG} />;
740 }
741 return this.props.children;
742 }
743 }
744
745 assertNamespacesMatch(
746 <svg {...expectSVG}>
747 <ErrorBoundary>
748 {usePortal(
749 <div {...expectHTML}>
750 <math {...expectMath}>
751 <BrokenRender />)
752 </math>
753 </div>,
754 )}
755 </ErrorBoundary>
756 {usePortal(<div {...expectHTML} />)}
757 </svg>,
758 );
759 });
760
761 // @gate !disableLegacyContext
762 // @gate !disableLegacyMode
763 it('should pass portal context when rendering subtree elsewhere', () => {
764 const portalContainer = document.createElement('div');
765
766 class Component extends React.Component {
767 static contextTypes = {
768 foo: PropTypes.string.isRequired,
769 };
770
771 render() {
772 return <div>{this.context.foo}</div>;
773 }
774 }
775
776 class Parent extends React.Component {
777 static childContextTypes = {
778 foo: PropTypes.string.isRequired,
779 };
780
781 getChildContext() {
782 return {
783 foo: 'bar',
784 };
785 }
786
787 render() {
788 return ReactDOM.createPortal(<Component />, portalContainer);
789 }
790 }
791
792 ReactDOM.render(<Parent />, container);
793 assertConsoleErrorDev([
794 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
795 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
796 ]);
797 expect(container.innerHTML).toBe('');
798 expect(portalContainer.innerHTML).toBe('<div>bar</div>');
799 });
800
801 // @gate !disableLegacyContext
802 // @gate !disableLegacyMode
803 it('should update portal context if it changes due to setState', () => {
804 const portalContainer = document.createElement('div');
805
806 class Component extends React.Component {
807 static contextTypes = {
808 foo: PropTypes.string.isRequired,
809 getFoo: PropTypes.func.isRequired,
810 };
811
812 render() {
813 return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
814 }
815 }
816
817 class Parent extends React.Component {
818 static childContextTypes = {
819 foo: PropTypes.string.isRequired,
820 getFoo: PropTypes.func.isRequired,
821 };
822
823 state = {
824 bar: 'initial',
825 };
826
827 getChildContext() {
828 return {
829 foo: this.state.bar,
830 getFoo: () => this.state.bar,
831 };
832 }
833
834 render() {
835 return ReactDOM.createPortal(<Component />, portalContainer);
836 }
837 }
838
839 const instance = ReactDOM.render(<Parent />, container);
840 assertConsoleErrorDev([
841 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
842 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
843 ]);
844 expect(portalContainer.innerHTML).toBe('<div>initial-initial</div>');
845 expect(container.innerHTML).toBe('');
846 instance.setState({bar: 'changed'});
847 expect(portalContainer.innerHTML).toBe('<div>changed-changed</div>');
848 expect(container.innerHTML).toBe('');
849 });
850
851 // @gate !disableLegacyContext
852 // @gate !disableLegacyMode
853 it('should update portal context if it changes due to re-render', () => {
854 const portalContainer = document.createElement('div');
855
856 class Component extends React.Component {
857 static contextTypes = {
858 foo: PropTypes.string.isRequired,
859 getFoo: PropTypes.func.isRequired,
860 };
861
862 render() {
863 return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
864 }
865 }
866
867 class Parent extends React.Component {
868 static childContextTypes = {
869 foo: PropTypes.string.isRequired,
870 getFoo: PropTypes.func.isRequired,
871 };
872
873 getChildContext() {
874 return {
875 foo: this.props.bar,
876 getFoo: () => this.props.bar,
877 };
878 }
879
880 render() {
881 return ReactDOM.createPortal(<Component />, portalContainer);
882 }
883 }
884
885 ReactDOM.render(<Parent bar="initial" />, container);
886 assertConsoleErrorDev([
887 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
888 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
889 ]);
890 expect(portalContainer.innerHTML).toBe('<div>initial-initial</div>');
891 expect(container.innerHTML).toBe('');
892 ReactDOM.render(<Parent bar="changed" />, container);
893 expect(portalContainer.innerHTML).toBe('<div>changed-changed</div>');
894 expect(container.innerHTML).toBe('');
895 });
896
897 // @gate !disableLegacyMode
898 it('findDOMNode should find dom element after expanding a fragment', () => {
899 class MyNode extends React.Component {
900 render() {
901 return !this.props.flag
902 ? [<div key="a" />]
903 : [<span key="b" />, <div key="a" />];
904 }
905 }
906
907 const myNodeA = ReactDOM.render(<MyNode />, container);
908 const a =
909 ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode(
910 myNodeA,
911 );
912 expect(a.tagName).toBe('DIV');
913
914 const myNodeB = ReactDOM.render(<MyNode flag={true} />, container);
915 expect(myNodeA === myNodeB).toBe(true);
916
917 const b =
918 ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode(
919 myNodeB,
920 );
921 expect(b.tagName).toBe('SPAN');
922 });
923
924 // @gate !disableLegacyMode
925 it('should bubble events from the portal to the parent', () => {
926 const portalContainer = document.createElement('div');
927 document.body.appendChild(portalContainer);
928 try {
929 const ops = [];
930 let portal = null;
931
932 ReactDOM.render(
933 <div onClick={() => ops.push('parent clicked')}>
934 {ReactDOM.createPortal(
935 <div
936 onClick={() => ops.push('portal clicked')}
937 ref={n => (portal = n)}>
938 portal
939 </div>,
940 portalContainer,
941 )}
942 </div>,
943 container,
944 );
945
946 expect(portal.tagName).toBe('DIV');
947
948 portal.click();
949
950 expect(ops).toEqual(['portal clicked', 'parent clicked']);
951 } finally {
952 document.body.removeChild(portalContainer);
953 }
954 });
955
956 // @gate !disableLegacyMode
957 it('should not onMouseLeave when staying in the portal', () => {
958 const portalContainer = document.createElement('div');
959 document.body.appendChild(portalContainer);
960
961 let ops = [];
962 let firstTarget = null;
963 let secondTarget = null;
964 let thirdTarget = null;
965
966 function simulateMouseMove(from, to) {
967 if (from) {
968 from.dispatchEvent(
969 new MouseEvent('mouseout', {
970 bubbles: true,
971 cancelable: true,
972 relatedTarget: to,
973 }),
974 );
975 }
976 if (to) {
977 to.dispatchEvent(
978 new MouseEvent('mouseover', {
979 bubbles: true,
980 cancelable: true,
981 relatedTarget: from,
982 }),
983 );
984 }
985 }
986
987 try {
988 ReactDOM.render(
989 <div>
990 <div
991 onMouseEnter={() => ops.push('enter parent')}
992 onMouseLeave={() => ops.push('leave parent')}>
993 <div ref={n => (firstTarget = n)} />
994 {ReactDOM.createPortal(
995 <div
996 onMouseEnter={() => ops.push('enter portal')}
997 onMouseLeave={() => ops.push('leave portal')}
998 ref={n => (secondTarget = n)}>
999 portal
1000 </div>,
1001 portalContainer,
1002 )}
1003 </div>
1004 <div ref={n => (thirdTarget = n)} />
1005 </div>,
1006 container,
1007 );
1008
1009 simulateMouseMove(null, firstTarget);
1010 expect(ops).toEqual(['enter parent']);
1011
1012 ops = [];
1013
1014 simulateMouseMove(firstTarget, secondTarget);
1015 expect(ops).toEqual([
1016 // Parent did not invoke leave because we're still inside the portal.
1017 'enter portal',
1018 ]);
1019
1020 ops = [];
1021
1022 simulateMouseMove(secondTarget, thirdTarget);
1023 expect(ops).toEqual([
1024 'leave portal',
1025 'leave parent', // Only when we leave the portal does onMouseLeave fire.
1026 ]);
1027 } finally {
1028 document.body.removeChild(portalContainer);
1029 }
1030 });
1031
1032 // Regression test for https://github.com/facebook/react/issues/19562
1033 // @gate !disableLegacyMode
1034 it('does not fire mouseEnter twice when relatedTarget is the root node', () => {
1035 let ops = [];
1036 let target = null;
1037
1038 function simulateMouseMove(from, to) {
1039 if (from) {
1040 from.dispatchEvent(
1041 new MouseEvent('mouseout', {
1042 bubbles: true,
1043 cancelable: true,
1044 relatedTarget: to,
1045 }),
1046 );
1047 }
1048 if (to) {
1049 to.dispatchEvent(
1050 new MouseEvent('mouseover', {
1051 bubbles: true,
1052 cancelable: true,
1053 relatedTarget: from,
1054 }),
1055 );
1056 }
1057 }
1058
1059 ReactDOM.render(
1060 <div
1061 ref={n => (target = n)}
1062 onMouseEnter={() => ops.push('enter')}
1063 onMouseLeave={() => ops.push('leave')}
1064 />,
1065 container,
1066 );
1067
1068 simulateMouseMove(null, container);
1069 expect(ops).toEqual([]);
1070
1071 ops = [];
1072 simulateMouseMove(container, target);
1073 expect(ops).toEqual(['enter']);
1074
1075 ops = [];
1076 simulateMouseMove(target, container);
1077 expect(ops).toEqual(['leave']);
1078
1079 ops = [];
1080 simulateMouseMove(container, null);
1081 expect(ops).toEqual([]);
1082 });
1083
1084 // @gate !disableLegacyMode
1085 it('listens to events that do not exist in the Portal subtree', () => {
1086 const onClick = jest.fn();
1087
1088 const ref = React.createRef();
1089 ReactDOM.render(
1090 <div onClick={onClick}>
1091 {ReactDOM.createPortal(<button ref={ref}>click</button>, document.body)}
1092 </div>,
1093 container,
1094 );
1095 const event = new MouseEvent('click', {
1096 bubbles: true,
1097 });
1098 ref.current.dispatchEvent(event);
1099
1100 expect(onClick).toHaveBeenCalledTimes(1);
1101 });
1102
1103 it('should throw on bad createPortal argument', () => {
1104 expect(() => {
1105 ReactDOM.createPortal(<div>portal</div>, null);
1106 }).toThrow('Target container is not a DOM element.');
1107 expect(() => {
1108 ReactDOM.createPortal(<div>portal</div>, document.createTextNode('hi'));
1109 }).toThrow('Target container is not a DOM element.');
1110 });
1111
1112 // @gate !disableLegacyMode
1113 it('should warn for non-functional event listeners', () => {
1114 class Example extends React.Component {
1115 render() {
1116 return <div onClick="woops" />;
1117 }
1118 }
1119 ReactDOM.render(<Example />, container);
1120 assertConsoleErrorDev([
1121 'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' +
1122 ' in div (at **)\n' +
1123 ' in Example (at **)',
1124 ]);
1125 });
1126
1127 // @gate !disableLegacyMode
1128 it('should warn with a special message for `false` event listeners', () => {
1129 class Example extends React.Component {
1130 render() {
1131 return <div onClick={false} />;
1132 }
1133 }
1134 ReactDOM.render(<Example />, container);
1135 assertConsoleErrorDev([
1136 'Expected `onClick` listener to be a function, instead got `false`.\n\n' +
1137 'If you used to conditionally omit it with onClick={condition && value}, ' +
1138 'pass onClick={condition ? value : undefined} instead.\n' +
1139 ' in div (at **)\n' +
1140 ' in Example (at **)',
1141 ]);
1142 });
1143
1144 // @gate !disableLegacyMode
1145 it('should not update event handlers until commit', () => {
1146 spyOnDev(console, 'error');
1147
1148 let ops = [];
1149 const handlerA = () => ops.push('A');
1150 const handlerB = () => ops.push('B');
1151
1152 function click() {
1153 const event = new MouseEvent('click', {
1154 bubbles: true,
1155 cancelable: true,
1156 });
1157 Object.defineProperty(event, 'timeStamp', {
1158 value: 0,
1159 });
1160 node.dispatchEvent(event);
1161 }
1162
1163 class Example extends React.Component {
1164 state = {flip: false, count: 0};
1165 flip() {
1166 this.setState({flip: true, count: this.state.count + 1});
1167 }
1168 tick() {
1169 this.setState({count: this.state.count + 1});
1170 }
1171 render() {
1172 const useB = !this.props.forceA && this.state.flip;
1173 return <div onClick={useB ? handlerB : handlerA} />;
1174 }
1175 }
1176
1177 class Click extends React.Component {
1178 constructor() {
1179 super();
1180 node.click();
1181 }
1182 render() {
1183 return null;
1184 }
1185 }
1186
1187 let inst;
1188 ReactDOM.render([<Example key="a" ref={n => (inst = n)} />], container);
1189 const node = container.firstChild;
1190 expect(node.tagName).toEqual('DIV');
1191
1192 click();
1193
1194 expect(ops).toEqual(['A']);
1195 ops = [];
1196
1197 // Render with the other event handler.
1198 inst.flip();
1199
1200 click();
1201
1202 expect(ops).toEqual(['B']);
1203 ops = [];
1204
1205 // Rerender without changing any props.
1206 inst.tick();
1207
1208 click();
1209
1210 expect(ops).toEqual(['B']);
1211 ops = [];
1212
1213 // Render a flip back to the A handler. The second component invokes the
1214 // click handler during render to simulate a click during an aborted
1215 // render. I use this hack because at current time we don't have a way to
1216 // test aborted ReactDOM renders.
1217 ReactDOM.render(
1218 [<Example key="a" forceA={true} />, <Click key="b" />],
1219 container,
1220 );
1221
1222 // Because the new click handler has not yet committed, we should still
1223 // invoke B.
1224 expect(ops).toEqual(['B']);
1225 ops = [];
1226
1227 // Any click that happens after commit, should invoke A.
1228 click();
1229 expect(ops).toEqual(['A']);
1230
1231 if (__DEV__) {
1232 expect(console.error).toHaveBeenCalledTimes(2);
1233 expect(console.error.mock.calls[0][0]).toMatch(
1234 'ReactDOM.render has not been supported since React 18',
1235 );
1236 expect(console.error.mock.calls[1][0]).toMatch(
1237 'ReactDOM.render has not been supported since React 18',
1238 );
1239 }
1240 });
1241
1242 // @gate !disableLegacyMode
1243 it('should not crash encountering low-priority tree', () => {
1244 ReactDOM.render(
1245 <div hidden={true}>
1246 <div />
1247 </div>,
1248 container,
1249 );
1250 });
1251
1252 // @gate !disableLegacyMode
1253 it('should not warn when rendering into an empty container', () => {
1254 ReactDOM.render(<div>foo</div>, container);
1255 expect(container.innerHTML).toBe('<div>foo</div>');
1256 ReactDOM.render(null, container);
1257 expect(container.innerHTML).toBe('');
1258 ReactDOM.render(<div>bar</div>, container);
1259 expect(container.innerHTML).toBe('<div>bar</div>');
1260 });
1261
1262 // @gate !disableLegacyMode
1263 it('should warn when replacing a container which was manually updated outside of React', async () => {
1264 // when not messing with the DOM outside of React
1265 ReactDOM.render(<div key="1">foo</div>, container);
1266 ReactDOM.render(<div key="1">bar</div>, container);
1267 expect(container.innerHTML).toBe('<div>bar</div>');
1268 // then we mess with the DOM before an update
1269 // we know this will error - that is expected right now
1270 // It's an error of type 'NotFoundError' with no message
1271 container.innerHTML = '<div>MEOW.</div>';
1272
1273 await expect(async () => {
1274 await act(() => {
1275 ReactDOM.render(<div key="2">baz</div>, container);
1276 });
1277 }).rejects.toThrow('The node to be removed is not a child of this node.');
1278 assertConsoleErrorDev([
1279 '' +
1280 'It looks like the React-rendered content of this container was ' +
1281 'removed without using React. This is not supported and will ' +
1282 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
1283 'to empty a container.',
1284 ]);
1285 });
1286
1287 // @gate !disableLegacyMode
1288 it('should warn when doing an update to a container manually updated outside of React', () => {
1289 // when not messing with the DOM outside of React
1290 ReactDOM.render(<div>foo</div>, container);
1291 ReactDOM.render(<div>bar</div>, container);
1292 expect(container.innerHTML).toBe('<div>bar</div>');
1293 // then we mess with the DOM before an update
1294 container.innerHTML = '<div>MEOW.</div>';
1295 ReactDOM.render(<div>baz</div>, container);
1296 assertConsoleErrorDev([
1297 '' +
1298 'It looks like the React-rendered content of this container was ' +
1299 'removed without using React. This is not supported and will ' +
1300 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
1301 'to empty a container.',
1302 ]);
1303 });
1304
1305 // @gate !disableLegacyMode
1306 it('should warn when doing an update to a container manually cleared outside of React', () => {
1307 // when not messing with the DOM outside of React
1308 ReactDOM.render(<div>foo</div>, container);
1309 ReactDOM.render(<div>bar</div>, container);
1310 expect(container.innerHTML).toBe('<div>bar</div>');
1311 // then we mess with the DOM before an update
1312 container.innerHTML = '';
1313 ReactDOM.render(<div>baz</div>, container);
1314 assertConsoleErrorDev([
1315 '' +
1316 'It looks like the React-rendered content of this container was ' +
1317 'removed without using React. This is not supported and will ' +
1318 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
1319 'to empty a container.',
1320 ]);
1321 });
1322
1323 // @gate !disableLegacyMode
1324 it('should render a text component with a text DOM node on the same document as the container', () => {
1325 // 1. Create a new document through the use of iframe
1326 // 2. Set up the spy to make asserts when a text component
1327 // is rendered inside the iframe container
1328 const textContent = 'Hello world';
1329 const iframe = document.createElement('iframe');
1330 document.body.appendChild(iframe);
1331 const iframeDocument = iframe.contentDocument;
1332 iframeDocument.write(
1333 '<!DOCTYPE html><html><head></head><body><div></div></body></html>',
1334 );
1335 iframeDocument.close();
1336 const iframeContainer = iframeDocument.body.firstChild;
1337
1338 let actualDocument;
1339 let textNode;
1340
1341 spyOnDevAndProd(iframeContainer, 'appendChild').mockImplementation(node => {
1342 actualDocument = node.ownerDocument;
1343 textNode = node;
1344 });
1345
1346 ReactDOM.render(textContent, iframeContainer);
1347
1348 expect(textNode.textContent).toBe(textContent);
1349 expect(actualDocument).not.toBe(document);
1350 expect(actualDocument).toBe(iframeDocument);
1351 expect(iframeContainer.appendChild).toHaveBeenCalledTimes(1);
1352 });
1353
1354 // @gate !disableLegacyMode
1355 it('should mount into a document fragment', () => {
1356 const fragment = document.createDocumentFragment();
1357 ReactDOM.render(<div>foo</div>, fragment);
1358 expect(container.innerHTML).toBe('');
1359 container.appendChild(fragment);
1360 expect(container.innerHTML).toBe('<div>foo</div>');
1361 });
1362
1363 // Regression test for https://github.com/facebook/react/issues/12643#issuecomment-413727104
1364 // @gate !disableLegacyMode
1365 it('should not diff memoized host components', () => {
1366 const inputRef = React.createRef();
1367 let didCallOnChange = false;
1368
1369 class Child extends React.Component {
1370 state = {};
1371 componentDidMount() {
1372 document.addEventListener('click', this.update, true);
1373 }
1374 componentWillUnmount() {
1375 document.removeEventListener('click', this.update, true);
1376 }
1377 update = () => {
1378 // We're testing that this setState()
1379 // doesn't cause React to commit updates
1380 // to the input outside (which would itself
1381 // prevent the parent's onChange parent handler
1382 // from firing).
1383 this.setState({});
1384 // Note that onChange was always broken when there was an
1385 // earlier setState() in a manual document capture phase
1386 // listener *in the same component*. But that's very rare.
1387 // Here we're testing that a *child* component doesn't break
1388 // the parent if this happens.
1389 };
1390 render() {
1391 return <div />;
1392 }
1393 }
1394
1395 class Parent extends React.Component {
1396 handleChange = val => {
1397 didCallOnChange = true;
1398 };
1399 render() {
1400 return (
1401 <div>
1402 <Child />
1403 <input
1404 ref={inputRef}
1405 type="checkbox"
1406 checked={true}
1407 onChange={this.handleChange}
1408 />
1409 </div>
1410 );
1411 }
1412 }
1413
1414 ReactDOM.render(<Parent />, container);
1415 inputRef.current.dispatchEvent(
1416 new MouseEvent('click', {
1417 bubbles: true,
1418 }),
1419 );
1420 expect(didCallOnChange).toBe(true);
1421 });
1422
1423 // @gate !disableLegacyMode
1424 it('unmounted legacy roots should never clear newer root content from a container', () => {
1425 const ref = React.createRef();
1426
1427 function OldApp() {
1428 const hideOnFocus = () => {
1429 // This app unmounts itself inside of a focus event.
1430 ReactDOM.unmountComponentAtNode(container);
1431 };
1432
1433 return (
1434 <button onFocus={hideOnFocus} ref={ref}>
1435 old
1436 </button>
1437 );
1438 }
1439
1440 function NewApp() {
1441 return <button ref={ref}>new</button>;
1442 }
1443
1444 ReactDOM.render(<OldApp />, container);
1445 ref.current.focus();
1446
1447 ReactDOM.render(<NewApp />, container);
1448
1449 // Calling focus again will flush previously scheduled discrete work for the old root-
1450 // but this should not clear out the newly mounted app.
1451 ref.current.focus();
1452
1453 expect(container.textContent).toBe('new');
1454 });
1455 });