Repository: wix/Detox
Pull request #2038 (2020-05-05 16:44:03)

refactor: unified configuration
**Description:**

Prepares ground for landing #1910.

Slightly extends configuration capabilities, but the main point is that a configuration object for `new Detox` now contains all data it needs. No more scattering over places and files.

As a side effect, you can specify all that you previously could do via `detox.init(config, userParams)`, but now those params reside also in `package.json` Detox's section, called `behavior`.

```json
{
  "detox": {
    "behavior": {
      "init": {
        "reinstallApp": true,
        "launchApp": true,
        "exportGlobals": true
      },
      "cleanup": {
        "shutdownDevice": false
      }
    },
  }
}
```

Also, this PR fixes a partially incorrect custom driver require logic, where it was not using `process.cwd()` to resolve relative path dependencies.

I plan to add the new config object extension to the markdown docs, and merge.


Patch:
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/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 02bef4e54..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,19 +143,7 @@ class Detox {
     this._client.setNonresponsivenessListener(this._onNonresnponsivenessEvent.bind(this));
     await this._client.connect();
 
-    let DeviceDriverClass = DEVICE_CLASSES[this._deviceConfig.type];
-    if (!DeviceDriverClass) {
-      try {
-        DeviceDriverClass = require(this._deviceConfig.type);
-      } catch (e) {
-        // noop, if we don't find a module to require, we'll hit the unsupported error below
-      }
-    }
-    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,
     });
@@ -178,7 +157,7 @@ class Detox {
       sessionConfig,
     });
 
-    if (params.initGlobals) {
+    if (behaviorConfig.exposeGlobals) {
       Object.assign(global, {
         ...deviceDriver.matchers,
         device: this.device,
@@ -189,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;
   }
 
@@ -266,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 cf79540ba..66e3944a0 100644
--- a/detox/src/devices/Device.js
+++ b/detox/src/devices/Device.js
@@ -1,7 +1,4 @@
 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
 
 class Device {
@@ -15,20 +12,11 @@ class Device {
     this.debug = debug;
   }
 
-  async prepare(params = {}) {
+  async prepare() {
     this._deviceId = await this.deviceDriver.acquireFreeDevice(this._deviceConfig.device || this._deviceConfig.name);
     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._deviceConfig.binaryPath, this._deviceConfig.testBinaryPath);
-    }
-
-    if (params.launchApp) {
-      await this.launchApp({newInstance: true});
-    }
   }
 
   createPayloadFileAndUpdatesParamsObject(key, launchKey, params, baseLaunchArgs) {
@@ -268,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() {
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 8aaab8e0d..a3b8909c6 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.js
@@ -84,11 +84,6 @@ class AndroidDriver extends DeviceDriverBase {
     await uninstallHelper.uninstall(deviceId, bundleId);
   }
 
-  async uninstallAppByApk(deviceId, apkPath) {
-    const bundleId = await this.getBundleIdFromBinary(apkPath);
-    await this.uninstallApp(deviceId, bundleId);
-  }
-
   async launchApp(deviceId, bundleId, launchArgs, languageAndLocale) {
     await this.emitter.emit('beforeLaunchApp', { deviceId, bundleId, launchArgs });
 
diff --git a/detox/src/devices/drivers/android/EmulatorDriver.js b/detox/src/devices/drivers/android/EmulatorDriver.js
index 96b17fe70..debaf43d0 100644
--- a/detox/src/devices/drivers/android/EmulatorDriver.js
+++ b/detox/src/devices/drivers/android/EmulatorDriver.js
@@ -73,8 +73,6 @@ class EmulatorDriver extends AndroidDriver {
       testBinaryPath,
     } = this._getInstallPaths(_binaryPath, _testBinaryPath);
 
-    await this.uninstallAppByApk(deviceId, binaryPath);
-
     const installHelper = new AppInstallHelper(this.adb);
     await installHelper.install(deviceId, binaryPath, testBinaryPath);
   }
diff --git a/detox/src/devices/drivers/android/tools/ADB.js b/detox/src/devices/drivers/android/tools/ADB.js
index 44bf9856d..fbfda0ed3 100644
--- a/detox/src/devices/drivers/android/tools/ADB.js
+++ b/detox/src/devices/drivers/android/tools/ADB.js
@@ -105,7 +105,7 @@ class ADB {
   }
 
   /*async*/ remoteInstall(deviceId, path) {
-    return this.shell(deviceId, `pm install ${path}`);
+    return this.shell(deviceId, `pm install -r -g -t ${path}`);
   }
 
   async uninstall(deviceId, appId) {
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/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 db8d1b534..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:
diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json
index b82aa6ca8..24175def0 100644
--- a/examples/demo-plugin/package.json
+++ b/examples/demo-plugin/package.json
@@ -18,7 +18,7 @@
           "foo": "bar"
         },
         "name": "plugin-example",
-        "type": "../../examples/demo-plugin/driver"
+        "type": "./driver"
       }
     }
   }


Test patch:
diff --git a/detox/src/Detox.test.js b/detox/src/Detox.test.js
index c65f4e021..16a8e6cf9 100644
--- a/detox/src/Detox.test.js
+++ b/detox/src/Detox.test.js
@@ -1,366 +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());
 
-    await detox.init();
-    await invokeDetoxCallback();
+      it('should reinstall the app', () => {
+        expect(device().uninstallApp).toHaveBeenCalled();
+        expect(device().installApp).toHaveBeenCalled();
+      });
 
-    expect(mockLogger.warn).toHaveBeenCalledWith({ event: 'APP_NONRESPONSIVE' }, expectedMessage);
+      it('should not launch the app', () =>
+        expect(device().launchApp).not.toHaveBeenCalled());
+    });
 
