Repository: wix/Detox
Pull request #2007 (2020-04-16 17:05:36)

Handle logging in async function
- [x] This is a small change 
---

**Description:**

Sorry I didn't file an issue cuz this was such a small change. 

When using mocha, and trying to log within a hook/test `detox/src/utils/customConsoleLogger.js:: getOrigin()` blows up because `userCallsite.getFileName()` is `undefined` and therefore `path.relative()` explodes.

Simple reproduce:
```
  before: async () => {
    console.log('**********BLAH');
```

PR is not the most elegant solution, but not sure there is much that can be done when using https://github.com/sindresorhus/callsites

Without this fix, I can not use Detox (and I want to, cuz really like it :)


Patch:
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index a357af457..1e4fa9b2c 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,42 +1,1 @@
-### IMPORTANT: Use search before opening an issue. For help, ask in Stack Overflow with the `detox` tag. Duplicate issues or questions will simply be closed.
-
-<!---
-Please fill this template before submitting an issue. This helps determine the issue cause much quicker.
-Remove the sections that are not relevant to your issue.
-If you are filing an enhancement request, you only need to fill the description section and can safely remove the other sections.
--->
-
-#### Description
-<!---
-Provide a clear and concise description of your problem and what version of Detox you are using. In case of an enhancement request, please provide use cases where the required functionality would be useful.
--->
-
-#### Steps to Reproduce
-<!---
-In case of a bug report, provide the steps necessary to reproduce the issue. If you are seeing a regression, try to provide the last known version where the issue did not reproduce.
-If possible, please provide a small demo project that reproduces the issue, or attach a video with the reproduction - this would be very appreciated.
--->
-
-#### Detox, Node, Device, Xcode and macOS Versions
-<!---
-Provide the macOS and Xcode versions you are using, and the device / simulator you tried it on.
--->
-
-* Detox:
-* React Native:
-* Node:
-* Device:
-* Xcode:
-* macOS: 
-
-#### Device and verbose Detox logs
-<!---
-Provide the device and verbose Detox logs so we can understand what happened. You can obtain them by passing the loglevel param: `detox test --loglevel verbose`
-
--->
-
-#### iOS build logs
-<!---
-If you are seeing a build problem (during npm install), provide the log found here: ~/Library/Detox/ios/{...}/detox_ios.log
-
--->
+### IMPORTANT: DO NOT USE THIS FORM FOR SUBMITTING ISSUES. USE THE DESIGNATED ISSUE TEMPLATES ON GITHUB AND POPULATE ALL THE REQUIRED INFORMATION.
diff --git a/README.md b/README.md
index e97086f1d..1d14c5ae0 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@ Gray box end-to-end testing and automation library for mobile apps.
 [![NPM Version](https://img.shields.io/npm/v/detox.svg?style=flat)](https://www.npmjs.com/package/detox)
 [![NPM Downloads](https://img.shields.io/npm/dm/detox.svg?style=flat)](https://www.npmjs.com/package/detox)
 [![Build Status](https://img.shields.io/jenkins/s/http/jenkins-oss.wixpress.com:8080/job/multi-detox-master.svg)](https://jenkins-oss.wixpress.com/job/multi-detox-master/)
+[![Coverage Status](https://coveralls.io/repos/github/wix/Detox/badge.svg?branch=master)](https://coveralls.io/github/wix/Detox?branch=master)
 
 - [About](#about)
 - [Getting Started](/docs/Introduction.GettingStarted.md)
diff --git a/detox/local-cli/run-server.js b/detox/local-cli/run-server.js
index 67a36ebcd..be63a0990 100644
--- a/detox/local-cli/run-server.js
+++ b/detox/local-cli/run-server.js
@@ -1,5 +1,4 @@
 const DetoxServer = require('../src/server/DetoxServer');
-const log = require('../src/utils/logger').child({ __filename });
 
 module.exports.command = 'run-server';
 module.exports.desc = 'Start a standalone Detox server';
@@ -30,7 +29,6 @@ module.exports.handler = async function runServer(argv) {
 
   new DetoxServer({
     port: +argv.port,
-    log,
     standalone: true,
   });
 };
diff --git a/detox/package.json b/detox/package.json
index dbef841a1..63e13af81 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -1,7 +1,7 @@
 {
   "name": "detox",
   "description": "E2E tests and automation for mobile",
-  "version": "16.2.0",
+  "version": "16.4.1",
   "bin": {
     "detox": "local-cli/cli.js"
   },
@@ -31,7 +31,7 @@
   "devDependencies": {
     "eslint": "^4.11.0",
     "eslint-plugin-node": "^6.0.1",
-    "jest": "25.1.x",
+    "jest": "25.3.x",
     "mockdate": "^2.0.1",
     "prettier": "1.7.0",
     "react-native": "0.62.x",
@@ -53,7 +53,7 @@
     "sanitize-filename": "^1.6.1",
     "shell-utils": "^1.0.9",
     "tail": "^2.0.0",
-    "telnet-client": "0.15.3",
+    "telnet-client": "1.2.8",
     "tempfile": "^2.0.0",
     "which": "^1.3.1",
     "ws": "^3.3.1",
@@ -114,6 +114,15 @@
     ],
     "resetMocks": true,
     "resetModules": true,
+    "coverageReporters": [
+      [
+        "lcov",
+        {
+          "projectRoot": ".."
+        }
+      ],
+      "html"
+    ],
     "coverageThreshold": {
       "global": {
         "statements": 100,
diff --git a/detox/runners/jest/SpecReporterCircus.js b/detox/runners/jest/SpecReporterCircus.js
index d94c44142..cade356a4 100644
--- a/detox/runners/jest/SpecReporterCircus.js
+++ b/detox/runners/jest/SpecReporterCircus.js
@@ -22,8 +22,10 @@ class SpecReporterCircus extends CircusTestEventListenerBase {
   }
 
   _onTestStart(event) {
+    const { test } = event;
     this._specReporter.onTestStart({
-      description: event.test.name,
+      description: test.name,
+      invocations: test.invocations,
     });
   }
 
@@ -31,6 +33,7 @@ class SpecReporterCircus extends CircusTestEventListenerBase {
     const { test } = event;
     const testInfo = {
       description: test.name,
+      invocations: test.invocations,
     };
     this._specReporter.onTestEnd(testInfo, test.errors.length ? 'failed' : 'success');
   }
diff --git a/detox/runners/jest/SpecReporterImpl.js b/detox/runners/jest/SpecReporterImpl.js
index ec8ce1171..af9ba1fb7 100644
--- a/detox/runners/jest/SpecReporterImpl.js
+++ b/detox/runners/jest/SpecReporterImpl.js
@@ -29,12 +29,12 @@ class SpecReporter extends ReporterBase {
     }
   }
 
-  onTestStart({description}) {
-    this._traceTest({description});
+  onTestStart({description, invocations = 1}) {
+    this._traceTest({description, invocations});
   }
 
-  onTestEnd({description}, result) {
-    let status = '';
+  onTestEnd({description, invocations = 1}, result) {
+    let status;
     switch (result) {
       case 'skipped': status = RESULT_SKIPPED; break;
       case 'failed': status = RESULT_FAILED; break;
@@ -42,7 +42,7 @@ class SpecReporter extends ReporterBase {
       case 'success': status = RESULT_SUCCESS; break;
       default: status = RESULT_OTHER; break;
     }
-    this._traceTest({description}, status);
+    this._traceTest({description, invocations}, status);
   }
 
   _regenerateSuitesDesc() {
@@ -58,8 +58,11 @@ class SpecReporter extends ReporterBase {
     this._suitesDesc = chalk.bold.white(this._suitesDesc);
   }
 
-  _traceTest({description}, status) {
-    const desc = this._suitesDesc + chalk.gray(description) + chalk.gray(status ? ` [${status}]` : '');
+  _traceTest({description, invocations}, _status = undefined) {
+    const testDescription = chalk.gray(description);
+    const retriesDescription = (invocations > 1) ? chalk.gray(` [Retry #${invocations - 1}]`) : '';
+    const status = chalk.gray(_status ? ` [${_status}]` : '');
+    const desc = this._suitesDesc + testDescription + retriesDescription + status;
     log.info({event: 'SPEC_STATE_CHANGE'}, desc);
   }
 }
diff --git a/detox/src/Detox.js b/detox/src/Detox.js
index 31479ceff..22c28b7c0 100644
--- a/detox/src/Detox.js
+++ b/detox/src/Detox.js
@@ -1,28 +1,18 @@
 const _ = require('lodash');
 const util = require('util');
+const {URL} = require('url');
 const logger = require('./utils/logger');
 const Deferred = require('./utils/Deferred');
-const log = logger.child({ __filename });
 const Device = require('./devices/Device');
-const IosDriver = require('./devices/drivers/ios/IosDriver');
-const SimulatorDriver = require('./devices/drivers/ios/SimulatorDriver');
-const EmulatorDriver = require('./devices/drivers/android/EmulatorDriver');
-const AttachedAndroidDriver = require('./devices/drivers/android/AttachedAndroidDriver');
 const DetoxRuntimeError = require('./errors/DetoxRuntimeError');
 const AsyncEmitter = require('./utils/AsyncEmitter');
 const MissingDetox = require('./utils/MissingDetox');
-const configuration = require('./configuration');
 const Client = require('./client/Client');
 const DetoxServer = require('./server/DetoxServer');
-const URL = require('url').URL;
 const ArtifactsManager = require('./artifacts/ArtifactsManager');
 
-const DEVICE_CLASSES = {
-  'ios.simulator': SimulatorDriver,
-  'ios.none': IosDriver,
-  'android.emulator': EmulatorDriver,
-  'android.attached': AttachedAndroidDriver,
-};
+const log = logger.child({ __filename });
+const driverRegistry = require('./devices/DriverRegistry').default;
 
 const _initHandle = Symbol('_initHandle');
 const _assertNoPendingInit = Symbol('_assertNoPendingInit');
@@ -37,11 +27,13 @@ class Detox {
 
     this[_initHandle] = null;
 
-    const {artifactsConfig, deviceConfig, session} = config;
+    const {artifactsConfig, behaviorConfig, deviceConfig, sessionConfig} = config;
 
     this._artifactsConfig = artifactsConfig;
+    this._behaviorConfig = behaviorConfig;
     this._deviceConfig = deviceConfig;
-    this._userSession = deviceConfig.session || session;
+    this._sessionConfig = sessionConfig;
+
     this._client = null;
     this._server = null;
     this._artifactsManager = null;
@@ -64,12 +56,12 @@ class Detox {
     this.device = null;
   }
 
-  init(userParams) {
+  init() {
     if (!this[_initHandle]) {
       this[_initHandle] = new Deferred();
 
       const { resolve, reject } = this[_initHandle];
-      this._doInit(userParams).then(resolve, reject);
+      this._doInit().then(resolve, reject);
     }
 
     return this[_initHandle].promise;
@@ -91,6 +83,10 @@ class Detox {
 
     if (this.device) {
       await this.device._cleanup();
+
+      if (this._behaviorConfig.cleanup.shutdownDevice) {
+        await this.device.shutdown();
+      }
     }
 
     if (this._server) {
@@ -133,17 +129,12 @@ class Detox {
     await this._artifactsManager.onSuiteEnd(suite);
   }
 
-  async _doInit(userParams) {
-    const sessionConfig = await this._getSessionConfig();
-    const params = {
-      launchApp: true,
-      initGlobals: true,
-      ...userParams,
-    };
+  async _doInit() {
+    const behaviorConfig = this._behaviorConfig.init;
+    const sessionConfig = this._sessionConfig;
 
-    if (!this._userSession) {
+    if (this._sessionConfig.autoStart) {
       this._server = new DetoxServer({
-        log: logger,
         port: new URL(sessionConfig.server).port,
       });
     }
@@ -152,12 +143,7 @@ class Detox {
     this._client.setNonresponsivenessListener(this._onNonresnponsivenessEvent.bind(this));
     await this._client.connect();
 
-    const DeviceDriverClass = DEVICE_CLASSES[this._deviceConfig.type];
-    if (!DeviceDriverClass) {
-      throw new Error(`'${this._deviceConfig.type}' is not supported`);
-    }
-
-    const deviceDriver = new DeviceDriverClass({
+    const deviceDriver = driverRegistry.resolve(this._deviceConfig.type, {
       client: this._client,
       emitter: this._eventEmitter,
     });
@@ -171,7 +157,7 @@ class Detox {
       sessionConfig,
     });
 
-    if (params.initGlobals) {
+    if (behaviorConfig.exposeGlobals) {
       Object.assign(global, {
         ...deviceDriver.matchers,
         device: this.device,
@@ -182,7 +168,19 @@ class Detox {
     this._artifactsManager.subscribeToDeviceEvents(this._eventEmitter);
     this._artifactsManager.registerArtifactPlugins(deviceDriver.declareArtifactPlugins());
 
-    await this.device.prepare(params);
+    await this.device.prepare();
+
+    if (behaviorConfig.reinstallApp) {
+      await this.device.uninstallApp();
+      await this.device.installApp();
+    }
+
+    if (behaviorConfig.launchApp) {
+      await this.device.launchApp({
+        newInstance: true
+      });
+    }
+
     return this;
   }
 
@@ -259,14 +257,6 @@ class Detox {
     }
   }
 
-  async _getSessionConfig() {
-    const session = this._userSession || await configuration.defaultSession();
-
-    configuration.validateSession(session);
-
-    return session;
-  }
-
   _onEmitError({ error, eventName, eventObj }) {
     log.error(
       { event: 'EMIT_ERROR', fn: eventName },
diff --git a/detox/src/DetoxExportWrapper.js b/detox/src/DetoxExportWrapper.js
index 9fc85ee9b..b108c8fae 100644
--- a/detox/src/DetoxExportWrapper.js
+++ b/detox/src/DetoxExportWrapper.js
@@ -2,8 +2,8 @@ const _ = require('lodash');
 const funpermaproxy = require('funpermaproxy');
 const Detox = require('./Detox');
 const DetoxConstants = require('./DetoxConstants');
-const log = require('./utils/logger').child({ __filename });
 const configuration = require('./configuration');
+const log = require('./utils/logger').child({ __filename });
 
 const _detox = Symbol('detox');
 
@@ -30,31 +30,46 @@ class DetoxExportWrapper {
   }
 
   async init(config, params) {
-    if (!params || params.initGlobals !== false) {
-      Detox.none.initContext(global);
-    }
+    let configError, exposeGlobals, resolvedConfig;
 
-    Detox.none.setError(null);
+    try {
+      resolvedConfig = await configuration.composeDetoxConfig(config, params);
+      exposeGlobals = resolvedConfig.behaviorConfig.init.exposeGlobals;
+    } catch (err) {
+      configError = err;
+      exposeGlobals = true;
+    }
 
     try {
-      this[_detox] = DetoxExportWrapper._createInstance(config);
+      if (exposeGlobals) {
+        Detox.none.initContext(global);
+      }
+
+      if (configError) {
+        throw configError;
+      }
+
+      this[_detox] = new Detox(resolvedConfig);
       await this[_detox].init(params);
+      Detox.none.setError(null);
+
+      return this[_detox];
     } catch (err) {
       Detox.none.setError(err);
 
       log.error({ event: 'DETOX_INIT_ERROR' }, '\n', err);
       throw err;
     }
-
-    return this[_detox];
   }
 
   async cleanup() {
-    Detox.none.cleanupContext(global);
-
-    if (this[_detox] !== Detox.none) {
-      await this[_detox].cleanup();
+    try {
+      if (this[_detox] !== Detox.none) {
+        await this[_detox].cleanup();
+      }
+    } finally {
       this[_detox] = Detox.none;
+      Detox.none.cleanupContext(global);
     }
   }
 
@@ -67,30 +82,6 @@ class DetoxExportWrapper {
   _defineProxy(name) {
     this[name] = funpermaproxy(() => this[_detox][name]);
   }
-
-  static _createInstance(detoxConfig) {
-    if (!detoxConfig) {
-      throw new Error(`No configuration was passed to detox, make sure you pass a detoxConfig when calling 'detox.init(detoxConfig)'`);
-    }
-
-    if (!(detoxConfig.configurations && _.size(detoxConfig.configurations) >= 1)) {
-      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,
-    });
-
-    return new Detox({
-      deviceConfig,
-      artifactsConfig,
-      session: detoxConfig.session,
-    });
-  }
 }
 
 module.exports = DetoxExportWrapper;
diff --git a/detox/src/artifacts/ArtifactsManager.js b/detox/src/artifacts/ArtifactsManager.js
index 0028183f8..a63ae4b6c 100644
--- a/detox/src/artifacts/ArtifactsManager.js
+++ b/detox/src/artifacts/ArtifactsManager.js
@@ -6,7 +6,7 @@ const FileArtifact = require('./templates/artifact/FileArtifact');
 const log = require('../utils/logger').child({ __filename });
 
 class ArtifactsManager {
-  constructor({ pathBuilder, plugins } = {}) {
+  constructor({ pathBuilder, plugins }) {
     this._pluginConfigs = plugins;
     this._idlePromise = Promise.resolve();
     this._idleCallbackRequests = [];
@@ -60,7 +60,7 @@ class ArtifactsManager {
       .catch(e => this._idleCallbackErrorHandle(e, caller));
   }
 
-  registerArtifactPlugins(artifactPluginFactoriesMap = {}) {
+  registerArtifactPlugins(artifactPluginFactoriesMap) {
     for (const [key, factory] of Object.entries(artifactPluginFactoriesMap)) {
       const config = this._pluginConfigs[key];
       this._artifactPlugins[key] = this._instantitateArtifactPlugin(factory, config);
diff --git a/detox/src/client/__mocks__/Client.js b/detox/src/client/__mocks__/Client.js
new file mode 100644
index 000000000..5d248746d
--- /dev/null
+++ b/detox/src/client/__mocks__/Client.js
@@ -0,0 +1,17 @@
+const Deferred = require('../../utils/Deferred');
+const FakeClient = jest.genMockFromModule('../Client');
+
+FakeClient.setInfiniteConnect = () => {
+  FakeClient.mockImplementationOnce(() => {
+    const client = new FakeClient();
+    client.deferred = new Deferred();
+    client.connect.mockReturnValue(client.deferred.promise)
+    client.cleanup.mockImplementation(() => {
+      client.deferred.reject('Fake error: aborted connection');
+    });
+
+    return client;
+  });
+};
+
+module.exports = FakeClient;
diff --git a/detox/src/configuration.js b/detox/src/configuration.js
index 9fa777419..bc03379b0 100644
--- a/detox/src/configuration.js
+++ b/detox/src/configuration.js
@@ -1,6 +1,7 @@
 const _ = require('lodash');
 const DetoxConfigError = require('./errors/DetoxConfigError');
 const uuid = require('./utils/uuid');
+const resolveModuleFromPath = require('./utils/resolveModuleFromPath');
 const argparse = require('./utils/argparse');
 const getPort = require('get-port');
 const buildDefaultArtifactsRootDirpath = require('./artifacts/utils/buildDefaultArtifactsRootDirpath');
@@ -12,17 +13,56 @@ const ScreenshotArtifactPlugin = require('./artifacts/screenshot/ScreenshotArtif
 const VideoArtifactPlugin = require('./artifacts/video/VideoArtifactPlugin');
 const ArtifactPathBuilder = require('./artifacts/utils/ArtifactPathBuilder');
 
-async function defaultSession() {
-  return {
-    server: `ws://localhost:${await getPort()}`,
-    sessionId: uuid.UUID()
-  };
+function throwOnEmptyDevice() {
+  throw new DetoxConfigError(`'device' property is empty, should hold the device query to run on (e.g. { "type": "iPhone 11 Pro" }, { "avdName": "Nexus_5X_API_29" })`);
 }
 
-function validateSession(session) {
-  if (!session) {
-    throw new Error(`No session configuration was found, pass settings under the session property`);
-  }
+function throwOnEmptyType() {
+  throw new DetoxConfigError(`'type' property is missing, should hold the device type to test on (e.g. "ios.simulator" or "android.emulator")`);
+}
+
+function throwOnEmptyBinaryPath() {
+  throw new DetoxConfigError(`'binaryPath' property is missing, should hold the app binary path`);
+}
+
+function composeBehaviorConfig({ detoxConfig, deviceConfig, userParams }) {
+  return _.defaultsDeep(
+    {
+      init: {
+        reinstallApp: argparse.getArgValue('reuse') ? false : undefined,
+      },
+      cleanup: {
+        shutdownDevice: argparse.getArgValue('cleanup') ? true : undefined,
+      },
+    },
+    userParams && {
+      init: {
+        exposeGlobals: userParams.initGlobals,
+        launchApp: userParams.launchApp,
+        reinstallApp: !userParams.reuse,
+      },
+    },
+    deviceConfig.behavior,
+    detoxConfig.behavior,
+    {
+      init: {
+        exposeGlobals: true,
+        reinstallApp: true,
+        launchApp: true,
+      },
+      cleanup: {
+        shutdownDevice: false,
+      },
+    }
+  );
+}
+
+async function composeSessionConfig({ detoxConfig, deviceConfig }) {
+  const session = deviceConfig.session || detoxConfig.session || {
+    autoStart: true,
+    server: `ws://localhost:${await getPort()}`,
+    sessionId: uuid.UUID(),
+  };
 
   if (!session.server) {
     throw new Error(`session.server property is missing, should hold the server address`);
@@ -31,22 +71,54 @@ function validateSession(session) {
   if (!session.sessionId) {
     throw new Error(`session.sessionId property is missing, should hold the server session id`);
   }
-}
 
-function throwOnEmptyDevice() {
-  throw new DetoxConfigError(`'device' property is empty, should hold the device query to run on (e.g. { "type": "iPhone 11 Pro" }, { "avdName": "Nexus_5X_API_29" })`);
+  return session;
 }
 
-function throwOnEmptyType() {
-  throw new DetoxConfigError(`'type' property is missing, should hold the device type to test on (e.g. "ios.simulator" or "android.emulator")`);
-}
+async function composeDetoxConfig(detoxConfig, userParams, cliConfig = getArtifactsCliConfig()) {
+  if (!detoxConfig) {
+    throw new Error(`No configuration was passed to detox, make sure you pass a detoxConfig when calling 'detox.init(detoxConfig)'`);
+  }
 
-function throwOnEmptyBinaryPath() {
-  throw new DetoxConfigError(`'binaryPath' property is missing, should hold the app binary path`);
+  const deviceConfig = composeDeviceConfig(detoxConfig);
+  const configurationName = _.findKey(detoxConfig.configurations, (config) => {
+    return config === deviceConfig;
+  });
+
+  const artifactsConfig = composeArtifactsConfig({
+    configurationName,
+    detoxConfig,
+    deviceConfig,
+    cliConfig,
+  });
+
+  const behaviorConfig = composeBehaviorConfig({
+    detoxConfig,
+    deviceConfig,
+    userParams,
+  });
+
+  const sessionConfig = await composeSessionConfig({
+    detoxConfig,
+    deviceConfig,
+  });
+
+  return {
+    artifactsConfig,
+    behaviorConfig,
+    deviceConfig,
+    sessionConfig,
+  };
 }
 
-function composeDeviceConfig({ configurations }) {
-  const configurationName = argparse.getArgValue('configuration');
+function composeDeviceConfig(options) {
+  const { configurations, selectedConfiguration } = options;
+
+  if (_.isEmpty(configurations)) {
+    throw new Error(`There are no device configurations in the detox config`);
+  }
+
+  const configurationName = selectedConfiguration || argparse.getArgValue('configuration');
   const deviceOverride = argparse.getArgValue('device-name');
 
   const deviceConfig = (!configurationName && _.size(configurations) === 1)
@@ -83,16 +155,11 @@ function getArtifactsCliConfig() {
   };
 }
 
-function resolveModuleFromPath(modulePath) {
-  const resolvedModulePath = require.resolve(modulePath, { paths: [process.cwd()]});
-  return require(resolvedModulePath);
-}
-
 function composeArtifactsConfig({
   configurationName,
   deviceConfig,
   detoxConfig,
-  cliConfig = getArtifactsCliConfig()
+  cliConfig,
 }) {
   const artifactsConfig = _.defaultsDeep(
       extendArtifactsConfig({
@@ -176,11 +243,10 @@ function resolveArtifactsPathBuilder(artifactsConfig) {
 }
 
 module.exports = {
-  defaultSession,
-  validateSession,
-  throwOnEmptyDevice,
-  throwOnEmptyType,
   throwOnEmptyBinaryPath,
-  composeDeviceConfig,
   composeArtifactsConfig,
+  composeBehaviorConfig,
+  composeDeviceConfig,
+  composeSessionConfig,
+  composeDetoxConfig,
 };
diff --git a/detox/src/configurations.mock.js b/detox/src/configurations.mock.js
index 3053d80a2..5d5c9d9ee 100644
--- a/detox/src/configurations.mock.js
+++ b/detox/src/configurations.mock.js
@@ -50,40 +50,6 @@ const pluginsAllResolved = {
   timeline: TimelineArtifactPlugin.parseConfig('all'),
 };
 
-const validOneDeviceNoSession = {
-  "configurations": {
-    "ios.sim.release": {
-      "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
-      "type": "ios.simulator",
-      "name": "iPhone 7 Plus, iOS 10.2"
-    }
-  }
-};
-
-const validOneIosNoneDeviceNoSession = {
-  "configurations": {
-    "ios.none": {
-      "type": "ios.none",
-      "name": "iPhone 7 Plus, iOS 10.2"
-    }
-  }
-};
-
-const validTwoDevicesNoSession = {
-  "configurations": {
-    "ios.sim.release": {
-      "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
-      "type": "ios.simulator",
-      "name": "iPhone 7 Plus, iOS 10.2"
-    },
-    "ios.sim.debug": {
-      "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/example.app",
-      "type": "ios.simulator",
-      "name": "iPhone 7 Plus, iOS 10.2"
-    }
-  }
-};
-
 const invalidDeviceNoBinary = {
   "configurations": {
     "ios.sim.release": {
@@ -93,29 +59,6 @@ const invalidDeviceNoBinary = {
   }
 };
 
-const invalidNoDevice = {
-  "configurations": {
-  }
-};
-
-const invalidDeviceNoDeviceType = {
-  "configurations": {
-    "ios.sim.release": {
-      "binaryPath": "here",
-      "name": "iPhone 7 Plus, iOS 10.2"
-    }
-  }
-};
-
-const invalidDeviceNoDeviceName = {
-  "configurations": {
-    "ios.sim.release": {
-      "binaryPath": "here",
-      "type": "ios.simulator"
-    }
-  }
-};
-
 const validOneDeviceAndSession = {
   "session": {
     "server": "ws://localhost:8099",
@@ -150,125 +93,13 @@ const pathsTests = {
   }
 };
 
-const invalidSessionNoSessionId = {
-  "session": {
-    "server": "ws://localhost:8099"
-  }
-};
-
-const invalidSessionNoServer = {
-  "session": {
-    "sessionId": "test"
-  }
-};
-
-const invalidOneDeviceTypeEmulatorNoSession = {
-  "configurations": {
-    "ios.sim.release": {
-      "binaryPath": "some.apk",
-      "type": "emulator",
-      "name": "an emulator"
-    }
-  }
-};
-
-const sessionPerConfiguration = {
-  "configurations": {
-    "ios.sim.none": {
-      "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
-      "type": "ios.none",
-      "name": "iPhone 7 Plus, iOS 10.2",
-      "session": {
-        "server": "ws://localhost:1111",
-        "sessionId": "test_1111"
-      }
-    },
-    "ios.sim.release": {
-      "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
-      "type": "ios.none",
-      "name": "iPhone 7 Plus, iOS 10.2",
-      "session": {
-        "server": "ws://localhost:2222",
-        "sessionId": "test_2222"
-      }
-    }
-  }
-};
-
-const sessionInCommonAndInConfiguration = {
-  "session": {
-    "server": "ws://localhost:1111",
-    "sessionId": "test_1"
-  },
-  "configurations": {
-    "ios.sim.none": {
-      "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
-      "type": "ios.none",
-      "name": "iPhone 7 Plus, iOS 10.2",
-      "session": {
-        "server": "ws://localhost:2222",
-        "sessionId": "test_2"
-      }
-    }
-  }
-};
-
-const validOneEmulator = {
-  "configurations": {
-    "android.emu.release": {
-      "binaryPath": "android/app/build/outputs/apk/app-debug.apk",
-      "type": "android.emulator",
-      "name": "Nexus 5X"
-    }
-  }
-};
-
-const deviceObjectSimulator = {
-  "configurations": {
-    "ios.sim.release": {
-      "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
-      "type": "ios.simulator",
-      "device": {
-        "name": "iPhone 7 Plus",
-        "os": "iOS 10.2"
-      }
-    }
-  }
-};
-
-const deviceObjectEmulator = {
-  "configurations": {
-    "android.emu.release": {
-      "binaryPath": "android/app/build/outputs/apk/app-debug.apk",
-      "type": "android.emulator",
-      "device": {
-        "avdName": "Nexus 5X"
-      }
-    }
-  }
-};
-
 module.exports = {
   allArtifactsConfiguration,
   defaultArtifactsConfiguration,
   pluginsAllResolved,
   pluginsDefaultsResolved,
   pluginsFailingResolved,
-  validOneDeviceNoSession,
-  validOneIosNoneDeviceNoSession,
-  validTwoDevicesNoSession,
   invalidDeviceNoBinary,
-  invalidNoDevice,
-  invalidDeviceNoDeviceType,
-  invalidDeviceNoDeviceName,
   validOneDeviceAndSession,
-  invalidSessionNoSessionId,
-  invalidSessionNoServer,
-  invalidOneDeviceTypeEmulatorNoSession,
-  sessionPerConfiguration,
-  sessionInCommonAndInConfiguration,
-  validOneEmulator,
   pathsTests,
-  deviceObjectEmulator,
-  deviceObjectSimulator,
 };
diff --git a/detox/src/devices/Device.js b/detox/src/devices/Device.js
index 288131d2d..66e3944a0 100644
--- a/detox/src/devices/Device.js
+++ b/detox/src/devices/Device.js
@@ -1,8 +1,5 @@
 const _ = require('lodash');
-const fs = require('fs');
-const path = require('path');
-const argparse = require('../utils/argparse');
-const debug = require('../utils/debug'); //debug utils, leave here even if unused
+const debug = require('../utils/debug'); // debug utils, leave here even if unused
 
 class Device {
   constructor({ deviceConfig, deviceDriver, emitter, sessionConfig }) {
@@ -15,22 +12,11 @@ class Device {
     this.debug = debug;
   }
 
-  async prepare(params = {}) {
-    this._binaryPath = this._getAbsolutePath(this._deviceConfig.binaryPath);
-    this._testBinaryPath = this._deviceConfig.testBinaryPath ? this._getAbsolutePath(this._deviceConfig.testBinaryPath) : null;
+  async prepare() {
     this._deviceId = await this.deviceDriver.acquireFreeDevice(this._deviceConfig.device || this._deviceConfig.name);
-    this._bundleId = await this.deviceDriver.getBundleIdFromBinary(this._binaryPath);
+    this._bundleId = await this.deviceDriver.getBundleIdFromBinary(this._deviceConfig.binaryPath);
 
     await this.deviceDriver.prepare();
-
-    if (!argparse.getArgValue('reuse') && !params.reuse) {
-      await this.deviceDriver.uninstallApp(this._deviceId, this._bundleId);
-      await this.deviceDriver.installApp(this._deviceId, this._binaryPath, this._testBinaryPath);
-    }
-
-    if (params.launchApp) {
-      await this.launchApp({newInstance: true});
-    }
   }
 
   createPayloadFileAndUpdatesParamsObject(key, launchKey, params, baseLaunchArgs) {
@@ -184,8 +170,8 @@ class Device {
   }
 
   async installApp(binaryPath, testBinaryPath) {
-    const _binaryPath = binaryPath || this._binaryPath;
-    const _testBinaryPath = testBinaryPath || this._testBinaryPath;
+    const _binaryPath = binaryPath || this._deviceConfig.binaryPath;
+    const _testBinaryPath = testBinaryPath || this._deviceConfig.testBinaryPath;
     await this.deviceDriver.installApp(this._deviceId, _binaryPath, _testBinaryPath);
   }
 
@@ -270,12 +256,6 @@ class Device {
 
   async _cleanup() {
     await this.deviceDriver.cleanup(this._deviceId, this._bundleId);
-
-    if (argparse.getArgValue('cleanup') && this._deviceId) {
-      await this.deviceDriver.shutdown(this._deviceId);
-    }
-
-    this.deviceDriver = null;
   }
 
   async pressBack() {
@@ -306,19 +286,6 @@ class Device {
     return launchArgs;
   }
 
-  _getAbsolutePath(appPath) {
-    if (path.isAbsolute(appPath)) {
-      return appPath;
-    }
-
-    const absPath = path.join(process.cwd(), appPath);
-    if (fs.existsSync(absPath)) {
-      return absPath;
-    } else {
-      throw new Error(`app binary not found at '${absPath}', did you build it?`);
-    }
-  }
-
   async _terminateApp() {
     await this.deviceDriver.terminate(this._deviceId, this._bundleId);
     this._processes[this._bundleId] = undefined;
@@ -326,7 +293,7 @@ class Device {
 
   async _reinstallApp() {
     await this.deviceDriver.uninstallApp(this._deviceId, this._bundleId);
-    await this.deviceDriver.installApp(this._deviceId, this._binaryPath, this._testBinaryPath);
+    await this.deviceDriver.installApp(this._deviceId, this._deviceConfig.binaryPath, this._deviceConfig.testBinaryPath);
   }
 }
 
diff --git a/detox/src/devices/DriverRegistry.js b/detox/src/devices/DriverRegistry.js
new file mode 100644
index 000000000..756d91761
--- /dev/null
+++ b/detox/src/devices/DriverRegistry.js
@@ -0,0 +1,34 @@
+const resolveModuleFromPath = require('../utils/resolveModuleFromPath');
+
+class DriverRegistry {
+  constructor(deviceClasses) {
+    this.deviceClasses = deviceClasses;
+  }
+
+  resolve(deviceType, opts) {
+    let DeviceDriverClass = this.deviceClasses[deviceType];
+
+    if (!DeviceDriverClass) {
+      try {
+        DeviceDriverClass = resolveModuleFromPath(deviceType);
+      } catch (e) {
+        // noop, if we don't find a module to require, we'll hit the unsupported error below
+      }
+    }
+
+    if (!DeviceDriverClass) {
+      throw new Error(`'${deviceType}' is not supported`);
+    }
+
+    return new DeviceDriverClass(opts);
+  }
+}
+
+DriverRegistry.default = new DriverRegistry({
+  'ios.none': require('./drivers/ios/IosDriver'),
+  'ios.simulator': require('./drivers/ios/SimulatorDriver'),
+  'android.emulator': require('./drivers/android/EmulatorDriver'),
+  'android.attached': require('./drivers/android/AttachedAndroidDriver'),
+});
+
+module.exports = DriverRegistry;
\ No newline at end of file
diff --git a/detox/src/devices/__mocks__/Device.js b/detox/src/devices/__mocks__/Device.js
new file mode 100644
index 000000000..ffd794cf6
--- /dev/null
+++ b/detox/src/devices/__mocks__/Device.js
@@ -0,0 +1,17 @@
+const Deferred = require('../../utils/Deferred');
+const FakeDevice = jest.genMockFromModule('../Device');
+
+FakeDevice.setInfiniteMethod = (methodName) => {
+  FakeDevice.mockImplementationOnce(() => {
+    const device = new FakeDevice();
+    device.deferred = new Deferred();
+    device[methodName].mockReturnValue(device.deferred.promise);
+    device._cleanup.mockImplementation(() => {
+      device.deferred.reject(`Fake error: aborted device.${methodName}()`);
+    });
+
+    return device;
+  });
+};
+
+module.exports = FakeDevice;
diff --git a/detox/src/devices/__mocks__/DriverRegistry.js b/detox/src/devices/__mocks__/DriverRegistry.js
new file mode 100644
index 000000000..da39039a1
--- /dev/null
+++ b/detox/src/devices/__mocks__/DriverRegistry.js
@@ -0,0 +1,16 @@
+const DriverRegistry = jest.requireActual('../DriverRegistry');
+const FakeDriver = jest.requireActual('../drivers/__mocks__/FakeDriver');
+
+class FakeDriverRegistry extends DriverRegistry {
+  constructor(...args) {
+    super(...args);
+    jest.spyOn(this, 'resolve');
+  }
+}
+
+FakeDriverRegistry.FakeDriver = FakeDriver;
+FakeDriverRegistry.default = new FakeDriverRegistry({
+  'fake.device': FakeDriver,
+});
+
+module.exports = FakeDriverRegistry;
\ No newline at end of file
diff --git a/detox/src/devices/drivers/__mocks__/FakeDriver.js b/detox/src/devices/drivers/__mocks__/FakeDriver.js
new file mode 100644
index 000000000..7fc50af3e
--- /dev/null
+++ b/detox/src/devices/drivers/__mocks__/FakeDriver.js
@@ -0,0 +1,19 @@
+const DeviceDriverBase = require('../DeviceDriverBase');
+
+class FakeDriver extends DeviceDriverBase {
+  constructor(...args) {
+    super(...args);
+
+    this.constructorArgs = args;
+    this.matchers = FakeDriver.matchers;
+  }
+
+  declareArtifactPlugins() {
+    return FakeDriver.artifactsPlugins;
+  }
+}
+
+FakeDriver.matchers = {};
+FakeDriver.artifactsPlugins = {};
+
+module.exports = FakeDriver;
diff --git a/detox/src/devices/drivers/android/AndroidDriver.js b/detox/src/devices/drivers/android/AndroidDriver.js
index 7e07e1bfd..a3b8909c6 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.js
@@ -9,6 +9,7 @@ const InvocationManager = invoke.InvocationManager;
 const ADB = require('./tools/ADB');
 const AAPT = require('./tools/AAPT');
 const APKPath = require('./tools/APKPath');
+const AppUninstallHelper = require('./tools/AppUninstallHelper');
 const DeviceDriverBase = require('../DeviceDriverBase');
 const DetoxApi = require('../../../android/espressoapi/Detox');
 const EspressoDetoxApi = require('../../../android/espressoapi/EspressoDetox');
@@ -24,6 +25,7 @@ const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
 const sleep = require('../../../utils/sleep');
 const retry = require('../../../utils/retry');
 const { interruptProcess, spawnAndLog } = require('../../../utils/exec');
+const getAbsoluteBinaryPath = require('../../../utils/getAbsoluteBinaryPath');
 const AndroidExpect = require('../../../android/expect');
 const { InstrumentationLogsParser } = require('./InstrumentationLogsParser');
 
@@ -63,39 +65,23 @@ class AndroidDriver extends DeviceDriverBase {
   }
 
   async getBundleIdFromBinary(apkPath) {
-    return await this.aapt.getPackageName(apkPath);
+    const binaryPath = getAbsoluteBinaryPath(apkPath);
+    return await this.aapt.getPackageName(binaryPath);
   }
 
-  async installApp(deviceId, binaryPath, testBinaryPath) {
+  async installApp(deviceId, _binaryPath, _testBinaryPath) {
+    const {
+      binaryPath,
+      testBinaryPath,
+    } = this._getInstallPaths(_binaryPath, _testBinaryPath);
     await this.adb.install(deviceId, binaryPath);
-    await this.adb.install(deviceId, testBinaryPath ? testBinaryPath : this.getTestApkPath(binaryPath));
-  }
-
-  async pressBack(deviceId) {
-    await this.uiDevice.pressBack();
-  }
-
-  getTestApkPath(originalApkPath) {
-    const testApkPath = APKPath.getTestApkPath(originalApkPath);
-
-    if (!fs.existsSync(testApkPath)) {
-      throw new Error(`'${testApkPath}' could not be found, did you run './gradlew assembleAndroidTest' ?`);
-    }
-
-    return testApkPath;
+    await this.adb.install(deviceId, testBinaryPath);
   }
 
   async uninstallApp(deviceId, bundleId) {
     await this.emitter.emit('beforeUninstallApp', { deviceId, bundleId });
-
-    if (await this.adb.isPackageInstalled(deviceId, bundleId)) {
-      await this.adb.uninstall(deviceId, bundleId);
-    }
-
-    const testBundle = `${bundleId}.test`;
-    if (await this.adb.isPackageInstalled(deviceId, testBundle)) {
-      await this.adb.uninstall(deviceId, testBundle);
-    }
+    const uninstallHelper = new AppUninstallHelper(this.adb);
+    await uninstallHelper.uninstall(deviceId, bundleId);
   }
 
   async launchApp(deviceId, bundleId, launchArgs, languageAndLocale) {
@@ -148,6 +134,10 @@ class AndroidDriver extends DeviceDriverBase {
       }
   }
 
+  async pressBack(deviceId) {
+    await this.uiDevice.pressBack();
+  }
+
   async sendToHome(deviceId, params) {
     await this.uiDevice.pressHome();
   }
@@ -250,6 +240,24 @@ class AndroidDriver extends DeviceDriverBase {
     await this.invocationManager.execute(call);
   }
 
+  _getInstallPaths(_binaryPath, _testBinaryPath) {
+    const binaryPath = getAbsoluteBinaryPath(_binaryPath);
+    const testBinaryPath = _testBinaryPath ? getAbsoluteBinaryPath(_testBinaryPath) : this._getTestApkPath(binaryPath);
+    return {
+      binaryPath,
+      testBinaryPath,
+    };
+  }
+
+  _getTestApkPath(originalApkPath) {
+    const testApkPath = APKPath.getTestApkPath(originalApkPath);
+
+    if (!fs.existsSync(testApkPath)) {
+      throw new Error(`'${testApkPath}' could not be found, did you run './gradlew assembleAndroidTest'?`);
+    }
+    return testApkPath;
+  }
+
   async _launchInstrumentationProcess(deviceId, bundleId, rawLaunchArgs) {
     const launchArgs = this._prepareLaunchArgs(rawLaunchArgs, true);
     const additionalLaunchArgs = this._prepareLaunchArgs({debug: false});
diff --git a/detox/src/devices/drivers/android/EmulatorDriver.js b/detox/src/devices/drivers/android/EmulatorDriver.js
index 24c022969..debaf43d0 100644
--- a/detox/src/devices/drivers/android/EmulatorDriver.js
+++ b/detox/src/devices/drivers/android/EmulatorDriver.js
@@ -6,14 +6,16 @@ const AndroidDriver = require('./AndroidDriver');
 const FreeEmulatorFinder = require('./emulator/FreeEmulatorFinder');
 const AVDValidator = require('./emulator/AVDValidator');
 const EmulatorLauncher = require('./emulator/EmulatorLauncher');
+const EmulatorVersionResolver = require('./emulator/EmulatorVersionResolver');
 const { EmulatorExec } = require('./tools/EmulatorExec');
 const EmulatorTelnet = require('./tools/EmulatorTelnet');
-const EmulatorVersionResolver = require('./emulator/EmulatorVersionResolver');
+const AppInstallHelper = require('./tools/AppInstallHelper');
 const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
 const DeviceRegistry = require('../../DeviceRegistry');
 const environment = require('../../../utils/environment');
 const retry = require('../../../utils/retry');
 const log = require('../../../utils/logger').child({ __filename });
+const argparse = require('../../../utils/argparse');
 
 const DetoxEmulatorsPortRange = {
   min: 10000,
@@ -61,6 +63,20 @@ class EmulatorDriver extends AndroidDriver {
     return adbName;
   }
 
+  async installApp(deviceId, _binaryPath, _testBinaryPath) {
+    if (argparse.getArgValue('force-adb-install') === 'true') {
+      return await super.installApp(deviceId, _binaryPath, _testBinaryPath);
+    }
+
+    const {
+      binaryPath,
+      testBinaryPath,
+    } = this._getInstallPaths(_binaryPath, _testBinaryPath);
+
+    const installHelper = new AppInstallHelper(this.adb);
+    await installHelper.install(deviceId, binaryPath, testBinaryPath);
+  }
+
   async cleanup(adbName, bundleId) {
     await this.deviceRegistry.disposeDevice(adbName);
     await super.cleanup(adbName, bundleId);
diff --git a/detox/src/devices/drivers/android/tools/AAPT.js b/detox/src/devices/drivers/android/tools/AAPT.js
index 39b973139..072022058 100644
--- a/detox/src/devices/drivers/android/tools/AAPT.js
+++ b/detox/src/devices/drivers/android/tools/AAPT.js
@@ -1,10 +1,7 @@
-const _ = require('lodash');
 const {getAaptPath} = require('../../../../utils/environment');
-const path = require('path');
 const exec = require('../../../../utils/exec').execWithRetriesAndLogs;
 const escape = require('../../../../utils/pipeCommands').escape.inQuotedString;
 const egrep = require('../../../../utils/pipeCommands').search.fragment;
-const fsext = require('../../../../utils/fsext');
 
 class AAPT {
   constructor() {
diff --git a/detox/src/devices/drivers/android/tools/ADB.js b/detox/src/devices/drivers/android/tools/ADB.js
index 0b38d6081..fbfda0ed3 100644
--- a/detox/src/devices/drivers/android/tools/ADB.js
+++ b/detox/src/devices/drivers/android/tools/ADB.js
@@ -104,6 +104,10 @@ class ADB {
     }
   }
 
+  /*async*/ remoteInstall(deviceId, path) {
+    return this.shell(deviceId, `pm install -r -g -t ${path}`);
+  }
+
   async uninstall(deviceId, appId) {
     await this.adbCmd(deviceId, `uninstall ${appId}`);
   }
@@ -216,6 +220,10 @@ class ADB {
     return this.spawn(deviceId, ['shell', shellCommand]);
   }
 
+  async push(deviceId, src, dst) {
+    await this.adbCmd(deviceId, `push "${src}" "${dst}"`);
+  }
+
   async pull(deviceId, src, dst = '') {
     await this.adbCmd(deviceId, `pull "${src}" "${dst}"`);
   }
diff --git a/detox/src/devices/drivers/android/tools/AppInstallHelper.js b/detox/src/devices/drivers/android/tools/AppInstallHelper.js
new file mode 100644
index 000000000..5e97e1788
--- /dev/null
+++ b/detox/src/devices/drivers/android/tools/AppInstallHelper.js
@@ -0,0 +1,36 @@
+const EMU_TEMP_PATH = '/data/local/tmp';
+const EMU_TEMP_INSTALL_PATH = `${EMU_TEMP_PATH}/detox`;
+
+class AppInstallHelper {
+  constructor(adb, tempDir = EMU_TEMP_INSTALL_PATH) {
+    this._adb = adb;
+    this._tempDir = tempDir;
+  }
+
+  async install(deviceId, appBinaryPath, testBinaryPath) {
+    await this._prepareTempDirOnDevice(deviceId);
+
+    const appBinaryPathOnTarget = this._getTargetBinaryPath(false);
+    await this._pushAndInstallBinary(deviceId, appBinaryPath, appBinaryPathOnTarget);
+
+    const testBinaryPathOnTarget = this._getTargetBinaryPath(true);
+    await this._pushAndInstallBinary(deviceId, testBinaryPath, testBinaryPathOnTarget);
+  }
+
+  async _prepareTempDirOnDevice(deviceId) {
+    await this._adb.shell(deviceId, `rm -fr ${this._tempDir}`);
+    await this._adb.shell(deviceId, `mkdir -p ${this._tempDir}`);
+  }
+
+  async _pushAndInstallBinary(deviceId, binaryPath, binaryPathOnTarget) {
+    await this._adb.push(deviceId, binaryPath, binaryPathOnTarget);
+    await this._adb.remoteInstall(deviceId, binaryPathOnTarget);
+  }
+
+  _getTargetBinaryPath(isTestBinary) {
+    const filename = isTestBinary ? 'Test.apk' : 'Application.apk';
+    return `${this._tempDir}/${filename}`;
+  }
+}
+
+module.exports = AppInstallHelper;
diff --git a/detox/src/devices/drivers/android/tools/AppUninstallHelper.js b/detox/src/devices/drivers/android/tools/AppUninstallHelper.js
new file mode 100644
index 000000000..345885899
--- /dev/null
+++ b/detox/src/devices/drivers/android/tools/AppUninstallHelper.js
@@ -0,0 +1,18 @@
+class AppUninstallHelper {
+  constructor(adb) {
+    this._adb = adb;
+  }
+
+  async uninstall(deviceId, bundleId) {
+    if (await this._adb.isPackageInstalled(deviceId, bundleId)) {
+      await this._adb.uninstall(deviceId, bundleId);
+    }
+
+    const testBundleId = `${bundleId}.test`;
+    if (await this._adb.isPackageInstalled(deviceId, testBundleId)) {
+      await this._adb.uninstall(deviceId, testBundleId);
+    }
+  }
+}
+
+module.exports = AppUninstallHelper;
diff --git a/detox/src/devices/drivers/ios/SimulatorDriver.js b/detox/src/devices/drivers/ios/SimulatorDriver.js
index 36e135d56..c379a68f2 100644
--- a/detox/src/devices/drivers/ios/SimulatorDriver.js
+++ b/detox/src/devices/drivers/ios/SimulatorDriver.js
@@ -9,6 +9,7 @@ const configuration = require('../../../configuration');
 const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
 const environment = require('../../../utils/environment');
 const argparse = require('../../../utils/argparse');
+const getAbsoluteBinaryPath = require('../../../utils/getAbsoluteBinaryPath');
 
 class SimulatorDriver extends IosDriver {
 
@@ -56,6 +57,7 @@ class SimulatorDriver extends IosDriver {
   }
 
   async getBundleIdFromBinary(appPath) {
+    appPath = getAbsoluteBinaryPath(appPath);
     try {
       const result = await exec(`/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "${path.join(appPath, 'Info.plist')}"`);
       const bundleId = _.trim(result.stdout);
@@ -75,7 +77,7 @@ class SimulatorDriver extends IosDriver {
   }
 
   async installApp(deviceId, binaryPath) {
-    await this.applesimutils.install(deviceId, binaryPath);
+    await this.applesimutils.install(deviceId, getAbsoluteBinaryPath(binaryPath));
   }
 
   async uninstallApp(deviceId, bundleId) {
diff --git a/detox/src/server/DetoxServer.js b/detox/src/server/DetoxServer.js
index ea7d23366..d7bb9d4ef 100644
--- a/detox/src/server/DetoxServer.js
+++ b/detox/src/server/DetoxServer.js
@@ -1,5 +1,7 @@
 const _ = require('lodash');
 const WebSocket = require('ws');
+const logger = require('../utils/logger').child({ __filename });
+
 const WebSocketServer = WebSocket.Server;
 
 const CLOSE_TIMEOUT = 10000;
@@ -7,12 +9,11 @@ const ROLE_TESTER = 'tester';
 const ROLE_TESTEE = 'testee';
 
 class DetoxServer {
-  constructor({ port, log, standalone = false }) {
+  constructor({ port, standalone = false }) {
     this.wss = new WebSocketServer({ port });
     this.sessions = {};
     this.standalone = standalone;
-    this.log = log.child({ __filename });
-    this.log.info(`server listening on localhost:${this.wss.options.port}...`);
+    logger.info(`server listening on localhost:${this.wss.options.port}...`);
     this._setup();
   }
 
@@ -30,28 +31,28 @@ class DetoxServer {
             if (action.params && action.params.sessionId && action.params.role) {
               sessionId = action.params.sessionId;
               role = action.params.role;
-              this.log.debug({ event: 'LOGIN' }, `role=${role}, sessionId=${sessionId}`);
+              logger.debug({ event: 'LOGIN' }, `role=${role}, sessionId=${sessionId}`);
               _.set(this.sessions, [sessionId, role], ws);
               action.type = 'loginSuccess';
               this.sendAction(ws, action);
-              this.log.debug({ event: 'LOGIN_SUCCESS' }, `role=${role}, sessionId=${sessionId}`);
+              logger.debug({ event: 'LOGIN_SUCCESS' }, `role=${role}, sessionId=${sessionId}`);
             }
           } else if (sessionId && role) {
-            this.log.trace({ event: 'MESSAGE', action: action.type }, `role=${role} action=${action.type} (sessionId=${sessionId})`);
+            logger.trace({ event: 'MESSAGE', action: action.type }, `role=${role} action=${action.type} (sessionId=${sessionId})`);
             this.sendToOtherRole(sessionId, role, action);
           }
         } catch (err) {
-          this.log.debug({ event: 'ERROR', err }, `Invalid JSON received, cannot parse`, err);
+          logger.debug({ event: 'ERROR', err }, `Invalid JSON received, cannot parse`, err);
         }
       });
 
       ws.on('error', (e) => {
-        this.log.warn({ event: 'WEBSOCKET_ERROR', role, sessionId }, `${e && e.message} (role=${role}, session=${sessionId})`);
+        logger.warn({ event: 'WEBSOCKET_ERROR', role, sessionId }, `${e && e.message} (role=${role}, session=${sessionId})`);
       });
 
       ws.on('close', () => {
         if (sessionId && role) {
-          this.log.debug({ event: 'DISCONNECT' }, `role=${role}, sessionId=${sessionId}`);
+          logger.debug({ event: 'DISCONNECT' }, `role=${role}, sessionId=${sessionId}`);
 
           if (role === ROLE_TESTEE) {
             this.sendToOtherRole(sessionId, role, { type: 'testeeDisconnected', messageId: -0xc1ea });
@@ -77,7 +78,7 @@ class DetoxServer {
     if (ws && ws.readyState === WebSocket.OPEN) {
       this.sendAction(ws, action);
     } else {
-      this.log.debug({ event: 'CANNOT_FORWARD' }, `role=${otherRole} not connected, cannot fw action (sessionId=${sessionId})`);
+      logger.debug({ event: 'CANNOT_FORWARD' }, `role=${otherRole} not connected, cannot fw action (sessionId=${sessionId})`);
 
       if (role === ROLE_TESTER && action.type === 'cleanup') {
         this.sendToOtherRole(sessionId, otherRole, {
@@ -95,12 +96,12 @@ class DetoxServer {
   _closeWithTimeout() {
     return new Promise((resolve) => {
       const handle = setTimeout(() => {
-        this.log.warn({ event: 'TIMEOUT' }, 'Detox server closed ungracefully on a timeout!!!');
+        logger.warn({ event: 'TIMEOUT' }, 'Detox server closed ungracefully on a timeout!!!');
         resolve();
       }, CLOSE_TIMEOUT);
 
       this.wss.close(() => {
-        this.log.debug({ event: 'WS_CLOSE' }, 'Detox server connections terminated gracefully');
+        logger.debug({ event: 'WS_CLOSE' }, 'Detox server connections terminated gracefully');
         clearTimeout(handle);
         resolve();
       });
diff --git a/detox/src/utils/customConsoleLogger.js b/detox/src/utils/customConsoleLogger.js
index a7be782bb..94f862939 100644
--- a/detox/src/utils/customConsoleLogger.js
+++ b/detox/src/utils/customConsoleLogger.js
@@ -11,8 +11,11 @@ function getStackDump() {
 
 function getOrigin() {
   const userCallsite = callsites()[USER_STACK_FRAME_INDEX];
-  const filename = path.relative(process.cwd(), userCallsite.getFileName());
-  return `at ${filename}:${userCallsite.getLineNumber() || '?'}:${userCallsite.getColumnNumber() || '?'}`;
+  const callsiteFilename = userCallsite && userCallsite.getFileName();
+  const callsiteLine = userCallsite && userCallsite.getLineNumber();
+  const callsiteCol = userCallsite && userCallsite.getColumnNumber();
+  const filename = callsiteFilename ? path.relative(process.cwd(), callsiteFilename) : '<unknown>';
+  return `at ${filename}:${callsiteLine || '?'}:${callsiteCol || '?'}`;
 }
 
 function override(consoleLevel, bunyanFn) {
diff --git a/detox/src/utils/getAbsoluteBinaryPath.js b/detox/src/utils/getAbsoluteBinaryPath.js
new file mode 100644
index 000000000..b5d32daf9
--- /dev/null
+++ b/detox/src/utils/getAbsoluteBinaryPath.js
@@ -0,0 +1,17 @@
+const fs = require('fs');
+const path = require('path');
+
+function getAbsoluteBinaryPath(appPath) {
+  if (path.isAbsolute(appPath)) {
+    return appPath;
+  }
+
+  const absPath = path.join(process.cwd(), appPath);
+  if (fs.existsSync(absPath)) {
+    return absPath;
+  } else {
+    throw new Error(`app binary not found at '${absPath}', did you build it?`);
+  }
+}
+
+module.exports = getAbsoluteBinaryPath;
diff --git a/detox/src/utils/resolveModuleFromPath.js b/detox/src/utils/resolveModuleFromPath.js
new file mode 100644
index 000000000..7485530ec
--- /dev/null
+++ b/detox/src/utils/resolveModuleFromPath.js
@@ -0,0 +1,6 @@
+function resolveModuleFromPath(modulePath) {
+  const resolvedModulePath = require.resolve(modulePath, { paths: [process.cwd()]});
+  return require(resolvedModulePath);
+}
+
+module.exports = resolveModuleFromPath;
diff --git a/docs/APIRef.Configuration.md b/docs/APIRef.Configuration.md
index 736461710..459b82d17 100644
--- a/docs/APIRef.Configuration.md
+++ b/docs/APIRef.Configuration.md
@@ -116,6 +116,54 @@ Below you can see mappings between the string presets and the corresponding obje
 | failing | `{ "enabled": true, "keepOnlyFailedTestsArtifacts": true } ` |
 | manual  | `{ "enabled": true, "shouldTakeAutomaticSnapshots": false }` |
 
+### Behavior Configuration
+
+If you need to tweak the flow of `detox.init()` or `detox.cleanup()` steps,
+you have a few options to change. These are the default behavior values:
+
+```json
+{
+  "detox": {
+    "behavior": {
+      "init": {
+        "reinstallApp": true,
+        "launchApp": true,
+        "exposeGlobals": true
+      },
+      "cleanup": {
+        "shutdownDevice": false
+      }
+    }
+  }
+}
+```
+
+For example, if you want to launch your tested app manually, e.g. via `device.launchApp()`,
+you should specify in `package.json`:
+
+```json
+{
+  "detox": {
+    "behavior": {
+      "init": {
+        "launchApp": false
+      }
+    }
+  }
+}
+```
+
+Setting `reinstallApp: false` will make the tests reuse the currently installed app on the device,
+provided you have installed it beforehand.
+
+If you do not wish to leak Detox globals (`expect`, `device`, `by`, etc.) to the global
+scope, you can set `"exposeGlobals": false` and destructure them respectively from the
+exported Detox interface:
+
+```js
+const { by, device, expect, element } = require('detox');
+```
+
 ### Server Configuration
 
 Detox can either initialize a server using a generated configuration, or can be overriden with a manual  configuration:
@@ -174,8 +222,6 @@ Session can also be set per configuration:
 
 `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.
-
 ## detox-cli
 
 ### Build Configuration
diff --git a/docs/APIRef.DetoxCLI.md b/docs/APIRef.DetoxCLI.md
index aba0bebbe..2532c52ff 100644
--- a/docs/APIRef.DetoxCLI.md
+++ b/docs/APIRef.DetoxCLI.md
@@ -77,13 +77,14 @@ Initiating your test suite. <sup>[[1]](#notice-passthrough)</sup>
 | --record-timeline [all/none] | [Jest Only] Record tests and events timeline, for visual display on the [chrome://tracing](chrome://tracing) tool. The default value is **none**. |
 | -r, --reuse                                   | Reuse existing installed app (do not delete + reinstall) for a faster run. |
 | -u, --cleanup                                 | Shutdown simulator when test is over, useful for CI scripts, to make sure detox exists cleanly with no residue |
-| -w, --workers                                 | [iOS Only] Specifies number of workers the test runner should spawn, requires a test runner with parallel execution support (Detox CLI currently supports Jest). *Note: For workers > 1, Jest's spec-level reporting is disabled, by default (can be overridden using --jest-report-specs).* |
+| -w, --workers                                 | Specifies number of workers the test runner should spawn, requires a test runner with parallel execution support (Detox CLI currently supports Jest). *Note: For workers > 1, Jest's spec-level reporting is disabled, by default (can be overridden using --jest-report-specs).* |
 | --jest-report-specs | [Jest Only] Whether to output logs per each running spec, in real-time. By default, disabled with multiple workers. |
 | -H, --headless                                | [Android Only] Launch Emulator in headless mode. Useful when running on CI. |
 | --gpu                                         | [Android Only] Launch Emulator with the specific -gpu [gpu mode] parameter. |
 | --device-launch-args | A list of passthrough-arguments to use when (if) devices (Android emulator / iOS simulator) are launched by Detox.<br />**Note: the value must be specified after an equal size (`=`) and inside quotes.** Usage example:<br />`--device-launch-args="-http-proxy http://1.1.1.1:8000 -no-snapshot-load"` |
 | --no-color                                    | Disable colors in log output |
 | --use-custom-logger | Use Detox' custom console-logging implementation, for logging Detox (non-device) logs. Disabling will fallback to node.js / test-runner's implementation (e.g. Jest / Mocha).<br />*Default: true* |
+| --force-adb-install | Due to problems with the `adb install` command on Android, Detox resorts to a different scheme for install APK's. Setting true will disable that and force usage of `adb install`, instead.<br/>This flag is temporary until the Detox way proves stable.<br/>*Default: false* |
 | --help                                        | Show help |
 
 ##### Notices
diff --git a/docs/Guide.ThirdPartyDrivers.md b/docs/Guide.ThirdPartyDrivers.md
new file mode 100644
index 000000000..3a90197a5
--- /dev/null
+++ b/docs/Guide.ThirdPartyDrivers.md
@@ -0,0 +1,62 @@
+# Third Party Drivers
+
+Detox comes with built-in support for running on Android and iOS by choosing a driver type in your detox configurations. For example, the following configuration uses the "ios.simulator" driver.
+
+```
+"ios.sim": {
+  "binaryPath": "bin/YourApp.app",
+  "type": "ios.simulator",
+}
+```
+
+While React Native officially supports Android and iOS, other platforms such as
+[Web](https://github.com/necolas/react-native-web) and [Windows](https://github.com/microsoft/react-native-windows)
+can be targeted. If your app targets a third party platform, you may which to use a [third party driver](#How-to-use-a-driver) to run your tests on said platform as of Detox v16.3.0. If one doesn't already exist you may want to [write your own](#Writing-a-new-driver)
+
+## How to use a driver
+
+Check to see if a [third party driver](#Existing-Third-party-drivers) already exists for the platform you wish to target. Mostly likely, the driver will have setup instructions. Overall the setup for any third party driver is fairly simple.
+
+1. Add the driver to your `package.json` withh `npm install --save-dev detox-driver-package` or `yarn add --dev detox-driver-package`
+1. Add a new detox configuration to your existing configurations with the `type` set to driver's package name.
+```
+"thirdparty.driver.config": {
+  "binaryPath": "bin/YourApp.app",
+  "type": "detox-driver-package",
+}
+```
+3. Run detox while specifying the name of your new configuration `detox test --configuration detox-driver-package`
+
+## Writing a new driver
+
+### Anatomy of a driver
+
+The architecture of a driver is split into a few different pieces. Understanding the [overall architecture of Detox](https://github.com/wix/Detox/blob/master/docs/Introduction.HowDetoxWorks.md#architecture) will help with this section
+
+1. The Device Driver - code runs on the Detox tester, within the test runner context. It implements the details for the
+[`device` object](https://github.com/wix/Detox/blob/master/docs/APIRef.DeviceObjectAPI.md) that is exposed in your detox tests. The implementation is responsible for managing device clients your tests will run on.
+1. Matchers - code powering the `expect` `element` `waitFor` and `by` globals in your tests.
+These helpers serialize your test code so they can be sent over the network to the device on which your tests are running.
+1. Driver Client - code running on the device being tested. The driver client communicates with the server over
+websocket where it receives information from the serialized matchers, and expectations, and also sends responses
+back of whether each step of your test succeeds or fails. Typically a device client will use an underlying library specific
+to the platform at hand to implement the expectations.
+
+### Implementation details
+
+You may want to read through the source of both the built-in, official drivers as well as
+existing third party drivers to get a sense of how the code is structured. You can also look at
+`examples/demo-plugin/driver.js` for a minimal driver implementation that doesn't really do anything
+useful. Your driver should extend `DeviceDriverBase` and export as `module.exports`.
+
+```
+const DeviceDriverBase = require('detox/src/devices/drivers/DeviceDriverBase');
+class MyNewDriver extends DeviceDriverBase {
+ // ...
+}
+module.exports = MyNewDriver;
+```
+
+## Existing Third party drivers
+
+* [detox-puppeteer](https://github.com/ouihealth/detox-puppeteer)
diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json
index 64a3c8ed5..57d6fecda 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": "16.2.0",
+  "version": "16.4.1",
   "private": true,
   "scripts": {
     "packager": "react-native start",
@@ -8,7 +8,7 @@
     "e2e": "mocha e2e --opts ./e2e/mocha.opts"
   },
   "devDependencies": {
-    "detox": "^16.2.0",
+    "detox": "^16.4.1",
     "mocha": "^6.1.3"
   },
   "detox": {}
diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json
index 00dbe2687..37647c309 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": "16.2.0",
+  "version": "16.4.1",
   "private": true,
   "devDependencies": {
-    "detox": "^16.2.0",
+    "detox": "^16.4.1",
     "mocha": "^6.1.3"
   },
   "detox": {
diff --git a/examples/demo-plugin/driver.js b/examples/demo-plugin/driver.js
new file mode 100644
index 000000000..ea236d715
--- /dev/null
+++ b/examples/demo-plugin/driver.js
@@ -0,0 +1,145 @@
+const DeviceDriverBase = require('detox/src/devices/drivers/DeviceDriverBase');
+const Client = require('detox/src/client/Client');
+
+class Expect {
+  constructor(invocationManager) {
+    this._invocationManager = invocationManager;
+
+    this.by = {
+      accessibilityLabel: (value) => {},
+      label: (value) => {},
+      id: (value) => value,
+      type: (value) => {},
+      traits: (value) => {},
+      value: (value) => {},
+      text: (value) => {},
+    };
+
+    this.element = this.element.bind(this);
+    this.expect = this.expect.bind(this);
+    this.waitFor = this.waitFor.bind(this);
+  }
+
+  expect(element) {
+    return {
+      toBeVisible: () => {
+        if (!element) {
+          throw new Error("Expectation failed");
+        }
+      }
+    }
+  }
+
+  element(matcher) {
+    return matcher === 'welcome';
+  }
+
+  waitFor(element) {
+  }
+}
+
+class LoginTestee {
+  constructor(sessionId) {
+    this.type = 'login';
+    this.params = { sessionId, role: 'testee' };
+    this.messageId;
+  }
+  async handle(response) {
+    if (response.type !== 'loginSuccess') throw new Error('Unexpected response type');
+  }
+}
+
+class PluginTestee {
+  constructor(config) {
+    this.configuration = config.client.configuration;
+    this.client = new Client(this.configuration);
+  }
+
+  async connect() {
+    await this.client.ws.open();
+
+    // NOTE: This is a sample way to handle events in a custom Testee client, but not needed
+    // for the test suite
+    // this.client.ws.ws.on('message', async (str) => {
+    //   const sendResponse = async (response) => {
+    //     this.client.ws.ws.send(JSON.stringify(response));
+    //   };
+
+    //   const action = JSON.parse(str);
+    //   const messageId = action.messageId;
+    //   if (!action.type) {
+    //     return;
+    //   }
+    //   if (action.type === 'loginSuccess') {
+    //     return;
+    //   } else if (action.type === 'deliverPayload') {
+    //     await sendResponse({
+    //       type: 'deliverPayloadDone',
+    //       messageId: action.messageId,
+    //     });
+    //   } else if (action.type === 'currentStatus') {
+    //     await sendResponse(
+    //       { type: 'currentStatusResult', params: { resources: [] } }
+    //     );
+    //   } else {
+    //     try {
+    //       await sendResponse({
+    //         type: 'invokeResult',
+    //         messageId: action.messageId,
+    //       });
+    //     } catch (error) {
+    //       this.client.ws.ws.send(
+    //         JSON.stringify({
+    //           type: 'testFailed',
+    //           messageId,
+    //           params: { details: str + '\n' + error.message },
+    //         }),
+    //       );
+    //     }
+    //   }
+    // });
+
+    await this.client.sendAction(new LoginTestee(this.configuration.sessionId));
+  }
+}
+
+class PluginDriver extends DeviceDriverBase {
+  constructor(config) {
+    super(config);
+    this.matchers = {};
+    this.testee = new PluginTestee(config);
+    this.matchers = new Expect();
+  }
+
+  async launchApp(deviceId, bundleId, launchArgs, languageAndLocale) {
+    await this.emitter.emit('beforeLaunchApp', {
+      bundleId,
+      deviceId,
+      launchArgs,
+    });
+
+    const pid = 'PID';
+    await this.emitter.emit('launchApp', {
+      bundleId,
+      deviceId,
+      launchArgs,
+      pid,
+    });
+
+    return pid;
+  }
+
+
+  validateDeviceConfig(deviceConfig) {
+    this.deviceConfig = deviceConfig;
+    if (!deviceConfig.binaryPath) {
+      configuration.throwOnEmptyBinaryPath();
+    }
+  }
+
+  async waitUntilReady() {
+    await this.testee.connect();
+  }
+}
+
+module.exports = PluginDriver;
diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json
new file mode 100644
index 000000000..d1eea9257
--- /dev/null
+++ b/examples/demo-plugin/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "demo-plugin",
+  "version": "16.4.1",
+  "private": true,
+  "scripts": {
+    "test:plugin": "detox test --configuration plugin -l verbose"
+  },
+  "devDependencies": {
+    "detox": "^16.4.1",
+    "jest": "24.8.x"
+  },
+  "detox": {
+    "test-runner": "jest",
+    "configurations": {
+      "plugin": {
+        "binaryPath": "my/random/path",
+        "device": {
+          "foo": "bar"
+        },
+        "name": "plugin-example",
+        "type": "./driver"
+      }
+    }
+  }
+}
diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json
index 4f0df2216..79c9e54e6 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": "16.2.0",
+  "version": "16.4.1",
   "private": true,
   "scripts": {},
   "devDependencies": {
-    "detox": "^16.2.0",
+    "detox": "^16.4.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 76afd0ad6..8b75d2276 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": "16.2.0",
+  "version": "16.4.1",
   "private": true,
   "scripts": {
     "test:ios-release": "detox test --configuration ios.sim.release -l verbose",
@@ -13,7 +13,7 @@
     "test:jest-circus:android-release-ci": "detox test --configuration android.emu.release -o e2e/config-circus.json -l verbose --workers 2 --headless --record-logs all --take-screenshots all"
   },
   "devDependencies": {
-    "detox": "^16.2.0",
+    "detox": "^16.4.1",
     "jest": "25.1.x",
     "jest-circus": "25.1.x",
     "sanitize-filename": "^1.6.1"
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index ce585a88a..f3ee55a55 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -1,6 +1,6 @@
 {
   "name": "example",
-  "version": "16.2.0",
+  "version": "16.4.1",
   "private": true,
   "scripts": {
     "start": "react-native start",
@@ -23,7 +23,7 @@
     "react-native": "0.62.x"
   },
   "devDependencies": {
-    "detox": "^16.2.0",
+    "detox": "^16.4.1",
     "mocha": "^6.1.3"
   },
   "detox": {
diff --git a/lerna.json b/lerna.json
index b8b260600..ec5bce402 100644
--- a/lerna.json
+++ b/lerna.json
@@ -9,15 +9,17 @@
     "examples/demo-react-native",
     "examples/demo-react-native-detox-instruments",
     "examples/demo-react-native-jest",
+    "examples/demo-plugin",
     "generation",
     "."
   ],
-  "version": "16.2.0",
+  "version": "16.4.1",
   "npmClient": "npm",
   "command": {
     "publish": {
       "ignoreChanges": [
         "demo-react-native-jest",
+        "demo-plugin",
         "examples/demo-react-native-detox-instruments",
         "detox-demo-native-android",
         "detox-demo-native-ios",
diff --git a/package.json b/package.json
index 4dd0b297c..a5e5a908f 100644
--- a/package.json
+++ b/package.json
@@ -18,5 +18,5 @@
     "semver": "5.x.x",
     "shell-utils": "1.x.x"
   },
-  "version": "16.2.0"
+  "version": "16.4.1"
 }
diff --git a/scripts/aggregate_coverage.sh b/scripts/aggregate_coverage.sh
index 4e1b3989c..2fe0ef777 100755
--- a/scripts/aggregate_coverage.sh
+++ b/scripts/aggregate_coverage.sh
@@ -3,21 +3,12 @@
 if [ -d aggregated-coverage ]; then
   npm install --no-save --no-package-lock "lcov-result-merger@3.x.x" "coveralls@3.x.x"
 
+  echo "Merging LCOV files"
   rm -f "aggregated-coverage/merged.lcov"
-  {
-    for lcov in aggregated-coverage/**/*.lcov; do
-      echo "Forcing relative paths in: $lcov"
-      sed -i 's/^SF:.*\(detox-[a-z]\+-[0-9\-]\+-[a-z]\+\)\//SF:/g' $lcov
-    done
+  node_modules/.bin/lcov-result-merger 'aggregated-coverage/**/*.lcov' "aggregated-coverage/merged.lcov"
 
-    echo "Merging LCOV files"
-    node_modules/.bin/lcov-result-merger 'aggregated-coverage/**/*.lcov' "aggregated-coverage/merged.lcov"
-  } && {
-    echo "Uploading to coveralls"
-    cat "aggregated-coverage/merged.lcov" | node_modules/.bin/coveralls
-  } || {
-    echo "Failed to aggregate the test coverage, skipping."
-  }
+  echo "Uploading to coveralls"
+  { cat "aggregated-coverage/merged.lcov" | node_modules/.bin/coveralls; } || echo "Failed to aggregate the test coverage, skipping."
 else
   echo "Aggregated coverage directory was not found, skipping."
 fi
diff --git a/scripts/ci.android.sh b/scripts/ci.android.sh
index cd2fa2ecd..857926dfe 100755
--- a/scripts/ci.android.sh
+++ b/scripts/ci.android.sh
@@ -9,16 +9,25 @@ pushd detox/android
 run_f "./gradlew test"
 popd
 
+mkdir -p coverage
+
 pushd detox/test
+
 # Workaround until react android issue will be fixed - react-native: 0.55
 mv node_modules/react-native/ReactAndroid/release.gradle node_modules/react-native/ReactAndroid/release.gradle.bak
 cp extras/release.gradle node_modules/react-native/ReactAndroid/
 
 run_f "npm run build:android"
+cp ../coverage/lcov.info ../../coverage/unit.lcov
+
 run_f "npm run e2e:android-ci"
+cp coverage/lcov.info ../../coverage/e2e-android-ci.lcov
+
 run_f "npm run e2e:android-timeout-ci"
+cp coverage/lcov.info ../../coverage/e2e-android-timeout-ci.lcov
+
 run_f "npm run e2e:jest-circus-timeout:android"
+cp coverage/lcov.info ../../coverage/e2e-jest-circus-timeout-android.lcov
 
-cp coverage/lcov.info coverage/e2e.lcov
 # run_f "npm run verify-artifacts:android"
 popd
diff --git a/scripts/ci.ios.sh b/scripts/ci.ios.sh
index aa03a41de..24f68aebc 100755
--- a/scripts/ci.ios.sh
+++ b/scripts/ci.ios.sh
@@ -4,12 +4,21 @@ source $(dirname "$0")/ci.sh
 
 run_f "$(dirname "$0")/unit.ios.sh"
 
+mkdir -p coverage
+
 pushd detox/test
+
 run_f "npm run build:ios"
+cp ../coverage/lcov.info ../../coverage/unit.lcov
+
 run_f "npm run e2e:ios-ci"
+cp coverage/lcov.info ../../coverage/e2e-ios-ci.lcov
+
 run_f "npm run e2e:ios-timeout-ci"
+cp coverage/lcov.info ../../coverage/e2e-ios-timeout-ci.lcov
+
 run_f "npm run e2e:jest-circus-timeout:ios"
+cp coverage/lcov.info ../../coverage/e2e-jest-circus-timeout-ios.lcov
 
-cp coverage/lcov.info coverage/e2e.lcov
 # run_f "npm run verify-artifacts:ios"
 popd
diff --git a/scripts/demo-projects.android.sh b/scripts/demo-projects.android.sh
index 773041a57..6738ab5a0 100755
--- a/scripts/demo-projects.android.sh
+++ b/scripts/demo-projects.android.sh
@@ -24,3 +24,7 @@ pushd examples/demo-react-native
 run_f "npm run test:android-release-ci"
 run_f "npm run test:android-explicit-require-ci"
 popd
+
+pushd examples/demo-plugin
+run_f "npm run test:plugin"
+popd


Test patch:
diff --git a/detox/local-cli/test.js b/detox/local-cli/test.js
index bf19c83a2..5821ddd2d 100644
--- a/detox/local-cli/test.js
+++ b/detox/local-cli/test.js
@@ -132,6 +132,12 @@ module.exports.builder = {
     group: 'Execution:',
     describe: `Use Detox' custom console-logging implementation, for logging Detox (non-device) logs. Disabling will fallback to node.js / test-runner's implementation (e.g. Jest / Mocha).`,
   },
+  'force-adb-install': {
+    boolean: true,
+    default: false,
+    group: 'Execution:',
+    describe: `Due to problems with the "adb install" command on Android, Detox resorts to a different scheme for install APK's. Setting true will disable that and force usage of "adb install", instead.`,
+  }
 };
 
 const collectExtraArgs = require('./utils/collectExtraArgs')(module.exports.builder);
@@ -231,6 +237,7 @@ module.exports.handler = async function test(program) {
         (program.artifactsLocation ? `--artifacts-location "${program.artifactsLocation}"` : ''),
         (program.deviceName ? `--device-name "${program.deviceName}"` : ''),
         (program.useCustomLogger ? `--use-custom-logger "${program.useCustomLogger}"` : ''),
+        (program.forceAdbInstall ? `--force-adb-install "${program.forceAdbInstall}"` : ''),
       ]),
       ...getPassthroughArguments(),
     ]).join(' ');
@@ -291,7 +298,8 @@ module.exports.handler = async function test(program) {
         'reportSpecs',
         'readOnlyEmu',
         'deviceLaunchArgs',
-        'useCustomLogger'
+        'useCustomLogger',
+        'forceAdbInstall',
       ]),
       DETOX_START_TIMESTAMP: Date.now(),
     };
diff --git a/detox/src/Detox.test.js b/detox/src/Detox.test.js
index 1f2ef48bb..16a8e6cf9 100644
--- a/detox/src/Detox.test.js
+++ b/detox/src/Detox.test.js
@@ -1,343 +1,556 @@
-const schemes = require('./configurations.mock');
+const _ = require('lodash');
+const configuration = require('./configuration');
 const testSummaries = require('./artifacts/__mocks__/testSummaries.mock');
 
-const defaultPlatformEnv = {
-  darwin: {},
-  linux: {
-    // Not set by default on Ubuntu
-    // XDG_DATA_HOME: '/home/detox-user/.local/share',
-  },
-  win32: {
-    // Required for appdatapath.js
-    LOCALAPPDATA: 'C:\\Users\\detox-user\\AppData\\Local',
-    USERPROFILE: 'C:\\Users\\detox-user',
-  },
-};
+jest.mock('./utils/logger');
+jest.mock('./devices/DriverRegistry');
+jest.mock('./artifacts/ArtifactsManager');
+jest.mock('./client/Client');
+jest.mock('./devices/Device');
+
+jest.mock('./server/DetoxServer', () => {
+  const FakeServer = jest.genMockFromModule('./server/DetoxServer');
+  return jest.fn().mockImplementation(() => new FakeServer());
+});
 
 describe('Detox', () => {
-  let client;
-  let mockLogger;
-  let fs;
+  let detoxConfig;
+
+  let logger;
+  let FakeDriverRegistry;
+  let Device;
+  let Client;
+  let DetoxServer;
+  let ArtifactsManager;
   let Detox;
   let detox;
 
-  const validDeviceConfig = schemes.validOneDeviceNoSession.configurations['ios.sim.release'];
-  const validDeviceConfigWithSession = schemes.sessionPerConfiguration.configurations['ios.sim.none'];
-  const invalidDeviceConfig = schemes.invalidDeviceNoDeviceType.configurations['ios.sim.release'];
-  const invalidDeviceTypeConfig = schemes.invalidOneDeviceTypeEmulatorNoSession.configurations['ios.sim.release'];
-  const validSession = schemes.validOneDeviceAndSession.session;
-  const clientMockData = {lastConstructorArguments: null};
-  const deviceMockData = {lastConstructorArguments: null};
+  function client() {
+    return Client.mock.instances[0];
+  }
+
+  function device() {
+    return Device.mock.instances[0];
+  }
+
+  function artifactsManager() {
+    return ArtifactsManager.mock.instances[0];
+  }
 
   beforeEach(async () => {
-    function setCustomClassMock(modulePath, dataObject, mockObject = {}) {
-      const JestMock = jest.genMockFromModule(modulePath);
-      class FinalMock extends JestMock {
-        constructor(...rest) {
-          super(rest);
-          dataObject.lastConstructorArguments = rest;
-
-          Object.keys(mockObject).forEach((key) => {
-            this[key] = mockObject[key].bind(this);
-          });
-        }
-        on(event, callback) {
-          if (event === 'launchApp') {
-            callback({});
-          }
-        }
-      }
-      jest.setMock(modulePath, FinalMock);
-    }
-
-    jest.mock('fs');
-    jest.mock('fs-extra');
-    fs = require('fs');
-    jest.mock('./ios/expect');
-
-    mockLogger = jest.genMockFromModule('./utils/logger');
-    mockLogger.child.mockReturnValue(mockLogger);
-    jest.mock('./utils/logger', () => mockLogger);
-
-    setCustomClassMock('./devices/Device', deviceMockData);
-
-    client = {
-      setNonresponsivenessListener: jest.fn(),
-      getPendingCrashAndReset: jest.fn(),
-      dumpPendingRequests: jest.fn(),
+    detoxConfig = await configuration.composeDetoxConfig({
+      configurations: {
+        test: {
+          type: 'fake.device',
+          binaryPath: '/tmp/fake/path',
+          device: 'a device',
+        },
+      },
+    });
+
+    logger = require('./utils/logger');
+    Device = require('./devices/Device');
+    FakeDriverRegistry = require('./devices/DriverRegistry');
+    ArtifactsManager = require('./artifacts/ArtifactsManager');
+    Client = require('./client/Client');
+    DetoxServer = require('./server/DetoxServer');
+    Detox = require('./Detox');
+  });
+
+  describe('when detox.init() is called', () => {
+    let globalMatcher;
+
+    const init = async () => {
+      detox = await new Detox(detoxConfig).init();
     };
-    setCustomClassMock('./client/Client', clientMockData, client);
 
-    process.env = Object.assign({}, defaultPlatformEnv[process.platform]);
+    beforeEach(() => {
+      FakeDriverRegistry.FakeDriver.matchers.globalMatcher = globalMatcher = jest.fn();
+    });
 
-    global.device = undefined;
+    afterEach(() => {
+      delete FakeDriverRegistry.FakeDriver.matchers.globalMatcher;
 
-    jest.mock('./devices/drivers/ios/IosDriver');
-    jest.mock('./devices/drivers/ios/SimulatorDriver');
-    jest.mock('./devices/Device');
-    jest.mock('./server/DetoxServer');
-  });
+      // cleanup spilled globals after detox.init()
+      delete global.device;
+      delete global.globalMatcher;
+    });
 
-  it(`Calling detox.cleanup() before .init() should pass without exceptions`, async () => {
-    process.env.cleanup = true;
-    Detox = require('./Detox');
+    describe('', () => {
+      beforeEach(init);
+
+      it('should create a DetoxServer automatically', () =>
+        expect(DetoxServer).toHaveBeenCalledWith({
+          port: expect.anything(),
+        }));
+
+      it('should create a new Client', () =>
+        expect(Client).toHaveBeenCalledWith(expect.objectContaining({
+          server: expect.any(String),
+          sessionId: expect.any(String),
+        })));
+
+      it('should add a non-responsiveness listener to client ', () =>
+        expect(client().setNonresponsivenessListener).toHaveBeenCalledWith(
+          expect.any(Function)
+        ));
+
+      it('should connect a client to the server', () =>
+        expect(client().connect).toHaveBeenCalled());
+
+      it('should resolve a device driver from the registry', () =>
+        expect(FakeDriverRegistry.default.resolve).toHaveBeenCalledWith(
+          detoxConfig.deviceConfig.type,
+          {
+            client: expect.anything(),
+            emitter: expect.anything(),
+          }
+        ));
 
-    detox = new Detox({deviceConfig: validDeviceConfig});
-    expect(() => detox.cleanup()).not.toThrowError();
-  });
+      it('should take device driver matchers to Detox', () =>
+        expect(detox.globalMatcher).toBe(globalMatcher));
 
-  it(`Calling detox.init() twice returns the same promise`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfig});
-    const promise1 = await detox.init();
-    const promise2 = await detox.init();
-    expect(promise1).toBe(promise2);
-  });
+      it('should take device to Detox', () =>
+        expect(detox.device).toBe(device()));
 
-  it(`Calling detox.beforeEach() before detox.init() completes, throws error`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfig});
-    const detoxInitPromise = detox.init();
-    await expect(detox.beforeEach(testSummaries.running())).rejects.toThrowError(/Aborted detox.init/);
-    await expect(detoxInitPromise).rejects.toThrowError(/Aborted detox.init/);
-  });
+      it('should instantiate Device', () =>
+        expect(Device).toHaveBeenCalledWith({
+          deviceConfig: detoxConfig.deviceConfig,
+          emitter: expect.anything(),
+          deviceDriver: expect.anything(),
+          sessionConfig: expect.anything(),
+        }));
 
-  it(`Calling detox.afterEach() before detox.init() completes, throws error`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfig});
-    const detoxInitPromise = detox.init();
-    await expect(detox.afterEach(testSummaries.failed())).rejects.toThrowError(/Aborted detox.init/);
-    await expect(detoxInitPromise).rejects.toThrowError(/Aborted detox.init/);
-  });
+      it('should expose device to global', () =>
+        expect(global.device).toBe(device()));
 
-  it(`Calling detox.cleanup() before detox.init() completes makes that .init() throw an error`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfig});
-    const detoxInitPromise = detox.init();
-    await detox.cleanup();
-    await expect(detoxInitPromise).rejects.toThrowError(/Aborted detox.init/);
-  });
+      it('should expose device driver matchers to global', () =>
+        expect(global.globalMatcher).toBe(globalMatcher));
 
-  it(`Not passing --cleanup should keep the currently running device up`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfig});
-    await detox.init();
-    const device = detox.device;
-    await detox.cleanup();
-    expect(device.shutdown).toHaveBeenCalledTimes(0);
-  });
+      it('should create artifacts manager', () =>
+        expect(ArtifactsManager).toHaveBeenCalledWith(detoxConfig.artifactsConfig));
 
-  it(`One valid device, detox should init with generated session config and default to this device`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfig});
-    await detox.init();
-    expect(clientMockData.lastConstructorArguments[0]).toBeDefined();
-  });
+      it('should subscribe artifacts manager to device events', () =>
+        expect(artifactsManager().subscribeToDeviceEvents).toHaveBeenCalledWith(
+          expect.objectContaining({ on: expect.any(Function) })
+        ));
 
-  it(`throws if device type is not supported`, async () => {
-    let exception = undefined;
+      it('should register plugins in the artifacts manager', () =>
+        expect(artifactsManager().registerArtifactPlugins).toHaveBeenCalledWith(
+          FakeDriverRegistry.FakeDriver.artifactsPlugins
+        ));
 
-    try {
-      Detox = require('./Detox');
-      detox = new Detox({deviceConfig: invalidDeviceTypeConfig});
-      await detox.init();
-    } catch (e) {
-      exception = e;
-    }
+      it('should prepare the device', () =>
+        expect(device().prepare).toHaveBeenCalled());
 
-    expect(exception).toBeDefined();
-  });
+      it('should reinstall the app', () => {
+        expect(device().uninstallApp).toHaveBeenCalled();
+        expect(device().installApp).toHaveBeenCalled();
+      });
 
-  it(`One valid device, detox should use session config and default to this device`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfig, session: validSession});
-    await detox.init();
+      it('should launch the app', () => {
+        expect(device().launchApp).toHaveBeenCalledWith({
+          newInstance: true,
+        });
+      });
 
-    expect(clientMockData.lastConstructorArguments[0]).toBe(validSession);
-  });
+      it('should return itself', async () =>
+        expect(await detox.init()).toBeInstanceOf(Detox));
+    });
 
-  it(`cleanup on a non initialized detox should not throw`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: invalidDeviceConfig});
-    detox.cleanup();
-  });
+    it('should return the same promise on consequent calls', () => {
+      detox = new Detox(detoxConfig);
 
-  it(`Detox should use session defined per configuration `, async () => {
-    process.env.configuration = 'ios.sim.none';
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfigWithSession});
-    await detox.init();
+      const initPromise1 = detox.init();
+      const initPromise2 = detox.init();
 
-    const expectedSession = validDeviceConfigWithSession.session;
-    expect(clientMockData.lastConstructorArguments[0]).toBe(expectedSession);
-  });
+      expect(initPromise1).toBe(initPromise2);
+    });
 
-  it(`Detox should prefer session defined per configuration over common session`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfigWithSession, session: {}});
-    await detox.init();
+    describe('with sessionConfig.autoStart undefined', () => {
+      beforeEach(() => { delete detoxConfig.sessionConfig.autoStart; });
+      beforeEach(init);
 
-    const expectedSession = validDeviceConfigWithSession.session;
-    expect(clientMockData.lastConstructorArguments[0]).toBe(expectedSession);
-  });
+      it('should not start DetoxServer', () =>
+        expect(DetoxServer).not.toHaveBeenCalled());
+    });
 
-  it('exports globals by default', async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfigWithSession});
-    await detox.init();
-    expect(global.device).toBeDefined();
-  });
+    describe('with behaviorConfig.init.exposeGlobals = false', () => {
+      beforeEach(() => {
+        detoxConfig.behaviorConfig.init.exposeGlobals = false;
+      });
 
-  it(`doesn't exports globals if requested`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfigWithSession});
-    await detox.init({initGlobals: false});
-    expect(global.device).not.toBeDefined();
-  });
+      beforeEach(init);
 
-  it(`handleAppCrash if client has a pending crash`, async () => {
-    client.getPendingCrashAndReset.mockReturnValueOnce('crash');
+      it('should take device driver matchers to Detox', () =>
+        expect(detox.globalMatcher).toBe(globalMatcher));
 
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfigWithSession});
-    await detox.init();
-    await detox.afterEach(testSummaries.failed());
-    expect(device.launchApp).toHaveBeenCalledTimes(1);
-  });
+      it('should take device to Detox', () =>
+        expect(detox.device).toBe(device()));
 
-  it(`handleAppCrash should not dump pending requests if testSummary has no timeout flag`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfigWithSession});
+      it('should not expose device to globals', () =>
+        expect(global.device).toBe(undefined));
 
-    await detox.init();
-    await detox.afterEach(testSummaries.failed());
+      it('should not expose globalMatcher to globals', () =>
+        expect(global.globalMatcher).toBe(undefined));
+    });
 
-    expect(client.dumpPendingRequests).not.toHaveBeenCalled();
-  });
+    describe('with behaviorConfig.init.reinstallApp = false', () => {
+      beforeEach(() => {
+        detoxConfig.behaviorConfig.init.reinstallApp = false;
+      });
 
-  it(`handleAppCrash should dump pending requests if testSummary has timeout flag`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfigWithSession});
+      beforeEach(init);
 
-    await detox.init();
-    await detox.afterEach(testSummaries.timedOut());
-    expect(client.dumpPendingRequests).toHaveBeenCalled();
-  });
+      it('should prepare the device', () =>
+        expect(device().prepare).toHaveBeenCalled());
 
-  it(`should register an async nonresponsiveness listener`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfigWithSession});
+      it('should not reinstall the app', () => {
+        expect(device().uninstallApp).not.toHaveBeenCalled();
+        expect(device().installApp).not.toHaveBeenCalled();
+      });
 
-    await detox.init();
+      it('should launch the app', () =>
+        expect(device().launchApp).toHaveBeenCalledWith({
+          newInstance: true
+        }));
+    });
 
-    expect(client.setNonresponsivenessListener).toHaveBeenCalled();
-  });
+    describe('with behaviorConfig.init.launchApp = false', () => {
+      beforeEach(() => {
+        detoxConfig.behaviorConfig.init.launchApp = false;
+      });
 
-  it(`should log thread-dump provided by a nonresponsiveness event`, async () => {
-    const callbackParams = {
-      threadDump: 'mockThreadDump',
-    };
-    const expectedMessage = [
-      'Application nonresponsiveness detected!',
-      'On Android, this could imply an ANR alert, which evidently causes tests to fail.',
-      'Here\'s the native main-thread stacktrace from the device, to help you out (refer to device logs for the complete thread dump):',
-      callbackParams.threadDump,
-      'Refer to https://developer.android.com/training/articles/perf-anr for further details.'
-    ].join('\n');
+      beforeEach(init);
 
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfigWithSession});
+      it('should prepare the device', () =>
+        expect(device().prepare).toHaveBeenCalled());
+
+      it('should reinstall the app', () => {
+        expect(device().uninstallApp).toHaveBeenCalled();
+        expect(device().installApp).toHaveBeenCalled();
+      });
+
+      it('should not launch the app', () =>
+        expect(device().launchApp).not.toHaveBeenCalled());
+    });
+
+    describe('and it gets unresponsiveness', () => {
+      let THREAD_DUMP = 'Simulated non-responsiveness';
+
+      beforeEach(init);
+
+      beforeEach(() => {
+        const listener = client().setNonresponsivenessListener.mock.calls[0][0];
+        listener({ threadDump: THREAD_DUMP });
+      });
 
-    await detox.init();
-    await invokeDetoxCallback();
+      it('should log a warning', () =>
+        expect(logger.warn).toHaveBeenCalledWith(
+          { event: 'APP_NONRESPONSIVE' },
+          expect.stringContaining(THREAD_DUMP)
+        ));
+    });
+
+    describe('and it gets emitter error', () => {
+      beforeEach(init);
 
-    expect(mockLogger.warn).toHaveBeenCalledWith({ event: 'APP_NONRESPONSIVE' }, expectedMessage);
+      it(`should log EMIT_ERROR if the internal emitter throws an error`, async () => {
+        const { emitter } = Device.mock.calls[0][0];
 
-    async function invokeDetoxCallback() {
-      const callback = client.setNonresponsivenessListener.mock.calls[0][0];
-      await callback(callbackParams);
-    }
+        const testError = new Error();
+        emitter.on('bootDevice', async () => { throw testError; })
+        await emitter.emit('bootDevice', {});
+
+        expect(logger.error).toHaveBeenCalledWith(
+          { event: 'EMIT_ERROR', fn: 'bootDevice' },
+          expect.stringMatching(/^Caught an exception.*bootDevice/),
+          testError
+        );
+      });
+    });
   });
 
-  it(`should log EMIT_ERROR if the internal emitter throws an error`, async () => {
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: validDeviceConfigWithSession});
+  describe('when detox.beforeEach() is called', () => {
+    describe('before detox.init() is called', () => {
+      beforeEach(() => {
+        detox = new Detox(detoxConfig);
+      });
+
+      it('should throw an error', () =>
+        expect(detox.beforeEach(testSummaries.running())).rejects.toThrowError(/of null/));
+    });
 
-    const emitter = detox._eventEmitter;
-    emitter.on('launchApp', () => { throw new Error('TestError'); });
-    await emitter.emit('launchApp');
+    describe('before detox.init() is finished', () => {
+      beforeEach(() => {
+        detox = new Detox(detoxConfig);
+      });
 
-    expect(mockLogger.error).toHaveBeenCalledWith(
-      { event: 'EMIT_ERROR', fn: 'launchApp' },
-      expect.stringMatching(/Caught an exception in.*emit.*launchApp.*undefined/),
-      expect.any(Error),
-    )
+      it('should throw an error', async () => {
+        const initPromise = detox.init();
+        await expect(detox.beforeEach(testSummaries.running())).rejects.toThrowError(/Aborted detox.init/);
+        await expect(initPromise).rejects.toThrowError(/Aborted detox.init/);
+      })
+    });
+
+    describe('after detox.init() is finished', () => {
+      beforeEach(async () => {
+        detox = await new Detox(detoxConfig).init();
+        device().launchApp.mockReset();
+      });
+
+      it('should validate test summary object', async () => {
+        await expect(detox.beforeEach('Test')).rejects.toThrowError(
+          /Invalid test summary was passed/
+        );
+      });
+
+      it('should validate test summary status', async () => {
+        await expect(detox.beforeEach({
+          ...testSummaries.running(),
+          status: undefined,
+        })).rejects.toThrowError(/Invalid test summary status/);
+      });
+
+      it('should validate test summary status', async () => {
+        await expect(detox.beforeEach({
+          ...testSummaries.running(),
+          status: undefined,
+        })).rejects.toThrowError(/Invalid test summary status/);
+      });
+
+      describe('with a valid test summary', () => {
+        beforeEach(() => detox.beforeEach(testSummaries.running()));
+
+        it('should trace DETOX_BEFORE_EACH event', () =>
+          expect(logger.trace).toHaveBeenCalledWith(
+            expect.objectContaining({ event: 'DETOX_BEFORE_EACH' }),
+            expect.any(String)
+          ));
+
+        it('should notify artifacts manager about "testStart', () =>
+          expect(artifactsManager().onTestStart).toHaveBeenCalledWith(testSummaries.running()));
+
+        it('should not relaunch app', async () => {
+          await detox.beforeEach(testSummaries.running());
+          expect(device().launchApp).not.toHaveBeenCalled();
+        });
+
+        it('should not dump pending network requests', async () => {
+          await detox.beforeEach(testSummaries.running());
+          expect(client().dumpPendingRequests).not.toHaveBeenCalled();
+        });
+      });
+
+      describe('if app has crashed', () => {
+        beforeEach(async () => {
+          client().getPendingCrashAndReset.mockReturnValueOnce('Simulated crash');
+          await detox.beforeEach(testSummaries.running());
+        });
+
+        it('should print log message', () =>
+          expect(logger.error).toHaveBeenCalledWith(
+            { event: 'APP_CRASH' }, expect.any(String)
+          ));
+
+        it('should also relaunch app', () =>
+          expect(device().launchApp).toHaveBeenCalledWith({ newInstance: true }));
+      });
+    });
   });
 
-  describe('.artifactsManager', () => {
-    let artifactsManager;
+  describe('when detox.afterEach() is called', () => {
+    describe('before detox.init() is called', () => {
+      beforeEach(() => {
+        detox = new Detox(detoxConfig);
+      });
 
-    beforeEach(async () => {
-      jest.mock('./artifacts/ArtifactsManager');
-      Detox = require('./Detox');
-      detox = new Detox({deviceConfig: validDeviceConfig});
-      await detox.init();
-      artifactsManager = detox._artifactsManager; // TODO: rewrite to avoid accessing private fields
+      it('should throw an error', () =>
+        expect(detox.afterEach(testSummaries.running())).rejects.toThrowError(/of null/));
     });
 
-    it(`Calling detox.beforeEach() will trigger artifacts manager .onTestStart`, async () => {
-      const testSummary = testSummaries.running();
-      await detox.beforeEach(testSummary);
+    describe('before detox.init() is finished', () => {
+      beforeEach(() => {
+        detox = new Detox(detoxConfig);
+      });
 
-      expect(artifactsManager.onTestStart).toHaveBeenCalledWith(testSummary);
+      it('should throw an error', async () => {
+        const initPromise = detox.init();
+        await expect(detox.afterEach(testSummaries.running())).rejects.toThrowError(/Aborted detox.init/);
+        await expect(initPromise).rejects.toThrowError(/Aborted detox.init/);
+      })
     });
 
-    it(`Calling detox.beforeEach() and detox.afterEach() with a deprecated signature will throw an exception`, async () => {
-      const { title, fullName, status } = testSummaries.running();
+    describe('after detox.init() is finished', () => {
+      beforeEach(async () => {
+        detox = await new Detox(detoxConfig).init();
+        await detox.beforeEach(testSummaries.running());
+      });
+
+      it('should validate non-object test summary', () =>
+        expect(detox.afterEach()).rejects.toThrowError(/Invalid test summary was passed/));
+
+      it('should validate against invalid test summary status', () =>
+        expect(detox.afterEach({})).rejects.toThrowError(/Invalid test summary status/));
+
+      describe('with a passing test summary', () => {
+        beforeEach(() => detox.afterEach(testSummaries.passed()));
+
+        it('should trace DETOX_AFTER_EACH event', () =>
+          expect(logger.trace).toHaveBeenCalledWith(
+            expect.objectContaining({ event: 'DETOX_AFTER_EACH' }),
+            expect.any(String)
+          ));
+
+        it('should notify artifacts manager about "testDone"', () =>
+          expect(artifactsManager().onTestDone).toHaveBeenCalledWith(testSummaries.passed()));
+      });
+
+      describe('with a failed test summary (due to failed asseration)', () => {
+        beforeEach(() => detox.afterEach(testSummaries.failed()));
+
+        it('should not dump pending network requests', () =>
+          expect(client().dumpPendingRequests).not.toHaveBeenCalled());
+      });
+
+      describe('with a failed test summary (due to app crash)', () => {
+        beforeEach(async () => {
+          client().getPendingCrashAndReset.mockReturnValueOnce('Simulated crash');
+          await detox.afterEach(testSummaries.failed());
+        });
+
+        it('should print log message', () =>
+          expect(logger.error).toHaveBeenCalledWith(
+            { event: 'APP_CRASH' }, expect.any(String)
+          ));
+
+        it('should also relaunch app', () =>
+          expect(device().launchApp).toHaveBeenCalledWith({ newInstance: true }));
+      });
 
-      await expect(detox.beforeEach(title, fullName, status)).rejects.toThrowError();
-      expect(artifactsManager.onTestStart).not.toHaveBeenCalled();
+      describe('with a failed test summary (due to a timeout)', () => {
+        beforeEach(() => detox.afterEach(testSummaries.timedOut()));
 
-      await expect(detox.afterEach(title, fullName, status)).rejects.toThrowError();
-      expect(artifactsManager.onTestDone).not.toHaveBeenCalled();
+        it('should dump pending network requests', () =>
+          expect(client().dumpPendingRequests).toHaveBeenCalled());
+      });
     });
+  });
 
-    it(`Calling detox.beforeEach() and detox.afterEach() with incorrect test status will throw an exception`, async () => {
-      const testSummary = { ...testSummaries.running(), status: 'incorrect status' };
+  describe('when detox.cleanup() is called', () => {
+    let initPromise;
 
-      await expect(detox.beforeEach(testSummary)).rejects.toThrowError();
-      expect(artifactsManager.onTestStart).not.toHaveBeenCalled();
+    const startInit = () => {
+      detox = new Detox(detoxConfig);
+      initPromise = detox.init();
+    };
 
-      await expect(detox.afterEach(testSummary)).rejects.toThrowError();
-      expect(artifactsManager.onTestDone).not.toHaveBeenCalled();
+    describe('before detox.init() has been called', () => {
+      it(`should not throw`, async () =>
+        await expect(new Detox(detoxConfig).cleanup()).resolves.not.toThrowError());
     });
 
-    it(`Calling detox.afterEach() should trigger artifactsManager.onTestDone`, async () => {
-      const testSummary = testSummaries.passed();
-      await detox.afterEach(testSummary);
+    describe('before client has connected', () => {
+      beforeEach(() => Client.setInfiniteConnect());
+      beforeEach(startInit);
+
+      it(`should not throw, but should reject detox.init() promise`, async () => {
+        await expect(detox.cleanup()).resolves.not.toThrowError();
+        await expect(initPromise).rejects.toThrowError(/Aborted detox.init.*execution/);
+      });
+    })
+
+    describe('before device has been prepared', () => {
+      beforeEach(() => Device.setInfiniteMethod('prepare'));
+      beforeEach(startInit);
 
-      expect(artifactsManager.onTestDone).toHaveBeenCalledWith(testSummary);
+      it(`should not throw, but should reject detox.init() promise`, async () => {
+        await expect(detox.cleanup()).resolves.not.toThrowError();
+        await expect(initPromise).rejects.toThrowError(/Aborted detox.init.*execution/);
+      });
     });
 
-    it(`Calling detox.cleanup() should trigger artifactsManager.onBeforeCleanup()`, async () => {
-      await detox.cleanup();
-      expect(artifactsManager.onBeforeCleanup).toHaveBeenCalledTimes(1);
+    describe('before app has been uninstalled', () => {
+      beforeEach(() => Device.setInfiniteMethod('uninstallApp'));
+      beforeEach(startInit);
+
+      it(`should not throw, but should reject detox.init() promise`, async () => {
+        await expect(detox.cleanup()).resolves.not.toThrowError();
+        await expect(initPromise).rejects.toThrowError(/Aborted detox.init.*execution/);
+      });
     });
 
-    it(`should trigger artifactsManager.onSuiteStart`, async() => {
-      const suite = {name: 'testSuiteName'};
+    describe('before app has been installed', () => {
+      beforeEach(() => Device.setInfiniteMethod('installApp'));
+      beforeEach(startInit);
 
-      await detox.suiteStart(suite);
+      it(`should not throw, but should reject detox.init() promise`, async () => {
+        await expect(detox.cleanup()).resolves.not.toThrowError();
+        await expect(initPromise).rejects.toThrowError(/Aborted detox.init.*execution/);
+      });
+    });
 
-      expect(artifactsManager.onSuiteStart).toHaveBeenCalledWith(suite);
+    describe('before app has been launched', () => {
+      beforeEach(() => Device.setInfiniteMethod('launchApp'));
+      beforeEach(startInit);
+
+      it(`should not throw, but should reject detox.init() promise`, async () => {
+        await expect(detox.cleanup()).resolves.not.toThrowError();
+        await expect(initPromise).rejects.toThrowError(/Aborted detox.init.*execution/);
+      });
     });
 
-    it(`should trigger artifactsManager.onSuiteEnd`, async() => {
-      const suite = {name: 'testSuiteName'};
+    describe('after detox.init()', () => {
+      beforeEach(async () => {
+        detox = new Detox(detoxConfig);
+        await detox.init();
+        await detox.cleanup();
+      });
 
-      await detox.suiteEnd(suite);
+      it(`should not shutdown the device`, () =>
+        expect(device().shutdown).not.toHaveBeenCalled());
 
-      expect(artifactsManager.onSuiteEnd).toHaveBeenCalledWith(suite);
+      it(`should trigger artifactsManager.onBeforeCleanup()`, () =>
+        expect(artifactsManager().onBeforeCleanup).toHaveBeenCalled());
+
+      it(`should dump pending network requests`, () =>
+        expect(client().dumpPendingRequests).toHaveBeenCalled());
+    });
+
+    describe('when behaviorConfig.cleanup.shutdownDevice = true', () => {
+      beforeEach(async () => {
+        detoxConfig.behaviorConfig.cleanup.shutdownDevice = true;
+        detox = await new Detox(detoxConfig).init();
+      });
+
+      it(`should shutdown the device on detox.cleanup()`, async () => {
+        await detox.cleanup();
+        expect(device().shutdown).toHaveBeenCalled();
+      });
+    });
+  });
+
+  describe('when detox.suiteStart() is called', () => {
+    beforeEach(async () => {
+      detox = await new Detox(detoxConfig).init();
+    });
+
+    it(`should pass suite info to artifactsManager.onSuiteStart`, async () => {
+      const suite = { name: 'testSuiteName' };
+      await detox.suiteStart(suite);
+      expect(artifactsManager().onSuiteStart).toHaveBeenCalledWith(suite);
+    });
+  });
+
+  describe('when detox.suiteEnd() is called', () => {
+    beforeEach(async () => {
+      detox = await new Detox(detoxConfig).init();
+    });
+
+    it(`should pass suite info to artifactsManager.onSuiteStart`, async () => {
+      const suite = { name: 'testSuiteName' };
+      await detox.suiteEnd(suite);
+      expect(artifactsManager().onSuiteEnd).toHaveBeenCalledWith(suite);
     });
   });
 });
diff --git a/detox/src/artifacts/ArtifactsManager.test.js b/detox/src/artifacts/ArtifactsManager.test.js
index 2dd587a7d..2e679ecb7 100644
--- a/detox/src/artifacts/ArtifactsManager.test.js
+++ b/detox/src/artifacts/ArtifactsManager.test.js
@@ -464,4 +464,39 @@ describe('ArtifactsManager', () => {
       });
     });
   });
+
+  describe('.subscribeToDeviceEvents', () => {
+    let artifactsManager, emitter;
+
+    beforeEach(() => {
+      artifactsManager = new proxy.ArtifactsManager({
+        pathBuilder: new proxy.ArtifactPathBuilder({
+          rootDir: '/tmp',
+        }),
+        plugins: {
+          mock: { setting: 'value' },
+        },
+      });
+
+      emitter = {
+        on: jest.fn(),
+      };
+    });
+
+    it.each([
+      ['bootDevice'],
+      ['beforeShutdownDevice'],
+      ['shutdownDevice'],
+      ['beforeLaunchApp'],
+      ['launchApp'],
+      ['appReady'],
+      ['beforeUninstallApp'],
+      ['beforeTerminateApp'],
+      ['terminateApp'],
+      ['createExternalArtifact'],
+    ])(`should subscribe to emitter's %s event`, (eventName) => {
+      artifactsManager.subscribeToDeviceEvents(emitter);
+      expect(emitter.on).toHaveBeenCalledWith(eventName, expect.any(Function));
+    });
+  });
 });
diff --git a/detox/src/configuration.test.js b/detox/src/configuration.test.js
index 7db45ea76..26af0b59f 100644
--- a/detox/src/configuration.test.js
+++ b/detox/src/configuration.test.js
@@ -2,36 +2,23 @@ const _ = require('lodash');
 const path = require('path');
 const schemes = require('./configurations.mock');
 
+jest.mock('./utils/argparse');
+
 describe('configuration', () => {
+  let args;
   let configuration;
-  beforeEach(() => {
-    configuration = require('./configuration');
-  });
-
-  it(`generate a default config`, async () => {
-    const config = await configuration.defaultSession();
-    expect(() => config.session.server).toBeDefined();
-    expect(() => config.session.sessionId).toBeDefined();
-  });
-
-  it(`providing a valid config`, () => {
-    expect(() => configuration.validateSession(schemes.validOneDeviceAndSession.session)).not.toThrow();
-  });
-
-  it(`providing empty server config should throw`, () => {
-    testFaultySession();
-  });
+  let detoxConfig;
+  let deviceConfig;
+  let userParams;
 
-  it(`providing server config with no session should throw`, () => {
-    testFaultySession(schemes.validOneDeviceNoSession.session);
-  });
-
-  it(`providing server config with no session.server should throw`, () => {
-    testFaultySession(schemes.invalidSessionNoServer.session);
-  });
+  beforeEach(() => {
+    args = {};
+    detoxConfig = {};
+    deviceConfig = {};
+    userParams = undefined;
 
-  it(`providing server config with no session.sessionId should throw`, () => {
-    testFaultySession(schemes.invalidSessionNoSessionId.session);
+    require('./utils/argparse').getArgValue.mockImplementation(key => args[key]);
+    configuration = require('./configuration');
   });
 
   describe('composeArtifactsConfig', () => {
@@ -40,6 +27,7 @@ describe('configuration', () => {
         configurationName: 'abracadabra',
         deviceConfig: {},
         detoxConfig: {},
+        cliConfig: {},
       })).toMatchObject({
         pathBuilder: expect.objectContaining({
           rootDir: expect.stringMatching(/^artifacts[\\\/]abracadabra\.\d{4}/),
@@ -157,6 +145,7 @@ describe('configuration', () => {
           },
         },
         detoxConfig: {},
+        cliConfig: {},
       }).pathBuilder).toBeInstanceOf(FakePathBuilder);
     });
 
@@ -165,10 +154,11 @@ describe('configuration', () => {
         configurationName: 'customization',
         deviceConfig: {
           artifacts: {
-            pathBuilder: 'package.json',
+            pathBuilder: './package.json',
           },
         },
         detoxConfig: {},
+        cliConfig: {},
       })).toMatchObject({
         pathBuilder: expect.objectContaining({
           "name": expect.any(String),
@@ -186,6 +176,7 @@ describe('configuration', () => {
           },
         },
         detoxConfig: {},
+        cliConfig: {},
       })).toMatchObject({
         rootDir: '.artifacts/',
       });
@@ -233,11 +224,315 @@ describe('configuration', () => {
     });
   });
 
-  function testFaultySession(config) {
-    try {
-      configuration.validateSession(config);
-    } catch (ex) {
-      expect(ex).toBeDefined();
-    }
-  }
+  describe('composeBehaviorConfig', () => {
+    let composed = () => configuration.composeBehaviorConfig({
+      deviceConfig,
+      detoxConfig,
+      userParams,
+    });
+
+    it('should return a default behavior if nothing is set', () => {
+      expect(composed()).toEqual({
+        init: {
+          exposeGlobals: true,
+          reinstallApp: true,
+          launchApp: true,
+        },
+        cleanup: {
+          shutdownDevice: false,
+        },
+      })
+    });
+
+    describe('if detox config is set', () => {
+      beforeEach(() => {
+        detoxConfig = {
+          behavior: {
+            init: {
+              exposeGlobals: false,
+              reinstallApp: false,
+              launchApp: false,
+            },
+            cleanup: {
+              shutdownDevice: true,
+            },
+          },
+        };
+      });
+
+      it('should override the defaults', () => {
+        const expected = _.cloneDeep(detoxConfig.behavior);
+        const actual = composed();
+
+        expect(actual).toEqual(expected);
+      });
+
+      describe('if device config is set', () => {
+        beforeEach(() => {
+          deviceConfig = {
+            behavior: {
+              init: {
+                exposeGlobals: true,
+                reinstallApp: true,
+                launchApp: true,
+              },
+              cleanup: {
+                shutdownDevice: false,
+              },
+            },
+          };
+        });
+
+        it('should override the defaults from detox config', () => {
+          const expected = _.cloneDeep(deviceConfig.behavior);
+          const actual = composed();
+
+          expect(actual).toEqual(expected);
+        });
+
+        describe('if user params is set', () => {
+          beforeEach(() => {
+            userParams = {
+              initGlobals: false,
+              launchApp: false,
+              reuse: false,
+            };
+          });
+
+          it('should override the defaults from device config', () => {
+            expect(composed()).toEqual({
+              init: {
+                exposeGlobals: false,
+                reinstallApp: true,
+                launchApp: false,
+              },
+              cleanup: {
+                shutdownDevice: false,
+              }
+            });
+          });
+
+          describe('if cli args are set', () => {
+            beforeEach(() => {
+              args.reuse = true;
+              args.cleanup = true;
+            });
+
+            it('should override the user params', () => {
+              expect(composed()).toEqual({
+                init: {
+                  exposeGlobals: false,
+                  reinstallApp: false,
+                  launchApp: false,
+                },
+                cleanup: {
+                  shutdownDevice: true,
+                }
+              });
+            });
+          });
+        });
+      });
+    });
+  });
+
+  describe('composeDeviceConfig', () => {
+    let configs;
+
+    beforeEach(() => {
+      configs = [1, 2].map(i => ({
+        type: `someDriver${i}`,
+        device: `someDevice${i}`,
+      }));
+    });
+
+    describe('validation', () => {
+      it('should throw if no configurations are passed', () => {
+        expect(() => configuration.composeDeviceConfig({
+          configurations: {},
+        })).toThrowError(/There are no device configurations/);
+      });
+
+      it('should throw if configuration driver (type) is not defined', () => {
+        expect(() => configuration.composeDeviceConfig({
+          configurations: {
+            undefinedDriver: {
+              device: { type: 'iPhone X' },
+            },
+          },
+        })).toThrowError(/type.*missing.*ios.simulator.*android.emulator/);
+      });
+
+      it('should throw if device query is not defined', () => {
+        expect(() => configuration.composeDeviceConfig({
+          configurations: {
+            undefinedDeviceQuery: {
+              type: 'ios.simulator',
+            },
+          },
+        })).toThrowError(/device.*empty.*device.*query.*type.*avdName/);
+      });
+    });
+
+    describe('for no specified configuration name', () => {
+      beforeEach(() => { delete args.configuration; });
+
+      describe('when there is a single config', () => {
+        it('should return it', () => {
+          const singleDeviceConfig = configs[0];
+
+          expect(configuration.composeDeviceConfig({
+            configurations: {singleDeviceConfig }
+          })).toBe(singleDeviceConfig);
+        });
+      });
+
+      describe('when there is more than one config', () => {
+        it('should throw if there is more than one config', () => {
+          const [config1, config2] = configs;
+          expect(() => configuration.composeDeviceConfig({
+            configurations: { config1, config2 },
+          })).toThrowError(/Cannot determine/);
+        });
+
+        describe('but also selectedConfiguration param is specified', () => {
+          it('should select that configuration', () => {
+            const [config1, config2] = configs;
+
+            expect(configuration.composeDeviceConfig({
+              selectedConfiguration: 'config1',
+              configurations: { config1, config2 },
+            })).toEqual(config1);
+          });
+        });
+      });
+    });
+
+    describe('for a specified configuration name', () => {
+      let sampleConfigs;
+
+      beforeEach(() => {
+        args.configuration = 'config2';
+
+        const [config1, config2] = [1, 2].map(i => ({
+          type: `someDriver${i}`,
+          device: `someDevice${i}`,
+        }));
+
+        sampleConfigs = { config1, config2 };
+      });
+
+      it('should return that config', () => {
+        expect(configuration.composeDeviceConfig({
+          configurations: sampleConfigs
+        })).toEqual(sampleConfigs.config2);
+      });
+
+      describe('if device-name override is present', () => {
+        beforeEach(() => { args['device-name'] = 'Override'; });
+
+        it('should return that config with an overriden device query', () => {
+          expect(configuration.composeDeviceConfig({
+            configurations: sampleConfigs
+          })).toEqual({
+            ...sampleConfigs.config2,
+            device: 'Override',
+          });
+        });
+      })
+    });
+  });
+
+  describe('composeSessionConfig', () => {
+    const compose = () => configuration.composeSessionConfig({
+      detoxConfig,
+      deviceConfig,
+    });
+
+    it('should generate a default config', async () => {
+      const sessionConfig = await compose();
+
+      expect(sessionConfig).toMatchObject({
+        autoStart: true,
+        server: expect.stringMatching(/^ws:.*localhost:/),
+        sessionId: expect.stringMatching(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i),
+      });
+    });
+
+    describe('if detoxConfig.session is defined', function() {
+      beforeEach(() => {
+        detoxConfig.session = {
+          server: 'ws://localhost:9999',
+          sessionId: 'someSessionId',
+        };
+      })
+
+      it('should return detoxConfig.session', async () => {
+        expect(await compose()).toEqual({
+          server: 'ws://localhost:9999',
+          sessionId: 'someSessionId',
+        });
+      });
+
+      test(`providing empty server config should throw`, () => {
+        delete detoxConfig.session.server;
+        expect(compose()).rejects.toThrowError(/session.server.*missing/);
+      });
+
+      test(`providing server config with no session should throw`, () => {
+        delete detoxConfig.session.sessionId;
+        expect(compose()).rejects.toThrowError(/session.sessionId.*missing/);
+      });
+
+      describe('if deviceConfig.session is defined', function() {
+        beforeEach(() => {
+          detoxConfig.session = {
+            server: 'ws://localhost:1111',
+            sessionId: 'anotherSession',
+          };
+        });
+
+        it('should return deviceConfig.session instead of detoxConfig.session', async () => {
+          expect(await compose()).toEqual({
+            server: 'ws://localhost:1111',
+            sessionId: 'anotherSession',
+          });
+        });
+      });
+    });
+  });
+
+  describe('composeDetoxConfig', () => {
+    it('should throw if no config given', async () => {
+      await expect(configuration.composeDetoxConfig()).rejects.toThrowError(
+        /No configuration was passed/
+      );
+    });
+
+    it('should return a complete Detox config', async () => {
+      const config = await configuration.composeDetoxConfig({
+        configurations: {
+          simple: {
+            type: 'ios.simulator',
+            device: 'iPhone X',
+          },
+        },
+      });
+
+      expect(config).toMatchObject({
+        artifactsConfig: expect.objectContaining({}),
+        behaviorConfig: expect.objectContaining({}),
+        deviceConfig: expect.objectContaining({}),
+        sessionConfig: expect.objectContaining({}),
+      });
+    });
+  });
+
+  describe('throwOnBinaryPath', () => {
+    it('should throw an error', () => {
+      expect(() => configuration.throwOnEmptyBinaryPath()).toThrowError(
+        /binaryPath.*missing/
+      );
+    })
+  })
 });
diff --git a/detox/src/devices/Device.test.js b/detox/src/devices/Device.test.js
index eb2369183..b2fda7457 100644
--- a/detox/src/devices/Device.test.js
+++ b/detox/src/devices/Device.test.js
@@ -1,10 +1,8 @@
 const _ = require('lodash');
-const path = require('path');
 const configurationsMock = require('../configurations.mock');
 
 const validScheme = configurationsMock.validOneDeviceAndSession;
 const invalidDeviceNoBinary = configurationsMock.invalidDeviceNoBinary;
-const invalidDeviceNoDeviceName = configurationsMock.invalidDeviceNoDeviceName;
 
 describe('Device', () => {
   let fs;
@@ -89,21 +87,21 @@ describe('Device', () => {
     }
   }
 
-  function schemeDevice(scheme, configuration) {
-    const device = new Device({
+  function schemeDevice(scheme, configuration, overrides) {
+    const device = new Device(_.merge({
       deviceConfig: scheme.configurations[configuration],
       deviceDriver: driverMock.driver,
       sessionConfig: scheme.session,
       emitter,
-    });
+    }, overrides));
 
     device.deviceDriver.acquireFreeDevice.mockReturnValue('mockDeviceId');
 
     return device;
   }
 
-  function validDevice() {
-    return schemeDevice(validScheme, 'ios.sim.release');
+  function validDevice(overrides) {
+    return schemeDevice(validScheme, 'ios.sim.release', overrides);
   }
 
   it('should return the name from the driver', async () => {
@@ -124,43 +122,6 @@ describe('Device', () => {
     expect(device.id).toEqual('mockDeviceId');
   });
 
-  describe('prepare()', () => {
-    it(`valid scheme, no binary, should throw`, async () => {
-      const device = validDevice();
-      fs.existsSync.mockReturnValue(false);
-      try {
-        await device.prepare();
-        fail('should throw')
-      } catch (ex) {
-        expect(ex.message).toMatch(/app binary not found at/)
-      }
-    });
-
-    it(`valid scheme, no binary, should not throw`, async () => {
-      const device = validDevice();
-      await device.prepare();
-    });
-
-    it(`when reuse is enabled in CLI args should not uninstall and install`, async () => {
-      const device = validDevice();
-      argparse.getArgValue.mockReturnValue(true);
-
-      await device.prepare();
-
-      expect(driverMock.driver.uninstallApp).not.toHaveBeenCalled();
-      expect(driverMock.driver.installApp).not.toHaveBeenCalled();
-    });
-
-    it(`when reuse is enabled in params should not uninstall and install`, async () => {
-      const device = validDevice();
-
-      await device.prepare({reuse: true});
-
-      expect(driverMock.driver.uninstallApp).not.toHaveBeenCalled();
-      expect(driverMock.driver.installApp).not.toHaveBeenCalled();
-    });
-  });
-
   describe('re/launchApp()', () => {
     const expectedDriverArgs = {
       "detoxServer": "ws://localhost:8099",
@@ -343,7 +304,8 @@ describe('Device', () => {
       device.deviceDriver.getBundleIdFromBinary.mockReturnValue('test.bundle');
       device.deviceDriver.launchApp.mockReturnValue(processId);
 
-      await device.prepare({launchApp: true});
+      await device.prepare();
+      await device.launchApp({newInstance: true});
       await device.launchApp({newInstance: false});
 
       expect(driverMock.driver.deliverPayload).not.toHaveBeenCalled();
@@ -355,7 +317,8 @@ describe('Device', () => {
       device.deviceDriver.getBundleIdFromBinary.mockReturnValue('test.bundle');
       device.deviceDriver.launchApp.mockReturnValue(processId);
 
-      await device.prepare({launchApp: true});
+      await device.prepare();
+      await device.launchApp({newInstance: true});
       await device.launchApp({url: 'url://me'});
 
       expect(driverMock.driver.deliverPayload).toHaveBeenCalledTimes(1);
@@ -384,7 +347,8 @@ describe('Device', () => {
       device.deviceDriver.getBundleIdFromBinary.mockReturnValue('test.bundle');
       device.deviceDriver.launchApp.mockReturnValue(processId);
 
-      await device.prepare({launchApp: true});
+      await device.prepare();
+      await device.launchApp({newInstance: true});
       await device.launchApp(launchParams);
 
       expect(driverMock.driver.deliverPayload).toHaveBeenCalledWith({delayPayload: true, url: 'url://me'});
@@ -414,7 +378,8 @@ describe('Device', () => {
       device.deviceDriver.launchApp.mockReturnValueOnce(processId).mockReturnValueOnce(processId);
       device.deviceDriver.createPayloadFile = () => 'url';
 
-      await device.prepare({launchApp: true});
+      await device.prepare();
+      await device.launchApp({newInstance: true});
       await device.launchApp(launchParams);
 
       expect(driverMock.driver.deliverPayload).toHaveBeenCalledWith({delayPayload: true, detoxUserActivityDataURL: 'url'});
@@ -430,7 +395,8 @@ describe('Device', () => {
       device.deviceDriver.launchApp.mockReturnValueOnce(processId).mockReturnValueOnce(processId);
       device.deviceDriver.createPayloadFile = () => 'url';
 
-      await device.prepare({launchApp: true});
+      await device.prepare();
+      await device.launchApp({newInstance: true});
       await device.launchApp(launchParams);
 
       expect(driverMock.driver.deliverPayload).toHaveBeenCalledTimes(1);
@@ -478,7 +444,7 @@ describe('Device', () => {
 
       await device.installApp('newAppPath');
 
-      expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, 'newAppPath', undefined);
+      expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, 'newAppPath', device._deviceConfig.testBinaryPath);
     });
 
     it(`with a custom test app path should use custom test app path`, async () => {
@@ -494,7 +460,7 @@ describe('Device', () => {
 
       await device.installApp();
 
-      expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, device._binaryPath, device._testBinaryPath);
+      expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, device._deviceConfig.binaryPath, device._deviceConfig.testBinaryPath);
     });
   });
 
@@ -512,7 +478,7 @@ describe('Device', () => {
 
       await device.uninstallApp();
 
-      expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith(device._deviceId, device._binaryPath);
+      expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith(device._deviceId, device._bundleId);
     });
   });
 
@@ -713,35 +679,6 @@ describe('Device', () => {
     expect(driverMock.driver.cleanup).toHaveBeenCalledTimes(1);
   });
 
-  it(`_cleanup() should shutdown a prepared device if --cleanup is passed from CLI`, async () => {
-    argparse.getArgValue.mockReturnValue(true);
-
-    const device = validDevice();
-    await device.prepare();
-    await device._cleanup();
-
-    expect(driverMock.driver.shutdown).toHaveBeenCalled();
-  });
-
-  it(`_cleanup() should not shutdown a prepared device if --cleanup is not passed from CLI`, async () => {
-    argparse.getArgValue.mockReturnValue(false);
-
-    const device = validDevice();
-    await device.prepare();
-    await device._cleanup();
-
-    expect(driverMock.driver.shutdown).not.toHaveBeenCalled();
-  });
-
-  it(`_cleanup() should not shutdown an unprepared device even if --cleanup is passed from CLI`, async () => {
-    argparse.getArgValue.mockReturnValue(true);
-
-    const device = validDevice();
-    await device._cleanup();
-
-    expect(driverMock.driver.shutdown).not.toHaveBeenCalled();
-  });
-
   it(`new Device() with invalid device config (no binary) should throw`, () => {
     // TODO: this is an invalid test, because it will pass only on SimulatorDriver
     expect(() => new Device({
@@ -759,7 +696,7 @@ describe('Device', () => {
 
   it(`should accept relative path for binary`, async () => {
     const actualPath = await launchAndTestBinaryPath('relativePath');
-    expect(actualPath).toEqual(path.join(process.cwd(), 'abcdef/123'));
+    expect(actualPath).toEqual('abcdef/123');
   });
 
   it(`pressBack() should invoke driver's pressBack()`, async () => {
@@ -816,7 +753,7 @@ describe('Device', () => {
     const device = schemeDevice(configurationsMock.pathsTests, configuration);
 
     await device.prepare();
-    await device.launchApp();
+    await device.installApp();
 
     return driverMock.driver.installApp.mock.calls[0][1];
   }
diff --git a/detox/src/devices/DriverRegistry.test.js b/detox/src/devices/DriverRegistry.test.js
new file mode 100644
index 000000000..7cdae0a21
--- /dev/null
+++ b/detox/src/devices/DriverRegistry.test.js
@@ -0,0 +1,85 @@
+jest.mock('./drivers/ios/IosDriver');
+jest.mock('./drivers/ios/SimulatorDriver');
+jest.mock('./drivers/android/EmulatorDriver');
+jest.mock('./drivers/android/AttachedAndroidDriver');
+jest.mock('../utils/resolveModuleFromPath');
+
+describe('DriverRegistry', () => {
+  let DriverRegistry;
+  let registry, opts;
+
+  beforeEach(() => {
+    DriverRegistry = require('./DriverRegistry');
+
+    opts = {
+      client: { whatever: Math.random() },
+      emitter: { anything: Math.random() },
+    };
+  });
+
+  describe('by default', () => {
+    beforeEach(() => {
+      registry = DriverRegistry.default;
+    });
+
+    it('should resolve "ios.none" to IosDriver', () => {
+      const IosDriver = require('./drivers/ios/IosDriver');
+      const driver = registry.resolve('ios.none', opts);
+
+      expect(driver).toBeInstanceOf(IosDriver);
+      expect(IosDriver).toHaveBeenCalledWith(opts);
+    });
+
+    it('should resolve "ios.simulator" to SimulatorDriver', () => {
+      const SimulatorDriver = require('./drivers/ios/SimulatorDriver');
+      const driver = registry.resolve('ios.simulator', opts);
+
+      expect(driver).toBeInstanceOf(SimulatorDriver);
+      expect(SimulatorDriver).toHaveBeenCalledWith(opts);
+    });
+
+    it('should resolve "android.attached" to AttachedAndroidDriver', () => {
+      const AttachedAndroidDriver = require('./drivers/android/AttachedAndroidDriver');
+      const driver = registry.resolve('android.attached', opts);
+
+      expect(driver).toBeInstanceOf(AttachedAndroidDriver);
+      expect(AttachedAndroidDriver).toHaveBeenCalledWith(opts);
+    });
+
+    it('should resolve "android.emulator" to EmulatorDriver', () => {
+      const EmulatorDriver = require('./drivers/android/EmulatorDriver');
+      const driver = registry.resolve('android.emulator', opts);
+
+      expect(driver).toBeInstanceOf(EmulatorDriver);
+      expect(EmulatorDriver).toHaveBeenCalledWith(opts);
+    });
+
+    it('should try to resolve unknown driver as a node.js dependency', () => {
+      require('../utils/resolveModuleFromPath').mockImplementation(() => {
+        return require('./drivers/__mocks__/FakeDriver');
+      });
+
+      const FakeDriver = require('./drivers/__mocks__/FakeDriver')
+      const driver = registry.resolve('fake-driver', opts);
+
+      expect(driver).toBeInstanceOf(FakeDriver);
+      expect(driver.constructorArgs).toEqual([opts]);
+    });
+
+    it('should throw errors if it cannot resolve a driver', () => {
+      expect(() => registry.resolve('imaginary.driver', opts))
+        .toThrowError(/imaginary.driver.*not supported/);
+    });
+  });
+
+  describe('constructor', () => {
+    it('should be extensible', () => {
+      const FakeDriver101 = require('./drivers/__mocks__/FakeDriver');
+      const registry = new DriverRegistry({ FakeDriver101 });
+      const driver = registry.resolve('FakeDriver101', opts);
+
+      expect(driver).toBeInstanceOf(FakeDriver101);
+      expect(driver.constructorArgs).toEqual([opts]);
+    });
+  });
+});
diff --git a/detox/src/devices/drivers/android/AndroidDriver.test.js b/detox/src/devices/drivers/android/AndroidDriver.test.js
index b320134af..97b147dc5 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.test.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.test.js
@@ -3,10 +3,21 @@ describe('Android driver', () => {
   const deviceId = 'device-id-mock';
   const bundleId = 'bundle-id-mock';
 
+  const mockGetAbsoluteBinaryPathImpl = (x) => `absolutePathOf(${x})`;
+  const mockAPKPathGetTestApkPathImpl = (x) => `testApkPathOf(${x})`;
+
   let logger;
   let client;
+  let getAbsoluteBinaryPath;
+  let fs;
   let exec;
   beforeEach(() => {
+    jest.mock('fs', () => ({
+      existsSync: jest.fn(),
+      realpathSync: jest.fn(),
+    }));
+    fs = require('fs');
+
     jest.mock('../../../utils/encoding', () => ({
       encodeBase64: (x) => `base64(${x})`,
     }));
@@ -19,6 +30,15 @@ describe('Android driver', () => {
       InstrumentationLogsParser: mockInstrumentationLogsParserClass,
     }));
 
+    jest.mock('../../../utils/getAbsoluteBinaryPath', () =>
+      jest.fn().mockImplementation((x) => `absolutePathOf(${x})`),
+    );
+    getAbsoluteBinaryPath = require('../../../utils/getAbsoluteBinaryPath');
+
+    jest.mock('./tools/APKPath', () => ({
+      getTestApkPath: mockAPKPathGetTestApkPathImpl,
+    }));
+
     const mockLogger = {
       warn: jest.fn(),
     };
@@ -213,6 +233,49 @@ describe('Android driver', () => {
     });
   });
 
+  describe('App installation', () => {
+    const binaryPath = 'mock-bin-path';
+    const testBinaryPath = 'mock-test-bin-path';
+
+    it('should adb-install the app\'s binary', async () => {
+      await uut.installApp(deviceId, binaryPath, testBinaryPath);
+
+      expect(getAbsoluteBinaryPath).toHaveBeenCalledWith(binaryPath);
+      expect(uut.adb.install).toHaveBeenCalledWith(deviceId, mockGetAbsoluteBinaryPathImpl(binaryPath));
+    });
+
+    it('should adb-install the test binary', async () => {
+      await uut.installApp(deviceId, binaryPath, testBinaryPath);
+
+      expect(getAbsoluteBinaryPath).toHaveBeenCalledWith(binaryPath);
+      expect(uut.adb.install).toHaveBeenCalledWith(deviceId, mockGetAbsoluteBinaryPathImpl(testBinaryPath));
+    });
+
+    it('should resort to auto test-binary path resolution, if not specific', async () => {
+      const expectedTestBinPath = mockAPKPathGetTestApkPathImpl(mockGetAbsoluteBinaryPathImpl(binaryPath));
+
+      fs.existsSync.mockReturnValue(true);
+
+      await uut.installApp(deviceId, binaryPath, undefined);
+
+      expect(fs.existsSync).toHaveBeenCalledWith(expectedTestBinPath);
+      expect(uut.adb.install).toHaveBeenCalledWith(deviceId, expectedTestBinPath);
+    });
+
+    it('should throw if auto test-binary path resolves an invalid file', async () => {
+      const expectedTestBinPath = mockAPKPathGetTestApkPathImpl(mockGetAbsoluteBinaryPathImpl(binaryPath));
+
+      fs.existsSync.mockReturnValue(false);
+
+      try {
+        await uut.installApp(deviceId, binaryPath, undefined);
+        fail('Expected an error');
+      } catch (err) {
+        expect(err.message).toContain(`'${expectedTestBinPath}'`);
+      }
+    });
+  });
+
   describe('net-port reversing', () => {
     const deviceId = 1010;
     const port = 1337;
@@ -234,6 +297,7 @@ class mockADBClass {
     this.getInstrumentationRunner = jest.fn();
     this.reverse = jest.fn();
     this.reverseRemove = jest.fn();
+    this.install = jest.fn();
 
     this.adbBin = 'ADB binary mock';
   }
diff --git a/detox/src/devices/drivers/android/tools/ADB.test.js b/detox/src/devices/drivers/android/tools/ADB.test.js
index bcfac0f21..3d70003f8 100644
--- a/detox/src/devices/drivers/android/tools/ADB.test.js
+++ b/detox/src/devices/drivers/android/tools/ADB.test.js
@@ -1,4 +1,3 @@
-
 describe('ADB', () => {
   const adbBinPath = `/Android/sdk-mock/platform-tools/adb`;
 
@@ -130,9 +129,6 @@ describe('ADB', () => {
     });
   });
 
-  describe('lookup device', () => {
-  });
-
   it(`install`, async () => {
     await adb.install('emulator-5556', 'path inside "quotes" to/app');
 
@@ -167,6 +163,27 @@ describe('ADB', () => {
     expect(await adb.pidof('', 'com.google.android.ext.services')).toBe(NaN);
   });
 
+  it('push', async () => {
+    const deviceId = 'mockEmulator';
+    const sourceFile = '/mock-source/file.xyz';
+    const destFile = '/sdcard/file.abc';
+    await adb.push(deviceId, sourceFile, destFile);
+
+    expect(exec).toHaveBeenCalledWith(
+      expect.stringContaining(`-s mockEmulator push "${sourceFile}" "${destFile}"`),
+      undefined, undefined, expect.anything());
+  });
+
+  it('remote-install', async () => {
+    const deviceId = 'mockEmulator';
+    const binaryPath = '/mock-path/filename.mock';
+    await adb.remoteInstall(deviceId, binaryPath);
+
+    expect(exec).toHaveBeenCalledWith(
+      expect.stringContaining(`-s mockEmulator shell "pm install -r -g -t ${binaryPath}"`),
+      undefined, undefined, expect.anything());
+  });
+
   describe('unlockScreen', () => {
     const deviceId = 'mockEmulator';
 
diff --git a/detox/src/devices/drivers/android/tools/AppInstallHelper.test.js b/detox/src/devices/drivers/android/tools/AppInstallHelper.test.js
new file mode 100644
index 000000000..2d03374e9
--- /dev/null
+++ b/detox/src/devices/drivers/android/tools/AppInstallHelper.test.js
@@ -0,0 +1,86 @@
+const deviceId = 'mock-device-id';
+const deviceTempDir = '/mock-tmp-dir';
+const appBinaryPath = '/mock-app-binary-path/binary.apk';
+const testBinaryPath = '/mock-test-binary-path/test/binary.apk';
+
+describe('Android app installation helper', () => {
+  let adb;
+  class MockAdbClass {
+    constructor() {
+      this.shell = (...args) => adb.shell(...args);
+      this.push = (...args) => adb.push(...args);
+      this.remoteInstall = (...args) => adb.remoteInstall(...args);
+    }
+  }
+
+  beforeEach(() => {
+    const ADBMock = jest.genMockFromModule('./ADB');
+    adb = new ADBMock();
+    jest.mock('./ADB', () => MockAdbClass);
+  });
+
+  let uut;
+  beforeEach(() => {
+    const AppInstallHelper = require('./AppInstallHelper');
+    uut = new AppInstallHelper(adb, deviceTempDir);
+  });
+
+  it('should recreate a temp dir on the device', async () => {
+    await uut.install(deviceId, appBinaryPath, testBinaryPath);
+
+    expect(adb.shell).toHaveBeenCalledWith(deviceId, `rm -fr ${deviceTempDir}`);
+    expect(adb.shell).toHaveBeenCalledWith(deviceId, `mkdir -p ${deviceTempDir}`);
+  });
+
+  it('should throw if shell command fails', async () => {
+    adb.shell.mockRejectedValue(new Error('mocked error in adb-shell'));
+
+    try {
+      await uut.install(deviceId, appBinaryPath, testBinaryPath);
+      fail('expected to throw');
+    } catch (err) {}
+  });
+
+  it('should push app-binary file to temp dir on device', async () => {
+    await uut.install(deviceId, appBinaryPath, testBinaryPath);
+    expect(adb.push).toHaveBeenCalledWith(deviceId, appBinaryPath, '/mock-tmp-dir/Application.apk');
+  });
+
+  it('should push test-binary file to temp dir on device', async () => {
+    await uut.install(deviceId, appBinaryPath, testBinaryPath);
+    expect(adb.push).toHaveBeenCalledWith(deviceId, testBinaryPath, '/mock-tmp-dir/Test.apk');
+  });
+
+  it('should fail if adb-push fails', async () => {
+    adb.push.mockRejectedValue(new Error('mocked error in adb-push'));
+
+    try {
+      await uut.install(deviceId, appBinaryPath, testBinaryPath);
+      fail('expected to throw');
+    } catch(err) {}
+  });
+
+  it('should remote-install both binaries via shell', async () => {
+    await uut.install(deviceId, appBinaryPath, testBinaryPath);
+    expect(adb.remoteInstall).toHaveBeenCalledWith(deviceId, '/mock-tmp-dir/Application.apk');
+    expect(adb.remoteInstall).toHaveBeenCalledWith(deviceId, '/mock-tmp-dir/Test.apk');
+  });
+
+  it('should fail if remote-install fails', async () => {
+    adb.remoteInstall.mockRejectedValue(new Error('mocked error in remote-install'));
+
+    try {
+      await uut.install(deviceId, appBinaryPath, testBinaryPath);
+      fail('expected to throw');
+    } catch(err) {}
+  });
+
+  it('should use a default temp-dir', async () => {
+    const AppInstallHelper = require('./AppInstallHelper');
+    uut = new AppInstallHelper(adb, undefined);
+
+    await uut.install(deviceId, appBinaryPath, testBinaryPath);
+
+    expect(adb.push).toHaveBeenCalledWith(deviceId, appBinaryPath, '/data/local/tmp/detox/Application.apk');
+  });
+});
diff --git a/detox/src/devices/drivers/android/tools/AppUninstallHelper.test.js b/detox/src/devices/drivers/android/tools/AppUninstallHelper.test.js
new file mode 100644
index 000000000..3ff8be1ac
--- /dev/null
+++ b/detox/src/devices/drivers/android/tools/AppUninstallHelper.test.js
@@ -0,0 +1,76 @@
+const deviceId = 'mock-device-id';
+const bundleId = 'mock-bundle-id';
+const testBundleId = 'mock-bundle-id.test';
+
+describe('Android app uninstall helper', () => {
+  let adb;
+  class MockAdbClass {
+    constructor() {
+      this.uninstall = (...args) => adb.uninstall(...args);
+      this.isPackageInstalled = (...args) => adb.isPackageInstalled(...args);
+    }
+  }
+
+  beforeEach(() => {
+    const ADBMock = jest.genMockFromModule('./ADB');
+    adb = new ADBMock();
+    adb.isPackageInstalled.mockResolvedValue(true);
+
+    jest.mock('./ADB', () => MockAdbClass);
+  });
+
+  let uut;
+  beforeEach(() => {
+    const AppUninstallHelper = require('./AppUninstallHelper');
+    uut = new AppUninstallHelper(adb);
+  });
+
+  it('should uninstall the app\'s binary using adb', async () => {
+    await uut.uninstall(deviceId, bundleId);
+    expect(adb.uninstall).toHaveBeenCalledWith(deviceId, bundleId);
+  });
+
+  it('should fail if app uninstall fails', async () => {
+    adb.uninstall.mockRejectedValue(new Error('mocked error in adb.uninstall'));
+    try {
+      await uut.uninstall(deviceId, bundleId);
+      fail('expected an error');
+    } catch (err) {}
+  });
+
+  it('should avoid uninstalling app if not already installed', async () => {
+    adb.isPackageInstalled.mockResolvedValue(false);
+
+    await uut.uninstall(deviceId, bundleId);
+
+    expect(adb.isPackageInstalled).toHaveBeenCalledWith(deviceId, bundleId);
+    expect(adb.uninstall).not.toHaveBeenCalledWith(deviceId, bundleId);
+  });
+
+  it('should uninstall the test binary using adb', async () => {
+    await uut.uninstall(deviceId, bundleId);
+    expect(adb.uninstall).toHaveBeenCalledWith(deviceId, testBundleId);
+  });
+
+  it('should fail if test binary uninstall fails', async () => {
+    adb.uninstall
+      .mockResolvedValueOnce(true)
+      .mockRejectedValueOnce(new Error('mocked error in adb.uninstall'));
+
+    try {
+      await uut.uninstall(deviceId, bundleId);
+      fail('expected an error');
+    } catch (err) {}
+  });
+
+  it('should avoid uninstalling test binary if not already installed', async () => {
+    adb.isPackageInstalled
+      .mockResolvedValueOnce(true)
+      .mockResolvedValueOnce(false);
+
+    await uut.uninstall(deviceId, bundleId);
+
+    expect(adb.isPackageInstalled).toHaveBeenCalledWith(deviceId, testBundleId);
+    expect(adb.uninstall).not.toHaveBeenCalledWith(deviceId, testBundleId);
+  });
+});
diff --git a/detox/src/index.test.js b/detox/src/index.test.js
index 8ef84108a..73a75ce10 100644
--- a/detox/src/index.test.js
+++ b/detox/src/index.test.js
@@ -1,257 +1,243 @@
-const schemes = require('./configurations.mock');
+const _ = require('lodash');
+
+jest.mock('./utils/logger');
+jest.mock('./configuration');
+jest.mock('./Detox');
+jest.mock('./utils/MissingDetox');
+
+const testUtils = {
+  randomObject: () => ({ [Math.random()]: Math.random() }),
+};
 
 describe('index', () => {
-  let detox;
-  let mockDevice;
-  let mockDetox;
+  let logger;
+  let configuration;
+  let Detox;
+  let index;
+  let detoxConfig;
+  let detoxInstance;
 
   beforeEach(() => {
-    mockDevice = { launchApp: jest.fn() };
-    mockDetox = {
-      init: jest.fn(() => {
-        mockDetox.device = mockDevice;
-        mockDetox.by = {
-          id: jest.fn(),
-        };
-      }),
-      deviceName: jest.fn(),
-      cleanup: jest.fn(),
-      beforeEach: jest.fn(),
-      afterEach: jest.fn(),
+    // valid enough configuration to pass with mocked dependencies
+    detoxConfig = {
+      behaviorConfig: {
+        init: {
+          exposeGlobals: _.sample([false, true]),
+        },
+      },
     };
 
-    jest
-      .mock('./server/DetoxServer')
-      .mock('./devices/Device')
-      .mock('./utils/logger')
-      .mock('./client/Client')
-      .mock('./Detox', () => {
-        const MissingDetox = require('./utils/MissingDetox');
-        const none = new MissingDetox();
-        none.cleanupContext = () => {}; // avoid ruining global state
-
-        return Object.assign(jest.fn(() => mockDetox), { none });
-      });
-
-    process.env.DETOX_UNIT_TEST = true;
-    detox = require('./index');
-  });
-
-  afterEach(() => {
-    process.env = {};
-    jest
-      .unmock('./server/DetoxServer')
-      .unmock('./devices/Device')
-      .unmock('./client/Client')
-      .unmock('./Detox')
-  });
-
-  it(`constructs detox without globals if initGlobals = false`, async () => {
-    const Detox = require('./Detox');
-
-    await detox.init(schemes.validOneDeviceNoSession, { initGlobals: false });
-
-    expect('by' in global).toBe(false);
-  });
-
-  it(`throws if there was no config passed`, async () => {
-    const logger = require('./utils/logger');
-    let exception = undefined;
-
-    try {
-      await detox.init();
-    } catch (e) {
-      exception = e;
-    }
-
-    expect(exception).toBeDefined();
-    expect(logger.error).toHaveBeenCalledWith({ event: 'DETOX_INIT_ERROR' }, '\n', exception);
+    logger = require('./utils/logger');
+    configuration = require('./configuration');
+    configuration.composeDetoxConfig.mockImplementation(async () => detoxConfig);
+
+    Detox = require('./Detox');
+
+    const MissingDetox = require('./utils/MissingDetox');
+    Detox.none = new MissingDetox();
+
+    index = require('./index');
+  });
+
+  describe('public interface', () => {
+    it.each([
+      ['an', 'object',   'DetoxConstants'],
+      ['an', 'object',   'by'],
+      ['an', 'object',   'device'],
+      ['a',  'function', 'init'],
+      ['a',  'function', 'cleanup'],
+      ['a',  'function', 'beforeEach'],
+      ['a',  'function', 'afterEach'],
+      ['a',  'function', 'suiteStart'],
+      ['a',  'function', 'suiteEnd'],
+      ['a',  'function', 'element'],
+      ['a',  'function', 'expect'],
+      ['a',  'function', 'waitFor'],
+    ])('should export %s %s called .%s', (_1, type, name) => {
+      expect(typeof index[name]).toBe(type);
+    });
   });
 
-  it(`throws if there is no devices in config`, async () => {
-    let exception = undefined;
+  describe('detox.init(config[, userParams])', () => {
+    it(`should pass args via calling configuration.composeDetoxConfig(config, userParams)`, async () => {
+      const [config, userParams] = [1, 2].map(testUtils.randomObject);
+      await index.init(config, userParams).catch(() => {});
 
-    try {
-      await detox.init(schemes.invalidNoDevice);
-    } catch (e) {
-      exception = e;
-    }
-
-    expect(exception).toBeDefined();
-  });
+      expect(configuration.composeDetoxConfig).toHaveBeenCalledWith(config, userParams);
+    });
 
-  it(`constructs detox with device config`, async () => {
-    const Detox = require('./Detox');
+    describe('when configuration is valid', () => {
+      beforeEach(async () => {
+        detox = await index.init();
+      });
 
-    await detox.init(schemes.validOneDeviceNoSession);
+      it(`should create a Detox instance with the composed config object`, () =>
+        expect(Detox).toHaveBeenCalledWith(detoxConfig));
 
-    expect(Detox).toHaveBeenCalledWith({
-      artifactsConfig: expect.objectContaining({
-        plugins: schemes.pluginsDefaultsResolved,
-        pathBuilder: expect.objectContaining({
-          rootDir: expect.stringMatching(/^artifacts[\\\/]ios\.sim\.release/),
-        }),
-      }),
-      deviceConfig: schemes.validOneDeviceNoSession.configurations['ios.sim.release'],
-      session: undefined,
-    });
-  });
+      it(`should return a Detox instance`, () =>
+        expect(detox).toBeInstanceOf(Detox));
 
-  it(`constructs detox with device config passed in '--configuration' cli value`, async () => {
-    process.env.configuration = 'ios.sim.debug';
-    const Detox = require('./Detox');
-
-    await detox.init(schemes.validTwoDevicesNoSession);
-
-    expect(Detox).toHaveBeenCalledWith({
-      artifactsConfig: expect.objectContaining({
-        plugins: schemes.pluginsDefaultsResolved,
-        pathBuilder: expect.objectContaining({
-          rootDir: expect.stringMatching(/^artifacts[\\\/]ios\.sim\.debug/),
-        }),
-      }),
-      deviceConfig: schemes.validTwoDevicesNoSession.configurations['ios.sim.debug'],
-      session: undefined,
+      it(`should set the last error to be null in Detox.none's storage`, () =>
+        expect(Detox.none.setError).toHaveBeenCalledWith(null));
     });
-  });
 
-  it(`throws if device passed in '--configuration' cli option doesn't exist`, async () => {
-    let exception = undefined;
-    process.env.configuration = 'nonexistent';
+    describe('when configuration is invalid', () => {
+      let configError, initPromise;
 
-    try {
-      await detox.init(schemes.validTwoDevicesNoSession);
-    } catch (e) {
-      exception = e;
-    }
+      beforeEach(async () => {
+        configError = new Error('Configuration test error');
 
-    expect(exception).toBeDefined();
-  });
+        configuration.composeDetoxConfig.mockImplementation(async () => {
+          throw configError;
+        });
 
-  it(`constructs detox with device name passed in '--device-name' cli value`, async () => {
-    process.env.deviceName = 'iPhone X';
-    const Detox = require('./Detox');
+        initPromise = index.init();
+        await initPromise.catch(() => {});
+      });
 
-    await detox.init(schemes.validOneDeviceNoSession);
+      it(`should rethrow that configuration error`, async () => {
+        await expect(initPromise).rejects.toThrowError(configError);
+      });
 
-    const expectedConfig = {
-      ...schemes.validOneDeviceNoSession.configurations['ios.sim.release'],
-      device: 'iPhone X'
-    };
+      it(`should not create a Detox instance`, () => {
+        expect(Detox).not.toHaveBeenCalled();
+      });
 
-    expect(Detox).toHaveBeenCalledWith({
-      artifactsConfig: expect.objectContaining({
-        plugins: schemes.pluginsDefaultsResolved,
-        pathBuilder: expect.objectContaining({
-          rootDir: expect.stringMatching(/^artifacts[\\\/]ios\.sim\.release/),
-        }),
-      }),
-      deviceConfig: expectedConfig,
-      session: undefined,
-    });
-  });
+      it(`should always mutate global with Detox.none vars`, async () => {
+        expect(Detox.none.initContext).toHaveBeenCalledWith(global);
+      });
 
-  it(`throws if a device has no name`, async () => {
-    let exception = undefined;
+      it(`should set the last error to Detox.none's storage`, async () => {
+        expect(Detox.none.setError).toHaveBeenCalledWith(configError);
+      });
 
-    try {
-      await detox.init(schemes.invalidDeviceNoDeviceName);
-    } catch (e) {
-      exception = e;
-    }
+      it(`should log that error with the logger`, async () => {
+        expect(logger.error).toHaveBeenCalledWith(
+          { event: 'DETOX_INIT_ERROR' },
+          '\n',
+          configError
+        );
+      });
+    });
 
-    expect(exception).toBeDefined();
-  });
+    describe('when behaviorConfig.init.exposeGlobals = true', () => {
+      beforeEach(async () => {
+        detoxConfig.behaviorConfig.init.exposeGlobals = true;
+        detox = await index.init();
+      });
 
-  it(`throws if a device is invalid`, async () => {
-    let exception = undefined;
+      it(`should touch globals with Detox.none.initContext`, () => {
+        expect(Detox.none.initContext).toHaveBeenCalledWith(global);
+      });
+    });
 
-    try {
-      await detox.init(schemes.invalidDeviceNoDeviceType);
-    } catch (e) {
-      exception = e;
-    }
+    describe('when behaviorConfig.init.exposeGlobals = false', () => {
+      beforeEach(async () => {
+        detoxConfig.behaviorConfig.init.exposeGlobals = false;
+        detox = await index.init();
+      });
 
-    expect(exception).toBeDefined();
+      it(`should not touch globals with Detox.none.initContext`, () => {
+        expect(Detox.none.initContext).not.toHaveBeenCalled();
+      });
+    });
   });
 
-  it(`throws if a device is invalid`, async () => {
-    let exception = undefined;
+  describe('detox.cleanup()', () => {
+    describe('when called before detox.init()', () => {
+      beforeEach(() => index.cleanup());
 
-    try {
-      await detox.init(schemes.invalidDeviceNoDeviceType);
-    } catch (e) {
-      exception = e;
-    }
+      it('should nevertheless cleanup globals with Detox.none.cleanupContext', () =>
+        expect(Detox.none.cleanupContext).toHaveBeenCalledWith(global));
+    });
 
-    expect(exception).toBeDefined();
-  });
+    describe('when called after detox.init()', () => {
+      beforeEach(async () => {
+        detox = await index.init();
+        await index.cleanup();
+      });
 
-  it(`initializes detox`, async () => {
-    const params = {};
+      it('should call cleanup in the current Detox instance', () =>
+        expect(detox.cleanup).toHaveBeenCalled());
 
-    await detox.init(schemes.validOneDeviceNoSession, params);
+      it('should call cleanup globals with Detox.none.cleanupContext', () =>
+        expect(Detox.none.cleanupContext).toHaveBeenCalledWith(global));
 
-    expect(mockDetox.init).toHaveBeenCalledWith(params);
-  });
+      describe('twice', () => {
+        beforeEach(() => index.cleanup());
 
-  it(`Basic usage`, async() => {
-    await detox.init(schemes.validOneDeviceNoSession);
-    await detox.cleanup();
+        it('should not call cleanup twice in the former Detox instance', () =>
+          expect(detox.cleanup).toHaveBeenCalledTimes(1));
+      });
+    });
   });
 
-  it(`Basic usage with memorized exported objects`, async() => {
-    const { device, by } = detox;
-
-    expect(() => { device, by }).not.toThrowError();
-    expect(() => { device.launchApp }).toThrowError();
-    expect(() => { by.id }).toThrowError();
+  describe.each([
+    ['beforeEach'],
+    ['afterEach'],
+    ['suiteStart'],
+    ['suiteEnd'],
+    ['element'],
+    ['expect'],
+    ['waitFor'],
+  ])('detox.%s()', (method) => {
+    let randomArgs;
 
-    await detox.init(schemes.validOneDeviceNoSession);
-
-    expect(device.launchApp).toEqual(expect.any(Function));
-    expect(by.id).toEqual(expect.any(Function));
+    beforeEach(() => {
+      randomArgs = [1, 2].map(testUtils.randomObject);
+    });
 
-    await detox.cleanup();
+    describe('before detox.init() has been called', () => {
+      beforeEach(() => {
+        Detox.none[method] = jest.fn();
+      });
 
-    expect(() => { device, by }).not.toThrowError();
-    expect(() => { device.launchApp }).toThrowError();
-    expect(() => { by.id }).toThrowError();
-  });
+      it(`should forward calls to the Detox.none instance`, async () => {
+        await index[method](...randomArgs);
+        expect(Detox.none[method]).toHaveBeenCalledWith(...randomArgs);
+      });
+    });
 
-  it(`Basic usage, do not throw an error if cleanup is done twice`, async() => {
-    await detox.init(schemes.validOneDeviceNoSession);
-    await detox.cleanup();
-    await detox.cleanup();
-  });
+    describe('after detox.init() has been called', () => {
+      beforeEach(async () => {
+        detoxConfig = { behaviorConfig: { init: {} } };
+        detoxInstance = await index.init();
+        detoxInstance[method] = jest.fn();
+      });
 
-  it(`Basic usage, if detox is undefined, do not throw an error`, async() => {
-    await detox.cleanup();
+      it(`should forward calls to the current Detox instance`, async () => {
+        await index[method](...randomArgs);
+        expect(detoxInstance[method]).toHaveBeenCalledWith(...randomArgs);
+      });
+    });
   });
 
-  it(`beforeEach() should be covered - with detox initialized`, async() => {
-    await detox.init(schemes.validOneDeviceNoSession);
-    await detox.beforeEach();
-  });
+  describe.each([
+    ['by'],
+    ['device'],
+  ])('detox.%s', (property) => {
+    describe('before detox.init() has been called', () => {
+      beforeEach(() => {
+        Detox.none[property] = testUtils.randomObject();
+      });
 
-  it(`beforeEach() should be covered - with detox not initialized`, async() => {
-    await expect(detox.beforeEach()).rejects.toThrow(/Make sure to call/);
-  });
+      it(`should return value of Detox.none["${property}"]`, () => {
+        expect(index[property]).toEqual(Detox.none[property]);
+      });
+    });
 
-  it(`error message should be covered - with detox failed to initialize`, async() => {
-    mockDetox.init.mockReturnValue(Promise.reject(new Error('InitMe error')));
+    describe('after detox.init() has been called', () => {
+      beforeEach(async () => {
+        detoxConfig = { behaviorConfig: { init: {} } };
+        detoxInstance = await index.init();
+        detoxInstance[property] = testUtils.randomObject();
+      });
 
-    await expect(detox.init(schemes.validOneDeviceNoSession)).rejects.toThrow(/InitMe error/);
-    await detox.cleanup(); // cleaning up
-    await expect(detox.beforeEach()).rejects.toThrow(/There was an error[\s\S]*InitMe error/mg);
+      it(`should forward calls to the current Detox instance`, () => {
+        expect(index[property]).toEqual(detoxInstance[property]);
+      });
+    });
   });
 
-  it(`afterEach() should be covered - with detox initialized`, async() => {
-    await detox.init(schemes.validOneDeviceNoSession);
-    await detox.afterEach();
-  });
 
-  it(`afterEach() should be covered - with detox not initialized`, async() => {
-    await expect(detox.afterEach()).rejects.toThrow(/Make sure to call/);
-  });
 });
diff --git a/detox/src/utils/customConsoleLogger.test.js b/detox/src/utils/customConsoleLogger.test.js
index 890ab6a44..0da3a8862 100644
--- a/detox/src/utils/customConsoleLogger.test.js
+++ b/detox/src/utils/customConsoleLogger.test.js
@@ -62,6 +62,16 @@ describe('customConsoleLogger', () => {
     expect(bunyanLogger.mock).toHaveBeenCalledWith(ignored(), expect.stringContaining(expectedOrigin), ignored(), ignored());
   });
 
+  it('should handle unknown file in origin', () => {
+    mockCallsites({file: undefined, line: undefined, col: undefined});
+
+    const logger = require('./customConsoleLogger');
+    logger.override('__log', bunyanLogger.mock);
+    console.__log('');
+
+    expect(bunyanLogger.mock).toHaveBeenCalledWith(ignored(), expect.stringContaining('<unknown>:?:?'), ignored(), ignored());
+  });  
+  
   it('should handle missing file line/column in origin', () => {
     mockCallsites({file: 'mockfilename', line: undefined, col: undefined});
 
diff --git a/detox/src/utils/getAbsoluteBinaryPath.test.js b/detox/src/utils/getAbsoluteBinaryPath.test.js
new file mode 100644
index 000000000..ba6d4c82b
--- /dev/null
+++ b/detox/src/utils/getAbsoluteBinaryPath.test.js
@@ -0,0 +1,19 @@
+const fs = require('fs');
+const path = require('path');
+const getAbsoluteBinaryPath = require('./getAbsoluteBinaryPath');
+
+describe('getAbsoluteBinaryPath', () => {
+  it('should return the given path if it is already absolute', async () => {
+    expect(getAbsoluteBinaryPath('/my/absolute/path')).toEqual('/my/absolute/path');
+  });
+
+  it('should return an absolute path if a relative path is passed in', async () => {
+    expect(getAbsoluteBinaryPath('src/utils/getAbsoluteBinaryPath.js')).toEqual(path.join(process.cwd(), 'src/utils/getAbsoluteBinaryPath.js'));
+  });
+
+  it('should throw exception if resulting absolute path does not exist', async () => {
+    expect(() => getAbsoluteBinaryPath('my/relative/path'))
+      .toThrowError();
+  });
+});
+
diff --git a/detox/src/utils/resolveModuleFromPath.test.js b/detox/src/utils/resolveModuleFromPath.test.js
new file mode 100644
index 000000000..659025a1d
--- /dev/null
+++ b/detox/src/utils/resolveModuleFromPath.test.js
@@ -0,0 +1,27 @@
+const _ = require('lodash');
+const path = require('path');
+const resolveModuleFromPath = require('./resolveModuleFromPath');
+
+const RELATIVE_PATH_TO_PACKAGE_JSON = '../../package.json';
+
+describe('resolveModuleFromPath', () => {
+  let packageJson;
+
+  beforeEach(() => {
+    packageJson = require(RELATIVE_PATH_TO_PACKAGE_JSON);
+  });
+
+  it('should resolve absolute paths', async () => {
+    const absolutePath = require.resolve(RELATIVE_PATH_TO_PACKAGE_JSON);
+    expect(path.isAbsolute(absolutePath)).toBe(true); // an assertion to be on the safe side
+    expect(resolveModuleFromPath(absolutePath)).toBe(packageJson);
+  });
+
+  it('should resolve relative paths', async () => {
+    expect(resolveModuleFromPath(`./package.json`)).toBe(packageJson);
+  });
+
+  it('should resolve node modules', async () => {
+    expect(resolveModuleFromPath('lodash/map')).toBe(require('lodash/map'));
+  });
+});
diff --git a/detox/test/e2e/config.js b/detox/test/e2e/config.js
index 3a20302f5..1eb23ba98 100644
--- a/detox/test/e2e/config.js
+++ b/detox/test/e2e/config.js
@@ -15,5 +15,5 @@ module.exports = {
     "!**/*.test.js"
   ],
   "coverageDirectory": "test/coverage",
-  "coverageReporters": ["lcov", "html"]
+  "coverageReporters": [["lcov", {"projectRoot": "../.." }], "html"]
 };
diff --git a/detox/test/package.json b/detox/test/package.json
index 779658a05..9d3b13ca8 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-test",
-  "version": "16.2.0",
+  "version": "16.4.1",
   "private": true,
   "engines": {
     "node": ">=8.3.0"
@@ -36,10 +36,10 @@
   },
   "devDependencies": {
     "@babel/core": "^7.4.5",
-    "detox": "^16.2.0",
+    "detox": "^16.4.1",
     "express": "^4.15.3",
-    "jest": "25.1.x",
-    "jest-circus": "25.1.x",
+    "jest": "25.3.x",
+    "jest-circus": "25.3.x",
     "jest-junit": "^8.0.0",
     "lodash": "^4.14.1",
     "nyc": "^14.0.0",
@@ -48,6 +48,16 @@
   "detox": {
     "test-runner": "jest",
     "runner-config": "e2e/config.js",
+    "behavior": {
+      "init": {
+        "reinstallApp": true,
+        "launchApp": true,
+        "exposeGlobals": true
+      },
+      "cleanup": {
+        "shutdownDevice": false
+      }
+    },
     "__session": {
       "server": "ws://localhost:8099",
       "sessionId": "test"
diff --git a/examples/demo-plugin/e2e/app-hello.test.js b/examples/demo-plugin/e2e/app-hello.test.js
new file mode 100644
index 000000000..034b00a34
--- /dev/null
+++ b/examples/demo-plugin/e2e/app-hello.test.js
@@ -0,0 +1,15 @@
+describe('Example (hello)', () => {
+  it('should have welcome', async () => {
+    await expect(element(by.id('welcome'))).toBeVisible();
+  });
+
+  it('should not have unwelcome', async () => {
+    let success = true;
+    try {
+      await expect(element(by.id('unwelcome'))).toBeVisible();
+      success = false;
+    } catch (e) {}
+
+    if (!success) throw new Error("test failed");
+  });
+});
diff --git a/examples/demo-plugin/e2e/config.json b/examples/demo-plugin/e2e/config.json
new file mode 100644
index 000000000..523800e2d
--- /dev/null
+++ b/examples/demo-plugin/e2e/config.json
@@ -0,0 +1,6 @@
+{
+  "setupFilesAfterEnv": ["./init.js"],
+  "testEnvironment": "node",
+  "reporters": ["detox/runners/jest/streamlineReporter"],
+  "verbose": true
+}
diff --git a/examples/demo-plugin/e2e/init.js b/examples/demo-plugin/e2e/init.js
new file mode 100644
index 000000000..1d63d35bb
--- /dev/null
+++ b/examples/demo-plugin/e2e/init.js
@@ -0,0 +1,31 @@
+const detox = require('detox');
+const config = require('../package.json').detox;
+const adapter = require('detox/runners/jest/adapter');
+const specReporter = require('detox/runners/jest/specReporter');
+const assignReporter = require('detox/runners/jest/assignReporter');
+
+jasmine.getEnv().addReporter(adapter);
+
+// This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level.
+// This is strictly optional.
+jasmine.getEnv().addReporter(specReporter);
+
+// This will post which device has assigned to run a suite, which can be useful in a multiple-worker tests run.
+// This is strictly optional.
+jasmine.getEnv().addReporter(assignReporter);
+
+// Set the default timeout
+jest.setTimeout(90000);
+
+beforeAll(async () => {
+  await detox.init(config);
+}, 300000);
+
+beforeEach(async () => {
+  await adapter.beforeEach();
+});
+
+afterAll(async () => {
+  await adapter.afterAll();
+  await detox.cleanup();
+});