main
js 567 lines 15.4 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 describe('ReactMultiChild', () => {
13 let React;
14 let ReactDOMClient;
15 let act;
16 let assertConsoleErrorDev;
17
18 beforeEach(() => {
19 jest.resetModules();
20 React = require('react');
21 ReactDOMClient = require('react-dom/client');
22 act = require('internal-test-utils').act;
23 assertConsoleErrorDev =
24 require('internal-test-utils').assertConsoleErrorDev;
25 });
26
27 describe('reconciliation', () => {
28 it('should update children when possible', async () => {
29 const container = document.createElement('div');
30 const root = ReactDOMClient.createRoot(container);
31
32 const mockMount = jest.fn();
33 const mockUpdate = jest.fn();
34 const mockUnmount = jest.fn();
35
36 class MockComponent extends React.Component {
37 componentDidMount = mockMount;
38 componentDidUpdate = mockUpdate;
39 componentWillUnmount = mockUnmount;
40 render() {
41 return <span />;
42 }
43 }
44
45 expect(mockMount).toHaveBeenCalledTimes(0);
46 expect(mockUpdate).toHaveBeenCalledTimes(0);
47 expect(mockUnmount).toHaveBeenCalledTimes(0);
48
49 await act(async () => {
50 root.render(
51 <div>
52 <MockComponent />
53 </div>,
54 );
55 });
56
57 expect(mockMount).toHaveBeenCalledTimes(1);
58 expect(mockUpdate).toHaveBeenCalledTimes(0);
59 expect(mockUnmount).toHaveBeenCalledTimes(0);
60
61 await act(async () => {
62 root.render(
63 <div>
64 <MockComponent />
65 </div>,
66 );
67 });
68
69 expect(mockMount).toHaveBeenCalledTimes(1);
70 expect(mockUpdate).toHaveBeenCalledTimes(1);
71 expect(mockUnmount).toHaveBeenCalledTimes(0);
72 });
73
74 it('should replace children with different constructors', async () => {
75 const container = document.createElement('div');
76 const root = ReactDOMClient.createRoot(container);
77
78 const mockMount = jest.fn();
79 const mockUnmount = jest.fn();
80
81 class MockComponent extends React.Component {
82 componentDidMount = mockMount;
83 componentWillUnmount = mockUnmount;
84 render() {
85 return <span />;
86 }
87 }
88
89 expect(mockMount).toHaveBeenCalledTimes(0);
90 expect(mockUnmount).toHaveBeenCalledTimes(0);
91
92 await act(async () => {
93 root.render(
94 <div>
95 <MockComponent />
96 </div>,
97 );
98 });
99
100 expect(mockMount).toHaveBeenCalledTimes(1);
101 expect(mockUnmount).toHaveBeenCalledTimes(0);
102
103 await act(async () => {
104 root.render(
105 <div>
106 <span />
107 </div>,
108 );
109 });
110
111 expect(mockMount).toHaveBeenCalledTimes(1);
112 expect(mockUnmount).toHaveBeenCalledTimes(1);
113 });
114
115 it('should NOT replace children with different owners', async () => {
116 const container = document.createElement('div');
117 const root = ReactDOMClient.createRoot(container);
118
119 const mockMount = jest.fn();
120 const mockUnmount = jest.fn();
121
122 class MockComponent extends React.Component {
123 componentDidMount = mockMount;
124 componentWillUnmount = mockUnmount;
125 render() {
126 return <span />;
127 }
128 }
129
130 class WrapperComponent extends React.Component {
131 render() {
132 return this.props.children || <MockComponent />;
133 }
134 }
135
136 expect(mockMount).toHaveBeenCalledTimes(0);
137 expect(mockUnmount).toHaveBeenCalledTimes(0);
138
139 await act(async () => {
140 root.render(<WrapperComponent />);
141 });
142
143 expect(mockMount).toHaveBeenCalledTimes(1);
144 expect(mockUnmount).toHaveBeenCalledTimes(0);
145
146 await act(async () => {
147 root.render(
148 <WrapperComponent>
149 <MockComponent />
150 </WrapperComponent>,
151 );
152 });
153
154 expect(mockMount).toHaveBeenCalledTimes(1);
155 expect(mockUnmount).toHaveBeenCalledTimes(0);
156 });
157
158 it('should replace children with different keys', async () => {
159 const container = document.createElement('div');
160 const root = ReactDOMClient.createRoot(container);
161
162 const mockMount = jest.fn();
163 const mockUnmount = jest.fn();
164
165 class MockComponent extends React.Component {
166 componentDidMount = mockMount;
167 componentWillUnmount = mockUnmount;
168 render() {
169 return <span />;
170 }
171 }
172
173 expect(mockMount).toHaveBeenCalledTimes(0);
174 expect(mockUnmount).toHaveBeenCalledTimes(0);
175
176 await act(async () => {
177 root.render(
178 <div>
179 <MockComponent key="A" />
180 </div>,
181 );
182 });
183
184 expect(mockMount).toHaveBeenCalledTimes(1);
185 expect(mockUnmount).toHaveBeenCalledTimes(0);
186
187 await act(async () => {
188 root.render(
189 <div>
190 <MockComponent key="B" />
191 </div>,
192 );
193 });
194
195 expect(mockMount).toHaveBeenCalledTimes(2);
196 expect(mockUnmount).toHaveBeenCalledTimes(1);
197 });
198
199 it('should warn for duplicated array keys with component stack info', async () => {
200 const container = document.createElement('div');
201 const root = ReactDOMClient.createRoot(container);
202
203 class WrapperComponent extends React.Component {
204 render() {
205 return <div>{this.props.children}</div>;
206 }
207 }
208
209 class Parent extends React.Component {
210 render() {
211 return (
212 <div>
213 <WrapperComponent>{this.props.children}</WrapperComponent>
214 </div>
215 );
216 }
217 }
218 await act(async () => {
219 root.render(<Parent>{[<div key="1" />]}</Parent>);
220 });
221
222 await act(async () => {
223 root.render(<Parent>{[<div key="1" />, <div key="1" />]}</Parent>);
224 });
225 assertConsoleErrorDev([
226 'Encountered two children with the same key, `1`. ' +
227 'Keys should be unique so that components maintain their identity ' +
228 'across updates. Non-unique keys may cause children to be ' +
229 'duplicated and/or omitted — the behavior is unsupported and ' +
230 'could change in a future version.\n' +
231 ' in div (at **)',
232 ]);
233 });
234
235 it('should warn for duplicated iterable keys with component stack info', async () => {
236 const container = document.createElement('div');
237 const root = ReactDOMClient.createRoot(container);
238
239 class WrapperComponent extends React.Component {
240 render() {
241 return <div>{this.props.children}</div>;
242 }
243 }
244
245 class Parent extends React.Component {
246 render() {
247 return (
248 <div>
249 <WrapperComponent>{this.props.children}</WrapperComponent>
250 </div>
251 );
252 }
253 }
254
255 function createIterable(array) {
256 return {
257 '@@iterator': function () {
258 let i = 0;
259 return {
260 next() {
261 const next = {
262 value: i < array.length ? array[i] : undefined,
263 done: i === array.length,
264 };
265 i++;
266 return next;
267 },
268 };
269 },
270 };
271 }
272 await act(async () => {
273 root.render(<Parent>{createIterable([<div key="1" />])}</Parent>);
274 });
275
276 await act(async () => {
277 root.render(
278 <Parent>{createIterable([<div key="1" />, <div key="1" />])}</Parent>,
279 );
280 });
281 assertConsoleErrorDev([
282 'Encountered two children with the same key, `1`. ' +
283 'Keys should be unique so that components maintain their identity ' +
284 'across updates. Non-unique keys may cause children to be ' +
285 'duplicated and/or omitted — the behavior is unsupported and ' +
286 'could change in a future version.\n' +
287 ' in div (at **)',
288 ]);
289 });
290 });
291
292 it('should warn for using maps as children with owner info', async () => {
293 class Parent extends React.Component {
294 render() {
295 return (
296 <div>
297 {
298 new Map([
299 ['foo', 0],
300 ['bar', 1],
301 ])
302 }
303 </div>
304 );
305 }
306 }
307 const container = document.createElement('div');
308 const root = ReactDOMClient.createRoot(container);
309 await act(async () => {
310 root.render(<Parent />);
311 });
312 assertConsoleErrorDev([
313 'Using Maps as children is not supported. ' +
314 'Use an array of keyed ReactElements instead.\n' +
315 ' in div (at **)\n' +
316 ' in Parent (at **)',
317 ]);
318 });
319
320 it('should NOT warn for using generator functions as components', async () => {
321 function* Foo() {
322 yield <h1 key="1">Hello</h1>;
323 yield <h1 key="2">World</h1>;
324 }
325
326 const container = document.createElement('div');
327 const root = ReactDOMClient.createRoot(container);
328 await act(async () => {
329 root.render(<Foo />);
330 });
331
332 expect(container.textContent).toBe('HelloWorld');
333 });
334
335 it('should warn for using generators as children props', async () => {
336 function* getChildren() {
337 yield <h1 key="1">Hello</h1>;
338 yield <h1 key="2">World</h1>;
339 }
340
341 function Foo() {
342 const children = getChildren();
343 return <div>{children}</div>;
344 }
345
346 const container = document.createElement('div');
347 const root = ReactDOMClient.createRoot(container);
348 await act(async () => {
349 root.render(<Foo />);
350 });
351 assertConsoleErrorDev([
352 'Using Iterators as children is unsupported and will likely yield ' +
353 'unexpected results because enumerating a generator mutates it. ' +
354 'You may convert it to an array with `Array.from()` or the ' +
355 '`[...spread]` operator before rendering. You can also use an ' +
356 'Iterable that can iterate multiple times over the same items.\n' +
357 ' in div (at **)\n' +
358 ' in Foo (at **)',
359 ]);
360
361 expect(container.textContent).toBe('HelloWorld');
362
363 // Test de-duplication
364 await act(async () => {
365 root.render(<Foo />);
366 });
367 });
368
369 it('should warn for using other types of iterators as children', async () => {
370 function Foo() {
371 let i = 0;
372 const iterator = {
373 [Symbol.iterator]() {
374 return iterator;
375 },
376 next() {
377 switch (i++) {
378 case 0:
379 return {done: false, value: <h1 key="1">Hello</h1>};
380 case 1:
381 return {done: false, value: <h1 key="2">World</h1>};
382 default:
383 return {done: true, value: undefined};
384 }
385 },
386 };
387 return iterator;
388 }
389
390 const container = document.createElement('div');
391 const root = ReactDOMClient.createRoot(container);
392 await act(async () => {
393 root.render(<Foo />);
394 });
395 assertConsoleErrorDev([
396 'Using Iterators as children is unsupported and will likely yield ' +
397 'unexpected results because enumerating a generator mutates it. ' +
398 'You may convert it to an array with `Array.from()` or the ' +
399 '`[...spread]` operator before rendering. You can also use an ' +
400 'Iterable that can iterate multiple times over the same items.\n' +
401 ' in Foo (at **)',
402 ]);
403
404 expect(container.textContent).toBe('HelloWorld');
405
406 // Test de-duplication
407 await act(async () => {
408 root.render(<Foo />);
409 });
410 });
411
412 it('should not warn for using generators in legacy iterables', async () => {
413 const fooIterable = {
414 '@@iterator': function* () {
415 yield <h1 key="1">Hello</h1>;
416 yield <h1 key="2">World</h1>;
417 },
418 };
419
420 function Foo() {
421 return fooIterable;
422 }
423
424 const container = document.createElement('div');
425 const root = ReactDOMClient.createRoot(container);
426 await act(async () => {
427 root.render(<Foo />);
428 });
429 expect(container.textContent).toBe('HelloWorld');
430
431 await act(async () => {
432 root.render(<Foo />);
433 });
434 expect(container.textContent).toBe('HelloWorld');
435 });
436
437 it('should not warn for using generators in modern iterables', async () => {
438 const fooIterable = {
439 [Symbol.iterator]: function* () {
440 yield <h1 key="1">Hello</h1>;
441 yield <h1 key="2">World</h1>;
442 },
443 };
444
445 function Foo() {
446 return fooIterable;
447 }
448
449 const div = document.createElement('div');
450 const root = ReactDOMClient.createRoot(div);
451 await act(async () => {
452 root.render(<Foo />);
453 });
454 expect(div.textContent).toBe('HelloWorld');
455
456 await act(async () => {
457 root.render(<Foo />);
458 });
459 expect(div.textContent).toBe('HelloWorld');
460 });
461
462 it('should reorder bailed-out children', async () => {
463 class LetterInner extends React.Component {
464 render() {
465 return <div>{this.props.char}</div>;
466 }
467 }
468
469 class Letter extends React.Component {
470 render() {
471 return <LetterInner char={this.props.char} />;
472 }
473 shouldComponentUpdate() {
474 return false;
475 }
476 }
477
478 class Letters extends React.Component {
479 render() {
480 const letters = this.props.letters.split('');
481 return (
482 <div>
483 {letters.map(c => (
484 <Letter key={c} char={c} />
485 ))}
486 </div>
487 );
488 }
489 }
490
491 const container = document.createElement('div');
492 const root = ReactDOMClient.createRoot(container);
493
494 // Two random strings -- some additions, some removals, some moves
495 await act(async () => {
496 root.render(<Letters letters="XKwHomsNjIkBcQWFbiZU" />);
497 });
498 expect(container.textContent).toBe('XKwHomsNjIkBcQWFbiZU');
499 await act(async () => {
500 root.render(<Letters letters="EHCjpdTUuiybDvhRJwZt" />);
501 });
502 expect(container.textContent).toBe('EHCjpdTUuiybDvhRJwZt');
503 });
504
505 it('prepares new children before unmounting old', async () => {
506 const log = [];
507
508 class Spy extends React.Component {
509 UNSAFE_componentWillMount() {
510 log.push(this.props.name + ' componentWillMount');
511 }
512 render() {
513 log.push(this.props.name + ' render');
514 return <div />;
515 }
516 componentDidMount() {
517 log.push(this.props.name + ' componentDidMount');
518 }
519 componentWillUnmount() {
520 log.push(this.props.name + ' componentWillUnmount');
521 }
522 }
523
524 // These are reference-unequal so they will be swapped even if they have
525 // matching keys
526 const SpyA = props => <Spy {...props} />;
527 const SpyB = props => <Spy {...props} />;
528
529 const container = document.createElement('div');
530 const root = ReactDOMClient.createRoot(container);
531 await act(async () => {
532 root.render(
533 <div>
534 <SpyA key="one" name="oneA" />
535 <SpyA key="two" name="twoA" />
536 </div>,
537 );
538 });
539 await act(async () => {
540 root.render(
541 <div>
542 <SpyB key="one" name="oneB" />
543 <SpyB key="two" name="twoB" />
544 </div>,
545 );
546 });
547
548 expect(log).toEqual([
549 'oneA componentWillMount',
550 'oneA render',
551 'twoA componentWillMount',
552 'twoA render',
553 'oneA componentDidMount',
554 'twoA componentDidMount',
555
556 'oneB componentWillMount',
557 'oneB render',
558 'twoB componentWillMount',
559 'twoB render',
560 'oneA componentWillUnmount',
561 'twoA componentWillUnmount',
562
563 'oneB componentDidMount',
564 'twoB componentDidMount',
565 ]);
566 });
567 });