From 0fa8116f0f4748a1f8a9b0f053e47327787d40a1 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 28 Nov 2016 20:11:12 +0000 Subject: [PATCH 1/3] 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. --- scripts/fiber/tests-failing.txt | 27 +++++++ scripts/fiber/tests-passing-except-dev.txt | 2 + scripts/fiber/tests-passing.txt | 28 +------ .../ReactMultiChildReconcile-test.js | 80 ++++++++++++++++--- 4 files changed, 99 insertions(+), 38 deletions(-) diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt index 85a13ca7c625..aebeb96e1567 100644 --- a/scripts/fiber/tests-failing.txt +++ b/scripts/fiber/tests-failing.txt @@ -100,6 +100,33 @@ src/renderers/shared/shared/__tests__/ReactEmptyComponent-test.js * should still throw when rendering to undefined * throws when rendering null at the top level +src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js +* should reset internal state if removed then readded in an iterable +* should create unique identity +* should preserve order if children order has not changed +* should transition from zero to one children correctly +* should transition from one to zero children correctly +* should transition from one child to null children +* should transition from null children to one child +* should remove nulled out children at the beginning +* should remove nulled out children at the end +* should reverse the order of two children +* should reverse the order of more than two children +* should cycle order correctly +* should cycle order correctly in the other direction +* should remove nulled out children and ignore new null children +* should remove nulled out children and reorder remaining +* should append children to the end +* should append multiple children to the end +* should prepend children to the beginning +* should prepend multiple children to the beginning +* should not prepend an empty child to the beginning +* should not append an empty child to the end +* should not insert empty children in the middle +* should insert one new child in the middle +* should insert multiple new truthy children in the middle +* should insert non-empty children in middle where nulls were + src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js * should correctly handle all possible children for render and update diff --git a/scripts/fiber/tests-passing-except-dev.txt b/scripts/fiber/tests-passing-except-dev.txt index f917d24e3551..62a9675e00c1 100644 --- a/scripts/fiber/tests-passing-except-dev.txt +++ b/scripts/fiber/tests-passing-except-dev.txt @@ -46,6 +46,8 @@ src/renderers/dom/shared/__tests__/ReactDOMInvalidARIAHook-test.js * should warn for an improperly cased aria-* prop src/renderers/dom/shared/__tests__/ReactMount-test.js +* should warn if mounting into dirty rendered markup +* should warn when mounting into document.body * should account for escaping on a checksum mismatch * should warn if render removes React-rendered children * should warn if the unmounted node was rendered by another copy of React diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 072432448638..9642d845b5d0 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -665,9 +665,7 @@ src/renderers/dom/shared/__tests__/ReactMount-test.js * should render different components in same root * should unmount and remount if the key changes * should reuse markup if rendering to the same target twice -* should warn if mounting into dirty rendered markup * should not warn if mounting into non-empty node -* should warn when mounting into document.body * passes the correct callback context src/renderers/dom/shared/__tests__/ReactMountDestruction-test.js @@ -1336,33 +1334,9 @@ src/renderers/shared/shared/__tests__/ReactMultiChild-test.js * should replace children with different keys src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js -* should reset internal state if removed then readded -* should create unique identity -* should preserve order if children order has not changed -* should transition from zero to one children correctly -* should transition from one to zero children correctly -* should transition from one child to null children -* should transition from null children to one child +* should reset internal state if removed then readded in an array * should transition from zero children to null children * should transition from null children to zero children -* should remove nulled out children at the beginning -* should remove nulled out children at the end -* should reverse the order of two children -* should reverse the order of more than two children -* should cycle order correctly -* should cycle order correctly in the other direction -* should remove nulled out children and ignore new null children -* should remove nulled out children and reorder remaining -* should append children to the end -* should append multiple children to the end -* should prepend children to the beginning -* should prepend multiple children to the beginning -* should not prepend an empty child to the beginning -* should not append an empty child to the end -* should not insert empty children in the middle -* should insert one new child in the middle -* should insert multiple new truthy children in the middle -* should insert non-empty children in middle where nulls were src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js * should throw if rendering both HTML and children diff --git a/src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js b/src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js index 43977bff1a6f..5de86177b0b4 100644 --- a/src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js +++ b/src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js @@ -129,9 +129,10 @@ class FriendsStatusDisplay extends React.Component { /> ); } + var childrenToRender = this.props.prepareChildren(children); return (
- {children} + {childrenToRender}
); } @@ -224,13 +225,13 @@ function verifyDomOrderingAccurate(outerContainer, statusDisplays) { expect(orderedDomKeys).toEqual(orderedLogicalKeys); } -/** - * Todo: Check that internal state is preserved across transitions - */ -function testPropsSequence(sequence) { +function testPropsSequenceWithPreparedChildren(sequence, prepareChildren) { var container = document.createElement('div'); var parentInstance = ReactDOM.render( - , + , container ); var statusDisplays = parentInstance.getStatusDisplays(); @@ -239,7 +240,10 @@ function testPropsSequence(sequence) { for (var i = 1; i < sequence.length; i++) { ReactDOM.render( - , + , container ); statusDisplays = parentInstance.getStatusDisplays(); @@ -251,12 +255,66 @@ function testPropsSequence(sequence) { } } +function prepareChildrenArray(childrenArray) { + return childrenArray; +} + +function prepareChildrenIterable(childrenArray) { + return { + '@@iterator': function*() { + for (const child of childrenArray) { + yield child; + } + }, + }; +} + +function testPropsSequence(sequence) { + testPropsSequenceWithPreparedChildren(sequence, prepareChildrenArray); + testPropsSequenceWithPreparedChildren(sequence, prepareChildrenIterable); +} + describe('ReactMultiChildReconcile', () => { beforeEach(() => { jest.resetModuleRegistry(); }); - it('should reset internal state if removed then readded', () => { + it('should reset internal state if removed then readded in an array', () => { + // Test basics. + var props = { + usernameToStatus: { + jcw: 'jcwStatus', + }, + }; + + var container = document.createElement('div'); + var parentInstance = ReactDOM.render( + , + container + ); + var statusDisplays = parentInstance.getStatusDisplays(); + var startingInternalState = statusDisplays.jcw.getInternalState(); + + // Now remove the child. + ReactDOM.render( + , + container + ); + statusDisplays = parentInstance.getStatusDisplays(); + expect(statusDisplays.jcw).toBeFalsy(); + + // Now reset the props that cause there to be a child + ReactDOM.render( + , + container + ); + statusDisplays = parentInstance.getStatusDisplays(); + expect(statusDisplays.jcw).toBeTruthy(); + expect(statusDisplays.jcw.getInternalState()) + .not.toBe(startingInternalState); + }); + + it('should reset internal state if removed then readded in an iterable', () => { // Test basics. var props = { usernameToStatus: { @@ -266,7 +324,7 @@ describe('ReactMultiChildReconcile', () => { var container = document.createElement('div'); var parentInstance = ReactDOM.render( - , + , container ); var statusDisplays = parentInstance.getStatusDisplays(); @@ -274,7 +332,7 @@ describe('ReactMultiChildReconcile', () => { // Now remove the child. ReactDOM.render( - , + , container ); statusDisplays = parentInstance.getStatusDisplays(); @@ -282,7 +340,7 @@ describe('ReactMultiChildReconcile', () => { // Now reset the props that cause there to be a child ReactDOM.render( - , + , container ); statusDisplays = parentInstance.getStatusDisplays(); From 71eefd073aefd32ef3ec0ecd4240f3aa181b8664 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 28 Nov 2016 20:42:08 +0000 Subject: [PATCH 2/3] 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. --- scripts/fiber/tests-failing.txt | 28 +--- scripts/fiber/tests-passing-except-dev.txt | 3 - scripts/fiber/tests-passing.txt | 25 +++ src/renderers/shared/fiber/ReactChildFiber.js | 145 +++++++++++++++++- src/shared/utils/getIteratorFn.js | 2 +- 5 files changed, 167 insertions(+), 36 deletions(-) diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt index aebeb96e1567..b2f3df94f4e6 100644 --- a/scripts/fiber/tests-failing.txt +++ b/scripts/fiber/tests-failing.txt @@ -100,35 +100,9 @@ src/renderers/shared/shared/__tests__/ReactEmptyComponent-test.js * should still throw when rendering to undefined * throws when rendering null at the top level -src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js -* should reset internal state if removed then readded in an iterable -* should create unique identity -* should preserve order if children order has not changed -* should transition from zero to one children correctly -* should transition from one to zero children correctly -* should transition from one child to null children -* should transition from null children to one child -* should remove nulled out children at the beginning -* should remove nulled out children at the end -* should reverse the order of two children -* should reverse the order of more than two children -* should cycle order correctly -* should cycle order correctly in the other direction -* should remove nulled out children and ignore new null children -* should remove nulled out children and reorder remaining -* should append children to the end -* should append multiple children to the end -* should prepend children to the beginning -* should prepend multiple children to the beginning -* should not prepend an empty child to the beginning -* should not append an empty child to the end -* should not insert empty children in the middle -* should insert one new child in the middle -* should insert multiple new truthy children in the middle -* should insert non-empty children in middle where nulls were - src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js * should correctly handle all possible children for render and update +* should reorder keyed text nodes src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js * should warn when stateless component returns array diff --git a/scripts/fiber/tests-passing-except-dev.txt b/scripts/fiber/tests-passing-except-dev.txt index 62a9675e00c1..56c7187592c0 100644 --- a/scripts/fiber/tests-passing-except-dev.txt +++ b/scripts/fiber/tests-passing-except-dev.txt @@ -172,9 +172,6 @@ src/renderers/shared/shared/__tests__/ReactCompositeComponent-test.js src/renderers/shared/shared/__tests__/ReactMultiChild-test.js * should warn for duplicated keys with component stack info -src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js -* should reorder keyed text nodes - src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js * should warn for childContextTypes on a functional component * should warn when given a ref diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 9642d845b5d0..475a0372a725 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -1335,8 +1335,33 @@ src/renderers/shared/shared/__tests__/ReactMultiChild-test.js src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js * should reset internal state if removed then readded in an array +* should reset internal state if removed then readded in an iterable +* should create unique identity +* should preserve order if children order has not changed +* should transition from zero to one children correctly +* should transition from one to zero children correctly +* should transition from one child to null children +* should transition from null children to one child * should transition from zero children to null children * should transition from null children to zero children +* should remove nulled out children at the beginning +* should remove nulled out children at the end +* should reverse the order of two children +* should reverse the order of more than two children +* should cycle order correctly +* should cycle order correctly in the other direction +* should remove nulled out children and ignore new null children +* should remove nulled out children and reorder remaining +* should append children to the end +* should append multiple children to the end +* should prepend children to the beginning +* should prepend multiple children to the beginning +* should not prepend an empty child to the beginning +* should not append an empty child to the end +* should not insert empty children in the middle +* should insert one new child in the middle +* should insert multiple new truthy children in the middle +* should insert non-empty children in middle where nulls were src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js * should throw if rendering both HTML and children diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 6a094f8250b6..e07fdf7646c0 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -545,6 +545,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // In this first iteration, we'll just live with hitting the bad case // (adding everything to a Map) in for every insert/move. + // If you change this code, also update reconcileChildrenIterator() which + // uses the same algorithm. + let resultingFirstChild : ?Fiber = null; let previousNewFiber : ?Fiber = null; @@ -676,10 +679,138 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber : Fiber, currentFirstChild : ?Fiber, newChildren : Iterator<*>, - priority : PriorityLevel) : null { - // TODO: Copy everything from reconcileChildrenArray but use the iterator - // instead. - return null; + priority : PriorityLevel) : ?Fiber { + + // This is the same implementation as reconcileChildrenArray(), + // but using the iterator instead. + + let resultingFirstChild : ?Fiber = null; + let previousNewFiber : ?Fiber = null; + + let oldFiber = currentFirstChild; + let lastPlacedIndex = 0; + let newIdx = 0; + let nextOldFiber = null; + + let step = newChildren.next(); + for (; oldFiber && !step.done; newIdx++, step = newChildren.next()) { + if (oldFiber) { + if (oldFiber.index > newIdx) { + nextOldFiber = oldFiber; + oldFiber = null; + } else { + nextOldFiber = oldFiber.sibling; + } + } + const newFiber = updateSlot( + returnFiber, + oldFiber, + step.value, + priority + ); + if (!newFiber) { + // TODO: This breaks on empty slots like null children. That's + // unfortunate because it triggers the slow path all the time. We need + // a better way to communicate whether this was a miss or null, + // boolean, undefined, etc. + if (!oldFiber) { + oldFiber = nextOldFiber; + } + break; + } + if (shouldTrackSideEffects) { + if (oldFiber && !newFiber.alternate) { + // We matched the slot, but we didn't reuse the existing fiber, so we + // need to delete the existing child. + deleteChild(returnFiber, oldFiber); + } + } + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); + if (!previousNewFiber) { + // TODO: Move out of the loop. This only happens for the first run. + resultingFirstChild = newFiber; + } else { + // TODO: Defer siblings if we're not at the right index for this slot. + // I.e. if we had null values before, then we want to defer this + // for each null value. However, we also don't want to call updateSlot + // with the previous one. + previousNewFiber.sibling = newFiber; + } + previousNewFiber = newFiber; + oldFiber = nextOldFiber; + } + + if (step.done) { + // We've reached the end of the new children. We can delete the rest. + deleteRemainingChildren(returnFiber, oldFiber); + return resultingFirstChild; + } + + if (!oldFiber) { + // If we don't have any more existing children we can choose a fast path + // since the rest will all be insertions. + for (; !step.done; newIdx++, step = newChildren.next()) { + const newFiber = createChild( + returnFiber, + step.value, + priority + ); + if (!newFiber) { + continue; + } + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); + if (!previousNewFiber) { + // TODO: Move out of the loop. This only happens for the first run. + resultingFirstChild = newFiber; + } else { + previousNewFiber.sibling = newFiber; + } + previousNewFiber = newFiber; + } + return resultingFirstChild; + } + + // Add all children to a key map for quick lookups. + const existingChildren = mapRemainingChildren(returnFiber, oldFiber); + + // Keep scanning and use the map to restore deleted items as moves. + for (; !step.done; newIdx++, step = newChildren.next()) { + const newFiber = updateFromMap( + existingChildren, + returnFiber, + newIdx, + step.value, + priority + ); + if (newFiber) { + if (shouldTrackSideEffects) { + if (newFiber.alternate) { + // The new fiber is a work in progress, but if there exists a + // current, that means that we reused the fiber. We need to delete + // it from the child list so that we don't add it to the deletion + // list. + existingChildren.delete( + newFiber.key === null ? newIdx : newFiber.key + ); + } + } + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); + if (!previousNewFiber) { + resultingFirstChild = newFiber; + } else { + previousNewFiber.sibling = newFiber; + } + previousNewFiber = newFiber; + } + } + + if (shouldTrackSideEffects) { + // Any existing children that weren't consumed above were deleted. We need + // to add them to the deletion list. + existingChildren.forEach(child => deleteChild(returnFiber, child)); + } + + return resultingFirstChild; } function reconcileSingleTextNode( @@ -919,10 +1050,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const iteratorFn = getIteratorFn(newChild); if (iteratorFn) { + const iterator = iteratorFn.call(newChild); + if (iterator == null) { + throw new Error('An iterable object provided no iterator.'); + } return reconcileChildrenIterator( returnFiber, currentFirstChild, - newChild, + iterator, priority ); } diff --git a/src/shared/utils/getIteratorFn.js b/src/shared/utils/getIteratorFn.js index 4ed780aa204e..8958e63c4a53 100644 --- a/src/shared/utils/getIteratorFn.js +++ b/src/shared/utils/getIteratorFn.js @@ -30,7 +30,7 @@ var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec. * @param {?object} maybeIterable * @return {?function} */ -function getIteratorFn(maybeIterable: ?any): ?(p: ReactElement) => void { +function getIteratorFn(maybeIterable: ?any): ?(() => ?Iterator<*>) { var iteratorFn = maybeIterable && ( (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL]) || maybeIterable[FAUX_ITERATOR_SYMBOL] From 2b7e0a0cc4f2944ad9e7ea9472ade511f97120dd Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 28 Nov 2016 21:35:10 +0000 Subject: [PATCH 3/3] 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. --- scripts/fiber/tests-passing-except-dev.txt | 4 +- scripts/fiber/tests-passing.txt | 27 +- .../children/__tests__/ReactChildren-test.js | 538 +++++++++++++++-- .../shared/__tests__/ReactMultiChild-test.js | 22 + .../__tests__/traverseAllChildren-test.js | 560 ------------------ 5 files changed, 532 insertions(+), 619 deletions(-) delete mode 100644 src/shared/utils/__tests__/traverseAllChildren-test.js diff --git a/scripts/fiber/tests-passing-except-dev.txt b/scripts/fiber/tests-passing-except-dev.txt index 56c7187592c0..5590ed90a215 100644 --- a/scripts/fiber/tests-passing-except-dev.txt +++ b/scripts/fiber/tests-passing-except-dev.txt @@ -171,11 +171,9 @@ src/renderers/shared/shared/__tests__/ReactCompositeComponent-test.js src/renderers/shared/shared/__tests__/ReactMultiChild-test.js * should warn for duplicated keys with component stack info +* should warn for using maps as children with owner info src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js * should warn for childContextTypes on a functional component * should warn when given a ref * should use correct name in key warning - -src/shared/utils/__tests__/traverseAllChildren-test.js -* should warn for using maps as children with owner info diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 475a0372a725..ba28e7da449e 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -108,6 +108,15 @@ src/isomorphic/children/__tests__/ReactChildren-test.js * should support identity for simple * should treat single arrayless child as being in array * should treat single child in array as expected +* should be called for each child +* should traverse children of different kinds +* should be called for each child in nested structure +* should retain key across two mappings +* should be called for each child in an iterable without keys +* should be called for each child in an iterable with keys +* should use keys from entry iterables +* should not enumerate enumerable numbers (#4776) +* should allow extension of native prototypes * should pass key to returned component * should invoke callback with the right context * should be called for each child @@ -122,6 +131,8 @@ src/isomorphic/children/__tests__/ReactChildren-test.js * should count the number of children in flat structure * should count the number of children in nested structure * should flatten children to an array +* should throw on object +* should throw on regex src/isomorphic/children/__tests__/onlyChild-test.js * should fail when passed two children @@ -1536,22 +1547,6 @@ src/shared/utils/__tests__/PooledClass-test.js src/shared/utils/__tests__/reactProdInvariant-test.js * should throw with the correct number of `%s`s in the URL -src/shared/utils/__tests__/traverseAllChildren-test.js -* should support identity for simple -* should treat single arrayless child as being in array -* should treat single child in array as expected -* should be called for each child -* should traverse children of different kinds -* should be called for each child in nested structure -* should retain key across two mappings -* should be called for each child in an iterable without keys -* should be called for each child in an iterable with keys -* should use keys from entry iterables -* should not enumerate enumerable numbers (#4776) -* should allow extension of native prototypes -* should throw on object -* should throw on regex - src/test/__tests__/ReactTestUtils-test.js * should have shallow rendering * should shallow render a functional component diff --git a/src/isomorphic/children/__tests__/ReactChildren-test.js b/src/isomorphic/children/__tests__/ReactChildren-test.js index 6eb4c89e2d12..c1e3bc342624 100644 --- a/src/isomorphic/children/__tests__/ReactChildren-test.js +++ b/src/isomorphic/children/__tests__/ReactChildren-test.js @@ -12,18 +12,19 @@ 'use strict'; describe('ReactChildren', () => { - var ReactChildren; var React; var ReactFragment; beforeEach(() => { - ReactChildren = require('ReactChildren'); + jest.resetModuleRegistry(); React = require('React'); ReactFragment = require('ReactFragment'); }); it('should support identity for simple', () => { + var context = {}; var callback = jasmine.createSpy().and.callFake(function(kid, index) { + expect(this).toBe(context); return kid; }); @@ -33,43 +34,478 @@ describe('ReactChildren', () => { // using structures that arrive from transforms. var instance =
{simpleKid}
; - ReactChildren.forEach(instance.props.children, callback); + React.Children.forEach(instance.props.children, callback, context); expect(callback).toHaveBeenCalledWith(simpleKid, 0); callback.calls.reset(); - var mappedChildren = ReactChildren.map(instance.props.children, callback); + var mappedChildren = React.Children.map(instance.props.children, callback, context); expect(callback).toHaveBeenCalledWith(simpleKid, 0); expect(mappedChildren[0]).toEqual(); }); it('should treat single arrayless child as being in array', () => { + var context = {}; var callback = jasmine.createSpy().and.callFake(function(kid, index) { + expect(this).toBe(context); return kid; }); var simpleKid = ; var instance =
{simpleKid}
; - ReactChildren.forEach(instance.props.children, callback); + React.Children.forEach(instance.props.children, callback, context); expect(callback).toHaveBeenCalledWith(simpleKid, 0); callback.calls.reset(); - var mappedChildren = ReactChildren.map(instance.props.children, callback); + var mappedChildren = React.Children.map(instance.props.children, callback, context); expect(callback).toHaveBeenCalledWith(simpleKid, 0); expect(mappedChildren[0]).toEqual(); }); it('should treat single child in array as expected', () => { + var context = {}; var callback = jasmine.createSpy().and.callFake(function(kid, index) { + expect(this).toBe(context); return kid; }); var simpleKid = ; var instance =
{[simpleKid]}
; - ReactChildren.forEach(instance.props.children, callback); + React.Children.forEach(instance.props.children, callback, context); expect(callback).toHaveBeenCalledWith(simpleKid, 0); callback.calls.reset(); - var mappedChildren = ReactChildren.map(instance.props.children, callback); + var mappedChildren = React.Children.map(instance.props.children, callback, context); expect(callback).toHaveBeenCalledWith(simpleKid, 0); expect(mappedChildren[0]).toEqual(); + }); + + it('should be called for each child', () => { + var zero =
; + var one = null; + var two =
; + var three = null; + var four =
; + var context = {}; + + var callback = + jasmine.createSpy().and.callFake(function(kid) { + expect(this).toBe(context); + return kid; + }); + + var instance = ( +
+ {zero} + {one} + {two} + {three} + {four} +
+ ); + + function assertCalls() { + expect(callback).toHaveBeenCalledWith(zero, 0); + expect(callback).toHaveBeenCalledWith(one, 1); + expect(callback).toHaveBeenCalledWith(two, 2); + expect(callback).toHaveBeenCalledWith(three, 3); + expect(callback).toHaveBeenCalledWith(four, 4); + callback.calls.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + + var mappedChildren = React.Children.map(instance.props.children, callback, context); + assertCalls(); + expect(mappedChildren).toEqual([ +
, +
, +
, + ]); + }); + + it('should traverse children of different kinds', () => { + var div =
; + var span = ; + var a = ; + + var context = {}; + var callback = + jasmine.createSpy().and.callFake(function(kid) { + expect(this).toBe(context); + return kid; + }); + + var instance = ( +
+ {div} + {[ReactFragment.create({span})]} + {ReactFragment.create({a: a})} + {'string'} + {1234} + {true} + {false} + {null} + {undefined} +
+ ); + + function assertCalls() { + expect(callback.calls.count()).toBe(9); + expect(callback).toHaveBeenCalledWith(div, 0); + expect(callback).toHaveBeenCalledWith(, 1); + expect(callback).toHaveBeenCalledWith(
, 2); + expect(callback).toHaveBeenCalledWith('string', 3); + expect(callback).toHaveBeenCalledWith(1234, 4); + expect(callback).toHaveBeenCalledWith(null, 5); + expect(callback).toHaveBeenCalledWith(null, 6); + expect(callback).toHaveBeenCalledWith(null, 7); + expect(callback).toHaveBeenCalledWith(null, 8); + callback.calls.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + + var mappedChildren = React.Children.map(instance.props.children, callback, context); + assertCalls(); + expect(mappedChildren).toEqual([ +
, + , + , + 'string', + 1234, + ]); + }); + + it('should be called for each child in nested structure', () => { + var zero =
; + var one = null; + var two =
; + var three = null; + var four =
; + var five =
; + // five is placed into a JS object with a key that is joined to the + // component key attribute. + // Precedence is as follows: + // 1. If grouped in an Object, the object key combined with `key` prop + // 2. If grouped in an Array, the `key` prop, falling back to array index + + var context = {}; + var callback = + jasmine.createSpy().and.callFake(function(kid) { + return kid; + }); + var instance = ( +
{[ + ReactFragment.create({ + firstHalfKey: [zero, one, two], + secondHalfKey: [three, four], + keyFive: five, + }), + ]}
+ ); + + function assertCalls() { + expect(callback.calls.count()).toBe(4); + expect(callback).toHaveBeenCalledWith(
, 0); + expect(callback).toHaveBeenCalledWith(
, 1); + expect(callback).toHaveBeenCalledWith(
, 2); + expect(callback).toHaveBeenCalledWith(
, 3); + callback.calls.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + + var mappedChildren = React.Children.map(instance.props.children, callback, context); + assertCalls(); + expect(mappedChildren).toEqual([ +
, +
, +
, +
, + ]); + }); + + it('should retain key across two mappings', () => { + var zeroForceKey =
; + var oneForceKey =
; + var context = {}; + var callback = + jasmine.createSpy().and.callFake(function(kid) { + expect(this).toBe(context); + return kid; + }); + + var forcedKeys = ( +
+ {zeroForceKey} + {oneForceKey} +
+ ); + + function assertCalls() { + expect(callback).toHaveBeenCalledWith(zeroForceKey, 0); + expect(callback).toHaveBeenCalledWith(oneForceKey, 1); + callback.calls.reset(); + } + + React.Children.forEach(forcedKeys.props.children, callback, context); + assertCalls(); + + var mappedChildren = React.Children.map(forcedKeys.props.children, callback, context); + assertCalls(); + expect(mappedChildren).toEqual([ +
, +
, + ]); + }); + + it('should be called for each child in an iterable without keys', () => { + spyOn(console, 'error'); + var threeDivIterable = { + '@@iterator': function() { + var i = 0; + return { + next: function() { + if (i++ < 3) { + return {value:
, done: false}; + } else { + return {value: undefined, done: true}; + } + }, + }; + }, + }; + + var context = {}; + var callback = + jasmine.createSpy().and.callFake(function(kid) { + expect(this).toBe(context); + return kid; + }); + + var instance = ( +
+ {threeDivIterable} +
+ ); + + function assertCalls() { + expect(callback.calls.count()).toBe(3); + expect(callback).toHaveBeenCalledWith(
, 0); + expect(callback).toHaveBeenCalledWith(
, 1); + expect(callback).toHaveBeenCalledWith(
, 2); + callback.calls.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Each child in an array or iterator should have a unique "key" prop.' + ); + console.error.calls.reset(); + + var mappedChildren = React.Children.map(instance.props.children, callback, context); + assertCalls(); + expectDev(console.error.calls.count()).toBe(0); + expect(mappedChildren).toEqual([ +
, +
, +
, + ]); + }); + + it('should be called for each child in an iterable with keys', () => { + var threeDivIterable = { + '@@iterator': function() { + var i = 0; + return { + next: function() { + if (i++ < 3) { + return {value:
, done: false}; + } else { + return {value: undefined, done: true}; + } + }, + }; + }, + }; + + var context = {}; + var callback = + jasmine.createSpy().and.callFake(function(kid) { + expect(this).toBe(context); + return kid; + }); + + var instance = ( +
+ {threeDivIterable} +
+ ); + + function assertCalls() { + expect(callback.calls.count()).toBe(3); + expect(callback).toHaveBeenCalledWith(
, 0); + expect(callback).toHaveBeenCalledWith(
, 1); + expect(callback).toHaveBeenCalledWith(
, 2); + callback.calls.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + + var mappedChildren = React.Children.map(instance.props.children, callback, context); + assertCalls(); + expect(mappedChildren).toEqual([ +
, +
, +
, + ]); + }); + + it('should use keys from entry iterables', () => { + spyOn(console, 'error'); + + var threeDivEntryIterable = { + '@@iterator': function() { + var i = 0; + return { + next: function() { + if (i++ < 3) { + return {value: ['#' + i,
], done: false}; + } else { + return {value: undefined, done: true}; + } + }, + }; + }, + }; + threeDivEntryIterable.entries = threeDivEntryIterable['@@iterator']; + + var context = {}; + var callback = + jasmine.createSpy().and.callFake(function(kid) { + expect(this).toBe(context); + return kid; + }); + + var instance = ( +
+ {threeDivEntryIterable} +
+ ); + + function assertCalls() { + expect(callback.calls.count()).toBe(3); + // TODO: why + expect(callback).toHaveBeenCalledWith(
, 0); + expect(callback).toHaveBeenCalledWith(
, 1); + expect(callback).toHaveBeenCalledWith(
, 2); + + callback.calls.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Using Maps as children is not yet fully supported. It is an ' + + 'experimental feature that might be removed. Convert it to a sequence ' + + '/ iterable of keyed ReactElements instead.' + ); + console.error.calls.reset(); + + var mappedChildren = React.Children.map(instance.props.children, callback, context); + assertCalls(); + expect(mappedChildren).toEqual([ +
, +
, +
, + ]); + expectDev(console.error.calls.count()).toBe(0); + }); + + it('should not enumerate enumerable numbers (#4776)', () => { + /*eslint-disable no-extend-native */ + Number.prototype['@@iterator'] = function() { + throw new Error('number iterator called'); + }; + /*eslint-enable no-extend-native */ + + try { + var instance = ( +
+ {5} + {12} + {13} +
+ ); + + var context = {}; + var callback = jasmine.createSpy().and.callFake(function(kid) { + expect(this).toBe(context); + return kid; + }); + + var assertCalls = function() { + expect(callback.calls.count()).toBe(3); + expect(callback).toHaveBeenCalledWith(5, 0); + expect(callback).toHaveBeenCalledWith(12, 1); + expect(callback).toHaveBeenCalledWith(13, 2); + callback.calls.reset(); + }; + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + + var mappedChildren = React.Children.map(instance.props.children, callback, context); + assertCalls(); + expect(mappedChildren).toEqual([5, 12, 13]); + } finally { + delete Number.prototype['@@iterator']; + } + }); + + it('should allow extension of native prototypes', () => { + /*eslint-disable no-extend-native */ + String.prototype.key = 'react'; + Number.prototype.key = 'rocks'; + /*eslint-enable no-extend-native */ + + var instance = ( +
+ {'a'} + {13} +
+ ); + + var context = {}; + var callback = jasmine.createSpy().and.callFake(function(kid) { + expect(this).toBe(context); + return kid; + }); + + function assertCalls() { + expect(callback.calls.count()).toBe(2, 0); + expect(callback).toHaveBeenCalledWith('a', 0); + expect(callback).toHaveBeenCalledWith(13, 1); + callback.calls.reset(); + } + + React.Children.forEach(instance.props.children, callback, context); + assertCalls(); + + var mappedChildren = React.Children.map(instance.props.children, callback, context); + assertCalls(); + expect(mappedChildren).toEqual([ + 'a', + 13, + ]); + + delete String.prototype.key; + delete Number.prototype.key; }); it('should pass key to returned component', () => { @@ -80,9 +516,9 @@ describe('ReactChildren', () => { var simpleKid = ; var instance =
{simpleKid}
; - var mappedChildren = ReactChildren.map(instance.props.children, mapFn); + var mappedChildren = React.Children.map(instance.props.children, mapFn); - expect(ReactChildren.count(mappedChildren)).toBe(1); + expect(React.Children.count(mappedChildren)).toBe(1); expect(mappedChildren[0]).not.toBe(simpleKid); expect(mappedChildren[0].props.children).toBe(simpleKid); expect(mappedChildren[0].key).toBe('.$simple'); @@ -100,13 +536,13 @@ describe('ReactChildren', () => { var simpleKid = ; var instance =
{simpleKid}
; - ReactChildren.forEach(instance.props.children, callback, scopeTester); + React.Children.forEach(instance.props.children, callback, scopeTester); expect(lastContext).toBe(scopeTester); var mappedChildren = - ReactChildren.map(instance.props.children, callback, scopeTester); + React.Children.map(instance.props.children, callback, scopeTester); - expect(ReactChildren.count(mappedChildren)).toBe(1); + expect(React.Children.count(mappedChildren)).toBe(1); expect(mappedChildren[0]).toBe(scopeTester); }); @@ -138,7 +574,7 @@ describe('ReactChildren', () => {
); - ReactChildren.forEach(instance.props.children, callback); + React.Children.forEach(instance.props.children, callback); expect(callback).toHaveBeenCalledWith(zero, 0); expect(callback).toHaveBeenCalledWith(one, 1); expect(callback).toHaveBeenCalledWith(two, 2); @@ -147,9 +583,9 @@ describe('ReactChildren', () => { callback.calls.reset(); var mappedChildren = - ReactChildren.map(instance.props.children, callback); + React.Children.map(instance.props.children, callback); expect(callback.calls.count()).toBe(5); - expect(ReactChildren.count(mappedChildren)).toBe(4); + expect(React.Children.count(mappedChildren)).toBe(4); // Keys default to indices. expect([ mappedChildren[0].key, @@ -215,7 +651,7 @@ describe('ReactChildren', () => { 'keyFive/.$keyFiveInner', ]); - ReactChildren.forEach(instance.props.children, callback); + React.Children.forEach(instance.props.children, callback); expect(callback.calls.count()).toBe(4); expect(callback).toHaveBeenCalledWith(frag[0], 0); expect(callback).toHaveBeenCalledWith(frag[1], 1); @@ -223,14 +659,14 @@ describe('ReactChildren', () => { expect(callback).toHaveBeenCalledWith(frag[3], 3); callback.calls.reset(); - var mappedChildren = ReactChildren.map(instance.props.children, callback); + var mappedChildren = React.Children.map(instance.props.children, callback); expect(callback.calls.count()).toBe(4); expect(callback).toHaveBeenCalledWith(frag[0], 0); expect(callback).toHaveBeenCalledWith(frag[1], 1); expect(callback).toHaveBeenCalledWith(frag[2], 2); expect(callback).toHaveBeenCalledWith(frag[3], 3); - expect(ReactChildren.count(mappedChildren)).toBe(4); + expect(React.Children.count(mappedChildren)).toBe(4); // Keys default to indices. expect([ mappedChildren[0].key, @@ -272,7 +708,7 @@ describe('ReactChildren', () => { var expectedForcedKeys = ['giraffe/.$keyZero', '.$keyOne']; var mappedChildrenForcedKeys = - ReactChildren.map(forcedKeys.props.children, mapFn); + React.Children.map(forcedKeys.props.children, mapFn); var mappedForcedKeys = mappedChildrenForcedKeys.map((c) => c.key); expect(mappedForcedKeys).toEqual(expectedForcedKeys); @@ -281,7 +717,7 @@ describe('ReactChildren', () => { '.$.$keyOne', ]; var remappedChildrenForcedKeys = - ReactChildren.map(mappedChildrenForcedKeys, mapFn); + React.Children.map(mappedChildrenForcedKeys, mapFn); expect( remappedChildrenForcedKeys.map((c) => c.key) ).toEqual(expectedRemappedForcedKeys); @@ -304,7 +740,7 @@ describe('ReactChildren', () => { ); expect(function() { - ReactChildren.map(instance.props.children, mapFn); + React.Children.map(instance.props.children, mapFn); }).not.toThrow(); }); @@ -315,12 +751,12 @@ describe('ReactChildren', () => {
); - var mapped = ReactChildren.map( + var mapped = React.Children.map( instance.props.children, element => element, ); - var mappedWithClone = ReactChildren.map( + var mappedWithClone = React.Children.map( instance.props.children, element => React.cloneElement(element), ); @@ -335,12 +771,12 @@ describe('ReactChildren', () => {
); - var mapped = ReactChildren.map( + var mapped = React.Children.map( instance.props.children, element => element, ); - var mappedWithClone = ReactChildren.map( + var mappedWithClone = React.Children.map( instance.props.children, element => React.cloneElement(element, {key: 'unique'}), ); @@ -349,19 +785,19 @@ describe('ReactChildren', () => { }); it('should return 0 for null children', () => { - var numberOfChildren = ReactChildren.count(null); + var numberOfChildren = React.Children.count(null); expect(numberOfChildren).toBe(0); }); it('should return 0 for undefined children', () => { - var numberOfChildren = ReactChildren.count(undefined); + var numberOfChildren = React.Children.count(undefined); expect(numberOfChildren).toBe(0); }); it('should return 1 for single child', () => { var simpleKid = ; var instance =
{simpleKid}
; - var numberOfChildren = ReactChildren.count(instance.props.children); + var numberOfChildren = React.Children.count(instance.props.children); expect(numberOfChildren).toBe(1); }); @@ -381,7 +817,7 @@ describe('ReactChildren', () => { {four}
); - var numberOfChildren = ReactChildren.count(instance.props.children); + var numberOfChildren = React.Children.count(instance.props.children); expect(numberOfChildren).toBe(5); }); @@ -408,23 +844,23 @@ describe('ReactChildren', () => { null, ]}
); - var numberOfChildren = ReactChildren.count(instance.props.children); + var numberOfChildren = React.Children.count(instance.props.children); expect(numberOfChildren).toBe(5); }); it('should flatten children to an array', () => { - expect(ReactChildren.toArray(undefined)).toEqual([]); - expect(ReactChildren.toArray(null)).toEqual([]); + expect(React.Children.toArray(undefined)).toEqual([]); + expect(React.Children.toArray(null)).toEqual([]); - expect(ReactChildren.toArray(
).length).toBe(1); - expect(ReactChildren.toArray([
]).length).toBe(1); + expect(React.Children.toArray(
).length).toBe(1); + expect(React.Children.toArray([
]).length).toBe(1); expect( - ReactChildren.toArray(
)[0].key + React.Children.toArray(
)[0].key ).toBe( - ReactChildren.toArray([
])[0].key + React.Children.toArray([
])[0].key ); - var flattened = ReactChildren.toArray([ + var flattened = React.Children.toArray([ [
,
,
], [
,
,
], ]); @@ -433,7 +869,7 @@ describe('ReactChildren', () => { expect(flattened[3].key).toContain('banana'); expect(flattened[1].key).not.toBe(flattened[3].key); - var reversed = ReactChildren.toArray([ + var reversed = React.Children.toArray([ [
,
,
], [
,
,
], ]); @@ -445,9 +881,31 @@ describe('ReactChildren', () => { expect(flattened[5].key).toBe(reversed[3].key); // null/undefined/bool are all omitted - expect(ReactChildren.toArray([1, 'two', null, undefined, true])).toEqual( + expect(React.Children.toArray([1, 'two', null, undefined, true])).toEqual( [1, 'two'] ); }); + it('should throw on object', () => { + expect(function() { + React.Children.forEach({a: 1, b: 2}, function() {}, null); + }).toThrowError( + 'Objects are not valid as a React child (found: object with keys ' + + '{a, b}). If you meant to render a collection of children, use an ' + + 'array instead or wrap the object using createFragment(object) from ' + + 'the React add-ons.' + ); + }); + + it('should throw on regex', () => { + // Really, we care about dates (#4840) but those have nondeterministic + // serialization (timezones) so let's test a regex instead: + expect(function() { + React.Children.forEach(/abc/, function() {}, null); + }).toThrowError( + 'Objects are not valid as a React child (found: /abc/). If you meant ' + + 'to render a collection of children, use an array instead or wrap the ' + + 'object using createFragment(object) from the React add-ons.' + ); + }); }); diff --git a/src/renderers/shared/shared/__tests__/ReactMultiChild-test.js b/src/renderers/shared/shared/__tests__/ReactMultiChild-test.js index 500d00aa39d8..a7f69035308b 100644 --- a/src/renderers/shared/shared/__tests__/ReactMultiChild-test.js +++ b/src/renderers/shared/shared/__tests__/ReactMultiChild-test.js @@ -197,5 +197,27 @@ describe('ReactMultiChild', () => { ' in Parent (at **)' ); }); + + it('should warn for using maps as children with owner info', () => { + spyOn(console, 'error'); + + class Parent extends React.Component { + render() { + return ( +
{new Map([['foo', 0], ['bar', 1]])}
+ ); + } + } + + var container = document.createElement('div'); + ReactDOM.render(, container); + + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: Using Maps as children is not yet fully supported. It is an ' + + 'experimental feature that might be removed. Convert it to a sequence ' + + '/ iterable of keyed ReactElements instead. Check the render method of `Parent`.' + ); + }); }); }); diff --git a/src/shared/utils/__tests__/traverseAllChildren-test.js b/src/shared/utils/__tests__/traverseAllChildren-test.js deleted file mode 100644 index d6f17280997d..000000000000 --- a/src/shared/utils/__tests__/traverseAllChildren-test.js +++ /dev/null @@ -1,560 +0,0 @@ -/** - * Copyright 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @emails react-core - */ - -'use strict'; - -describe('traverseAllChildren', () => { - var traverseAllChildren; - var React; - var ReactFragment; - var ReactTestUtils; - - beforeEach(() => { - jest.resetModuleRegistry(); - traverseAllChildren = require('traverseAllChildren'); - React = require('React'); - ReactFragment = require('ReactFragment'); - ReactTestUtils = require('ReactTestUtils'); - }); - - function frag(obj) { - return ReactFragment.create(obj); - } - - it('should support identity for simple', () => { - var traverseContext = []; - var traverseFn = - jasmine.createSpy().and.callFake(function(context, kid, key, index) { - context.push(true); - }); - - var simpleKid = ; - - // Jasmine doesn't provide a way to test that the fn was invoked with scope. - var instance =
{simpleKid}
; - traverseAllChildren(instance.props.children, traverseFn, traverseContext); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - simpleKid, - '.$simple' - ); - expect(traverseContext.length).toEqual(1); - }); - - it('should treat single arrayless child as being in array', () => { - var traverseContext = []; - var traverseFn = - jasmine.createSpy().and.callFake(function(context, kid, key, index) { - context.push(true); - }); - - var simpleKid = ; - var instance =
{simpleKid}
; - traverseAllChildren(instance.props.children, traverseFn, traverseContext); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - simpleKid, - '.0' - ); - expect(traverseContext.length).toEqual(1); - }); - - it('should treat single child in array as expected', () => { - spyOn(console, 'error'); - var traverseContext = []; - var traverseFn = - jasmine.createSpy().and.callFake(function(context, kid, key, index) { - context.push(true); - }); - - var simpleKid = ; - var instance =
{[simpleKid]}
; - traverseAllChildren(instance.props.children, traverseFn, traverseContext); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - simpleKid, - '.0' - ); - expect(traverseContext.length).toEqual(1); - expectDev(console.error.calls.count()).toBe(1); - expectDev(console.error.calls.argsFor(0)[0]).toContain( - 'Warning: Each child in an array or iterator should have a unique "key" prop.' - ); - }); - - it('should be called for each child', () => { - var zero =
; - var one = null; - var two =
; - var three = null; - var four =
; - - var traverseContext = []; - var traverseFn = - jasmine.createSpy().and.callFake(function(context, kid, key, index) { - context.push(true); - }); - - var instance = ( -
- {zero} - {one} - {two} - {three} - {four} -
- ); - - traverseAllChildren(instance.props.children, traverseFn, traverseContext); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - zero, - '.$keyZero' - ); - expect(traverseFn).toHaveBeenCalledWith(traverseContext, one, '.1'); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - two, - '.$keyTwo' - ); - expect(traverseFn).toHaveBeenCalledWith(traverseContext, three, '.3'); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - four, - '.$keyFour' - ); - }); - - it('should traverse children of different kinds', () => { - var div =
; - var span = ; - var a = ; - - var traverseContext = []; - var traverseFn = - jasmine.createSpy().and.callFake(function(context, kid, key, index) { - context.push(true); - }); - - var instance = ( -
- {div} - {[frag({span})]} - {frag({a: a})} - {'string'} - {1234} - {true} - {false} - {null} - {undefined} -
- ); - - traverseAllChildren(instance.props.children, traverseFn, traverseContext); - - expect(traverseFn.calls.count()).toBe(9); - expect(traverseContext.length).toEqual(9); - - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, div, '.$divNode' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, , '.1:0:$span/.$spanNode' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext,
, '.2:$a/.$aNode' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, 'string', '.3' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, 1234, '.4' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, null, '.5' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, null, '.6' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, null, '.7' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, null, '.8' - ); - }); - - it('should be called for each child in nested structure', () => { - var zero =
; - var one = null; - var two =
; - var three = null; - var four =
; - var five =
; - // five is placed into a JS object with a key that is joined to the - // component key attribute. - // Precedence is as follows: - // 1. If grouped in an Object, the object key combined with `key` prop - // 2. If grouped in an Array, the `key` prop, falling back to array index - - - var traverseContext = []; - var traverseFn = - jasmine.createSpy().and.callFake(function(context, kid, key, index) { - context.push(true); - }); - - var instance = ( -
{[ - frag({ - firstHalfKey: [zero, one, two], - secondHalfKey: [three, four], - keyFive: five, - }), - ]}
- ); - - traverseAllChildren(instance.props.children, traverseFn, traverseContext); - expect(traverseFn.calls.count()).toBe(4); - expect(traverseContext.length).toEqual(4); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, -
, - '.0:$firstHalfKey/.$keyZero' - ); - - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, -
, - '.0:$firstHalfKey/.$keyTwo' - ); - - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, -
, - '.0:$secondHalfKey/.$keyFour' - ); - - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, -
, - '.0:$keyFive/.$keyFiveInner' - ); - }); - - it('should retain key across two mappings', () => { - var zeroForceKey =
; - var oneForceKey =
; - var traverseContext = []; - var traverseFn = - jasmine.createSpy().and.callFake(function(context, kid, key, index) { - context.push(true); - }); - - var forcedKeys = ( -
- {zeroForceKey} - {oneForceKey} -
- ); - - traverseAllChildren(forcedKeys.props.children, traverseFn, traverseContext); - expect(traverseContext.length).toEqual(2); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - zeroForceKey, - '.$keyZero' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - oneForceKey, - '.$keyOne' - ); - }); - - it('should be called for each child in an iterable without keys', () => { - spyOn(console, 'error'); - var threeDivIterable = { - '@@iterator': function() { - var i = 0; - return { - next: function() { - if (i++ < 3) { - return {value:
, done: false}; - } else { - return {value: undefined, done: true}; - } - }, - }; - }, - }; - - var traverseContext = []; - var traverseFn = - jasmine.createSpy().and.callFake(function(context, kid, key, index) { - context.push(kid); - }); - - var instance = ( -
- {threeDivIterable} -
- ); - - traverseAllChildren(instance.props.children, traverseFn, traverseContext); - expect(traverseFn.calls.count()).toBe(3); - - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - traverseContext[0], - '.0' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - traverseContext[1], - '.1' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - traverseContext[2], - '.2' - ); - - expectDev(console.error.calls.count()).toBe(1); - expectDev(console.error.calls.argsFor(0)[0]).toContain( - 'Warning: Each child in an array or iterator should have a unique "key" prop.' - ); - }); - - it('should be called for each child in an iterable with keys', () => { - var threeDivIterable = { - '@@iterator': function() { - var i = 0; - return { - next: function() { - if (i++ < 3) { - return {value:
, done: false}; - } else { - return {value: undefined, done: true}; - } - }, - }; - }, - }; - - var traverseContext = []; - var traverseFn = - jasmine.createSpy().and.callFake(function(context, kid, key, index) { - context.push(kid); - }); - - var instance = ( -
- {threeDivIterable} -
- ); - - traverseAllChildren(instance.props.children, traverseFn, traverseContext); - expect(traverseFn.calls.count()).toBe(3); - - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - traverseContext[0], - '.$#1' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - traverseContext[1], - '.$#2' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - traverseContext[2], - '.$#3' - ); - }); - - it('should use keys from entry iterables', () => { - spyOn(console, 'error'); - - var threeDivEntryIterable = { - '@@iterator': function() { - var i = 0; - return { - next: function() { - if (i++ < 3) { - return {value: ['#' + i,
], done: false}; - } else { - return {value: undefined, done: true}; - } - }, - }; - }, - }; - threeDivEntryIterable.entries = threeDivEntryIterable['@@iterator']; - - var traverseContext = []; - var traverseFn = - jasmine.createSpy().and.callFake(function(context, kid, key, index) { - context.push(kid); - }); - - var instance = ( -
- {threeDivEntryIterable} -
- ); - - traverseAllChildren(instance.props.children, traverseFn, traverseContext); - expect(traverseFn.calls.count()).toBe(3); - - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - traverseContext[0], - '.$#1:0' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - traverseContext[1], - '.$#2:0' - ); - expect(traverseFn).toHaveBeenCalledWith( - traverseContext, - traverseContext[2], - '.$#3:0' - ); - - expectDev(console.error.calls.count()).toBe(1); - expectDev(console.error.calls.argsFor(0)[0]).toContain( - 'Warning: Using Maps as children is not yet fully supported. It is an ' + - 'experimental feature that might be removed. Convert it to a sequence ' + - '/ iterable of keyed ReactElements instead.' - ); - }); - - it('should not enumerate enumerable numbers (#4776)', () => { - /*eslint-disable no-extend-native */ - Number.prototype['@@iterator'] = function() { - throw new Error('number iterator called'); - }; - /*eslint-enable no-extend-native */ - - try { - var instance = ( -
- {5} - {12} - {13} -
- ); - - var traverseFn = jasmine.createSpy(); - - traverseAllChildren(instance.props.children, traverseFn, null); - expect(traverseFn.calls.count()).toBe(3); - - expect(traverseFn).toHaveBeenCalledWith( - null, - 5, - '.0' - ); - expect(traverseFn).toHaveBeenCalledWith( - null, - 12, - '.1' - ); - expect(traverseFn).toHaveBeenCalledWith( - null, - 13, - '.2' - ); - } finally { - delete Number.prototype['@@iterator']; - } - }); - - it('should allow extension of native prototypes', () => { - /*eslint-disable no-extend-native */ - String.prototype.key = 'react'; - Number.prototype.key = 'rocks'; - /*eslint-enable no-extend-native */ - - var instance = ( -
- {'a'} - {13} -
- ); - - var traverseFn = jasmine.createSpy(); - - traverseAllChildren(instance.props.children, traverseFn, null); - expect(traverseFn.calls.count()).toBe(2); - - expect(traverseFn).toHaveBeenCalledWith( - null, - 'a', - '.0' - ); - expect(traverseFn).toHaveBeenCalledWith( - null, - 13, - '.1' - ); - - delete String.prototype.key; - delete Number.prototype.key; - }); - - it('should throw on object', () => { - expect(function() { - traverseAllChildren({a: 1, b: 2}, function() {}, null); - }).toThrowError( - 'Objects are not valid as a React child (found: object with keys ' + - '{a, b}). If you meant to render a collection of children, use an ' + - 'array instead or wrap the object using createFragment(object) from ' + - 'the React add-ons.' - ); - }); - - it('should throw on regex', () => { - // Really, we care about dates (#4840) but those have nondeterministic - // serialization (timezones) so let's test a regex instead: - expect(function() { - traverseAllChildren(/abc/, function() {}, null); - }).toThrowError( - 'Objects are not valid as a React child (found: /abc/). If you meant ' + - 'to render a collection of children, use an array instead or wrap the ' + - 'object using createFragment(object) from the React add-ons.' - ); - }); - - it('should warn for using maps as children with owner info', () => { - spyOn(console, 'error'); - - class Parent extends React.Component { - render() { - return ( -
{new Map([['foo', 0], ['bar', 1]])}
- ); - } - } - - ReactTestUtils.renderIntoDocument(); - - expectDev(console.error.calls.count()).toBe(1); - expectDev(console.error.calls.argsFor(0)[0]).toBe( - 'Warning: Using Maps as children is not yet fully supported. It is an ' + - 'experimental feature that might be removed. Convert it to a sequence ' + - '/ iterable of keyed ReactElements instead. Check the render method of `Parent`.' - ); - }); -});