diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystTouchBubblingTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystTouchBubblingTestCase.java new file mode 100644 index 00000000000000..4eac61d1f4d295 --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystTouchBubblingTestCase.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2013-present, 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. + */ + +package com.facebook.react.tests; + +import android.view.View; + +import com.facebook.react.testing.ReactInstanceSpecForTest; +import com.facebook.react.testing.ReactAppInstrumentationTestCase; +import com.facebook.react.testing.SingleTouchGestureGenerator; +import com.facebook.react.testing.StringRecordingModule; + +/** + * This test is to verify that touch events bubbles up to the right handler. We emulate couple + * of different gestures on top of the application reflecting following layout: + * + * +---------------------------------------------------------------------------------------+ + * | | + * | +----------------------------------------------------------------------------------+ | + * | | +-------------+ +----------------+ | | + * | | | +---+ | | | | | + * | | | | A | | | | | | + * | | | +---+ | | C | | | + * | | | {B} | | | | | + * | | | | {D} | | | | + * | | +-------------+ +----------------+ | | + * | | | | + * | | | | + * | +----------------------------------------------------------------------------------+ | + * | + * | +----------------------------------------------------------------------------------+ | + * | | | | + * | | | | + * | | | | + * | | {E} | | + * | | | | + * | | | | + * | +----------------------------------------------------------------------------------+ | + * +---------------------------------------------------------------------------------------+ + * + * Then in each test case we eiter tap the center of a particular view (from A to E) or we start + * a gesture in one view and end it with another. + * View with names in brackets (e.g. {D}) have touch handlers set whereas all other views are not + * declared to handler touch events. + */ +public class CatalystTouchBubblingTestCase extends ReactAppInstrumentationTestCase { + + private final StringRecordingModule mRecordingModule = new StringRecordingModule(); + + @Override + protected String getReactApplicationKeyUnderTest() { + return "TouchBubblingTestAppModule"; + } + + /** + * 1) Simulate touch event at view A, expect {B} touch handler to fire + * 2) Simulate touch event at view C, expect {D} touch handler to fire + */ + public void testSimpleClickAtInnerElements() { + mRecordingModule.reset(); + View innerButton = getViewByTestId("A"); + assertNotNull(innerButton); + createGestureGenerator().startGesture(innerButton).endGesture(); + waitForBridgeAndUIIdle(); + assertEquals(1, mRecordingModule.getCalls().size()); + assertEquals("inner", mRecordingModule.getCalls().get(0)); + + mRecordingModule.reset(); + innerButton = getViewByTestId("C"); + assertNotNull(innerButton); + createGestureGenerator().startGesture(innerButton).endGesture(); + waitForBridgeAndUIIdle(); + assertEquals(1, mRecordingModule.getCalls().size()); + assertEquals("outer", mRecordingModule.getCalls().get(0)); + } + + /** + * 1) Start touch at view A, then drag and release on view {B} (but outside of A), expect {B}'s + * touch handler to fire + * 2) Do the same with view C and {D} + */ + public void testDownOnInnerUpOnTouchableParent() { + View innerButton = getViewByTestId("A"); + View touchableParent = getViewByTestId("B"); + + SingleTouchGestureGenerator gestureGenerator = createGestureGenerator(); + gestureGenerator.startGesture(innerButton); + // wait for tapped view measurements + waitForBridgeAndUIIdle(); + + gestureGenerator.dragTo(touchableParent, 15).endGesture(); + waitForBridgeAndUIIdle(); + assertEquals(1, mRecordingModule.getCalls().size()); + assertEquals("inner", mRecordingModule.getCalls().get(0)); + + // Do same with second inner view + mRecordingModule.reset(); + + touchableParent = getViewByTestId("D"); + innerButton = getViewByTestId("C"); + + gestureGenerator = createGestureGenerator(); + gestureGenerator.startGesture(innerButton); + // wait for tapped view measurements + waitForBridgeAndUIIdle(); + + gestureGenerator.dragTo(touchableParent, 15).endGesture(); + waitForBridgeAndUIIdle(); + assertEquals(1, mRecordingModule.getCalls().size()); + assertEquals("outer", mRecordingModule.getCalls().get(0)); + } + + /** + * Start gesture at view A, then drag and release on view {E}. Expect no touch handlers to fire + */ + public void testDragOutOfTouchable() { + View outsideView = getViewByTestId("E"); + View innerButton = getViewByTestId("A"); + + SingleTouchGestureGenerator gestureGenerator = createGestureGenerator(); + gestureGenerator.startGesture(innerButton); + // wait for tapped view measurements + waitForBridgeAndUIIdle(); + + gestureGenerator.dragTo(outsideView, 15).endGesture(); + waitForBridgeAndUIIdle(); + assertTrue(mRecordingModule.getCalls().isEmpty()); + } + + /** + * In this scenario we start gesture at view A (has two touchable parents {B} and {D}) then we + * drag and release gesture on view {D}, but outside of {B}. We expect no touch handler to fire + */ + public void testNoEventWhenDragOutOfFirstTouchableParentToItsTouchableParent() { + View topLevelTouchable = getViewByTestId("C"); + View innerButton = getViewByTestId("A"); + + SingleTouchGestureGenerator gestureGenerator = createGestureGenerator(); + gestureGenerator.startGesture(innerButton); + // wait for tapped view measurements + waitForBridgeAndUIIdle(); + + gestureGenerator.dragTo(topLevelTouchable, 15).endGesture(); + waitForBridgeAndUIIdle(); + assertTrue(mRecordingModule.getCalls().isEmpty()); + } + + @Override + protected ReactInstanceSpecForTest createReactInstanceSpecForTest() { + return new ReactInstanceSpecForTest() + .addNativeModule(mRecordingModule); + } +} diff --git a/ReactAndroid/src/androidTest/js/TestBundle.js b/ReactAndroid/src/androidTest/js/TestBundle.js index f925d81f82d65a..1e027fbca597c8 100644 --- a/ReactAndroid/src/androidTest/js/TestBundle.js +++ b/ReactAndroid/src/androidTest/js/TestBundle.js @@ -80,6 +80,10 @@ var apps = [ appKey: 'TimePickerDialogTestApp', component: () => require('TimePickerDialogTestModule').TimePickerDialogTestApp }, +{ + appKey: 'TouchBubblingTestAppModule', + component: () => require('TouchBubblingTestAppModule') +}, ]; diff --git a/ReactAndroid/src/androidTest/js/TouchBubblingTestAppModule.js b/ReactAndroid/src/androidTest/js/TouchBubblingTestAppModule.js new file mode 100644 index 00000000000000..e792d2d73b8214 --- /dev/null +++ b/ReactAndroid/src/androidTest/js/TouchBubblingTestAppModule.js @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2013-present, 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 TouchBubblingTestAppModule + */ + +'use strict'; + +var Recording = require('NativeModules').Recording; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); +var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); + +var TouchBubblingTestApp = React.createClass({ + handlePress: function(record) { + Recording.record(record); + }, + render: function() { + return ( + + + + + + + + + + + + + + + + ); + }, +}); + +var styles = StyleSheet.create({ + container: { + flexDirection: 'column', + backgroundColor: '#ccdd44', + }, + element: { + backgroundColor: '#ff0000', + height: 100, + margin: 30, + }, + outer: { + backgroundColor: '#00ff00', + height: 100, + margin: 30, + flexDirection: 'row', + justifyContent: 'space-between', + }, + inner: { + backgroundColor: '#0000ff', + height: 50, + width: 50, + margin: 10, + }, + superinner: { + backgroundColor: '#eeeeee', + height: 20, + width: 20, + } +}); + +module.exports = TouchBubblingTestApp;