diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index b0284cc7ae9d1..2ead20ee739ac 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -650,6 +650,25 @@ var ReactCompositeComponentMixin = assign({}, } }, + /** + * @private + */ + _renderValidatedComponentWithoutOwnerOrContext: function() { + var inst = this._instance; + var renderedComponent = inst.render(); + if (__DEV__) { + // We allow auto-mocks to proceed as if they're returning null. + if (typeof renderedComponent === 'undefined' && + inst.render._isMockFunction) { + // This is probably bad practice. Consider warning here and + // deprecating this convenience. + renderedComponent = null; + } + } + + return renderedComponent; + }, + /** * @private */ @@ -665,16 +684,8 @@ var ReactCompositeComponentMixin = assign({}, ReactCurrentOwner.current = this; var inst = this._instance; try { - renderedComponent = inst.render(); - if (__DEV__) { - // We allow auto-mocks to proceed as if they're returning null. - if (typeof renderedComponent === 'undefined' && - inst.render._isMockFunction) { - // This is probably bad practice. Consider warning here and - // deprecating this convenience. - renderedComponent = null; - } - } + renderedComponent = + this._renderValidatedComponentWithoutOwnerOrContext(); } finally { ReactContext.current = previousContext; ReactCurrentOwner.current = null; @@ -734,11 +745,124 @@ var ReactCompositeComponentMixin = assign({}, }); +var ShallowMixin = assign({}, + ReactCompositeComponentMixin, { + + /** + * Initializes the component, renders markup, and registers event listeners. + * + * @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 + */ + mountComponent: function(rootID, transaction, mountDepth) { + ReactComponent.Mixin.mountComponent.call( + this, + rootID, + transaction, + mountDepth + ); + + var inst = this._instance; + + // Store a reference from the instance back to the internal representation + ReactInstanceMap.set(inst, this); + + this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING; + + // No context for shallow-mounted components. + inst.props = this._processProps(this._currentElement.props); + + var initialState = inst.getInitialState ? inst.getInitialState() : null; + if (__DEV__) { + // We allow auto-mocks to proceed as if they're returning null. + if (typeof initialState === 'undefined' && + inst.getInitialState._isMockFunction) { + // This is probably bad practice. Consider warning here and + // deprecating this convenience. + initialState = null; + } + } + invariant( + typeof initialState === 'object' && !Array.isArray(initialState), + '%s.getInitialState(): must return an object or null', + inst.constructor.displayName || 'ReactCompositeComponent' + ); + inst.state = initialState; + + this._pendingState = null; + this._pendingForceUpdate = false; + + if (inst.componentWillMount) { + inst.componentWillMount(); + // When mounting, calls to `setState` by `componentWillMount` will set + // `this._pendingState` without triggering a re-render. + if (this._pendingState) { + inst.state = this._pendingState; + this._pendingState = null; + } + } + + // No recursive call to instantiateReactComponent for shallow rendering. + this._renderedComponent = + this._renderValidatedComponentWithoutOwnerOrContext(); + + // Done with mounting, `setState` will now trigger UI changes. + this._compositeLifeCycleState = null; + + // No call to this._renderedComponent.mountComponent for shallow + // rendering. + + if (inst.componentDidMount) { + transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); + } + + return this._renderedComponent; + }, + + /** + * Call the component's `render` method and update the DOM accordingly. + * + * @param {ReactReconcileTransaction} transaction + * @internal + */ + _updateRenderedComponent: function(transaction) { + var prevComponentInstance = this._renderedComponent; + var prevRenderedElement = prevComponentInstance._currentElement; + // Use the without-owner-or-context variant of _rVC below: + var nextRenderedElement = this._renderValidatedComponentWithoutOwnerOrContext(); + if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) { + prevComponentInstance.receiveComponent( + nextRenderedElement, + transaction + ); + } else { + // These two IDs are actually the same! But nothing should rely on that. + var thisID = this._rootNodeID; + var prevComponentID = prevComponentInstance._rootNodeID; + // Don't unmount previous instance since it was never mounted, due to + // shallow render. + //prevComponentInstance.unmountComponent(); + this._renderedComponent = nextRenderedElement; + // ^ no instantiateReactComponent + // + // no recursive mountComponent + return nextRenderedElement; + } + } + +}); + var ReactCompositeComponent = { LifeCycle: CompositeLifeCycle, - Mixin: ReactCompositeComponentMixin + Mixin: ReactCompositeComponentMixin, + + ShallowMixin: ShallowMixin }; diff --git a/src/core/ReactElement.js b/src/core/ReactElement.js index b7967f7f2d03d..013bc3eac26f8 100644 --- a/src/core/ReactElement.js +++ b/src/core/ReactElement.js @@ -106,7 +106,21 @@ var ReactElement = function(type, key, ref, owner, context, props) { // an external backing store so that we can freeze the whole object. // This can be replaced with a WeakMap once they are implemented in // commonly used development environments. - this._store = { validated: false, props: props }; + this._store = { props: props }; + + // To make comparing ReactElements easier for testing purposes, we make + // the validation flag non-enumerable (where possible, which should + // include every environment we run tests in), so the test framework + // ignores it. + try { + Object.defineProperty(this._store, 'validated', { + configurable: false, + enumerable: false, + writable: true + }); + } catch (x) { + } + this._store.validated = false; // We're not allowed to set props directly on the object so we early // return and rely on the prototype membrane to forward to the backing diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index 0527174f1e283..d1069f7dd5b31 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -17,12 +17,15 @@ var EventPropagators = require('EventPropagators'); var React = require('React'); var ReactElement = require('ReactElement'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); +var ReactCompositeComponent = require('ReactCompositeComponent'); +var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactInstanceMap = require('ReactInstanceMap'); var ReactMount = require('ReactMount'); var ReactUpdates = require('ReactUpdates'); var SyntheticEvent = require('SyntheticEvent'); var assign = require('Object.assign'); +var instantiateReactComponent = require('instantiateReactComponent'); var topLevelTypes = EventConstants.topLevelTypes; @@ -298,10 +301,53 @@ var ReactTestUtils = { }; }, + createRenderer: function() { + return new ReactShallowRenderer(); + }, + Simulate: null, SimulateNative: {} }; +/** + * @class ReactShallowRenderer + */ +var ReactShallowRenderer = function() { + this._instance = null; +}; + +ReactShallowRenderer.prototype.getRenderOutput = function() { + return (this._instance && this._instance._renderedComponent) || null; +}; + +var ShallowComponentWrapper = function(inst) { + this._instance = inst; +} +assign( + ShallowComponentWrapper.prototype, + ReactCompositeComponent.ShallowMixin +); + +ReactShallowRenderer.prototype.render = function(element) { + var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); + this._render(element, transaction); + ReactUpdates.ReactReconcileTransaction.release(transaction); +}; + +ReactShallowRenderer.prototype._render = function(element, transaction) { + if (!this._instance) { + var rootID = ReactInstanceHandles.createReactRootID(); + var instance = new ShallowComponentWrapper(new element.type(element.props)); + instance.construct(element); + + instance.mountComponent(rootID, transaction, 0); + + this._instance = instance; + } else { + this._instance.receiveComponent(element, transaction); + } +}; + /** * Exports: * diff --git a/src/test/__tests__/ReactTestUtils-test.js b/src/test/__tests__/ReactTestUtils-test.js new file mode 100644 index 0000000000000..a68ffbf26581b --- /dev/null +++ b/src/test/__tests__/ReactTestUtils-test.js @@ -0,0 +1,113 @@ +/** + * 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', function() { + var SomeComponent = React.createClass({ + render: function() { + return ( +