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