From 82f9bfec6ddf20386c8729e4636549697b5a1688 Mon Sep 17 00:00:00 2001 From: Scott Feeney Date: Wed, 29 Oct 2014 10:02:22 -0700 Subject: [PATCH] Basic shallow rendering support WIP to fix #2393. Doesn't yet support refs or updating. --- src/core/ReactCompositeComponent.js | 67 +++++++++++++++++++ src/test/ReactTestUtils.js | 80 +++++++++++++++++++++++ src/test/__tests__/ReactTestUtils-test.js | 64 ++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 src/test/__tests__/ReactTestUtils-test.js diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index 0c1f187e144..f72754a48b5 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -220,6 +220,73 @@ var ReactCompositeComponentMixin = { } ), + /** + * Renders the component shallowly, returning a ReactElement. + * + * @param {string} rootID DOM ID of the root node. + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction + * @param {number} mountDepth number of components in the owner hierarchy + * @return {ReactElement} Shallow rendering of the component. + * @final + * @internal + */ + _shallowMountComponent: function(rootID, transaction, mountDepth) { + ReactComponent.Mixin.mountComponent.call( + this, + rootID, + transaction, + mountDepth + ); + this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING; + + if (this.__reactAutoBindMap) { + this._bindAutoBindMethods(); + } + + this.context = this._processContext(this._currentElement._context); + this.props = this._processProps(this.props); + + this.state = this.getInitialState ? this.getInitialState() : null; + + if (__DEV__) { + // We allow auto-mocks to proceed as if they're returning null. + if (typeof this.state === 'undefined' && + this.getInitialState && this.getInitialState._isMockFunction) { + // This is probably bad practice. Consider warning here and + // deprecating this convenience. + this.state = null; + } + } + + invariant( + typeof this.state === 'object' && !Array.isArray(this.state), + '%s.getInitialState(): must return an object or null', + this.constructor.displayName || 'ReactCompositeComponent' + ); + + this._pendingState = null; + this._pendingForceUpdate = false; + + if (this.componentWillMount) { + this.componentWillMount(); + // When mounting, calls to `setState` by `componentWillMount` will set + // `this._pendingState` without triggering a re-render. + if (this._pendingState) { + this.state = this._pendingState; + this._pendingState = null; + } + } + + var renderedElement = this._renderValidatedComponent(); + // In a regular mount, we would then call instantiateReactComponent() on + // the above. For a shallow mount though, we want to stop here. + + // Done with mounting, `setState` will now trigger UI changes. + this._compositeLifeCycleState = null; + + return renderedElement; + }, + /** * Releases any resources allocated by `mountComponent`. * diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index 3e2deccc86a..0f3abe7e040 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -17,12 +17,14 @@ var EventPropagators = require('EventPropagators'); var React = require('React'); var ReactElement = require('ReactElement'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); +var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactMount = require('ReactMount'); var ReactTextComponent = require('ReactTextComponent'); var ReactUpdates = require('ReactUpdates'); var SyntheticEvent = require('SyntheticEvent'); var assign = require('Object.assign'); +var instantiateReactComponent = require('instantiateReactComponent'); var topLevelTypes = EventConstants.topLevelTypes; @@ -275,6 +277,46 @@ var ReactTestUtils = { ); }, + areElementsEquivalent: function(re1, re2) { + return ( + re1.type === re2.type && + re1.key === re2.key && + re1.ref === re2.ref && + ReactTestUtils._arePropsEquivalent(re1._store.props, re2._store.props) + ); + }, + + _arePropsEquivalent: function(props1, props2) { + // Is every key in props1 (except children) also in props2 and equal? + Object.keys(props1).forEach(function(key) { + if (key !== 'children' && props1[key] !== props2[key]) { + return false; + } + }); + + // Are there no keys unique to props2? + if (Object.keys(props1).length !== Object.keys(props2).length) { + return false; + } + + // If neither element's props has children, we're good. + if (props1.children == null && props2.children == null) { + return true; + } + + // Lastly, if they do have children, compare each. + if (props1.children.length !== props2.children.length) { + return false; + } + for (var i = 0; i < props1.children.length; i++) { + if (!ReactTestUtils.areElementsEquivalent(props1.children[i], + props2.children[i])) { + return false; + } + } + return true; + }, + nativeTouchData: function(x, y) { return { touches: [ @@ -283,10 +325,48 @@ var ReactTestUtils = { }; }, + createRenderer: function() { + return new ReactShallowRenderer(); + }, + Simulate: null, SimulateNative: {} }; +/** + * @class ReactShallowRenderer + */ +var ReactShallowRenderer = function() {}; + +ReactShallowRenderer.prototype.getRenderOutput = function() { + return this._renderOutput; +}; + +ReactShallowRenderer.prototype.render = function(element) { + var instance = instantiateReactComponent(element, null); + var rootID = ReactInstanceHandles.createReactRootID(); + + // transaction stuff copied from ReactComponent, mountComponentIntoNode + var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); + transaction.perform( + this._render, + this, + element, + instance, + rootID, + transaction + ); + ReactUpdates.ReactReconcileTransaction.release(transaction); +}; + +ReactShallowRenderer.prototype._render = function( + element, + instance, + rootID, + transaction) { + this._renderOutput = instance._shallowMountComponent(rootID, transaction, 0); +}; + /** * Exports: * diff --git a/src/test/__tests__/ReactTestUtils-test.js b/src/test/__tests__/ReactTestUtils-test.js new file mode 100644 index 00000000000..591b070ddc5 --- /dev/null +++ b/src/test/__tests__/ReactTestUtils-test.js @@ -0,0 +1,64 @@ +/** + * Copyright 2013-2014, 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"; + +var React; +var ReactTestUtils; + +var mocks; +var warn; + + +describe('ReactTestUtils', function() { + + beforeEach(function() { + mocks = require('mocks'); + + React = require('React'); + ReactTestUtils = require('ReactTestUtils'); + + warn = console.warn; + console.warn = mocks.getMockFunction(); + }); + + afterEach(function() { + console.warn = warn; + }); + + it('should have shallow rendering in the test utils', function() { + var SomeComponent = React.createClass({ + render: function() { + return ( +
+ + +
+ ); + } + }); + + var shallowRenderer = ReactTestUtils.createRenderer(); + shallowRenderer.render(); + // shallowRenderer.attachRef('myRefName', someMock); + + var result = shallowRenderer.getRenderOutput(); + + var expected = ( +
+ + +
+ ); + + expect(ReactTestUtils.areElementsEquivalent(result, expected)).toBe(true); + }); +});