/**
* 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.app.Application;
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.CatalystInstance;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.ApplicationHolder;
import com.facebook.react.common.futures.SimpleSettableFuture;
import com.facebook.react.devsupport.DevSupportManager;
import com.facebook.react.modules.core.Timing;
import com.facebook.soloader.SoLoader;
import static org.mockito.Mockito.mock;
/**
* 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 ReactTestHelper#catalystInstanceBuilder()}
* instead of {@link com.facebook.react.bridge.CatalystInstanceImpl.Builder} to build catalyst
* instance for testing purposes
*
*/
public abstract class ReactIntegrationTestCase extends AndroidTestCase {
// we need a bigger timeout for CI builds because they run on a slow emulator
private static final long IDLE_TIMEOUT_MS = 60000;
private @Nullable CatalystInstance 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;
final SimpleSettableFuture<Void> semaphore = new SimpleSettableFuture<>();
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
if (contextToDestroy != null) {
contextToDestroy.destroy();
}
semaphore.set(null);
}
});
semaphore.getOrThrow();
}
}
/**
* 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(), mock(DevSupportManager.class));
simpleSettableFuture.set(timing);
}
});
try {
return simpleSettableFuture.get(5000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void initializeWithInstance(CatalystInstance instance) {
mInstance = instance;
mBridgeIdleSignaler = new ReactBridgeIdleSignaler();
mInstance.addBridgeIdleDebugListener(mBridgeIdleSignaler);
getContext().initializeWithInstance(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 setUp() throws Exception {
super.setUp();
SoLoader.init(getContext(), /* native exopackage */ false);
ApplicationHolder.setApplication((Application) getContext().getApplicationContext());
}
@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);
}
}
}