mirror of
https://github.com/status-im/react-native.git
synced 2025-02-04 13:44:04 +00:00
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:
parent
040909904c
commit
a99c5160ee
@ -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'
|
||||
|
17
ReactAndroid/src/androidTest/AndroidManifest.xml
Normal file
17
ReactAndroid/src/androidTest/AndroidManifest.xml
Normal 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>
|
136
ReactAndroid/src/androidTest/assets/ScrollViewTestModule.js
Normal file
136
ReactAndroid/src/androidTest/assets/ScrollViewTestModule.js
Normal 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;
|
30
ReactAndroid/src/androidTest/assets/TestBundle.js
Normal file
30
ReactAndroid/src/androidTest/assets/TestBundle.js
Normal 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);
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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!");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user