Skip to content

Commit

Permalink
Implement createNodeMock for ReactTestRenderer (#7649)
Browse files Browse the repository at this point in the history
* 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

(cherry picked from commit f3569a2)
  • Loading branch information
aweary authored and zpao committed Oct 4, 2016
1 parent ec4c0f1 commit d5059c9
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion src/renderers/shared/stack/reconciler/ReactOwner.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ var ReactOwner = {
component: ReactInstance,
ref: string,
owner: ReactInstance,
transaction,
): void {
invariant(
isValidOwner(owner),
Expand All @@ -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);
},

/**
Expand Down
8 changes: 6 additions & 2 deletions src/renderers/shared/stack/reconciler/ReactReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
15 changes: 11 additions & 4 deletions src/renderers/shared/stack/reconciler/ReactRef.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}

Expand All @@ -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);
}
};

Expand Down
7 changes: 5 additions & 2 deletions src/renderers/shared/utils/CallbackQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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,
Expand All @@ -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;
Expand Down
30 changes: 22 additions & 8 deletions src/renderers/testing/ReactTestMount.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,7 +55,8 @@ TopLevelWrapper.isReactTopLevelWrapper = true;
*/
function mountComponentIntoNode(
componentInstance,
transaction) {
transaction,
) {
var image = ReactReconciler.mountComponent(
componentInstance,
transaction,
Expand All @@ -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;
Expand Down Expand Up @@ -135,22 +148,23 @@ 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);

// 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);
},
Expand Down
12 changes: 10 additions & 2 deletions src/renderers/testing/ReactTestReconcileTransaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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.
*/
Expand Down
15 changes: 10 additions & 5 deletions src/renderers/testing/ReactTestRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ var ReactTestComponent = function(element) {
this._renderedChildren = null;
this._topLevelWrapper = null;
};

ReactTestComponent.prototype.mountComponent = function(
transaction,
nativeParent,
Expand All @@ -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,
Expand All @@ -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 = [];
Expand Down Expand Up @@ -136,7 +142,6 @@ ReactComponentEnvironment.injection.injectEnvironment({

var ReactTestRenderer = {
create: ReactTestMount.render,

/* eslint-disable camelcase */
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
/* eslint-enable camelcase */
Expand Down
75 changes: 75 additions & 0 deletions src/renderers/testing/__tests__/ReactTestRenderer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit d5059c9

Please sign in to comment.