() {
+ @Override
+ public boolean apply(View view) {
+ Object tag = view.getTag();
+ return tag != null && tag.equals(tagValue);
+ }
+ };
+ }
+}
diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ScreenshotingFrameLayout.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ScreenshotingFrameLayout.java
new file mode 100644
index 000000000..ffc648e79
--- /dev/null
+++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/ScreenshotingFrameLayout.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2014-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.testing;
+
+import javax.annotation.Nullable;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.os.Looper;
+import android.widget.FrameLayout;
+
+/**
+ * A FrameLayout that allows you to access the result of the last time its hierarchy was drawn. It
+ * accomplishes this by drawing its hierarchy into a software Canvas, saving the resulting Bitmap
+ * and then drawing that Bitmap to the actual Canvas provided by the system.
+ */
+public class ScreenshotingFrameLayout extends FrameLayout {
+
+ private @Nullable Bitmap mBitmap;
+ private Canvas mCanvas;
+
+ public ScreenshotingFrameLayout(Context context) {
+ super(context);
+ mCanvas = new Canvas();
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (mBitmap == null) {
+ mBitmap = createNewBitmap(canvas);
+ mCanvas.setBitmap(mBitmap);
+ } else if (mBitmap.getWidth() != canvas.getWidth() ||
+ mBitmap.getHeight() != canvas.getHeight()) {
+ mBitmap.recycle();
+ mBitmap = createNewBitmap(canvas);
+ mCanvas.setBitmap(mBitmap);
+ }
+
+ super.dispatchDraw(mCanvas);
+ canvas.drawBitmap(mBitmap, 0, 0, null);
+ }
+
+ private static Bitmap createNewBitmap(Canvas canvas) {
+ return Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
+ }
+
+ public Bitmap getLastDrawnBitmap() {
+ if (mBitmap == null) {
+ throw new RuntimeException("View has not been drawn yet!");
+ }
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ throw new RuntimeException(
+ "Must access screenshots from main thread or you may get partially drawn Bitmaps");
+ }
+ if (!isScreenshotReady()) {
+ throw new RuntimeException("Trying to get screenshot, but the view is dirty or needs layout");
+ }
+ return Bitmap.createBitmap(mBitmap);
+ }
+
+ public boolean isScreenshotReady() {
+ return !isDirty() && !isLayoutRequested();
+ }
+}
diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/SingleTouchGestureGenerator.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/SingleTouchGestureGenerator.java
new file mode 100644
index 000000000..278facbcb
--- /dev/null
+++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/SingleTouchGestureGenerator.java
@@ -0,0 +1,161 @@
+/**
+ * Copyright (c) 2014-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.testing;
+
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+/**
+ * Provides methods for generating touch events and dispatching them directly to a given view.
+ * Events scenarios are based on {@link android.test.TouchUtils} but they get gets dispatched
+ * directly through the view hierarchy using {@link View#dispatchTouchEvent} method instead of
+ * using instrumentation API.
+ *
+ * All the events for a gesture are dispatched immediately which makes tests run very fast.
+ * The eventTime for each event is still set correctly. Android's gesture recognizers check
+ * eventTime in order to figure out gesture speed, and therefore scroll vs fling is recognized.
+ */
+public class SingleTouchGestureGenerator {
+
+ private static final long DEFAULT_DELAY_MS = 20;
+
+ private View mDispatcherView;
+ private IdleWaiter mIdleWaiter;
+ private long mLastDownTime;
+ private long mEventTime;
+ private float mLastX;
+ private float mLastY;
+
+ private ViewConfiguration mViewConfig;
+
+ public SingleTouchGestureGenerator(View view, IdleWaiter idleWaiter) {
+ mDispatcherView = view;
+ mIdleWaiter = idleWaiter;
+ mViewConfig = ViewConfiguration.get(view.getContext());
+ }
+
+ private SingleTouchGestureGenerator dispatchEvent(
+ final int action,
+ final float x,
+ final float y,
+ long eventTime) {
+ mEventTime = eventTime;
+ if (action == MotionEvent.ACTION_DOWN) {
+ mLastDownTime = eventTime;
+ }
+ mLastX = x;
+ mLastY = y;
+ mDispatcherView.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ MotionEvent event = MotionEvent.obtain(mLastDownTime, mEventTime, action, x, y, 0);
+ mDispatcherView.dispatchTouchEvent(event);
+ event.recycle();
+ }
+ });
+ mIdleWaiter.waitForBridgeAndUIIdle();
+ return this;
+ }
+
+ private float getViewCenterX(View view) {
+ int[] xy = new int[2];
+ view.getLocationOnScreen(xy);
+ int viewWidth = view.getWidth();
+ return xy[0] + (viewWidth / 2.0f);
+ }
+
+ private float getViewCenterY(View view) {
+ int[] xy = new int[2];
+ view.getLocationOnScreen(xy);
+ int viewHeight = view.getHeight();
+ return xy[1] + (viewHeight / 2.0f);
+ }
+
+ public SingleTouchGestureGenerator startGesture(float x, float y) {
+ return dispatchEvent(MotionEvent.ACTION_DOWN, x, y, SystemClock.uptimeMillis());
+ }
+
+ public SingleTouchGestureGenerator startGesture(View view) {
+ return startGesture(getViewCenterX(view), getViewCenterY(view));
+ }
+
+ private SingleTouchGestureGenerator dispatchDelayedEvent(
+ int action,
+ float x,
+ float y,
+ long delay) {
+ return dispatchEvent(action, x, y, mEventTime + delay);
+ }
+
+ public SingleTouchGestureGenerator endGesture(float x, float y, long delay) {
+ return dispatchDelayedEvent(MotionEvent.ACTION_UP, x, y, delay);
+ }
+
+ public SingleTouchGestureGenerator endGesture(float x, float y) {
+ return endGesture(x, y, DEFAULT_DELAY_MS);
+ }
+
+ public SingleTouchGestureGenerator endGesture() {
+ return endGesture(mLastX, mLastY);
+ }
+
+ public SingleTouchGestureGenerator moveGesture(float x, float y, long delay) {
+ return dispatchDelayedEvent(MotionEvent.ACTION_MOVE, x, y, delay);
+ }
+
+ public SingleTouchGestureGenerator moveBy(float dx, float dy, long delay) {
+ return moveGesture(mLastX + dx, mLastY + dy, delay);
+ }
+
+ public SingleTouchGestureGenerator moveBy(float dx, float dy) {
+ return moveBy(dx, dy, DEFAULT_DELAY_MS);
+ }
+
+ public SingleTouchGestureGenerator clickViewAt(float x, float y) {
+ float touchSlop = mViewConfig.getScaledTouchSlop();
+ return startGesture(x, y).moveBy(touchSlop / 2.0f, touchSlop / 2.0f).endGesture();
+ }
+
+ public SingleTouchGestureGenerator drag(
+ float fromX,
+ float fromY,
+ float toX,
+ float toY,
+ int stepCount,
+ long totalDelay) {
+
+ float xStep = (toX - fromX) / stepCount;
+ float yStep = (toY - fromY) / stepCount;
+
+ float x = fromX;
+ float y = fromY;
+
+ for (int i = 0; i < stepCount; i++) {
+ x += xStep;
+ y += yStep;
+ moveGesture(x, y, totalDelay / stepCount);
+ }
+ return this;
+ }
+
+ public SingleTouchGestureGenerator dragTo(float toX, float toY, int stepCount, long totalDelay) {
+ return drag(mLastX, mLastY, toX, toY, stepCount, totalDelay);
+ }
+
+ public SingleTouchGestureGenerator dragTo(View view, int stepCount, long totalDelay) {
+ return dragTo(getViewCenterX(view), getViewCenterY(view), stepCount, totalDelay);
+ }
+
+ public SingleTouchGestureGenerator dragTo(View view, int stepCount) {
+ return dragTo(view, stepCount, stepCount * DEFAULT_DELAY_MS);
+ }
+}
diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/StringRecordingModule.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/StringRecordingModule.java
new file mode 100644
index 000000000..8caa3b8f7
--- /dev/null
+++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/StringRecordingModule.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2014-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.testing;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.facebook.react.bridge.BaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+
+/**
+ * Native module provides single method {@link #record} which records its single string argument
+ * in calls array
+ */
+public class StringRecordingModule extends BaseJavaModule {
+
+ private final List mCalls = new ArrayList();
+
+ @Override
+ public String getName() {
+ return "Recording";
+ }
+
+ @ReactMethod
+ public void record(String text) {
+ mCalls.add(text);
+ }
+
+ public void reset() {
+ mCalls.clear();
+ }
+
+ public List getCalls() {
+ return mCalls;
+ }
+}
diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ReactHorizontalScrollViewTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ReactHorizontalScrollViewTestCase.java
new file mode 100644
index 000000000..963442d47
--- /dev/null
+++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ReactHorizontalScrollViewTestCase.java
@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2014-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 java.util.ArrayList;
+
+import android.view.View;
+import android.widget.HorizontalScrollView;
+
+import com.facebook.react.testing.AbstractScrollViewTestCase;
+import com.facebook.react.testing.SingleTouchGestureGenerator;
+import com.facebook.react.uimanager.PixelUtil;
+
+/**
+ * Integration test for horizontal ScrollView.
+ * See ScrollViewTestModule.js
+ */
+public class ReactHorizontalScrollViewTestCase extends AbstractScrollViewTestCase {
+
+ @Override
+ protected String getReactApplicationKeyUnderTest() {
+ return "HorizontalScrollViewTestApp";
+ }
+
+ private void dragLeft() {
+ dragLeft(200);
+ }
+
+ private void dragLeft(int durationMs) {
+ createGestureGenerator()
+ .startGesture(150, 50)
+ .dragTo(50, 60, 10, durationMs)
+ .endGesture(50, 60);
+ }
+
+ public void testScrolling() {
+ HorizontalScrollView scrollView = getViewAtPath(0);
+ assertNotNull(scrollView);
+ assertEquals(0, scrollView.getScrollX());
+
+ dragLeft();
+
+ assertTrue("Expected to scroll by at least 50 pixels", scrollView.getScrollX() >= 50);
+ }
+
+ public void testScrollEvents() {
+ HorizontalScrollView scrollView = getViewAtPath(0);
+
+ dragLeft();
+
+ waitForBridgeAndUIIdle();
+ mScrollListenerModule.waitForScrollIdle();
+ waitForBridgeAndUIIdle();
+
+ ArrayList xOffsets = mScrollListenerModule.getXOffsets();
+ assertFalse("Expected to receive at least one scroll event", xOffsets.isEmpty());
+ assertTrue("Expected offset to be greater than 0", xOffsets.get(xOffsets.size() - 1) > 0);
+ assertTrue(
+ "Expected no item click event fired",
+ mScrollListenerModule.getItemsPressed().isEmpty());
+ assertEquals(
+ "Expected last offset to be offset of scroll view",
+ PixelUtil.toDIPFromPixel(scrollView.getScrollX()),
+ xOffsets.get(xOffsets.size() - 1).doubleValue(),
+ 1e-5);
+ }
+
+ public void testScrollAndClick() throws Exception {
+ SingleTouchGestureGenerator gestureGenerator = createGestureGenerator();
+
+ // Slowly drag the ScrollView to prevent fling
+ dragLeft(15000);
+
+ waitForBridgeAndUIIdle();
+ getInstrumentation().waitForIdleSync();
+
+ // Find visible item to be clicked
+ View visibleItem = null;
+ int visibleItemNumber = 0;
+ for (; visibleItemNumber < 100; visibleItemNumber++) {
+ visibleItem = getViewAtPath(0, 0, visibleItemNumber);
+ int pos[] = new int[2];
+ visibleItem.getLocationInWindow(pos);
+ if (pos[0] >= 0) {
+ break;
+ }
+ }
+
+ // Click first visible item
+ gestureGenerator.startGesture(visibleItem).endGesture();
+ waitForBridgeAndUIIdle();
+
+ ArrayList xOffsets = mScrollListenerModule.getXOffsets();
+ ArrayList itemIds = mScrollListenerModule.getItemsPressed();
+ assertFalse("Expected to receive at least one scroll event", xOffsets.isEmpty());
+ assertTrue("Expected offset to be greater than 0", xOffsets.get(xOffsets.size() - 1) > 0);
+ assertEquals("Expected to receive exactly one item click event", 1, itemIds.size());
+ assertEquals(visibleItemNumber, (int) itemIds.get(0));
+ }
+
+ /**
+ * Verify that 'scrollTo' command makes ScrollView start scrolling
+ */
+ public void testScrollToCommand() throws Exception {
+ HorizontalScrollView scrollView = getViewAtPath(0);
+ ScrollViewTestModule jsModule =
+ getReactContext().getCatalystInstance().getJSModule(ScrollViewTestModule.class);
+
+ assertEquals(0, scrollView.getScrollX());
+
+ jsModule.scrollTo(300, 0);
+ waitForBridgeAndUIIdle();
+ getInstrumentation().waitForIdleSync();
+
+ // Unfortunately we need to use timeouts here in order to wait for scroll animation to happen
+ // there is no better way (yet) for waiting for scroll animation to finish
+ long timeout = 10000;
+ long interval = 50;
+ long start = System.currentTimeMillis();
+ while (System.currentTimeMillis() - start < timeout) {
+ if (scrollView.getScrollX() > 0) {
+ break;
+ }
+ Thread.sleep(interval);
+ }
+ assertNotSame(0, scrollView.getScrollX());
+ }
+}
diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ReactScrollViewTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ReactScrollViewTestCase.java
new file mode 100644
index 000000000..45a47a673
--- /dev/null
+++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ReactScrollViewTestCase.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2014-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 java.util.ArrayList;
+
+import android.view.View;
+import android.widget.ScrollView;
+
+import com.facebook.react.testing.AbstractScrollViewTestCase;
+import com.facebook.react.testing.SingleTouchGestureGenerator;
+import com.facebook.react.uimanager.PixelUtil;
+
+/**
+ * Integration test for vertical ScrollView.
+ * See ScrollViewTestModule.js
+ */
+public class ReactScrollViewTestCase extends AbstractScrollViewTestCase {
+
+ @Override
+ protected String getReactApplicationKeyUnderTest() {
+ return "ScrollViewTestApp";
+ }
+
+ private void dragUp() {
+ dragUp(200);
+ }
+
+ private void dragUp(int durationMs) {
+ createGestureGenerator()
+ .startGesture(200, 200)
+ .dragTo(180, 100, 10, durationMs)
+ .endGesture(180, 100);
+ }
+
+ public void testScrolling() {
+ ScrollView scrollView = getViewAtPath(0);
+ assertNotNull(scrollView);
+ assertEquals(0, scrollView.getScrollY());
+
+ dragUp();
+
+ assertTrue("Expected to scroll by at least 50 pixels", scrollView.getScrollY() >= 50);
+ }
+
+ public void testScrollEvents() {
+ ScrollView scrollView = getViewAtPath(0);
+
+ dragUp();
+
+ waitForBridgeAndUIIdle();
+ mScrollListenerModule.waitForScrollIdle();
+ waitForBridgeAndUIIdle();
+
+ ArrayList yOffsets = mScrollListenerModule.getYOffsets();
+ assertFalse("Expected to receive at least one scroll event", yOffsets.isEmpty());
+ assertTrue("Expected offset to be greater than 0", yOffsets.get(yOffsets.size() - 1) > 0);
+ assertTrue(
+ "Expected no item click event fired",
+ mScrollListenerModule.getItemsPressed().isEmpty());
+ assertEquals(
+ "Expected last offset to be offset of scroll view",
+ PixelUtil.toDIPFromPixel(scrollView.getScrollY()),
+ yOffsets.get(yOffsets.size() - 1).doubleValue(),
+ 1e-5);
+ assertTrue("Begin and End Drag should be called", mScrollListenerModule.dragEventsMatch());
+ }
+
+ public void testScrollAndClick() throws Exception {
+ SingleTouchGestureGenerator gestureGenerator = createGestureGenerator();
+
+ // Slowly drag the ScrollView to prevent fling
+ dragUp(15000);
+
+ waitForBridgeAndUIIdle();
+ getInstrumentation().waitForIdleSync();
+
+ // Find visible item to be clicked
+ View visibleItem = null;
+ int visibleItemNumber = 0;
+ for (; visibleItemNumber < 100; visibleItemNumber++) {
+ visibleItem = getViewAtPath(0, 0, visibleItemNumber);
+ int pos[] = new int[2];
+ visibleItem.getLocationInWindow(pos);
+ if (pos[1] >= 0) {
+ break;
+ }
+ }
+
+ // Click first visible item
+ gestureGenerator.startGesture(visibleItem).endGesture();
+ waitForBridgeAndUIIdle();
+
+ ArrayList yOffsets = mScrollListenerModule.getYOffsets();
+ ArrayList itemIds = mScrollListenerModule.getItemsPressed();
+ assertFalse("Expected to receive at least one scroll event", yOffsets.isEmpty());
+ assertTrue("Expected offset to be greater than 0", yOffsets.get(yOffsets.size() - 1) > 0);
+ assertEquals("Expected to receive exactly one item click event", 1, itemIds.size());
+ assertEquals(visibleItemNumber, (int) itemIds.get(0));
+ }
+
+ /**
+ * Verify that 'scrollTo' command makes ScrollView start scrolling
+ */
+ public void testScrollToCommand() throws Exception {
+ ScrollView scrollView = getViewAtPath(0);
+ ScrollViewTestModule jsModule =
+ getReactContext().getCatalystInstance().getJSModule(ScrollViewTestModule.class);
+
+ assertEquals(0, scrollView.getScrollY());
+
+ jsModule.scrollTo(0, 300);
+ waitForBridgeAndUIIdle();
+ getInstrumentation().waitForIdleSync();
+
+ // Unfortunately we need to use timeouts here in order to wait for scroll animation to happen
+ // there is no better way (yet) for waiting for scroll animation to finish
+ long timeout = 10000;
+ long interval = 50;
+ long start = System.currentTimeMillis();
+ while (System.currentTimeMillis() - start < timeout) {
+ if (scrollView.getScrollY() > 0) {
+ break;
+ }
+ Thread.sleep(interval);
+ }
+ assertNotSame(0, scrollView.getScrollY());
+ assertFalse("Drag should not be called with scrollTo", mScrollListenerModule.dragEventsMatch());
+ }
+}
diff --git a/circle.yml b/circle.yml
index 51feb2c00..ff3c01145 100644
--- a/circle.yml
+++ b/circle.yml
@@ -9,4 +9,8 @@ dependencies:
test:
override:
# gradle is flaky in CI envs, found a solution here http://stackoverflow.com/questions/28409608/gradle-assembledebug-and-predexdebug-fail-with-circleci
- - TERM=dumb ./gradlew cleanTest test -PpreDexEnable=false -Pcom.android.build.threadPoolSize=1 -Dorg.gradle.parallel=false -Dorg.gradle.jvmargs="-Xms512m -Xmx512m" -Dorg.gradle.daemon=false
+ - TERM=dumb ./gradlew testDebugUnitTest -PpreDexEnable=false -Pcom.android.build.threadPoolSize=1 -Dorg.gradle.parallel=false -Dorg.gradle.jvmargs="-Xms512m -Xmx512m" -Dorg.gradle.daemon=false
+ # build JS bundle
+ - node local-cli/cli.js bundle --platform android --dev true --entry-file ReactAndroid/src/androidTest/assets/TestBundle.js --bundle-output ReactAndroid/src/androidTest/assets/AndroidTestBundle.js
+ # run instrumentation tests on device
+ - TERM=dumb ./gradlew connectedCheck