-    async function invokeDetoxCallback() {
-      const callback = client.setNonresponsivenessListener.mock.calls[0][0];
-      await callback(callbackParams);
-    }
-  });
+    describe('and it gets unresponsiveness', () => {
+      let THREAD_DUMP = 'Simulated non-responsiveness';
 
-  it('properly instantiates configuration pointing to a plugin driver', async () => {
-    let instantiated = false;
-    class MockDriverPlugin {
-      constructor(config) {
-        instantiated = true;
-      }
-      on() {}
-      declareArtifactPlugins() {}
-    }
-    jest.mock('driver-plugin', () => MockDriverPlugin, { virtual: true });
-    const pluginDeviceConfig = {
-      "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
-      "type": "driver-plugin",
-      "name": "MyPlugin"
-    };
+      beforeEach(init);
 
-    Detox = require('./Detox');
-    detox = new Detox({deviceConfig: pluginDeviceConfig});
-    await detox.init();
+      beforeEach(() => {
+        const listener = client().setNonresponsivenessListener.mock.calls[0][0];
+        listener({ threadDump: THREAD_DUMP });
+      });
+
+      it('should log a warning', () =>
+        expect(logger.warn).toHaveBeenCalledWith(
+          { event: 'APP_NONRESPONSIVE' },
+          expect.stringContaining(THREAD_DUMP)
+        ));
+    });
 
-    expect(instantiated).toBe(true);
+    describe('and it gets emitter error', () => {
+      beforeEach(init);
+
+      it(`should log EMIT_ERROR if the internal emitter throws an error`, async () => {
+        const { emitter } = Device.mock.calls[0][0];
+
+        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 42fddef50..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,27 +122,6 @@ describe('Device', () => {
     expect(device.id).toEqual('mockDeviceId');
   });
 
-  describe('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",
@@ -327,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();
@@ -339,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);
@@ -368,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'});
@@ -398,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'});
@@ -414,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);
@@ -697,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({
@@ -800,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/tools/ADB.test.js b/detox/src/devices/drivers/android/tools/ADB.test.js
index f6a6e7d81..3d70003f8 100644
--- a/detox/src/devices/drivers/android/tools/ADB.test.js
+++ b/detox/src/devices/drivers/android/tools/ADB.test.js
@@ -180,7 +180,7 @@ describe('ADB', () => {
     await adb.remoteInstall(deviceId, binaryPath);
 
     expect(exec).toHaveBeenCalledWith(
-      expect.stringContaining(`-s mockEmulator shell "pm install ${binaryPath}"`),
+      expect.stringContaining(`-s mockEmulator shell "pm install -r -g -t ${binaryPath}"`),
       undefined, undefined, expect.anything());
   });
 
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/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/package.json b/detox/test/package.json
index f8e4fae5d..7798a5505 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -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"