Repository: wix/Detox
Pull request #4205 (2023-10-01 14:04:24)

(Android) Make activity launch more supervised, debuggable
## Description

<!--
Thank you for contributing!

Step 1: Before submitting a pull request that introduces a new functionality or fixes a bug,
please open an issue where we could discuss the suggestion, problem - and potential ways to fix.
-->

- This pull request addresses the issue described here: N/A

<!--
Step 2: Provide an overview of how your fix / enhancement works.
If possible, provide screenshots of the before and after states (even for simple command line options - show the terminal).
-->

In this pull request, I have made adjustments to the way the app's activity is launched by Detox, making it in a more supervised way and thus more debuggable. That includes the following:

1. React markers, noting important things such as Javascript loading, bundle downloading, native package initialization, etc. - are now logged by Detox. To avoid log spamming, only major "root" events are logged.
1. Before this change, the app's activity used to be launched in the very beginning of Detox (i.e. the beginning of the Detox native test, namely, in `Detox.java`). This means that things such as app crashes / ANR's were not reported to the test runner, if happening at a very early stage. This PR introduces a fundamental change where the connection to the Detox test-runner is made as the first step, with activity launching happening only following that. This also helps make sure that the react markers are not subscribed-to too late, resulting in the missing-out of some of the earlier markers.

This is all happening as the result of some internal incidents.

_Note: This could potentially pave way to making `MonitoredInstrumentation` scarce. @noomorph_

#### TODO

Introduce this for iOS.

<!--
Step 3: Please review the checklist below.
-->

---


