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:
Andrew Chen (Eng) 2018-08-29 12:24:09 -07:00 committed by Facebook Github Bot
parent bf8e1b4ffa
commit afe0843bee
7 changed files with 369 additions and 107 deletions

View File

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

View File

@ -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"),
],
)

View File

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

View File

@ -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",
],

View File

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

View File

@ -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"),
],
)

View File

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