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

Implement createNodeMock for ReactTestRenderer #7649

Merged
merged 15 commits into from
Sep 13, 2016
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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this.reactMountReady is accessed via getReactMountReady(), let’s add getMockConfig() for consistency and use that instead outside.

}

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's add a newline before this line too for consistency now that we’ve put some of them between methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're already calling Object.assign on the prototype, do you think it would be worth just assigning these there instead, like we do in a few other places? It would be a bit cleaner.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's keep it the way it is. If we want to make this cleaner we should just use ES6 classes instead but this should be done separately. We should not mix stylistic and meaningful changes in the same PR.

var element = this._currentElement;
var options = transaction.getTestOptions();
return options.createNodeMock(element);
};

ReactTestComponent.prototype.unmountComponent = function() {};

ReactTestComponent.prototype.toJSON = function() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here

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 */
Copy link
Collaborator

Choose a reason for hiding this comment

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

And keep this one, so the diff is smaller. In general better not to formatting changes to code you didn’t directly touch unless it’s horribly formatted

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