> _For features/enhancements:_
 - [ ] I have added/updated the relevant references in the [documentation](https://github.com/wix/Detox/tree/master/docs) files.

> _For API changes:_
 - [ ] I have made the necessary changes in the [types index](https://github.com/wix/Detox/blob/master/detox/index.d.ts) file.



Patch:
diff --git a/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt b/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt
new file mode 100644
index 000000000..fd03bad18
--- /dev/null
+++ b/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt
@@ -0,0 +1,76 @@
+package com.wix.detox
+
+import android.app.Instrumentation.ActivityMonitor
+import android.content.Context
+import android.content.Intent
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+
+class ActivityLaunchHelper(private val activityTestRule: ActivityTestRule<*>) {
+
+    private val launchArgs = LaunchArgs()
+    private val intentsFactory = LaunchIntentsFactory()
+
+    fun launchActivityUnderTest() {
+        val intent = extractInitialIntent()
+        activityTestRule.launchActivity(intent)
+    }
+
+    fun launchMainActivity() {
+        val activity = activityTestRule.activity
+        launchActivitySync(intentsFactory.activityLaunchIntent(activity))
+    }
+
+    fun startActivityFromUrl(url: String) {
+        launchActivitySync(intentsFactory.intentWithUrl(url, false))
+    }
+
+    fun startActivityFromNotification(dataFilePath: String) {
+        val notificationData = NotificationDataParser(dataFilePath!!).toBundle()
+        val intent = intentsFactory.intentWithNotificationData(appContext, notificationData, false)
+        launchActivitySync(intent)
+    }
+
+    private fun extractInitialIntent(): Intent =
+        if (launchArgs.hasUrlOverride()) {
+            intentsFactory.intentWithUrl(launchArgs.urlOverride, true)
+        } else if (launchArgs.hasNotificationPath()) {
+            val notificationData = NotificationDataParser(launchArgs.notificationPath).toBundle()
+            intentsFactory.intentWithNotificationData(appContext, notificationData, true)
+        } else {
+            intentsFactory.cleanIntent()
+        }.also {
+            it.putExtra(INTENT_LAUNCH_ARGS_KEY, launchArgs.asIntentBundle())
+        }
+
+    private fun 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.
+        val activity = activityTestRule.activity
+        val activityMonitor = ActivityMonitor(activity.javaClass.name, null, true)
+        activity.startActivity(intent)
+
+        InstrumentationRegistry.getInstrumentation().run {
+            addMonitor(activityMonitor)
+            waitForMonitorWithTimeout(activityMonitor, ACTIVITY_LAUNCH_TIMEOUT)
+        }
+    }
+
+    private val appContext: Context
+        get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
+
+    companion object {
+        private const val INTENT_LAUNCH_ARGS_KEY = "launchArgs"
+        private const val ACTIVITY_LAUNCH_TIMEOUT = 10000L
+    }
+}
diff --git a/detox/android/detox/src/full/java/com/wix/detox/Detox.java b/detox/android/detox/src/full/java/com/wix/detox/Detox.java
index 87d211c79..7ad2a1927 100644
--- a/detox/android/detox/src/full/java/com/wix/detox/Detox.java
+++ b/detox/android/detox/src/full/java/com/wix/detox/Detox.java
@@ -1,18 +1,13 @@
 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>
  *
@@ -67,12 +62,7 @@ import androidx.test.rule.ActivityTestRule;
  * <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 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 static ActivityLaunchHelper sActivityLaunchHelper;
 
     private Detox() {
     }
@@ -132,15 +122,10 @@ public final class Detox {
         DetoxConfig.CONFIG = detoxConfig;
         DetoxConfig.CONFIG.apply();
 
-        sActivityTestRule = activityTestRule;
-
-        UiControllerSpy.attachThroughProxy();
-
-        Intent intent = extractInitialIntent();
-        sActivityTestRule.launchActivity(intent);
+        sActivityLaunchHelper = new ActivityLaunchHelper(activityTestRule);
 
         try {
-            DetoxMain.run(context);
+            DetoxMain.run(context, sActivityLaunchHelper);
         } catch (Exception e) {
             Thread.currentThread().interrupt();
             throw new RuntimeException("Detox got interrupted prematurely", e);
@@ -148,56 +133,15 @@ public final class Detox {
     }
 
     public static void launchMainActivity() {
-        final Activity activity = sActivityTestRule.getActivity();
-        launchActivitySync(sIntentsFactory.activityLaunchIntent(activity));
+        sActivityLaunchHelper.launchMainActivity();
     }
 
     public static void startActivityFromUrl(String url) {
-        launchActivitySync(sIntentsFactory.intentWithUrl(url, false));
+        sActivityLaunchHelper.startActivityFromUrl(url);
     }
 
     public static void startActivityFromNotification(String 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);
+        sActivityLaunchHelper.startActivityFromNotification(dataFilePath);
     }
 
     private static Context getAppContext() {
diff --git a/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt b/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt
index 5e4f0bd88..cbec2bea8 100644
--- a/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt
+++ b/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt
@@ -2,8 +2,21 @@ package com.wix.detox
 
 import android.content.Context
 import android.util.Log
-import com.wix.detox.adapters.server.*
-import com.wix.detox.common.DetoxLog.Companion.LOG_TAG
+import com.wix.detox.adapters.server.CleanupActionHandler
+import com.wix.detox.adapters.server.DetoxActionHandler
+import com.wix.detox.adapters.server.DetoxActionsDispatcher
+import com.wix.detox.adapters.server.DetoxServerAdapter
+import com.wix.detox.adapters.server.DetoxServerInfo
+import com.wix.detox.adapters.server.InstrumentsEventsActionsHandler
+import com.wix.detox.adapters.server.InstrumentsRecordingStateActionHandler
+import com.wix.detox.adapters.server.InvokeActionHandler
+import com.wix.detox.adapters.server.OutboundServerAdapter
+import com.wix.detox.adapters.server.PrematureReadyHandler
+import com.wix.detox.adapters.server.QueryStatusActionHandler
+import com.wix.detox.adapters.server.ReactNativeReloadActionHandler
+import com.wix.detox.adapters.server.ReadyActionHandler
+import com.wix.detox.common.DetoxLog
+import com.wix.detox.espresso.UiControllerSpy
 import com.wix.detox.instruments.DetoxInstrumentsManager
 import com.wix.detox.reactnative.ReactNativeExtension
 import com.wix.invoke.MethodInvocation
@@ -14,24 +27,28 @@ private const val TERMINATION_ACTION = "_terminate"
 
 object DetoxMain {
     @JvmStatic
-    fun run(rnHostHolder: Context) {
+    fun run(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) {
         val detoxServerInfo = DetoxServerInfo()
-        Log.i(LOG_TAG, "Detox server connection details: $detoxServerInfo")
-
         val testEngineFacade = TestEngineFacade()
         val actionsDispatcher = DetoxActionsDispatcher()
         val externalAdapter = DetoxServerAdapter(actionsDispatcher, detoxServerInfo, IS_READY_ACTION, TERMINATION_ACTION)
-        initActionHandlers(actionsDispatcher, externalAdapter, testEngineFacade, rnHostHolder)
+
+        initActionHandlers(activityLaunchHelper, actionsDispatcher, externalAdapter, testEngineFacade, rnHostHolder)
         actionsDispatcher.dispatchAction(INIT_ACTION, "", 0)
         actionsDispatcher.join()
     }
 
-    private fun doInit(externalAdapter: DetoxServerAdapter, rnHostHolder: Context) {
-        externalAdapter.connect()
-
+    private fun doInit(externalAdapter: DetoxServerAdapter) {
         initCrashHandler(externalAdapter)
         initANRListener(externalAdapter)
-        initReactNativeIfNeeded(rnHostHolder)
+        initEspresso()
+        initReactNative()
+
+        externalAdapter.connect()
+    }
+
+    private fun onConnected(activityLaunchHelper: ActivityLaunchHelper, rnHostHolder: Context) {
+        launchApp(rnHostHolder, activityLaunchHelper)
     }
 
     private fun doTeardown(serverAdapter: DetoxServerAdapter, actionsDispatcher: DetoxActionsDispatcher, testEngineFacade: TestEngineFacade) {
@@ -41,20 +58,35 @@ object DetoxMain {
         actionsDispatcher.teardown()
     }
 
-    private fun initActionHandlers(actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) {
+    private fun initActionHandlers(activityLaunchHelper: ActivityLaunchHelper, actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) {
         // Primary actions
         with(actionsDispatcher) {
+            var prematureIsReadyHandler: PrematureReadyHandler? = PrematureReadyHandler()
             val rnReloadHandler = ReactNativeReloadActionHandler(rnHostHolder, serverAdapter, testEngineFacade)
 
-            associateActionHandler(INIT_ACTION, object : DetoxActionHandler {
+            associateActionHandler(INIT_ACTION, object: DetoxActionHandler {
                 override fun handle(params: String, messageId: Long) =
                     synchronized(this@DetoxMain) {
-                        this@DetoxMain.doInit(serverAdapter, rnHostHolder)
+                        this@DetoxMain.doInit(serverAdapter)
                     }
             })
-            associateActionHandler(IS_READY_ACTION, ReadyActionHandler(serverAdapter, testEngineFacade))
+            associateActionHandler(IS_READY_ACTION, prematureIsReadyHandler!!)
 
-            associateActionHandler("loginSuccess", ScarceActionHandler())
+            associateActionHandler("loginSuccess", object: DetoxActionHandler {
+                override fun handle(params: String, messageId: Long) {
+                    synchronized(this@DetoxMain) {
+                        this@DetoxMain.onConnected(activityLaunchHelper, rnHostHolder)
+                        associateActionHandler(IS_READY_ACTION, ReadyActionHandler(serverAdapter, testEngineFacade))
+
+                        prematureIsReadyHandler?.let {
+                            if (it.isActionPending) {
+                                actionsDispatcher.dispatchAction(IS_READY_ACTION, it.params!!, it.messageId!!)
+                            }
+                            prematureIsReadyHandler = null
+                        }
+                    }
+                }
+            })
             associateActionHandler("reactNativeReload", object: DetoxActionHandler {
                 override fun handle(params: String, messageId: Long) =
                     synchronized(this@DetoxMain) {
@@ -98,7 +130,17 @@ object DetoxMain {
         DetoxANRHandler(outboundServerAdapter).attach()
     }
 
-    private fun initReactNativeIfNeeded(rnHostHolder: Context) {
+    private fun initEspresso() {
+        UiControllerSpy.attachThroughProxy()
+    }
+
+    private fun initReactNative() {
+        ReactNativeExtension.initIfNeeded()
+    }
+
+    private fun launchApp(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) {
+        Log.i(DetoxLog.LOG_TAG, "Launching the tested activity!")
+        activityLaunchHelper.launchActivityUnderTest()
         ReactNativeExtension.waitForRNBootstrap(rnHostHolder)
     }
 }
diff --git a/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt b/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt
index 43bed23a4..80f5801c2 100644
--- a/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt
+++ b/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt
@@ -6,7 +6,7 @@ import android.content.Intent
 import android.net.Uri
 import android.os.Bundle
 
-internal class LaunchIntentsFactory {
+class LaunchIntentsFactory {
 
     /**
      * Constructs an intent tightly associated with a specific activity.
diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt
index ef0416465..1765d3944 100644
--- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt
+++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt
@@ -3,6 +3,7 @@ package com.wix.detox.adapters.server
 import android.content.Context
 import android.util.Log
 import com.wix.detox.TestEngineFacade
+import com.wix.detox.common.DetoxLog
 import com.wix.detox.common.extractRootCause
 import com.wix.detox.instruments.DetoxInstrumentsException
 import com.wix.detox.instruments.DetoxInstrumentsManager
@@ -27,6 +28,20 @@ class ReadyActionHandler(
     }
 }
 
+class PrematureReadyHandler(): DetoxActionHandler {
+    var isActionPending = false
+    var params: String? = null
+    var messageId: Long? = null
+
+    override fun handle(params: String, messageId: Long) {
+        Log.i(DetoxLog.LOG_TAG, "Got a premature ready action, saving for later...")
+
+        this.isActionPending = true
+        this.params = params
+        this.messageId = messageId
+    }
+}
+
 open class ReactNativeReloadActionHandler(
         private val appContext: Context,
         private val outboundServerAdapter: OutboundServerAdapter,
@@ -152,7 +167,3 @@ class InstrumentsEventsActionsHandler(
         outboundServerAdapter.sendMessage("eventDone", emptyMap<String, Any>(), messageId)
     }
 }
-
-class ScarceActionHandler: DetoxActionHandler {
-    override fun handle(params: String, messageId: Long) {}
-}
diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt
index f2828de4e..947c69ea2 100644
--- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt
+++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt
@@ -1,7 +1,9 @@
 package com.wix.detox.adapters.server
 
+import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import com.wix.detox.LaunchArgs
+import com.wix.detox.common.DetoxLog
 
 private const val DEFAULT_URL = "ws://localhost:8099"
 
@@ -9,7 +11,7 @@ class DetoxServerInfo internal constructor(launchArgs: LaunchArgs = LaunchArgs()
     val serverUrl: String = launchArgs.detoxServerUrl ?: DEFAULT_URL
     val sessionId: String = launchArgs.detoxSessionId ?: InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.packageName
 
-    override fun toString(): String {
-        return "url=$serverUrl, sessionId=$sessionId"
+    init {
+        Log.i(DetoxLog.LOG_TAG, "Detox server connection details: url=$serverUrl, sessionId=$sessionId")
     }
 }
diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt
new file mode 100644
index 000000000..bbabce01d
--- /dev/null
+++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt
@@ -0,0 +1,44 @@
+package com.wix.detox.reactnative
+
+import android.util.Log
+import com.facebook.react.bridge.ReactMarker
+import com.facebook.react.bridge.ReactMarkerConstants
+import com.facebook.react.bridge.ReactMarkerConstants.*
+
+object ReactMarkersLogger : ReactMarker.MarkerListener {
+
+    fun attach() {
+        ReactMarker.addListener(this)
+    }
+
+    override fun logMarker(marker: ReactMarkerConstants, p1: String?, p2: Int) {
+        when {
+            marker == DOWNLOAD_START ||
+            marker == DOWNLOAD_END ||
+            marker == BUILD_REACT_INSTANCE_MANAGER_START ||
+            marker == BUILD_REACT_INSTANCE_MANAGER_END ||
+            marker == REACT_BRIDGE_LOADING_START ||
+            marker == REACT_BRIDGE_LOADING_END ||
+            marker == REACT_BRIDGELESS_LOADING_START ||
+            marker == REACT_BRIDGELESS_LOADING_END ||
+            marker == CREATE_MODULE_START ||
+            marker == CREATE_MODULE_END ||
+            marker == NATIVE_MODULE_SETUP_START ||
+            marker == NATIVE_MODULE_SETUP_END ||
+            marker == PRE_RUN_JS_BUNDLE_START ||
+            marker == RUN_JS_BUNDLE_START ||
+            marker == RUN_JS_BUNDLE_END ||
+            marker == CONTENT_APPEARED ||
+            marker == CREATE_CATALYST_INSTANCE_START ||
+            marker == CREATE_CATALYST_INSTANCE_END ||
+            marker == DESTROY_CATALYST_INSTANCE_START ||
+            marker == DESTROY_CATALYST_INSTANCE_END ||
+            marker == CREATE_REACT_CONTEXT_START ||
+            marker == CREATE_REACT_CONTEXT_END ||
+            marker == PROCESS_PACKAGES_START ||
+            marker == PROCESS_PACKAGES_END ||
+                false ->
+                Log.d("Detox.RNMarker", "$marker ($p1)")
+        }
+    }
+}
diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt
index 19e4ae247..0962f1d13 100644
--- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt
+++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt
@@ -14,6 +14,32 @@ private const val LOG_TAG = "DetoxRNExt"
 object ReactNativeExtension {
     private var rnIdlingResources: ReactNativeIdlingResources? = null
 
+    fun initIfNeeded() {
+        if (!ReactNativeInfo.isReactNativeApp()) {
+            return
+        }
+
+        ReactMarkersLogger.attach()
+    }
+
+    /**
+     * Wait for React-Native to finish loading (i.e. make RN context available).
+     *
+     * @param applicationContext The app context, implicitly assumed to be a [ReactApplication] instance.
+     */
+    fun waitForRNBootstrap(applicationContext: Context) {
+        if (!ReactNativeInfo.isReactNativeApp()) {
+            return
+        }
+
+        (applicationContext as ReactApplication).let {
+            val reactContext = awaitNewReactNativeContext(it, null)
+
+            enableOrDisableSynchronization(reactContext)
+            hackRN50WaitForReady()
+        }
+    }
+
     /**
      * Reloads the React Native context and thus all javascript code.
      *
@@ -40,26 +66,7 @@ object ReactNativeExtension {
             val reactContext = awaitNewReactNativeContext(it, previousReactContext)
 
             enableOrDisableSynchronization(reactContext, networkSyncEnabled)
-            hackRN50OrHigherWaitForReady()
-        }
-    }
-
-    /**
-     * Wait for React-Native to finish loading (i.e. make RN context available).
-     *
-     * @param applicationContext The app context, implicitly assumed to be a [ReactApplication] instance.
-     */
-    @JvmStatic
-    fun waitForRNBootstrap(applicationContext: Context) {
-        if (!ReactNativeInfo.isReactNativeApp()) {
-            return
-        }
-
-        (applicationContext as ReactApplication).let {
-            val reactContext = awaitNewReactNativeContext(it, null)
-
-            enableOrDisableSynchronization(reactContext)
-            hackRN50OrHigherWaitForReady()
+            hackRN50WaitForReady()
         }
     }
 
@@ -145,7 +152,7 @@ object ReactNativeExtension {
         }
     }
 
-    private fun hackRN50OrHigherWaitForReady() {
+    private fun hackRN50WaitForReady() {
         if (ReactNativeInfo.rnVersion().minor in 50..62) {
             try {
                 //TODO- Temp hack to make Detox usable for RN>=50 till we find a better sync solution.
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt b/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt
index 742af777f..051be7931 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt
@@ -11,11 +11,8 @@ class UiControllerSpy: MethodsSpy("uiController") {
     fun eventInjectionsIterator(): Iterator<CallInfo?> = historyOf("injectMotionEvent").iterator()
 
     companion object {
-        @JvmStatic
         val instance = UiControllerSpy()
 
-        @JvmStatic
-        @JvmOverloads
         fun attachThroughProxy(spy: UiControllerSpy = instance) {
             val eventsInjectorReflected = EventsInjectorReflected(getUiController())
 
diff --git a/detox/package.json b/detox/package.json
index f2160e382..f4c26c025 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -1,7 +1,7 @@
 {
   "name": "detox",
   "description": "E2E tests and automation for mobile",
-  "version": "20.13.0",
+  "version": "20.14.0-smoke.0",
   "bin": {
     "detox": "local-cli/cli.js"
   },
diff --git a/detox/src/android/espressoapi/Detox.js b/detox/src/android/espressoapi/Detox.js
index bbba04e3a..dfa11192f 100644
--- a/detox/src/android/espressoapi/Detox.js
+++ b/detox/src/android/espressoapi/Detox.js
@@ -58,17 +58,6 @@ class Detox {
     };
   }
 
-  static extractInitialIntent() {
-    return {
-      target: {
-        type: "Class",
-        value: "com.wix.detox.Detox"
-      },
-      method: "extractInitialIntent",
-      args: []
-    };
-  }
-
   static getAppContext() {
     return {
       target: {
diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json
index d61cf7c7e..dcbfc1cb7 100644
--- a/examples/demo-native-android/package.json
+++ b/examples/demo-native-android/package.json
@@ -1,13 +1,13 @@
 {
   "name": "detox-demo-native-android",
-  "version": "20.13.0",
+  "version": "20.14.0-smoke.0",
   "private": true,
   "scripts": {
     "packager": "react-native start",
     "detox-server": "detox run-server"
   },
   "devDependencies": {
-    "detox": "^20.13.0"
+    "detox": "^20.14.0-smoke.0"
   },
   "detox": {}
 }
diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json
index aa887f7f8..bcefee80e 100644
--- a/examples/demo-native-ios/package.json
+++ b/examples/demo-native-ios/package.json
@@ -1,9 +1,9 @@
 {
   "name": "detox-demo-native-ios",
-  "version": "20.13.0",
+  "version": "20.14.0-smoke.0",
   "private": true,
   "devDependencies": {
-    "detox": "^20.13.0"
+    "detox": "^20.14.0-smoke.0"
   },
   "detox": {
     "specs": "",
diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json
index 2d33e4c13..432f95d16 100644
--- a/examples/demo-plugin/package.json
+++ b/examples/demo-plugin/package.json
@@ -1,12 +1,12 @@
 {
   "name": "demo-plugin",
-  "version": "20.13.0",
+  "version": "20.14.0-smoke.0",
   "private": true,
   "scripts": {
     "test:plugin": "detox test --configuration plugin -l verbose"
   },
   "devDependencies": {
-    "detox": "^20.13.0",
+    "detox": "^20.14.0-smoke.0",
     "jest": "^28.0.0"
   },
   "detox": {
diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json
index d2cf24860..4f009b673 100644
--- a/examples/demo-react-native-detox-instruments/package.json
+++ b/examples/demo-react-native-detox-instruments/package.json
@@ -1,10 +1,10 @@
 {
   "name": "demo-react-native-detox-instruments",
-  "version": "20.13.0",
+  "version": "20.14.0-smoke.0",
   "private": true,
   "scripts": {},
   "devDependencies": {
-    "detox": "^20.13.0"
+    "detox": "^20.14.0-smoke.0"
   },
   "detox": {
     "configurations": {
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index f0a3182a2..cd6dcb50a 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -1,6 +1,6 @@
 {
   "name": "example",
-  "version": "20.13.0",
+  "version": "20.14.0-smoke.0",
   "private": true,
   "scripts": {
     "start": "react-native start",
@@ -32,7 +32,7 @@
     "@types/fs-extra": "^9.0.13",
     "@types/jest": "^29.2.1",
     "@types/react": "^18.0.24",
-    "detox": "^20.13.0",
+    "detox": "^20.14.0-smoke.0",
     "fs-extra": "^9.1.0",
     "jest": "^29.2.1",
     "ts-jest": "^29.0.3",
diff --git a/lerna.json b/lerna.json
index 0b7332624..48f28747c 100644
--- a/lerna.json
+++ b/lerna.json
@@ -13,7 +13,7 @@
     "generation",
     "."
   ],
-  "version": "20.13.0",
+  "version": "20.14.0-smoke.0",
   "npmClient": "npm",
   "command": {
     "publish": {
diff --git a/package.json b/package.json
index d503a60da..283101384 100644
--- a/package.json
+++ b/package.json
@@ -54,5 +54,5 @@
     "shell-utils": "1.x.x",
     "unified": "^10.1.0"
   },
-  "version": "20.13.0"
+  "version": "20.14.0-smoke.0"
 }


Test patch:
diff --git a/detox/test/e2e/19.crash-handling.test.js b/detox/test/e2e/19.crash-handling.test.js
index 1912538af..0c1f12021 100644
--- a/detox/test/e2e/19.crash-handling.test.js
+++ b/detox/test/e2e/19.crash-handling.test.js
@@ -51,12 +51,14 @@ describe('Crash Handling', () => {
   it(':android: Should throw a detailed error upon app bootstrap crash', async () => {
     const error = await expectToThrow(
       () => relaunchAppWithArgs({ detoxAndroidCrashingActivity: true }),
-      'Failed to run application on the device');
+      'The app has crashed, see the details below:');
 
     // It's important that the native-error message (containing the native stack-trace) would also
     // be included in the error's stack property, in order for Jest (specifically) to properly output all
     // of that into the shell, as we expect it to.
-    jestExpect(error.stack).toContain('Native stacktrace dump:\njava.lang.IllegalStateException: This is an intentional crash!');
-    jestExpect(error.stack).toContain('\tat com.example.CrashingActivity.onResume');
+    jestExpect(error.stack).toContain('java.lang.RuntimeException: Unable to resume activity');
+
+    // In particular, we want the original cause to be bundled in.
+    jestExpect(error.stack).toContain('Caused by: java.lang.IllegalStateException: This is an intentional crash!');
   }, 60000);
 });
diff --git a/detox/test/package.json b/detox/test/package.json
index 553107d80..d3c5e322f 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-test",
-  "version": "20.13.0",
+  "version": "20.14.0-smoke.0",
   "private": true,
   "engines": {
     "node": ">=14.5.0"
@@ -56,7 +56,7 @@
     "@typescript-eslint/eslint-plugin": "^5.4.0",
     "@typescript-eslint/parser": "^5.4.0",
     "cross-env": "^7.0.3",
-    "detox": "^20.13.0",
+    "detox": "^20.14.0-smoke.0",
     "eslint": "^8.41.0",
     "eslint-plugin-unicorn": "^47.0.0",
     "execa": "^5.1.1",