mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-29 12:45:37 +08: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:
committed by
facebook-github-bot-9
parent
040909904c
commit
a99c5160ee
@@ -217,6 +217,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildConfigField 'boolean', 'IS_INTERNAL_BUILD', 'false'
|
buildConfigField 'boolean', 'IS_INTERNAL_BUILD', 'false'
|
||||||
|
testApplicationId "com.facebook.react.tests"
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets.main {
|
sourceSets.main {
|
||||||
@@ -258,6 +260,8 @@ dependencies {
|
|||||||
testCompile "org.mockito:mockito-core:${MOCKITO_CORE_VERSION}"
|
testCompile "org.mockito:mockito-core:${MOCKITO_CORE_VERSION}"
|
||||||
testCompile "org.easytesting:fest-assert-core:${FEST_ASSERT_CORE_VERSION}"
|
testCompile "org.easytesting:fest-assert-core:${FEST_ASSERT_CORE_VERSION}"
|
||||||
testCompile "org.robolectric:robolectric:${ROBOLECTRIC_VERSION}"
|
testCompile "org.robolectric:robolectric:${ROBOLECTRIC_VERSION}"
|
||||||
|
|
||||||
|
androidTestCompile "com.android.support.test:testing-support-lib:0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: 'release.gradle'
|
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:
|
test:
|
||||||
override:
|
override:
|
||||||
# gradle is flaky in CI envs, found a solution here http://stackoverflow.com/questions/28409608/gradle-assembledebug-and-predexdebug-fail-with-circleci
|
# 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
|
||||||
|
|||||||
Reference in New Issue
Block a user