Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Re: Supervised android launch (#4293)" #4296

Merged
merged 1 commit into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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) {
[email protected](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) {
[email protected](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
Loading