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
+});