main
js 1,227 lines 36 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('ReactChildren', () => {
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, assertConsoleErrorDev} = require('internal-test-utils'));
23 });
24
25 it('should support identity for simple', () => {
26 const context = {};
27 const callback = jest.fn().mockImplementation(function (kid, index) {
28 expect(this).toBe(context);
29 return kid;
30 });
31
32 const simpleKid = <span key="simple" />;
33
34 // First pass children into a component to fully simulate what happens when
35 // using structures that arrive from transforms.
36
37 const instance = <div>{simpleKid}</div>;
38 React.Children.forEach(instance.props.children, callback, context);
39 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
40 callback.mockClear();
41 const mappedChildren = React.Children.map(
42 instance.props.children,
43 callback,
44 context,
45 );
46 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
47 expect(mappedChildren[0]).toEqual(<span key=".$simple" />);
48 });
49
50 it('should support Portal components', () => {
51 const context = {};
52 const callback = jest.fn().mockImplementation(function (kid, index) {
53 expect(this).toBe(context);
54 return kid;
55 });
56 const ReactDOM = require('react-dom');
57 const portalContainer = document.createElement('div');
58
59 const simpleChild = <span key="simple" />;
60 const reactPortal = ReactDOM.createPortal(simpleChild, portalContainer);
61
62 const parentInstance = <div>{reactPortal}</div>;
63 React.Children.forEach(parentInstance.props.children, callback, context);
64 expect(callback).toHaveBeenCalledWith(reactPortal, 0);
65 callback.mockClear();
66 const mappedChildren = React.Children.map(
67 parentInstance.props.children,
68 callback,
69 context,
70 );
71 expect(callback).toHaveBeenCalledWith(reactPortal, 0);
72 expect(mappedChildren[0]).toEqual(reactPortal);
73 });
74
75 it('should treat single arrayless child as being in array', () => {
76 const context = {};
77 const callback = jest.fn().mockImplementation(function (kid, index) {
78 expect(this).toBe(context);
79 return kid;
80 });
81
82 const simpleKid = <span />;
83 const instance = <div>{simpleKid}</div>;
84 React.Children.forEach(instance.props.children, callback, context);
85 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
86 callback.mockClear();
87 const mappedChildren = React.Children.map(
88 instance.props.children,
89 callback,
90 context,
91 );
92 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
93 expect(mappedChildren[0]).toEqual(<span key=".0" />);
94 });
95
96 it('should treat single child in array as expected', () => {
97 const context = {};
98 const callback = jest.fn().mockImplementation(function (kid, index) {
99 expect(this).toBe(context);
100 return kid;
101 });
102
103 const simpleKid = <span key="simple" />;
104 const instance = <div>{[simpleKid]}</div>;
105 React.Children.forEach(instance.props.children, callback, context);
106 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
107 callback.mockClear();
108 const mappedChildren = React.Children.map(
109 instance.props.children,
110 callback,
111 context,
112 );
113 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
114 expect(mappedChildren[0]).toEqual(<span key=".$simple" />);
115 });
116
117 it('should be called for each child', () => {
118 const zero = <div key="keyZero" />;
119 const one = null;
120 const two = <div key="keyTwo" />;
121 const three = null;
122 const four = <div key="keyFour" />;
123 const context = {};
124
125 const callback = jest.fn().mockImplementation(function (kid) {
126 expect(this).toBe(context);
127 return kid;
128 });
129
130 const instance = (
131 <div>
132 {zero}
133 {one}
134 {two}
135 {three}
136 {four}
137 </div>
138 );
139
140 function assertCalls() {
141 expect(callback).toHaveBeenCalledWith(zero, 0);
142 expect(callback).toHaveBeenCalledWith(one, 1);
143 expect(callback).toHaveBeenCalledWith(two, 2);
144 expect(callback).toHaveBeenCalledWith(three, 3);
145 expect(callback).toHaveBeenCalledWith(four, 4);
146 callback.mockClear();
147 }
148
149 React.Children.forEach(instance.props.children, callback, context);
150 assertCalls();
151
152 const mappedChildren = React.Children.map(
153 instance.props.children,
154 callback,
155 context,
156 );
157 assertCalls();
158 expect(mappedChildren).toEqual([
159 <div key=".$keyZero" />,
160 <div key=".$keyTwo" />,
161 <div key=".$keyFour" />,
162 ]);
163 });
164
165 it('should traverse children of different kinds', () => {
166 const div = <div key="divNode" />;
167 const span = <span key="spanNode" />;
168 const a = <a key="aNode" />;
169
170 const context = {};
171 const callback = jest.fn().mockImplementation(function (kid) {
172 expect(this).toBe(context);
173 return kid;
174 });
175
176 const instance = (
177 <div>
178 {div}
179 {[[span]]}
180 {[a]}
181 {'string'}
182 {1234}
183 {true}
184 {false}
185 {null}
186 {undefined}
187 {9n}
188 </div>
189 );
190
191 function assertCalls() {
192 expect(callback).toHaveBeenCalledTimes(10);
193 expect(callback).toHaveBeenCalledWith(div, 0);
194 expect(callback).toHaveBeenCalledWith(span, 1);
195 expect(callback).toHaveBeenCalledWith(a, 2);
196 expect(callback).toHaveBeenCalledWith('string', 3);
197 expect(callback).toHaveBeenCalledWith(1234, 4);
198 expect(callback).toHaveBeenCalledWith(null, 5);
199 expect(callback).toHaveBeenCalledWith(null, 6);
200 expect(callback).toHaveBeenCalledWith(null, 7);
201 expect(callback).toHaveBeenCalledWith(null, 8);
202 expect(callback).toHaveBeenCalledWith(9n, 9);
203 callback.mockClear();
204 }
205
206 React.Children.forEach(instance.props.children, callback, context);
207 assertCalls();
208
209 const mappedChildren = React.Children.map(
210 instance.props.children,
211 callback,
212 context,
213 );
214 assertCalls();
215 expect(mappedChildren).toEqual([
216 <div key=".$divNode" />,
217 <span key=".1:0:$spanNode" />,
218 <a key=".2:$aNode" />,
219 'string',
220 1234,
221 9n,
222 ]);
223 });
224
225 it('should be called for each child in nested structure', () => {
226 const zero = <div key="keyZero" />;
227 const one = null;
228 const two = <div key="keyTwo" />;
229 const three = null;
230 const four = <div key="keyFour" />;
231 const five = <div key="keyFive" />;
232
233 const context = {};
234 const callback = jest.fn().mockImplementation(function (kid) {
235 return kid;
236 });
237
238 const instance = <div>{[[zero, one, two], [three, four], five]}</div>;
239
240 function assertCalls() {
241 expect(callback).toHaveBeenCalledTimes(6);
242 expect(callback).toHaveBeenCalledWith(zero, 0);
243 expect(callback).toHaveBeenCalledWith(one, 1);
244 expect(callback).toHaveBeenCalledWith(two, 2);
245 expect(callback).toHaveBeenCalledWith(three, 3);
246 expect(callback).toHaveBeenCalledWith(four, 4);
247 expect(callback).toHaveBeenCalledWith(five, 5);
248 callback.mockClear();
249 }
250
251 React.Children.forEach(instance.props.children, callback, context);
252 assertCalls();
253
254 const mappedChildren = React.Children.map(
255 instance.props.children,
256 callback,
257 context,
258 );
259 assertCalls();
260 expect(mappedChildren).toEqual([
261 <div key=".0:$keyZero" />,
262 <div key=".0:$keyTwo" />,
263 <div key=".1:$keyFour" />,
264 <div key=".$keyFive" />,
265 ]);
266 });
267
268 it('should retain key across two mappings', () => {
269 const zeroForceKey = <div key="keyZero" />;
270 const oneForceKey = <div key="keyOne" />;
271 const context = {};
272 const callback = jest.fn().mockImplementation(function (kid) {
273 expect(this).toBe(context);
274 return kid;
275 });
276
277 const forcedKeys = (
278 <div>
279 {zeroForceKey}
280 {oneForceKey}
281 </div>
282 );
283
284 function assertCalls() {
285 expect(callback).toHaveBeenCalledWith(zeroForceKey, 0);
286 expect(callback).toHaveBeenCalledWith(oneForceKey, 1);
287 callback.mockClear();
288 }
289
290 React.Children.forEach(forcedKeys.props.children, callback, context);
291 assertCalls();
292
293 const mappedChildren = React.Children.map(
294 forcedKeys.props.children,
295 callback,
296 context,
297 );
298 assertCalls();
299 expect(mappedChildren).toEqual([
300 <div key=".$keyZero" />,
301 <div key=".$keyOne" />,
302 ]);
303 });
304
305 it('should be called for each child in an iterable without keys', async () => {
306 const threeDivIterable = {
307 '@@iterator': function () {
308 let i = 0;
309 return {
310 next: function () {
311 if (i++ < 3) {
312 return {value: <div />, done: false};
313 } else {
314 return {value: undefined, done: true};
315 }
316 },
317 };
318 },
319 };
320
321 const context = {};
322 const callback = jest.fn().mockImplementation(function (kid) {
323 expect(this).toBe(context);
324 return kid;
325 });
326
327 function assertCalls() {
328 expect(callback).toHaveBeenCalledTimes(3);
329 expect(callback).toHaveBeenCalledWith(<div />, 0);
330 expect(callback).toHaveBeenCalledWith(<div />, 1);
331 expect(callback).toHaveBeenCalledWith(<div />, 2);
332 callback.mockClear();
333 }
334
335 const instance = <div>{threeDivIterable}</div>;
336
337 React.Children.forEach(instance.props.children, callback, context);
338
339 assertCalls();
340
341 const mappedChildren = React.Children.map(
342 instance.props.children,
343 callback,
344 context,
345 );
346 assertCalls();
347 expect(mappedChildren).toEqual([
348 <div key=".0" />,
349 <div key=".1" />,
350 <div key=".2" />,
351 ]);
352
353 const container = document.createElement('div');
354 const root = ReactDOMClient.createRoot(container);
355 await act(() => {
356 root.render(instance);
357 });
358 assertConsoleErrorDev([
359 'Each child in a list should have a unique "key" prop.\n\n' +
360 'Check the top-level render call using <div>. It was passed a child from div.' +
361 ' See https://react.dev/link/warning-keys for more information.\n' +
362 ' in div (at **)',
363 ]);
364 });
365
366 it('should be called for each child in an iterable with keys', () => {
367 const threeDivIterable = {
368 '@@iterator': function () {
369 let i = 0;
370 return {
371 next: function () {
372 if (i++ < 3) {
373 return {value: <div key={'#' + i} />, done: false};
374 } else {
375 return {value: undefined, done: true};
376 }
377 },
378 };
379 },
380 };
381
382 const context = {};
383 const callback = jest.fn().mockImplementation(function (kid) {
384 expect(this).toBe(context);
385 return kid;
386 });
387
388 const instance = <div>{threeDivIterable}</div>;
389
390 function assertCalls() {
391 expect(callback).toHaveBeenCalledTimes(3);
392 expect(callback).toHaveBeenCalledWith(<div key="#1" />, 0);
393 expect(callback).toHaveBeenCalledWith(<div key="#2" />, 1);
394 expect(callback).toHaveBeenCalledWith(<div key="#3" />, 2);
395 callback.mockClear();
396 }
397
398 React.Children.forEach(instance.props.children, callback, context);
399 assertCalls();
400
401 const mappedChildren = React.Children.map(
402 instance.props.children,
403 callback,
404 context,
405 );
406 assertCalls();
407 expect(mappedChildren).toEqual([
408 <div key=".$#1" />,
409 <div key=".$#2" />,
410 <div key=".$#3" />,
411 ]);
412 });
413
414 it('should not enumerate enumerable numbers (#4776)', () => {
415 /*eslint-disable no-extend-native */
416 Number.prototype['@@iterator'] = function () {
417 throw new Error('number iterator called');
418 };
419 /*eslint-enable no-extend-native */
420
421 try {
422 const instance = (
423 <div>
424 {5}
425 {12}
426 {13}
427 </div>
428 );
429
430 const context = {};
431 const callback = jest.fn().mockImplementation(function (kid) {
432 expect(this).toBe(context);
433 return kid;
434 });
435
436 const assertCalls = function () {
437 expect(callback).toHaveBeenCalledTimes(3);
438 expect(callback).toHaveBeenCalledWith(5, 0);
439 expect(callback).toHaveBeenCalledWith(12, 1);
440 expect(callback).toHaveBeenCalledWith(13, 2);
441 callback.mockClear();
442 };
443
444 React.Children.forEach(instance.props.children, callback, context);
445 assertCalls();
446
447 const mappedChildren = React.Children.map(
448 instance.props.children,
449 callback,
450 context,
451 );
452 assertCalls();
453 expect(mappedChildren).toEqual([5, 12, 13]);
454 } finally {
455 delete Number.prototype['@@iterator'];
456 }
457 });
458
459 it('should allow extension of native prototypes', () => {
460 /*eslint-disable no-extend-native */
461 String.prototype.key = 'react';
462 Number.prototype.key = 'rocks';
463 /*eslint-enable no-extend-native */
464
465 const instance = (
466 <div>
467 {'a'}
468 {13}
469 </div>
470 );
471
472 const context = {};
473 const callback = jest.fn().mockImplementation(function (kid) {
474 expect(this).toBe(context);
475 return kid;
476 });
477
478 function assertCalls() {
479 expect(callback).toHaveBeenCalledTimes(2);
480 expect(callback).toHaveBeenCalledWith('a', 0);
481 expect(callback).toHaveBeenCalledWith(13, 1);
482 callback.mockClear();
483 }
484
485 React.Children.forEach(instance.props.children, callback, context);
486 assertCalls();
487
488 const mappedChildren = React.Children.map(
489 instance.props.children,
490 callback,
491 context,
492 );
493 assertCalls();
494 expect(mappedChildren).toEqual(['a', 13]);
495
496 delete String.prototype.key;
497 delete Number.prototype.key;
498 });
499
500 it('should pass key to returned component', () => {
501 const mapFn = function (kid, index) {
502 return <div>{kid}</div>;
503 };
504
505 const simpleKid = <span key="simple" />;
506
507 const instance = <div>{simpleKid}</div>;
508 const mappedChildren = React.Children.map(instance.props.children, mapFn);
509
510 expect(React.Children.count(mappedChildren)).toBe(1);
511 expect(mappedChildren[0]).not.toBe(simpleKid);
512 expect(mappedChildren[0].props.children).toBe(simpleKid);
513 expect(mappedChildren[0].key).toBe('.$simple');
514 });
515
516 it('should invoke callback with the right context', () => {
517 let lastContext;
518 const callback = function (kid, index) {
519 lastContext = this;
520 return this;
521 };
522
523 // TODO: Use an object to test, after non-object fragments has fully landed.
524 const scopeTester = 'scope tester';
525
526 const simpleKid = <span key="simple" />;
527 const instance = <div>{simpleKid}</div>;
528 React.Children.forEach(instance.props.children, callback, scopeTester);
529 expect(lastContext).toBe(scopeTester);
530
531 const mappedChildren = React.Children.map(
532 instance.props.children,
533 callback,
534 scopeTester,
535 );
536
537 expect(React.Children.count(mappedChildren)).toBe(1);
538 expect(mappedChildren[0]).toBe(scopeTester);
539 });
540
541 it('should be called for each child in array', () => {
542 const zero = <div key="keyZero" />;
543 const one = null;
544 const two = <div key="keyTwo" />;
545 const three = null;
546 const four = <div key="keyFour" />;
547
548 const mapped = [
549 <div key="giraffe" />, // Key should be joined to obj key
550 null, // Key should be added even if we don't supply it!
551 <div />, // Key should be added even if not supplied!
552 <span />, // Map from null to something.
553 <div key="keyFour" />,
554 ];
555 const callback = jest.fn().mockImplementation(function (kid, index) {
556 return mapped[index];
557 });
558
559 const instance = (
560 <div>
561 {zero}
562 {one}
563 {two}
564 {three}
565 {four}
566 </div>
567 );
568
569 React.Children.forEach(instance.props.children, callback);
570 expect(callback).toHaveBeenCalledWith(zero, 0);
571 expect(callback).toHaveBeenCalledWith(one, 1);
572 expect(callback).toHaveBeenCalledWith(two, 2);
573 expect(callback).toHaveBeenCalledWith(three, 3);
574 expect(callback).toHaveBeenCalledWith(four, 4);
575 callback.mockClear();
576
577 const mappedChildren = React.Children.map(
578 instance.props.children,
579 callback,
580 );
581 expect(callback).toHaveBeenCalledTimes(5);
582 expect(React.Children.count(mappedChildren)).toBe(4);
583 // Keys default to indices.
584 expect([
585 mappedChildren[0].key,
586 mappedChildren[1].key,
587 mappedChildren[2].key,
588 mappedChildren[3].key,
589 ]).toEqual(['giraffe/.$keyZero', '.$keyTwo', '.3', '.$keyFour']);
590
591 expect(callback).toHaveBeenCalledWith(zero, 0);
592 expect(callback).toHaveBeenCalledWith(one, 1);
593 expect(callback).toHaveBeenCalledWith(two, 2);
594 expect(callback).toHaveBeenCalledWith(three, 3);
595 expect(callback).toHaveBeenCalledWith(four, 4);
596
597 expect(mappedChildren[0]).toEqual(<div key="giraffe/.$keyZero" />);
598 expect(mappedChildren[1]).toEqual(<div key=".$keyTwo" />);
599 expect(mappedChildren[2]).toEqual(<span key=".3" />);
600 expect(mappedChildren[3]).toEqual(<div key=".$keyFour" />);
601 });
602
603 it('should be called for each child in nested structure with mapping', () => {
604 const zero = <div key="keyZero" />;
605 const one = null;
606 const two = <div key="keyTwo" />;
607 const three = null;
608 const four = <div key="keyFour" />;
609 const five = <div key="keyFive" />;
610
611 const zeroMapped = <div key="giraffe" />; // Key should be overridden
612 const twoMapped = <div />; // Key should be added even if not supplied!
613 const fourMapped = <div key="keyFour" />;
614 const fiveMapped = <div />;
615
616 const callback = jest.fn().mockImplementation(function (kid) {
617 switch (kid) {
618 case zero:
619 return zeroMapped;
620 case two:
621 return twoMapped;
622 case four:
623 return fourMapped;
624 case five:
625 return fiveMapped;
626 default:
627 return kid;
628 }
629 });
630
631 const frag = [[zero, one, two], [three, four], five];
632 const instance = <div>{[frag]}</div>;
633
634 React.Children.forEach(instance.props.children, callback);
635 expect(callback).toHaveBeenCalledTimes(6);
636 expect(callback).toHaveBeenCalledWith(zero, 0);
637 expect(callback).toHaveBeenCalledWith(one, 1);
638 expect(callback).toHaveBeenCalledWith(two, 2);
639 expect(callback).toHaveBeenCalledWith(three, 3);
640 expect(callback).toHaveBeenCalledWith(four, 4);
641 expect(callback).toHaveBeenCalledWith(five, 5);
642 callback.mockClear();
643
644 const mappedChildren = React.Children.map(
645 instance.props.children,
646 callback,
647 );
648 expect(callback).toHaveBeenCalledTimes(6);
649 expect(callback).toHaveBeenCalledWith(zero, 0);
650 expect(callback).toHaveBeenCalledWith(one, 1);
651 expect(callback).toHaveBeenCalledWith(two, 2);
652 expect(callback).toHaveBeenCalledWith(three, 3);
653 expect(callback).toHaveBeenCalledWith(four, 4);
654 expect(callback).toHaveBeenCalledWith(five, 5);
655
656 expect(React.Children.count(mappedChildren)).toBe(4);
657 // Keys default to indices.
658 expect([
659 mappedChildren[0].key,
660 mappedChildren[1].key,
661 mappedChildren[2].key,
662 mappedChildren[3].key,
663 ]).toEqual([
664 'giraffe/.0:0:$keyZero',
665 '.0:0:$keyTwo',
666 '.0:1:$keyFour',
667 '.0:$keyFive',
668 ]);
669
670 expect(mappedChildren[0]).toEqual(<div key="giraffe/.0:0:$keyZero" />);
671 expect(mappedChildren[1]).toEqual(<div key=".0:0:$keyTwo" />);
672 expect(mappedChildren[2]).toEqual(<div key=".0:1:$keyFour" />);
673 expect(mappedChildren[3]).toEqual(<div key=".0:$keyFive" />);
674 });
675
676 it('should retain key across two mappings with conditions', () => {
677 const zeroForceKey = <div key="keyZero" />;
678 const oneForceKey = <div key="keyOne" />;
679
680 // Key should be joined to object key
681 const zeroForceKeyMapped = <div key="giraffe" />;
682 // Key should be added even if we don't supply it!
683 const oneForceKeyMapped = <div />;
684
685 const mapFn = function (kid, index) {
686 return index === 0 ? zeroForceKeyMapped : oneForceKeyMapped;
687 };
688
689 const forcedKeys = (
690 <div>
691 {zeroForceKey}
692 {oneForceKey}
693 </div>
694 );
695
696 const expectedForcedKeys = ['giraffe/.$keyZero', '.$keyOne'];
697 const mappedChildrenForcedKeys = React.Children.map(
698 forcedKeys.props.children,
699 mapFn,
700 );
701 const mappedForcedKeys = mappedChildrenForcedKeys.map(c => c.key);
702 expect(mappedForcedKeys).toEqual(expectedForcedKeys);
703
704 const expectedRemappedForcedKeys = [
705 'giraffe/.$giraffe/.$keyZero',
706 '.$.$keyOne',
707 ];
708 const remappedChildrenForcedKeys = React.Children.map(
709 mappedChildrenForcedKeys,
710 mapFn,
711 );
712 expect(remappedChildrenForcedKeys.map(c => c.key)).toEqual(
713 expectedRemappedForcedKeys,
714 );
715 });
716
717 it('should not throw if key provided is a dupe with array key', () => {
718 const zero = <div />;
719 const one = <div key="0" />;
720
721 const mapFn = function () {
722 return null;
723 };
724
725 const instance = (
726 <div>
727 {zero}
728 {one}
729 </div>
730 );
731
732 expect(function () {
733 React.Children.map(instance.props.children, mapFn);
734 }).not.toThrow();
735 });
736
737 it('should use the same key for a cloned element', () => {
738 const instance = (
739 <div>
740 <div />
741 </div>
742 );
743
744 const mapped = React.Children.map(
745 instance.props.children,
746 element => element,
747 );
748
749 const mappedWithClone = React.Children.map(
750 instance.props.children,
751 element => React.cloneElement(element),
752 );
753
754 expect(mapped[0].key).toBe(mappedWithClone[0].key);
755 });
756
757 it('should use the same key for a cloned element with key', () => {
758 const instance = (
759 <div>
760 <div key="unique" />
761 </div>
762 );
763
764 const mapped = React.Children.map(
765 instance.props.children,
766 element => element,
767 );
768
769 const mappedWithClone = React.Children.map(
770 instance.props.children,
771 element => React.cloneElement(element, {key: 'unique'}),
772 );
773
774 expect(mapped[0].key).toBe(mappedWithClone[0].key);
775 });
776
777 it('should return 0 for null children', () => {
778 const numberOfChildren = React.Children.count(null);
779 expect(numberOfChildren).toBe(0);
780 });
781
782 it('should return 0 for undefined children', () => {
783 const numberOfChildren = React.Children.count(undefined);
784 expect(numberOfChildren).toBe(0);
785 });
786
787 it('should return 1 for single child', () => {
788 const simpleKid = <span key="simple" />;
789 const instance = <div>{simpleKid}</div>;
790 const numberOfChildren = React.Children.count(instance.props.children);
791 expect(numberOfChildren).toBe(1);
792 });
793
794 it('should count the number of children in flat structure', () => {
795 const zero = <div key="keyZero" />;
796 const one = null;
797 const two = <div key="keyTwo" />;
798 const three = null;
799 const four = <div key="keyFour" />;
800
801 const instance = (
802 <div>
803 {zero}
804 {one}
805 {two}
806 {three}
807 {four}
808 </div>
809 );
810 const numberOfChildren = React.Children.count(instance.props.children);
811 expect(numberOfChildren).toBe(5);
812 });
813
814 it('should count the number of children in nested structure', () => {
815 const zero = <div key="keyZero" />;
816 const one = null;
817 const two = <div key="keyTwo" />;
818 const three = null;
819 const four = <div key="keyFour" />;
820 const five = <div key="keyFive" />;
821
822 const instance = (
823 <div>{[[[zero, one, two], [three, four], five], null]}</div>
824 );
825 const numberOfChildren = React.Children.count(instance.props.children);
826 expect(numberOfChildren).toBe(7);
827 });
828
829 it('should flatten children to an array', () => {
830 expect(React.Children.toArray(undefined)).toEqual([]);
831 expect(React.Children.toArray(null)).toEqual([]);
832
833 expect(React.Children.toArray(<div />).length).toBe(1);
834 expect(React.Children.toArray([<div />]).length).toBe(1);
835 expect(React.Children.toArray(<div />)[0].key).toBe(
836 React.Children.toArray([<div />])[0].key,
837 );
838
839 const flattened = React.Children.toArray([
840 [<div key="apple" />, <div key="banana" />, <div key="camel" />],
841 [<div key="banana" />, <div key="camel" />, <div key="deli" />],
842 ]);
843 expect(flattened.length).toBe(6);
844 expect(flattened[1].key).toContain('banana');
845 expect(flattened[3].key).toContain('banana');
846 expect(flattened[1].key).not.toBe(flattened[3].key);
847
848 const reversed = React.Children.toArray([
849 [<div key="camel" />, <div key="banana" />, <div key="apple" />],
850 [<div key="deli" />, <div key="camel" />, <div key="banana" />],
851 ]);
852 expect(flattened[0].key).toBe(reversed[2].key);
853 expect(flattened[1].key).toBe(reversed[1].key);
854 expect(flattened[2].key).toBe(reversed[0].key);
855 expect(flattened[3].key).toBe(reversed[5].key);
856 expect(flattened[4].key).toBe(reversed[4].key);
857 expect(flattened[5].key).toBe(reversed[3].key);
858
859 // null/undefined/bool are all omitted
860 expect(React.Children.toArray([1, 'two', null, undefined, true])).toEqual([
861 1,
862 'two',
863 ]);
864 });
865
866 it('warns for mapped list children without keys', async () => {
867 spyOnDev(console, 'error').mockImplementation(() => {});
868 function ComponentRenderingMappedChildren({children}) {
869 return (
870 <div>
871 {React.Children.map(children, child => (
872 <div />
873 ))}
874 </div>
875 );
876 }
877
878 const container = document.createElement('div');
879 const root = ReactDOMClient.createRoot(container);
880 await act(() => {
881 root.render(
882 <ComponentRenderingMappedChildren>
883 {[<div />]}
884 </ComponentRenderingMappedChildren>,
885 );
886 });
887 if (__DEV__) {
888 const calls = console.error.mock.calls;
889 console.error.mockRestore();
890 expect(calls.length).toBe(1);
891 expect(calls[0][0]).toEqual(
892 'Each child in a list should have a unique "key" prop.%s%s See https://react.dev/link/warning-keys for more information.',
893 );
894 }
895 });
896
897 it('does not warn for mapped static children without keys', async () => {
898 function ComponentRenderingMappedChildren({children}) {
899 return (
900 <div>
901 {React.Children.map(children, child => (
902 <div />
903 ))}
904 </div>
905 );
906 }
907
908 const container = document.createElement('div');
909 const root = ReactDOMClient.createRoot(container);
910 await act(() => {
911 root.render(
912 <ComponentRenderingMappedChildren>
913 <div />
914 <div />
915 </ComponentRenderingMappedChildren>,
916 );
917 });
918 });
919
920 it('warns for cloned list children without keys', async () => {
921 function ComponentRenderingClonedChildren({children}) {
922 return (
923 <div>
924 {React.Children.map(children, child => React.cloneElement(child))}
925 </div>
926 );
927 }
928
929 const container = document.createElement('div');
930 const root = ReactDOMClient.createRoot(container);
931 await act(() => {
932 root.render(
933 <ComponentRenderingClonedChildren>
934 {[<div />]}
935 </ComponentRenderingClonedChildren>,
936 );
937 });
938 assertConsoleErrorDev([
939 'Each child in a list should have a unique "key" prop.\n\n' +
940 'Check the render method of `ComponentRenderingClonedChildren`.' +
941 ' See https://react.dev/link/warning-keys for more information.\n' +
942 ' in div (at **)',
943 ]);
944 });
945
946 it('does not warn for cloned static children without keys', async () => {
947 function ComponentRenderingClonedChildren({children}) {
948 return (
949 <div>
950 {React.Children.map(children, child => React.cloneElement(child))}
951 </div>
952 );
953 }
954
955 const container = document.createElement('div');
956 const root = ReactDOMClient.createRoot(container);
957 await act(() => {
958 root.render(
959 <ComponentRenderingClonedChildren>
960 <div />
961 <div />
962 </ComponentRenderingClonedChildren>,
963 );
964 });
965 });
966
967 it('warns for flattened list children without keys', async () => {
968 function ComponentRenderingFlattenedChildren({children}) {
969 return <div>{React.Children.toArray(children)}</div>;
970 }
971
972 const container = document.createElement('div');
973 const root = ReactDOMClient.createRoot(container);
974 await act(() => {
975 root.render(
976 <ComponentRenderingFlattenedChildren>
977 {[<div />]}
978 </ComponentRenderingFlattenedChildren>,
979 );
980 });
981 assertConsoleErrorDev([
982 'Each child in a list should have a unique "key" prop.\n\n' +
983 'Check the render method of `ComponentRenderingFlattenedChildren`.' +
984 ' See https://react.dev/link/warning-keys for more information.\n' +
985 ' in div (at **)',
986 ]);
987 });
988
989 it('does not warn for flattened static children without keys', async () => {
990 function ComponentRenderingFlattenedChildren({children}) {
991 return <div>{React.Children.toArray(children)}</div>;
992 }
993
994 const container = document.createElement('div');
995 const root = ReactDOMClient.createRoot(container);
996 await act(() => {
997 root.render(
998 <ComponentRenderingFlattenedChildren>
999 <div />
1000 <div />
1001 </ComponentRenderingFlattenedChildren>,
1002 );
1003 });
1004 });
1005
1006 it('does not throw on children without `_store`', async () => {
1007 function ComponentRenderingFlattenedChildren({children}) {
1008 return <div>{React.Children.toArray(children)}</div>;
1009 }
1010
1011 const source = <div />;
1012 const productionElement = {};
1013 Object.entries(source).forEach(([key, value]) => {
1014 if (key !== '_owner' && key !== '_store') {
1015 productionElement[key] = value;
1016 }
1017 });
1018 Object.freeze(productionElement);
1019
1020 const container = document.createElement('div');
1021 const root = ReactDOMClient.createRoot(container);
1022 await act(() => {
1023 root.render(
1024 <ComponentRenderingFlattenedChildren>
1025 {productionElement}
1026 </ComponentRenderingFlattenedChildren>,
1027 );
1028 });
1029 });
1030
1031 it('should escape keys', () => {
1032 const zero = <div key="1" />;
1033 const one = <div key="1=::=2" />;
1034 const instance = (
1035 <div>
1036 {zero}
1037 {one}
1038 </div>
1039 );
1040 const mappedChildren = React.Children.map(
1041 instance.props.children,
1042 kid => kid,
1043 );
1044 expect(mappedChildren).toEqual([
1045 <div key=".$1" />,
1046 <div key=".$1=0=2=2=02" />,
1047 ]);
1048 });
1049
1050 it('should combine keys when map returns an array', () => {
1051 const instance = (
1052 <div>
1053 <div key="a" />
1054 {false}
1055 <div key="b" />
1056 <p />
1057 </div>
1058 );
1059 const mappedChildren = React.Children.map(
1060 instance.props.children,
1061 // Try a few things: keyed, unkeyed, hole, and a cloned element.
1062 kid => [
1063 <span key="x" />,
1064 null,
1065 <span key="y" />,
1066 kid,
1067 kid && React.cloneElement(kid, {key: 'z'}),
1068 <hr />,
1069 ],
1070 );
1071 expect(mappedChildren.length).toBe(18);
1072
1073 // <div key="a">
1074 expect(mappedChildren[0].type).toBe('span');
1075 expect(mappedChildren[0].key).toBe('.$a/.$x');
1076 expect(mappedChildren[1].type).toBe('span');
1077 expect(mappedChildren[1].key).toBe('.$a/.$y');
1078 expect(mappedChildren[2].type).toBe('div');
1079 expect(mappedChildren[2].key).toBe('.$a/.$a');
1080 expect(mappedChildren[3].type).toBe('div');
1081 expect(mappedChildren[3].key).toBe('.$a/.$z');
1082 expect(mappedChildren[4].type).toBe('hr');
1083 expect(mappedChildren[4].key).toBe('.$a/.5');
1084
1085 // false
1086 expect(mappedChildren[5].type).toBe('span');
1087 expect(mappedChildren[5].key).toBe('.1/.$x');
1088 expect(mappedChildren[6].type).toBe('span');
1089 expect(mappedChildren[6].key).toBe('.1/.$y');
1090 expect(mappedChildren[7].type).toBe('hr');
1091 expect(mappedChildren[7].key).toBe('.1/.5');
1092
1093 // <div key="b">
1094 expect(mappedChildren[8].type).toBe('span');
1095 expect(mappedChildren[8].key).toBe('.$b/.$x');
1096 expect(mappedChildren[9].type).toBe('span');
1097 expect(mappedChildren[9].key).toBe('.$b/.$y');
1098 expect(mappedChildren[10].type).toBe('div');
1099 expect(mappedChildren[10].key).toBe('.$b/.$b');
1100 expect(mappedChildren[11].type).toBe('div');
1101 expect(mappedChildren[11].key).toBe('.$b/.$z');
1102 expect(mappedChildren[12].type).toBe('hr');
1103 expect(mappedChildren[12].key).toBe('.$b/.5');
1104
1105 // <p>
1106 expect(mappedChildren[13].type).toBe('span');
1107 expect(mappedChildren[13].key).toBe('.3/.$x');
1108 expect(mappedChildren[14].type).toBe('span');
1109 expect(mappedChildren[14].key).toBe('.3/.$y');
1110 expect(mappedChildren[15].type).toBe('p');
1111 expect(mappedChildren[15].key).toBe('.3/.3');
1112 expect(mappedChildren[16].type).toBe('p');
1113 expect(mappedChildren[16].key).toBe('.3/.$z');
1114 expect(mappedChildren[17].type).toBe('hr');
1115 expect(mappedChildren[17].key).toBe('.3/.5');
1116 });
1117
1118 it('should throw on object', () => {
1119 expect(function () {
1120 React.Children.forEach({a: 1, b: 2}, function () {}, null);
1121 }).toThrow(
1122 'Objects are not valid as a React child (found: object with keys ' +
1123 '{a, b}).' +
1124 (__DEV__
1125 ? ' If you meant to render a collection of children, use an ' +
1126 'array instead.'
1127 : ''),
1128 );
1129 });
1130
1131 it('should render React.lazy after suspending', async () => {
1132 const lazyElement = React.lazy(async () => ({default: <div key="hi" />}));
1133 function Component() {
1134 return React.Children.map([lazyElement], c =>
1135 React.cloneElement(c, {children: 'hi'}),
1136 );
1137 }
1138 const container = document.createElement('div');
1139 const root = ReactDOMClient.createRoot(container);
1140 await act(() => {
1141 root.render(<Component />);
1142 });
1143
1144 expect(container.innerHTML).toBe('<div>hi</div>');
1145 });
1146
1147 it('should render cached Promises after suspending', async () => {
1148 const promise = Promise.resolve(<div key="hi" />);
1149 function Component() {
1150 return React.Children.map([promise], c =>
1151 React.cloneElement(c, {children: 'hi'}),
1152 );
1153 }
1154 const container = document.createElement('div');
1155 const root = ReactDOMClient.createRoot(container);
1156 await act(() => {
1157 root.render(<Component />);
1158 });
1159
1160 expect(container.innerHTML).toBe('<div>hi</div>');
1161 });
1162
1163 it('should throw on regex', () => {
1164 // Really, we care about dates (#4840) but those have nondeterministic
1165 // serialization (timezones) so let's test a regex instead:
1166 expect(function () {
1167 React.Children.forEach(/abc/, function () {}, null);
1168 }).toThrow(
1169 'Objects are not valid as a React child (found: /abc/).' +
1170 (__DEV__
1171 ? ' If you meant to render a collection of children, use an ' +
1172 'array instead.'
1173 : ''),
1174 );
1175 });
1176
1177 describe('with fragments enabled', () => {
1178 it('warns for keys for arrays of elements in a fragment', async () => {
1179 class ComponentReturningArray extends React.Component {
1180 render() {
1181 return [<div />, <div />];
1182 }
1183 }
1184
1185 const container = document.createElement('div');
1186 const root = ReactDOMClient.createRoot(container);
1187 await act(() => {
1188 root.render(<ComponentReturningArray />);
1189 });
1190 assertConsoleErrorDev([
1191 'Each child in a list should have a unique "key" prop.' +
1192 '\n\nCheck the top-level render call using <ComponentReturningArray>. It was passed a child from ComponentReturningArray. ' +
1193 'See https://react.dev/link/warning-keys for more information.' +
1194 '\n in div (at **)' +
1195 '\n in ComponentReturningArray (at **)',
1196 ]);
1197 });
1198
1199 it('does not warn when there are keys on elements in a fragment', async () => {
1200 class ComponentReturningArray extends React.Component {
1201 render() {
1202 return [<div key="foo" />, <div key="bar" />];
1203 }
1204 }
1205
1206 const container = document.createElement('div');
1207 const root = ReactDOMClient.createRoot(container);
1208 await act(() => {
1209 root.render(<ComponentReturningArray />);
1210 });
1211 });
1212
1213 it('warns for keys for arrays at the top level', async () => {
1214 const container = document.createElement('div');
1215 const root = ReactDOMClient.createRoot(container);
1216 await act(() => {
1217 root.render([<div />, <div />]);
1218 });
1219 assertConsoleErrorDev([
1220 'Each child in a list should have a unique "key" prop.' +
1221 '\n\nCheck the top-level render call using <Root>. ' +
1222 'See https://react.dev/link/warning-keys for more information.' +
1223 '\n in div (at **)',
1224 ]);
1225 });
1226 });
1227 });