Repository: wix/Detox
Pull request #1740 (2019-11-06 15:40:29)

Break ReactNativeSupport's loading into subconcerns
Pure maintenance work done as the aftermath of #1731.


Patch:
diff --git a/detox/android/detox/src/main/java/com/wix/detox/DetoxActionHandlers.kt b/detox/android/detox/src/main/java/com/wix/detox/DetoxActionHandlers.kt
index ee7257087..9690b75d3 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/DetoxActionHandlers.kt
+++ b/detox/android/detox/src/main/java/com/wix/detox/DetoxActionHandlers.kt
@@ -7,7 +7,6 @@ import com.wix.invoke.MethodInvocation
 import org.json.JSONArray
 import org.json.JSONException
 import org.json.JSONObject
-import java.lang.Exception
 import java.lang.reflect.InvocationTargetException
 
 private const val LOG_TAG = "DetoxManager"
@@ -28,14 +27,14 @@ class ReadyActionHandler(
 }
 
 class ReactNativeReloadActionHandler(
-        private val rnContext: Context,
+        private val appContext: Context,
         private val wsClient: WebSocketClient,
         private val testEngineFacade: TestEngineFacade)
     : DetoxActionHandler {
 
     override fun handle(params: String, messageId: Long) {
         testEngineFacade.syncIdle()
-        testEngineFacade.reloadReactNative(rnContext)
+        testEngineFacade.reloadReactNative(appContext)
         wsClient.sendAction("ready", emptyMap<Any, Any>(), messageId)
     }
 }
