From 9f9ee697f3c52d1e0e907b3385ef1324582c201b Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Sat, 18 Jan 2014 15:41:15 -0800 Subject: [PATCH 1/3] Simpler ReactTestUtils.simulateEventOnDOMComponent --- src/test/ReactTestUtils.js | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index c7d94c7b2cbe8..cd4ff3057000a 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -22,7 +22,6 @@ var ReactComponent = require('ReactComponent'); var ReactDOM = require('ReactDOM'); var ReactEventEmitter = require('ReactEventEmitter'); var ReactTextComponent = require('ReactTextComponent'); -var ReactMount = require('ReactMount'); var mergeInto = require('mergeInto'); var copyProperties = require('copyProperties'); @@ -250,19 +249,11 @@ var ReactTestUtils = { * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent. */ simulateEventOnDOMComponent: function(topLevelType, comp, fakeNativeEvent) { - var reactRootID = comp._rootNodeID || comp._rootDomId; - if (!reactRootID) { - throw new Error('Simulating event on non-rendered component'); - } - var virtualHandler = - ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( - topLevelType - ); - var node = ReactMount.getNode(reactRootID); - fakeNativeEvent.target = node; - /* jsdom is returning nodes without id's - fixing that issue here. */ - ReactMount.setID(node, reactRootID); - virtualHandler(fakeNativeEvent); + ReactTestUtils.simulateEventOnNode( + topLevelType, + comp.getDOMNode(), + fakeNativeEvent + ); }, nativeTouchData: function(x, y) { From 36e97bac21642f73c15d45176dbd304fff327c5a Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Sat, 18 Jan 2014 16:44:15 -0800 Subject: [PATCH 2/3] Simulate synthetic events using ReactTestUtils Most of the time this is what you want to do, so I've renamed what was Simulate to be SimulateNative. Now Simulate.change does what you expect, so this fixes #517 and fixes #519. --- src/browser/ReactEventEmitter.js | 2 + .../__tests__/ReactEventEmitter-test.js | 20 ++--- .../__tests__/ReactDOMInput-test.js | 11 ++- .../__tests__/ReactDOMTextarea-test.js | 4 +- .../__tests__/AnalyticsEventPlugin-test.js | 8 +- src/core/__tests__/ReactBind-test.js | 4 +- src/event/EventPluginHub.js | 2 + src/event/EventPluginRegistry.js | 23 ++++- src/test/ReactTestUtils.js | 89 +++++++++++++++---- 9 files changed, 120 insertions(+), 43 deletions(-) diff --git a/src/browser/ReactEventEmitter.js b/src/browser/ReactEventEmitter.js index 56de0dcb25637..11697d0caf303 100644 --- a/src/browser/ReactEventEmitter.js +++ b/src/browser/ReactEventEmitter.js @@ -318,6 +318,8 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, { } }, + eventNameDispatchConfigs: EventPluginHub.eventNameDispatchConfigs, + registrationNameModules: EventPluginHub.registrationNameModules, putListener: EventPluginHub.putListener, diff --git a/src/browser/__tests__/ReactEventEmitter-test.js b/src/browser/__tests__/ReactEventEmitter-test.js index 6c1ca8e3c05f1..2aeaecbedfb19 100644 --- a/src/browser/__tests__/ReactEventEmitter-test.js +++ b/src/browser/__tests__/ReactEventEmitter-test.js @@ -142,10 +142,10 @@ describe('ReactEventEmitter', function() { it('should not invoke handlers if ReactEventEmitter is disabled', function() { registerSimpleTestHandler(); ReactEventEmitter.setEnabled(false); - ReactTestUtils.Simulate.click(CHILD); + ReactTestUtils.SimulateNative.click(CHILD); expect(LISTENER.mock.calls.length).toBe(0); ReactEventEmitter.setEnabled(true); - ReactTestUtils.Simulate.click(CHILD); + ReactTestUtils.SimulateNative.click(CHILD); expect(LISTENER.mock.calls.length).toBe(1); }); @@ -312,11 +312,11 @@ describe('ReactEventEmitter', function() { ON_TOUCH_TAP_KEY, recordID.bind(null, getID(CHILD)) ); - ReactTestUtils.Simulate.touchStart( + ReactTestUtils.SimulateNative.touchStart( CHILD, ReactTestUtils.nativeTouchData(0, 0) ); - ReactTestUtils.Simulate.touchEnd( + ReactTestUtils.SimulateNative.touchEnd( CHILD, ReactTestUtils.nativeTouchData(0, 0) ); @@ -330,11 +330,11 @@ describe('ReactEventEmitter', function() { ON_TOUCH_TAP_KEY, recordID.bind(null, getID(CHILD)) ); - ReactTestUtils.Simulate.touchStart( + ReactTestUtils.SimulateNative.touchStart( CHILD, ReactTestUtils.nativeTouchData(0, 0) ); - ReactTestUtils.Simulate.touchEnd( + ReactTestUtils.SimulateNative.touchEnd( CHILD, ReactTestUtils.nativeTouchData(0, tapMoveThreshold - 1) ); @@ -348,11 +348,11 @@ describe('ReactEventEmitter', function() { ON_TOUCH_TAP_KEY, recordID.bind(null, getID(CHILD)) ); - ReactTestUtils.Simulate.touchStart( + ReactTestUtils.SimulateNative.touchStart( CHILD, ReactTestUtils.nativeTouchData(0, 0) ); - ReactTestUtils.Simulate.touchEnd( + ReactTestUtils.SimulateNative.touchEnd( CHILD, ReactTestUtils.nativeTouchData(0, tapMoveThreshold + 1) ); @@ -415,11 +415,11 @@ describe('ReactEventEmitter', function() { ON_TOUCH_TAP_KEY, recordID.bind(null, getID(GRANDPARENT)) ); - ReactTestUtils.Simulate.touchStart( + ReactTestUtils.SimulateNative.touchStart( CHILD, ReactTestUtils.nativeTouchData(0, 0) ); - ReactTestUtils.Simulate.touchEnd( + ReactTestUtils.SimulateNative.touchEnd( CHILD, ReactTestUtils.nativeTouchData(0, 0) ); diff --git a/src/browser/dom/components/__tests__/ReactDOMInput-test.js b/src/browser/dom/components/__tests__/ReactDOMInput-test.js index 88e91f2b9d949..d2049a6d517c9 100644 --- a/src/browser/dom/components/__tests__/ReactDOMInput-test.js +++ b/src/browser/dom/components/__tests__/ReactDOMInput-test.js @@ -125,7 +125,7 @@ describe('ReactDOMInput', function() { var node = renderTextInput(stub); node.value = 'giraffe'; - ReactTestUtils.Simulate.input(node); + ReactTestUtils.Simulate.change(node); expect(node.value).toBe('0'); }); @@ -171,9 +171,8 @@ describe('ReactDOMInput', function() { aNode.checked = false; expect(cNode.checked).toBe(true); - // Now let's run the actual ReactDOMInput change event handler (on radio - // inputs, ChangeEventPlugin listens for the `click` event so trigger that) - ReactTestUtils.Simulate.click(bNode); + // Now let's run the actual ReactDOMInput change event handler + ReactTestUtils.Simulate.change(bNode); // The original state should have been restored expect(aNode.checked).toBe(true); @@ -192,7 +191,7 @@ describe('ReactDOMInput', function() { expect(link.requestChange.mock.calls.length).toBe(0); instance.getDOMNode().value = 'test'; - ReactTestUtils.Simulate.input(instance.getDOMNode()); + ReactTestUtils.Simulate.change(instance.getDOMNode()); expect(link.requestChange.mock.calls.length).toBe(1); expect(link.requestChange.mock.calls[0][0]).toEqual('test'); @@ -265,7 +264,7 @@ describe('ReactDOMInput', function() { expect(link.requestChange.mock.calls.length).toBe(0); instance.getDOMNode().checked = false; - ReactTestUtils.Simulate.click(instance.getDOMNode()); + ReactTestUtils.Simulate.change(instance.getDOMNode()); expect(link.requestChange.mock.calls.length).toBe(1); expect(link.requestChange.mock.calls[0][0]).toEqual(false); diff --git a/src/browser/dom/components/__tests__/ReactDOMTextarea-test.js b/src/browser/dom/components/__tests__/ReactDOMTextarea-test.js index 955e3c13f2f2b..576f21997ca7e 100644 --- a/src/browser/dom/components/__tests__/ReactDOMTextarea-test.js +++ b/src/browser/dom/components/__tests__/ReactDOMTextarea-test.js @@ -139,7 +139,7 @@ describe('ReactDOMTextarea', function() { var node = renderTextarea(stub); node.value = 'giraffe'; - ReactTestUtils.Simulate.input(node); + ReactTestUtils.Simulate.change(node); expect(node.value).toBe('0'); }); @@ -216,7 +216,7 @@ describe('ReactDOMTextarea', function() { expect(link.requestChange.mock.calls.length).toBe(0); instance.getDOMNode().value = 'test'; - ReactTestUtils.Simulate.input(instance.getDOMNode()); + ReactTestUtils.Simulate.change(instance.getDOMNode()); expect(link.requestChange.mock.calls.length).toBe(1); expect(link.requestChange.mock.calls[0][0]).toEqual('test'); diff --git a/src/browser/eventPlugins/__tests__/AnalyticsEventPlugin-test.js b/src/browser/eventPlugins/__tests__/AnalyticsEventPlugin-test.js index 14059dec8665a..770d73db6ec02 100644 --- a/src/browser/eventPlugins/__tests__/AnalyticsEventPlugin-test.js +++ b/src/browser/eventPlugins/__tests__/AnalyticsEventPlugin-test.js @@ -99,14 +99,14 @@ describe('AnalyticsEventPlugin', function() { // Simulate some clicks for (var i = 0; i < numClickEvents; i++) { - ReactTestUtils.Simulate.click(renderedComponent.refs.testDiv); + ReactTestUtils.SimulateNative.click(renderedComponent.refs.testDiv); } // Simulate some double clicks for (i = 0; i < numDoubleClickEvents; i++) { - ReactTestUtils.Simulate.doubleClick(renderedComponent.refs.testDiv); + ReactTestUtils.SimulateNative.doubleClick(renderedComponent.refs.testDiv); } // Simulate some other events not being tracked for analytics - ReactTestUtils.Simulate.focus(renderedComponent.refs.testDiv); + ReactTestUtils.SimulateNative.focus(renderedComponent.refs.testDiv); window.mockRunTimersOnce(); expect(cb).toBeCalled(); @@ -143,7 +143,7 @@ describe('AnalyticsEventPlugin', function() { var error = false; try { - ReactTestUtils.Simulate.click(renderedComponent.refs.testDiv); + ReactTestUtils.SimulateNative.click(renderedComponent.refs.testDiv); } catch(e) { error = true; } diff --git a/src/core/__tests__/ReactBind-test.js b/src/core/__tests__/ReactBind-test.js index 000303a353a1e..00743a04f64a5 100644 --- a/src/core/__tests__/ReactBind-test.js +++ b/src/core/__tests__/ReactBind-test.js @@ -52,8 +52,8 @@ describe('autobinding', function() { render: function() { return (
); diff --git a/src/event/EventPluginHub.js b/src/event/EventPluginHub.js index cdee4f710f01b..74494c66a60b5 100644 --- a/src/event/EventPluginHub.js +++ b/src/event/EventPluginHub.js @@ -141,6 +141,8 @@ var EventPluginHub = { }, + eventNameDispatchConfigs: EventPluginRegistry.eventNameDispatchConfigs, + registrationNameModules: EventPluginRegistry.registrationNameModules, /** diff --git a/src/event/EventPluginRegistry.js b/src/event/EventPluginRegistry.js index a81e9614f7a85..730c1879e652f 100644 --- a/src/event/EventPluginRegistry.js +++ b/src/event/EventPluginRegistry.js @@ -85,6 +85,14 @@ function recomputePluginOrdering() { * @private */ function publishEventForPlugin(dispatchConfig, PluginModule, eventName) { + invariant( + !EventPluginRegistry.eventNameDispatchConfigs[eventName], + 'EventPluginHub: More than one plugin attempted to publish the same ' + + 'event name, `%s`.', + eventName + ); + EventPluginRegistry.eventNameDispatchConfigs[eventName] = dispatchConfig; + var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames; if (phasedRegistrationNames) { for (var phaseName in phasedRegistrationNames) { @@ -142,7 +150,12 @@ var EventPluginRegistry = { plugins: [], /** - * Mapping from registration names to plugin modules. + * Mapping from event name to dispatch config + */ + eventNameDispatchConfigs: {}, + + /** + * Mapping from registration name to plugin module */ registrationNameModules: {}, @@ -243,6 +256,14 @@ var EventPluginRegistry = { } } EventPluginRegistry.plugins.length = 0; + + var eventNameDispatchConfigs = EventPluginRegistry.eventNameDispatchConfigs; + for (var eventName in eventNameDispatchConfigs) { + if (eventNameDispatchConfigs.hasOwnProperty(eventName)) { + delete eventNameDispatchConfigs[eventName]; + } + } + var registrationNameModules = EventPluginRegistry.registrationNameModules; for (var registrationName in registrationNameModules) { if (registrationNameModules.hasOwnProperty(registrationName)) { diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index cd4ff3057000a..53ae01ab19c33 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -16,12 +16,19 @@ * @providesModule ReactTestUtils */ +"use strict"; + var EventConstants = require('EventConstants'); +var EventPluginHub = require('EventPluginHub'); +var EventPropagators = require('EventPropagators'); var React = require('React'); var ReactComponent = require('ReactComponent'); var ReactDOM = require('ReactDOM'); var ReactEventEmitter = require('ReactEventEmitter'); +var ReactMount = require('ReactMount'); var ReactTextComponent = require('ReactTextComponent'); +var ReactUpdates = require('ReactUpdates'); +var SyntheticEvent = require('SyntheticEvent'); var mergeInto = require('mergeInto'); var copyProperties = require('copyProperties'); @@ -227,12 +234,12 @@ var ReactTestUtils = { /** * Simulates a top level event being dispatched from a raw event that occured - * on and `Element` node. + * 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. */ - simulateEventOnNode: function(topLevelType, node, fakeNativeEvent) { + simulateNativeEventOnNode: function(topLevelType, node, fakeNativeEvent) { var virtualHandler = ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( topLevelType @@ -248,8 +255,11 @@ var ReactTestUtils = { * @param comp {!ReactDOMComponent} * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent. */ - simulateEventOnDOMComponent: function(topLevelType, comp, fakeNativeEvent) { - ReactTestUtils.simulateEventOnNode( + simulateNativeEventOnDOMComponent: function( + topLevelType, + comp, + fakeNativeEvent) { + ReactTestUtils.simulateNativeEventOnNode( topLevelType, comp.getDOMNode(), fakeNativeEvent @@ -264,7 +274,8 @@ var ReactTestUtils = { }; }, - Simulate: null // Will populate + Simulate: {}, + SimulateNative: {} }; /** @@ -272,33 +283,75 @@ var ReactTestUtils = { * * - `ReactTestUtils.Simulate.click(Element/ReactDOMComponent)` * - `ReactTestUtils.Simulate.mouseMove(Element/ReactDOMComponent)` - * - `ReactTestUtils.Simulate.mouseIn/ReactDOMComponent)` - * - `ReactTestUtils.Simulate.mouseOut(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( + ReactEventEmitter.eventNameDispatchConfigs[eventType], + ReactMount.getID(node), + fakeNativeEvent + ); + mergeInto(event, eventData); + EventPropagators.accumulateTwoPhaseDispatches(event); + + ReactUpdates.batchedUpdates(function() { + EventPluginHub.enqueueEvents(event); + EventPluginHub.processEventQueue(); + }); + }; +} + +var eventType; +for (eventType in ReactEventEmitter.eventNameDispatchConfigs) { + /** + * @param {!Element || ReactDOMComponent} domComponentOrNode + * @param {?object} eventData Fake event data to use in SyntheticEvent. + */ + ReactTestUtils.Simulate[eventType] = makeSimulator(eventType); +} + +/** + * 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. You certainly may write test cases for these event - * types, but it doesn't make sense to simulate them at this low of a level. In - * this case, the way you test an `onDragDone` event is by simulating a series - * of `mouseMove`/ `mouseDown`/`mouseUp` events - Then, a synthetic event of - * type `onDragDone` will be constructed and dispached through your system - * automatically. + * 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 makeSimulator(eventType) { +function makeNativeSimulator(eventType) { return function(domComponentOrNode, nativeEventData) { var fakeNativeEvent = new Event(eventType); mergeInto(fakeNativeEvent, nativeEventData); if (ReactTestUtils.isDOMComponent(domComponentOrNode)) { - ReactTestUtils.simulateEventOnDOMComponent( + ReactTestUtils.simulateNativeEventOnDOMComponent( eventType, domComponentOrNode, fakeNativeEvent ); } else if (!!domComponentOrNode.tagName) { // Will allow on actual dom nodes. - ReactTestUtils.simulateEventOnNode( + ReactTestUtils.simulateNativeEventOnNode( eventType, domComponentOrNode, fakeNativeEvent @@ -307,7 +360,6 @@ function makeSimulator(eventType) { }; } -ReactTestUtils.Simulate = {}; var eventType; for (eventType in topLevelTypes) { // Event type is stored as 'topClick' - we transform that to 'click' @@ -317,7 +369,8 @@ for (eventType in topLevelTypes) { * @param {!Element || ReactDOMComponent} domComponentOrNode * @param {?Event} nativeEventData Fake native event to use in SyntheticEvent. */ - ReactTestUtils.Simulate[convenienceName] = makeSimulator(eventType); + ReactTestUtils.SimulateNative[convenienceName] = + makeNativeSimulator(eventType); } module.exports = ReactTestUtils; From a447d30a2e2061964a683805c11484a8846e2349 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Tue, 18 Feb 2014 23:44:14 -0800 Subject: [PATCH 3/3] Rebuild simulators after injecting plugins --- src/test/ReactTestUtils.js | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index 53ae01ab19c33..8d34272703b1e 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -274,7 +274,7 @@ var ReactTestUtils = { }; }, - Simulate: {}, + Simulate: null, SimulateNative: {} }; @@ -314,15 +314,33 @@ function makeSimulator(eventType) { }; } -var eventType; -for (eventType in ReactEventEmitter.eventNameDispatchConfigs) { - /** - * @param {!Element || ReactDOMComponent} domComponentOrNode - * @param {?object} eventData Fake event data to use in SyntheticEvent. - */ - ReactTestUtils.Simulate[eventType] = makeSimulator(eventType); +function buildSimulators() { + ReactTestUtils.Simulate = {}; + + var eventType; + for (eventType in ReactEventEmitter.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: *