From fbe19e90b958d2fb217d67bf462e7fb03e4cf6d4 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Thu, 20 Oct 2016 11:17:06 -0700 Subject: [PATCH] Fix captured/bubbled in ReactNativeTreeTraversal (#8019) Follow-up to #7741. Added a test for RN event bubbling that fails before the fix. (cherry picked from commit a8beab3341f699efcbc886b0b63710891a688db4) --- .../native/ReactNativeTreeTraversal.js | 4 +- src/renderers/native/__mocks__/UIManager.js | 46 ++++++++++ .../__tests__/ReactNativeEvents-test.js | 89 +++++++++++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/renderers/native/__tests__/ReactNativeEvents-test.js diff --git a/src/renderers/native/ReactNativeTreeTraversal.js b/src/renderers/native/ReactNativeTreeTraversal.js index 988d3d7b71..4e98fa8950 100644 --- a/src/renderers/native/ReactNativeTreeTraversal.js +++ b/src/renderers/native/ReactNativeTreeTraversal.js @@ -82,10 +82,10 @@ function traverseTwoPhase(inst, fn, arg) { } var i; for (i = path.length; i-- > 0;) { - fn(path[i], false, arg); + fn(path[i], 'captured', arg); } for (i = 0; i < path.length; i++) { - fn(path[i], true, arg); + fn(path[i], 'bubbled', arg); } } diff --git a/src/renderers/native/__mocks__/UIManager.js b/src/renderers/native/__mocks__/UIManager.js index ddcba12ca0..dffb8fd9f6 100644 --- a/src/renderers/native/__mocks__/UIManager.js +++ b/src/renderers/native/__mocks__/UIManager.js @@ -18,6 +18,52 @@ var RCTUIManager = { updateView: jest.fn(), removeSubviewsFromContainerWithID: jest.fn(), replaceExistingNonRootView: jest.fn(), + customBubblingEventTypes: { + topBlur: { + phasedRegistrationNames: { + bubbled: 'onBlur', + captured: 'onBlurCapture', + }, + }, + topFocus: { + phasedRegistrationNames: { + bubbled: 'onFocus', + captured: 'onFocusCapture', + }, + }, + topTouchCancel: { + phasedRegistrationNames: { + bubbled: 'onTouchCancel', + captured: 'onTouchCancelCapture', + }, + }, + topTouchEnd: { + phasedRegistrationNames: { + bubbled: 'onTouchEnd', + captured: 'onTouchEndCapture', + }, + }, + topTouchMove: { + phasedRegistrationNames: { + bubbled: 'onTouchMove', + captured: 'onTouchMoveCapture', + }, + }, + topTouchStart: { + phasedRegistrationNames: { + bubbled: 'onTouchStart', + captured: 'onTouchStartCapture', + }, + }, + }, + customDirectEventTypes: { + topAccessibilityTap: { + registrationName: 'onAccessibilityTap', + }, + topTextLayout: { + registrationName: 'onTextLayout', + }, + }, }; module.exports = RCTUIManager; diff --git a/src/renderers/native/__tests__/ReactNativeEvents-test.js b/src/renderers/native/__tests__/ReactNativeEvents-test.js new file mode 100644 index 0000000000..8110dcff20 --- /dev/null +++ b/src/renderers/native/__tests__/ReactNativeEvents-test.js @@ -0,0 +1,89 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +var RCTEventEmitter; +var React; +var ReactErrorUtils; +var ReactNative; +var UIManager; +var createReactNativeComponentClass; + +beforeEach(() => { + jest.resetModuleRegistry(); + + RCTEventEmitter = require('RCTEventEmitter'); + React = require('React'); + ReactErrorUtils = require('ReactErrorUtils'); + ReactNative = require('ReactNative'); + UIManager = require('UIManager'); + createReactNativeComponentClass = require('createReactNativeComponentClass'); + + // Ensure errors from event callbacks are properly surfaced (otherwise, + // jest/jsdom swallows them when we do the .dispatchEvent call) + ReactErrorUtils.invokeGuardedCallback = + ReactErrorUtils.invokeGuardedCallbackWithCatch; +}); + +it('handles events', () => { + expect(RCTEventEmitter.register.mock.calls.length).toBe(1); + var EventEmitter = RCTEventEmitter.register.mock.calls[0][0]; + var View = createReactNativeComponentClass({ + validAttributes: { foo: true }, + uiViewClassName: 'View', + }); + + + var log = []; + ReactNative.render( + log.push('outer touchend')} + onTouchEndCapture={() => log.push('outer touchend capture')} + onTouchStart={() => log.push('outer touchstart')} + onTouchStartCapture={() => log.push('outer touchstart capture')}> + log.push('inner touchend capture')} + onTouchEnd={() => log.push('inner touchend')} + onTouchStartCapture={() => log.push('inner touchstart capture')} + onTouchStart={() => log.push('inner touchstart')} + /> + , + 1 + ); + + expect(UIManager.createView.mock.calls.length).toBe(2); + var innerTag = UIManager.createView.mock.calls[1][0]; + + EventEmitter.receiveTouches( + 'topTouchStart', + [{target: innerTag, identifier: 17}], + [0] + ); + EventEmitter.receiveTouches( + 'topTouchEnd', + [{target: innerTag, identifier: 17}], + [0] + ); + + expect(log).toEqual([ + 'outer touchstart capture', + 'inner touchstart capture', + 'inner touchstart', + 'outer touchstart', + 'outer touchend capture', + 'inner touchend capture', + 'inner touchend', + 'outer touchend', + ]); +});