Skip to content

Commit

Permalink
Revert "Re: Supervised android launch (#4293)"
Browse files Browse the repository at this point in the history
This reverts commit a630b87.
  • Loading branch information
d4vidi committed Dec 18, 2023
1 parent 1512e9a commit 16ef1b5
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 248 deletions.

This file was deleted.

78 changes: 70 additions & 8 deletions detox/android/detox/src/full/java/com/wix/detox/Detox.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.wix.detox;

import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

import com.wix.detox.config.DetoxConfig;
import com.wix.detox.espresso.UiControllerSpy;

import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;

import com.wix.detox.config.DetoxConfig;

/**
* <p>Static class.</p>
*
Expand Down Expand Up @@ -62,7 +67,12 @@
* <p>If not set, then Detox tests are no ops. So it's safe to mix it with other tests.</p>
*/
public final class Detox {
private static ActivityLaunchHelper sActivityLaunchHelper;
private static final String INTENT_LAUNCH_ARGS_KEY = "launchArgs";
private static final long ACTIVITY_LAUNCH_TIMEOUT = 10000L;

private static final LaunchArgs sLaunchArgs = new LaunchArgs();
private static final LaunchIntentsFactory sIntentsFactory = new LaunchIntentsFactory();
private static ActivityTestRule sActivityTestRule;

private Detox() {
}
Expand Down Expand Up @@ -122,20 +132,72 @@ public static void runTests(ActivityTestRule activityTestRule, @NonNull final Co
DetoxConfig.CONFIG = detoxConfig;
DetoxConfig.CONFIG.apply();

sActivityLaunchHelper = new ActivityLaunchHelper(activityTestRule);
DetoxMain.run(context, sActivityLaunchHelper);
sActivityTestRule = activityTestRule;

UiControllerSpy.attachThroughProxy();

Intent intent = extractInitialIntent();
sActivityTestRule.launchActivity(intent);

try {
DetoxMain.run(context);
} catch (Exception e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Detox got interrupted prematurely", e);
}
}

public static void launchMainActivity() {
sActivityLaunchHelper.launchMainActivity();
final Activity activity = sActivityTestRule.getActivity();
launchActivitySync(sIntentsFactory.activityLaunchIntent(activity));
}

public static void startActivityFromUrl(String url) {
sActivityLaunchHelper.startActivityFromUrl(url);
launchActivitySync(sIntentsFactory.intentWithUrl(url, false));
}

public static void startActivityFromNotification(String dataFilePath) {
sActivityLaunchHelper.startActivityFromNotification(dataFilePath);
Bundle notificationData = new NotificationDataParser(dataFilePath).toBundle();
Intent intent = sIntentsFactory.intentWithNotificationData(getAppContext(), notificationData, false);
launchActivitySync(intent);
}

private static Intent extractInitialIntent() {
Intent intent;

if (sLaunchArgs.hasUrlOverride()) {
intent = sIntentsFactory.intentWithUrl(sLaunchArgs.getUrlOverride(), true);
} else if (sLaunchArgs.hasNotificationPath()) {
Bundle notificationData = new NotificationDataParser(sLaunchArgs.getNotificationPath()).toBundle();
intent = sIntentsFactory.intentWithNotificationData(getAppContext(), notificationData, true);
} else {
intent = sIntentsFactory.cleanIntent();
}
intent.putExtra(INTENT_LAUNCH_ARGS_KEY, sLaunchArgs.asIntentBundle());
return intent;
}

private static void launchActivitySync(Intent intent) {
// Ideally, we would just call sActivityTestRule.launchActivity(intent) and get it over with.
// BUT!!! as it turns out, Espresso has an issue where doing this for an activity running in the background
// would have Espresso set up an ActivityMonitor which will spend its time waiting for the activity to load, *without
// ever being released*. It will finally fail after a 45 seconds timeout.
// Without going into full details, it seems that activity test rules were not meant to be used this way. However,
// the all-new ActivityScenario implementation introduced in androidx could probably support this (e.g. by using
// dedicated methods such as moveToState(), which give better control over the lifecycle).
// In any case, this is the core reason for this issue: https://github.com/wix/Detox/issues/1125
// What it forces us to do, then, is this -
// 1. Launch the activity by "ourselves" from the OS (i.e. using context.startActivity()).
// 2. Set up an activity monitor by ourselves -- such that it would block until the activity is ready.
// ^ Hence the code below.

final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
final Activity activity = sActivityTestRule.getActivity();
final Instrumentation.ActivityMonitor activityMonitor = new Instrumentation.ActivityMonitor(activity.getClass().getName(), null, true);

activity.startActivity(intent);
instrumentation.addMonitor(activityMonitor);
instrumentation.waitForMonitorWithTimeout(activityMonitor, ACTIVITY_LAUNCH_TIMEOUT);
}

private static Context getAppContext() {
Expand Down
113 changes: 44 additions & 69 deletions detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,35 @@ package com.wix.detox
import android.content.Context
import android.util.Log
import com.wix.detox.adapters.server.*
import com.wix.detox.common.DetoxLog
import com.wix.detox.espresso.UiControllerSpy
import com.wix.detox.common.DetoxLog.Companion.LOG_TAG
import com.wix.detox.instruments.DetoxInstrumentsManager
import com.wix.detox.reactnative.ReactNativeExtension
import com.wix.invoke.MethodInvocation
import java.util.concurrent.CountDownLatch

private const val INIT_ACTION = "_init"
private const val IS_READY_ACTION = "isReady"
private const val TERMINATION_ACTION = "_terminate"

object DetoxMain {
private val handshakeLock = CountDownLatch(1)

@JvmStatic
fun run(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) {
fun run(rnHostHolder: Context) {
val detoxServerInfo = DetoxServerInfo()
Log.i(LOG_TAG, "Detox server connection details: $detoxServerInfo")

val testEngineFacade = TestEngineFacade()
val actionsDispatcher = DetoxActionsDispatcher()
val serverAdapter = DetoxServerAdapter(actionsDispatcher, detoxServerInfo, TERMINATION_ACTION)

initCrashHandler(serverAdapter)
initANRListener(serverAdapter)
initEspresso()
initReactNative()

setupActionHandlers(actionsDispatcher, serverAdapter, testEngineFacade, rnHostHolder)
serverAdapter.connect()

launchActivityOnCue(rnHostHolder, activityLaunchHelper)
val externalAdapter = DetoxServerAdapter(actionsDispatcher, detoxServerInfo, IS_READY_ACTION, TERMINATION_ACTION)
initActionHandlers(actionsDispatcher, externalAdapter, testEngineFacade, rnHostHolder)
actionsDispatcher.dispatchAction(INIT_ACTION, "", 0)
actionsDispatcher.join()
}

/**
* Launch the tested activity "on cue", namely, right after a connection is established and the handshake
* completes successfully.
*
* This has to be synchronized so that an `isReady` isn't handled *before* the activity is launched (albeit not fully
* initialized - all native modules and everything) and a react context is available.
*
* As a better alternative, it would make sense to execute this as a simple action from within the actions
* dispatcher (i.e. handler of `loginSuccess`), in which case, no inter-thread locking would be required
* thanks to the usage of Handlers. However, in this type of a solution, errors / crashes would be reported
* not by instrumentation itself, but based on the `AppWillTerminateWithError` message; In it's own, it is a good
* thing, but for a reason we're not sure of yet, it is ignored by the test runner at this point in the flow.
*/
@Synchronized
private fun launchActivityOnCue(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) {
awaitHandshake()
launchActivity(rnHostHolder, activityLaunchHelper)
}

private fun awaitHandshake() {
handshakeLock.await()
}
private fun doInit(externalAdapter: DetoxServerAdapter, rnHostHolder: Context) {
externalAdapter.connect()

private fun onLoginSuccess() {
handshakeLock.countDown()
initCrashHandler(externalAdapter)
initANRListener(externalAdapter)
initReactNativeIfNeeded(rnHostHolder)
}

private fun doTeardown(serverAdapter: DetoxServerAdapter, actionsDispatcher: DetoxActionsDispatcher, testEngineFacade: TestEngineFacade) {
Expand All @@ -68,28 +41,35 @@ object DetoxMain {
actionsDispatcher.teardown()
}

private fun setupActionHandlers(actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) {
class SynchronizedActionHandler(private val actionHandler: DetoxActionHandler): DetoxActionHandler {
override fun handle(params: String, messageId: Long) {
synchronized(this@DetoxMain) {
actionHandler.handle(params, messageId)
}
}
}

private fun initActionHandlers(actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) {
// Primary actions
with(actionsDispatcher) {
val readyHandler = SynchronizedActionHandler( ReadyActionHandler(serverAdapter, testEngineFacade) )
val rnReloadHandler = SynchronizedActionHandler( ReactNativeReloadActionHandler(rnHostHolder, serverAdapter, testEngineFacade) )
val rnReloadHandler = ReactNativeReloadActionHandler(rnHostHolder, serverAdapter, testEngineFacade)

associateActionHandler("loginSuccess", ::onLoginSuccess)
associateActionHandler("isReady", readyHandler)
associateActionHandler("reactNativeReload", rnReloadHandler)
associateActionHandler(INIT_ACTION, object : DetoxActionHandler {
override fun handle(params: String, messageId: Long) =
synchronized(this@DetoxMain) {
this@DetoxMain.doInit(serverAdapter, rnHostHolder)
}
})
associateActionHandler(IS_READY_ACTION, ReadyActionHandler(serverAdapter, testEngineFacade))

associateActionHandler("loginSuccess", ScarceActionHandler())
associateActionHandler("reactNativeReload", object: DetoxActionHandler {
override fun handle(params: String, messageId: Long) =
synchronized(this@DetoxMain) {
rnReloadHandler.handle(params, messageId)
}
})
associateActionHandler("invoke", InvokeActionHandler(MethodInvocation(), serverAdapter))
associateActionHandler("cleanup", CleanupActionHandler(serverAdapter, testEngineFacade) {
dispatchAction(TERMINATION_ACTION, "", 0)
})
associateActionHandler(TERMINATION_ACTION) { -> doTeardown(serverAdapter, actionsDispatcher, testEngineFacade) }
associateActionHandler(TERMINATION_ACTION, object: DetoxActionHandler {
override fun handle(params: String, messageId: Long) {
this@DetoxMain.doTeardown(serverAdapter, actionsDispatcher, testEngineFacade)
}
})

if (DetoxInstrumentsManager.supports()) {
val instrumentsManager = DetoxInstrumentsManager(rnHostHolder)
Expand All @@ -100,8 +80,13 @@ object DetoxMain {

// Secondary actions
with(actionsDispatcher) {
val queryStatusHandler = SynchronizedActionHandler( QueryStatusActionHandler(serverAdapter, testEngineFacade) )
associateSecondaryActionHandler("currentStatus", queryStatusHandler)
val queryStatusHandler = QueryStatusActionHandler(serverAdapter, testEngineFacade)
associateActionHandler("currentStatus", object: DetoxActionHandler {
override fun handle(params: String, messageId: Long) =
synchronized(this@DetoxMain) {
queryStatusHandler.handle(params, messageId)
}
}, false)
}
}

Expand All @@ -113,17 +98,7 @@ object DetoxMain {
DetoxANRHandler(outboundServerAdapter).attach()
}

private fun initEspresso() {
UiControllerSpy.attachThroughProxy()
}

private fun initReactNative() {
ReactNativeExtension.initIfNeeded()
}

private fun launchActivity(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) {
Log.i(DetoxLog.LOG_TAG, "Launching the tested activity!")
activityLaunchHelper.launchActivityUnderTest()
private fun initReactNativeIfNeeded(rnHostHolder: Context) {
ReactNativeExtension.waitForRNBootstrap(rnHostHolder)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle

class LaunchIntentsFactory {
internal class LaunchIntentsFactory {

/**
* Constructs an intent tightly associated with a specific activity.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,7 @@ class InstrumentsEventsActionsHandler(
outboundServerAdapter.sendMessage("eventDone", emptyMap<String, Any>(), messageId)
}
}

class ScarceActionHandler: DetoxActionHandler {
override fun handle(params: String, messageId: Long) {}
}
Loading

0 comments on commit 16ef1b5

Please sign in to comment.