Implement createNodeMock for ReactTestRenderer (#7649)

* Implement optional mockConfig and getMockRef

* default mockConfig, walk render tree

* Pass mockConfig to transaction

* Attach mockConfig to transaction

* type mockConfig in ReactRef

* Expect object in native component ref test

* Fix argument name for attachRefs

* Add mockConfig support to legacy refs

* Pass transaction to getPublicInstance

* Implement getMockConfig on ReactTestReconcileTransaction

* Merge defaultMockConfig and mockConfig options

* Rename mockConfig to testOptions

* Break getPublicInstnce into three lines

* createMockRef -> createNodeMock
This commit is contained in:
Brandon Dail
2016-09-13 09:09:20 -05:00
committed by GitHub
parent 38c4ade6cc
commit f3569a2c31
9 changed files with 143 additions and 26 deletions
@@ -1100,10 +1100,10 @@ var ReactCompositeComponent = {
* @final
* @private
*/
attachRef: function(ref, component) {
attachRef: function(ref, component, transaction) {
var inst = this.getPublicInstance();
invariant(inst != null, 'Stateless function components cannot have refs.');
var publicComponentInstance = component.getPublicInstance();
var publicComponentInstance = component.getPublicInstance(transaction);
if (__DEV__) {
var componentName = component && component.getName ?
component.getName() : 'a component';
@@ -73,6 +73,7 @@ var ReactOwner = {
component: ReactInstance,
ref: string,
owner: ReactInstance,
transaction,
): void {
invariant(
isValidOwner(owner),
@@ -81,7 +82,7 @@ var ReactOwner = {
'`render` method, or you have multiple copies of React loaded ' +
'(details: https://fb.me/react-refs-must-have-owner).'
);
owner.attachRef(ref, component);
owner.attachRef(ref, component, transaction);
},
/**
@@ -20,8 +20,12 @@ var warning = require('warning');
* Helper to call ReactRef.attachRefs with this composite component, split out
* to avoid allocations in the transaction mount-ready queue.
*/
function attachRefs() {
ReactRef.attachRefs(this, this._currentElement);
function attachRefs(transaction) {
ReactRef.attachRefs(
this,
this._currentElement,
transaction,
);
}
var ReactReconciler = {
@@ -16,15 +16,21 @@ var ReactOwner = require('ReactOwner');
import type { ReactInstance } from 'ReactInstanceType';
import type { ReactElement } from 'ReactElementType';
import type { Transaction } from 'Transaction';
var ReactRef = {};
function attachRef(ref, component, owner) {
function attachRef(ref, component, owner, transaction) {
if (typeof ref === 'function') {
ref(component.getPublicInstance());
ref(component.getPublicInstance(transaction));
} else {
// Legacy ref
ReactOwner.addComponentAsRefTo(component, ref, owner);
ReactOwner.addComponentAsRefTo(
component,
ref,
owner,
transaction,
);
}
}
@@ -40,13 +46,14 @@ function detachRef(ref, component, owner) {
ReactRef.attachRefs = function(
instance: ReactInstance,
element: ReactElement | string | number | null | false,
transaction: Transaction,
): void {
if (element === null || typeof element !== 'object') {
return;
}
var ref = element.ref;
if (ref != null) {
attachRef(ref, instance, element._owner);
attachRef(ref, instance, element._owner, transaction);
}
};
+5 -2
View File
@@ -30,10 +30,12 @@ var invariant = require('invariant');
class CallbackQueue<T> {
_callbacks: ?Array<() => void>;
_contexts: ?Array<T>;
_arg: ?mixed;
constructor() {
constructor(arg) {
this._callbacks = null;
this._contexts = null;
this._arg = arg;
}
/**
@@ -59,6 +61,7 @@ class CallbackQueue<T> {
notifyAll() {
var callbacks = this._callbacks;
var contexts = this._contexts;
var arg = this._arg;
if (callbacks && contexts) {
invariant(
callbacks.length === contexts.length,
@@ -67,7 +70,7 @@ class CallbackQueue<T> {
this._callbacks = null;
this._contexts = null;
for (var i = 0; i < callbacks.length; i++) {
callbacks[i].call(contexts[i]);
callbacks[i].call(contexts[i], arg);
}
callbacks.length = 0;
contexts.length = 0;
+22 -8
View File
@@ -20,6 +20,16 @@ var getHostComponentFromComposite = require('getHostComponentFromComposite');
var instantiateReactComponent = require('instantiateReactComponent');
var invariant = require('invariant');
type TestRendererOptions = {
createNodeMock: (element: ReactElement) => Object,
};
var defaultTestOptions = {
createNodeMock: function() {
return null;
},
};
/**
* Temporary (?) hack so that we can store all top-level pending updates on
* composites instead of having to worry about different types of components
@@ -45,7 +55,8 @@ TopLevelWrapper.isReactTopLevelWrapper = true;
*/
function mountComponentIntoNode(
componentInstance,
transaction) {
transaction,
) {
var image = ReactReconciler.mountComponent(
componentInstance,
transaction,
@@ -65,13 +76,15 @@ function mountComponentIntoNode(
* @param {number} containerTag container element to mount into.
*/
function batchedMountComponentIntoNode(
componentInstance) {
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
componentInstance,
options,
) {
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(options);
var image = transaction.perform(
mountComponentIntoNode,
null,
componentInstance,
transaction
transaction,
);
ReactUpdates.ReactReconcileTransaction.release(transaction);
return image;
@@ -135,11 +148,12 @@ ReactTestInstance.prototype.toJSON = function() {
var ReactTestMount = {
render: function(
nextElement: ReactElement<any>
nextElement: ReactElement<any>,
options?: TestRendererOptions,
): ReactTestInstance {
var nextWrappedElement = React.createElement(
TopLevelWrapper,
{ child: nextElement }
{child: nextElement},
);
var instance = instantiateReactComponent(nextWrappedElement, false);
@@ -147,10 +161,10 @@ var ReactTestMount = {
// 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
instance,
Object.assign({}, defaultTestOptions, options),
);
return new ReactTestInstance(instance);
},
@@ -57,9 +57,10 @@ var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING];
*
* @class ReactTestReconcileTransaction
*/
function ReactTestReconcileTransaction() {
function ReactTestReconcileTransaction(testOptions) {
this.reinitializeTransaction();
this.reactMountReady = CallbackQueue.getPooled(null);
this.testOptions = testOptions;
this.reactMountReady = CallbackQueue.getPooled(this);
}
var Mixin = {
@@ -82,6 +83,13 @@ var Mixin = {
return this.reactMountReady;
},
/**
* @return {object} the options passed to ReactTestRenderer
*/
getTestOptions: function() {
return this.testOptions;
},
/**
* @return {object} The queue to collect React async events.
*/
+10 -5
View File
@@ -43,6 +43,7 @@ var ReactTestComponent = function(element) {
this._renderedChildren = null;
this._topLevelWrapper = null;
};
ReactTestComponent.prototype.mountComponent = function(
transaction,
nativeParent,
@@ -52,6 +53,7 @@ ReactTestComponent.prototype.mountComponent = function(
var element = this._currentElement;
this.mountChildren(element.props.children, transaction, context);
};
ReactTestComponent.prototype.receiveComponent = function(
nextElement,
transaction,
@@ -60,13 +62,17 @@ ReactTestComponent.prototype.receiveComponent = function(
this._currentElement = nextElement;
this.updateChildren(nextElement.props.children, transaction, context);
};
ReactTestComponent.prototype.getHostNode = function() {};
ReactTestComponent.prototype.getPublicInstance = function() {
// I can't say this makes a ton of sense but it seems better than throwing.
// Maybe we'll revise later if someone has a good use case.
return null;
ReactTestComponent.prototype.getPublicInstance = function(transaction) {
var element = this._currentElement;
var options = transaction.getTestOptions();
return options.createNodeMock(element);
};
ReactTestComponent.prototype.unmountComponent = function() {};
ReactTestComponent.prototype.toJSON = function() {
var {children, ...props} = this._currentElement.props;
var childrenJSON = [];
@@ -136,7 +142,6 @@ ReactComponentEnvironment.injection.injectEnvironment({
var ReactTestRenderer = {
create: ReactTestMount.render,
/* eslint-disable camelcase */
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
/* eslint-enable camelcase */
@@ -231,6 +231,81 @@ describe('ReactTestRenderer', () => {
);
});
it('allows an optional createNodeMock function', () => {
var mockDivInstance = { appendChild: () => {} };
var mockInputInstance = { focus: () => {} };
var mockListItemInstance = { click: () => {} };
var mockAnchorInstance = { hover: () => {} };
var log = [];
class Foo extends React.Component {
componentDidMount() {
log.push(this.refs.bar);
}
render() {
return (
<a ref="bar">Hello, world</a>
);
}
}
function createNodeMock(element) {
switch (element.type) {
case 'div':
return mockDivInstance;
case 'input':
return mockInputInstance;
case 'li':
return mockListItemInstance;
case 'a':
return mockAnchorInstance;
default:
return {};
}
}
ReactTestRenderer.create(
<div ref={(r) => log.push(r)} />,
{createNodeMock}
);
ReactTestRenderer.create(
<input ref={(r) => log.push(r)} />,
{createNodeMock},
);
ReactTestRenderer.create(
<div>
<span>
<ul>
<li ref={(r) => log.push(r)} />
</ul>
<ul>
<li ref={(r) => log.push(r)} />
<li ref={(r) => log.push(r)} />
</ul>
</span>
</div>,
{createNodeMock, foobar: true},
);
ReactTestRenderer.create(
<Foo />,
{createNodeMock},
);
ReactTestRenderer.create(
<div ref={(r) => log.push(r)} />,
);
ReactTestRenderer.create(
<div ref={(r) => log.push(r)} />,
{}
);
expect(log).toEqual([
mockDivInstance,
mockInputInstance,
mockListItemInstance,
mockListItemInstance,
mockListItemInstance,
mockAnchorInstance,
null,
null,
]);
});
it('supports error boundaries', () => {
var log = [];
class Angry extends React.Component {