Skip to content

Commit

Permalink
Add rudimentary test renderer (#6944)
Browse files Browse the repository at this point in the history
(cherry picked from commit 50982ce)
  • Loading branch information
sophiebits authored and zpao committed Jun 14, 2016
1 parent 39ddfdd commit 90007f7
Show file tree
Hide file tree
Showing 4 changed files with 488 additions and 0 deletions.
155 changes: 155 additions & 0 deletions src/renderers/testing/ReactTestMount.js
Original file line number Diff line number Diff line change
@@ -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;
103 changes: 103 additions & 0 deletions src/renderers/testing/ReactTestReconcileTransaction.js
Original file line number Diff line number Diff line change
@@ -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<object>} List of operation wrap procedures.
* TODO: convert to array<TransactionWrapper>
*/
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;
133 changes: 133 additions & 0 deletions src/renderers/testing/ReactTestRenderer.js
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 90007f7

Please sign in to comment.