@samitouri / QOS-React / commits / c74034589e

[Fiber] Support iterables (#8446)

* Add iterable cases to MultiChildReconcile test Stack currently supports rendering iterables, but Fiber does not. Previously we didn't have any public API tests for iterables. We have tests for traverseAllChildren() which is shared between React.Children and Stack. However Fiber doesn't currently use it, and likely won't. So this commit is a first step towards actually testing iterable support via public API. The next step will be to port traverseAllChildren() tests to test React.Children API instead. * Implement iterable reconciliation in Fiber This uses the same exact algorithm as array reconciliation but uses iterator to step through. This gets reconcile tests to pass again but introduces a regression in ReactMultiChildText case which uses Maps as children. It passed before because Maps were ignored, but now it's failing because this actually runs the Map code path in Fiber. We can throw early in this case when we want to follow up on this. * Rewrite traverseAllChildren() tests against React.Children API This function was used in React.Children and Stack. The corresponding reconciliation functionality is being tested by ReactMultiChild tests. So we can move these tests to ReactChildren and test its public API.

Dan Abramov committed Nov 28, 2016 at 23:44 UTC c74034589e6875402b004b243a3debeda406a7ae
9 files changed +745 -640
scripts/fiber/tests-failing.txt
+1
@@ -99,6 +99,7 @@ src/renderers/shared/shared/__tests__/ReactEmptyComponent-test.js
99
100 src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js
101 * should correctly handle all possible children for render and update
102 +* should reorder keyed text nodes
103
104 src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js
105 * should warn when stateless component returns array
scripts/fiber/tests-passing-except-dev.txt
+1 -6
@@ -171,14 +171,9 @@ src/renderers/shared/shared/__tests__/ReactCompositeComponent-test.js
171
172 src/renderers/shared/shared/__tests__/ReactMultiChild-test.js
173 * should warn for duplicated keys with component stack info
174 -
175 -src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js
176 -* should reorder keyed text nodes
174 +* should warn for using maps as children with owner info
175
176 src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js
177 * should warn for childContextTypes on a functional component
178 * should warn when given a ref
179 * should use correct name in key warning
182 -
183 -src/shared/utils/__tests__/traverseAllChildren-test.js
184 -* should warn for using maps as children with owner info
scripts/fiber/tests-passing.txt
+13 -17
@@ -108,6 +108,15 @@ src/isomorphic/children/__tests__/ReactChildren-test.js
108 * should support identity for simple
109 * should treat single arrayless child as being in array
110 * should treat single child in array as expected
111 +* should be called for each child
112 +* should traverse children of different kinds
113 +* should be called for each child in nested structure
114 +* should retain key across two mappings
115 +* should be called for each child in an iterable without keys
116 +* should be called for each child in an iterable with keys
117 +* should use keys from entry iterables
118 +* should not enumerate enumerable numbers (#4776)
119 +* should allow extension of native prototypes
120 * should pass key to returned component
121 * should invoke callback with the right context
122 * should be called for each child
@@ -122,6 +131,8 @@ src/isomorphic/children/__tests__/ReactChildren-test.js
131 * should count the number of children in flat structure
132 * should count the number of children in nested structure
133 * should flatten children to an array
134 +* should throw on object
135 +* should throw on regex
136
137 src/isomorphic/children/__tests__/onlyChild-test.js
138 * should fail when passed two children
@@ -1335,7 +1346,8 @@ src/renderers/shared/shared/__tests__/ReactMultiChild-test.js
1346 * should replace children with different keys
1347
1348 src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js
1338 -* should reset internal state if removed then readded
1349 +* should reset internal state if removed then readded in an array
1350 +* should reset internal state if removed then readded in an iterable
1351 * should create unique identity
1352 * should preserve order if children order has not changed
1353 * should transition from zero to one children correctly
@@ -1536,22 +1548,6 @@ src/shared/utils/__tests__/PooledClass-test.js
1548 src/shared/utils/__tests__/reactProdInvariant-test.js
1549 * should throw with the correct number of `%s`s in the URL
1550
1539 -src/shared/utils/__tests__/traverseAllChildren-test.js
1540 -* should support identity for simple
1541 -* should treat single arrayless child as being in array
1542 -* should treat single child in array as expected
1543 -* should be called for each child
1544 -* should traverse children of different kinds
1545 -* should be called for each child in nested structure
1546 -* should retain key across two mappings
1547 -* should be called for each child in an iterable without keys
1548 -* should be called for each child in an iterable with keys
1549 -* should use keys from entry iterables
1550 -* should not enumerate enumerable numbers (#4776)
1551 -* should allow extension of native prototypes
1552 -* should throw on object
1553 -* should throw on regex
1554 -
1551 src/test/__tests__/ReactTestUtils-test.js
1552 * should have shallow rendering
1553 * should shallow render a functional component
src/isomorphic/children/__tests__/ReactChildren-test.js
+498 -40
@@ -12,18 +12,19 @@
12 'use strict';
13
14 describe('ReactChildren', () => {
15 - var ReactChildren;
15 var React;
16 var ReactFragment;
17
18 beforeEach(() => {
20 - ReactChildren = require('ReactChildren');
19 + jest.resetModuleRegistry();
20 React = require('React');
21 ReactFragment = require('ReactFragment');
22 });
23
24 it('should support identity for simple', () => {
25 + var context = {};
26 var callback = jasmine.createSpy().and.callFake(function(kid, index) {
27 + expect(this).toBe(context);
28 return kid;
29 });
30
@@ -33,43 +34,478 @@ describe('ReactChildren', () => {
34 // using structures that arrive from transforms.
35
36 var instance = <div>{simpleKid}</div>;
36 - ReactChildren.forEach(instance.props.children, callback);
37 + React.Children.forEach(instance.props.children, callback, context);
38 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
39 callback.calls.reset();
39 - var mappedChildren = ReactChildren.map(instance.props.children, callback);
40 + var mappedChildren = React.Children.map(instance.props.children, callback, context);
41 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
42 expect(mappedChildren[0]).toEqual(<span key=".$simple" />);
43 });
44
45 it('should treat single arrayless child as being in array', () => {
46 + var context = {};
47 var callback = jasmine.createSpy().and.callFake(function(kid, index) {
48 + expect(this).toBe(context);
49 return kid;
50 });
51
52 var simpleKid = <span />;
53 var instance = <div>{simpleKid}</div>;
51 - ReactChildren.forEach(instance.props.children, callback);
54 + React.Children.forEach(instance.props.children, callback, context);
55 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
56 callback.calls.reset();
54 - var mappedChildren = ReactChildren.map(instance.props.children, callback);
57 + var mappedChildren = React.Children.map(instance.props.children, callback, context);
58 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
59 expect(mappedChildren[0]).toEqual(<span key=".0" />);
60 });
61
62 it('should treat single child in array as expected', () => {
63 + var context = {};
64 var callback = jasmine.createSpy().and.callFake(function(kid, index) {
65 + expect(this).toBe(context);
66 return kid;
67 });
68
69 var simpleKid = <span key="simple" />;
70 var instance = <div>{[simpleKid]}</div>;
66 - ReactChildren.forEach(instance.props.children, callback);
71 + React.Children.forEach(instance.props.children, callback, context);
72 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
73 callback.calls.reset();
69 - var mappedChildren = ReactChildren.map(instance.props.children, callback);
74 + var mappedChildren = React.Children.map(instance.props.children, callback, context);
75 expect(callback).toHaveBeenCalledWith(simpleKid, 0);
76 expect(mappedChildren[0]).toEqual(<span key=".$simple" />);
77 + });
78 +
79 + it('should be called for each child', () => {
80 + var zero = <div key="keyZero" />;
81 + var one = null;
82 + var two = <div key="keyTwo" />;
83 + var three = null;
84 + var four = <div key="keyFour" />;
85 + var context = {};
86 +
87 + var callback =
88 + jasmine.createSpy().and.callFake(function(kid) {
89 + expect(this).toBe(context);
90 + return kid;
91 + });
92 +
93 + var instance = (
94 + <div>
95 + {zero}
96 + {one}
97 + {two}
98 + {three}
99 + {four}
100 + </div>
101 + );
102 +
103 + function assertCalls() {
104 + expect(callback).toHaveBeenCalledWith(zero, 0);
105 + expect(callback).toHaveBeenCalledWith(one, 1);
106 + expect(callback).toHaveBeenCalledWith(two, 2);
107 + expect(callback).toHaveBeenCalledWith(three, 3);
108 + expect(callback).toHaveBeenCalledWith(four, 4);
109 + callback.calls.reset();
110 + }
111 +
112 + React.Children.forEach(instance.props.children, callback, context);
113 + assertCalls();
114 +
115 + var mappedChildren = React.Children.map(instance.props.children, callback, context);
116 + assertCalls();
117 + expect(mappedChildren).toEqual([
118 + <div key=".$keyZero" />,
119 + <div key=".$keyTwo" />,
120 + <div key=".$keyFour" />,
121 + ]);
122 + });
123 +
124 + it('should traverse children of different kinds', () => {
125 + var div = <div key="divNode" />;
126 + var span = <span key="spanNode" />;
127 + var a = <a key="aNode" />;
128 +
129 + var context = {};
130 + var callback =
131 + jasmine.createSpy().and.callFake(function(kid) {
132 + expect(this).toBe(context);
133 + return kid;
134 + });
135 +
136 + var instance = (
137 + <div>
138 + {div}
139 + {[ReactFragment.create({span})]}
140 + {ReactFragment.create({a: a})}
141 + {'string'}
142 + {1234}
143 + {true}
144 + {false}
145 + {null}
146 + {undefined}
147 + </div>
148 + );
149 +
150 + function assertCalls() {
151 + expect(callback.calls.count()).toBe(9);
152 + expect(callback).toHaveBeenCalledWith(div, 0);
153 + expect(callback).toHaveBeenCalledWith(<span key="span/.$spanNode" />, 1);
154 + expect(callback).toHaveBeenCalledWith(<a key="a/.$aNode" />, 2);
155 + expect(callback).toHaveBeenCalledWith('string', 3);
156 + expect(callback).toHaveBeenCalledWith(1234, 4);
157 + expect(callback).toHaveBeenCalledWith(null, 5);
158 + expect(callback).toHaveBeenCalledWith(null, 6);
159 + expect(callback).toHaveBeenCalledWith(null, 7);
160 + expect(callback).toHaveBeenCalledWith(null, 8);
161 + callback.calls.reset();
162 + }
163 +
164 + React.Children.forEach(instance.props.children, callback, context);
165 + assertCalls();
166 +
167 + var mappedChildren = React.Children.map(instance.props.children, callback, context);
168 + assertCalls();
169 + expect(mappedChildren).toEqual([
170 + <div key=".$divNode" />,
171 + <span key=".1:0:$span/.$spanNode" />,
172 + <a key=".2:$a/.$aNode" />,
173 + 'string',
174 + 1234,
175 + ]);
176 + });
177 +
178 + it('should be called for each child in nested structure', () => {
179 + var zero = <div key="keyZero" />;
180 + var one = null;
181 + var two = <div key="keyTwo" />;
182 + var three = null;
183 + var four = <div key="keyFour" />;
184 + var five = <div key="keyFiveInner" />;
185 + // five is placed into a JS object with a key that is joined to the
186 + // component key attribute.
187 + // Precedence is as follows:
188 + // 1. If grouped in an Object, the object key combined with `key` prop
189 + // 2. If grouped in an Array, the `key` prop, falling back to array index
190 +
191 + var context = {};
192 + var callback =
193 + jasmine.createSpy().and.callFake(function(kid) {
194 + return kid;
195 + });
196
197 + var instance = (
198 + <div>{[
199 + ReactFragment.create({
200 + firstHalfKey: [zero, one, two],
201 + secondHalfKey: [three, four],
202 + keyFive: five,
203 + }),
204 + ]}</div>
205 + );
206 +
207 + function assertCalls() {
208 + expect(callback.calls.count()).toBe(4);
209 + expect(callback).toHaveBeenCalledWith(<div key="firstHalfKey/.$keyZero" />, 0);
210 + expect(callback).toHaveBeenCalledWith(<div key="firstHalfKey/.$keyTwo" />, 1);
211 + expect(callback).toHaveBeenCalledWith(<div key="secondHalfKey/.$keyFour" />, 2);
212 + expect(callback).toHaveBeenCalledWith(<div key="keyFive/.$keyFiveInner" />, 3);
213 + callback.calls.reset();
214 + }
215 +
216 + React.Children.forEach(instance.props.children, callback, context);
217 + assertCalls();
218 +
219 + var mappedChildren = React.Children.map(instance.props.children, callback, context);
220 + assertCalls();
221 + expect(mappedChildren).toEqual([
222 + <div key=".0:$firstHalfKey/.$keyZero" />,
223 + <div key=".0:$firstHalfKey/.$keyTwo" />,
224 + <div key=".0:$secondHalfKey/.$keyFour" />,
225 + <div key=".0:$keyFive/.$keyFiveInner" />,
226 + ]);
227 + });
228 +
229 + it('should retain key across two mappings', () => {
230 + var zeroForceKey = <div key="keyZero" />;
231 + var oneForceKey = <div key="keyOne" />;
232 + var context = {};
233 + var callback =
234 + jasmine.createSpy().and.callFake(function(kid) {
235 + expect(this).toBe(context);
236 + return kid;
237 + });
238 +
239 + var forcedKeys = (
240 + <div>
241 + {zeroForceKey}
242 + {oneForceKey}
243 + </div>
244 + );
245 +
246 + function assertCalls() {
247 + expect(callback).toHaveBeenCalledWith(zeroForceKey, 0);
248 + expect(callback).toHaveBeenCalledWith(oneForceKey, 1);
249 + callback.calls.reset();
250 + }
251 +
252 + React.Children.forEach(forcedKeys.props.children, callback, context);
253 + assertCalls();
254 +
255 + var mappedChildren = React.Children.map(forcedKeys.props.children, callback, context);
256 + assertCalls();
257 + expect(mappedChildren).toEqual([
258 + <div key=".$keyZero" />,
259 + <div key=".$keyOne" />,
260 + ]);
261 + });
262 +
263 + it('should be called for each child in an iterable without keys', () => {
264 + spyOn(console, 'error');
265 + var threeDivIterable = {
266 + '@@iterator': function() {
267 + var i = 0;
268 + return {
269 + next: function() {
270 + if (i++ < 3) {
271 + return {value: <div />, done: false};
272 + } else {
273 + return {value: undefined, done: true};
274 + }
275 + },
276 + };
277 + },
278 + };
279 +
280 + var context = {};
281 + var callback =
282 + jasmine.createSpy().and.callFake(function(kid) {
283 + expect(this).toBe(context);
284 + return kid;
285 + });
286 +
287 + var instance = (
288 + <div>
289 + {threeDivIterable}
290 + </div>
291 + );
292 +
293 + function assertCalls() {
294 + expect(callback.calls.count()).toBe(3);
295 + expect(callback).toHaveBeenCalledWith(<div />, 0);
296 + expect(callback).toHaveBeenCalledWith(<div />, 1);
297 + expect(callback).toHaveBeenCalledWith(<div />, 2);
298 + callback.calls.reset();
299 + }
300 +
301 + React.Children.forEach(instance.props.children, callback, context);
302 + assertCalls();
303 + expectDev(console.error.calls.count()).toBe(1);
304 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
305 + 'Warning: Each child in an array or iterator should have a unique "key" prop.'
306 + );
307 + console.error.calls.reset();
308 +
309 + var mappedChildren = React.Children.map(instance.props.children, callback, context);
310 + assertCalls();
311 + expectDev(console.error.calls.count()).toBe(0);
312 + expect(mappedChildren).toEqual([
313 + <div key=".0" />,
314 + <div key=".1" />,
315 + <div key=".2" />,
316 + ]);
317 + });
318 +
319 + it('should be called for each child in an iterable with keys', () => {
320 + var threeDivIterable = {
321 + '@@iterator': function() {
322 + var i = 0;
323 + return {
324 + next: function() {
325 + if (i++ < 3) {
326 + return {value: <div key={'#' + i} />, done: false};
327 + } else {
328 + return {value: undefined, done: true};
329 + }
330 + },
331 + };
332 + },
333 + };
334 +
335 + var context = {};
336 + var callback =
337 + jasmine.createSpy().and.callFake(function(kid) {
338 + expect(this).toBe(context);
339 + return kid;
340 + });
341 +
342 + var instance = (
343 + <div>
344 + {threeDivIterable}
345 + </div>
346 + );
347 +
348 + function assertCalls() {
349 + expect(callback.calls.count()).toBe(3);
350 + expect(callback).toHaveBeenCalledWith(<div key="#1" />, 0);
351 + expect(callback).toHaveBeenCalledWith(<div key="#2" />, 1);
352 + expect(callback).toHaveBeenCalledWith(<div key="#3" />, 2);
353 + callback.calls.reset();
354 + }
355 +
356 + React.Children.forEach(instance.props.children, callback, context);
357 + assertCalls();
358 +
359 + var mappedChildren = React.Children.map(instance.props.children, callback, context);
360 + assertCalls();
361 + expect(mappedChildren).toEqual([
362 + <div key=".$#1" />,
363 + <div key=".$#2" />,
364 + <div key=".$#3" />,
365 + ]);
366 + });
367 +
368 + it('should use keys from entry iterables', () => {
369 + spyOn(console, 'error');
370 +
371 + var threeDivEntryIterable = {
372 + '@@iterator': function() {
373 + var i = 0;
374 + return {
375 + next: function() {
376 + if (i++ < 3) {
377 + return {value: ['#' + i, <div />], done: false};
378 + } else {
379 + return {value: undefined, done: true};
380 + }
381 + },
382 + };
383 + },
384 + };
385 + threeDivEntryIterable.entries = threeDivEntryIterable['@@iterator'];
386 +
387 + var context = {};
388 + var callback =
389 + jasmine.createSpy().and.callFake(function(kid) {
390 + expect(this).toBe(context);
391 + return kid;
392 + });
393 +
394 + var instance = (
395 + <div>
396 + {threeDivEntryIterable}
397 + </div>
398 + );
399 +
400 + function assertCalls() {
401 + expect(callback.calls.count()).toBe(3);
402 + // TODO: why
403 + expect(callback).toHaveBeenCalledWith(<div />, 0);
404 + expect(callback).toHaveBeenCalledWith(<div />, 1);
405 + expect(callback).toHaveBeenCalledWith(<div />, 2);
406 +
407 + callback.calls.reset();
408 + }
409 +
410 + React.Children.forEach(instance.props.children, callback, context);
411 + assertCalls();
412 + expectDev(console.error.calls.count()).toBe(1);
413 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
414 + 'Warning: Using Maps as children is not yet fully supported. It is an ' +
415 + 'experimental feature that might be removed. Convert it to a sequence ' +
416 + '/ iterable of keyed ReactElements instead.'
417 + );
418 + console.error.calls.reset();
419 +
420 + var mappedChildren = React.Children.map(instance.props.children, callback, context);
421 + assertCalls();
422 + expect(mappedChildren).toEqual([
423 + <div key=".$#1:0" />,
424 + <div key=".$#2:0" />,
425 + <div key=".$#3:0" />,
426 + ]);
427 + expectDev(console.error.calls.count()).toBe(0);
428 + });
429 +
430 + it('should not enumerate enumerable numbers (#4776)', () => {
431 + /*eslint-disable no-extend-native */
432 + Number.prototype['@@iterator'] = function() {
433 + throw new Error('number iterator called');
434 + };
435 + /*eslint-enable no-extend-native */
436 +
437 + try {
438 + var instance = (
439 + <div>
440 + {5}
441 + {12}
442 + {13}
443 + </div>
444 + );
445 +
446 + var context = {};
447 + var callback = jasmine.createSpy().and.callFake(function(kid) {
448 + expect(this).toBe(context);
449 + return kid;
450 + });
451 +
452 + var assertCalls = function() {
453 + expect(callback.calls.count()).toBe(3);
454 + expect(callback).toHaveBeenCalledWith(5, 0);
455 + expect(callback).toHaveBeenCalledWith(12, 1);
456 + expect(callback).toHaveBeenCalledWith(13, 2);
457 + callback.calls.reset();
458 + };
459 +
460 + React.Children.forEach(instance.props.children, callback, context);
461 + assertCalls();
462 +
463 + var mappedChildren = React.Children.map(instance.props.children, callback, context);
464 + assertCalls();
465 + expect(mappedChildren).toEqual([5, 12, 13]);
466 + } finally {
467 + delete Number.prototype['@@iterator'];
468 + }
469 + });
470 +
471 + it('should allow extension of native prototypes', () => {
472 + /*eslint-disable no-extend-native */
473 + String.prototype.key = 'react';
474 + Number.prototype.key = 'rocks';
475 + /*eslint-enable no-extend-native */
476 +
477 + var instance = (
478 + <div>
479 + {'a'}
480 + {13}
481 + </div>
482 + );
483 +
484 + var context = {};
485 + var callback = jasmine.createSpy().and.callFake(function(kid) {
486 + expect(this).toBe(context);
487 + return kid;
488 + });
489 +
490 + function assertCalls() {
491 + expect(callback.calls.count()).toBe(2, 0);
492 + expect(callback).toHaveBeenCalledWith('a', 0);
493 + expect(callback).toHaveBeenCalledWith(13, 1);
494 + callback.calls.reset();
495 + }
496 +
497 + React.Children.forEach(instance.props.children, callback, context);
498 + assertCalls();
499 +
500 + var mappedChildren = React.Children.map(instance.props.children, callback, context);
501 + assertCalls();
502 + expect(mappedChildren).toEqual([
503 + 'a',
504 + 13,
505 + ]);
506 +
507 + delete String.prototype.key;
508 + delete Number.prototype.key;
509 });
510
511 it('should pass key to returned component', () => {
@@ -80,9 +516,9 @@ describe('ReactChildren', () => {
516 var simpleKid = <span key="simple" />;
517
518 var instance = <div>{simpleKid}</div>;
83 - var mappedChildren = ReactChildren.map(instance.props.children, mapFn);
519 + var mappedChildren = React.Children.map(instance.props.children, mapFn);
520
85 - expect(ReactChildren.count(mappedChildren)).toBe(1);
521 + expect(React.Children.count(mappedChildren)).toBe(1);
522 expect(mappedChildren[0]).not.toBe(simpleKid);
523 expect(mappedChildren[0].props.children).toBe(simpleKid);
524 expect(mappedChildren[0].key).toBe('.$simple');
@@ -100,13 +536,13 @@ describe('ReactChildren', () => {
536
537 var simpleKid = <span key="simple" />;
538 var instance = <div>{simpleKid}</div>;
103 - ReactChildren.forEach(instance.props.children, callback, scopeTester);
539 + React.Children.forEach(instance.props.children, callback, scopeTester);
540 expect(lastContext).toBe(scopeTester);
541
542 var mappedChildren =
107 - ReactChildren.map(instance.props.children, callback, scopeTester);
543 + React.Children.map(instance.props.children, callback, scopeTester);
544
109 - expect(ReactChildren.count(mappedChildren)).toBe(1);
545 + expect(React.Children.count(mappedChildren)).toBe(1);
546 expect(mappedChildren[0]).toBe(scopeTester);
547 });
548
@@ -138,7 +574,7 @@ describe('ReactChildren', () => {
574 </div>
575 );
576
141 - ReactChildren.forEach(instance.props.children, callback);
577 + React.Children.forEach(instance.props.children, callback);
578 expect(callback).toHaveBeenCalledWith(zero, 0);
579 expect(callback).toHaveBeenCalledWith(one, 1);
580 expect(callback).toHaveBeenCalledWith(two, 2);
@@ -147,9 +583,9 @@ describe('ReactChildren', () => {
583 callback.calls.reset();
584
585 var mappedChildren =
150 - ReactChildren.map(instance.props.children, callback);
586 + React.Children.map(instance.props.children, callback);
587 expect(callback.calls.count()).toBe(5);
152 - expect(ReactChildren.count(mappedChildren)).toBe(4);
588 + expect(React.Children.count(mappedChildren)).toBe(4);
589 // Keys default to indices.
590 expect([
591 mappedChildren[0].key,
@@ -215,7 +651,7 @@ describe('ReactChildren', () => {
651 'keyFive/.$keyFiveInner',
652 ]);
653
218 - ReactChildren.forEach(instance.props.children, callback);
654 + React.Children.forEach(instance.props.children, callback);
655 expect(callback.calls.count()).toBe(4);
656 expect(callback).toHaveBeenCalledWith(frag[0], 0);
657 expect(callback).toHaveBeenCalledWith(frag[1], 1);
@@ -223,14 +659,14 @@ describe('ReactChildren', () => {
659 expect(callback).toHaveBeenCalledWith(frag[3], 3);
660 callback.calls.reset();
661
226 - var mappedChildren = ReactChildren.map(instance.props.children, callback);
662 + var mappedChildren = React.Children.map(instance.props.children, callback);
663 expect(callback.calls.count()).toBe(4);
664 expect(callback).toHaveBeenCalledWith(frag[0], 0);
665 expect(callback).toHaveBeenCalledWith(frag[1], 1);
666 expect(callback).toHaveBeenCalledWith(frag[2], 2);
667 expect(callback).toHaveBeenCalledWith(frag[3], 3);
668
233 - expect(ReactChildren.count(mappedChildren)).toBe(4);
669 + expect(React.Children.count(mappedChildren)).toBe(4);
670 // Keys default to indices.
671 expect([
672 mappedChildren[0].key,
@@ -272,7 +708,7 @@ describe('ReactChildren', () => {
708
709 var expectedForcedKeys = ['giraffe/.$keyZero', '.$keyOne'];
710 var mappedChildrenForcedKeys =
275 - ReactChildren.map(forcedKeys.props.children, mapFn);
711 + React.Children.map(forcedKeys.props.children, mapFn);
712 var mappedForcedKeys = mappedChildrenForcedKeys.map((c) => c.key);
713 expect(mappedForcedKeys).toEqual(expectedForcedKeys);
714
@@ -281,7 +717,7 @@ describe('ReactChildren', () => {
717 '.$.$keyOne',
718 ];
719 var remappedChildrenForcedKeys =
284 - ReactChildren.map(mappedChildrenForcedKeys, mapFn);
720 + React.Children.map(mappedChildrenForcedKeys, mapFn);
721 expect(
722 remappedChildrenForcedKeys.map((c) => c.key)
723 ).toEqual(expectedRemappedForcedKeys);
@@ -304,7 +740,7 @@ describe('ReactChildren', () => {
740 );
741
742 expect(function() {
307 - ReactChildren.map(instance.props.children, mapFn);
743 + React.Children.map(instance.props.children, mapFn);
744 }).not.toThrow();
745 });
746
@@ -315,12 +751,12 @@ describe('ReactChildren', () => {
751 </div>
752 );
753
318 - var mapped = ReactChildren.map(
754 + var mapped = React.Children.map(
755 instance.props.children,
756 element => element,
757 );
758
323 - var mappedWithClone = ReactChildren.map(
759 + var mappedWithClone = React.Children.map(
760 instance.props.children,
761 element => React.cloneElement(element),
762 );
@@ -335,12 +771,12 @@ describe('ReactChildren', () => {
771 </div>
772 );
773
338 - var mapped = ReactChildren.map(
774 + var mapped = React.Children.map(
775 instance.props.children,
776 element => element,
777 );
778
343 - var mappedWithClone = ReactChildren.map(
779 + var mappedWithClone = React.Children.map(
780 instance.props.children,
781 element => React.cloneElement(element, {key: 'unique'}),
782 );
@@ -349,19 +785,19 @@ describe('ReactChildren', () => {
785 });
786
787 it('should return 0 for null children', () => {
352 - var numberOfChildren = ReactChildren.count(null);
788 + var numberOfChildren = React.Children.count(null);
789 expect(numberOfChildren).toBe(0);
790 });
791
792 it('should return 0 for undefined children', () => {
357 - var numberOfChildren = ReactChildren.count(undefined);
793 + var numberOfChildren = React.Children.count(undefined);
794 expect(numberOfChildren).toBe(0);
795 });
796
797 it('should return 1 for single child', () => {
798 var simpleKid = <span key="simple" />;
799 var instance = <div>{simpleKid}</div>;
364 - var numberOfChildren = ReactChildren.count(instance.props.children);
800 + var numberOfChildren = React.Children.count(instance.props.children);
801 expect(numberOfChildren).toBe(1);
802 });
803
@@ -381,7 +817,7 @@ describe('ReactChildren', () => {
817 {four}
818 </div>
819 );
384 - var numberOfChildren = ReactChildren.count(instance.props.children);
820 + var numberOfChildren = React.Children.count(instance.props.children);
821 expect(numberOfChildren).toBe(5);
822 });
823
@@ -408,23 +844,23 @@ describe('ReactChildren', () => {
844 null,
845 ]}</div>
846 );
411 - var numberOfChildren = ReactChildren.count(instance.props.children);
847 + var numberOfChildren = React.Children.count(instance.props.children);
848 expect(numberOfChildren).toBe(5);
849 });
850
851 it('should flatten children to an array', () => {
416 - expect(ReactChildren.toArray(undefined)).toEqual([]);
417 - expect(ReactChildren.toArray(null)).toEqual([]);
852 + expect(React.Children.toArray(undefined)).toEqual([]);
853 + expect(React.Children.toArray(null)).toEqual([]);
854
419 - expect(ReactChildren.toArray(<div />).length).toBe(1);
420 - expect(ReactChildren.toArray([<div />]).length).toBe(1);
855 + expect(React.Children.toArray(<div />).length).toBe(1);
856 + expect(React.Children.toArray([<div />]).length).toBe(1);
857 expect(
422 - ReactChildren.toArray(<div />)[0].key
858 + React.Children.toArray(<div />)[0].key
859 ).toBe(
424 - ReactChildren.toArray([<div />])[0].key
860 + React.Children.toArray([<div />])[0].key
861 );
862
427 - var flattened = ReactChildren.toArray([
863 + var flattened = React.Children.toArray([
864 [<div key="apple" />, <div key="banana" />, <div key="camel" />],
865 [<div key="banana" />, <div key="camel" />, <div key="deli" />],
866 ]);
@@ -433,7 +869,7 @@ describe('ReactChildren', () => {
869 expect(flattened[3].key).toContain('banana');
870 expect(flattened[1].key).not.toBe(flattened[3].key);
871
436 - var reversed = ReactChildren.toArray([
872 + var reversed = React.Children.toArray([
873 [<div key="camel" />, <div key="banana" />, <div key="apple" />],
874 [<div key="deli" />, <div key="camel" />, <div key="banana" />],
875 ]);
@@ -445,9 +881,31 @@ describe('ReactChildren', () => {
881 expect(flattened[5].key).toBe(reversed[3].key);
882
883 // null/undefined/bool are all omitted
448 - expect(ReactChildren.toArray([1, 'two', null, undefined, true])).toEqual(
884 + expect(React.Children.toArray([1, 'two', null, undefined, true])).toEqual(
885 [1, 'two']
886 );
887 });
888
889 + it('should throw on object', () => {
890 + expect(function() {
891 + React.Children.forEach({a: 1, b: 2}, function() {}, null);
892 + }).toThrowError(
893 + 'Objects are not valid as a React child (found: object with keys ' +
894 + '{a, b}). If you meant to render a collection of children, use an ' +
895 + 'array instead or wrap the object using createFragment(object) from ' +
896 + 'the React add-ons.'
897 + );
898 + });
899 +
900 + it('should throw on regex', () => {
901 + // Really, we care about dates (#4840) but those have nondeterministic
902 + // serialization (timezones) so let's test a regex instead:
903 + expect(function() {
904 + React.Children.forEach(/abc/, function() {}, null);
905 + }).toThrowError(
906 + 'Objects are not valid as a React child (found: /abc/). If you meant ' +
907 + 'to render a collection of children, use an array instead or wrap the ' +
908 + 'object using createFragment(object) from the React add-ons.'
909 + );
910 + });
911 });
src/renderers/shared/fiber/ReactChildFiber.js
+140 -5
@@ -545,6 +545,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
545 // In this first iteration, we'll just live with hitting the bad case
546 // (adding everything to a Map) in for every insert/move.
547
548 + // If you change this code, also update reconcileChildrenIterator() which
549 + // uses the same algorithm.
550 +
551 let resultingFirstChild : ?Fiber = null;
552 let previousNewFiber : ?Fiber = null;
553
@@ -676,10 +679,138 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
679 returnFiber : Fiber,
680 currentFirstChild : ?Fiber,
681 newChildren : Iterator<*>,
679 - priority : PriorityLevel) : null {
680 - // TODO: Copy everything from reconcileChildrenArray but use the iterator
681 - // instead.
682 - return null;
682 + priority : PriorityLevel) : ?Fiber {
683 +
684 + // This is the same implementation as reconcileChildrenArray(),
685 + // but using the iterator instead.
686 +
687 + let resultingFirstChild : ?Fiber = null;
688 + let previousNewFiber : ?Fiber = null;
689 +
690 + let oldFiber = currentFirstChild;
691 + let lastPlacedIndex = 0;
692 + let newIdx = 0;
693 + let nextOldFiber = null;
694 +
695 + let step = newChildren.next();
696 + for (; oldFiber && !step.done; newIdx++, step = newChildren.next()) {
697 + if (oldFiber) {
698 + if (oldFiber.index > newIdx) {
699 + nextOldFiber = oldFiber;
700 + oldFiber = null;
701 + } else {
702 + nextOldFiber = oldFiber.sibling;
703 + }
704 + }
705 + const newFiber = updateSlot(
706 + returnFiber,
707 + oldFiber,
708 + step.value,
709 + priority
710 + );
711 + if (!newFiber) {
712 + // TODO: This breaks on empty slots like null children. That's
713 + // unfortunate because it triggers the slow path all the time. We need
714 + // a better way to communicate whether this was a miss or null,
715 + // boolean, undefined, etc.
716 + if (!oldFiber) {
717 + oldFiber = nextOldFiber;
718 + }
719 + break;
720 + }
721 + if (shouldTrackSideEffects) {
722 + if (oldFiber && !newFiber.alternate) {
723 + // We matched the slot, but we didn't reuse the existing fiber, so we
724 + // need to delete the existing child.
725 + deleteChild(returnFiber, oldFiber);
726 + }
727 + }
728 + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
729 + if (!previousNewFiber) {
730 + // TODO: Move out of the loop. This only happens for the first run.
731 + resultingFirstChild = newFiber;
732 + } else {
733 + // TODO: Defer siblings if we're not at the right index for this slot.
734 + // I.e. if we had null values before, then we want to defer this
735 + // for each null value. However, we also don't want to call updateSlot
736 + // with the previous one.
737 + previousNewFiber.sibling = newFiber;
738 + }
739 + previousNewFiber = newFiber;
740 + oldFiber = nextOldFiber;
741 + }
742 +
743 + if (step.done) {
744 + // We've reached the end of the new children. We can delete the rest.
745 + deleteRemainingChildren(returnFiber, oldFiber);
746 + return resultingFirstChild;
747 + }
748 +
749 + if (!oldFiber) {
750 + // If we don't have any more existing children we can choose a fast path
751 + // since the rest will all be insertions.
752 + for (; !step.done; newIdx++, step = newChildren.next()) {
753 + const newFiber = createChild(
754 + returnFiber,
755 + step.value,
756 + priority
757 + );
758 + if (!newFiber) {
759 + continue;
760 + }
761 + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
762 + if (!previousNewFiber) {
763 + // TODO: Move out of the loop. This only happens for the first run.
764 + resultingFirstChild = newFiber;
765 + } else {
766 + previousNewFiber.sibling = newFiber;
767 + }
768 + previousNewFiber = newFiber;
769 + }
770 + return resultingFirstChild;
771 + }
772 +
773 + // Add all children to a key map for quick lookups.
774 + const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
775 +
776 + // Keep scanning and use the map to restore deleted items as moves.
777 + for (; !step.done; newIdx++, step = newChildren.next()) {
778 + const newFiber = updateFromMap(
779 + existingChildren,
780 + returnFiber,
781 + newIdx,
782 + step.value,
783 + priority
784 + );
785 + if (newFiber) {
786 + if (shouldTrackSideEffects) {
787 + if (newFiber.alternate) {
788 + // The new fiber is a work in progress, but if there exists a
789 + // current, that means that we reused the fiber. We need to delete
790 + // it from the child list so that we don't add it to the deletion
791 + // list.
792 + existingChildren.delete(
793 + newFiber.key === null ? newIdx : newFiber.key
794 + );
795 + }
796 + }
797 + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
798 + if (!previousNewFiber) {
799 + resultingFirstChild = newFiber;
800 + } else {
801 + previousNewFiber.sibling = newFiber;
802 + }
803 + previousNewFiber = newFiber;
804 + }
805 + }
806 +
807 + if (shouldTrackSideEffects) {
808 + // Any existing children that weren't consumed above were deleted. We need
809 + // to add them to the deletion list.
810 + existingChildren.forEach(child => deleteChild(returnFiber, child));
811 + }
812 +
813 + return resultingFirstChild;
814 }
815
816 function reconcileSingleTextNode(
@@ -919,10 +1050,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
1050
1051 const iteratorFn = getIteratorFn(newChild);
1052 if (iteratorFn) {
1053 + const iterator = iteratorFn.call(newChild);
1054 + if (iterator == null) {
1055 + throw new Error('An iterable object provided no iterator.');
1056 + }
1057 return reconcileChildrenIterator(
1058 returnFiber,
1059 currentFirstChild,
925 - newChild,
1060 + iterator,
1061 priority
1062 );
1063 }
src/renderers/shared/shared/__tests__/ReactMultiChild-test.js
+22
@@ -197,5 +197,27 @@ describe('ReactMultiChild', () => {
197 ' in Parent (at **)'
198 );
199 });
200 +
201 + it('should warn for using maps as children with owner info', () => {
202 + spyOn(console, 'error');
203 +
204 + class Parent extends React.Component {
205 + render() {
206 + return (
207 + <div>{new Map([['foo', 0], ['bar', 1]])}</div>
208 + );
209 + }
210 + }
211 +
212 + var container = document.createElement('div');
213 + ReactDOM.render(<Parent />, container);
214 +
215 + expectDev(console.error.calls.count()).toBe(1);
216 + expectDev(console.error.calls.argsFor(0)[0]).toBe(
217 + 'Warning: Using Maps as children is not yet fully supported. It is an ' +
218 + 'experimental feature that might be removed. Convert it to a sequence ' +
219 + '/ iterable of keyed ReactElements instead. Check the render method of `Parent`.'
220 + );
221 + });
222 });
223 });
src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js
+69 -11
@@ -129,9 +129,10 @@ class FriendsStatusDisplay extends React.Component {
129 />
130 );
131 }
132 + var childrenToRender = this.props.prepareChildren(children);
133 return (
134 <div>
134 - {children}
135 + {childrenToRender}
136 </div>
137 );
138 }
@@ -224,13 +225,13 @@ function verifyDomOrderingAccurate(outerContainer, statusDisplays) {
225 expect(orderedDomKeys).toEqual(orderedLogicalKeys);
226 }
227
227 -/**
228 - * Todo: Check that internal state is preserved across transitions
229 - */
230 -function testPropsSequence(sequence) {
228 +function testPropsSequenceWithPreparedChildren(sequence, prepareChildren) {
229 var container = document.createElement('div');
230 var parentInstance = ReactDOM.render(
233 - <FriendsStatusDisplay {...sequence[0]} />,
231 + <FriendsStatusDisplay
232 + {...sequence[0]}
233 + prepareChildren={prepareChildren}
234 + />,
235 container
236 );
237 var statusDisplays = parentInstance.getStatusDisplays();
@@ -239,7 +240,10 @@ function testPropsSequence(sequence) {
240
241 for (var i = 1; i < sequence.length; i++) {
242 ReactDOM.render(
242 - <FriendsStatusDisplay {...sequence[i]} />,
243 + <FriendsStatusDisplay
244 + {...sequence[i]}
245 + prepareChildren={prepareChildren}
246 + />,
247 container
248 );
249 statusDisplays = parentInstance.getStatusDisplays();
@@ -251,12 +255,66 @@ function testPropsSequence(sequence) {
255 }
256 }
257
258 +function prepareChildrenArray(childrenArray) {
259 + return childrenArray;
260 +}
261 +
262 +function prepareChildrenIterable(childrenArray) {
263 + return {
264 + '@@iterator': function*() {
265 + for (const child of childrenArray) {
266 + yield child;
267 + }
268 + },
269 + };
270 +}
271 +
272 +function testPropsSequence(sequence) {
273 + testPropsSequenceWithPreparedChildren(sequence, prepareChildrenArray);
274 + testPropsSequenceWithPreparedChildren(sequence, prepareChildrenIterable);
275 +}
276 +
277 describe('ReactMultiChildReconcile', () => {
278 beforeEach(() => {
279 jest.resetModuleRegistry();
280 });
281
259 - it('should reset internal state if removed then readded', () => {
282 + it('should reset internal state if removed then readded in an array', () => {
283 + // Test basics.
284 + var props = {
285 + usernameToStatus: {
286 + jcw: 'jcwStatus',
287 + },
288 + };
289 +
290 + var container = document.createElement('div');
291 + var parentInstance = ReactDOM.render(
292 + <FriendsStatusDisplay {...props} prepareChildren={prepareChildrenArray} />,
293 + container
294 + );
295 + var statusDisplays = parentInstance.getStatusDisplays();
296 + var startingInternalState = statusDisplays.jcw.getInternalState();
297 +
298 + // Now remove the child.
299 + ReactDOM.render(
300 + <FriendsStatusDisplay prepareChildren={prepareChildrenArray} />,
301 + container
302 + );
303 + statusDisplays = parentInstance.getStatusDisplays();
304 + expect(statusDisplays.jcw).toBeFalsy();
305 +
306 + // Now reset the props that cause there to be a child
307 + ReactDOM.render(
308 + <FriendsStatusDisplay {...props} prepareChildren={prepareChildrenArray} />,
309 + container
310 + );
311 + statusDisplays = parentInstance.getStatusDisplays();
312 + expect(statusDisplays.jcw).toBeTruthy();
313 + expect(statusDisplays.jcw.getInternalState())
314 + .not.toBe(startingInternalState);
315 + });
316 +
317 + it('should reset internal state if removed then readded in an iterable', () => {
318 // Test basics.
319 var props = {
320 usernameToStatus: {
@@ -266,7 +324,7 @@ describe('ReactMultiChildReconcile', () => {
324
325 var container = document.createElement('div');
326 var parentInstance = ReactDOM.render(
269 - <FriendsStatusDisplay {...props} />,
327 + <FriendsStatusDisplay {...props} prepareChildren={prepareChildrenIterable} />,
328 container
329 );
330 var statusDisplays = parentInstance.getStatusDisplays();
@@ -274,7 +332,7 @@ describe('ReactMultiChildReconcile', () => {
332
333 // Now remove the child.
334 ReactDOM.render(
277 - <FriendsStatusDisplay />,
335 + <FriendsStatusDisplay prepareChildren={prepareChildrenIterable} />,
336 container
337 );
338 statusDisplays = parentInstance.getStatusDisplays();
@@ -282,7 +340,7 @@ describe('ReactMultiChildReconcile', () => {
340
341 // Now reset the props that cause there to be a child
342 ReactDOM.render(
285 - <FriendsStatusDisplay {...props} />,
343 + <FriendsStatusDisplay {...props} prepareChildren={prepareChildrenIterable} />,
344 container
345 );
346 statusDisplays = parentInstance.getStatusDisplays();
src/shared/utils/__tests__/traverseAllChildren-test.js deleted
-560
@@ -1,560 +0,0 @@
1 -/**
2 - * Copyright 2013-present, Facebook, Inc.
3 - * All rights reserved.
4 - *
5 - * This source code is licensed under the BSD-style license found in the
6 - * LICENSE file in the root directory of this source tree. An additional grant
7 - * of patent rights can be found in the PATENTS file in the same directory.
8 - *
9 - * @emails react-core
10 - */
11 -
12 -'use strict';
13 -
14 -describe('traverseAllChildren', () => {
15 - var traverseAllChildren;
16 - var React;
17 - var ReactFragment;
18 - var ReactTestUtils;
19 -
20 - beforeEach(() => {
21 - jest.resetModuleRegistry();
22 - traverseAllChildren = require('traverseAllChildren');
23 - React = require('React');
24 - ReactFragment = require('ReactFragment');
25 - ReactTestUtils = require('ReactTestUtils');
26 - });
27 -
28 - function frag(obj) {
29 - return ReactFragment.create(obj);
30 - }
31 -
32 - it('should support identity for simple', () => {
33 - var traverseContext = [];
34 - var traverseFn =
35 - jasmine.createSpy().and.callFake(function(context, kid, key, index) {
36 - context.push(true);
37 - });
38 -
39 - var simpleKid = <span key="simple" />;
40 -
41 - // Jasmine doesn't provide a way to test that the fn was invoked with scope.
42 - var instance = <div>{simpleKid}</div>;
43 - traverseAllChildren(instance.props.children, traverseFn, traverseContext);
44 - expect(traverseFn).toHaveBeenCalledWith(
45 - traverseContext,
46 - simpleKid,
47 - '.$simple'
48 - );
49 - expect(traverseContext.length).toEqual(1);
50 - });
51 -
52 - it('should treat single arrayless child as being in array', () => {
53 - var traverseContext = [];
54 - var traverseFn =
55 - jasmine.createSpy().and.callFake(function(context, kid, key, index) {
56 - context.push(true);
57 - });
58 -
59 - var simpleKid = <span />;
60 - var instance = <div>{simpleKid}</div>;
61 - traverseAllChildren(instance.props.children, traverseFn, traverseContext);
62 - expect(traverseFn).toHaveBeenCalledWith(
63 - traverseContext,
64 - simpleKid,
65 - '.0'
66 - );
67 - expect(traverseContext.length).toEqual(1);
68 - });
69 -
70 - it('should treat single child in array as expected', () => {
71 - spyOn(console, 'error');
72 - var traverseContext = [];
73 - var traverseFn =
74 - jasmine.createSpy().and.callFake(function(context, kid, key, index) {
75 - context.push(true);
76 - });
77 -
78 - var simpleKid = <span />;
79 - var instance = <div>{[simpleKid]}</div>;
80 - traverseAllChildren(instance.props.children, traverseFn, traverseContext);
81 - expect(traverseFn).toHaveBeenCalledWith(
82 - traverseContext,
83 - simpleKid,
84 - '.0'
85 - );
86 - expect(traverseContext.length).toEqual(1);
87 - expectDev(console.error.calls.count()).toBe(1);
88 - expectDev(console.error.calls.argsFor(0)[0]).toContain(
89 - 'Warning: Each child in an array or iterator should have a unique "key" prop.'
90 - );
91 - });
92 -
93 - it('should be called for each child', () => {
94 - var zero = <div key="keyZero" />;
95 - var one = null;
96 - var two = <div key="keyTwo" />;
97 - var three = null;
98 - var four = <div key="keyFour" />;
99 -
100 - var traverseContext = [];
101 - var traverseFn =
102 - jasmine.createSpy().and.callFake(function(context, kid, key, index) {
103 - context.push(true);
104 - });
105 -
106 - var instance = (
107 - <div>
108 - {zero}
109 - {one}
110 - {two}
111 - {three}
112 - {four}
113 - </div>
114 - );
115 -
116 - traverseAllChildren(instance.props.children, traverseFn, traverseContext);
117 - expect(traverseFn).toHaveBeenCalledWith(
118 - traverseContext,
119 - zero,
120 - '.$keyZero'
121 - );
122 - expect(traverseFn).toHaveBeenCalledWith(traverseContext, one, '.1');
123 - expect(traverseFn).toHaveBeenCalledWith(
124 - traverseContext,
125 - two,
126 - '.$keyTwo'
127 - );
128 - expect(traverseFn).toHaveBeenCalledWith(traverseContext, three, '.3');
129 - expect(traverseFn).toHaveBeenCalledWith(
130 - traverseContext,
131 - four,
132 - '.$keyFour'
133 - );
134 - });
135 -
136 - it('should traverse children of different kinds', () => {
137 - var div = <div key="divNode" />;
138 - var span = <span key="spanNode" />;
139 - var a = <a key="aNode" />;
140 -
141 - var traverseContext = [];
142 - var traverseFn =
143 - jasmine.createSpy().and.callFake(function(context, kid, key, index) {
144 - context.push(true);
145 - });
146 -
147 - var instance = (
148 - <div>
149 - {div}
150 - {[frag({span})]}
151 - {frag({a: a})}
152 - {'string'}
153 - {1234}
154 - {true}
155 - {false}
156 - {null}
157 - {undefined}
158 - </div>
159 - );
160 -
161 - traverseAllChildren(instance.props.children, traverseFn, traverseContext);
162 -
163 - expect(traverseFn.calls.count()).toBe(9);
164 - expect(traverseContext.length).toEqual(9);
165 -
166 - expect(traverseFn).toHaveBeenCalledWith(
167 - traverseContext, div, '.$divNode'
168 - );
169 - expect(traverseFn).toHaveBeenCalledWith(
170 - traverseContext, <span key="span/.$spanNode" />, '.1:0:$span/.$spanNode'
171 - );
172 - expect(traverseFn).toHaveBeenCalledWith(
173 - traverseContext, <a key="a/.$aNode" />, '.2:$a/.$aNode'
174 - );
175 - expect(traverseFn).toHaveBeenCalledWith(
176 - traverseContext, 'string', '.3'
177 - );
178 - expect(traverseFn).toHaveBeenCalledWith(
179 - traverseContext, 1234, '.4'
180 - );
181 - expect(traverseFn).toHaveBeenCalledWith(
182 - traverseContext, null, '.5'
183 - );
184 - expect(traverseFn).toHaveBeenCalledWith(
185 - traverseContext, null, '.6'
186 - );
187 - expect(traverseFn).toHaveBeenCalledWith(
188 - traverseContext, null, '.7'
189 - );
190 - expect(traverseFn).toHaveBeenCalledWith(
191 - traverseContext, null, '.8'
192 - );
193 - });
194 -
195 - it('should be called for each child in nested structure', () => {
196 - var zero = <div key="keyZero" />;
197 - var one = null;
198 - var two = <div key="keyTwo" />;
199 - var three = null;
200 - var four = <div key="keyFour" />;
201 - var five = <div key="keyFiveInner" />;
202 - // five is placed into a JS object with a key that is joined to the
203 - // component key attribute.
204 - // Precedence is as follows:
205 - // 1. If grouped in an Object, the object key combined with `key` prop
206 - // 2. If grouped in an Array, the `key` prop, falling back to array index
207 -
208 -
209 - var traverseContext = [];
210 - var traverseFn =
211 - jasmine.createSpy().and.callFake(function(context, kid, key, index) {
212 - context.push(true);
213 - });
214 -
215 - var instance = (
216 - <div>{[
217 - frag({
218 - firstHalfKey: [zero, one, two],
219 - secondHalfKey: [three, four],
220 - keyFive: five,
221 - }),
222 - ]}</div>
223 - );
224 -
225 - traverseAllChildren(instance.props.children, traverseFn, traverseContext);
226 - expect(traverseFn.calls.count()).toBe(4);
227 - expect(traverseContext.length).toEqual(4);
228 - expect(traverseFn).toHaveBeenCalledWith(
229 - traverseContext,
230 - <div key="firstHalfKey/.$keyZero" />,
231 - '.0:$firstHalfKey/.$keyZero'
232 - );
233 -
234 - expect(traverseFn).toHaveBeenCalledWith(
235 - traverseContext,
236 - <div key="firstHalfKey/.$keyTwo" />,
237 - '.0:$firstHalfKey/.$keyTwo'
238 - );
239 -
240 - expect(traverseFn).toHaveBeenCalledWith(
241 - traverseContext,
242 - <div key="secondHalfKey/.$keyFour" />,
243 - '.0:$secondHalfKey/.$keyFour'
244 - );
245 -
246 - expect(traverseFn).toHaveBeenCalledWith(
247 - traverseContext,
248 - <div key="keyFive/.$keyFiveInner" />,
249 - '.0:$keyFive/.$keyFiveInner'
250 - );
251 - });
252 -
253 - it('should retain key across two mappings', () => {
254 - var zeroForceKey = <div key="keyZero" />;
255 - var oneForceKey = <div key="keyOne" />;
256 - var traverseContext = [];
257 - var traverseFn =
258 - jasmine.createSpy().and.callFake(function(context, kid, key, index) {
259 - context.push(true);
260 - });
261 -
262 - var forcedKeys = (
263 - <div>
264 - {zeroForceKey}
265 - {oneForceKey}
266 - </div>
267 - );
268 -
269 - traverseAllChildren(forcedKeys.props.children, traverseFn, traverseContext);
270 - expect(traverseContext.length).toEqual(2);
271 - expect(traverseFn).toHaveBeenCalledWith(
272 - traverseContext,
273 - zeroForceKey,
274 - '.$keyZero'
275 - );
276 - expect(traverseFn).toHaveBeenCalledWith(
277 - traverseContext,
278 - oneForceKey,
279 - '.$keyOne'
280 - );
281 - });
282 -
283 - it('should be called for each child in an iterable without keys', () => {
284 - spyOn(console, 'error');
285 - var threeDivIterable = {
286 - '@@iterator': function() {
287 - var i = 0;
288 - return {
289 - next: function() {
290 - if (i++ < 3) {
291 - return {value: <div />, done: false};
292 - } else {
293 - return {value: undefined, done: true};
294 - }
295 - },
296 - };
297 - },
298 - };
299 -
300 - var traverseContext = [];
301 - var traverseFn =
302 - jasmine.createSpy().and.callFake(function(context, kid, key, index) {
303 - context.push(kid);
304 - });
305 -
306 - var instance = (
307 - <div>
308 - {threeDivIterable}
309 - </div>
310 - );
311 -
312 - traverseAllChildren(instance.props.children, traverseFn, traverseContext);
313 - expect(traverseFn.calls.count()).toBe(3);
314 -
315 - expect(traverseFn).toHaveBeenCalledWith(
316 - traverseContext,
317 - traverseContext[0],
318 - '.0'
319 - );
320 - expect(traverseFn).toHaveBeenCalledWith(
321 - traverseContext,
322 - traverseContext[1],
323 - '.1'
324 - );
325 - expect(traverseFn).toHaveBeenCalledWith(
326 - traverseContext,
327 - traverseContext[2],
328 - '.2'
329 - );
330 -
331 - expectDev(console.error.calls.count()).toBe(1);
332 - expectDev(console.error.calls.argsFor(0)[0]).toContain(
333 - 'Warning: Each child in an array or iterator should have a unique "key" prop.'
334 - );
335 - });
336 -
337 - it('should be called for each child in an iterable with keys', () => {
338 - var threeDivIterable = {
339 - '@@iterator': function() {
340 - var i = 0;
341 - return {
342 - next: function() {
343 - if (i++ < 3) {
344 - return {value: <div key={'#' + i} />, done: false};
345 - } else {
346 - return {value: undefined, done: true};
347 - }
348 - },
349 - };
350 - },
351 - };
352 -
353 - var traverseContext = [];
354 - var traverseFn =
355 - jasmine.createSpy().and.callFake(function(context, kid, key, index) {
356 - context.push(kid);
357 - });
358 -
359 - var instance = (
360 - <div>
361 - {threeDivIterable}
362 - </div>
363 - );
364 -
365 - traverseAllChildren(instance.props.children, traverseFn, traverseContext);
366 - expect(traverseFn.calls.count()).toBe(3);
367 -
368 - expect(traverseFn).toHaveBeenCalledWith(
369 - traverseContext,
370 - traverseContext[0],
371 - '.$#1'
372 - );
373 - expect(traverseFn).toHaveBeenCalledWith(
374 - traverseContext,
375 - traverseContext[1],
376 - '.$#2'
377 - );
378 - expect(traverseFn).toHaveBeenCalledWith(
379 - traverseContext,
380 - traverseContext[2],
381 - '.$#3'
382 - );
383 - });
384 -
385 - it('should use keys from entry iterables', () => {
386 - spyOn(console, 'error');
387 -
388 - var threeDivEntryIterable = {
389 - '@@iterator': function() {
390 - var i = 0;
391 - return {
392 - next: function() {
393 - if (i++ < 3) {
394 - return {value: ['#' + i, <div />], done: false};
395 - } else {
396 - return {value: undefined, done: true};
397 - }
398 - },
399 - };
400 - },
401 - };
402 - threeDivEntryIterable.entries = threeDivEntryIterable['@@iterator'];
403 -
404 - var traverseContext = [];
405 - var traverseFn =
406 - jasmine.createSpy().and.callFake(function(context, kid, key, index) {
407 - context.push(kid);
408 - });
409 -
410 - var instance = (
411 - <div>
412 - {threeDivEntryIterable}
413 - </div>
414 - );
415 -
416 - traverseAllChildren(instance.props.children, traverseFn, traverseContext);
417 - expect(traverseFn.calls.count()).toBe(3);
418 -
419 - expect(traverseFn).toHaveBeenCalledWith(
420 - traverseContext,
421 - traverseContext[0],
422 - '.$#1:0'
423 - );
424 - expect(traverseFn).toHaveBeenCalledWith(
425 - traverseContext,
426 - traverseContext[1],
427 - '.$#2:0'
428 - );
429 - expect(traverseFn).toHaveBeenCalledWith(
430 - traverseContext,
431 - traverseContext[2],
432 - '.$#3:0'
433 - );
434 -
435 - expectDev(console.error.calls.count()).toBe(1);
436 - expectDev(console.error.calls.argsFor(0)[0]).toContain(
437 - 'Warning: Using Maps as children is not yet fully supported. It is an ' +
438 - 'experimental feature that might be removed. Convert it to a sequence ' +
439 - '/ iterable of keyed ReactElements instead.'
440 - );
441 - });
442 -
443 - it('should not enumerate enumerable numbers (#4776)', () => {
444 - /*eslint-disable no-extend-native */
445 - Number.prototype['@@iterator'] = function() {
446 - throw new Error('number iterator called');
447 - };
448 - /*eslint-enable no-extend-native */
449 -
450 - try {
451 - var instance = (
452 - <div>
453 - {5}
454 - {12}
455 - {13}
456 - </div>
457 - );
458 -
459 - var traverseFn = jasmine.createSpy();
460 -
461 - traverseAllChildren(instance.props.children, traverseFn, null);
462 - expect(traverseFn.calls.count()).toBe(3);
463 -
464 - expect(traverseFn).toHaveBeenCalledWith(
465 - null,
466 - 5,
467 - '.0'
468 - );
469 - expect(traverseFn).toHaveBeenCalledWith(
470 - null,
471 - 12,
472 - '.1'
473 - );
474 - expect(traverseFn).toHaveBeenCalledWith(
475 - null,
476 - 13,
477 - '.2'
478 - );
479 - } finally {
480 - delete Number.prototype['@@iterator'];
481 - }
482 - });
483 -
484 - it('should allow extension of native prototypes', () => {
485 - /*eslint-disable no-extend-native */
486 - String.prototype.key = 'react';
487 - Number.prototype.key = 'rocks';
488 - /*eslint-enable no-extend-native */
489 -
490 - var instance = (
491 - <div>
492 - {'a'}
493 - {13}
494 - </div>
495 - );
496 -
497 - var traverseFn = jasmine.createSpy();
498 -
499 - traverseAllChildren(instance.props.children, traverseFn, null);
500 - expect(traverseFn.calls.count()).toBe(2);
501 -
502 - expect(traverseFn).toHaveBeenCalledWith(
503 - null,
504 - 'a',
505 - '.0'
506 - );
507 - expect(traverseFn).toHaveBeenCalledWith(
508 - null,
509 - 13,
510 - '.1'
511 - );
512 -
513 - delete String.prototype.key;
514 - delete Number.prototype.key;
515 - });
516 -
517 - it('should throw on object', () => {
518 - expect(function() {
519 - traverseAllChildren({a: 1, b: 2}, function() {}, null);
520 - }).toThrowError(
521 - 'Objects are not valid as a React child (found: object with keys ' +
522 - '{a, b}). If you meant to render a collection of children, use an ' +
523 - 'array instead or wrap the object using createFragment(object) from ' +
524 - 'the React add-ons.'
525 - );
526 - });
527 -
528 - it('should throw on regex', () => {
529 - // Really, we care about dates (#4840) but those have nondeterministic
530 - // serialization (timezones) so let's test a regex instead:
531 - expect(function() {
532 - traverseAllChildren(/abc/, function() {}, null);
533 - }).toThrowError(
534 - 'Objects are not valid as a React child (found: /abc/). If you meant ' +
535 - 'to render a collection of children, use an array instead or wrap the ' +
536 - 'object using createFragment(object) from the React add-ons.'
537 - );
538 - });
539 -
540 - it('should warn for using maps as children with owner info', () => {
541 - spyOn(console, 'error');
542 -
543 - class Parent extends React.Component {
544 - render() {
545 - return (
546 - <div>{new Map([['foo', 0], ['bar', 1]])}</div>
547 - );
548 - }
549 - }
550 -
551 - ReactTestUtils.renderIntoDocument(<Parent />);
552 -
553 - expectDev(console.error.calls.count()).toBe(1);
554 - expectDev(console.error.calls.argsFor(0)[0]).toBe(
555 - 'Warning: Using Maps as children is not yet fully supported. It is an ' +
556 - 'experimental feature that might be removed. Convert it to a sequence ' +
557 - '/ iterable of keyed ReactElements instead. Check the render method of `Parent`.'
558 - );
559 - });
560 -});
src/shared/utils/getIteratorFn.js
+1 -1
@@ -30,7 +30,7 @@ var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
30 * @param {?object} maybeIterable
31 * @return {?function}
32 */
33 -function getIteratorFn(maybeIterable: ?any): ?(p: ReactElement<any>) => void {
33 +function getIteratorFn(maybeIterable: ?any): ?(() => ?Iterator<*>) {
34 var iteratorFn = maybeIterable && (
35 (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL]) ||
36 maybeIterable[FAUX_ITERATOR_SYMBOL]