Skip to content

Commit

Permalink
Allow to render child custom component in shallow renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
lasekio committed Nov 20, 2015
1 parent acabb22 commit 20088d7
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 37 deletions.
36 changes: 18 additions & 18 deletions src/renderers/shared/reconciler/ReactChildReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -55,7 +55,7 @@ var ReactChildReconciler = {
return null;
}
var childInstances = {};
traverseAllChildren(nestedChildNodes, instantiateChild, childInstances);
traverseAllChildren(nestedChildNodes, this.instantiateChild.bind(this), childInstances);
return childInstances;
},

Expand Down
16 changes: 8 additions & 8 deletions src/renderers/shared/reconciler/ReactMultiChild.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,21 +191,21 @@ 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 {
ReactCurrentOwner.current = null;
}
}
}
return ReactChildReconciler.instantiateChildren(
return this._childReconciler.instantiateChildren(
nestedChildren, transaction, context
);
},
Expand All @@ -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
);
},
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -417,7 +417,7 @@ var ReactMultiChild = {
*/
unmountChildren: function() {
var renderedChildren = this._renderedChildren;
ReactChildReconciler.unmountChildren(renderedChildren);
this._childReconciler.unmountChildren(renderedChildren);
this._renderedChildren = null;
},

Expand Down
129 changes: 118 additions & 11 deletions src/test/ReactTestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand All @@ -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() {
Expand Down Expand Up @@ -401,17 +432,92 @@ NoopInternalComponent.prototype = {
},
};

var instantiateReactComponent = function(node) {
var instance;

if (node === null || node && typeof node.type === 'string' || typeof node === 'string' || typeof node === '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();
},
}
);

Expand All @@ -436,6 +542,7 @@ ReactShallowRenderer.prototype.render = function(element, context) {
context = emptyObject;
}
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(true);
transaction.useCreateElement = false;
this._render(element, transaction, context);
ReactUpdates.ReactReconcileTransaction.release(transaction);
};
Expand Down
52 changes: 52 additions & 0 deletions src/test/__tests__/ReactTestUtils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<div>
<ChildComponent />
</div>);
},
});

var shallowRenderer = ReactTestUtils.createRenderer();
shallowRenderer.render(<SomeComponent />);
var result = shallowRenderer.getRenderOutput();

expect(result).toEqual(<div>
<ChildComponent />
</div>);

expect(renderSpy).not.toHaveBeenCalled();
});

it('can shallowly render custom components which are on white list', function() {
var ChildComponent = React.createClass({
render: function() {
return <div className="child"></div>;
},
});

ReactTestUtils.renderComponentsAsChild.push(ChildComponent);

var SomeComponent = React.createClass({
render: function() {
return (<div>
<ChildComponent />
</div>);
},
});

var shallowRenderer = ReactTestUtils.createRenderer();
shallowRenderer.render(<SomeComponent />);
var result = shallowRenderer.getRenderOutput();

expect(result).toEqual(<div>
<div className="child"></div>
</div>);
});

it('can scryRenderedDOMComponentsWithClass with className contains \\n', function() {
var Wrapper = React.createClass({
render: function() {
Expand Down

0 comments on commit 20088d7

Please sign in to comment.