Android Instrumentations tests are ready to be run in github/CI open source environment

Reviewed By: mkonicek

Differential Revision: D2769217

fb-gh-sync-id: 7469af816241d8b642753cca21f6542b971e9572
This commit is contained in:
Konstantin Raev 2015-12-21 09:37:27 -08:00 committed by facebook-github-bot-9
parent 040909904c
commit a99c5160ee
26 changed files with 2306 additions and 1 deletions

View File

@ -217,6 +217,8 @@ android {
}
buildConfigField 'boolean', 'IS_INTERNAL_BUILD', 'false'
testApplicationId "com.facebook.react.tests"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
sourceSets.main {
@ -258,6 +260,8 @@ dependencies {
testCompile "org.mockito:mockito-core:${MOCKITO_CORE_VERSION}"
testCompile "org.easytesting:fest-assert-core:${FEST_ASSERT_CORE_VERSION}"
testCompile "org.robolectric:robolectric:${ROBOLECTRIC_VERSION}"
androidTestCompile "com.android.support.test:testing-support-lib:0.1"
}
apply from: 'release.gradle'

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.react.tests"
android:versionCode="1"
android:versionName="1.0" >
<application>
<activity
android:name="com.facebook.react.testing.ReactAppTestActivity"
android:theme="@style/Theme.ReactNative.AppCompat.Light.NoActionBar.FullScreen"/>
</application>
<uses-sdk android:targetSdkVersion="7" />
<supports-screens android:anyDensity="true" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- needed for screenshot tests -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

View File

@ -0,0 +1,136 @@
/**
* @providesModule ScrollViewTestModule
* @jsx React.DOM
*/
'use strict';
var BatchedBridge = require('BatchedBridge');
var React = require('React');
var View = require('View');
var ScrollView = require('ScrollView');
var Text = require('Text');
var StyleSheet = require('StyleSheet');
var TouchableWithoutFeedback = require('TouchableWithoutFeedback');
var ScrollListener = require('NativeModules').ScrollListener;
var NUM_ITEMS = 100;
// Shared by integration tests for ScrollView and HorizontalScrollView
var scrollViewApp;
var Item = React.createClass({
render: function() {
return (
<TouchableWithoutFeedback onPress={this.props.onPress}>
<View style={styles.item_container}>
<Text style={styles.item_text}>{this.props.text}</Text>
</View>
</TouchableWithoutFeedback>
);
},
});
var getInitialState = function() {
var data = [];
for (var i = 0; i < NUM_ITEMS; i++) {
data[i] = {text: 'Item ' + i + '!'};
}
return {
data: data,
};
};
var onScroll = function(e) {
ScrollListener.onScroll(e.nativeEvent.contentOffset.x, e.nativeEvent.contentOffset.y);
};
var onScrollBeginDrag = function(e) {
ScrollListener.onScrollBeginDrag(e.nativeEvent.contentOffset.x, e.nativeEvent.contentOffset.y);
};
var onScrollEndDrag = function(e) {
ScrollListener.onScrollEndDrag(e.nativeEvent.contentOffset.x, e.nativeEvent.contentOffset.y);
};
var onItemPress = function(itemNumber) {
ScrollListener.onItemPress(itemNumber);
};
var ScrollViewTestApp = React.createClass({
getInitialState: getInitialState,
onScroll: onScroll,
onItemPress: onItemPress,
onScrollBeginDrag: onScrollBeginDrag,
onScrollEndDrag: onScrollEndDrag,
scrollTo: function(destX, destY) {
this.refs.scrollView.scrollTo(destY, destX);
},
render: function() {
scrollViewApp = this;
var children = this.state.data.map((item, index) => (
<Item
key={index} text={item.text}
onPress={this.onItemPress.bind(this, index)} />
));
return (
<ScrollView onScroll={this.onScroll} onScrollBeginDrag={this.onScrollBeginDrag} onScrollEndDrag={this.onScrollEndDrag} ref="scrollView">
{children}
</ScrollView>
);
},
});
var HorizontalScrollViewTestApp = React.createClass({
getInitialState: getInitialState,
onScroll: onScroll,
onItemPress: onItemPress,
scrollTo: function(destX, destY) {
this.refs.scrollView.scrollTo(destY, destX);
},
render: function() {
scrollViewApp = this;
var children = this.state.data.map((item, index) => (
<Item
key={index} text={item.text}
onPress={this.onItemPress.bind(this, index)} />
));
return (
<ScrollView horizontal={true} onScroll={this.onScroll} ref="scrollView">
{children}
</ScrollView>
);
},
});
var styles = StyleSheet.create({
item_container: {
padding: 30,
backgroundColor: '#ffffff',
},
item_text: {
flex: 1,
fontSize: 18,
alignSelf: 'center',
},
});
var ScrollViewTestModule = {
ScrollViewTestApp: ScrollViewTestApp,
HorizontalScrollViewTestApp: HorizontalScrollViewTestApp,
scrollTo: function(destX, destY) {
scrollViewApp.scrollTo(destX, destY);
},
};
BatchedBridge.registerCallableModule(
'ScrollViewTestModule',
ScrollViewTestModule
);
module.exports = ScrollViewTestModule;

View File

@ -0,0 +1,30 @@
/**
* 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.
*/
'use strict';
// Disable YellowBox so we do not have to mock its dependencies
console.disableYellowBox = true;
// Include modules used by integration tests
require('ScrollViewTestModule');
// Define catalyst test apps used in integration tests
var AppRegistry = require('AppRegistry');
var apps = [
{
appKey: 'ScrollViewTestApp',
component: () => require('ScrollViewTestModule').ScrollViewTestApp
},
{
appKey: 'HorizontalScrollViewTestApp',
component: () => require('ScrollViewTestModule').HorizontalScrollViewTestApp
},
];
AppRegistry.registerConfig(apps);

View File

@ -0,0 +1,114 @@
/**
* 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.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.testing.ReactInstanceSpecForTest;
import com.facebook.react.testing.ReactAppInstrumentationTestCase;
/**
* Shared by {@link ReactScrollViewTestCase} and {@link ReactHorizontalScrollViewTestCase}.
* See also ScrollViewTestModule.js
*/
public abstract class AbstractScrollViewTestCase extends ReactAppInstrumentationTestCase {
protected ScrollListenerModule mScrollListenerModule;
protected static interface ScrollViewTestModule extends JavaScriptModule {
public void scrollTo(float destX, float destY);
}
@Override
protected void tearDown() throws Exception {
waitForBridgeAndUIIdle(60000);
super.tearDown();
}
@Override
protected ReactInstanceSpecForTest createReactInstanceSpecForTest() {
mScrollListenerModule = new ScrollListenerModule();
return super.createReactInstanceSpecForTest()
.addNativeModule(mScrollListenerModule)
.addJSModule(ScrollViewTestModule.class);
}
// See ScrollViewListenerModule.js
protected static class ScrollListenerModule extends BaseJavaModule {
private final ArrayList<Double> mXOffsets = new ArrayList<Double>();
private final ArrayList<Double> mYOffsets = new ArrayList<Double>();
private final ArrayList<Integer> mItemsPressed = new ArrayList<Integer>();
private final Semaphore mScrollSignaler = new Semaphore(0);
private boolean mScrollBeginDragCalled;
private boolean mScrollEndDragCalled;
// Matches ScrollViewListenerModule.js
@Override
public String getName() {
return "ScrollListener";
}
@ReactMethod
public void onScroll(double x, double y) {
mXOffsets.add(x);
mYOffsets.add(y);
mScrollSignaler.release();
}
@ReactMethod
public void onItemPress(int itemNumber) {
mItemsPressed.add(itemNumber);
}
@ReactMethod
public void onScrollBeginDrag(double x, double y) {
mScrollBeginDragCalled = true;
}
@ReactMethod
public void onScrollEndDrag(double x, double y) {
mScrollEndDragCalled = true;
}
public void waitForScrollIdle() {
while (true) {
try {
boolean gotScrollSignal = mScrollSignaler.tryAcquire(1000, TimeUnit.MILLISECONDS);
if (!gotScrollSignal) {
return;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public ArrayList<Double> getXOffsets() {
return mXOffsets;
}
public ArrayList<Double> getYOffsets() {
return mYOffsets;
}
public ArrayList<Integer> getItemsPressed() {
return mItemsPressed;
}
public boolean dragEventsMatch() {
return mScrollBeginDragCalled && mScrollEndDragCalled;
}
}
}

View File

@ -0,0 +1,57 @@
/**
* 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 com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
/**
* NativeModule for tests that allows assertions from JS to propagate to Java.
*/
public class AssertModule extends BaseJavaModule {
private boolean mGotSuccess;
private boolean mGotFailure;
private @Nullable String mFirstFailureStackTrace;
@Override
public String getName() {
return "Assert";
}
@ReactMethod
public void fail(String stackTrace) {
if (!mGotFailure) {
mGotFailure = true;
mFirstFailureStackTrace = stackTrace;
}
}
@ReactMethod
public void success() {
mGotSuccess = true;
}
/**
* Allows the user of this module to verify that asserts are actually being called from JS and
* that none of them failed.
*/
public void verifyAssertsAndReset() {
assertFalse("First failure: " + mFirstFailureStackTrace, mGotFailure);
assertTrue("Received no assertions during the test!", mGotSuccess);
mGotFailure = false;
mGotSuccess = false;
mFirstFailureStackTrace = null;
}
}

View File

@ -0,0 +1,58 @@
/**
* 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 com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableMap;
/**
* Dummy implementation of storage module, used for testing
*/
public final class FakeAsyncLocalStorage extends BaseJavaModule {
private static WritableMap errorMessage;
static {
errorMessage = Arguments.createMap();
errorMessage.putString("message", "Fake Async Local Storage");
}
@Override
public String getName() {
return "AsyncSQLiteDBStorage";
}
@ReactMethod
public void multiGet(final ReadableArray keys, final Callback callback) {
callback.invoke(errorMessage, null);
}
@ReactMethod
public void multiSet(final ReadableArray keyValueArray, final Callback callback) {
callback.invoke(errorMessage);
}
@ReactMethod
public void multiRemove(final ReadableArray keys, final Callback callback) {
callback.invoke(errorMessage);
}
@ReactMethod
public void clear(Callback callback) {
callback.invoke(errorMessage);
}
@ReactMethod
public void getAllKeys(final Callback callback) {
callback.invoke(errorMessage, null);
}
}

View File

@ -0,0 +1,17 @@
/**
* 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;
/**
* Interface for something that knows how to wait for bridge and UI idle.
*/
public interface IdleWaiter {
void waitForBridgeAndUIIdle();
}

View File

@ -0,0 +1,49 @@
/**
* 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.List;
import android.view.View;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.ReactPackage;
/**
* This class wraps {@class ReactInstanceSpecForTest} in {@class ReactPackage} interface.
* TODO(6788898): Refactor test code to use ReactPackage instead of SpecForTest
*/
public class InstanceSpecForTestPackage implements ReactPackage {
private final ReactInstanceSpecForTest mSpecForTest;
public InstanceSpecForTestPackage(ReactInstanceSpecForTest specForTest) {
mSpecForTest = specForTest;
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext catalystApplicationContext) {
return mSpecForTest.getExtraNativeModulesForTest();
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return mSpecForTest.getExtraJSModulesForTest();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return mSpecForTest.getExtraViewManagers();
}
}

View File

@ -0,0 +1,59 @@
/**
* 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.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactMethod;
/**
* This class is used to verify that some JS integration tests have completed successfully.
* The JS integration tests can be started from a ReactIntegrationTestCase and upon
* finishing successfully the {@link JSIntegrationTestChecker#testDone()} method will be called.
* To verify if the test has completed successfully, call {#link JSIntegrationTestChecker#await()}
* to wait for the test to run, and {#link JSIntegrationTestChecker#isTestDone()} to check if it
* completed successfully.
*/
public class JSIntegrationTestChecker extends BaseJavaModule {
private final CountDownLatch mLatch;
public JSIntegrationTestChecker() {
mLatch = new CountDownLatch(1);
}
@Override
public String getName() {
return "TestModule";
}
@ReactMethod
public void markTestCompleted() {
mLatch.countDown();
}
@ReactMethod
public void verifySnapshot(Callback callback) {
}
public boolean await(long ms) {
try {
return mLatch.await(ms, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignore) {
return false;
}
}
public boolean isTestDone() {
return mLatch.getCount() == 0;
}
}

View File

@ -0,0 +1,47 @@
/**
* 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.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
/**
* Custom implementation of {@link org.junit.runners.model.MultipleFailureException} that includes
* stack information of collected exception as a part of the message.
*/
public class MultipleFailureException extends org.junit.runners.model.MultipleFailureException {
public MultipleFailureException(List<Throwable> errors) {
super(errors);
}
@Override
public String getMessage() {
StringBuilder sb = new StringBuilder();
List<Throwable> errors = getFailures();
sb.append(String.format("There were %d errors:", errors.size()));
int i = 0;
for (Throwable e : errors) {
sb.append(String.format("%n---- Error #%d", i));
sb.append("\n" + getStackTraceAsString(e));
i++;
}
sb.append("\n");
return sb.toString();
}
private static String getStackTraceAsString(Throwable throwable) {
StringWriter stringWriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringWriter));
return stringWriter.toString();
}
}

View File

@ -0,0 +1,138 @@
/**
* 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 com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
/**
* Mock Networking module that records last request received by {@link #sendRequest} method and
* returns reponse code and body that should be set with {@link #setResponse}
*/
public class NetworkRecordingModuleMock extends ReactContextBaseJavaModule {
public int mRequestCount = 0;
public @Nullable String mRequestMethod;
public @Nullable String mRequestURL;
public @Nullable ReadableArray mRequestHeaders;
public @Nullable ReadableMap mRequestData;
public int mLastRequestId;
public boolean mAbortedRequest;
private final boolean mCompleteRequest;
public NetworkRecordingModuleMock(ReactApplicationContext reactContext) {
this(reactContext, true);
}
public NetworkRecordingModuleMock(ReactApplicationContext reactContext, boolean completeRequest) {
super(reactContext);
mCompleteRequest = completeRequest;
}
public static interface RequestListener {
public void onRequest(String method, String url, ReadableArray header, ReadableMap data);
}
private int mResponseCode;
private @Nullable String mResponseBody;
private @Nullable RequestListener mRequestListener;
public void setResponse(int code, String body) {
mResponseCode = code;
mResponseBody = body;
}
public void setRequestListener(RequestListener requestListener) {
mRequestListener = requestListener;
}
@Override
public final String getName() {
return "RCTNetworking";
}
private void fireReactCallback(
Callback callback,
int status,
@Nullable String headers,
@Nullable String body) {
callback.invoke(status, headers, body);
}
@ReactMethod
public final void sendRequest(
String method,
String url,
int requestId,
ReadableArray headers,
ReadableMap data,
boolean incrementalUpdates) {
mLastRequestId = requestId;
mRequestCount++;
mRequestMethod = method;
mRequestURL = url;
mRequestHeaders = headers;
mRequestData = data;
if (mRequestListener != null) {
mRequestListener.onRequest(method, url, headers, data);
}
if (mCompleteRequest) {
onResponseReceived(requestId, mResponseCode, null);
onDataReceived(requestId, mResponseBody);
onRequestComplete(requestId, null);
}
}
@ReactMethod
public void abortRequest(int requestId) {
mLastRequestId = requestId;
mAbortedRequest = true;
}
private void onDataReceived(int requestId, String data) {
WritableArray args = Arguments.createArray();
args.pushInt(requestId);
args.pushString(data);
getEventEmitter().emit("didReceiveNetworkData", args);
}
private void onRequestComplete(int requestId, @Nullable String error) {
WritableArray args = Arguments.createArray();
args.pushInt(requestId);
args.pushString(error);
getEventEmitter().emit("didCompleteNetworkResponse", args);
}
private void onResponseReceived(int requestId, int code, WritableMap headers) {
WritableArray args = Arguments.createArray();
args.pushInt(requestId);
args.pushInt(code);
args.pushMap(headers);
getEventEmitter().emit("didReceiveNetworkResponse", args);
}
private DeviceEventManagerModule.RCTDeviceEventEmitter getEventEmitter() {
return getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
}
}

View File

@ -0,0 +1,166 @@
/**
* 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 java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import android.graphics.Bitmap;
import android.test.ActivityInstrumentationTestCase2;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
/**
* Base class for instrumentation tests that runs React based react application in UI mode
*/
public abstract class ReactAppInstrumentationTestCase extends
ActivityInstrumentationTestCase2<ReactAppTestActivity> implements IdleWaiter {
public ReactAppInstrumentationTestCase() {
super(ReactAppTestActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
final ReactAppTestActivity activity = getActivity();
try {
runTestOnUiThread(new Runnable() {
@Override
public void run() {
activity.loadApp(
getReactApplicationKeyUnderTest(),
createReactInstanceSpecForTest(),
getEnableDevSupport());
}
});
} catch (Throwable t) {
throw new Exception("Unable to load react app", t);
}
waitForBridgeAndUIIdle();
assertTrue("Layout never occurred!", activity.waitForLayout(5000));
waitForBridgeAndUIIdle();
}
@Override
protected void tearDown() throws Exception {
ReactAppTestActivity activity = getActivity();
super.tearDown();
activity.waitForDestroy(5000);
}
public ViewGroup getRootView() {
return (ViewGroup) getActivity().getRootView();
}
/**
* This method isn't safe since it doesn't factor in layout-only view removal. Use
* {@link #getViewByTestId(String)} instead.
*/
@Deprecated
public <T extends View> T getViewAtPath(int... path) {
return ReactTestHelper.getViewAtPath((ViewGroup) getRootView().getParent(), path);
}
public <T extends View> T getViewByTestId(String testID) {
return (T) ReactTestHelper
.getViewWithReactTestId((ViewGroup) getRootView().getParent(), testID);
}
public SingleTouchGestureGenerator createGestureGenerator() {
return new SingleTouchGestureGenerator(getRootView(), this);
}
public void waitForBridgeAndUIIdle() {
getActivity().waitForBridgeAndUIIdle();
}
public void waitForBridgeAndUIIdle(long timeoutMs) {
getActivity().waitForBridgeAndUIIdle(timeoutMs);
}
protected Bitmap getScreenshot() {
// Wait for the UI to settle. If the UI is doing animations, this may be unsafe!
getInstrumentation().waitForIdleSync();
final CountDownLatch latch = new CountDownLatch(1);
final BitmapHolder bitmapHolder = new BitmapHolder();
final Runnable getScreenshotRunnable = new Runnable() {
private static final int MAX_TRIES = 1000;
// This is the constant used in the support library for APIs that don't have Choreographer
private static final int FRAME_DELAY_MS = 10;
private int mNumRuns = 0;
@Override
public void run() {
mNumRuns++;
ReactAppTestActivity activity = getActivity();
if (!activity.isScreenshotReady()) {
if (mNumRuns > MAX_TRIES) {
throw new RuntimeException(
"Waited " + MAX_TRIES + " frames to get screenshot but it's still not ready!");
}
activity.postDelayed(this, FRAME_DELAY_MS);
return;
}
bitmapHolder.bitmap = getActivity().getCurrentScreenshot();
latch.countDown();
}
};
getActivity().runOnUiThread(getScreenshotRunnable);
try {
if (!latch.await(5000, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Timed out waiting for screenshot runnable to run!");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return Assertions.assertNotNull(bitmapHolder.bitmap);
}
/**
* Implement this method to provide application key to be launched. List of available
* application is located in TestBundle.js file
*/
protected abstract String getReactApplicationKeyUnderTest();
protected boolean getEnableDevSupport() {
return false;
}
/**
* Override this method to provide extra native modules to be loaded before the app starts
*/
protected ReactInstanceSpecForTest createReactInstanceSpecForTest() {
return new ReactInstanceSpecForTest();
}
protected ReactContext getReactContext() {
return getActivity().getReactContext();
}
/**
* Helper class to pass the bitmap between execution scopes in {@link #getScreenshot()}.
*/
private static class BitmapHolder {
public @Nullable volatile Bitmap bitmap;
}
}

View File

@ -0,0 +1,236 @@
/**
* 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 java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.LifecycleState;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactRootView;
import com.facebook.react.shell.MainReactPackage;
public class ReactAppTestActivity extends FragmentActivity implements
DefaultHardwareBackBtnHandler
{
private static final String DEFAULT_BUNDLE_NAME = "AndroidTestBundle.js";
private static final int ROOT_VIEW_ID = 8675309;
private static final long IDLE_TIMEOUT_MS = 15000;
private CountDownLatch mLayoutEvent = new CountDownLatch(1);
private @Nullable ReactBridgeIdleSignaler mBridgeIdleSignaler;
private ScreenshotingFrameLayout mScreenshotingFrameLayout;
private final CountDownLatch mDestroyCountDownLatch = new CountDownLatch(1);
private @Nullable ReactInstanceManager mReactInstanceManager;
private @Nullable ReactRootView mReactRootView;
private LifecycleState mLifecycleState = LifecycleState.BEFORE_RESUME;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
overridePendingTransition(0, 0);
// We wrap screenshot layout in another FrameLayout in order to handle custom dimensions of the
// screenshot view set through {@link #setScreenshotDimensions}
FrameLayout rootView = new FrameLayout(this);
setContentView(rootView);
mScreenshotingFrameLayout = new ScreenshotingFrameLayout(this);
mScreenshotingFrameLayout.setId(ROOT_VIEW_ID);
rootView.addView(mScreenshotingFrameLayout);
mReactRootView = new ReactRootView(this);
mScreenshotingFrameLayout.addView(mReactRootView);
}
@Override
protected void onPause() {
super.onPause();
mLifecycleState = LifecycleState.BEFORE_RESUME;
overridePendingTransition(0, 0);
if (mReactInstanceManager != null) {
mReactInstanceManager.onPause();
}
}
@Override
protected void onResume() {
super.onResume();
mLifecycleState = LifecycleState.RESUMED;
if (mReactInstanceManager != null) {
mReactInstanceManager.onResume(this, this);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mDestroyCountDownLatch.countDown();
if (mReactInstanceManager != null) {
mReactInstanceManager.onDestroy();
}
}
public void waitForDestroy(long timeoutMs) throws InterruptedException {
mDestroyCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
}
public void loadApp(String appKey, ReactInstanceSpecForTest spec, boolean enableDevSupport) {
loadApp(appKey, spec, null, DEFAULT_BUNDLE_NAME, enableDevSupport);
}
public void loadApp(String appKey, ReactInstanceSpecForTest spec, String bundleName) {
loadApp(appKey, spec, null, bundleName, false /* = useDevSupport */);
}
public void resetRootViewForScreenshotTests() {
if (mReactInstanceManager != null) {
mReactInstanceManager.onDestroy();
mReactInstanceManager = null;
}
mReactRootView = new ReactRootView(this);
mScreenshotingFrameLayout.removeAllViews();
mScreenshotingFrameLayout.addView(mReactRootView);
}
public void loadApp(
String appKey,
ReactInstanceSpecForTest spec,
@Nullable Bundle initialProps,
String bundleName,
boolean useDevSupport) {
final CountDownLatch currentLayoutEvent = mLayoutEvent = new CountDownLatch(1);
mBridgeIdleSignaler = new ReactBridgeIdleSignaler();
ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
.setApplication(getApplication())
.setBundleAssetName(bundleName)
// By not setting a JS module name, we force the bundle to be always loaded from
// assets, not the devserver, even if dev mode is enabled (such as when testing redboxes).
// This makes sense because we never run the devserver in tests.
//.setJSMainModuleName()
.addPackage(spec.getAlternativeReactPackageForTest() != null ?
spec.getAlternativeReactPackageForTest() : new MainReactPackage())
.addPackage(new InstanceSpecForTestPackage(spec))
.setUseDeveloperSupport(useDevSupport)
.setBridgeIdleDebugListener(mBridgeIdleSignaler)
.setInitialLifecycleState(mLifecycleState);
mReactInstanceManager = builder.build();
mReactInstanceManager.onResume(this, this);
Assertions.assertNotNull(mReactRootView).getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
currentLayoutEvent.countDown();
}
});
Assertions.assertNotNull(mReactRootView)
.startReactApplication(mReactInstanceManager, appKey, initialProps);
}
public boolean waitForLayout(long millis) throws InterruptedException {
return mLayoutEvent.await(millis, TimeUnit.MILLISECONDS);
}
public void waitForBridgeAndUIIdle() {
waitForBridgeAndUIIdle(IDLE_TIMEOUT_MS);
}
public void waitForBridgeAndUIIdle(long timeoutMs) {
ReactIdleDetectionUtil.waitForBridgeAndUIIdle(
Assertions.assertNotNull(mBridgeIdleSignaler),
getReactContext(),
timeoutMs);
}
public View getRootView() {
return Assertions.assertNotNull(mReactRootView);
}
public ReactContext getReactContext() {
return waitForReactContext();
}
// Because react context is created asynchronously, we may have to wait until it is available.
// It's simpler than exposing synchronosition mechanism to notify listener than react context
// creation has completed.
private ReactContext waitForReactContext() {
Assertions.assertNotNull(mReactInstanceManager);
try {
while (true) {
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
if (reactContext != null) {
return reactContext;
}
Thread.sleep(100);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public void postDelayed(Runnable r, int delayMS) {
getRootView().postDelayed(r, delayMS);
}
/**
* Does not ensure that this is run on the UI thread or that the UI Looper is idle like
* {@link ReactAppInstrumentationTestCase#getScreenshot()}. You probably want to use that
* instead.
*/
public Bitmap getCurrentScreenshot() {
return mScreenshotingFrameLayout.getLastDrawnBitmap();
}
public boolean isScreenshotReady() {
return mScreenshotingFrameLayout.isScreenshotReady();
}
public void setScreenshotDimensions(int width, int height) {
mScreenshotingFrameLayout.setLayoutParams(new FrameLayout.LayoutParams(width, height));
}
@Override
public void invokeDefaultOnBackPressed() {
super.onBackPressed();
}
@Override
public void onRequestPermissionsResult(
int requestCode,
String[] permissions,
int[] grantResults) {
}
}

View File

@ -0,0 +1,62 @@
/**
* 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.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
/**
* Utility class that uses {@link NotThreadSafeBridgeIdleDebugListener} interface to allow callers
* to wait for the bridge to be idle.
*/
public class ReactBridgeIdleSignaler implements NotThreadSafeBridgeIdleDebugListener {
// Starts at 1 since bridge starts idle. The logic here is that the semaphore is only acquirable
// if the bridge is idle.
private final Semaphore mBridgeIdleSemaphore = new Semaphore(1);
private volatile boolean mIsBridgeIdle = true;
@Override
public void onTransitionToBridgeIdle() {
mIsBridgeIdle = true;
mBridgeIdleSemaphore.release();
}
@Override
public void onTransitionToBridgeBusy() {
mIsBridgeIdle = false;
try {
if (!mBridgeIdleSemaphore.tryAcquire(15000, TimeUnit.MILLISECONDS)) {
throw new RuntimeException(
"Timed out waiting to acquire the test idle listener semaphore. Deadlock?");
}
} catch (InterruptedException e) {
throw new RuntimeException("Got interrupted", e);
}
}
public boolean isBridgeIdle() {
return mIsBridgeIdle;
}
public boolean waitForIdle(long millis) {
try {
if (mBridgeIdleSemaphore.tryAcquire(millis, TimeUnit.MILLISECONDS)) {
mBridgeIdleSemaphore.release();
return true;
}
return false;
} catch (InterruptedException e) {
throw new RuntimeException("Got interrupted", e);
}
}
}

View File

@ -0,0 +1,125 @@
/**
* 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.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import android.app.Instrumentation;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
import android.view.Choreographer;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.UiThreadUtil;
public class ReactIdleDetectionUtil {
/**
* Waits for both the UI thread and bridge to be idle. It determines this by waiting for the
* bridge to become idle, then waiting for the UI thread to become idle, then checking if the
* bridge is idle again (if the bridge was idle before and is still idle after running the UI
* thread to idle, then there are no more events to process in either place).
* <p/>
* Also waits for any Choreographer callbacks to run after the initial sync since things like UI
* events are initiated from Choreographer callbacks.
*/
public static void waitForBridgeAndUIIdle(
ReactBridgeIdleSignaler idleSignaler,
final ReactContext reactContext,
long timeoutMs) {
UiThreadUtil.assertNotOnUiThread();
long startTime = SystemClock.elapsedRealtime();
waitInner(idleSignaler, timeoutMs);
long timeToWait = Math.max(1, timeoutMs - (SystemClock.elapsedRealtime() - startTime));
waitForChoreographer(timeToWait);
waitForJSIdle(reactContext);
timeToWait = Math.max(1, timeoutMs - (SystemClock.elapsedRealtime() - startTime));
waitInner(idleSignaler, timeToWait);
timeToWait = Math.max(1, timeoutMs - (SystemClock.elapsedRealtime() - startTime));
waitForChoreographer(timeToWait);
}
private static void waitForChoreographer(long timeToWait) {
final int waitFrameCount = 2;
final CountDownLatch latch = new CountDownLatch(1);
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
Choreographer.getInstance().postFrameCallback(
new Choreographer.FrameCallback() {
private int frameCount = 0;
@Override
public void doFrame(long frameTimeNanos) {
frameCount++;
if (frameCount == waitFrameCount) {
latch.countDown();
} else {
Choreographer.getInstance().postFrameCallback(this);
}
}
});
}
});
try {
if (!latch.await(timeToWait, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Timed out waiting for Choreographer");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void waitForJSIdle(ReactContext reactContext) {
if (!reactContext.hasActiveCatalystInstance()) {
return;
}
final CountDownLatch latch = new CountDownLatch(1);
reactContext.runOnJSQueueThread(
new Runnable() {
@Override
public void run() {
latch.countDown();
}
});
try {
if (!latch.await(5000, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Timed out waiting for JS thread");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void waitInner(ReactBridgeIdleSignaler idleSignaler, long timeToWait) {
// TODO gets broken in gradle, do we need it?
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
long startTime = SystemClock.elapsedRealtime();
boolean bridgeWasIdle = false;
while (SystemClock.elapsedRealtime() - startTime < timeToWait) {
boolean bridgeIsIdle = idleSignaler.isBridgeIdle();
if (bridgeIsIdle && bridgeWasIdle) {
return;
}
bridgeWasIdle = bridgeIsIdle;
long newTimeToWait = Math.max(1, timeToWait - (SystemClock.elapsedRealtime() - startTime));
idleSignaler.waitForIdle(newTimeToWait);
instrumentation.waitForIdleSync();
}
throw new RuntimeException("Timed out waiting for bridge and UI idle!");
}
}

View File

@ -0,0 +1,69 @@
/**
* 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 android.annotation.SuppressLint;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.ReactPackage;
/**
* A spec that allows a test to add additional NativeModules/JS modules to the ReactInstance. This
* can also be used to stub out existing native modules by adding another module with the same name
* as a built-in module.
*/
@SuppressLint("JavatestsIncorrectFolder")
public class ReactInstanceSpecForTest {
private final List<NativeModule> mNativeModules = new ArrayList<>();
private final List<Class<? extends JavaScriptModule>> mJSModuleSpecs = new ArrayList<>();
private final List<ViewManager> mViewManagers = new ArrayList<>();
private ReactPackage mReactPackage = null;
public ReactInstanceSpecForTest addNativeModule(NativeModule module) {
mNativeModules.add(module);
return this;
}
public ReactInstanceSpecForTest addJSModule(Class jsClass) {
mJSModuleSpecs.add(jsClass);
return this;
}
public ReactInstanceSpecForTest setPackage(ReactPackage reactPackage) {
mReactPackage = reactPackage;
return this;
}
public ReactInstanceSpecForTest addViewManager(ViewManager viewManager) {
mViewManagers.add(viewManager);
return this;
}
public List<NativeModule> getExtraNativeModulesForTest() {
return mNativeModules;
}
public List<Class<? extends JavaScriptModule>> getExtraJSModulesForTest() {
return mJSModuleSpecs;
}
public ReactPackage getAlternativeReactPackageForTest() {
return mReactPackage;
}
public List<ViewManager> getExtraViewManagers() {
return mViewManagers;
}
}

View File

@ -0,0 +1,212 @@
/**
* 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 java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import android.support.test.InstrumentationRegistry;
import android.test.AndroidTestCase;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.CatalystInstanceImpl;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.futures.SimpleSettableFuture;
import com.facebook.react.modules.core.Timing;
/**
* Use this class for writing integration tests of catalyst. This class will run all JNI call
* within separate android looper, thus you don't need to care about starting your own looper.
*
* Keep in mind that all JS remote method calls and script load calls are asynchronous and you
* should not expect them to return results immediately.
*
* In order to write catalyst integration:
* 1) Make {@link ReactIntegrationTestCase} a base class of your test case
* 2) Use {@link ReactIntegrationTestCase.ReactTestInstanceBuilder}
* instead of {@link com.facebook.react.bridge.CatalystInstanceImpl.Builder} to build catalyst
* instance for testing purposes
*
*/
public abstract class ReactIntegrationTestCase extends AndroidTestCase {
private static final long SETUP_TIMEOUT_MS = 5000;
private static final long IDLE_TIMEOUT_MS = 15000;
private @Nullable CatalystInstanceImpl mInstance;
private @Nullable ReactBridgeIdleSignaler mBridgeIdleSignaler;
private @Nullable ReactApplicationContext mReactContext;
@Override
public ReactApplicationContext getContext() {
if (mReactContext == null) {
mReactContext = new ReactApplicationContext(super.getContext());
Assertions.assertNotNull(mReactContext);
}
return mReactContext;
}
public void shutDownContext() {
if (mInstance != null) {
final ReactContext contextToDestroy = mReactContext;
mReactContext = null;
mInstance = null;
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
if (contextToDestroy != null) {
contextToDestroy.onDestroy();
}
}
});
}
}
/**
* This method isn't safe since it doesn't factor in layout-only view removal. Use
* {@link #getViewByTestId} instead.
*/
@Deprecated
public <T extends View> T getViewAtPath(ViewGroup rootView, int... path) {
return ReactTestHelper.getViewAtPath(rootView, path);
}
public <T extends View> T getViewByTestId(ViewGroup rootView, String testID) {
return (T) ReactTestHelper.getViewWithReactTestId(rootView, testID);
}
public static class Event {
private final CountDownLatch mLatch;
public Event() {
this(1);
}
public Event(int counter) {
mLatch = new CountDownLatch(counter);
}
public void occur() {
mLatch.countDown();
}
public boolean didOccur() {
return mLatch.getCount() == 0;
}
public boolean await(long millis) {
try {
return mLatch.await(millis, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignore) {
return false;
}
}
}
/**
* Timing module needs to be created on the main thread so that it gets the correct Choreographer.
*/
protected Timing createTimingModule() {
final SimpleSettableFuture<Timing> simpleSettableFuture = new SimpleSettableFuture<Timing>();
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
Timing timing = new Timing(getContext());
simpleSettableFuture.set(timing);
}
});
try {
return simpleSettableFuture.get(5000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public class ReactTestInstanceBuilder extends CatalystInstanceImpl.Builder {
@Override
public CatalystInstanceImpl build() {
// Call build in separate looper and wait for it to finish before returning
final Event setupEvent = new Event();
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
mInstance = ReactTestInstanceBuilder.super.build();
mBridgeIdleSignaler = new ReactBridgeIdleSignaler();
mInstance.addBridgeIdleDebugListener(mBridgeIdleSignaler);
getContext().initializeWithInstance(mInstance);
setupEvent.occur();
}
});
if (!setupEvent.await(SETUP_TIMEOUT_MS)) {
throw new RuntimeException(
"Instance setup should take less than " + SETUP_TIMEOUT_MS + "ms");
}
return mInstance;
}
}
public boolean waitForBridgeIdle(long millis) {
return Assertions.assertNotNull(mBridgeIdleSignaler).waitForIdle(millis);
}
public void waitForIdleSync() {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
public void waitForBridgeAndUIIdle() {
ReactIdleDetectionUtil.waitForBridgeAndUIIdle(
Assertions.assertNotNull(mBridgeIdleSignaler),
getContext(),
IDLE_TIMEOUT_MS);
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
shutDownContext();
}
protected static void initializeJavaModule(final BaseJavaModule javaModule) {
final Semaphore semaphore = new Semaphore(0);
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
javaModule.initialize();
if (javaModule instanceof LifecycleEventListener) {
((LifecycleEventListener) javaModule).onHostResume();
}
semaphore.release();
}
});
try {
SoftAssertions.assertCondition(
semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS),
"Timed out initializing timing module");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,32 @@
/**
* 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 com.facebook.react.modules.debug.DeveloperSettings;
/**
* Default ReactSettings for tests.
*/
public class ReactSettingsForTests implements DeveloperSettings {
@Override
public boolean isFpsDebugEnabled() {
return false;
}
@Override
public boolean isAnimationFpsDebugEnabled() {
return false;
}
@Override
public boolean isJSDevModeEnabled() {
return true;
}
}

View File

@ -0,0 +1,129 @@
/**
* 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.view.View;
import android.view.ViewGroup;
import com.facebook.react.bridge.CatalystInstanceImpl;
import com.facebook.react.bridge.JSBundleLoader;
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
import com.facebook.react.bridge.JSCJavaScriptExecutor;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.NativeModuleRegistry;
import com.facebook.react.bridge.JavaScriptModulesConfig;
import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec;
import com.android.internal.util.Predicate;
public class ReactTestHelper {
public static class ReactInstanceEasyBuilder {
private final ReactIntegrationTestCase mTestCase;
private final NativeModuleRegistry.Builder mNativeModuleRegistryBuilder;
private final JavaScriptModulesConfig.Builder mJSModulesConfigBuilder;
private ReactInstanceEasyBuilder(ReactIntegrationTestCase testCase) {
mTestCase = testCase;
mNativeModuleRegistryBuilder = new NativeModuleRegistry.Builder();
mJSModulesConfigBuilder = new JavaScriptModulesConfig.Builder();
}
public CatalystInstanceImpl build() {
CatalystInstanceImpl instance = mTestCase.new ReactTestInstanceBuilder()
.setCatalystQueueConfigurationSpec(CatalystQueueConfigurationSpec.createDefault())
.setJSExecutor(new JSCJavaScriptExecutor())
.setRegistry(mNativeModuleRegistryBuilder.build())
.setJSModulesConfig(mJSModulesConfigBuilder.build())
.setJSBundleLoader(JSBundleLoader.createFileLoader(
mTestCase.getContext(),
"assets://AndroidTestBundle.js"))
.setNativeModuleCallExceptionHandler(
new NativeModuleCallExceptionHandler() {
@Override
public void handleException(Exception e) {
throw new RuntimeException(e);
}
})
.build();
instance.runJSBundle();
mTestCase.waitForBridgeAndUIIdle();
return instance;
}
public ReactInstanceEasyBuilder addNativeModule(NativeModule module) {
mNativeModuleRegistryBuilder.add(module);
return this;
}
public ReactInstanceEasyBuilder addJSModule(Class moduleInterfaceClass) {
mJSModulesConfigBuilder.add(moduleInterfaceClass);
return this;
}
}
public static ReactInstanceEasyBuilder catalystInstanceBuilder(
ReactIntegrationTestCase testCase) {
return new ReactInstanceEasyBuilder(testCase);
}
/**
* Gets the view at given path in the UI hierarchy, ignoring modals.
*/
public static <T extends View> T getViewAtPath(ViewGroup rootView, int... path) {
// The application root element is wrapped in a helper view in order
// to be able to display modals. See renderApplication.js.
ViewGroup appWrapperView = rootView;
View view = appWrapperView.getChildAt(0);
for (int i = 0; i < path.length; i++) {
view = ((ViewGroup) view).getChildAt(path[i]);
}
return (T) view;
}
/**
* Gets the view with a given react test ID in the UI hierarchy. React test ID is currently
* propagated into view content description.
*/
public static View getViewWithReactTestId(View rootView, String testId) {
return findChild(rootView, hasTagValue(testId));
}
public static String getTestId(View view) {
return view.getTag() instanceof String ? (String) view.getTag() : null;
}
private static View findChild(View root, Predicate<View> predicate) {
if (predicate.apply(root)) {
return root;
}
if (root instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) root;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
View result = findChild(child, predicate);
if (result != null) {
return result;
}
}
}
return null;
}
private static Predicate<View> hasTagValue(final String tagValue) {
return new Predicate<View>() {
@Override
public boolean apply(View view) {
Object tag = view.getTag();
return tag != null && tag.equals(tagValue);
}
};
}
}

View File

@ -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();
}
}

View File

@ -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.
* <p>
* 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);
}
}

View File

@ -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<String> mCalls = new ArrayList<String>();
@Override
public String getName() {
return "Recording";
}
@ReactMethod
public void record(String text) {
mCalls.add(text);
}
public void reset() {
mCalls.clear();
}
public List<String> getCalls() {
return mCalls;
}
}

View File

@ -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<Double> 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<Double> xOffsets = mScrollListenerModule.getXOffsets();
ArrayList<Integer> 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());
}
}

View File

@ -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<Double> 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<Double> yOffsets = mScrollListenerModule.getYOffsets();
ArrayList<Integer> 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());
}
}

View File

@ -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