@@ -45,7 +44,6 @@ class InvokeActionHandler(
         private val wsClient: WebSocketClient)
     : DetoxActionHandler {
 
-
     override fun handle(params: String, messageId: Long) {
         try {
             val invocationResult = methodInvocation.invoke(params)
@@ -61,7 +59,6 @@ class InvokeActionHandler(
 }
 
 class CleanupActionHandler(
-        private val rnContext: Context,
         private val wsClient: WebSocketClient,
         private val testEngineFacade: TestEngineFacade,
         private val doStopDetox: () -> Unit)
@@ -69,10 +66,9 @@ class CleanupActionHandler(
     override fun handle(params: String, messageId: Long) {
         val stopRunner = JSONObject(params).optBoolean("stopRunner", false)
         if (stopRunner) {
-            testEngineFacade.softResetReactNative()
             doStopDetox()
         } else {
-            testEngineFacade.hardResetReactNative(rnContext)
+            testEngineFacade.resetReactNative()
         }
         wsClient.sendAction("cleanupDone", emptyMap<Any, Any>(), messageId)
     }
diff --git a/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java b/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java
index 0596c4b98..2ff129231 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/DetoxManager.java
@@ -7,6 +7,7 @@ import android.os.Looper;
 import androidx.annotation.NonNull;
 import android.util.Log;
 
+import com.wix.detox.reactnative.ReactNativeExtension;
 import com.wix.detox.systeminfo.Environment;
 import com.wix.invoke.MethodInvocation;
 
@@ -80,7 +81,7 @@ class DetoxManager implements WebSocketClient.ActionHandler {
                 if (stopping) return;
                 stopping = true;
 
-                testEngineFacade.hardResetReactNative(reactNativeHostHolder);
+                testEngineFacade.resetReactNative();
 
                 actionHandlers.clear();
                 readyActionHandler = null;
@@ -118,9 +119,7 @@ class DetoxManager implements WebSocketClient.ActionHandler {
     }
 
     private void initReactNativeIfNeeded() {
-        if (ReactNativeSupport.isReactNativeApp()) {
-            ReactNativeSupport.waitForReactNativeLoad(reactNativeHostHolder, null);
-        }
+        ReactNativeExtension.waitForRNBootstrap(reactNativeHostHolder);
     }
 
     private void initWSClient() {
@@ -139,7 +138,7 @@ class DetoxManager implements WebSocketClient.ActionHandler {
         actionHandlers.put("reactNativeReload", new ReactNativeReloadActionHandler(reactNativeHostHolder, wsClient, testEngineFacade));
         actionHandlers.put("invoke", new InvokeActionHandler(new MethodInvocation(), wsClient));
         actionHandlers.put("currentStatus", new QueryStatusActionHandler(wsClient, testEngineFacade));
-        actionHandlers.put("cleanup", new CleanupActionHandler(reactNativeHostHolder, wsClient, testEngineFacade, new Function0<Unit>() {
+        actionHandlers.put("cleanup", new CleanupActionHandler(wsClient, testEngineFacade, new Function0<Unit>() {
             @Override
             public Unit invoke() {
                 stop();
diff --git a/detox/android/detox/src/main/java/com/wix/detox/ReactInstanceEventListenerProxy.java b/detox/android/detox/src/main/java/com/wix/detox/ReactInstanceEventListenerProxy.java
deleted file mode 100644
index a9314a75f..000000000
--- a/detox/android/detox/src/main/java/com/wix/detox/ReactInstanceEventListenerProxy.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.wix.detox;
-
-/**
- * Created by simonracz on 29/05/2017.
- */
-
-public interface ReactInstanceEventListenerProxy {
-    void onReactContextInitialized(Object reactContext);
-}
diff --git a/detox/android/detox/src/main/java/com/wix/detox/ReactNativeSupport.java b/detox/android/detox/src/main/java/com/wix/detox/ReactNativeSupport.java
deleted file mode 100644
index 9ae4a1f59..000000000
--- a/detox/android/detox/src/main/java/com/wix/detox/ReactNativeSupport.java
+++ /dev/null
@@ -1,333 +0,0 @@
-package com.wix.detox;
-
-import android.content.Context;
-import android.os.Looper;
-import android.util.Log;
-
-import com.facebook.react.ReactApplication;
-import com.facebook.react.ReactInstanceManager;
-import com.facebook.react.bridge.ReactContext;
-import com.wix.detox.espresso.AnimatedModuleIdlingResource;
-import com.wix.detox.espresso.ReactBridgeIdlingResource;
-import com.wix.detox.espresso.ReactNativeNetworkIdlingResource;
-import com.wix.detox.espresso.ReactNativeTimersIdlingResource;
-import com.wix.detox.espresso.ReactNativeUIModuleIdlingResource;
-
-import org.joor.Reflect;
-import org.joor.ReflectException;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.test.espresso.IdlingRegistry;
-import androidx.test.espresso.base.IdlingResourceRegistry;
-import androidx.test.platform.app.InstrumentationRegistry;
-import okhttp3.OkHttpClient;
-
-
-/**
- * Created by simonracz on 15/05/2017.
- */
-
-//TODO Dear reader, if you get this far and find this class messy you are not alone. :) It needs a refactor.
-
-public class ReactNativeSupport {
-    private static final String LOG_TAG = "DetoxRNSupport";
-
-    private static final String FIELD_UI_BG_MSG_QUEUE = "mUiBackgroundMessageQueueThread";
-    private static final String FIELD_NATIVE_MODULES_MSG_QUEUE = "mNativeModulesMessageQueueThread";
-    private static final String FIELD_JS_MSG_QUEUE = "mJSMessageQueueThread";
-    private static final String METHOD_GET_LOOPER = "getLooper";
-
-    private ReactNativeSupport() {
-        // static class
-    }
-
-    static boolean isReactNativeApp() {
-        Class<?> found = null;
-        try {
-            found = Class.forName("com.facebook.react.ReactApplication");
-        } catch (ClassNotFoundException e) {
-            return false;
-        }
-        return (found != null);
-    }
-
-    /**
-     * Returns the instanceManager using reflection.
-     *
-     * @param context the object that has a getReactNativeHost() method
-     * @return Returns the instanceManager as an Object or null
-     */
-    public static ReactInstanceManager getInstanceManager(@NonNull Context context) {
-        return ((ReactApplication) context).getReactNativeHost().getReactInstanceManager();
-    }
-
-    /**
-     * <p>
-     * Reloads the React Native application.
-     * </p>
-     *
-     * <p>
-     * It is a lot faster to reload a React Native application this way,
-     * than to reload the whole Activity or Application.
-     * </p>
-     *
-     * @param reactNativeHostHolder the object that has a getReactNativeHost() method
-     */
-    static void reloadApp(@NonNull Context reactNativeHostHolder) {
-        if (!isReactNativeApp()) {
-            return;
-        }
-        Log.i(LOG_TAG, "Reloading React Native");
-        currentReactContext = null;
-
-        removeEspressoIdlingResources(reactNativeHostHolder);
-
-        final ReactInstanceManager instanceManager = getInstanceManager(reactNativeHostHolder);
-        if (instanceManager == null) {
-            return;
-        }
-
-        final Context prereloadReactContext = instanceManager.getCurrentReactContext();
-
-        // Must be called on the UI thread!
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                instanceManager.recreateReactContextInBackground();
-            }
-        });
-
-        waitForReactNativeLoad(reactNativeHostHolder, prereloadReactContext);
-    }
-
-    // Ideally we would not store this at all.
-    public static ReactContext currentReactContext = null;
-
-    /**
-     * <p>
-     * Waits for a ReactContext to be created. Can be called any time.
-     * </p>
-     * @param reactNativeHostHolder the object that has a getReactNativeHost() method
-     * @param previousReactContext The previous context that we had before *re*-loading. Can be null.
-     */
-    static void waitForReactNativeLoad(@NonNull Context reactNativeHostHolder, final @Nullable Context previousReactContext) {
-        if (!isReactNativeApp()) {
-            return;
-        }
-
-        final ReactInstanceManager instanceManager = getInstanceManager(reactNativeHostHolder);
-        if (instanceManager == null) {
-            throw new RuntimeException("ReactInstanceManager is null!");
-        }
-
-        final CountDownLatch countDownLatch = new CountDownLatch(1);
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        ReactContext reactContext = instanceManager.getCurrentReactContext();
-                        if (reactContext != null && reactContext != previousReactContext) {
-                            Log.d(LOG_TAG, "Got new RN-context directly and immediately");
-                            countDownLatch.countDown();
-                            return;
-                        }
-
-                        instanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
-                            @Override
-                            public void onReactContextInitialized(ReactContext context) {
-                                Log.i(LOG_TAG, "Got new RN-context async'ly through listener");
-                                instanceManager.removeReactInstanceEventListener(this);
-                                countDownLatch.countDown();
-                            }
-                        });
-                    }
-                });
-
-
-        for (int i = 0; ; ) {
-            try {
-                if (!countDownLatch.await(1, TimeUnit.SECONDS)) {
-                    i++;
-                    if (i >= 60) {
-                        // First load can take a lot of time. (packager)
-                        // Loads afterwards should take less than a second.
-                        throw new RuntimeException("waited a whole minute for the new RN-context");
-                    }
-                } else {
-                    break;
-                }
-                // Due to an ugly timing issue in RN
-                // it is possible that our listener won't be ever called
-                // That's why we have to check the reactContext regularly.
-                ReactContext reactContext = instanceManager.getCurrentReactContext();
-                if (reactContext != null && reactContext != previousReactContext) {
-                    Log.d(LOG_TAG, "Got new RN-context explicitly while polling (#iteration="+i+")");
-                    break;
-                }
-            } catch (InterruptedException e) {
-                throw new RuntimeException("waiting for new RN-context got interrupted", e);
-            }
-        }
-
-        currentReactContext = instanceManager.getCurrentReactContext();
-        setupEspressoIdlingResources(currentReactContext);
-
-        hackRN50OrHigherWaitForReload();
-    }
-
-    private static void hackRN50OrHigherWaitForReload() {
-        if (ReactNativeCompat.getMinor() >= 50) {
-            try {
-                //TODO- Temp hack to make Detox usable for RN>=50 till we find a better sync solution.
-                Thread.sleep(1000);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-        }
-    }
-
-    private static ReactNativeTimersIdlingResource rnTimerIdlingResource = null;
-    private static ReactBridgeIdlingResource rnBridgeIdlingResource = null;
-    private static ReactNativeUIModuleIdlingResource rnUIModuleIdlingResource = null;
-    private static AnimatedModuleIdlingResource animIdlingResource = null;
-
-    private static void setupEspressoIdlingResources(@NonNull ReactContext reactContext) {
-        removeEspressoIdlingResources(reactContext);
-        Log.i(LOG_TAG, "Setting up Espresso Idling Resources for React Native.");
-
-        setupReactNativeQueueInterrogators(reactContext);
-
-        rnBridgeIdlingResource = new ReactBridgeIdlingResource(reactContext);
-        rnTimerIdlingResource = new ReactNativeTimersIdlingResource(reactContext);
-        rnUIModuleIdlingResource = new ReactNativeUIModuleIdlingResource(reactContext);
-        animIdlingResource = new AnimatedModuleIdlingResource(reactContext);
-
-        IdlingRegistry.getInstance().register(rnTimerIdlingResource);
-        IdlingRegistry.getInstance().register(rnBridgeIdlingResource);
-        IdlingRegistry.getInstance().register(rnUIModuleIdlingResource);
-        IdlingRegistry.getInstance().register(animIdlingResource);
-
-        if (networkSyncEnabled) {
-            setupNetworkIdlingResource();
-        }
-    }
-
-    private static void setupReactNativeQueueInterrogators(@NonNull Object reactContext) {
-        Looper UIBackgroundMessageQueue = getLooperFromQueue(reactContext, FIELD_UI_BG_MSG_QUEUE);
-        Looper JSMessageQueue = getLooperFromQueue(reactContext, FIELD_JS_MSG_QUEUE);
-        Looper JMativeModulesMessageQueue = getLooperFromQueue(reactContext, FIELD_NATIVE_MODULES_MSG_QUEUE);
-
-//        IdlingRegistry.getInstance().registerLooperAsIdlingResource(UIBackgroundMessageQueue);
-        IdlingRegistry.getInstance().registerLooperAsIdlingResource(JSMessageQueue);
-        IdlingRegistry.getInstance().registerLooperAsIdlingResource(JMativeModulesMessageQueue);
-
-        IdlingResourceRegistry irr = Reflect.on(androidx.test.espresso.Espresso.class).field("baseRegistry").get();
-        irr.sync(IdlingRegistry.getInstance().getResources(), IdlingRegistry.getInstance().getLoopers());
-    }
-
-    private static Looper getLooperFromQueue(@NonNull Object reactContext, String queueName) {
-        Object queue;
-        Looper looper = null;
-
-        try {
-            queue = Reflect.on(reactContext).field(queueName).get();
-            if (queue != null) {
-                looper = Reflect.on(queue).call(METHOD_GET_LOOPER).get();
-            }
-        } catch (ReflectException e) {
-            Log.e(LOG_TAG, "Could not find looper queue: " + queueName, e.getCause());
-            return null;
-        }
-        return looper;
-    }
-
-    static void removeEspressoIdlingResources(@NonNull Context reactNativeHostHolder) {
-        final ReactInstanceManager instanceManager = getInstanceManager(reactNativeHostHolder);
-        removeNetworkIdlingResource();
-        removeEspressoIdlingResources(instanceManager.getCurrentReactContext());
-    }
-
-    private static void removeEspressoIdlingResources(ReactContext reactContext) {
-
-        Log.i(LOG_TAG, "Removing Espresso IdlingResources for React Native.");
-
-        IdlingRegistry.getInstance().unregister(rnTimerIdlingResource);
-        IdlingRegistry.getInstance().unregister(rnBridgeIdlingResource);
-        IdlingRegistry.getInstance().unregister(rnUIModuleIdlingResource);
-        IdlingRegistry.getInstance().unregister(animIdlingResource);
-
-        reactContext.getCatalystInstance().removeBridgeIdleDebugListener(rnBridgeIdlingResource);
-    }
-
-    private static boolean networkSyncEnabled = true;
-    public static void enableNetworkSynchronization(boolean enable) {
-        if (!isReactNativeApp()) return;
-        if (networkSyncEnabled == enable) return;
-
-        if (enable) {
-            setupNetworkIdlingResource();
-        } else {
-            removeNetworkIdlingResource();
-        }
-        networkSyncEnabled = enable;
-    }
-
-    private static ReactNativeNetworkIdlingResource networkIR = null;
-    private final static String CLASS_NETWORK_MODULE = "com.facebook.react.modules.network.NetworkingModule";
-    private final static String METHOD_GET_NATIVE_MODULE = "getNativeModule";
-    private final static String METHOD_HAS_NATIVE_MODULE = "hasNativeModule";
-    private final static String FIELD_OKHTTP_CLIENT = "mClient";
-
-    private static void setupNetworkIdlingResource() {
-        Class<?> networkModuleClass;
-        try {
-            networkModuleClass = Class.forName(CLASS_NETWORK_MODULE);
-        } catch (ClassNotFoundException e) {
-            Log.e(LOG_TAG, "NetworkingModule is not on classpath.");
-            return;
-        }
-
-        if (currentReactContext == null) {
-            return;
-        }
-
-        try {
-            if (!(boolean) Reflect.on(currentReactContext).call(METHOD_HAS_NATIVE_MODULE, networkModuleClass).get()) {
-                Log.e(LOG_TAG, "Can't find Networking Module.");
-                return;
-            }
-
-            OkHttpClient client = Reflect.on(currentReactContext)
-                    .call(METHOD_GET_NATIVE_MODULE, networkModuleClass)
-                    .field(FIELD_OKHTTP_CLIENT)
-                    .get();
-            networkIR = new ReactNativeNetworkIdlingResource(client.dispatcher());
-            IdlingRegistry.getInstance().register(networkIR);
-
-        } catch (ReflectException e) {
-            Log.e(LOG_TAG, "Can't set up Networking Module listener", e.getCause());
-        }
-    }
-
-    private static void removeNetworkIdlingResource() {
-        if (networkIR != null) {
-            networkIR.stop();
-            IdlingRegistry.getInstance().unregister(networkIR);
-            networkIR = null;
-        }
-    }
-
-    public static void pauseRNTimersIdlingResource() {
-        if (rnTimerIdlingResource != null) {
-            rnTimerIdlingResource.pause();
-        }
-    }
-
-    public static void resumeRNTimersIdlingResource() {
-        rnTimerIdlingResource.resume();
-    }
-}
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/AnimatedModuleIdlingResource.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/AnimatedModuleIdlingResource.java
index 962b7e12c..fb0f88805 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/AnimatedModuleIdlingResource.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/AnimatedModuleIdlingResource.java
@@ -4,7 +4,7 @@ import androidx.annotation.NonNull;
 import android.util.Log;
 import android.view.Choreographer;
 
-import com.wix.detox.ReactNativeCompat;
+import com.wix.detox.reactnative.ReactNativeCompat;
 
 import org.joor.Reflect;
 import org.joor.ReflectException;
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/EspressoDetox.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/EspressoDetox.java
index 3971d7edc..dfa185698 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/EspressoDetox.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/EspressoDetox.java
@@ -9,7 +9,7 @@ import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 
-import com.wix.detox.ReactNativeSupport;
+import com.wix.detox.reactnative.ReactNativeExtension;
 
 import org.hamcrest.Matcher;
 import org.joor.Reflect;
@@ -66,10 +66,9 @@ public class EspressoDetox {
 
             @Override
             public void perform(UiController uiController, View view) {
-                Activity activity;
-                if (ReactNativeSupport.currentReactContext != null) {
-                    activity = Reflect.on(ReactNativeSupport.currentReactContext).call(METHOD_GET_ACTIVITY).get();
-                } else {
+                Activity activity = ReactNativeExtension.getRNActivity(view.getContext().getApplicationContext());
+
+                if (activity == null) {
                     activity = getActivity(view.getContext());
                     if (activity == null && view instanceof ViewGroup) {
                         ViewGroup v = (ViewGroup)view;
@@ -79,9 +78,11 @@ public class EspressoDetox {
                         }
                     }
                 }
+
                 if (activity == null) {
                     throw new RuntimeException("Couldn't get a hold of the Activity");
                 }
+
                 switch (orientation) {
                     case 0:
                         activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
@@ -104,7 +105,7 @@ public class EspressoDetox {
     }
 
     public static void setSynchronization(boolean enabled) {
-        ReactNativeSupport.enableNetworkSynchronization(enabled);
+        ReactNativeExtension.toggleNetworkSynchronization(enabled);
     }
 
     public static void setURLBlacklist(final ArrayList<String> urls) {
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/action/RNClickAction.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/action/RNClickAction.java
index 565c051df..b4c13bacf 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/action/RNClickAction.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/action/RNClickAction.java
@@ -4,7 +4,7 @@ import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.View;
 
-import com.wix.detox.ReactNativeSupport;
+import com.wix.detox.reactnative.ReactNativeExtension;
 
 import org.hamcrest.Matcher;
 
@@ -38,11 +38,11 @@ public class RNClickAction implements ViewAction {
 
     @Override
     public void perform(UiController uiController, View view) {
-        ReactNativeSupport.pauseRNTimersIdlingResource();
+        ReactNativeExtension.toggleTimersSynchronization(false);
         try {
             clickAction.perform(uiController, view);
         } finally {
-            ReactNativeSupport.resumeRNTimersIdlingResource();
+            ReactNativeExtension.toggleTimersSynchronization(true);
         }
         uiController.loopMainThreadUntilIdle();
     }
diff --git a/detox/android/detox/src/main/java/com/wix/detox/ReactNativeCompat.java b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeCompat.java
similarity index 95%
rename from detox/android/detox/src/main/java/com/wix/detox/ReactNativeCompat.java
rename to detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeCompat.java
index 17a71c347..9dcb0f8c1 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/ReactNativeCompat.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeCompat.java
@@ -1,4 +1,4 @@
-package com.wix.detox;
+package com.wix.detox.reactnative;
 
 import org.joor.Reflect;
 
diff --git a/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeExtension.kt b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeExtension.kt
new file mode 100644
index 000000000..62333a364
--- /dev/null
+++ b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeExtension.kt
@@ -0,0 +1,133 @@
+package com.wix.detox.reactnative
+
+import android.app.Activity
+import android.content.Context
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import com.facebook.react.ReactApplication
+import com.facebook.react.ReactInstanceManager
+import com.facebook.react.bridge.ReactContext
+
+private const val LOG_TAG = "DetoxRNExt"
+
+object ReactNativeExtension {
+    private var rnIdlingResources: ReactNativeIdlingResources? = null
+
+    /**
+     * Reloads the React Native context and thus all javascript code.
+     *
+     * It is a lot faster to reload a React Native application this way,
+     * than to reload the whole Activity or Application.
+     *
+     * @param applicationContext The app context, implicitly assumed to be a [ReactApplication] instance.
+     */
+    @JvmStatic
+    fun reloadReactNative(applicationContext: Context) {
+        if (!ReactNativeInfo.isReactNativeApp()) {
+            return
+        }
+
+        Log.i(LOG_TAG, "Reloading React Native")
+
+        (applicationContext as ReactApplication).let {
+            val networkSyncEnabled = rnIdlingResources?.networkSyncEnabled ?: true
+            clearIdlingResources()
+
+            val previousReactContext = getCurrentReactContextSafe(it)
+
+            reloadReactNativeInBackground(it)
+            val reactContext = awaitNewReactNativeContext(it, previousReactContext)
+
+            setupIdlingResources(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)
+
+            setupIdlingResources(reactContext)
+            hackRN50OrHigherWaitForReady()
+        }
+    }
+
+    @JvmStatic
+    fun clearAllSynchronization() = clearIdlingResources()
+
+    @JvmStatic
+    fun getRNActivity(applicationContext: Context): Activity? {
+        if (ReactNativeInfo.isReactNativeApp()) {
+            return getCurrentReactContextSafe(applicationContext as ReactApplication)?.currentActivity
+        }
+        return null
+    }
+
+    @JvmStatic
+    fun toggleNetworkSynchronization(enable: Boolean) {
+        if (!ReactNativeInfo.isReactNativeApp()) {
+            return
+        }
+
+        rnIdlingResources?.setNetworkSynchronization(enable)
+    }
+
+    @JvmStatic
+    fun toggleTimersSynchronization(enable: Boolean) {
+        rnIdlingResources?.let {
+            if (enable) it.resumeRNTimersIdlingResource() else it.pauseRNTimersIdlingResource()
+        }
+    }
+
+    private fun reloadReactNativeInBackground(reactApplication: ReactApplication) {
+        val rnReloader = ReactNativeReLoader(InstrumentationRegistry.getInstrumentation(), reactApplication)
+        rnReloader.reloadInBackground()
+    }
+
+    private fun awaitNewReactNativeContext(reactApplication: ReactApplication, previousReactContext: ReactContext?): ReactContext {
+        val rnLoadingMonitor = ReactNativeLoadingMonitor(InstrumentationRegistry.getInstrumentation(), reactApplication, previousReactContext)
+        return rnLoadingMonitor.getNewContext()!!
+    }
+
+    private fun setupIdlingResources(reactContext: ReactContext, networkSyncEnabled: Boolean = true) {
+        rnIdlingResources = ReactNativeIdlingResources(reactContext, networkSyncEnabled).apply {
+            registerAll()
+        }
+    }
+
+    private fun hackRN50OrHigherWaitForReady() {
+        if (ReactNativeCompat.getMinor() >= 50) {
+            try {
+                //TODO- Temp hack to make Detox usable for RN>=50 till we find a better sync solution.
+                Thread.sleep(1000)
+            } catch (e: InterruptedException) {
+                e.printStackTrace()
+            }
+
+        }
+    }
+
+    private fun clearIdlingResources() {
+        rnIdlingResources?.unregisterAll()
+        rnIdlingResources = null
+    }
+
+    private fun getInstanceManagerSafe(reactApplication: ReactApplication): ReactInstanceManager {
+        return reactApplication.reactNativeHost.reactInstanceManager
+                ?: throw RuntimeException("ReactInstanceManager is null!")
+    }
+
+    private fun getCurrentReactContextSafe(reactApplication: ReactApplication): ReactContext? {
+        return getInstanceManagerSafe(reactApplication).currentReactContext
+    }
+}
diff --git a/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeIdlingResources.kt b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeIdlingResources.kt
new file mode 100644
index 000000000..4bda4aa90
--- /dev/null
+++ b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeIdlingResources.kt
@@ -0,0 +1,170 @@
+package com.wix.detox.reactnative
+
+import android.os.Looper
+import android.util.Log
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.base.IdlingResourceRegistry
+import com.facebook.react.bridge.ReactContext
+import com.facebook.react.modules.network.NetworkingModule
+import com.wix.detox.espresso.*
+import okhttp3.OkHttpClient
+import org.joor.Reflect
+import org.joor.ReflectException
+
+private const val LOG_TAG = "DetoxRNIdleRes"
+
+class MQThreadsReflector(private val reactContext: ReactContext) {
+    fun getQueue(queueName: String): MQThreadReflected? {
+        try {
+            val queue = Reflect.on(reactContext).field(queueName).get() as Any?
+            return MQThreadReflected(queue)
+        } catch (e: ReflectException) {
+            Log.e(LOG_TAG, "Could not find queue: $queueName", e)
+        }
+        return null
+    }
+}
+
+class MQThreadReflected(private val queue: Any?) {
+    fun getLooper(): Looper? {
+        try {
+            if (queue != null) {
+                return Reflect.on(queue).call(METHOD_GET_LOOPER).get()
+            }
+        } catch (e: ReflectException) {
+            Log.e(LOG_TAG, "Could not find looper for queue", e)
+        }
+        return null
+    }
+
+    companion object {
+        const val METHOD_GET_LOOPER = "getLooper"
+    }
+}
+
+class NetworkingModuleReflected(private val reactContext: ReactContext) {
+    fun getHttpClient(): OkHttpClient? {
+        if (reactContext.hasNativeModule(NetworkingModule::class.java)) {
+            val networkNativeModule = reactContext.getNativeModule(NetworkingModule::class.java)
+            try {
+                return Reflect.on(networkNativeModule).field(FIELD_OKHTTP_CLIENT).get()
+            } catch (e: ReflectException) {
+                Log.e(LOG_TAG, "Can't set up Networking Module listener", e)
+            }
+        }
+        return null
+    }
+
+    companion object {
+        private const val FIELD_OKHTTP_CLIENT = "mClient"
+    }
+}
+
+class ReactNativeIdlingResources
+    constructor(
+        private val reactContext: ReactContext,
+        internal var networkSyncEnabled: Boolean = true) {
+
+    companion object {
+        private const val FIELD_UI_BG_MSG_QUEUE = "mUiBackgroundMessageQueueThread"
+        private const val FIELD_NATIVE_MODULES_MSG_QUEUE = "mNativeModulesMessageQueueThread"
+        private const val FIELD_JS_MSG_QUEUE = "mJSMessageQueueThread"
+    }
+
+    private var rnTimerIdlingResource: ReactNativeTimersIdlingResource? = null
+    private var rnBridgeIdlingResource: ReactBridgeIdlingResource? = null
+    private var rnUIModuleIdlingResource: ReactNativeUIModuleIdlingResource? = null
+    private var animIdlingResource: AnimatedModuleIdlingResource? = null
+    private var networkIdlingResource: ReactNativeNetworkIdlingResource? = null
+
+    fun registerAll() {
+        removeEspressoIdlingResources(reactContext)
+        Log.i(LOG_TAG, "Setting up Espresso Idling Resources for React Native.")
+
+        registerMQThreadsInterrogators(reactContext)
+
+        rnBridgeIdlingResource = ReactBridgeIdlingResource(reactContext)
+        rnTimerIdlingResource = ReactNativeTimersIdlingResource(reactContext)
+        rnUIModuleIdlingResource = ReactNativeUIModuleIdlingResource(reactContext)
+        animIdlingResource = AnimatedModuleIdlingResource(reactContext)
+
+        IdlingRegistry.getInstance().apply {
+            register(rnTimerIdlingResource)
+            register(rnBridgeIdlingResource)
+            register(rnUIModuleIdlingResource)
+            register(animIdlingResource)
+        }
+
+        if (networkSyncEnabled) {
+            setupNetworkIdlingResource()
+        }
+    }
+
+    fun unregisterAll() {
+        removeNetworkIdlingResource()
+        removeEspressoIdlingResources(reactContext)
+    }
+
+    fun setNetworkSynchronization(enable: Boolean) {
+        if (networkSyncEnabled == enable) {
+            return
+        }
+
+        if (enable) {
+            setupNetworkIdlingResource()
+        } else {
+            removeNetworkIdlingResource()
+        }
+        networkSyncEnabled = enable
+    }
+
+    fun pauseRNTimersIdlingResource() = rnTimerIdlingResource?.pause()
+    fun resumeRNTimersIdlingResource() = rnTimerIdlingResource?.resume()
+
+    private fun registerMQThreadsInterrogators(reactContext: ReactContext) {
+        val mqThreadsReflector = MQThreadsReflector(reactContext)
+        val mqUIBackground = mqThreadsReflector.getQueue(FIELD_UI_BG_MSG_QUEUE)?.getLooper()
+        val mqJS = mqThreadsReflector.getQueue(FIELD_JS_MSG_QUEUE)?.getLooper()
+        val mqNativeModules = mqThreadsReflector.getQueue(FIELD_NATIVE_MODULES_MSG_QUEUE)?.getLooper()
+
+        IdlingRegistry.getInstance().apply {
+//            registerLooperAsIdlingResource(mqUIBackground)
+            registerLooperAsIdlingResource(mqJS)
+            registerLooperAsIdlingResource(mqNativeModules)
+
+            val irr: IdlingResourceRegistry = Reflect.on(androidx.test.espresso.Espresso::class.java).field("baseRegistry").get()
+            irr.sync(this.resources, this.loopers)
+        }
+    }
+
+    private fun removeEspressoIdlingResources(reactContext: ReactContext) {
+        Log.i(LOG_TAG, "Removing Espresso IdlingResources for React Native.")
+
+        IdlingRegistry.getInstance().apply {
+            unregister(rnTimerIdlingResource)
+            unregister(rnBridgeIdlingResource)
+            unregister(rnUIModuleIdlingResource)
+            unregister(animIdlingResource)
+        }
+
+        reactContext.catalystInstance.removeBridgeIdleDebugListener(rnBridgeIdlingResource)
+    }
+
+    private fun setupNetworkIdlingResource() {
+        try {
+            val client = NetworkingModuleReflected(reactContext).getHttpClient()!!
+            networkIdlingResource = ReactNativeNetworkIdlingResource(client.dispatcher())
+            IdlingRegistry.getInstance().register(networkIdlingResource)
+        } catch (e: ReflectException) {
+            Log.e(LOG_TAG, "Can't set up Networking Module listener", e.cause)
+        }
+    }
+
+    private fun removeNetworkIdlingResource() {
+        networkIdlingResource?.let {
+            it.stop()
+            IdlingRegistry.getInstance().unregister(networkIdlingResource)
+            networkIdlingResource = null
+        }
+    }
+}
diff --git a/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeInfo.kt b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeInfo.kt
new file mode 100644
index 000000000..94e0ac4d7
--- /dev/null
+++ b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeInfo.kt
@@ -0,0 +1,10 @@
+package com.wix.detox.reactnative
+
+object ReactNativeInfo {
+    fun isReactNativeApp(): Boolean = try {
+        Class.forName("com.facebook.react.ReactApplication")
+        true
+    } catch (e: ClassNotFoundException) {
+        false
+    }
+}
diff --git a/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeLoadingMonitor.kt b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeLoadingMonitor.kt
new file mode 100644
index 000000000..cd62ba782
--- /dev/null
+++ b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeLoadingMonitor.kt
@@ -0,0 +1,77 @@
+package com.wix.detox.reactnative
+
+import android.app.Instrumentation
+import android.util.Log
+import com.facebook.react.ReactApplication
+import com.facebook.react.ReactInstanceManager
+import com.facebook.react.bridge.ReactContext
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+private const val LOG_TAG = "DetoxRNLoading"
+
+open class ReactNativeLoadingMonitor(
+        private val instrumentation: Instrumentation,
+        private val rnApplication: ReactApplication,
+        private val previousReactContext: ReactContext?) {
+    val countDownLatch = CountDownLatch(1)
+
+    fun getNewContext(): ReactContext? {
+        subscribeToNewRNContextUpdates()
+        return awaitNewRNContext()
+    }
+
+    private fun subscribeToNewRNContextUpdates() {
+        instrumentation.runOnMainSync(
+                Runnable {
+                    val rnInstanceManager = rnApplication.reactNativeHost.reactInstanceManager
+                    val reactContext = rnInstanceManager.currentReactContext
+                    if (reactContext != null && reactContext !== previousReactContext) {
+                        Log.d(LOG_TAG, "Got new RN-context directly and immediately")
+                        countDownLatch.countDown()
+                        return@Runnable
+                    }
+
+                    rnInstanceManager.addReactInstanceEventListener(object : ReactInstanceManager.ReactInstanceEventListener {
+                        override fun onReactContextInitialized(context: ReactContext) {
+                            Log.i(LOG_TAG, "Got new RN-context async'ly through listener")
+                            rnInstanceManager.removeReactInstanceEventListener(this)
+                            countDownLatch.countDown()
+                        }
+                    })
+                })
+    }
+
+    private fun awaitNewRNContext(): ReactContext? {
+        val rnInstanceManager = rnApplication.reactNativeHost.reactInstanceManager
+
+        var i = 0
+        while (true) {
+            try {
+                if (!countDownLatch.await(1, TimeUnit.SECONDS)) {
+                    i++
+                    if (i >= 60) {
+                        // First load can take a lot of time. (packager)
+                        // Loads afterwards should take less than a second.
+                        throw RuntimeException("waited a whole minute for the new RN-context")
+                    }
+                } else {
+                    break
+                }
+
+                // Due to an ugly timing issue in RN
+                // it is possible that our listener won't be ever called
+                // That's why we have to check the reactContext regularly.
+                val reactContext = rnInstanceManager.currentReactContext
+                if (reactContext != null && reactContext !== previousReactContext) {
+                    Log.d(LOG_TAG, "Got new RN-context explicitly while polling (#iteration=$i)")
+                    break
+                }
+            } catch (e: InterruptedException) {
+                throw RuntimeException("waiting for new RN-context got interrupted", e)
+            }
+        }
+
+        return rnInstanceManager.currentReactContext
+    }
+}
diff --git a/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeReloader.kt b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeReloader.kt
new file mode 100644
index 000000000..493ee959f
--- /dev/null
+++ b/detox/android/detox/src/main/java/com/wix/detox/reactnative/ReactNativeReloader.kt
@@ -0,0 +1,16 @@
+package com.wix.detox.reactnative
+
+import android.app.Instrumentation
+import com.facebook.react.ReactApplication
+
+open class ReactNativeReLoader(
+        private val instrumentation: Instrumentation,
+        private val rnApplication: ReactApplication) {
+
+    fun reloadInBackground() {
+        val rnInstanceManager = rnApplication.reactNativeHost.reactInstanceManager
+        instrumentation.runOnMainSync {
+            rnInstanceManager.recreateReactContextInBackground()
+        }
+    }
+}
diff --git a/detox/package.json b/detox/package.json
index e39d7ac97..fb7d12c8b 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -1,7 +1,7 @@
 {
   "name": "detox",
   "description": "E2E tests and automation for mobile",
-  "version": "14.7.1",
+  "version": "14.6.1",
   "bin": {
     "detox": "local-cli/cli.js"
   },
diff --git a/detox/src/Detox.js b/detox/src/Detox.js
index a4bb63f77..6c7a1be43 100644
--- a/detox/src/Detox.js
+++ b/detox/src/Detox.js
@@ -23,12 +23,12 @@ const DEVICE_CLASSES = {
 };
 
 class Detox {
-  constructor({artifactsConfig, deviceConfig, session}) {
+  constructor({deviceConfig, session}) {
     this._deviceConfig = deviceConfig;
     this._userSession = deviceConfig.session || session;
     this._client = null;
     this._server = null;
-    this._artifactsManager = new ArtifactsManager(artifactsConfig);
+    this._artifactsManager = new ArtifactsManager();
 
     this.device = null;
   }
diff --git a/detox/src/DetoxExportWrapper.js b/detox/src/DetoxExportWrapper.js
index a8c693997..701f92aba 100644
--- a/detox/src/DetoxExportWrapper.js
+++ b/detox/src/DetoxExportWrapper.js
@@ -64,17 +64,8 @@ class DetoxExportWrapper {
         throw new Error(`There are no device configurations in the detox config`);
       }
 
-      const deviceConfig = configuration.composeDeviceConfig(detoxConfig);
-      const configurationName = _.findKey(detoxConfig.configurations, (config) => config === deviceConfig);
-      const artifactsConfig = configuration.composeArtifactsConfig({
-        configurationName,
-        detoxConfig,
-        deviceConfig,
-      });
-
       instance = new Detox({
-        deviceConfig,
-        artifactsConfig,
+        deviceConfig: DetoxExportWrapper._getDeviceConfig(detoxConfig),
         session: detoxConfig.session,
       });
 
@@ -91,6 +82,32 @@ class DetoxExportWrapper {
     }
   }
 
+  static _getDeviceConfig({ configurations }) {
+    const configurationName = argparse.getArgValue('configuration');
+    const deviceOverride = argparse.getArgValue('device-name');
+
+    const deviceConfig = (!configurationName && _.size(configurations) === 1)
+      ? _.values(configurations)[0]
+      : configurations[configurationName];
+
+    if (!deviceConfig) {
+      throw new Error(`Cannot determine which configuration to use. use --configuration to choose one of the following:
+                        ${Object.keys(configurations)}`);
+    }
+
+    if (!deviceConfig.type) {
+      configuration.throwOnEmptyType();
+    }
+
+    deviceConfig.device = deviceOverride || deviceConfig.device || deviceConfig.name;
+    delete deviceConfig.name;
+
+    if (_.isEmpty(deviceConfig.device)) {
+      configuration.throwOnEmptyDevice();
+    }
+
+    return deviceConfig;
+  }
 }
 
 module.exports = DetoxExportWrapper;
diff --git a/detox/src/artifacts/ArtifactsManager.js b/detox/src/artifacts/ArtifactsManager.js
index d9b249ba5..8d73f59d0 100644
--- a/detox/src/artifacts/ArtifactsManager.js
+++ b/detox/src/artifacts/ArtifactsManager.js
@@ -4,39 +4,26 @@ const path = require('path');
 const util = require('util');
 const FileArtifact = require('./templates/artifact/FileArtifact');
 const log = require('../utils/logger').child({ __filename });
+const argparse = require('../utils/argparse');
 const ArtifactPathBuilder = require('./utils/ArtifactPathBuilder');
 
 class ArtifactsManager {
-  constructor({ rootDir, pathBuilder, plugins } = {}) {
+  constructor(pathBuilder) {
     this.onTerminate = _.once(this.onTerminate.bind(this));
 
-    this._pluginConfigs = plugins;
     this._idlePromise = Promise.resolve();
     this._idleCallbackRequests = [];
     this._activeArtifacts = [];
     this._artifactPlugins = {};
-
-    this._pathBuilder = this._instantiatePathBuilder(pathBuilder, rootDir);
-  }
-
-  _instantiatePathBuilder(pathBuilderFactory, rootDir) {
-    if (typeof pathBuilderFactory === 'function') {
-      return pathBuilderFactory({ rootDir });
-    }
-
-    if (pathBuilderFactory) {
-      return pathBuilderFactory;
-    }
-
-    return new ArtifactPathBuilder({ rootDir });
+    this._pathBuilder = pathBuilder || new ArtifactPathBuilder({
+      artifactsRootDir: argparse.getArgValue('artifacts-location') || 'artifacts',
+    });
   }
 
-  _instantitateArtifactPlugin(pluginFactory, pluginUserConfig) {
+  _instantitateArtifactPlugin(pluginFactory) {
     const artifactsApi = {
       plugin: null,
 
-      userConfig: { ...pluginUserConfig },
-
       preparePathForArtifact: async (artifactName, testSummary) => {
         const artifactPath = this._pathBuilder.buildPathForTestArtifact(artifactName, testSummary);
         const artifactDir = path.dirname(artifactPath);
@@ -83,8 +70,7 @@ class ArtifactsManager {
 
   registerArtifactPlugins(artifactPluginFactoriesMap = {}) {
     for (const [key, factory] of Object.entries(artifactPluginFactoriesMap)) {
-      const config = this._pluginConfigs[key];
-      this._artifactPlugins[key] = this._instantitateArtifactPlugin(factory, config);
+      this._artifactPlugins[key] = this._instantitateArtifactPlugin(factory);
     }
   }
 
diff --git a/detox/src/artifacts/__mocks__/FakePathBuilder.js b/detox/src/artifacts/__mocks__/FakePathBuilder.js
deleted file mode 100644
index 762912021..000000000
--- a/detox/src/artifacts/__mocks__/FakePathBuilder.js
+++ /dev/null
@@ -1,7 +0,0 @@
-class FakePathBuilder {
-  buildPathForTestArtifact(artifactName, testSummary) {
-    return (testSummary ? (testSummary.fullName + '/') : '') + artifactName;
-  }
-}
-
-module.exports = FakePathBuilder;
\ No newline at end of file
diff --git a/detox/src/artifacts/instruments/SimulatorInstrumentsPlugin.js b/detox/src/artifacts/instruments/SimulatorInstrumentsPlugin.js
index 94bcaa245..3e04c07ca 100644
--- a/detox/src/artifacts/instruments/SimulatorInstrumentsPlugin.js
+++ b/detox/src/artifacts/instruments/SimulatorInstrumentsPlugin.js
@@ -1,11 +1,16 @@
+const fs = require('fs-extra');
 const temporaryPath = require('../utils/temporaryPath');
+const argparse = require('../../utils/argparse');
+const log = require('../../utils/logger').child({ __filename });
 const WholeTestRecorderPlugin = require('../templates/plugin/WholeTestRecorderPlugin');
 const SimulatorInstrumentsRecording = require('./SimulatorInstrumentsRecording');
 
 class SimulatorInstrumentsPlugin extends WholeTestRecorderPlugin {
-  constructor({ api, client }) {
-    super({ api });
-    this.client = client;
+  constructor(config) {
+    super(config);
+
+    this.client = config.client;
+    this.enabled = argparse.getArgValue('record-performance') === 'all';
   }
 
   async onBeforeUninstallApp(event) {
@@ -59,22 +64,6 @@ class SimulatorInstrumentsPlugin extends WholeTestRecorderPlugin {
   async preparePathForTestArtifact(testSummary) {
     return this.api.preparePathForArtifact('test.dtxrec', testSummary);
   }
-
-  static parseConfig(config) {
-    switch (config) {
-      case 'all':
-        return {
-          enabled: true,
-          keepOnlyFailedTestsArtifacts: false,
-        };
-      case 'none':
-      default:
-        return {
-          enabled: false,
-          keepOnlyFailedTestsArtifacts: false,
-        };
-    }
-  }
 }
 
 module.exports = SimulatorInstrumentsPlugin;
diff --git a/detox/src/artifacts/log/LogArtifactPlugin.js b/detox/src/artifacts/log/LogArtifactPlugin.js
index c36c3f627..cf5d9e927 100644
--- a/detox/src/artifacts/log/LogArtifactPlugin.js
+++ b/detox/src/artifacts/log/LogArtifactPlugin.js
@@ -1,5 +1,4 @@
-const logger = require('../../utils/logger');
-const FileArtifact = require('../templates/artifact/FileArtifact');
+const argparse = require('../../utils/argparse');
 const StartupAndTestRecorderPlugin = require('../templates/plugin/StartupAndTestRecorderPlugin');
 const getTimeStampString = require('../utils/getTimeStampString');
 
@@ -7,26 +6,13 @@ const getTimeStampString = require('../utils/getTimeStampString');
  * @abstract
  */
 class LogArtifactPlugin extends StartupAndTestRecorderPlugin {
-  constructor({ api }) {
-    super({ api });
-  }
-
-  async onAfterAll() {
-    await super.onAfterAll();
+  constructor(config) {
+    super(config);
 
-    if (this.shouldKeepArtifactOfSession()) {
-      this.api.requestIdleCallback(async () => {
-        const [jsonLogPath, plainLogPath] = await Promise.all([
-          this.api.preparePathForArtifact(`detox_pid_${process.pid}.json.log`),
-          this.api.preparePathForArtifact(`detox_pid_${process.pid}.log`),
-        ]);
+    const recordLogs = argparse.getArgValue('record-logs');
 
-        await Promise.all([
-          new FileArtifact({ temporaryPath: logger.jsonFileStreamPath }).save(jsonLogPath, { append: true }),
-          new FileArtifact({ temporaryPath: logger.plainFileStreamPath }).save(plainLogPath, { append: true })
-        ]);
-      });
-    }
+    this.enabled = recordLogs && recordLogs !== 'none';
+    this.keepOnlyFailedTestsArtifacts = recordLogs === 'failing';
   }
 
   async onBeforeShutdownDevice(event) {
@@ -47,27 +33,6 @@ class LogArtifactPlugin extends StartupAndTestRecorderPlugin {
   async preparePathForTestArtifact(testSummary) {
     return this.api.preparePathForArtifact('process.log', testSummary);
   }
-
-  static parseConfig(config) {
-    switch (config) {
-      case 'failing':
-        return {
-          enabled: true,
-          keepOnlyFailedTestsArtifacts: true,
-        };
-      case 'all':
-        return {
-          enabled: true,
-          keepOnlyFailedTestsArtifacts: false,
-        };
-      case 'none':
-      default:
-        return {
-          enabled: false,
-          keepOnlyFailedTestsArtifacts: false,
-        };
-    }
-  }
 }
 
 module.exports = LogArtifactPlugin;
diff --git a/detox/src/artifacts/screenshot/ScreenshotArtifactPlugin.js b/detox/src/artifacts/screenshot/ScreenshotArtifactPlugin.js
index c2ef86994..117fa3ef4 100644
--- a/detox/src/artifacts/screenshot/ScreenshotArtifactPlugin.js
+++ b/detox/src/artifacts/screenshot/ScreenshotArtifactPlugin.js
@@ -1,46 +1,25 @@
+const _ = require('lodash');
+const argparse = require('../../utils/argparse');
+const log = require('../../utils/logger').child({ __filename });
 const TwoSnapshotsPerTestPlugin = require('../templates/plugin/TwoSnapshotsPerTestPlugin');
 
 /***
  * @abstract
  */
 class ScreenshotArtifactPlugin extends TwoSnapshotsPerTestPlugin {
-  constructor({ api }) {
-    super({ api });
+  constructor(config) {
+    super(config);
+
+    const takeScreenshots = argparse.getArgValue('take-screenshots');
+
+    this.enabled = !takeScreenshots || takeScreenshots !== 'none';
+    this.shouldTakeAutomaticSnapshots = takeScreenshots === 'failing' || takeScreenshots === 'all';
+    this.keepOnlyFailedTestsArtifacts = takeScreenshots === 'failing';
   }
 
   async preparePathForSnapshot(testSummary, name) {
     return this.api.preparePathForArtifact(`${name}.png`, testSummary);
   }
-
-  static parseConfig(config) {
-    switch (config) {
-      case 'failing':
-        return {
-          enabled: true,
-          shouldTakeAutomaticSnapshots: true,
-          keepOnlyFailedTestsArtifacts: true,
-        };
-      case 'all':
-        return {
-          enabled: true,
-          shouldTakeAutomaticSnapshots: true,
-          keepOnlyFailedTestsArtifacts: false,
-        };
-      case 'none':
-        return {
-          enabled: false,
-          shouldTakeAutomaticSnapshots: false,
-          keepOnlyFailedTestsArtifacts: false,
-        };
-      case 'manual':
-      default:
-        return {
-          enabled: true,
-          shouldTakeAutomaticSnapshots: false,
-          keepOnlyFailedTestsArtifacts: false,
-        };
-    }
-  }
 }
 
 module.exports = ScreenshotArtifactPlugin;
diff --git a/detox/src/artifacts/templates/artifact/FileArtifact.js b/detox/src/artifacts/templates/artifact/FileArtifact.js
index e898f6753..17d53e83c 100644
--- a/detox/src/artifacts/templates/artifact/FileArtifact.js
+++ b/detox/src/artifacts/templates/artifact/FileArtifact.js
@@ -1,6 +1,5 @@
 const fs = require('fs-extra');
 const Artifact = require('./Artifact');
-const appendFile = require('../../../utils/appendFile');
 
 class FileArtifact extends Artifact {
   constructor(template) {
@@ -12,36 +11,23 @@ class FileArtifact extends Artifact {
     }
   }
 
-  async doSave(artifactPath, options = {}) {
-    await FileArtifact.moveTemporaryFile(this.logger, this.temporaryPath, artifactPath, options.append);
+  async doSave(artifactPath) {
+    await FileArtifact.moveTemporaryFile(this.logger, this.temporaryPath, artifactPath);
   }
 
   async doDiscard() {
     await fs.remove(this.temporaryPath);
   }
 
-  static async moveTemporaryFile(logger, source, destination, canAppend = false) {
-    if (!await fs.exists(source)) {
-      logger.warn({event: 'MOVE_FILE_MISSING'}, `did not find temporary file: ${source}`);
-      return false;
-    }
-
-    if (!await fs.exists(destination)) {
-      logger.debug({event: 'MOVE_FILE'}, `moving "${source}" to ${destination}`);
+  static async moveTemporaryFile(logger, source, destination) {
+    if (await fs.exists(source)) {
+      logger.debug({ event: 'MOVE_FILE' }, `moving "${source}" to ${destination}`);
       await fs.move(source, destination);
       return true;
+    } else {
+      logger.warn({ event: 'MOVE_FILE_MISSING'} , `did not find temporary file: ${source}`);
+      return false;
     }
-
-    if (canAppend) {
-      logger.debug({event: 'MOVE_FILE'}, `moving "${source}" to ${destination} via appending`);
-      await appendFile(source, destination);
-      await fs.remove(source);
-      return true;
-    }
-
-    logger.warn({event: 'MOVE_FILE_EXISTS'}, `cannot overwrite: "${source}" => "${destination}"`);
-    await fs.remove(source);
-    return false;
   }
 }
 
diff --git a/detox/src/artifacts/templates/plugin/ArtifactPlugin.js b/detox/src/artifacts/templates/plugin/ArtifactPlugin.js
index 0914ecbf2..d04169ea1 100644
--- a/detox/src/artifacts/templates/plugin/ArtifactPlugin.js
+++ b/detox/src/artifacts/templates/plugin/ArtifactPlugin.js
@@ -12,8 +12,8 @@ class ArtifactPlugin {
   constructor({ api }) {
     this.api = api;
     this.context = {};
-    this.enabled = api.userConfig.enabled;
-    this.keepOnlyFailedTestsArtifacts = api.userConfig.keepOnlyFailedTestsArtifacts;
+    this.enabled = false;
+    this.keepOnlyFailedTestsArtifacts = false;
     this.priority = 16;
     this._disableReason = '';
     this._hasFailingTests = false;
diff --git a/detox/src/artifacts/templates/plugin/__mocks__/ArtifactsApi.mock.js b/detox/src/artifacts/templates/plugin/__mocks__/ArtifactsApi.mock.js
index 75b9dc75c..228d21fd4 100644
--- a/detox/src/artifacts/templates/plugin/__mocks__/ArtifactsApi.mock.js
+++ b/detox/src/artifacts/templates/plugin/__mocks__/ArtifactsApi.mock.js
@@ -1,7 +1,5 @@
 class ArtifactsApiMock {
-  constructor({ config }) {
-    this.userConfig = config;
-
+  constructor() {
     this.preparePathForArtifact = jest.fn();
     this.trackArtifact = jest.fn();
     this.untrackArtifact = jest.fn();
diff --git a/detox/src/artifacts/utils/ArtifactPathBuilder.js b/detox/src/artifacts/utils/ArtifactPathBuilder.js
index 3da60a5b3..69688092c 100644
--- a/detox/src/artifacts/utils/ArtifactPathBuilder.js
+++ b/detox/src/artifacts/utils/ArtifactPathBuilder.js
@@ -2,8 +2,8 @@ const path = require('path');
 const constructSafeFilename = require('../../utils/constructSafeFilename');
 
 class ArtifactPathBuilder {
-  constructor({ rootDir }) {
-    this._rootDir = rootDir;
+  constructor({ artifactsRootDir }) {
+    this._rootDir = artifactsRootDir;
   }
 
   get rootDir() {
diff --git a/detox/src/artifacts/utils/buildDefaultArtifactsRootDirpath.js b/detox/src/artifacts/utils/buildDefaultArtifactsRootDirpath.js
index 72b68f8d8..709fc6ef6 100644
--- a/detox/src/artifacts/utils/buildDefaultArtifactsRootDirpath.js
+++ b/detox/src/artifacts/utils/buildDefaultArtifactsRootDirpath.js
@@ -1,14 +1,13 @@
 const path = require('path');
 const getTimeStampString = require('./getTimeStampString');
 
-function buildDefaultRootForArtifactsRootDirpath(configuration, rootDir) {
-  if (rootDir.endsWith('/') || rootDir.endsWith('\\')) {
-    return rootDir;
+function buildDefaultRootForArtifactsRootDirpath(configuration, artifactsLocation) {
+  if (artifactsLocation.endsWith('/') || artifactsLocation.endsWith('\\')) {
+    return artifactsLocation;
   }
 
-  const seed = Number(process.env.DETOX_START_TIMESTAMP || String(Date.now()));
-  const subdir = `${configuration}.${getTimeStampString(new Date(seed))}`;
-  return path.join(rootDir, subdir);
+  const subdir = `${configuration}.${getTimeStampString()}`;
+  return path.join(artifactsLocation, subdir);
 }
 
 module.exports = buildDefaultRootForArtifactsRootDirpath;
diff --git a/detox/src/artifacts/video/VideoArtifactPlugin.js b/detox/src/artifacts/video/VideoArtifactPlugin.js
index c569c90e9..8f4be5bcb 100644
--- a/detox/src/artifacts/video/VideoArtifactPlugin.js
+++ b/detox/src/artifacts/video/VideoArtifactPlugin.js
@@ -1,34 +1,19 @@
+const argparse = require('../../utils/argparse');
 const WholeTestRecorderPlugin = require('../templates/plugin/WholeTestRecorderPlugin');
 
 class VideoArtifactPlugin extends WholeTestRecorderPlugin {
-  constructor({ api }) {
-    super({ api });
+  constructor(config) {
+    super(config);
+
+    const recordVideos = argparse.getArgValue('record-videos');
+
+    this.enabled = recordVideos && recordVideos !== 'none';
+    this.keepOnlyFailedTestsArtifacts = recordVideos === 'failing';
   }
 
   async preparePathForTestArtifact(testSummary) {
     return this.api.preparePathForArtifact('test.mp4', testSummary);
   }
-
-  static parseConfig(config) {
-    switch (config) {
-      case 'failing':
-        return {
-          enabled: true,
-          keepOnlyFailedTestsArtifacts: true,
-        };
-      case 'all':
-        return {
-          enabled: true,
-          keepOnlyFailedTestsArtifacts: false,
-        };
-      case 'none':
-      default:
-        return {
-          enabled: false,
-          keepOnlyFailedTestsArtifacts: false,
-        };
-    }
-  }
 }
 
 module.exports = VideoArtifactPlugin;
diff --git a/detox/src/configuration.js b/detox/src/configuration.js
index f657cf3ca..00647a0c3 100644
--- a/detox/src/configuration.js
+++ b/detox/src/configuration.js
@@ -1,15 +1,6 @@
-const _ = require('lodash');
-const path = require('path');
 const DetoxConfigError = require('./errors/DetoxConfigError');
 const uuid = require('./utils/uuid');
-const argparse = require('./utils/argparse');
 const getPort = require('get-port');
-const buildDefaultArtifactsRootDirpath = require('./artifacts/utils/buildDefaultArtifactsRootDirpath');
-
-const SimulatorInstrumentsPlugin = require('./artifacts/instruments/SimulatorInstrumentsPlugin');
-const LogArtifactPlugin = require('./artifacts/log/LogArtifactPlugin');
-const ScreenshotArtifactPlugin = require('./artifacts/screenshot/ScreenshotArtifactPlugin');
-const VideoArtifactPlugin = require('./artifacts/video/VideoArtifactPlugin');
 
 async function defaultSession() {
   return {
@@ -44,106 +35,10 @@ function throwOnEmptyBinaryPath() {
   throw new DetoxConfigError(`'binaryPath' property is missing, should hold the app binary path`);
 }
 
-function composeDeviceConfig({ configurations }) {
-  const configurationName = argparse.getArgValue('configuration');
-  const deviceOverride = argparse.getArgValue('device-name');
-
-  const deviceConfig = (!configurationName && _.size(configurations) === 1)
-    ? _.values(configurations)[0]
-    : configurations[configurationName];
-
-  if (!deviceConfig) {
-    throw new Error(`Cannot determine which configuration to use. use --configuration to choose one of the following:
-                        ${Object.keys(configurations)}`);
-  }
-
-  if (!deviceConfig.type) {
-    throwOnEmptyType();
-  }
-
-  deviceConfig.device = deviceOverride || deviceConfig.device || deviceConfig.name;
-  delete deviceConfig.name;
-
-  if (_.isEmpty(deviceConfig.device)) {
-    throwOnEmptyDevice();
-  }
-
-  return deviceConfig;
-}
-
-function getArtifactsCliConfig() {
-  return {
-    artifactsLocation: argparse.getArgValue('artifacts-location'),
-    recordLogs: argparse.getArgValue('record-logs'),
-    takeScreenshots: argparse.getArgValue('take-screenshots'),
-    recordVideos: argparse.getArgValue('record-videos'),
-    recordPerformance: argparse.getArgValue('record-performance'),
-  };
-}
-
-function resolveModuleFromPath(modulePath) {
-  return path.isAbsolute(modulePath)
-    ? require(modulePath)
-    : require(path.join(process.cwd(), modulePath));
-}
-
-function composeArtifactsConfig({
-  configurationName,
-  deviceConfig,
-  detoxConfig,
-  cliConfig = getArtifactsCliConfig()
-}) {
-  const artifactsConfig = _.defaultsDeep(
-      {
-        rootDir: cliConfig.artifactsLocation,
-        plugins: {
-          log: cliConfig.recordLogs,
-          screenshot: cliConfig.takeScreenshots,
-          video: cliConfig.recordVideos,
-          instruments: cliConfig.recordPerformance,
-        },
-      },
-      deviceConfig.artifacts,
-      detoxConfig.artifacts,
-      {
-        rootDir: 'artifacts',
-        pathBuilder: null,
-        plugins: {
-          log: 'none',
-          screenshot: 'manual',
-          video: 'none',
-          instruments: 'none',
-        },
-      }
-  );
-
-  artifactsConfig.rootDir = buildDefaultArtifactsRootDirpath(
-    configurationName,
-    artifactsConfig.rootDir
-  );
-
-  if (typeof artifactsConfig.pathBuilder === 'string') {
-    artifactsConfig.pathBuilder = resolveModuleFromPath(artifactsConfig.pathBuilder);
-  }
-
-  artifactsConfig.plugins = _.mapValues(artifactsConfig.plugins, (value, key) => {
-    switch (key) {
-      case 'instruments': return SimulatorInstrumentsPlugin.parseConfig(value);
-      case 'log': return LogArtifactPlugin.parseConfig(value);
-      case 'screenshot': return ScreenshotArtifactPlugin.parseConfig(value);
-      case 'video': return VideoArtifactPlugin.parseConfig(value);
-    }
-  });
-
-  return artifactsConfig;
-}
-
 module.exports = {
   defaultSession,
   validateSession,
   throwOnEmptyDevice,
   throwOnEmptyType,
-  throwOnEmptyBinaryPath,
-  composeDeviceConfig,
-  composeArtifactsConfig,
+  throwOnEmptyBinaryPath
 };
diff --git a/detox/src/configurations.mock.js b/detox/src/configurations.mock.js
index e3e624924..ee9891e3b 100644
--- a/detox/src/configurations.mock.js
+++ b/detox/src/configurations.mock.js
@@ -1,50 +1,3 @@
-const SimulatorInstrumentsPlugin = require('./artifacts/instruments/SimulatorInstrumentsPlugin');
-const LogArtifactPlugin = require('./artifacts/log/LogArtifactPlugin');
-const ScreenshotArtifactPlugin = require('./artifacts/screenshot/ScreenshotArtifactPlugin');
-const VideoArtifactPlugin = require('./artifacts/video/VideoArtifactPlugin');
-
-const defaultArtifactsConfiguration = {
-  rootDir: 'artifacts',
-  pathBuilder: null,
-  plugins: {
-    log: 'none',
-    screenshot: 'manual',
-    video: 'none',
-    instruments: 'none',
-  },
-};
-
-const allArtifactsConfiguration = {
-  rootDir: 'artifacts',
-  pathBuilder: null,
-  plugins: {
-    log: 'all',
-    screenshot: 'all',
-    video: 'all',
-    instruments: 'all',
-  },
-};
-
-const pluginsDefaultsResolved = {
-  log: LogArtifactPlugin.parseConfig('none'),
-  screenshot: ScreenshotArtifactPlugin.parseConfig('manual'),
-  video: VideoArtifactPlugin.parseConfig('none'),
-  instruments: SimulatorInstrumentsPlugin.parseConfig('none'),
-};
-
-const pluginsFailingResolved = {
-  log: LogArtifactPlugin.parseConfig('failing'),
-  screenshot: ScreenshotArtifactPlugin.parseConfig('failing'),
-  video: VideoArtifactPlugin.parseConfig('failing'),
-};
-
-const pluginsAllResolved = {
-  log: LogArtifactPlugin.parseConfig('all'),
-  screenshot: ScreenshotArtifactPlugin.parseConfig('all'),
-  video: VideoArtifactPlugin.parseConfig('all'),
-  instruments: SimulatorInstrumentsPlugin.parseConfig('all'),
-};
-
 const validOneDeviceNoSession = {
   "configurations": {
     "ios.sim.release": {
@@ -244,11 +197,6 @@ const deviceObjectEmulator = {
 };
 
 module.exports = {
-  allArtifactsConfiguration,
-  defaultArtifactsConfiguration,
-  pluginsAllResolved,
-  pluginsDefaultsResolved,
-  pluginsFailingResolved,
   validOneDeviceNoSession,
   validOneIosNoneDeviceNoSession,
   validTwoDevicesNoSession,
diff --git a/detox/src/devices/android/AAPT.js b/detox/src/devices/android/AAPT.js
index 814765e06..8d90e511c 100644
--- a/detox/src/devices/android/AAPT.js
+++ b/detox/src/devices/android/AAPT.js
@@ -12,7 +12,7 @@ class AAPT {
   }
 
   async _prepare() {
-    this.aaptBin = this.aaptBin || `"${escape(await getAaptPath())}"`;
+    this.aaptBin = this.aaptBin || await getAaptPath();
   }
 
   async getPackageName(apkPath) {
diff --git a/detox/src/devices/android/ADB.js b/detox/src/devices/android/ADB.js
index 6429ec93a..be9be26ff 100644
--- a/detox/src/devices/android/ADB.js
+++ b/detox/src/devices/android/ADB.js
@@ -258,7 +258,7 @@ class ADB {
 
   async adbCmd(deviceId, params, options) {
     const serial = `${deviceId ? `-s ${deviceId}` : ''}`;
-    const cmd = `"${this.adbBin}" ${serial} ${params}`;
+    const cmd = `${this.adbBin} ${serial} ${params}`;
     const retries = _.get(options, 'retries', 1);
     _.unset(options, 'retries');
 
diff --git a/detox/src/devices/android/Emulator.js b/detox/src/devices/android/Emulator.js
index 1efba3edc..895f2c9f4 100644
--- a/detox/src/devices/android/Emulator.js
+++ b/detox/src/devices/android/Emulator.js
@@ -20,7 +20,7 @@ class Emulator {
   }
 
   async exec(cmd) {
-    return (await exec(`"${this.emulatorBin}" ${cmd}`)).stdout;
+    return (await exec(`${this.emulatorBin} ${cmd}`)).stdout;
   }
 
   async boot(emulatorName, options = {port: undefined}) {
diff --git a/detox/src/utils/appendFile.js b/detox/src/utils/appendFile.js
deleted file mode 100644
index 1519f4e68..000000000
--- a/detox/src/utils/appendFile.js
+++ /dev/null
@@ -1,17 +0,0 @@
-const fs = require('fs');
-
-async function appendFile(src, dest) {
-  const writeStream = fs.createWriteStream(dest, {flags: 'a'});
-  const readStream = fs.createReadStream(src);
-
-  const promise = new Promise((resolve, reject) => {
-    readStream.on('error', e => reject(e));
-    writeStream.on('error', /* istanbul ignore next */ e => reject(e));
-    writeStream.on('close', () => resolve());
-  });
-
-  readStream.pipe(writeStream);
-  return promise;
-}
-
-module.exports = appendFile;
diff --git a/detox/src/utils/logger.js b/detox/src/utils/logger.js
index 7929d1807..c352a6197 100644
--- a/detox/src/utils/logger.js
+++ b/detox/src/utils/logger.js
@@ -3,7 +3,6 @@ const path = require('path');
 const bunyan = require('bunyan');
 const bunyanDebugStream = require('bunyan-debug-stream');
 const argparse = require('./argparse');
-const temporaryPath = require('../artifacts/utils/temporaryPath');
 
 function adaptLogLevelName(level) {
   switch (level) {
@@ -62,26 +61,26 @@ function createPlainBunyanStream({ logPath, level }) {
 function init() {
   const levelFromArg = argparse.getArgValue('loglevel');
   const level = adaptLogLevelName(levelFromArg);
+  const logBaseFilename = path.join(argparse.getArgValue('artifacts-location') || '', `detox_pid_${process.pid}`);
+  const shouldRecordLogs = typeof DETOX_CLI === 'undefined' && ['failing', 'all'].indexOf(argparse.getArgValue('record-logs')) >= 0;
+
   const bunyanStreams = [createPlainBunyanStream({ level })];
+  if (shouldRecordLogs) {
+    const jsonFileStreamPath = logBaseFilename + '.json.log';
+    const plainFileStreamPath = logBaseFilename + '.log';
+
+    fs.ensureFileSync(jsonFileStreamPath);
+    fs.ensureFileSync(plainFileStreamPath);
+
+    bunyanStreams.push({
+      level,
+      path: jsonFileStreamPath,
+    });
 
-  let jsonFileStreamPath, plainFileStreamPath;
-  if (!global.DETOX_CLI && !global.IS_RUNNING_DETOX_UNIT_TESTS) {
-    {
-      jsonFileStreamPath = temporaryPath.for.log();
-      fs.ensureFileSync(jsonFileStreamPath);
-      bunyanStreams.push({
-        level,
-        path: jsonFileStreamPath,
-      });
-    }
-    {
-      plainFileStreamPath = temporaryPath.for.log();
-      fs.ensureFileSync(plainFileStreamPath);
-      bunyanStreams.push(createPlainBunyanStream({
-        level,
-        logPath: plainFileStreamPath,
-      }));
-    }
+    bunyanStreams.push(createPlainBunyanStream({
+      level,
+      logPath: plainFileStreamPath,
+    }));
   }
 
   const logger = bunyan.createLogger({
@@ -89,14 +88,6 @@ function init() {
     streams: bunyanStreams,
   });
 
-  if (jsonFileStreamPath) {
-    logger.jsonFileStreamPath = jsonFileStreamPath;
-  }
-
-  if (plainFileStreamPath) {
-    logger.plainFileStreamPath = plainFileStreamPath;
-  }
-
   return logger;
 }
 
diff --git a/docs/APIRef.Configuration.md b/docs/APIRef.Configuration.md
index 83b1634a2..b0d2b1aeb 100644
--- a/docs/APIRef.Configuration.md
+++ b/docs/APIRef.Configuration.md
@@ -1,11 +1,11 @@
 # Configuration Options
 
-## Configuring package.json
+## Configuring package.json 
 
 ### Device Configuration
 
 `configurations` holds all the device configurations, if there is only one configuration in `configurations` `detox build` and `detox test` will default to it, to choose a specific configuration use `--configuration` param
-
+	
 |Configuration Params|Details|
 |---|---|
 |`type`| Device type, available options are `ios.simulator`, `ios.none`, `android.emulator`, and `android.attached`. |
@@ -13,7 +13,7 @@
 |`testBinaryPath`| (optional, Android only): relative path to the test app (apk) |
 |`device`| Device query, e.g. `{ "byType": "iPhone 11 Pro" }` for iOS simulator or `{ "avdName": "Nexus_5X_API_29" }` |
 |`build`| **[optional]** Build command (either `xcodebuild`, `react-native run-ios`, etc...), will be later available through detox CLI tool.|
-
+	
 **Example:**
 
 ```js
@@ -52,62 +52,27 @@
     }
   }
 }
-```
-
-### Artifacts Configuration
-
-Detox can control artifacts collection via settings from `package.json`:
-
-```js
-{
-  "detox": {
-    "artifacts": {
-      "rootDir": ".artifacts",
-      "pathBuilder": "config/pathbuilder.js",
-      "plugins": {
-        "instruments": "none",
-        "log": "all",
-        "screenshot": "failing",
-        "video": "none"
-      }
-    },
-    "configurations": {
-      "ios.sim.release": {
-        "binaryPath": "/path/to/app",
-        "device": { /* ... */ },
-        "artifacts": {
-          "rootDir": ".artifacts/ios",
-          "plugins": { "instruments": "all" }
-        }
-      }
-    }
-  }
-}
-```
-
-As can be seen from the example above, in a specific configuration you may override individual properties from the default one.
-
-CLI arguments (e.g., `--artifacts-location`, `--record-logs`) still have the highest priority and override their counterparts from JSON.
-
+```	
+	
 ### Server Configuration
 
 Detox can either initialize a server using a generated configuration, or can be overriden with a manual  configuration:
-
+	
 ```json
-  "detox": {
-    ...
-    "session": {
-    "server": "ws://localhost:8099",
-    "sessionId": "YourProjectSessionId"
-    }
-  }
+	"detox": {
+	  ...
+	  "session": {
+		"server": "ws://localhost:8099",
+		"sessionId": "YourProjectSessionId"
+	  }
+	}
 ```
 
 Session can also be set per configuration:
 
 ```json
   "detox": {
-  ...
+	...
     "configurations": {
       "ios.sim.debug": {
         ...
@@ -118,7 +83,7 @@ Session can also be set per configuration:
       }
     }
   }
-```
+```	
 
 ### Test Runner Configuration
 
@@ -126,11 +91,11 @@ Session can also be set per configuration:
 
 ##### Mocha
 ```json
-  "detox": {
-    ...
-    "test-runner": "mocha",
-    "runner-config": "path/to/mocha.opts"
-  }
+	"detox": {
+	  ...
+	  "test-runner": "mocha",
+	  "runner-config": "path/to/mocha.opts"
+	}
 ```
 
 `mocha.opts` refers to `--opts` in https://mochajs.org/#mochaopts
@@ -138,47 +103,47 @@ Session can also be set per configuration:
 ##### Jest
 
 ```json
-  "detox": {
-    ...
-    "test-runner": "jest"
-    "runner-config": "path/to/config.json"
-  }
+	"detox": {
+	  ...
+	  "test-runner": "jest"
+	  "runner-config": "path/to/config.json"
+	}
 ```
 
 `config.json` refers to `--config` in https://facebook.github.io/jest/docs/en/configuration.html
 
->NOTE: jest tests will run in band, as Detox does not currently supports parallelization.
+>NOTE: jest tests will run in band, as Detox does not currently supports parallelization. 
 
 ## detox-cli
 
 ### Build Configuration
 
-In your detox config (in `package.json`) paste your build command into the configuration's `build` field.
-The build command will be triggered when running `detox build`.
+In your detox config (in `package.json`) paste your build command into the configuration's `build` field. 
+The build command will be triggered when running `detox build`.  
 **This is only a convience method, to help you manage building multiple configurations of your app and couple them to your tests. You can also choose not to use it and provide a compiled `app` by yourself.**
 
 You can choose to build your project in any of these ways...
 
 * If there's only one configuration, you can simply use:
 
-  ```sh
-  detox build
-  ```
+	```sh
+	detox build
+	```
 
 * To choose a specific configuration:
-
-  ```sh
-  detox build --configuration yourConfiguration
-  ```
+	
+	```sh
+	detox build --configuration yourConfiguration
+	```
 * Building with xcodebuild:
 
-  ```sh
-  xcodebuild -project ios/YourProject.xcodeproj -scheme YourProject -sdk iphonesimulator -derivedDataPath ios/build
-  ```
-
+	```sh
+	xcodebuild -project ios/YourProject.xcodeproj -scheme YourProject -sdk iphonesimulator -derivedDataPath ios/build
+	```
+	
 * Building using React Native, this is the least suggested way of running your build, since it also starts a random simulator and installs the app on it.
-
-  ```sh
+	
+  ```sh 
   react-native run-ios
   ```
 
@@ -189,17 +154,17 @@ You can choose to build your project in any of these ways...
 
 * If there's only one configuration, you can simply use:
 
-  ```sh
-  detox test ./e2e
-  ```
+	```sh
+	detox test ./e2e
+	```
 
 where `./e2e` is the path to your Detox tests folder.
 
 * For multiple configurations, choose your configuration by passing `--configuration` param:
-
-  ```sh
-  detox test ./e2e --configuration yourConfiguration
-  ```
+	
+	```sh
+	detox test ./e2e --configuration yourConfiguration
+	```
 
 ### Faster test runs with app reuse
 
diff --git a/docs/Troubleshooting.Synchronization.md b/docs/Troubleshooting.Synchronization.md
index f2436312e..5437e6d8e 100644
--- a/docs/Troubleshooting.Synchronization.md
+++ b/docs/Troubleshooting.Synchronization.md
@@ -20,7 +20,7 @@ When the automatic synchronization mechanism doesn't work, we have 2 potential p
 
 * We are waiting too much - The test will appear to hang and fail with timeout. This happens because Detox thinks an asychronous operations is currently taking place and is waiting for it endlessly.
 
-* We are not waiting enough - The test will appear to fail at some point because an element isn't found according to an expectation or isn't found when attempting to perform an action on it. This happens because Detox didn't take some asynchronous operation into account and isn't waiting until it completes.
+* We are not waiting enough - The test will appear to fail at some point becuase an element isn't found according to an expectation or isn't found when attempting to perform an action on it. This happens because Detox didn't take some asynchronous operation into account and isn't waiting until it completes.
 
 ### Switching to manual synchronization as a workaround
 
diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json
index 7fceb9ab8..b559125a5 100644
--- a/examples/demo-native-android/package.json
+++ b/examples/demo-native-android/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-demo-native-android",
-  "version": "14.7.1",
+  "version": "14.6.1",
   "private": true,
   "scripts": {
     "packager": "react-native start",
@@ -8,7 +8,7 @@
     "e2e": "mocha e2e --opts ./e2e/mocha.opts"
   },
   "devDependencies": {
-    "detox": "^14.7.1",
+    "detox": "^14.6.1",
     "mocha": "^6.1.3"
   },
   "detox": {}
diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json
index 14113b5d9..ead1e0be2 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": "14.7.1",
+  "version": "14.6.1",
   "private": true,
   "devDependencies": {
-    "detox": "^14.7.1",
+    "detox": "^14.6.1",
     "mocha": "^6.1.3"
   },
   "detox": {
diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json
index 1fd1b7a4b..6b2627f15 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": "14.7.1",
+  "version": "14.6.1",
   "private": true,
   "scripts": {},
   "devDependencies": {
-    "detox": "^14.7.1",
+    "detox": "^14.6.1",
     "mocha": "6.x.x"
   },
   "detox": {
diff --git a/examples/demo-react-native-jest/package.json b/examples/demo-react-native-jest/package.json
index 8cf263624..b6c077c30 100644
--- a/examples/demo-react-native-jest/package.json
+++ b/examples/demo-react-native-jest/package.json
@@ -1,6 +1,6 @@
 {
   "name": "demo-react-native-jest",
-  "version": "14.7.1",
+  "version": "14.6.1",
   "private": true,
   "scripts": {
     "test:ios-release": "detox test --configuration ios.sim.release -l verbose",
@@ -13,18 +13,14 @@
     "test:jest-circus:android-release-ci": "detox test --configuration android.emu.release -l verbose -o e2e/config-circus.json --workers 2"
   },
   "devDependencies": {
-    "detox": "^14.7.1",
+    "detox": "^14.6.1",
     "jest": "24.8.x",
-    "jest-circus": "24.8.x",
-    "sanitize-filename": "^1.6.1"
+    "jest-circus": "24.8.x"
   },
   "detox": {
     "test-runner": "jest",
     "configurations": {
       "ios.sim.release": {
-        "artifacts": {
-          "pathBuilder": "e2e/detox.pathbuilder.ios.js"
-        },
         "binaryPath": "../demo-react-native/ios/build/Build/Products/Release-iphonesimulator/example.app",
         "type": "ios.simulator",
         "device": {
@@ -32,9 +28,6 @@
         }
       },
       "android.emu.release": {
-        "artifacts": {
-          "pathBuilder": "e2e/detox.pathbuilder.android.js"
-        },
         "binaryPath": "../demo-react-native/android/app/build/outputs/apk/release/app-release.apk",
         "type": "android.emulator",
         "device": {
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index 3bace362a..b97822bdf 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -1,6 +1,6 @@
 {
   "name": "example",
-  "version": "14.7.1",
+  "version": "14.6.1",
   "private": true,
   "scripts": {
     "start": "react-native start",
@@ -21,7 +21,7 @@
     "react-native": "0.60.x"
   },
   "devDependencies": {
-    "detox": "^14.7.1",
+    "detox": "^14.6.1",
     "mocha": "^6.1.3"
   },
   "detox": {
diff --git a/lerna.json b/lerna.json
index fe5acb258..0754ad9b0 100644
--- a/lerna.json
+++ b/lerna.json
@@ -11,7 +11,7 @@
     "examples/demo-react-native-jest",
     "generation"
   ],
-  "version": "14.7.1",
+  "version": "14.6.1",
   "npmClient": "npm",
   "command": {
     "publish": {


Test patch:
diff --git a/detox/__tests__/setupJest.js b/detox/__tests__/setupJest.js
index ec6a26419..b64b7a83a 100644
--- a/detox/__tests__/setupJest.js
+++ b/detox/__tests__/setupJest.js
@@ -28,4 +28,3 @@ function mockPackageJson(mockContent) {
 
 global.mockPackageJson = mockPackageJson;
 global.callCli = callCli;
-global.IS_RUNNING_DETOX_UNIT_TESTS = true;
diff --git a/detox/android/detox/src/main/java/com/wix/detox/TestEngineFacade.kt b/detox/android/detox/src/main/java/com/wix/detox/TestEngineFacade.kt
index c14c34679..02ad4dedb 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/TestEngineFacade.kt
+++ b/detox/android/detox/src/main/java/com/wix/detox/TestEngineFacade.kt
@@ -5,17 +5,12 @@ import androidx.test.espresso.Espresso
 import androidx.test.espresso.IdlingResource
 import com.wix.detox.espresso.EspressoDetox
 import com.wix.detox.espresso.UiAutomatorHelper
+import com.wix.detox.reactnative.ReactNativeExtension
 
 class TestEngineFacade {
     fun awaitIdle() = Espresso.onIdle()
     fun syncIdle() = UiAutomatorHelper.espressoSync() // TODO Check whether this can be replaced with #awaitIdle()
     fun getBusyIdlingResources() = EspressoDetox.getBusyEspressoResources() as List<IdlingResource>
-    fun reloadReactNative(rnContext: Context) = ReactNativeSupport.reloadApp(rnContext)
-    fun softResetReactNative() {
-        ReactNativeSupport.currentReactContext = null
-    }
-    fun hardResetReactNative(context: Context) {
-        softResetReactNative()
-        ReactNativeSupport.removeEspressoIdlingResources(context)
-    }
+    fun reloadReactNative(appContext: Context) = ReactNativeExtension.reloadReactNative(appContext)
+    fun resetReactNative() = ReactNativeExtension.clearAllSynchronization()
 }
diff --git a/detox/android/detox/src/test/java/com/wix/detox/DetoxActionHandlersSpec.kt b/detox/android/detox/src/test/java/com/wix/detox/DetoxActionHandlersSpec.kt
index f20998533..a96d190ec 100644
--- a/detox/android/detox/src/test/java/com/wix/detox/DetoxActionHandlersSpec.kt
+++ b/detox/android/detox/src/test/java/com/wix/detox/DetoxActionHandlersSpec.kt
@@ -1,6 +1,6 @@
 package com.wix.detox
 
-import com.facebook.react.bridge.ReactContext
+import android.content.Context
 import com.nhaarman.mockitokotlin2.*
 import com.wix.detox.UTHelpers.yieldToOtherThreads
 import com.wix.invoke.MethodInvocation
@@ -15,12 +15,12 @@ object DetoxActionHandlersSpec: Spek({
         val params = "{\"mock\": \"params\"}"
         val messageId = 666L
 
-        lateinit var rnContext: ReactContext
+        lateinit var appContext: Context
         lateinit var wsClient: WebSocketClient
         lateinit var testEngineFacade: TestEngineFacade
 
         beforeEachTest {
-            rnContext = mock()
+            appContext = mock()
             wsClient = mock()
 
             testEngineFacade = mock()
@@ -57,11 +57,11 @@ object DetoxActionHandlersSpec: Spek({
         }
 
         describe("React-native reload action") {
-            fun uut() = ReactNativeReloadActionHandler(rnContext, wsClient, testEngineFacade)
+            fun uut() = ReactNativeReloadActionHandler(appContext, wsClient, testEngineFacade)
 
             it("should reload the app") {
                 uut().handle(params, messageId)
-                verify(testEngineFacade).reloadReactNative(rnContext)
+                verify(testEngineFacade).reloadReactNative(appContext)
             }
 
             it("should reply with a 'ready' ACK when ready") {
@@ -82,7 +82,7 @@ object DetoxActionHandlersSpec: Spek({
                     verify(wsClient, never()).sendAction(any(), any(), any())
                 }
                 yieldToOtherThreads(executor)
-                verify(testEngineFacade, times(1)).reloadReactNative(any())
+                verify(testEngineFacade, times(1)).reloadReactNative(eq(appContext))
                 verify(wsClient, times(1)).sendAction(any(), any(), any())
             }
         }
diff --git a/detox/local-cli/test.js b/detox/local-cli/test.js
index ed4057b8e..67daea6b3 100644
--- a/detox/local-cli/test.js
+++ b/detox/local-cli/test.js
@@ -3,6 +3,7 @@ const path = require('path');
 const cp = require('child_process');
 const fs = require('fs-extra');
 const environment = require('../src/utils/environment');
+const buildDefaultArtifactsRootDirpath = require('../src/artifacts/utils/buildDefaultArtifactsRootDirpath');
 const DetoxConfigError = require('../src/errors/DetoxConfigError');
 
 const log = require('../src/utils/logger').child({ __filename });
@@ -80,28 +81,33 @@ module.exports.builder = {
   a: {
     alias: 'artifacts-location',
     group: 'Debugging:',
-    describe: 'Artifacts (logs, screenshots, etc) root directory.'
+    describe: 'Artifacts (logs, screenshots, etc) root directory.',
+    default: 'artifacts'
   },
   'record-logs': {
     group: 'Debugging:',
     choices: ['failing', 'all', 'none'],
+    default: 'none',
     describe: 'Save logs during each test to artifacts directory. Pass "failing" to save logs of failing tests only.'
   },
   'take-screenshots': {
     group: 'Debugging:',
     choices: ['manual', 'failing', 'all', 'none'],
+    default: 'manual',
     describe:
       'Save screenshots before and after each test to artifacts directory. Pass "failing" to save screenshots of failing tests only.'
   },
   'record-videos': {
     group: 'Debugging:',
     choices: ['failing', 'all', 'none'],
+    default: 'none',
     describe:
       'Save screen recordings of each test to artifacts directory. Pass "failing" to save recordings of failing tests only.'
   },
   'record-performance': {
     group: 'Debugging:',
     choices: ['all', 'none'],
+    default: 'none',
     describe:
       '[iOS Only] Save Detox Instruments performance recordings of each test to artifacts directory.'
   },
@@ -144,6 +150,8 @@ module.exports.builder = {
 const collectExtraArgs = require('./utils/collectExtraArgs')(module.exports.builder);
 
 module.exports.handler = async function test(program) {
+  program.artifactsLocation = buildDefaultArtifactsRootDirpath(program.configuration, program.artifactsLocation);
+
   const config = getDetoxSection();
 
   if (!program.file && config.file) {
@@ -205,19 +213,6 @@ module.exports.handler = async function test(program) {
     return args.concat(fallbackTestFolder);
   }
 
-  function safeGuardArguments(args) {
-    if (_.last(args).includes(' ')) {
-      return args;
-    }
-
-    const safeArg = _.findLast(args, a => a.includes(' '));
-    if (!safeArg) {
-      return args;
-    }
-
-    return [..._.pull(args, safeArg), safeArg]
-  }
-
   function runMocha() {
     if (program.workers !== '1') {
       log.warn('Can not use -w, --workers. Parallel test execution is only supported with iOS and Jest');
@@ -225,24 +220,22 @@ module.exports.handler = async function test(program) {
 
     const command = _.compact([
       (path.join('node_modules', '.bin', runner)),
-      ...safeGuardArguments([
-        (runnerConfig ? `--opts ${runnerConfig}` : ''),
-        (program.configuration ? `--configuration ${program.configuration}` : ''),
-        (program.loglevel ? `--loglevel ${program.loglevel}` : ''),
-        (program.noColor ? '--no-colors' : ''),
-        (program.cleanup ? `--cleanup` : ''),
-        (program.reuse ? `--reuse` : ''),
-        (isFinite(program.debugSynchronization) ? `--debug-synchronization ${program.debugSynchronization}` : ''),
-        (platform ? `--grep ${getPlatformSpecificString()} --invert` : ''),
-        (program.headless ? `--headless` : ''),
-        (program.gpu ? `--gpu ${program.gpu}` : ''),
-        (hasCustomValue('record-logs') ? `--record-logs ${program.recordLogs}` : ''),
-        (hasCustomValue('take-screenshots') ? `--take-screenshots ${program.takeScreenshots}` : ''),
-        (hasCustomValue('record-videos') ? `--record-videos ${program.recordVideos}` : ''),
-        (hasCustomValue('record-performance') ? `--record-performance ${program.recordPerformance}` : ''),
-        (program.artifactsLocation ? `--artifacts-location "${program.artifactsLocation}"` : ''),
-        (program.deviceName ? `--device-name "${program.deviceName}"` : ''),
-      ]),
+      (runnerConfig ? `--opts ${runnerConfig}` : ''),
+      (program.configuration ? `--configuration ${program.configuration}` : ''),
+      (program.loglevel ? `--loglevel ${program.loglevel}` : ''),
+      (program.noColor ? '--no-colors' : ''),
+      (program.cleanup ? `--cleanup` : ''),
+      (program.reuse ? `--reuse` : ''),
+      (isFinite(program.debugSynchronization) ? `--debug-synchronization ${program.debugSynchronization}` : ''),
+      (platform ? `--grep ${getPlatformSpecificString()} --invert` : ''),
+      (program.headless ? `--headless` : ''),
+      (program.gpu ? `--gpu ${program.gpu}` : ''),
+      (hasCustomValue('record-logs') ? `--record-logs ${program.recordLogs}` : ''),
+      (hasCustomValue('take-screenshots') ? `--take-screenshots ${program.takeScreenshots}` : ''),
+      (hasCustomValue('record-videos') ? `--record-videos ${program.recordVideos}` : ''),
+      (hasCustomValue('record-performance') ? `--record-performance ${program.recordPerformance}` : ''),
+      (program.artifactsLocation ? `--artifacts-location "${program.artifactsLocation}"` : ''),
+      (program.deviceName ? `--device-name "${program.deviceName}"` : ''),
       ...getPassthroughArguments(),
     ]).join(' ');
 
@@ -274,36 +267,31 @@ module.exports.handler = async function test(program) {
 
     const command = _.compact([
       path.join('node_modules', '.bin', runner),
-      ...safeGuardArguments([
-        (program.noColor ? ' --no-color' : ''),
-        (runnerConfig ? `--config ${runnerConfig}` : ''),
-        (platform ? shellQuote(`--testNamePattern=^((?!${getPlatformSpecificString()}).)*$`) : ''),
-        `--maxWorkers ${program.workers}`,
-      ]),
+      (runnerConfig ? `--config=${runnerConfig}` : ''),
+      (program.noColor ? ' --no-color' : ''),
+      `--maxWorkers=${program.workers}`,
+      (platform ? shellQuote(`--testNamePattern=^((?!${getPlatformSpecificString()}).)*$`) : ''),
       ...getPassthroughArguments(),
     ]).join(' ');
 
-    const detoxEnvironmentVariables = {
-      ..._.pick(program, [
-        'configuration',
-        'loglevel',
-        'cleanup',
-        'reuse',
-        'debugSynchronization',
-        'gpu',
-        'headless',
-        'artifactsLocation',
-        'recordLogs',
-        'takeScreenshots',
-        'recordVideos',
-        'recordPerformance',
-        'deviceName',
-        'reportSpecs',
-        'readOnlyEmu',
-        'deviceLaunchArgs',
-      ]),
-      DETOX_START_TIMESTAMP: Date.now(),
-    };
+    const detoxEnvironmentVariables = _.pick(program, [
+      'configuration',
+      'loglevel',
+      'cleanup',
+      'reuse',
+      'debugSynchronization',
+      'gpu',
+      'headless',
+      'artifactsLocation',
+      'recordLogs',
+      'takeScreenshots',
+      'recordVideos',
+      'recordPerformance',
+      'deviceName',
+      'reportSpecs',
+      'readOnlyEmu',
+      'deviceLaunchArgs',
+    ]);
 
     launchTestRunner(command, detoxEnvironmentVariables);
   }
diff --git a/detox/local-cli/test.test.js b/detox/local-cli/test.test.js
index 187dbfe1e..9b4fe0d88 100644
--- a/detox/local-cli/test.test.js
+++ b/detox/local-cli/test.test.js
@@ -45,7 +45,7 @@ describe('test', () => {
       await callCli('./test', 'test');
 
       expect(mockExec).toHaveBeenCalledWith(
-        expect.stringContaining(`${normalize('node_modules/.bin/mocha')} --opts e2e/mocha.opts --configuration only --grep :ios: --invert`),
+        expect.stringContaining(`${normalize('node_modules/.bin/mocha')} --opts e2e/mocha.opts --configuration only --grep :ios: --invert --artifacts-location "${normalize('artifacts/only.')}`),
         expect.anything()
       );
 
@@ -90,11 +90,15 @@ describe('test', () => {
 
       expect(mockExec).toHaveBeenCalledWith(
         expect.stringContaining(
-          `${normalize('node_modules/.bin/jest')} --config e2e/config.json ${shellQuote('--testNamePattern=^((?!:ios:).)*$')} --maxWorkers 1 "e2e"`
+          `${normalize('node_modules/.bin/jest')} --config=e2e/config.json --maxWorkers=1 ${shellQuote('--testNamePattern=^((?!:ios:).)*$')} "e2e"`
         ),
         expect.objectContaining({
           env: expect.objectContaining({
             configuration: 'only',
+            recordLogs: 'none',
+            takeScreenshots: 'manual',
+            recordVideos: 'none',
+            artifactsLocation: expect.stringContaining(normalize('artifacts/only.')),
           }),
         })
       );
@@ -133,7 +137,7 @@ describe('test', () => {
     );
 
     const expectWorkersArg = ({value}) => expect(mockExec).toHaveBeenCalledWith(
-      expect.stringContaining(`--maxWorkers ${value}`),
+      expect.stringContaining(`--maxWorkers=${value}`),
       expect.anything(),
     );
 
@@ -248,7 +252,7 @@ describe('test', () => {
     }
     expect(mockExec).toHaveBeenCalledWith(
       expect.stringContaining(
-        `${normalize('node_modules/.bin/mocha')} --opts e2e/mocha.opts --configuration only --debug-synchronization 3000 --grep :ios: --invert`
+        `${normalize('node_modules/.bin/mocha')} --opts e2e/mocha.opts --configuration only --debug-synchronization 3000 --grep :ios: --invert --artifacts-location "${normalize('artifacts/only.')}`
       ),
       expect.anything()
     );
diff --git a/detox/src/artifacts/ArtifactsManager.test.js b/detox/src/artifacts/ArtifactsManager.test.js
index 996bf9d8f..1fab21adc 100644
--- a/detox/src/artifacts/ArtifactsManager.test.js
+++ b/detox/src/artifacts/ArtifactsManager.test.js
@@ -3,17 +3,14 @@ const sleep = require('../utils/sleep');
 const testSummaries = require('./templates/plugin/__mocks__/testSummaries.mock');
 
 describe('ArtifactsManager', () => {
-  let proxy, FakePathBuilder;
+  let proxy;
 
   beforeEach(() => {
     jest.mock('fs-extra');
-    jest.mock('./__mocks__/FakePathBuilder');
     jest.mock('./utils/ArtifactPathBuilder');
     jest.mock('../utils/argparse');
     jest.mock('../utils/logger');
 
-    FakePathBuilder = require('./__mocks__/FakePathBuilder');
-
     proxy = {
       get ArtifactPathBuilder() {
         return require('./utils/ArtifactPathBuilder');
@@ -37,37 +34,22 @@ describe('ArtifactsManager', () => {
     let artifactsManager;
 
     beforeEach(() => {
-      artifactsManager = new proxy.ArtifactsManager({
-        rootDir: '/tmp',
-        plugins: {},
+      proxy.argparse.getArgValue.mockImplementation((key) => {
+        return (key === 'artifacts-location') ? '/tmp' : '';
       });
+
+      artifactsManager = new proxy.ArtifactsManager();
     });
 
     it('should provide artifacts location to path builder', async () => {
       expect(proxy.ArtifactPathBuilder).toHaveBeenCalledWith({
-        rootDir: '/tmp',
+        artifactsRootDir: '/tmp',
       });
     });
 
     it('should correctly terminate itself (without errors)', async () => {
       await artifactsManager.onTerminate();
     });
-
-    describe('with { pathBuilder } instance', () => {
-      it('should require that module as pathBuilder', () => {
-        const pathBuilder = new FakePathBuilder();
-        artifactsManager = new proxy.ArtifactsManager({ pathBuilder });
-        expect(artifactsManager._pathBuilder).toBe(pathBuilder);
-      });
-    })
-
-    describe('with { pathBuilder } function', () => {
-      it('should require that module as pathBuilder', () => {
-        const pathBuilder = jest.fn(() => new FakePathBuilder());
-        artifactsManager = new proxy.ArtifactsManager({ pathBuilder, rootDir: '/tmp/42' });
-        expect(pathBuilder).toHaveBeenCalledWith({ rootDir: '/tmp/42' });
-      });
-    })
   });
 
   describe('when plugin factory is registered', () => {
@@ -78,18 +60,12 @@ describe('ArtifactsManager', () => {
         onBeforeLaunchApp: jest.fn(),
       });
 
-      artifactsManager = new proxy.ArtifactsManager({
-        rootDir: '/tmp',
-        plugins: {
-          mock: { setting: 'value' },
-        },
-      });
+      artifactsManager = new proxy.ArtifactsManager();
       artifactsManager.registerArtifactPlugins({ mock: factory });
     });
 
     it('should get called immediately', () => {
       expect(factory).toHaveBeenCalledWith(expect.objectContaining({
-        userConfig: { setting: 'value' },
         preparePathForArtifact: expect.any(Function),
         trackArtifact: expect.any(Function),
         untrackArtifact: expect.any(Function),
@@ -110,7 +86,6 @@ describe('ArtifactsManager', () => {
 
         return (testPlugin = {
           name: 'testPlugin',
-          userConfig: api.userConfig,
           disable: jest.fn(),
           onBootDevice: jest.fn(),
           onBeforeShutdownDevice: jest.fn(),
@@ -128,22 +103,12 @@ describe('ArtifactsManager', () => {
         });
       };
 
-      pathBuilder = new FakePathBuilder();
-      artifactsManager = new proxy.ArtifactsManager({
-        pathBuilder,
-        plugins: {
-          testPlugin: {
-            lifecycle: 'all',
-          }
-        }
-      });
-      artifactsManager.registerArtifactPlugins({ testPlugin: testPluginFactory });
-    });
+      pathBuilder = {
+        buildPathForTestArtifact: jest.fn(),
+      };
 
-    describe('.userConfig', () => {
-      it('should contain plugin config', async () => {
-        expect(artifactsApi.userConfig).toEqual({ lifecycle: 'all' });
-      });
+      artifactsManager = new proxy.ArtifactsManager(pathBuilder);
+      artifactsManager.registerArtifactPlugins({ testPlugin: testPluginFactory });
     });
 
     describe('.preparePathForArtifact()', () => {
diff --git a/detox/src/artifacts/instruments/SimulatorInstrumentsPlugin.test.js b/detox/src/artifacts/instruments/SimulatorInstrumentsPlugin.test.js
deleted file mode 100644
index b42164285..000000000
--- a/detox/src/artifacts/instruments/SimulatorInstrumentsPlugin.test.js
+++ /dev/null
@@ -1,20 +0,0 @@
-const SimulatorInstrumentsPlugin = require('./SimulatorInstrumentsPlugin');
-
-describe('SimulatorInstrumentsPlugin', () => {
-    describe('static parseConfig(config)', () => {
-        const parseConfig = SimulatorInstrumentsPlugin.parseConfig;
-
-        const ENABLE_MODES = ['all'].map(x => [x]);
-        const DISABLE_MODES = ['none', 'manual', 'failing', { enabled: true }].map(x => [x]);
-        const INCLUSIVE_MODES = ['all', 'manual', 'none', 'failing', { keepOnlyFailedTestsArtifacts: true }].map(x => [x]);
-
-        it.each(ENABLE_MODES)('should enable plugin if config = %j', (config) =>
-            expect(parseConfig(config).enabled).toBe(true));
-
-        it.each(DISABLE_MODES)('should disable plugin if config = %j', (config) =>
-            expect(parseConfig(config).enabled).toBe(false));
-
-        it.each(INCLUSIVE_MODES)('should save all screenshots if config = %j', (config) =>
-            expect(parseConfig(config).keepOnlyFailedTestsArtifacts).toBe(false));
-    });
-});
diff --git a/detox/src/artifacts/log/LogArtifactPlugin.test.js b/detox/src/artifacts/log/LogArtifactPlugin.test.js
deleted file mode 100644
index 0abf12cff..000000000
--- a/detox/src/artifacts/log/LogArtifactPlugin.test.js
+++ /dev/null
@@ -1,25 +0,0 @@
-const LogArtifactPlugin = require('./LogArtifactPlugin');
-
-describe('LogArtifactPlugin', () => {
-    describe('static parseConfig(config)', () => {
-        const parseConfig = LogArtifactPlugin.parseConfig;
-
-        const ENABLE_MODES = ['all', 'failing'].map(x => [x]);
-        const DISABLE_MODES = ['none', { enabled: true }].map(x => [x]);
-
-        const INCLUSIVE_MODES = ['all', 'manual', 'none', { keepOnlyFailedTestsArtifacts: true }].map(x => [x]);
-        const EXCLUSIVE_MODES = ['failing'].map(x => [x]);
-
-        it.each(ENABLE_MODES)('should enable plugin if config = %j', (config) =>
-            expect(parseConfig(config).enabled).toBe(true));
-
-        it.each(DISABLE_MODES)('should disable plugin if config = %j', (config) =>
-            expect(parseConfig(config).enabled).toBe(false));
-
-        it.each(INCLUSIVE_MODES)('should save all screenshots if config = %j', (config) =>
-            expect(parseConfig(config).keepOnlyFailedTestsArtifacts).toBe(false));
-
-        it.each(EXCLUSIVE_MODES)('should save all screenshots if config = %j', (config) =>
-            expect(parseConfig(config).keepOnlyFailedTestsArtifacts).toBe(true));
-    });
-});
diff --git a/detox/src/artifacts/log/ios/SimulatorLogPlugin.test.js b/detox/src/artifacts/log/ios/SimulatorLogPlugin.test.js
index f2221864e..7ad46591e 100644
--- a/detox/src/artifacts/log/ios/SimulatorLogPlugin.test.js
+++ b/detox/src/artifacts/log/ios/SimulatorLogPlugin.test.js
@@ -65,7 +65,7 @@ describe('SimulatorLogPlugin', () => {
         },
       };
 
-      artifactsManager = new ArtifactsManager({ pathBuilder: fakePathBuilder });
+      artifactsManager = new ArtifactsManager(fakePathBuilder);
       artifactsManager.registerArtifactPlugins({
         log: (api) => new SimulatorLogPlugin({
           api,
diff --git a/detox/src/artifacts/screenshot/ScreenshotArtifactPlugin.test.js b/detox/src/artifacts/screenshot/ScreenshotArtifactPlugin.test.js
deleted file mode 100644
index 8889e6662..000000000
--- a/detox/src/artifacts/screenshot/ScreenshotArtifactPlugin.test.js
+++ /dev/null
@@ -1,35 +0,0 @@
-const ScreenshotArtifactPlugin = require('./ScreenshotArtifactPlugin');
-
-describe('ScreenshotArtifactPlugin', () => {
-  describe('static parseConfig(config)', () => {
-    const parseConfig = ScreenshotArtifactPlugin.parseConfig;
-
-    const ENABLE_MODES = ['all', 'manual', 'failing', { enabled: false }].map(x => [x]);
-    const DISABLE_MODES = ['none'].map(x => [x]);
-
-    const AUTOMATIC_MODES = ['all', 'failing'].map(x => [x]);
-    const MANUAL_MODES = ['manual', 'none', { shouldTakeAutomaticSnapshots: true }].map(x => [x]);
-
-    const INCLUSIVE_MODES = ['all', 'manual', 'none', { keepOnlyFailedTestsArtifacts: true }].map(x => [x]);
-    const EXCLUSIVE_MODES = ['failing'].map(x => [x]);
-
-    it.each(ENABLE_MODES)('should enable plugin if config = %j', (config) =>
-        expect(parseConfig(config).enabled).toBe(true));
-
-    it.each(DISABLE_MODES)('should disable plugin if config = %j', (config) =>
-        expect(parseConfig(config).enabled).toBe(false));
-
-    it.each(AUTOMATIC_MODES)('should take automatic screenshots if config = %j', (config) =>
-        expect(parseConfig(config).shouldTakeAutomaticSnapshots).toBe(true));
-
-    it.each(MANUAL_MODES)('should not take automatic screenshots if config = %j', (config) =>
-        expect(parseConfig(config).shouldTakeAutomaticSnapshots).toBe(false));
-
-    it.each(INCLUSIVE_MODES)('should save all screenshots if config = %j', (config) =>
-        expect(parseConfig(config).keepOnlyFailedTestsArtifacts).toBe(false));
-
-    it.each(EXCLUSIVE_MODES)('should save all screenshots if config = %j', (config) =>
-        expect(parseConfig(config).keepOnlyFailedTestsArtifacts).toBe(true));
-  });
-});
-
diff --git a/detox/src/artifacts/templates/artifact/FileArtifact.test.js b/detox/src/artifacts/templates/artifact/FileArtifact.test.js
index 263adb489..cb4023bc0 100644
--- a/detox/src/artifacts/templates/artifact/FileArtifact.test.js
+++ b/detox/src/artifacts/templates/artifact/FileArtifact.test.js
@@ -5,7 +5,7 @@ const tempfile = require('tempfile');
 describe('FileArtifact', () => {
   let FileArtifact, fileArtifact, logger, temporaryPath, destinationPath;
 
-  beforeEach(async () => {
+  beforeEach(() => {
     jest.mock('../../../utils/logger');
     logger = require('../../../utils/logger');
     FileArtifact = require('./FileArtifact');
@@ -14,44 +14,39 @@ describe('FileArtifact', () => {
     destinationPath = tempfile('.artifact');
   });
 
-  afterEach(async () => {
-    await Promise.all([
-      fs.remove(temporaryPath),
-      fs.remove(destinationPath)
-    ]);
-  });
+  afterEach(async () => await Promise.all([
+    fs.remove(temporaryPath),
+    fs.remove(destinationPath)
+  ]));
 
   describe('save', () => {
-    beforeEach(() => {
-      jest.spyOn(FileArtifact, 'moveTemporaryFile').mockImplementation(_.noop);
-    });
-
-    describe('if temporary file is passed to constructor', () => {
-      beforeEach(() => {
-        fileArtifact = new FileArtifact({
-          name: 'CustomArtifact',
-          temporaryPath,
-        });
+    describe('if temporary file exists', () => {
+      beforeEach(async () => {
+        await fs.ensureFile(temporaryPath);
       });
 
-      describe('when called simply', () => {
-        beforeEach(async () => {
-          await fileArtifact.save(destinationPath);
-        });
+      it('should move file to the specified location and log a message', async () => {
+        fileArtifact = new FileArtifact({ temporaryPath });
+        await fileArtifact.save(destinationPath);
 
-        it('should call FileArtifact.moveTemporaryFile', async () => {
-          expect(FileArtifact.moveTemporaryFile).toHaveBeenCalledWith(logger, temporaryPath, destinationPath, undefined);
-        });
+        expect(await fs.exists(destinationPath)).toBe(true);
+        expect(await fs.exists(temporaryPath)).toBe(false);
+
+        expect(logger.debug).toHaveBeenCalledWith({ event: 'MOVE_FILE' }, expect.any(String));
       });
+    });
 
-      describe('when called with { append: true }', () => {
-        beforeEach(async () => {
-          await fileArtifact.save(destinationPath, {append: true});
-        });
+    describe('if temporary file does not exist', () => {
+      beforeEach(async () => {
+        await fs.remove(temporaryPath);
+      });
 
-        it('should call FileArtifact.moveTemporaryFile with extra param', async () => {
-          expect(FileArtifact.moveTemporaryFile).toHaveBeenCalledWith(logger, temporaryPath, destinationPath, true);
-        });
+      it('should log a warning', async () => {
+        fileArtifact = new FileArtifact({ temporaryPath });
+        await fileArtifact.save(destinationPath);
+
+        expect(await fs.exists(destinationPath)).toBe(false);
+        expect(logger.warn).toHaveBeenCalledWith({ event: 'MOVE_FILE_MISSING' }, expect.any(String));
       });
     });
 
@@ -66,25 +61,26 @@ describe('FileArtifact', () => {
         });
       });
 
-      describe('when save() is called without start()', () => {
-        beforeEach(async () => {
+      describe('and start() was never called', () => {
+        it('should fail to find the file on save()', async () => {
           await fileArtifact.save(destinationPath);
-        });
 
-        it('should not call FileArtifact.moveTemporaryFile', async () => {
-          await fileArtifact.save(destinationPath);
-          expect(FileArtifact.moveTemporaryFile).not.toHaveBeenCalled();
+          expect(await fs.exists(destinationPath)).toBe(false);
+          expect(await fs.exists(temporaryPath)).toBe(false);
         });
       });
 
       describe('and start() was called', () => {
         beforeEach(async () => {
           await fileArtifact.start();
-          await fileArtifact.save(destinationPath);
         });
 
-        it('should call FileArtifact.moveTemporaryFile', async () => {
-          expect(FileArtifact.moveTemporaryFile).toHaveBeenCalledWith(logger, temporaryPath, destinationPath, undefined);
+        it('should move file successfully and log', async () => {
+          await fileArtifact.save(destinationPath);
+
+          expect(await fs.exists(destinationPath)).toBe(true);
+          expect(await fs.exists(temporaryPath)).toBe(false);
+          expect(logger.debug).toHaveBeenCalledWith({ event: 'MOVE_FILE' }, expect.any(String));
         });
       });
     });
@@ -119,68 +115,19 @@ describe('FileArtifact', () => {
 
   describe('static helper methods', () => {
     describe('.moveTemporaryFile', () => {
-      describe('if temporary file does not exist', () => {
-        beforeEach(async () => {
-          await fs.remove(temporaryPath);
-          await fs.writeFile(destinationPath, 'Hello');
-        });
-
-        it('should log a warning', async () => {
-          const result = await FileArtifact.moveTemporaryFile(logger, temporaryPath, destinationPath);
+      it('should move file from source to destination and log that', async () => {
+        await fs.ensureFile(temporaryPath);
+        await FileArtifact.moveTemporaryFile(logger, temporaryPath, destinationPath);
 
-          expect(result).toBe(false);
-          expect(await fs.readFile(destinationPath, 'utf8')).toBe('Hello');
-          expect(logger.warn).toHaveBeenCalledWith({ event: 'MOVE_FILE_MISSING' }, expect.any(String));
-        });
+        expect(await fs.exists(destinationPath)).toBe(true);
+        expect(logger.debug).toHaveBeenCalledWith({ event: 'MOVE_FILE' }, expect.any(String));
       });
 
-      describe('if temporary file exists', () => {
-        beforeEach(async () => {
-          await fs.ensureFile(temporaryPath);
-        });
+      it('should log error if source file does not exist', async () => {
+        await FileArtifact.moveTemporaryFile(logger, temporaryPath, destinationPath);
 
-        describe('and the destination file does not exist', () => {
-          beforeEach(async () => {
-            await fs.remove(destinationPath);
-          });
-
-          it('should move file to the specified location and log a message', async () => {
-            const result = await FileArtifact.moveTemporaryFile(logger, temporaryPath, destinationPath);
-
-            expect(result).toBe(true);
-            expect(await fs.exists(destinationPath)).toBe(true);
-            expect(await fs.exists(temporaryPath)).toBe(false);
-
-            expect(logger.debug).toHaveBeenCalledWith({ event: 'MOVE_FILE' }, expect.any(String));
-          });
-        });
-
-        describe('and the destination file exists', () => {
-          beforeEach(async () => {
-            await fs.writeFile(destinationPath, 'My file');
-            await fs.writeFile(temporaryPath, ' and more to it');
-          });
-
-          it('should remove temporary file but refuse to append file', async () => {
-            const result = await FileArtifact.moveTemporaryFile(logger, temporaryPath, destinationPath);
-
-            expect(result).toBe(false);
-            expect(await fs.exists(temporaryPath)).toBe(false);
-            expect(await fs.readFile(destinationPath, 'utf8')).toBe('My file');
-
-            expect(logger.warn).toHaveBeenCalledWith({ event: 'MOVE_FILE_EXISTS' }, expect.any(String));
-          });
-
-          it('should move temporary file via appending if canAppend = true', async () => {
-            const result = await FileArtifact.moveTemporaryFile(logger, temporaryPath, destinationPath, true);
-
-            expect(result).toBe(true);
-            expect(await fs.exists(temporaryPath)).toBe(false);
-            expect(await fs.readFile(destinationPath, 'utf8')).toBe('My file and more to it');
-
-            expect(logger.debug).toHaveBeenCalledWith({ event: 'MOVE_FILE' }, expect.stringContaining('appending'));
-          });
-        });
+        expect(await fs.exists(destinationPath)).toBe(false);
+        expect(logger.warn).toHaveBeenCalledWith({ event: 'MOVE_FILE_MISSING' }, expect.any(String));
       });
     });
   });
diff --git a/detox/src/artifacts/templates/plugin/ArtifactPlugin.test.js b/detox/src/artifacts/templates/plugin/ArtifactPlugin.test.js
index c78678492..59ccb1d93 100644
--- a/detox/src/artifacts/templates/plugin/ArtifactPlugin.test.js
+++ b/detox/src/artifacts/templates/plugin/ArtifactPlugin.test.js
@@ -6,18 +6,12 @@ const testSummaries = require('./__mocks__/testSummaries.mock');
 
 class TestArtifactPlugin extends ArtifactPlugin {}
 
-describe('ArtifactPlugin', () => {
+describe(ArtifactPlugin, () => {
   let api;
   let plugin;
 
   beforeEach(() => {
-    api = {
-      userConfig: {
-        enabled: false,
-        keepOnlyFailedTestsArtifacts: false,
-      },
-    };
-
+    api = {};
     plugin = new TestArtifactPlugin({ api });
   });
 
diff --git a/detox/src/artifacts/templates/plugin/StartupAndTestRecorderPlugin.test.js b/detox/src/artifacts/templates/plugin/StartupAndTestRecorderPlugin.test.js
index 078c06589..3437433f4 100644
--- a/detox/src/artifacts/templates/plugin/StartupAndTestRecorderPlugin.test.js
+++ b/detox/src/artifacts/templates/plugin/StartupAndTestRecorderPlugin.test.js
@@ -9,12 +9,7 @@ describe('StartupAndTestRecorderPlugin', () => {
   let plugin;
 
   beforeEach(() => {
-    api = new ArtifactsApi({
-      config: {
-        enabled: false,
-        keepOnlyFailedTestsArtifacts: false,
-      },
-    });
+    api = new ArtifactsApi();
     plugin = new FakeStartupAndTestRecorderPlugin({ api });
   });
 
diff --git a/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.js b/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.js
index 90b2b2506..1696c23ff 100644
--- a/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.js
+++ b/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.js
@@ -7,8 +7,7 @@ class TwoSnapshotsPerTestPlugin extends ArtifactPlugin {
   constructor({ api }) {
     super({ api });
 
-    this.shouldTakeAutomaticSnapshots = this.api.userConfig.shouldTakeAutomaticSnapshots;
-    this.keepOnlyFailedTestsArtifacts = this.api.userConfig.keepOnlyFailedTestsArtifacts;
+    this.shouldTakeAutomaticSnapshots = true;
     this.snapshots = {
       fromTest: {},
       fromSession: {},
diff --git a/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.test.js b/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.test.js
index 1d722f736..d6a1b74c3 100644
--- a/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.test.js
+++ b/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.test.js
@@ -9,13 +9,7 @@ describe('TwoSnapshotsPerTestPlugin', () => {
   let plugin;
 
   beforeEach(() => {
-    api = new ArtifactsApi({
-      config: {
-        enabled: true,
-        shouldTakeAutomaticSnapshots: true,
-        keepOnlyFailedTestsArtifacts: false,
-      },
-    });
+    api = new ArtifactsApi();
     plugin = new FakeTwoSnapshotsPerTestPlugin({ api });
   });
 
@@ -279,8 +273,9 @@ describe('TwoSnapshotsPerTestPlugin', () => {
 });
 
 class FakeTwoSnapshotsPerTestPlugin extends TwoSnapshotsPerTestPlugin {
-  constructor({ api }) {
-    super({ api });
+  constructor(...args) {
+    super(...args);
+    this.enabled = true;
     this.createTestArtifact = jest.fn(this.createTestArtifact.bind(this));
 
     const nonDeletable = { deleteProperty: () => true };
diff --git a/detox/src/artifacts/templates/plugin/WholeTestRecorderPlugin.test.js b/detox/src/artifacts/templates/plugin/WholeTestRecorderPlugin.test.js
index fe7f513e0..eaaa56152 100644
--- a/detox/src/artifacts/templates/plugin/WholeTestRecorderPlugin.test.js
+++ b/detox/src/artifacts/templates/plugin/WholeTestRecorderPlugin.test.js
@@ -8,12 +8,7 @@ describe('WholeTestRecorderPlugin', () => {
   let plugin;
 
   beforeEach(() => {
-    api = new ArtifactsApi({
-      config: {
-        enabled: false,
-        keepOnlyFailedTestsArtifacts: false,
-      },
-    });
+    api = new ArtifactsApi();
     plugin = new FakeWholeTestRecorderPlugin({ api });
   });
 
diff --git a/detox/src/artifacts/utils/ArtifactPathBuilder.test.js b/detox/src/artifacts/utils/ArtifactPathBuilder.test.js
index 1f7a22746..6d28c9032 100644
--- a/detox/src/artifacts/utils/ArtifactPathBuilder.test.js
+++ b/detox/src/artifacts/utils/ArtifactPathBuilder.test.js
@@ -8,7 +8,7 @@ describe(ArtifactPathBuilder, () => {
   describe('precise tests', () => {
     beforeEach(() => {
       pathBuilder = new ArtifactPathBuilder({
-        rootDir: '/tmp'
+        artifactsRootDir: '/tmp'
       });
     });
 
@@ -35,7 +35,7 @@ describe(ArtifactPathBuilder, () => {
   describe('snapshot tests', () => {
     beforeEach(() => {
       pathBuilder = new ArtifactPathBuilder({
-        rootDir: '/tmp/subdir',
+        artifactsRootDir: '/tmp/subdir',
       });
     });
 
diff --git a/detox/src/artifacts/utils/buildDefaultArtifactsRootDirpath.test.js b/detox/src/artifacts/utils/buildDefaultArtifactsRootDirpath.test.js
index 5fb825f08..a625df28d 100644
--- a/detox/src/artifacts/utils/buildDefaultArtifactsRootDirpath.test.js
+++ b/detox/src/artifacts/utils/buildDefaultArtifactsRootDirpath.test.js
@@ -1,23 +1,15 @@
 const path = require('path');
-const buildDefaultArtifactsRootDirpath = require('./buildDefaultArtifactsRootDirpath');
 
 describe('buildDefaultArtifactsRootDirpath', () => {
-  beforeEach(() => {
-    process.env.DETOX_START_TIMESTAMP = '1572951641499';
-  });
+  let buildDefaultArtifactsRootDirpath;
 
-  afterAll(() => {
-    delete process.env.DETOX_START_TIMESTAMP;
+  beforeEach(() => {
+    jest.mock('./getTimeStampString', () => () => 'Today 1:23:45PM');
+    buildDefaultArtifactsRootDirpath = require('./buildDefaultArtifactsRootDirpath');
   });
 
   it('should append subdir if dir does not end with /', () => {
-    expect(buildDefaultArtifactsRootDirpath('iphone8', 'artifacts')).toBe(path.join('artifacts', 'iphone8.2019-11-05 11-00-41Z'));
-  });
-
-  it('should fall back to Date.now if environment variable DETOX_START_TIMESTAMP is unset', () => {
-    delete process.env.DETOX_START_TIMESTAMP;
-    expect(buildDefaultArtifactsRootDirpath('iphone8', 'artifacts')).toMatch(/^artifacts[\\\/]iphone8\.\d{4}-\d{2}-\d{2} \d{2}-\d{2}-\d{2}Z$/);
-    expect(buildDefaultArtifactsRootDirpath('iphone8', 'artifacts')).not.toBe(path.join('artifacts', 'iphone8.2019-11-05 11-00-41Z'));
+    expect(buildDefaultArtifactsRootDirpath('iphone8', 'artifacts')).toBe(path.join('artifacts', 'iphone8.Today 1:23:45PM'));
   });
 
   it('should not append subdir if dir ends with /', () => {
diff --git a/detox/src/artifacts/video/VideoArtifactPlugin.test.js b/detox/src/artifacts/video/VideoArtifactPlugin.test.js
deleted file mode 100644
index 81ab3ff16..000000000
--- a/detox/src/artifacts/video/VideoArtifactPlugin.test.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const VideoArtifactPlugin = require('./VideoArtifactPlugin');
-
-describe('VideoArtifactPlugin', () => {
-  describe('static parseConfig(config)', () => {
-    const parseConfig = VideoArtifactPlugin.parseConfig;
-
-    const ENABLE_MODES = ['all', 'failing'].map(x => [x]);
-    const DISABLE_MODES = ['none', { enabled: true }].map(x => [x]);
-
-    const INCLUSIVE_MODES = ['all', 'manual', 'none', { keepOnlyFailedTestsArtifacts: true }].map(x => [x]);
-    const EXCLUSIVE_MODES = ['failing'].map(x => [x]);
-
-    it.each(ENABLE_MODES)('should enable plugin if config = %j', (config) =>
-        expect(parseConfig(config).enabled).toBe(true));
-
-    it.each(DISABLE_MODES)('should disable plugin if config = %j', (config) =>
-        expect(parseConfig(config).enabled).toBe(false));
-
-    it.each(INCLUSIVE_MODES)('should save all screenshots if config = %j', (config) =>
-        expect(parseConfig(config).keepOnlyFailedTestsArtifacts).toBe(false));
-
-    it.each(EXCLUSIVE_MODES)('should save all screenshots if config = %j', (config) =>
-        expect(parseConfig(config).keepOnlyFailedTestsArtifacts).toBe(true));
-  });
-});
-
diff --git a/detox/src/configuration.test.js b/detox/src/configuration.test.js
index 867279901..ef91f7794 100644
--- a/detox/src/configuration.test.js
+++ b/detox/src/configuration.test.js
@@ -1,10 +1,5 @@
 const _ = require('lodash');
-const path = require('path');
 const schemes = require('./configurations.mock');
-const SimulatorInstrumentsPlugin = require('./artifacts/instruments/SimulatorInstrumentsPlugin');
-const LogArtifactPlugin = require('./artifacts/log/LogArtifactPlugin');
-const ScreenshotArtifactPlugin = require('./artifacts/screenshot/ScreenshotArtifactPlugin');
-const VideoArtifactPlugin = require('./artifacts/video/VideoArtifactPlugin');
 
 describe('configuration', () => {
   let configuration;
@@ -38,155 +33,6 @@ describe('configuration', () => {
     testFaultySession(schemes.invalidSessionNoSessionId.session);
   });
 
-  describe('composeArtifactsConfig', () => {
-    it('should produce a default config', () => {
-      expect(configuration.composeArtifactsConfig({
-        configurationName: 'abracadabra',
-        deviceConfig: {},
-        detoxConfig: {},
-      })).toEqual({
-        rootDir: expect.stringMatching(/^artifacts[\\\/]abracadabra\.\d{4}/),
-        pathBuilder: null,
-        plugins: schemes.pluginsDefaultsResolved,
-      });
-    });
-
-    it('should use artifacts config from the selected configuration', () => {
-      expect(configuration.composeArtifactsConfig({
-        configurationName: 'abracadabra',
-        deviceConfig: {
-          artifacts: {
-            ...schemes.allArtifactsConfiguration,
-            rootDir: 'otherPlace',
-            pathBuilder: _.noop,
-          }
-        },
-        detoxConfig: {},
-        cliConfig: {}
-      })).toEqual({
-        rootDir: expect.stringMatching(/^otherPlace[\\\/]abracadabra\.\d{4}/),
-        pathBuilder: _.noop,
-        plugins: schemes.pluginsAllResolved,
-      });
-    });
-
-    it('should use global artifacts config', () => {
-      expect(configuration.composeArtifactsConfig({
-        configurationName: 'abracadabra',
-        deviceConfig: {},
-        detoxConfig: {
-          artifacts: {
-            ...schemes.allArtifactsConfiguration,
-            rootDir: 'otherPlace',
-            pathBuilder: _.noop,
-          }
-        },
-        cliConfig: {}
-      })).toEqual({
-        rootDir: expect.stringMatching(/^otherPlace[\\\/]abracadabra\.\d{4}/),
-        pathBuilder: _.noop,
-        plugins: schemes.pluginsAllResolved,
-      });
-    });
-
-    it('should use CLI config', () => {
-      expect(configuration.composeArtifactsConfig({
-        configurationName: 'abracadabra',
-        deviceConfig: {},
-        detoxConfig: {},
-        cliConfig: {
-          artifactsLocation: 'otherPlace',
-          recordLogs: 'all',
-          takeScreenshots: 'all',
-          recordVideos: 'all',
-          recordPerformance: 'all',
-        }
-      })).toEqual({
-        rootDir: expect.stringMatching(/^otherPlace[\\\/]abracadabra\.\d{4}/),
-        pathBuilder: null,
-        plugins: schemes.pluginsAllResolved,
-      });
-    });
-
-    it('should prefer CLI config over selected configuration over global config', () => {
-      expect(configuration.composeArtifactsConfig({
-        configurationName: 'priority',
-        cliConfig: {
-          artifactsLocation: 'cli',
-        },
-        deviceConfig: {
-          artifacts: {
-            rootDir: 'configuration',
-            pathBuilder: _.identity,
-            plugins: {
-              log: 'failing',
-            },
-          },
-        },
-        detoxConfig: {
-          artifacts: {
-            rootDir: 'global',
-            pathBuilder: _.noop,
-            plugins: {
-              screenshot: 'all',
-            },
-          },
-        },
-      })).toEqual({
-        rootDir: expect.stringMatching(/^cli[\\\/]priority\.\d{4}/),
-        pathBuilder: _.identity,
-        plugins: {
-          log: schemes.pluginsFailingResolved.log,
-          screenshot: schemes.pluginsAllResolved.screenshot,
-          video: schemes.pluginsDefaultsResolved.video,
-          instruments: schemes.pluginsDefaultsResolved.instruments,
-        },
-      });
-    });
-
-    it('should resolve path builder from string (absolute path)', () => {
-      expect(configuration.composeArtifactsConfig({
-        configurationName: 'customization',
-        deviceConfig: {
-          artifacts: {
-            pathBuilder: path.join(__dirname, 'artifacts/__mocks__/FakePathBuilder')
-          },
-        },
-        detoxConfig: {},
-      })).toEqual(expect.objectContaining({
-        pathBuilder: require('./artifacts/__mocks__/FakePathBuilder'),
-      }));
-    });
-
-    it('should resolve path builder from string (relative path)', () => {
-      expect(configuration.composeArtifactsConfig({
-        configurationName: 'customization',
-        deviceConfig: {
-          artifacts: {
-            pathBuilder: 'package.json',
-          },
-        },
-        detoxConfig: {},
-      })).toEqual(expect.objectContaining({
-        pathBuilder: require(path.join(process.cwd(), 'package.json')),
-      }));
-    });
-
-    it('should not append configuration with timestamp if rootDir ends with slash', () => {
-      expect(configuration.composeArtifactsConfig({
-        configurationName: 'customization',
-        deviceConfig: {
-          artifacts: {
-            rootDir: '.artifacts/'
-          },
-        },
-        detoxConfig: {},
-      })).toEqual(expect.objectContaining({
-        rootDir: '.artifacts/',
-      }));
-    });
-  });
-
   function testFaultySession(config) {
     try {
       configuration.validateSession(config);
diff --git a/detox/src/devices/android/ADB.test.js b/detox/src/devices/android/ADB.test.js
index fd6e1878a..1e23ae0c3 100644
--- a/detox/src/devices/android/ADB.test.js
+++ b/detox/src/devices/android/ADB.test.js
@@ -44,7 +44,7 @@ describe('ADB', () => {
   describe('devices', () => {
     it(`should invoke ADB`, async () => {
       await adb.devices();
-      expect(exec).toHaveBeenCalledWith(`"${adbBinPath}"  devices`, { verbosity: 'high' }, undefined, 1);
+      expect(exec).toHaveBeenCalledWith(`${adbBinPath}  devices`, { verbosity: 'high' }, undefined, 1);
       expect(exec).toHaveBeenCalledTimes(1);
     });
 
@@ -137,11 +137,11 @@ describe('ADB', () => {
     await adb.install('emulator-5556', 'path inside "quotes" to/app');
 
     expect(exec).toHaveBeenCalledWith(
-      expect.stringContaining('adb" -s emulator-5556 shell "getprop ro.build.version.sdk"'),
+      expect.stringContaining('adb -s emulator-5556 shell "getprop ro.build.version.sdk"'),
       {}, undefined, 5);
 
     expect(exec).toHaveBeenCalledWith(
-      expect.stringContaining('adb" -s emulator-5556 install -rg "path inside \\"quotes\\" to/app"'),
+      expect.stringContaining('adb -s emulator-5556 install -rg "path inside \\"quotes\\" to/app"'),
       undefined, undefined, 1);
   });
 
diff --git a/detox/src/index.test.js b/detox/src/index.test.js
index 1b1d3f2e8..522838500 100644
--- a/detox/src/index.test.js
+++ b/detox/src/index.test.js
@@ -71,11 +71,6 @@ describe('index', () => {
     await detox.init(schemes.validOneDeviceNoSession);
 
     expect(Detox).toHaveBeenCalledWith({
-      artifactsConfig: {
-        ...schemes.defaultArtifactsConfiguration,
-        plugins: schemes.pluginsDefaultsResolved,
-        rootDir: expect.stringMatching(/^artifacts[\\\/]ios\.sim\.release/),
-      },
       deviceConfig: schemes.validOneDeviceNoSession.configurations['ios.sim.release'],
       session: undefined,
     });
@@ -88,11 +83,6 @@ describe('index', () => {
     await detox.init(schemes.validTwoDevicesNoSession);
 
     expect(Detox).toHaveBeenCalledWith({
-      artifactsConfig: {
-        ...schemes.defaultArtifactsConfiguration,
-        plugins: schemes.pluginsDefaultsResolved,
-        rootDir: expect.stringMatching(/^artifacts[\\\/]ios\.sim\.debug/),
-      },
       deviceConfig: schemes.validTwoDevicesNoSession.configurations['ios.sim.debug'],
       session: undefined,
     });
@@ -123,11 +113,6 @@ describe('index', () => {
     }
 
     expect(Detox).toHaveBeenCalledWith({
-      artifactsConfig: {
-        ...schemes.defaultArtifactsConfiguration,
-        plugins: schemes.pluginsDefaultsResolved,
-        rootDir: expect.stringMatching(/^artifacts[\\\/]ios\.sim\.release/),
-      },
       deviceConfig: expectedConfig,
       session: undefined,
     });
diff --git a/detox/src/utils/appendFile.test.js b/detox/src/utils/appendFile.test.js
deleted file mode 100644
index 0b36b5e51..000000000
--- a/detox/src/utils/appendFile.test.js
+++ /dev/null
@@ -1,35 +0,0 @@
-const fs = require('fs-extra');
-const tempfile = require('tempfile');
-const appendFile = require('./appendFile');
-
-describe('appendFile', () => {
-  let src, dest;
-
-  beforeEach(() => {
-    src = tempfile();
-    dest = tempfile();
-  });
-
-  afterEach(async () => {
-    await Promise.all([src, dest].map(f => fs.remove(f)));
-  });
-
-  it('should throw error if source file does not exist', async () => {
-    await expect(appendFile(tempfile(), dest)).rejects.toThrowError(/ENOENT/);
-  });
-
-  it('should append source file contents to destination file contents', async () => {
-    await fs.writeFile(dest, 'Begin\n');
-    await fs.writeFile(src, 'End');
-
-    await appendFile(src, dest);
-    expect(await fs.readFile(dest, 'utf8')).toEqual('Begin\nEnd');
-  });
-
-  it('should create a new file in destination if it does not exist', async () => {
-    await fs.writeFile(src, 'Begin and End');
-
-    await appendFile(src, dest);
-    expect(await fs.readFile(dest, 'utf8')).toEqual('Begin and End');
-  });
-});
diff --git a/detox/test/package.json b/detox/test/package.json
index 8f4875de5..77195dcfe 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-test",
-  "version": "14.7.1",
+  "version": "14.6.1",
   "private": true,
   "engines": {
     "node": ">=8.3.0"
@@ -31,7 +31,7 @@
   },
   "devDependencies": {
     "@babel/core": "^7.4.5",
-    "detox": "^14.7.1",
+    "detox": "^14.6.1",
     "express": "^4.15.3",
     "jest": "24.9.x",
     "jest-circus": "24.9.x",
diff --git a/examples/demo-react-native-jest/e2e/detox.pathbuilder.android.js b/examples/demo-react-native-jest/e2e/detox.pathbuilder.android.js
deleted file mode 100644
index 6af0c372a..000000000
--- a/examples/demo-react-native-jest/e2e/detox.pathbuilder.android.js
+++ /dev/null
@@ -1,4 +0,0 @@
-const CustomPathBuilder = require('./detox.pathbuilder');
-module.exports = ({ rootDir }) => {
-  return new CustomPathBuilder({ rootDir, platform: 'android' });
-};
diff --git a/examples/demo-react-native-jest/e2e/detox.pathbuilder.ios.js b/examples/demo-react-native-jest/e2e/detox.pathbuilder.ios.js
deleted file mode 100644
index a19d11f37..000000000
--- a/examples/demo-react-native-jest/e2e/detox.pathbuilder.ios.js
+++ /dev/null
@@ -1,4 +0,0 @@
-const CustomPathBuilder = require('./detox.pathbuilder');
-module.exports = ({ rootDir }) => {
-  return new CustomPathBuilder({ rootDir, platform: 'ios' });
-};
diff --git a/examples/demo-react-native-jest/e2e/detox.pathbuilder.js b/examples/demo-react-native-jest/e2e/detox.pathbuilder.js
deleted file mode 100644
index 039c5eb6e..000000000
--- a/examples/demo-react-native-jest/e2e/detox.pathbuilder.js
+++ /dev/null
@@ -1,21 +0,0 @@
-const path = require('path');
-const sanitizeFilename = require('sanitize-filename');
-
-const SANITIZE_OPTIONS = {replacement: '_'};
-const sanitize = (filename) => sanitizeFilename(filename, SANITIZE_OPTIONS);
-
-class CustomPathBuilder {
-  constructor({ platform, rootDir }) {
-    this.platform = platform;
-    this.rootDir = rootDir;
-  }
-
-  buildPathForTestArtifact(artifactName, testSummary = null) {
-    const fullName = testSummary && testSummary.fullName || '';
-    const segments = [this.rootDir, this.platform, sanitize(fullName), sanitize(artifactName)];
-
-    return path.join(...segments.filter(Boolean));
-  }
-}
-
-module.exports = CustomPathBuilder;