Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rudimentary test renderer #6944

Merged
merged 1 commit into from
Jun 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra jsdoc blocks (rootId and containerTag)

* @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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you help me understand why this is returning the component when it’s looking at the _renderedComponent all the way down? Is it because when there is no more _renderedComponent we have reached the bottom of that component hierarchy?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if A renders B renders div, I want the div and that div doesn't have _renderedComponent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌 that’s the scenario I was thinking. Thank you.

}


// =============================================================================

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