From afe0843bee0b145e7259fa0b06a3e47ddf8bdafb Mon Sep 17 00:00:00 2001 From: "Andrew Chen (Eng)" Date: Wed, 29 Aug 2018 12:24:09 -0700 Subject: [PATCH] Move ReactNativeTestRule to OSS Summary: And migrated ReactRootViewTestCase to use ReactNativeTestRule. Reviewed By: mdvacca Differential Revision: D9557362 fbshipit-source-id: 1469d0ad8c125b5ea729371d81956e61780c56cf --- .../java/com/facebook/react/testing/BUCK | 2 +- .../java/com/facebook/react/testing/rule/BUCK | 35 ++++ .../testing/rule/ReactNativeTestRule.java | 177 ++++++++++++++++++ .../java/com/facebook/react/tests/BUCK | 2 +- .../react/tests/ReactRootViewTestCase.java | 105 ----------- .../java/com/facebook/react/tests/core/BUCK | 26 +++ .../react/tests/core/ReactRootViewTest.java | 129 +++++++++++++ 7 files changed, 369 insertions(+), 107 deletions(-) create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/testing/rule/BUCK create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/testing/rule/ReactNativeTestRule.java delete mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/tests/ReactRootViewTestCase.java create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/BUCK create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/ReactRootViewTest.java diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK index 8b61c439e..aae982146 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK @@ -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", diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/rule/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/rule/BUCK new file mode 100644 index 000000000..897cab8e8 --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/rule/BUCK @@ -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"), + ], +) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/rule/ReactNativeTestRule.java b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/rule/ReactNativeTestRule.java new file mode 100644 index 000000000..7b6a29312 --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/rule/ReactNativeTestRule.java @@ -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 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; + } +} diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK index e39c0147a..872b86982 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK @@ -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", ], diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ReactRootViewTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ReactRootViewTestCase.java deleted file mode 100644 index f912347b4..000000000 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/ReactRootViewTestCase.java +++ /dev/null @@ -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); - } -} diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/BUCK new file mode 100644 index 000000000..a4f25d9c1 --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/BUCK @@ -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"), + ], +) diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/ReactRootViewTest.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/ReactRootViewTest.java new file mode 100644 index 000000000..c4aa39d3e --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/ReactRootViewTest.java @@ -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 getNativeModules(ReactApplicationContext context) { + List modules = new ArrayList<>(super.getNativeModules(context)); + modules.add( + ModuleSpec.nativeModuleSpec( + StringRecordingModule.class, + new Provider() { + @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); + } +}