@samitouri / QOS-React-1 / commits / 696953fd51

Convert ReactDOMFiber to createRoot (#28077)

Copies the existing tests to `ReactDOMLegacyFiber`, and updates `ReactDOMFiber` to new root.

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