diff --git a/src/renderers/testing/ReactTestMount.js b/src/renderers/testing/ReactTestMount.js new file mode 100644 index 0000000000000..517d1c0839a1c --- /dev/null +++ b/src/renderers/testing/ReactTestMount.js @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule ReactTestMount + * @flow + */ +'use strict'; + +var ReactElement = require('ReactElement'); +var ReactInstrumentation = require('ReactInstrumentation'); +var ReactReconciler = require('ReactReconciler'); +var ReactUpdates = require('ReactUpdates'); + +var emptyObject = require('emptyObject'); +var getHostComponentFromComposite = require('getHostComponentFromComposite'); +var instantiateReactComponent = require('instantiateReactComponent'); + +/** + * Temporary (?) hack so that we can store all top-level pending updates on + * composites instead of having to worry about different types of components + * here. + */ +var TopLevelWrapper = function() {}; +TopLevelWrapper.prototype.isReactComponent = {}; +if (__DEV__) { + TopLevelWrapper.displayName = 'TopLevelWrapper'; +} +TopLevelWrapper.prototype.render = function() { + // this.props is actually a ReactElement + return this.props; +}; + +/** + * Mounts this component and inserts it into the DOM. + * + * @param {ReactComponent} componentInstance The instance to mount. + * @param {number} rootID ID of the root node. + * @param {number} containerTag container element to mount into. + * @param {ReactReconcileTransaction} transaction + */ +function mountComponentIntoNode( + componentInstance, + transaction) { + var image = ReactReconciler.mountComponent( + componentInstance, + transaction, + null, + null, + emptyObject + ); + componentInstance._renderedComponent._topLevelWrapper = componentInstance; + return image; +} + +/** + * Batched mount. + * + * @param {ReactComponent} componentInstance The instance to mount. + * @param {number} rootID ID of the root node. + * @param {number} containerTag container element to mount into. + */ +function batchedMountComponentIntoNode( + componentInstance) { + var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); + var image = transaction.perform( + mountComponentIntoNode, + null, + componentInstance, + transaction + ); + ReactUpdates.ReactReconcileTransaction.release(transaction); + return image; +} + +var ReactTestInstance = function(component) { + this._component = component; +}; +ReactTestInstance.prototype.getInstance = function() { + return this._component._renderedComponent.getPublicInstance(); +}; +ReactTestInstance.prototype.toJSON = function() { + var inst = getHostComponentFromComposite(this._component); + return inst.toJSON(); +}; + +/** + * As soon as `ReactMount` is refactored to not rely on the DOM, we can share + * code between the two. For now, we'll hard code the ID logic. + */ +var ReactHostMount = { + + render: function( + nextElement: ReactElement + ): ?ReactComponent { + var nextWrappedElement = new ReactElement( + TopLevelWrapper, + null, + null, + null, + null, + null, + nextElement + ); + + // var prevComponent = ReactHostMount._instancesByContainerID[containerTag]; + // if (prevComponent) { + // var prevWrappedElement = prevComponent._currentElement; + // var prevElement = prevWrappedElement.props; + // if (shouldUpdateReactComponent(prevElement, nextElement)) { + // ReactUpdateQueue.enqueueElementInternal(prevComponent, nextWrappedElement); + // if (callback) { + // ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback); + // } + // return prevComponent; + // } + // } + + var instance = instantiateReactComponent(nextWrappedElement); + + if (__DEV__) { + // Mute future events from the top level wrapper. + // It is an implementation detail that devtools should not know about. + instance._debugID = 0; + + if (__DEV__) { + ReactInstrumentation.debugTool.onBeginFlush(); + } + } + + // The initial render is synchronous but any updates that happen during + // rendering, in componentWillMount or componentDidMount, will be batched + // according to the current batching strategy. + + ReactUpdates.batchedUpdates( + batchedMountComponentIntoNode, + instance + ); + if (__DEV__) { + // The instance here is TopLevelWrapper so we report mount for its child. + ReactInstrumentation.debugTool.onMountRootComponent( + instance._renderedComponent._debugID + ); + ReactInstrumentation.debugTool.onEndFlush(); + } + return new ReactTestInstance(instance); + }, + +}; + +module.exports = ReactHostMount; diff --git a/src/renderers/testing/ReactTestReconcileTransaction.js b/src/renderers/testing/ReactTestReconcileTransaction.js new file mode 100644 index 0000000000000..94716e59d8608 --- /dev/null +++ b/src/renderers/testing/ReactTestReconcileTransaction.js @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule ReactTestReconcileTransaction + * @flow + */ +'use strict'; + +var CallbackQueue = require('CallbackQueue'); +var PooledClass = require('PooledClass'); +var Transaction = require('Transaction'); + +/** + * Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks during + * the performing of the transaction. + */ +var ON_DOM_READY_QUEUEING = { + /** + * Initializes the internal `onDOMReady` queue. + */ + initialize: function() { + this.reactMountReady.reset(); + }, + + /** + * After DOM is flushed, invoke all registered `onDOMReady` callbacks. + */ + close: function() { + this.reactMountReady.notifyAll(); + }, +}; + +/** + * Executed within the scope of the `Transaction` instance. Consider these as + * being member methods, but with an implied ordering while being isolated from + * each other. + */ +var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING]; + +/** + * Currently: + * - The order that these are listed in the transaction is critical: + * - Suppresses events. + * - Restores selection range. + * + * Future: + * - Restore document/overflow scroll positions that were unintentionally + * modified via DOM insertions above the top viewport boundary. + * - Implement/integrate with customized constraint based layout system and keep + * track of which dimensions must be remeasured. + * + * @class ReactTestReconcileTransaction + */ +function ReactTestReconcileTransaction() { + this.reinitializeTransaction(); + this.reactMountReady = CallbackQueue.getPooled(null); +} + +var Mixin = { + /** + * @see Transaction + * @abstract + * @final + * @return {array} List of operation wrap procedures. + * TODO: convert to array + */ + getTransactionWrappers: function() { + return TRANSACTION_WRAPPERS; + }, + + /** + * @return {object} The queue to collect `onDOMReady` callbacks with. + * TODO: convert to ReactMountReady + */ + getReactMountReady: function() { + return this.reactMountReady; + }, + + /** + * `PooledClass` looks for this, and will invoke this before allowing this + * instance to be reused. + */ + destructor: function() { + CallbackQueue.release(this.reactMountReady); + this.reactMountReady = null; + }, +}; + +Object.assign( + ReactTestReconcileTransaction.prototype, + Transaction.Mixin, + ReactTestReconcileTransaction, + Mixin +); + +PooledClass.addPoolingTo(ReactTestReconcileTransaction); + +module.exports = ReactTestReconcileTransaction; diff --git a/src/renderers/testing/ReactTestRenderer.js b/src/renderers/testing/ReactTestRenderer.js new file mode 100644 index 0000000000000..56785d7f972c9 --- /dev/null +++ b/src/renderers/testing/ReactTestRenderer.js @@ -0,0 +1,133 @@ +/** + * 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. + * + * @providesModule ReactTestRenderer + */ + +'use strict'; + +var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); +var ReactEmptyComponent = require('ReactEmptyComponent'); +var ReactMultiChild = require('ReactMultiChild'); +var ReactHostComponent = require('ReactHostComponent'); +var ReactTestMount = require('ReactTestMount'); +var ReactTestReconcileTransaction = require('ReactTestReconcileTransaction'); +var ReactUpdates = require('ReactUpdates'); + +var renderSubtreeIntoContainer = require('renderSubtreeIntoContainer'); + +/** + * Drill down (through composites and empty components) until we get a native or + * native text component. + * + * This is pretty polymorphic but unavoidable with the current structure we have + * for `_renderedChildren`. + */ +function getRenderedHostOrTextFromComponent(component) { + var rendered; + while ((rendered = component._renderedComponent)) { + component = rendered; + } + return component; +} + + +// ============================================================================= + +var ReactTestComponent = function(element) { + this._currentElement = element; + this._renderedChildren = null; + this._topLevelWrapper = null; +}; +ReactTestComponent.prototype.mountComponent = function( + transaction, + nativeParent, + nativeContainerInfo, + context +) { + var element = this._currentElement; + this.mountChildren(element.props.children, transaction, context); +}; +ReactTestComponent.prototype.receiveComponent = function( + nextElement, + transaction, + context +) { + this._currentElement = nextElement; + this.updateChildren(nextElement.props.children, transaction, context); +}; +ReactTestComponent.prototype.getHostNode = function() {}; +ReactTestComponent.prototype.unmountComponent = function() {}; +ReactTestComponent.prototype.toJSON = function() { + var {children, ...props} = this._currentElement.props; + var childrenJSON = []; + for (var key in this._renderedChildren) { + var inst = this._renderedChildren[key]; + inst = getRenderedHostOrTextFromComponent(inst); + var json = inst.toJSON(); + if (json !== undefined) { + childrenJSON.push(json); + } + } + return { + type: this._currentElement.type, + props: props, + children: childrenJSON.length ? childrenJSON : null, + }; +}; +Object.assign(ReactTestComponent.prototype, ReactMultiChild.Mixin); + +// ============================================================================= + +var ReactTestTextComponent = function(element) { + this._currentElement = element; +}; +ReactTestTextComponent.prototype.mountComponent = function() {}; +ReactTestTextComponent.prototype.receiveComponent = function(nextElement) { + this._currentElement = nextElement; +}; +ReactTestTextComponent.prototype.getHostNode = function() {}; +ReactTestTextComponent.prototype.unmountComponent = function() {}; +ReactTestTextComponent.prototype.toJSON = function() { + return this._currentElement; +}; + +// ============================================================================= + +var ReactTestEmptyComponent = function(element) { + this._currentElement = null; +}; +ReactTestEmptyComponent.prototype.mountComponent = function() {}; +ReactTestEmptyComponent.prototype.receiveComponent = function() {}; +ReactTestEmptyComponent.prototype.getHostNode = function() {}; +ReactTestEmptyComponent.prototype.unmountComponent = function() {}; +ReactTestEmptyComponent.prototype.toJSON = function() {}; + +// ============================================================================= + +ReactUpdates.injection.injectReconcileTransaction( + ReactTestReconcileTransaction +); +ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy); + +ReactHostComponent.injection.injectGenericComponentClass(ReactTestComponent); +ReactHostComponent.injection.injectTextComponentClass(ReactTestTextComponent); +ReactEmptyComponent.injection.injectEmptyComponentFactory(function() { + return new ReactTestEmptyComponent(); +}); + +var ReactTestRenderer = { + create: ReactTestMount.render, + + /* eslint-disable camelcase */ + unstable_batchedUpdates: ReactUpdates.batchedUpdates, + unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer, + /* eslint-enable camelcase */ +}; + +module.exports = ReactTestRenderer; diff --git a/src/renderers/testing/__tests__/ReactTestRenderer-test.js b/src/renderers/testing/__tests__/ReactTestRenderer-test.js new file mode 100644 index 0000000000000..5e233226b3905 --- /dev/null +++ b/src/renderers/testing/__tests__/ReactTestRenderer-test.js @@ -0,0 +1,97 @@ +/** + * 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'; + +var React = require('React'); +var ReactTestRenderer = require('ReactTestRenderer'); + +describe('ReactTestRenderer', function() { + + it('renders a simple component', function() { + function Link() { + return ; + } + var renderer = ReactTestRenderer.create(); + expect(renderer.toJSON()).toEqual({ + type: 'a', + props: { role: 'link' }, + children: null, + }); + }); + + it('renders some basics with an update', function() { + var renders = 0; + var Component = React.createClass({ + getInitialState: function() { + return {x: 3}; + }, + render: function() { + renders++; + return ( +
+ {this.state.x} + + +
+ ); + }, + componentDidMount: function() { + this.setState({x: 7}); + }, + }); + + var Child = () => (renders++, ); + var Null = () => (renders++, null); + + var renderer = ReactTestRenderer.create(); + expect(renderer.toJSON()).toEqual({ + type: 'div', + props: { className: 'purple' }, + children: [ + 7, + { type: 'moo', props: {}, children: null }, + ], + }); + expect(renders).toBe(6); + }); + + it('exposes the instance', function() { + class Mouse extends React.Component { + constructor() { + super(); + this.state = {mouse: 'mouse'}; + } + handleMoose() { + this.setState({mouse: 'moose'}); + } + render() { + return
{this.state.mouse}
; + } + } + var renderer = ReactTestRenderer.create(); + + expect(renderer.toJSON()).toEqual({ + type: 'div', + props: {}, + children: ['mouse'], + }); + + var mouse = renderer.getInstance(); + mouse.handleMoose(); + expect(renderer.toJSON()).toEqual({ + type: 'div', + props: {}, + children: ['moose'], + }); + }); + +});