mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user