From 5daa0c3d744434727f9386cb9ee503666636420b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awomir=20Laskowski?= Date: Fri, 20 Nov 2015 03:51:00 +0100 Subject: [PATCH] Allow to render child custom component in shallow renderer --- .../shared/reconciler/ReactChildReconciler.js | 36 ++--- .../shared/reconciler/ReactMultiChild.js | 16 +-- src/test/ReactTestUtils.js | 132 ++++++++++++++++-- src/test/__tests__/ReactTestUtils-test.js | 52 +++++++ 4 files changed, 199 insertions(+), 37 deletions(-) diff --git a/src/renderers/shared/reconciler/ReactChildReconciler.js b/src/renderers/shared/reconciler/ReactChildReconciler.js index 2b2a63a66610b..f291bc89456b8 100644 --- a/src/renderers/shared/reconciler/ReactChildReconciler.js +++ b/src/renderers/shared/reconciler/ReactChildReconciler.js @@ -19,29 +19,29 @@ var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); var traverseAllChildren = require('traverseAllChildren'); var warning = require('warning'); -function instantiateChild(childInstances, child, name) { - // We found a component instance. - var keyUnique = (childInstances[name] === undefined); - if (__DEV__) { - warning( - keyUnique, - 'flattenChildren(...): Encountered two children with the same key, ' + - '`%s`. Child keys must be unique; when two children share a key, only ' + - 'the first child will be used.', - name - ); - } - if (child != null && keyUnique) { - childInstances[name] = instantiateReactComponent(child, null); - } -} - /** * ReactChildReconciler provides helpers for initializing or updating a set of * children. Its output is suitable for passing it onto ReactMultiChild which * does diffed reordering and insertion. */ var ReactChildReconciler = { + _instantiateReactComponent: instantiateReactComponent, + instantiateChild: function(childInstances, child, name) { + // We found a component instance. + var keyUnique = (childInstances[name] === undefined); + if (__DEV__) { + warning( + keyUnique, + 'flattenChildren(...): Encountered two children with the same key, ' + + '`%s`. Child keys must be unique; when two children share a key, only ' + + 'the first child will be used.', + name + ); + } + if (child != null && keyUnique) { + childInstances[name] = this._instantiateReactComponent(child, null); + } + }, /** * Generates a "mount image" for each of the supplied children. In the case * of `ReactDOMComponent`, a mount image is a string of markup. @@ -55,7 +55,7 @@ var ReactChildReconciler = { return null; } var childInstances = {}; - traverseAllChildren(nestedChildNodes, instantiateChild, childInstances); + traverseAllChildren(nestedChildNodes, this.instantiateChild.bind(this), childInstances); return childInstances; }, diff --git a/src/renderers/shared/reconciler/ReactMultiChild.js b/src/renderers/shared/reconciler/ReactMultiChild.js index 39c5a7a2fde7c..1d6ee26007886 100644 --- a/src/renderers/shared/reconciler/ReactMultiChild.js +++ b/src/renderers/shared/reconciler/ReactMultiChild.js @@ -191,13 +191,13 @@ var ReactMultiChild = { * @lends {ReactMultiChild.prototype} */ Mixin: { - + _childReconciler: ReactChildReconciler, _reconcilerInstantiateChildren: function(nestedChildren, transaction, context) { if (__DEV__) { if (this._currentElement) { try { ReactCurrentOwner.current = this._currentElement._owner; - return ReactChildReconciler.instantiateChildren( + return this._childReconciler.instantiateChildren( nestedChildren, transaction, context ); } finally { @@ -205,7 +205,7 @@ var ReactMultiChild = { } } } - return ReactChildReconciler.instantiateChildren( + return this._childReconciler.instantiateChildren( nestedChildren, transaction, context ); }, @@ -220,13 +220,13 @@ var ReactMultiChild = { } finally { ReactCurrentOwner.current = null; } - return ReactChildReconciler.updateChildren( + return this._childReconciler.updateChildren( prevChildren, nextChildren, transaction, context ); } } nextChildren = flattenChildren(nextNestedChildrenElements); - return ReactChildReconciler.updateChildren( + return this._childReconciler.updateChildren( prevChildren, nextChildren, transaction, context ); }, @@ -275,7 +275,7 @@ var ReactMultiChild = { try { var prevChildren = this._renderedChildren; // Remove any rendered children. - ReactChildReconciler.unmountChildren(prevChildren); + this._childReconciler.unmountChildren(prevChildren); // TODO: The setTextContent operation should be enough for (var name in prevChildren) { if (prevChildren.hasOwnProperty(name)) { @@ -309,7 +309,7 @@ var ReactMultiChild = { try { var prevChildren = this._renderedChildren; // Remove any rendered children. - ReactChildReconciler.unmountChildren(prevChildren); + this._childReconciler.unmountChildren(prevChildren); for (var name in prevChildren) { if (prevChildren.hasOwnProperty(name)) { this._unmountChild(prevChildren[name]); @@ -417,7 +417,7 @@ var ReactMultiChild = { */ unmountChildren: function() { var renderedChildren = this._renderedChildren; - ReactChildReconciler.unmountChildren(renderedChildren); + this._childReconciler.unmountChildren(renderedChildren); this._renderedChildren = null; }, diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index 751c5de44c858..01b02c174298f 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -29,6 +29,9 @@ var assign = require('Object.assign'); var emptyObject = require('emptyObject'); var findDOMNode = require('findDOMNode'); var invariant = require('invariant'); +var originInstantiateReactComponent = require('instantiateReactComponent'); +var ReactMultiChild = require('ReactMultiChild'); +var ReactChildReconciler = require('ReactChildReconciler'); var topLevelTypes = EventConstants.topLevelTypes; @@ -76,6 +79,7 @@ function findAllInRenderedTreeInternal(inst, test) { * @lends ReactTestUtils */ var ReactTestUtils = { + renderComponentsAsChild: [], renderIntoDocument: function(instance) { var div = document.createElement('div'); // None of our tests actually require attaching the container to the @@ -355,6 +359,37 @@ var ReactTestUtils = { SimulateNative: {}, }; +function rebuildElementsFromRenderedComponent(component) { + if (!component._currentElement) { + return null; + } + + if (typeof component._currentElement === 'string') { + return component._currentElement; + } + + var props = assign({}, component._currentElement.props); + + if (props.children) { + delete props.children; + } + + if (ReactTestUtils.renderComponentsAsChild.indexOf(component._currentElement.type) !== -1) { + return rebuildElementsFromRenderedComponent(component._renderedComponent); + } + + var args = [component._currentElement.type, props]; + + for (var key in component._renderedChildren) { + var childComponent = component._renderedChildren[key]; + var element = rebuildElementsFromRenderedComponent(childComponent); + + args.push(element); + } + + return React.createElement.apply(null, args); +} + /** * @class ReactShallowRenderer */ @@ -363,11 +398,7 @@ var ReactShallowRenderer = function() { }; ReactShallowRenderer.prototype.getRenderOutput = function() { - return ( - (this._instance && this._instance._renderedComponent && - this._instance._renderedComponent._renderedOutput) - || null - ); + return rebuildElementsFromRenderedComponent(this._instance._renderedComponent); }; ReactShallowRenderer.prototype.getMountedInstance = function() { @@ -401,17 +432,96 @@ NoopInternalComponent.prototype = { }, }; +var instantiateReactComponent = function(node) { + var instance; + + if (node === null || + node && + typeof node === 'string' || + typeof node.type === 'string' || + typeof node === 'number') { + instance = new ShallowDOMComponentWrapper(node); + + instance.construct(node); + + return instance; + } else if (node && typeof node.type === 'function') { + + if (ReactTestUtils.renderComponentsAsChild.indexOf(node.type) !== -1) { + instance = new ShallowComponentWrapper(node.type); + + instance.construct(node); + } else { + instance = new NoopInternalComponent(node); + } + + return instance; + } + + var component = originInstantiateReactComponent.apply(this, arguments); + + return component; +}; + var ShallowComponentWrapper = function() { }; + assign( ShallowComponentWrapper.prototype, ReactCompositeComponent.Mixin, { - _instantiateReactComponent: function(element) { - return new NoopInternalComponent(element); - }, + _instantiateReactComponent: instantiateReactComponent, _replaceNodeWithMarkup: function() {}, - _renderValidatedComponent: - ReactCompositeComponent.Mixin - ._renderValidatedComponentWithoutOwnerOrContext, + getNativeNode: function() {}, + } +); + +var ShallowDOMComponentWrapper = function() {}; + +assign( + ShallowDOMComponentWrapper.prototype, + ShallowComponentWrapper.prototype, + ReactMultiChild.Mixin, + { + _childReconciler: assign( + {}, + ReactChildReconciler, + { + _instantiateReactComponent: instantiateReactComponent, + } + ), + + mountComponent: function( + transaction, + nativeParent, + nativeContainerInfo, + context + ) { + if (typeof this._currentElement !== 'object' || + this._currentElement === null) { + return; + } + + var props = this._currentElement.props; + + this.mountChildren( + props.children, + transaction, + context + ); + }, + + receiveComponent: function(nextElement, transaction, context) { + if (typeof this._currentElement !== 'object' || + this._currentElement === null) { + + return; + } + this._currentElement = nextElement; + this.updateChildren(nextElement.props.children, transaction, context); + }, + + unmountComponent: function() { + this.unmountChildren(); + }, } ); diff --git a/src/test/__tests__/ReactTestUtils-test.js b/src/test/__tests__/ReactTestUtils-test.js index 78f6f0ca921fb..ca0f2c9b5f8f6 100644 --- a/src/test/__tests__/ReactTestUtils-test.js +++ b/src/test/__tests__/ReactTestUtils-test.js @@ -257,6 +257,58 @@ describe('ReactTestUtils', function() { }); + it('should not shallowly render custom components by default', function() { + var renderSpy = jasmine.createSpy('ChildComponent.render'); + + var ChildComponent = React.createClass({ + render: renderSpy, + }); + + var SomeComponent = React.createClass({ + render: function() { + return (
+ +
); + }, + }); + + var shallowRenderer = ReactTestUtils.createRenderer(); + shallowRenderer.render(); + var result = shallowRenderer.getRenderOutput(); + + expect(result).toEqual(
+ +
); + + expect(renderSpy).not.toHaveBeenCalled(); + }); + + it('can shallowly render custom components which are on white list', function() { + var ChildComponent = React.createClass({ + render: function() { + return
; + }, + }); + + ReactTestUtils.renderComponentsAsChild.push(ChildComponent); + + var SomeComponent = React.createClass({ + render: function() { + return (
+ +
); + }, + }); + + var shallowRenderer = ReactTestUtils.createRenderer(); + shallowRenderer.render(); + var result = shallowRenderer.getRenderOutput(); + + expect(result).toEqual(
+
+
); + }); + it('can scryRenderedDOMComponentsWithClass with className contains \\n', function() { var Wrapper = React.createClass({ render: function() {