You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
508 lines
16 KiB
508 lines
16 KiB
/** |
|
* Copyright 2013-2015, 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 ReactTestUtils |
|
*/ |
|
|
|
'use strict'; |
|
|
|
var EventConstants = require("./EventConstants"); |
|
var EventPluginHub = require("./EventPluginHub"); |
|
var EventPropagators = require("./EventPropagators"); |
|
var React = require("./React"); |
|
var ReactElement = require("./ReactElement"); |
|
var ReactEmptyComponent = require("./ReactEmptyComponent"); |
|
var ReactBrowserEventEmitter = require("./ReactBrowserEventEmitter"); |
|
var ReactCompositeComponent = require("./ReactCompositeComponent"); |
|
var ReactInstanceHandles = require("./ReactInstanceHandles"); |
|
var ReactInstanceMap = require("./ReactInstanceMap"); |
|
var ReactMount = require("./ReactMount"); |
|
var ReactUpdates = require("./ReactUpdates"); |
|
var SyntheticEvent = require("./SyntheticEvent"); |
|
|
|
var assign = require("./Object.assign"); |
|
|
|
var topLevelTypes = EventConstants.topLevelTypes; |
|
|
|
function Event(suffix) {} |
|
|
|
/** |
|
* @class ReactTestUtils |
|
*/ |
|
|
|
/** |
|
* Todo: Support the entire DOM.scry query syntax. For now, these simple |
|
* utilities will suffice for testing purposes. |
|
* @lends ReactTestUtils |
|
*/ |
|
var ReactTestUtils = { |
|
renderIntoDocument: function(instance) { |
|
var div = document.createElement('div'); |
|
// None of our tests actually require attaching the container to the |
|
// DOM, and doing so creates a mess that we rely on test isolation to |
|
// clean up, so we're going to stop honoring the name of this method |
|
// (and probably rename it eventually) if no problems arise. |
|
// document.documentElement.appendChild(div); |
|
return React.render(instance, div); |
|
}, |
|
|
|
isElement: function(element) { |
|
return ReactElement.isValidElement(element); |
|
}, |
|
|
|
isElementOfType: function(inst, convenienceConstructor) { |
|
return ( |
|
ReactElement.isValidElement(inst) && |
|
inst.type === convenienceConstructor |
|
); |
|
}, |
|
|
|
isDOMComponent: function(inst) { |
|
// TODO: Fix this heuristic. It's just here because composites can currently |
|
// pretend to be DOM components. |
|
return !!(inst && inst.tagName && inst.getDOMNode); |
|
}, |
|
|
|
isDOMComponentElement: function(inst) { |
|
return !!(inst && |
|
ReactElement.isValidElement(inst) && |
|
!!inst.tagName); |
|
}, |
|
|
|
isCompositeComponent: function(inst) { |
|
return typeof inst.render === 'function' && |
|
typeof inst.setState === 'function'; |
|
}, |
|
|
|
isCompositeComponentWithType: function(inst, type) { |
|
return !!(ReactTestUtils.isCompositeComponent(inst) && |
|
(inst.constructor === type)); |
|
}, |
|
|
|
isCompositeComponentElement: function(inst) { |
|
if (!ReactElement.isValidElement(inst)) { |
|
return false; |
|
} |
|
// We check the prototype of the type that will get mounted, not the |
|
// instance itself. This is a future proof way of duck typing. |
|
var prototype = inst.type.prototype; |
|
return ( |
|
typeof prototype.render === 'function' && |
|
typeof prototype.setState === 'function' |
|
); |
|
}, |
|
|
|
isCompositeComponentElementWithType: function(inst, type) { |
|
return !!(ReactTestUtils.isCompositeComponentElement(inst) && |
|
(inst.constructor === type)); |
|
}, |
|
|
|
getRenderedChildOfCompositeComponent: function(inst) { |
|
if (!ReactTestUtils.isCompositeComponent(inst)) { |
|
return null; |
|
} |
|
var internalInstance = ReactInstanceMap.get(inst); |
|
return internalInstance._renderedComponent.getPublicInstance(); |
|
}, |
|
|
|
findAllInRenderedTree: function(inst, test) { |
|
if (!inst) { |
|
return []; |
|
} |
|
var ret = test(inst) ? [inst] : []; |
|
if (ReactTestUtils.isDOMComponent(inst)) { |
|
var internalInstance = ReactInstanceMap.get(inst); |
|
var renderedChildren = internalInstance |
|
._renderedComponent |
|
._renderedChildren; |
|
var key; |
|
for (key in renderedChildren) { |
|
if (!renderedChildren.hasOwnProperty(key)) { |
|
continue; |
|
} |
|
if (!renderedChildren[key].getPublicInstance) { |
|
continue; |
|
} |
|
ret = ret.concat( |
|
ReactTestUtils.findAllInRenderedTree( |
|
renderedChildren[key].getPublicInstance(), |
|
test |
|
) |
|
); |
|
} |
|
} else if (ReactTestUtils.isCompositeComponent(inst)) { |
|
ret = ret.concat( |
|
ReactTestUtils.findAllInRenderedTree( |
|
ReactTestUtils.getRenderedChildOfCompositeComponent(inst), |
|
test |
|
) |
|
); |
|
} |
|
return ret; |
|
}, |
|
|
|
/** |
|
* Finds all instance of components in the rendered tree that are DOM |
|
* components with the class name matching `className`. |
|
* @return an array of all the matches. |
|
*/ |
|
scryRenderedDOMComponentsWithClass: function(root, className) { |
|
return ReactTestUtils.findAllInRenderedTree(root, function(inst) { |
|
var instClassName = inst.props.className; |
|
return ReactTestUtils.isDOMComponent(inst) && ( |
|
(instClassName && (' ' + instClassName + ' ').indexOf(' ' + className + ' ') !== -1) |
|
); |
|
}); |
|
}, |
|
|
|
/** |
|
* Like scryRenderedDOMComponentsWithClass but expects there to be one result, |
|
* and returns that one result, or throws exception if there is any other |
|
* number of matches besides one. |
|
* @return {!ReactDOMComponent} The one match. |
|
*/ |
|
findRenderedDOMComponentWithClass: function(root, className) { |
|
var all = |
|
ReactTestUtils.scryRenderedDOMComponentsWithClass(root, className); |
|
if (all.length !== 1) { |
|
throw new Error('Did not find exactly one match ' + |
|
'(found: ' + all.length + ') for class:' + className |
|
); |
|
} |
|
return all[0]; |
|
}, |
|
|
|
|
|
/** |
|
* Finds all instance of components in the rendered tree that are DOM |
|
* components with the tag name matching `tagName`. |
|
* @return an array of all the matches. |
|
*/ |
|
scryRenderedDOMComponentsWithTag: function(root, tagName) { |
|
return ReactTestUtils.findAllInRenderedTree(root, function(inst) { |
|
return ReactTestUtils.isDOMComponent(inst) && |
|
inst.tagName === tagName.toUpperCase(); |
|
}); |
|
}, |
|
|
|
/** |
|
* Like scryRenderedDOMComponentsWithTag but expects there to be one result, |
|
* and returns that one result, or throws exception if there is any other |
|
* number of matches besides one. |
|
* @return {!ReactDOMComponent} The one match. |
|
*/ |
|
findRenderedDOMComponentWithTag: function(root, tagName) { |
|
var all = ReactTestUtils.scryRenderedDOMComponentsWithTag(root, tagName); |
|
if (all.length !== 1) { |
|
throw new Error('Did not find exactly one match for tag:' + tagName); |
|
} |
|
return all[0]; |
|
}, |
|
|
|
|
|
/** |
|
* Finds all instances of components with type equal to `componentType`. |
|
* @return an array of all the matches. |
|
*/ |
|
scryRenderedComponentsWithType: function(root, componentType) { |
|
return ReactTestUtils.findAllInRenderedTree(root, function(inst) { |
|
return ReactTestUtils.isCompositeComponentWithType( |
|
inst, |
|
componentType |
|
); |
|
}); |
|
}, |
|
|
|
/** |
|
* Same as `scryRenderedComponentsWithType` but expects there to be one result |
|
* and returns that one result, or throws exception if there is any other |
|
* number of matches besides one. |
|
* @return {!ReactComponent} The one match. |
|
*/ |
|
findRenderedComponentWithType: function(root, componentType) { |
|
var all = ReactTestUtils.scryRenderedComponentsWithType( |
|
root, |
|
componentType |
|
); |
|
if (all.length !== 1) { |
|
throw new Error( |
|
'Did not find exactly one match for componentType:' + componentType |
|
); |
|
} |
|
return all[0]; |
|
}, |
|
|
|
/** |
|
* Pass a mocked component module to this method to augment it with |
|
* useful methods that allow it to be used as a dummy React component. |
|
* Instead of rendering as usual, the component will become a simple |
|
* <div> containing any provided children. |
|
* |
|
* @param {object} module the mock function object exported from a |
|
* module that defines the component to be mocked |
|
* @param {?string} mockTagName optional dummy root tag name to return |
|
* from render method (overrides |
|
* module.mockTagName if provided) |
|
* @return {object} the ReactTestUtils object (for chaining) |
|
*/ |
|
mockComponent: function(module, mockTagName) { |
|
mockTagName = mockTagName || module.mockTagName || "div"; |
|
|
|
module.prototype.render.mockImplementation(function() { |
|
return React.createElement( |
|
mockTagName, |
|
null, |
|
this.props.children |
|
); |
|
}); |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
* Simulates a top level event being dispatched from a raw event that occured |
|
* on an `Element` node. |
|
* @param topLevelType {Object} A type from `EventConstants.topLevelTypes` |
|
* @param {!Element} node The dom to simulate an event occurring on. |
|
* @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent. |
|
*/ |
|
simulateNativeEventOnNode: function(topLevelType, node, fakeNativeEvent) { |
|
fakeNativeEvent.target = node; |
|
ReactBrowserEventEmitter.ReactEventListener.dispatchEvent( |
|
topLevelType, |
|
fakeNativeEvent |
|
); |
|
}, |
|
|
|
/** |
|
* Simulates a top level event being dispatched from a raw event that occured |
|
* on the `ReactDOMComponent` `comp`. |
|
* @param topLevelType {Object} A type from `EventConstants.topLevelTypes`. |
|
* @param comp {!ReactDOMComponent} |
|
* @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent. |
|
*/ |
|
simulateNativeEventOnDOMComponent: function( |
|
topLevelType, |
|
comp, |
|
fakeNativeEvent) { |
|
ReactTestUtils.simulateNativeEventOnNode( |
|
topLevelType, |
|
comp.getDOMNode(), |
|
fakeNativeEvent |
|
); |
|
}, |
|
|
|
nativeTouchData: function(x, y) { |
|
return { |
|
touches: [ |
|
{pageX: x, pageY: y} |
|
] |
|
}; |
|
}, |
|
|
|
createRenderer: function() { |
|
return new ReactShallowRenderer(); |
|
}, |
|
|
|
Simulate: null, |
|
SimulateNative: {} |
|
}; |
|
|
|
/** |
|
* @class ReactShallowRenderer |
|
*/ |
|
var ReactShallowRenderer = function() { |
|
this._instance = null; |
|
}; |
|
|
|
ReactShallowRenderer.prototype.getRenderOutput = function() { |
|
return ( |
|
(this._instance && this._instance._renderedComponent && |
|
this._instance._renderedComponent._renderedOutput) |
|
|| null |
|
); |
|
}; |
|
|
|
var NoopInternalComponent = function(element) { |
|
this._renderedOutput = element; |
|
this._currentElement = element === null || element === false ? |
|
ReactEmptyComponent.emptyElement : |
|
element; |
|
}; |
|
|
|
NoopInternalComponent.prototype = { |
|
|
|
mountComponent: function() { |
|
}, |
|
|
|
receiveComponent: function(element) { |
|
this._renderedOutput = element; |
|
this._currentElement = element === null || element === false ? |
|
ReactEmptyComponent.emptyElement : |
|
element; |
|
}, |
|
|
|
unmountComponent: function() { |
|
} |
|
|
|
}; |
|
|
|
var ShallowComponentWrapper = function() { }; |
|
assign( |
|
ShallowComponentWrapper.prototype, |
|
ReactCompositeComponent.Mixin, { |
|
_instantiateReactComponent: function(element) { |
|
return new NoopInternalComponent(element); |
|
}, |
|
_replaceNodeWithMarkupByID: function() {}, |
|
_renderValidatedComponent: |
|
ReactCompositeComponent.Mixin. |
|
_renderValidatedComponentWithoutOwnerOrContext |
|
} |
|
); |
|
|
|
ReactShallowRenderer.prototype.render = function(element, context) { |
|
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); |
|
this._render(element, transaction, context); |
|
ReactUpdates.ReactReconcileTransaction.release(transaction); |
|
}; |
|
|
|
ReactShallowRenderer.prototype.unmount = function() { |
|
if (this._instance) { |
|
this._instance.unmountComponent(); |
|
} |
|
}; |
|
|
|
ReactShallowRenderer.prototype._render = function(element, transaction, context) { |
|
if (!this._instance) { |
|
var rootID = ReactInstanceHandles.createReactRootID(); |
|
var instance = new ShallowComponentWrapper(element.type); |
|
instance.construct(element); |
|
|
|
instance.mountComponent(rootID, transaction, context); |
|
|
|
this._instance = instance; |
|
} else { |
|
this._instance.receiveComponent(element, transaction, context); |
|
} |
|
}; |
|
|
|
/** |
|
* Exports: |
|
* |
|
* - `ReactTestUtils.Simulate.click(Element/ReactDOMComponent)` |
|
* - `ReactTestUtils.Simulate.mouseMove(Element/ReactDOMComponent)` |
|
* - `ReactTestUtils.Simulate.change(Element/ReactDOMComponent)` |
|
* - ... (All keys from event plugin `eventTypes` objects) |
|
*/ |
|
function makeSimulator(eventType) { |
|
return function(domComponentOrNode, eventData) { |
|
var node; |
|
if (ReactTestUtils.isDOMComponent(domComponentOrNode)) { |
|
node = domComponentOrNode.getDOMNode(); |
|
} else if (domComponentOrNode.tagName) { |
|
node = domComponentOrNode; |
|
} |
|
|
|
var fakeNativeEvent = new Event(); |
|
fakeNativeEvent.target = node; |
|
// We don't use SyntheticEvent.getPooled in order to not have to worry about |
|
// properly destroying any properties assigned from `eventData` upon release |
|
var event = new SyntheticEvent( |
|
ReactBrowserEventEmitter.eventNameDispatchConfigs[eventType], |
|
ReactMount.getID(node), |
|
fakeNativeEvent |
|
); |
|
assign(event, eventData); |
|
EventPropagators.accumulateTwoPhaseDispatches(event); |
|
|
|
ReactUpdates.batchedUpdates(function() { |
|
EventPluginHub.enqueueEvents(event); |
|
EventPluginHub.processEventQueue(); |
|
}); |
|
}; |
|
} |
|
|
|
function buildSimulators() { |
|
ReactTestUtils.Simulate = {}; |
|
|
|
var eventType; |
|
for (eventType in ReactBrowserEventEmitter.eventNameDispatchConfigs) { |
|
/** |
|
* @param {!Element || ReactDOMComponent} domComponentOrNode |
|
* @param {?object} eventData Fake event data to use in SyntheticEvent. |
|
*/ |
|
ReactTestUtils.Simulate[eventType] = makeSimulator(eventType); |
|
} |
|
} |
|
|
|
// Rebuild ReactTestUtils.Simulate whenever event plugins are injected |
|
var oldInjectEventPluginOrder = EventPluginHub.injection.injectEventPluginOrder; |
|
EventPluginHub.injection.injectEventPluginOrder = function() { |
|
oldInjectEventPluginOrder.apply(this, arguments); |
|
buildSimulators(); |
|
}; |
|
var oldInjectEventPlugins = EventPluginHub.injection.injectEventPluginsByName; |
|
EventPluginHub.injection.injectEventPluginsByName = function() { |
|
oldInjectEventPlugins.apply(this, arguments); |
|
buildSimulators(); |
|
}; |
|
|
|
buildSimulators(); |
|
|
|
/** |
|
* Exports: |
|
* |
|
* - `ReactTestUtils.SimulateNative.click(Element/ReactDOMComponent)` |
|
* - `ReactTestUtils.SimulateNative.mouseMove(Element/ReactDOMComponent)` |
|
* - `ReactTestUtils.SimulateNative.mouseIn/ReactDOMComponent)` |
|
* - `ReactTestUtils.SimulateNative.mouseOut(Element/ReactDOMComponent)` |
|
* - ... (All keys from `EventConstants.topLevelTypes`) |
|
* |
|
* Note: Top level event types are a subset of the entire set of handler types |
|
* (which include a broader set of "synthetic" events). For example, onDragDone |
|
* is a synthetic event. Except when testing an event plugin or React's event |
|
* handling code specifically, you probably want to use ReactTestUtils.Simulate |
|
* to dispatch synthetic events. |
|
*/ |
|
|
|
function makeNativeSimulator(eventType) { |
|
return function(domComponentOrNode, nativeEventData) { |
|
var fakeNativeEvent = new Event(eventType); |
|
assign(fakeNativeEvent, nativeEventData); |
|
if (ReactTestUtils.isDOMComponent(domComponentOrNode)) { |
|
ReactTestUtils.simulateNativeEventOnDOMComponent( |
|
eventType, |
|
domComponentOrNode, |
|
fakeNativeEvent |
|
); |
|
} else if (!!domComponentOrNode.tagName) { |
|
// Will allow on actual dom nodes. |
|
ReactTestUtils.simulateNativeEventOnNode( |
|
eventType, |
|
domComponentOrNode, |
|
fakeNativeEvent |
|
); |
|
} |
|
}; |
|
} |
|
|
|
var eventType; |
|
for (eventType in topLevelTypes) { |
|
// Event type is stored as 'topClick' - we transform that to 'click' |
|
var convenienceName = eventType.indexOf('top') === 0 ? |
|
eventType.charAt(3).toLowerCase() + eventType.substr(4) : eventType; |
|
/** |
|
* @param {!Element || ReactDOMComponent} domComponentOrNode |
|
* @param {?Event} nativeEventData Fake native event to use in SyntheticEvent. |
|
*/ |
|
ReactTestUtils.SimulateNative[convenienceName] = |
|
makeNativeSimulator(eventType); |
|
} |
|
|
|
module.exports = ReactTestUtils;
|
|
|