Move ReactNativeTestRule to OSS
Summary: And migrated ReactRootViewTestCase to use ReactNativeTestRule. Reviewed By: mdvacca Differential Revision: D9557362 fbshipit-source-id: 1469d0ad8c125b5ea729371d81956e61780c56cf
This commit is contained in:
parent
bf8e1b4ffa
commit
afe0843bee
|
@ -3,7 +3,7 @@ load("//ReactNative:DEFS.bzl", "react_native_dep", "react_native_integration_tes
|
|||
rn_android_library(
|
||||
name = "testing",
|
||||
srcs = glob(
|
||||
["**/*.java"],
|
||||
["*.java"],
|
||||
exclude = [
|
||||
"idledetection/**/*.java",
|
||||
"network/**/*.java",
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# BUILD FILE SYNTAX: SKYLARK
|
||||
load(
|
||||
"@xplat//ReactNative:DEFS.bzl",
|
||||
"react_native_dep",
|
||||
"react_native_integration_tests_target",
|
||||
"react_native_target",
|
||||
"rn_android_library",
|
||||
)
|
||||
|
||||
rn_android_library(
|
||||
name = "rule",
|
||||
srcs = glob(["*.java"]),
|
||||
visibility = [
|
||||
"PUBLIC",
|
||||
],
|
||||
deps = [
|
||||
react_native_dep("java/com/facebook/testing/instrumentation:instrumentation"),
|
||||
react_native_dep("java/com/facebook/testing/instrumentation/base:base"),
|
||||
react_native_dep("third-party/java/espresso:espresso"),
|
||||
react_native_dep("third-party/java/jsr-305:jsr-305"),
|
||||
react_native_dep("third-party/java/junit:junit"),
|
||||
react_native_dep("third-party/java/testing-support-lib:testing-support-lib"),
|
||||
react_native_dep("third-party/android/support/v4:lib-support-v4"),
|
||||
react_native_dep("third-party/android/support/v7/appcompat-orig:appcompat"),
|
||||
react_native_dep("third-party/java/jsr-305:jsr-305"),
|
||||
react_native_integration_tests_target("java/com/facebook/react/testing:testing"),
|
||||
react_native_integration_tests_target("java/com/facebook/react/testing/idledetection:idledetection"),
|
||||
react_native_target("java/com/facebook/react:react"),
|
||||
react_native_target("java/com/facebook/react/bridge:bridge"),
|
||||
react_native_target("java/com/facebook/react/common:common"),
|
||||
react_native_target("java/com/facebook/react/modules/core:core"),
|
||||
react_native_target("java/com/facebook/react/shell:shell"),
|
||||
react_native_target("java/com/facebook/react/uimanager:uimanager"),
|
||||
],
|
||||
)
|
|
@ -0,0 +1,177 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.react.testing.rule;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.support.test.rule.ActivityTestRule;
|
||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.common.LifecycleState;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
import com.facebook.react.testing.ReactInstanceSpecForTest;
|
||||
import com.facebook.react.testing.ReactTestHelper;
|
||||
import com.facebook.react.testing.idledetection.ReactBridgeIdleSignaler;
|
||||
import com.facebook.react.testing.idledetection.ReactIdleDetectionUtil;
|
||||
import com.facebook.react.uimanager.ReactShadowNode;
|
||||
import com.facebook.react.uimanager.UIImplementation;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
/** A test rule to simplify React Native rendering tests. */
|
||||
public class ReactNativeTestRule implements TestRule {
|
||||
|
||||
// we need a bigger timeout for CI builds because they run on a slow emulator
|
||||
private static final long IDLE_TIMEOUT_MS = 120000;
|
||||
|
||||
@Rule public ActivityTestRule<Activity> mActivityRule = new ActivityTestRule<>(Activity.class);
|
||||
|
||||
private final String mBundleName;
|
||||
private ReactPackage mReactPackage;
|
||||
private ReactInstanceManager mReactInstanceManager;
|
||||
private ReactBridgeIdleSignaler mBridgeIdleSignaler;
|
||||
private ReactRootView mView;
|
||||
private CountDownLatch mLatch;
|
||||
|
||||
public ReactNativeTestRule(String bundleName) {
|
||||
this(bundleName, null);
|
||||
}
|
||||
|
||||
public ReactNativeTestRule(String bundleName, ReactPackage reactPackage) {
|
||||
mBundleName = bundleName;
|
||||
mReactPackage = reactPackage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement apply(final Statement base, final Description description) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
setUp();
|
||||
base.evaluate();
|
||||
tearDown();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void setUp() {
|
||||
final Activity activity = mActivityRule.launchActivity(null);
|
||||
mView = new ReactRootView(activity);
|
||||
|
||||
activity.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mBridgeIdleSignaler = new ReactBridgeIdleSignaler();
|
||||
mReactInstanceManager =
|
||||
ReactTestHelper.getReactTestFactory()
|
||||
.getReactInstanceManagerBuilder()
|
||||
.setApplication(activity.getApplication())
|
||||
.setBundleAssetName(mBundleName)
|
||||
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE)
|
||||
.setBridgeIdleDebugListener(mBridgeIdleSignaler)
|
||||
.addPackage(mReactPackage != null ? mReactPackage : new MainReactPackage())
|
||||
.build();
|
||||
mReactInstanceManager.onHostResume(activity);
|
||||
|
||||
// This threading garbage will be replaced by Surface
|
||||
final AtomicBoolean isLayoutUpdated = new AtomicBoolean(false);
|
||||
mReactInstanceManager.addReactInstanceEventListener(
|
||||
new ReactInstanceManager.ReactInstanceEventListener() {
|
||||
@Override
|
||||
public void onReactContextInitialized(ReactContext reactContext) {
|
||||
final UIManagerModule uiManagerModule =
|
||||
reactContext.getCatalystInstance().getNativeModule(UIManagerModule.class);
|
||||
uiManagerModule
|
||||
.getUIImplementation()
|
||||
.setLayoutUpdateListener(
|
||||
new UIImplementation.LayoutUpdateListener() {
|
||||
@Override
|
||||
public void onLayoutUpdated(ReactShadowNode reactShadowNode) {
|
||||
uiManagerModule.getUIImplementation().removeLayoutUpdateListener();
|
||||
isLayoutUpdated.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
mView
|
||||
.getViewTreeObserver()
|
||||
.addOnGlobalLayoutListener(
|
||||
new OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
if (isLayoutUpdated.get()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
} else {
|
||||
mView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
mLatch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void tearDown() {
|
||||
final ReactRootView view = mView;
|
||||
final ReactInstanceManager reactInstanceManager = mReactInstanceManager;
|
||||
mView = null;
|
||||
mReactInstanceManager = null;
|
||||
mActivityRule
|
||||
.getActivity()
|
||||
.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
view.unmountReactApplication();
|
||||
reactInstanceManager.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Renders the react component and waits until the layout has completed before returning */
|
||||
public void render(final String componentName) {
|
||||
mLatch = new CountDownLatch(1);
|
||||
final Activity activity = mActivityRule.getActivity();
|
||||
activity.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ReactRootView view = getView();
|
||||
view.startReactApplication(mReactInstanceManager, componentName);
|
||||
activity.setContentView(view);
|
||||
}
|
||||
});
|
||||
int timeoutSec = 10;
|
||||
try {
|
||||
mLatch.await(timeoutSec, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to render " + componentName + " after " + timeoutSec + " seconds");
|
||||
}
|
||||
}
|
||||
|
||||
public void waitForIdleSync() {
|
||||
ReactIdleDetectionUtil.waitForBridgeAndUIIdle(
|
||||
mBridgeIdleSignaler,
|
||||
mReactInstanceManager.getCurrentReactContext(),
|
||||
IDLE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/** Returns the react view */
|
||||
public ReactRootView getView() {
|
||||
return mView;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ load("//ReactNative:DEFS.bzl", "react_native_dep", "react_native_integration_tes
|
|||
|
||||
rn_android_library(
|
||||
name = "tests",
|
||||
srcs = glob(["**/*.java"]),
|
||||
srcs = glob(["*.java"]),
|
||||
visibility = [
|
||||
"PUBLIC",
|
||||
],
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2014-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.tests;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.facebook.react.testing.ReactInstanceSpecForTest;
|
||||
import com.facebook.react.testing.ReactAppInstrumentationTestCase;
|
||||
import com.facebook.react.testing.StringRecordingModule;
|
||||
import com.facebook.react.ReactRootView;
|
||||
|
||||
import org.junit.Ignore;
|
||||
|
||||
/**
|
||||
* Integration test for {@link ReactRootView}.
|
||||
*/
|
||||
public class ReactRootViewTestCase extends ReactAppInstrumentationTestCase {
|
||||
|
||||
private StringRecordingModule mRecordingModule;
|
||||
|
||||
@Override
|
||||
protected String getReactApplicationKeyUnderTest() {
|
||||
return "CatalystRootViewTestApp";
|
||||
}
|
||||
|
||||
@Ignore("t6596940: fix intermittently failing test")
|
||||
public void testResizeRootView() throws Throwable {
|
||||
final ReactRootView rootView = (ReactRootView) getRootView();
|
||||
final View childView = rootView.getChildAt(0);
|
||||
|
||||
assertEquals(rootView.getWidth(), childView.getWidth());
|
||||
|
||||
final int newWidth = rootView.getWidth() / 2;
|
||||
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
rootView.setLayoutParams(new FrameLayout.LayoutParams(
|
||||
newWidth,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
});
|
||||
|
||||
getInstrumentation().waitForIdleSync();
|
||||
waitForBridgeAndUIIdle();
|
||||
|
||||
assertEquals(newWidth, childView.getWidth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that removing the root view from hierarchy will trigger subviews removal both on JS and
|
||||
* native side
|
||||
*/
|
||||
public void testRemoveRootView() throws Throwable {
|
||||
final ReactRootView rootView = (ReactRootView) getRootView();
|
||||
|
||||
assertEquals(1, rootView.getChildCount());
|
||||
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ViewGroup parent = (ViewGroup) rootView.getParent();
|
||||
parent.removeView(rootView);
|
||||
// removing from parent should not remove child views, child views should be removed as
|
||||
// an effect of native call to UIManager.removeRootView
|
||||
assertEquals(1, rootView.getChildCount());
|
||||
}
|
||||
});
|
||||
|
||||
getInstrumentation().waitForIdleSync();
|
||||
waitForBridgeAndUIIdle();
|
||||
|
||||
assertEquals("root component should not be automatically unmounted", 0, mRecordingModule.getCalls().size());
|
||||
assertEquals(1, rootView.getChildCount());
|
||||
|
||||
runTestOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
rootView.unmountReactApplication();
|
||||
}
|
||||
});
|
||||
waitForBridgeAndUIIdle();
|
||||
|
||||
assertEquals(1, mRecordingModule.getCalls().size());
|
||||
assertEquals("RootComponentWillUnmount", mRecordingModule.getCalls().get(0));
|
||||
assertEquals(0, rootView.getChildCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactInstanceSpecForTest createReactInstanceSpecForTest() {
|
||||
mRecordingModule = new StringRecordingModule();
|
||||
return new ReactInstanceSpecForTest()
|
||||
.addNativeModule(mRecordingModule);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
# BUILD FILE SYNTAX: SKYLARK
|
||||
load(
|
||||
"@xplat//ReactNative:DEFS.bzl",
|
||||
"react_native_dep",
|
||||
"react_native_integration_tests_target",
|
||||
"react_native_target",
|
||||
"rn_android_library",
|
||||
)
|
||||
|
||||
rn_android_library(
|
||||
name = "core",
|
||||
srcs = glob(["*.java"]),
|
||||
deps = [
|
||||
react_native_dep("java/com/facebook/fbreact/testing:testing"),
|
||||
react_native_dep("third-party/java/espresso:espresso"),
|
||||
react_native_dep("third-party/java/fest:fest"),
|
||||
react_native_dep("third-party/java/junit:junit"),
|
||||
react_native_dep("third-party/java/testing-support-lib:testing-support-lib"),
|
||||
react_native_integration_tests_target("java/com/facebook/react/testing:testing"),
|
||||
react_native_integration_tests_target("java/com/facebook/react/testing/rule:rule"),
|
||||
react_native_target("java/com/facebook/react:react"),
|
||||
react_native_target("java/com/facebook/react/bridge:bridge"),
|
||||
react_native_target("java/com/facebook/react/shell:shell"),
|
||||
react_native_target("java/com/facebook/react/uimanager:uimanager"),
|
||||
],
|
||||
)
|
|
@ -0,0 +1,129 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.react.tests.core;
|
||||
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.facebook.react.bridge.ModuleSpec;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
import com.facebook.react.testing.StringRecordingModule;
|
||||
import com.facebook.react.testing.rule.ReactNativeTestRule;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.inject.Provider;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ReactRootViewTest {
|
||||
|
||||
final StringRecordingModule mRecordingModule = new StringRecordingModule();
|
||||
final ReactPackage mReactPackage = new MainReactPackage() {
|
||||
@Override
|
||||
public List<ModuleSpec> getNativeModules(ReactApplicationContext context) {
|
||||
List<ModuleSpec> modules = new ArrayList<>(super.getNativeModules(context));
|
||||
modules.add(
|
||||
ModuleSpec.nativeModuleSpec(
|
||||
StringRecordingModule.class,
|
||||
new Provider<NativeModule>() {
|
||||
@Override
|
||||
public NativeModule get() {
|
||||
return mRecordingModule;
|
||||
}
|
||||
}));
|
||||
return modules;
|
||||
}
|
||||
};
|
||||
|
||||
@Rule
|
||||
public ReactNativeTestRule mReactNativeRule =
|
||||
new ReactNativeTestRule("AndroidTestBundle.js", mReactPackage);
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
mReactNativeRule.render("CatalystRootViewTestApp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResizeRootView() {
|
||||
final ReactRootView rootView = mReactNativeRule.getView();
|
||||
final View childView = rootView.getChildAt(0);
|
||||
|
||||
assertThat(rootView.getWidth()).isEqualTo(childView.getWidth());
|
||||
|
||||
final int newWidth = rootView.getWidth() / 2;
|
||||
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
instrumentation.runOnMainSync(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
rootView.setLayoutParams(new FrameLayout.LayoutParams(
|
||||
newWidth,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
});
|
||||
|
||||
instrumentation.waitForIdleSync();
|
||||
mReactNativeRule.waitForIdleSync();
|
||||
|
||||
assertThat(newWidth).isEqualTo(childView.getWidth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that removing the root view from hierarchy will trigger subviews removal both on JS and
|
||||
* native side
|
||||
*/
|
||||
@Test
|
||||
public void testRemoveRootView() {
|
||||
final ReactRootView rootView = mReactNativeRule.getView();
|
||||
|
||||
assertThat(rootView.getChildCount()).isEqualTo(1);
|
||||
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
instrumentation.runOnMainSync(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ViewGroup parent = (ViewGroup) rootView.getParent();
|
||||
parent.removeView(rootView);
|
||||
// removing from parent should not remove child views, child views should be removed as
|
||||
// an effect of native call to UIManager.removeRootView
|
||||
assertThat(rootView.getChildCount()).isEqualTo(1);
|
||||
}
|
||||
});
|
||||
|
||||
instrumentation.waitForIdleSync();
|
||||
mReactNativeRule.waitForIdleSync();
|
||||
|
||||
assertThat(mRecordingModule.getCalls().size())
|
||||
.isEqualTo(0)
|
||||
.overridingErrorMessage("root component should not be automatically unmounted");
|
||||
assertThat(rootView.getChildCount()).isEqualTo(1);
|
||||
|
||||
instrumentation.runOnMainSync(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
rootView.unmountReactApplication();
|
||||
}
|
||||
});
|
||||
mReactNativeRule.waitForIdleSync();
|
||||
|
||||
assertThat(mRecordingModule.getCalls().size()).isEqualTo(1);
|
||||
assertThat(mRecordingModule.getCalls().get(0)).isEqualTo("RootComponentWillUnmount");
|
||||
assertThat(rootView.getChildCount()).isEqualTo(0);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue