Repository: wix/Detox
Pull request #2698 (2021-03-22 15:31:37)

fix: do not print stack for known errors
## Description

- This pull request addresses the issue of too verbose stack traces for already known / handled Detox runtime errors.

That's a regression caused by https://github.com/wix/Detox/pull/2680 .

In order to omit stack traces for errors that we throw, I've replaced all over the codebase `new Error` with `new DetoxRuntimeError` in the majority of cases.

To make the transition as smooth as possible I've overloaded `DetoxRuntimeError` constructor so it can accept not only a plain object but a string as well, similar to `new Error(msg)`. 

Before:
![image](https://user-images.githubusercontent.com/1962469/112015344-6856a080-8b34-11eb-8f83-6edc35b32312.png)

After:
![image](https://user-images.githubusercontent.com/1962469/112015375-6db3eb00-8b34-11eb-8e30-f45b0ec600e9.png)

P. S. To make sure there is not even a single place where I incorrectly require `DetoxRuntimeError`, I rely on eslint checks. To make sure I always require it, beforehand, I've added `no-undef: error` validation as a bonus.


Patch:
diff --git a/detox/.eslintrc b/detox/.eslintrc
index 38acedaaa..6b338a596 100644
--- a/detox/.eslintrc
+++ b/detox/.eslintrc
@@ -1,6 +1,13 @@
 {
+  "env": {
+    "node": true,
+    "jest": true
+  },
   "plugins": ["node"],
   "extends": ["plugin:node/recommended"],
+  "globals": {
+    "fail": true,
+  },
   "parserOptions": {
     "ecmaVersion": 2018,
     "sourceType": "module",
@@ -10,6 +17,7 @@
   },
   "rules": {
     "no-debugger": "error",
+    "no-undef": "warn",
     "no-mixed-spaces-and-tabs": "error",
     "node/no-unpublished-require": "warn"
   }
diff --git a/detox/local-cli/init.js b/detox/local-cli/init.js
index 000e248db..46111361f 100644
--- a/detox/local-cli/init.js
+++ b/detox/local-cli/init.js
@@ -3,6 +3,7 @@ const fs = require('fs');
 const path = require('path');
 const mochaTemplates = require('./templates/mocha');
 const jestTemplates = require('./templates/jest');
+const DetoxRuntimeError = require('../src/errors/DetoxRuntimeError');
 const log = require('../src/utils/logger').child({ __filename });
 
 let exitCode = 0;
@@ -30,7 +31,7 @@ module.exports.handler = async function init(argv) {
       createJestFolderE2E();
       break;
     default:
-      throw new Error([
+      throw new DetoxRuntimeError([
         `Convenience scaffolding for \`${runner}\` test runner is not supported currently.\n`,
         'Supported runners at the moment are: `mocha` and `jest`:',
         '* detox init -r mocha',
diff --git a/detox/local-cli/run-server.js b/detox/local-cli/run-server.js
index be63a0990..fc4b297b5 100644
--- a/detox/local-cli/run-server.js
+++ b/detox/local-cli/run-server.js
@@ -1,4 +1,5 @@
 const DetoxServer = require('../src/server/DetoxServer');
+const DetoxRuntimeError = require('../src/errors/DetoxRuntimeError');
 
 module.exports.command = 'run-server';
 module.exports.desc = 'Start a standalone Detox server';
@@ -24,7 +25,7 @@ module.exports.builder = {
 
 module.exports.handler = async function runServer(argv) {
   if (isNaN(argv.port) || argv.port < 1 || argv.port > 65535) {
-    throw new Error(`The port should be between 1 and 65535, got ${argv.port}`)
+    throw new DetoxRuntimeError(`The port should be between 1 and 65535, got ${argv.port}`)
   }
 
   new DetoxServer({
diff --git a/detox/runners/jest-circus/utils/assertExistingContext.js b/detox/runners/jest-circus/utils/assertExistingContext.js
index 84bf5a903..21a412981 100644
--- a/detox/runners/jest-circus/utils/assertExistingContext.js
+++ b/detox/runners/jest-circus/utils/assertExistingContext.js
@@ -1,4 +1,5 @@
 const { filterErrorStack } = require('../../../src/utils/errorUtils');
+const { DetoxRuntimeError } = require('../../../src/errors/DetoxRuntimeError');
 
 function findUserConstructor() {
   let wasInBaseClass = false;
@@ -23,7 +24,7 @@ function findUserConstructor() {
 
 function assertExistingContext(context) {
   if (!context) {
-    const error = new Error(`Please add both arguments to super() call in your environment constructor, e.g.:
+    const error = new DetoxRuntimeError(`Please add both arguments to super() call in your environment constructor, e.g.:
 
  class CustomDetoxEnvironment extends DetoxCircusEnvironment {
 -  constructor(config) {
diff --git a/detox/runners/jest-circus/utils/assertJestCircus26.js b/detox/runners/jest-circus/utils/assertJestCircus26.js
index 9ab704e83..5591a0669 100644
--- a/detox/runners/jest-circus/utils/assertJestCircus26.js
+++ b/detox/runners/jest-circus/utils/assertJestCircus26.js
@@ -1,27 +1,28 @@
 const fs = require('fs');
 const path = require('path');
+const DetoxRuntimeError = require('../../../src/errors/DetoxRuntimeError');
 
 function assertJestCircus26(config) {
   if (!/jest-circus/.test(config.testRunner)) {
-    throw new Error('Cannot run tests without "jest-circus" npm package, exiting.');
+    throw new DetoxRuntimeError('Cannot run tests without "jest-circus" npm package, exiting.');
   }
 
   const circusPackageJson = path.join(path.dirname(config.testRunner), 'package.json');
   if (!fs.existsSync(circusPackageJson)) {
-    throw new Error('Check that you have an installed copy of "jest-circus" npm package, exiting.');
+    throw new DetoxRuntimeError('Check that you have an installed copy of "jest-circus" npm package, exiting.');
   }
 
   const circusVersion = require(circusPackageJson).version || '';
   const [major, minor, patch] = circusVersion.split('.');
   if (major < 26) {
-    throw new Error(
+    throw new DetoxRuntimeError(
       `Cannot use older versions of "jest-circus", exiting.\n` +
       `You have jest-circus@${circusVersion}. Update to ^26.0.0 or newer.`
     );
   }
 
   if (major == 26 && minor == 5 && patch < 2) {
-    throw new Error(
+    throw new DetoxRuntimeError(
       `You have jest-circus@${circusVersion} currently installed.\n` +
       `Unfortunately, it is incompatible with Detox due to this critical issue:\n\n` +
       `https://github.com/wix/Detox/issues/2390\n\n` +
diff --git a/detox/src/android/AndroidExpect.js b/detox/src/android/AndroidExpect.js
index 708b80838..d68eba583 100644
--- a/detox/src/android/AndroidExpect.js
+++ b/detox/src/android/AndroidExpect.js
@@ -1,3 +1,4 @@
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 const { NativeElement } = require('./core/NativeElement');
 const { NativeMatcher } = require('./core/NativeMatcher');
 const { NativeExpectElement } = require('./core/NativeExpect');
@@ -25,7 +26,7 @@ class AndroidExpect {
       return new NativeElement(this._invocationManager, this._emitter, matcher);
     }
 
-    throw new Error(`element() argument is invalid, expected a native matcher, but got ${typeof element}`);
+    throw new DetoxRuntimeError(`element() argument is invalid, expected a native matcher, but got ${typeof element}`);
   }
 
   // Matcher can be null only if there is only one webview on the hierarchy tree.
@@ -39,18 +40,18 @@ class AndroidExpect {
       });
     }
 
-    throw new Error(`web() argument is invalid, expected a native matcher, but got ${typeof element}`);
+    throw new DetoxRuntimeError(`web() argument is invalid, expected a native matcher, but got ${typeof element}`);
   }
 
   expect(element) {
     if (element instanceof WebElement) return new WebExpectElement(this._invocationManager, element);
     if (element instanceof NativeElement) return new NativeExpectElement(this._invocationManager, element);
-    throw new Error(`expect() argument is invalid, expected a native or web matcher, but got ${typeof element}`);
+    throw new DetoxRuntimeError(`expect() argument is invalid, expected a native or web matcher, but got ${typeof element}`);
   }
 
   waitFor(element) {
     if (element instanceof NativeElement) return new NativeWaitForElement(this._invocationManager, element);
-    throw new Error(`waitFor() argument is invalid, got ${typeof element}`);
+    throw new DetoxRuntimeError(`waitFor() argument is invalid, got ${typeof element}`);
   }
 }
 
diff --git a/detox/src/android/core/NativeElement.js b/detox/src/android/core/NativeElement.js
index 61e5cb9ad..ccd8cda2b 100644
--- a/detox/src/android/core/NativeElement.js
+++ b/detox/src/android/core/NativeElement.js
@@ -1,6 +1,7 @@
 const fs = require('fs-extra');
 const path = require('path');
 const tempfile = require('tempfile');
+const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
 const invoke = require('../../invoke');
 const DetoxMatcherApi = require('../espressoapi/DetoxMatcher');
 const { ActionInteraction } = require('../interactions/native')
@@ -15,12 +16,12 @@ class NativeElement {
   }
 
   _selectElementWithMatcher(matcher) {
-    // if (!(matcher instanceof NativeMatcher)) throw new Error(`Element _selectElementWithMatcher argument must be a valid NativeMatcher, got ${typeof matcher}`);
+    // if (!(matcher instanceof NativeMatcher)) throw new DetoxRuntimeError(`Element _selectElementWithMatcher argument must be a valid NativeMatcher, got ${typeof matcher}`);
     this._call = invoke.call(invoke.Espresso, 'onView', matcher._call);
   }
 
   atIndex(index) {
-    if (typeof index !== 'number') throw new Error(`Element atIndex argument must be a number, got ${typeof index}`);
+    if (typeof index !== 'number') throw new DetoxRuntimeError(`Element atIndex argument must be a number, got ${typeof index}`);
     const matcher = this._originalMatcher;
     this._originalMatcher._call = invoke.callDirectly(DetoxMatcherApi.matcherForAtIndex(index, matcher._call.value));
 
diff --git a/detox/src/android/core/WebElement.js b/detox/src/android/core/WebElement.js
index 6a1c5c67f..63df8662a 100644
--- a/detox/src/android/core/WebElement.js
+++ b/detox/src/android/core/WebElement.js
@@ -1,3 +1,4 @@
+const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
 const invoke = require('../../invoke');
 const EspressoWebDetoxApi = require('../espressoapi/web/EspressoWebDetox');
 const WebViewElementApi = require('../espressoapi/web/WebViewElement');
@@ -114,7 +115,7 @@ class WebViewElement {
       });
     }
 
-    throw new Error(`element() argument is invalid, expected a web matcher, but got ${typeof element}`);
+    throw new DetoxRuntimeError(`element() argument is invalid, expected a web matcher, but got ${typeof element}`);
   }
 }
 
diff --git a/detox/src/android/interactions/native.js b/detox/src/android/interactions/native.js
index edd8e3c44..facbed13b 100644
--- a/detox/src/android/interactions/native.js
+++ b/detox/src/android/interactions/native.js
@@ -1,3 +1,4 @@
+const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
 const DetoxAssertionApi = require('../espressoapi/DetoxAssertion');
 const EspressoDetoxApi = require('../espressoapi/EspressoDetox');
 const { NativeMatcher } = require('../core/NativeMatcher');
@@ -44,8 +45,8 @@ class WaitForInteraction extends Interaction {
   }
 
   async withTimeout(timeout) {
-    if (typeof timeout !== 'number') throw new Error(`WaitForInteraction withTimeout argument must be a number, got ${typeof timeout}`);
-    if (timeout < 0) throw new Error('timeout must be larger than 0');
+    if (typeof timeout !== 'number') throw new DetoxRuntimeError(`WaitForInteraction withTimeout argument must be a number, got ${typeof timeout}`);
+    if (timeout < 0) throw new DetoxRuntimeError('timeout must be larger than 0');
 
     this._call = DetoxAssertionApi.waitForAssertMatcher(call(this._element._call), this._assertionMatcher._call.value, timeout / 1000);
     await this.execute();
@@ -59,17 +60,17 @@ class WaitForInteraction extends Interaction {
 class WaitForActionInteractionBase extends Interaction {
   constructor(invocationManager, element, matcher, searchMatcher) {
     super(invocationManager);
-    //if (!(element instanceof NativeElement)) throw new Error(`WaitForActionInteraction ctor 1st argument must be a valid NativeElement, got ${typeof element}`);
-    //if (!(matcher instanceof NativeMatcher)) throw new Error(`WaitForActionInteraction ctor 2nd argument must be a valid NativeMatcher, got ${typeof matcher}`);
+    //if (!(element instanceof NativeElement)) throw new DetoxRuntimeError(`WaitForActionInteraction ctor 1st argument must be a valid NativeElement, got ${typeof element}`);
+    //if (!(matcher instanceof NativeMatcher)) throw new DetoxRuntimeError(`WaitForActionInteraction ctor 2nd argument must be a valid NativeMatcher, got ${typeof matcher}`);
     if (!(searchMatcher instanceof NativeMatcher))
-      throw new Error(`WaitForActionInteraction ctor 3rd argument must be a valid NativeMatcher, got ${typeof searchMatcher}`);
+      throw new DetoxRuntimeError(`WaitForActionInteraction ctor 3rd argument must be a valid NativeMatcher, got ${typeof searchMatcher}`);
     this._element = element;
     this._originalMatcher = matcher;
     this._searchMatcher = searchMatcher;
   }
 
   _prepare(searchAction) {
-    //if (!searchAction instanceof Action) throw new Error(`WaitForActionInteraction _execute argument must be a valid Action, got ${typeof searchAction}`);
+    //if (!searchAction instanceof Action) throw new DetoxRuntimeError(`WaitForActionInteraction _execute argument must be a valid Action, got ${typeof searchAction}`);
 
     this._call = DetoxAssertionApi.waitForAssertMatcherWithSearchAction(
       call(this._element._call),
diff --git a/detox/src/android/matchers/native.js b/detox/src/android/matchers/native.js
index 9a8835211..6db3eef09 100644
--- a/detox/src/android/matchers/native.js
+++ b/detox/src/android/matchers/native.js
@@ -1,3 +1,4 @@
+const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
 const invoke = require('../../invoke');
 const DetoxMatcherApi = require('../espressoapi/DetoxMatcher');
 const { NativeMatcher } = require('../core/NativeMatcher');
@@ -63,7 +64,7 @@ class ToggleMatcher extends NativeMatcher {
 class TraitsMatcher extends NativeMatcher {
   constructor(value) {
     super();
-    if ((typeof value !== 'object') || (!value instanceof Array)) throw new Error(`TraitsMatcher ctor argument must be an array, got ${typeof value}`);
+    if ((typeof value !== 'object') || (!value instanceof Array)) throw new DetoxRuntimeError(`TraitsMatcher ctor argument must be an array, got ${typeof value}`);
 
     this._call = invoke.callDirectly(DetoxMatcherApi.matcherForAnything());
   }
diff --git a/detox/src/artifacts/ArtifactsManager.js b/detox/src/artifacts/ArtifactsManager.js
index 4b4f347db..cc940ef09 100644
--- a/detox/src/artifacts/ArtifactsManager.js
+++ b/detox/src/artifacts/ArtifactsManager.js
@@ -3,6 +3,7 @@ const fs = require('fs-extra');
 const path = require('path');
 const util = require('util');
 const FileArtifact = require('./templates/artifact/FileArtifact');
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 const log = require('../utils/logger').child({ __filename });
 
 class ArtifactsManager {
@@ -211,7 +212,7 @@ class ArtifactsManager {
         return pluginsByPriority;
       /* istanbul ignore next */
       default: // is
-        throw new Error(`Unknown plugins grouping strategy: ${strategy}`);
+        throw new DetoxRuntimeError(`Unknown plugins grouping strategy: ${strategy}`);
     }
   }
 
diff --git a/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js b/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js
index 49ec075d6..692783ed1 100644
--- a/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js
+++ b/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js
@@ -2,6 +2,7 @@ const _ = require('lodash');
 const Client = require('../../client/Client');
 const ArtifactPlugin = require('../templates/plugin/ArtifactPlugin');
 const FileArtifact = require('../templates/artifact/FileArtifact');
+const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
 const setUniqueProperty = require('../../utils/setUniqueProperty');
 
 class IosUIHierarchyPlugin extends ArtifactPlugin {
@@ -31,7 +32,7 @@ class IosUIHierarchyPlugin extends ArtifactPlugin {
 
   async onCreateExternalArtifact(e) {
     if (!e.artifact) {
-      throw new Error('Internal error: expected Artifact instance in the event');
+      throw new DetoxRuntimeError('Internal error: expected Artifact instance in the event');
     }
 
     this._registerSnapshot(e.name, e.artifact);
diff --git a/detox/src/client/AsyncWebSocket.js b/detox/src/client/AsyncWebSocket.js
index 7f792dff4..b24d4d425 100644
--- a/detox/src/client/AsyncWebSocket.js
+++ b/detox/src/client/AsyncWebSocket.js
@@ -1,7 +1,8 @@
 const _ = require('lodash');
 const util = require('util');
-const log = require('../utils/logger').child({ __filename, class: 'AsyncWebSocket' });
 const WebSocket = require('ws');
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
+const log = require('../utils/logger').child({ __filename, class: 'AsyncWebSocket' });
 
 class AsyncWebSocket {
 
@@ -57,7 +58,7 @@ class AsyncWebSocket {
 
   async send(message, messageId) {
     if (!this.ws) {
-      throw new Error(`Can't send a message on a closed websocket, init the by calling 'open()'. Message:  ${JSON.stringify(message)}`);
+      throw new DetoxRuntimeError(`Can't send a message on a closed websocket, init the by calling 'open()'. Message:  ${JSON.stringify(message)}`);
     }
 
     return new Promise(async(resolve, reject) => {
@@ -91,7 +92,7 @@ class AsyncWebSocket {
           this.ws.onclose();
         }
       } else {
-        reject(new Error(`websocket is closed, init the by calling 'open()'`));
+        reject(new DetoxRuntimeError(`websocket is closed, init the by calling 'open()'`));
       }
     });
   }
diff --git a/detox/src/client/actions/actions.js b/detox/src/client/actions/actions.js
index 0239d0194..b3dcecf2c 100644
--- a/detox/src/client/actions/actions.js
+++ b/detox/src/client/actions/actions.js
@@ -13,7 +13,7 @@ class Action {
 
   expectResponseOfType(response, type) {
     if (response.type !== type) {
-      throw new Error(`was expecting '${type}' , got ${JSON.stringify(response)}`);
+      throw new DetoxRuntimeError(`was expecting '${type}' , got ${JSON.stringify(response)}`);
     }
   }
 }
@@ -123,13 +123,13 @@ class Invoke extends Action {
             : '\nTIP: To print view hierarchy on failed actions/matches, use log-level verbose or higher.';
         }
 
-        throw new Error(message);
+        throw new DetoxRuntimeError(message);
       case 'invokeResult':
         return response.params;
       case 'error':
-        throw new Error(response.params.error);
+        throw new DetoxRuntimeError(response.params.error);
       default:
-        throw new Error(`tried to invoke an action on app, got an unsupported response: ${JSON.stringify(response)}`);
+        throw new DetoxRuntimeError(`tried to invoke an action on app, got an unsupported response: ${JSON.stringify(response)}`);
     }
   }
 }
diff --git a/detox/src/devices/Device.js b/detox/src/devices/Device.js
index 193d58044..80ebb7278 100644
--- a/detox/src/devices/Device.js
+++ b/detox/src/devices/Device.js
@@ -1,5 +1,6 @@
 const _ = require('lodash');
 const LaunchArgsEditor = require('./LaunchArgsEditor');
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 const debug = require('../utils/debug'); // debug utils, leave here even if unused
 const { traceCall } = require('../utils/trace');
 
@@ -101,7 +102,7 @@ class Device {
 
   async takeScreenshot(name) {
     if (!name) {
-      throw new Error('Cannot take a screenshot with an empty name.');
+      throw new DetoxRuntimeError('Cannot take a screenshot with an empty name.');
     }
 
     return this.deviceDriver.takeScreenshot(this._deviceId, name);
@@ -184,7 +185,7 @@ class Device {
 
   async openURL(params) {
     if (typeof params !== 'object' || !params.url) {
-      throw new Error(`openURL must be called with JSON params, and a value for 'url' key must be provided. example: await device.openURL({url: "url", sourceApp[optional]: "sourceAppBundleID"}`);
+      throw new DetoxRuntimeError(`openURL must be called with JSON params, and a value for 'url' key must be provided. example: await device.openURL({url: "url", sourceApp[optional]: "sourceAppBundleID"}`);
     }
 
     await this.deviceDriver.deliverPayload(params, this._deviceId);
@@ -383,7 +384,7 @@ class Device {
     });
 
     if (paramsCounter > 1) {
-      throw new Error(`Call to 'launchApp(${JSON.stringify(params)})' must contain only one of ${JSON.stringify(singleParams)}.`);
+      throw new DetoxRuntimeError(`Call to 'launchApp(${JSON.stringify(params)})' must contain only one of ${JSON.stringify(singleParams)}.`);
     }
 
     return (paramsCounter === 1);
diff --git a/detox/src/devices/DriverRegistry.js b/detox/src/devices/DriverRegistry.js
index e9cb72863..3d597cd89 100644
--- a/detox/src/devices/DriverRegistry.js
+++ b/detox/src/devices/DriverRegistry.js
@@ -1,3 +1,4 @@
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 const resolveModuleFromPath = require('../utils/resolveModuleFromPath');
 
 class DriverRegistry {
@@ -12,7 +13,7 @@ class DriverRegistry {
       DeviceDriverClass = resolveModuleFromPath(deviceType).DriverClass;
 
       if (!DeviceDriverClass) {
-        throw new Error(`The custom driver '${deviceType}' does not export DriverClass property`);
+        throw new DetoxRuntimeError(`The custom driver '${deviceType}' does not export DriverClass property`);
       }
     }
 
diff --git a/detox/src/devices/LaunchArgsEditor.js b/detox/src/devices/LaunchArgsEditor.js
index 66b5f3da0..6eac22905 100644
--- a/detox/src/devices/LaunchArgsEditor.js
+++ b/detox/src/devices/LaunchArgsEditor.js
@@ -1,9 +1,10 @@
 const _ = require('lodash');
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 
 class LaunchArgsEditor {
   constructor(launchArgs) {
     if (!launchArgs) {
-      throw new Error('Cannot instantiate a launch-arguments editor with no reference arguments-object!');
+      throw new DetoxRuntimeError('Cannot instantiate a launch-arguments editor with no reference arguments-object!');
     }
 
     this._args = launchArgs;
diff --git a/detox/src/devices/drivers/android/AndroidDriver.js b/detox/src/devices/drivers/android/AndroidDriver.js
index bcf36b849..88589d3be 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.js
@@ -22,6 +22,7 @@ const ADBScreenrecorderPlugin = require('../../../artifacts/video/ADBScreenrecor
 const AndroidDevicePathBuilder = require('../../../artifacts/utils/AndroidDevicePathBuilder');
 const TimelineArtifactPlugin = require('../../../artifacts/timeline/TimelineArtifactPlugin');
 const temporaryPath = require('../../../artifacts/utils/temporaryPath');
+const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
 const sleep = require('../../../utils/sleep');
 const retry = require('../../../utils/retry');
 const pressAnyKey = require('../../../utils/pressAnyKey');
@@ -250,7 +251,7 @@ class AndroidDriver extends DeviceDriverBase {
     const testApkPath = APKPath.getTestApkPath(originalApkPath);
 
     if (!fs.existsSync(testApkPath)) {
-      throw new Error(`'${testApkPath}' could not be found, did you run './gradlew assembleAndroidTest'?`);
+      throw new DetoxRuntimeError(`'${testApkPath}' could not be found, did you run './gradlew assembleAndroidTest'?`);
     }
     return testApkPath;
   }
@@ -334,7 +335,7 @@ class AndroidDriver extends DeviceDriverBase {
   async _queryPID(adbName, bundleId) {
     const pid = await this.adb.pidof(adbName, bundleId);
     if (!pid) {
-      throw new Error('PID still not available');
+      throw new DetoxRuntimeError('PID still not available');
     }
     return pid;
   }
diff --git a/detox/src/devices/drivers/android/emulator/EmulatorDriver.js b/detox/src/devices/drivers/android/emulator/EmulatorDriver.js
index 72ffd86e3..f85ed19b3 100644
--- a/detox/src/devices/drivers/android/emulator/EmulatorDriver.js
+++ b/detox/src/devices/drivers/android/emulator/EmulatorDriver.js
@@ -11,6 +11,7 @@ const EmulatorVersionResolver = require('./EmulatorVersionResolver');
 const FreeEmulatorFinder = require('./FreeEmulatorFinder');
 const { EmulatorExec } = require('../exec/EmulatorExec');
 const DeviceRegistry = require('../../../DeviceRegistry');
+const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError');
 const environment = require('../../../../utils/environment');
 const log = require('../../../../utils/logger').child({ __filename });
 const argparse = require('../../../../utils/argparse');
@@ -102,7 +103,7 @@ class EmulatorDriver extends AndroidDriver {
       const height = config['hw.lcd.height'];
 
       if (width === undefined || height === undefined) {
-        throw new Error(`Emulator with name ${avdName} has a corrupt config.ini file (${configFile}), try fixing it by recreating an emulator.`);
+        throw new DetoxRuntimeError(`Emulator with name ${avdName} has a corrupt config.ini file (${configFile}), try fixing it by recreating an emulator.`);
       }
 
       config['skin.name'] = `${width}x${height}`;
diff --git a/detox/src/devices/drivers/android/exec/ADB.js b/detox/src/devices/drivers/android/exec/ADB.js
index e6cdeb9db..4cfd1dc17 100644
--- a/detox/src/devices/drivers/android/exec/ADB.js
+++ b/detox/src/devices/drivers/android/exec/ADB.js
@@ -252,7 +252,7 @@ class ADB {
     const instrumentationRunners = await this.listInstrumentation(deviceId);
     const instrumentationRunner = this._instrumentationRunnerForBundleId(instrumentationRunners, bundleId);
     if (instrumentationRunner === 'undefined') {
-      throw new Error(`No instrumentation runner found on device ${deviceId} for package ${bundleId}`);
+      throw new DetoxRuntimeError(`No instrumentation runner found on device ${deviceId} for package ${bundleId}`);
     }
 
     return instrumentationRunner;
diff --git a/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceAllocation.js b/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceAllocation.js
index c40b427a8..75024562b 100644
--- a/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceAllocation.js
+++ b/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceAllocation.js
@@ -1,5 +1,6 @@
 const AndroidDeviceAllocation = require('../../AndroidDeviceAllocation');
 const GenyCloudInstanceHandle = require('../GenyCloudInstanceHandle');
+const DetoxRuntimeError = require('../../../../../errors/DetoxRuntimeError');
 const retry = require('../../../../../utils/retry');
 const logger = require('../../../../../utils/logger').child({ __filename });
 
@@ -73,7 +74,7 @@ class GenyCloudInstanceAllocation extends AndroidDeviceAllocation {
     return await retry(options, async () => {
       const _instance = await this._instanceLookupService.getInstance(instance.uuid);
       if (!_instance.isOnline()) {
-        throw new Error(`Timeout waiting for instance ${instance.uuid} to be ready`);
+        throw new DetoxRuntimeError(`Timeout waiting for instance ${instance.uuid} to be ready`);
       }
       return _instance;
     });
diff --git a/detox/src/devices/drivers/ios/IosDriver.js b/detox/src/devices/drivers/ios/IosDriver.js
index 08294abf4..7e56c20e7 100644
--- a/detox/src/devices/drivers/ios/IosDriver.js
+++ b/detox/src/devices/drivers/ios/IosDriver.js
@@ -4,6 +4,7 @@ const DeviceDriverBase = require('../DeviceDriverBase');
 
 const TimelineArtifactPlugin = require('../../../artifacts/timeline/TimelineArtifactPlugin');
 const IosUIHierarchyPlugin = require('../../../artifacts/uiHierarchy/IosUIHierarchyPlugin');
+const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
 
 class IosDriver extends DeviceDriverBase {
   declareArtifactPlugins() {
@@ -38,7 +39,7 @@ class IosDriver extends DeviceDriverBase {
   }
 
   async setOrientation(deviceId, orientation) {
-    if (!['portrait', 'landscape'].some(option => option === orientation)) throw new Error("orientation should be either 'portrait' or 'landscape', but got " + (orientation + ")"));
+    if (!['portrait', 'landscape'].some(option => option === orientation)) throw new DetoxRuntimeError("orientation should be either 'portrait' or 'landscape', but got " + (orientation + ")"));
     return await this.client.setOrientation({orientation});
   }
 
diff --git a/detox/src/devices/drivers/ios/SimulatorDriver.js b/detox/src/devices/drivers/ios/SimulatorDriver.js
index 7e251a1c6..91ee3babe 100644
--- a/detox/src/devices/drivers/ios/SimulatorDriver.js
+++ b/detox/src/devices/drivers/ios/SimulatorDriver.js
@@ -50,7 +50,7 @@ class SimulatorDriver extends IosDriver {
     const detoxFrameworkPath = await environment.getFrameworkPath();
 
     if (!fs.existsSync(detoxFrameworkPath)) {
-      throw new Error(`${detoxFrameworkPath} could not be found, this means either you changed a version of Xcode or Detox postinstall script was unsuccessful.
+      throw new DetoxRuntimeError(`${detoxFrameworkPath} could not be found, this means either you changed a version of Xcode or Detox postinstall script was unsuccessful.
       To attempt a fix try running 'detox clean-framework-cache && detox build-framework-cache'`);
     }
   }
@@ -67,7 +67,7 @@ class SimulatorDriver extends IosDriver {
 
     const deviceComment = this._commentDevice(deviceQuery);
     if (!udid) {
-      throw new Error(`Failed to find device matching ${deviceComment}`);
+      throw new DetoxRuntimeError(`Failed to find device matching ${deviceComment}`);
     }
 
     await this._boot(udid, deviceQuery.type || deviceQuery);
@@ -85,7 +85,7 @@ class SimulatorDriver extends IosDriver {
       }
       return bundleId;
     } catch (ex) {
-      throw new Error(`field CFBundleIdentifier not found inside Info.plist of app binary at ${appPath}`);
+      throw new DetoxRuntimeError(`field CFBundleIdentifier not found inside Info.plist of app binary at ${appPath}`);
     }
   }
 
diff --git a/detox/src/devices/drivers/ios/tools/AppleSimUtils.js b/detox/src/devices/drivers/ios/tools/AppleSimUtils.js
index c84dcc925..a9649e3a2 100644
--- a/detox/src/devices/drivers/ios/tools/AppleSimUtils.js
+++ b/detox/src/devices/drivers/ios/tools/AppleSimUtils.js
@@ -1,5 +1,6 @@
 const _ = require('lodash');
 const path = require('path');
+const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError');
 const {joinArgs} = require('../../../../utils/argparse');
 const exec = require('../../../../utils/exec');
 const log = require('../../../../utils/logger').child({ __filename });
@@ -62,7 +63,7 @@ class AppleSimUtils {
   async _findDeviceByUDID(udid) {
     const [device] = await this.list({ byId: udid, maxResults: 1 });
     if (!device) {
-      throw new Error(`Can't find device with UDID = "${udid}"`);
+      throw new DetoxRuntimeError(`Can't find device with UDID = "${udid}"`);
     }
 
     return device;
@@ -79,7 +80,7 @@ class AppleSimUtils {
 
     if (!deviceTypeIdentifier || !deviceRuntimeIdentifier) {
       const deviceInfoStr = JSON.stringify(deviceInfo, null, 4);
-      throw new Error(`Unable to create device from: ${deviceInfoStr}`);
+      throw new DetoxRuntimeError(`Unable to create device from: ${deviceInfoStr}`);
     }
 
     const { stdout: udid } = await this._execSimctl({
@@ -247,7 +248,7 @@ class AppleSimUtils {
     if (_.get(result, 'stdout')) {
       await exec.execWithRetriesAndLogs(`fbsimctl ${udid} set_location ${lat} ${lon}`, { retries: 1 });
     } else {
-      throw new Error(`setLocation currently supported only through fbsimctl.
+      throw new DetoxRuntimeError(`setLocation currently supported only through fbsimctl.
       Install fbsimctl using:
       "brew tap facebook/fb && export CODE_SIGNING_REQUIRED=NO && brew install fbsimctl"`);
     }
@@ -301,7 +302,7 @@ class AppleSimUtils {
       parsed = JSON.parse(out);
 
     } catch (ex) {
-      throw new Error(`Could not parse response from applesimutils, please update applesimutils and try again.
+      throw new DetoxRuntimeError(`Could not parse response from applesimutils, please update applesimutils and try again.
       'brew uninstall applesimutils && brew tap wix/brew && brew install applesimutils'`);
     }
     return parsed;
diff --git a/detox/src/errors/DetoxRuntimeError.js b/detox/src/errors/DetoxRuntimeError.js
index 0c2b0fb0d..b4a185991 100644
--- a/detox/src/errors/DetoxRuntimeError.js
+++ b/detox/src/errors/DetoxRuntimeError.js
@@ -2,21 +2,8 @@ const _ = require('lodash');
 const util = require('util');
 
 class DetoxRuntimeError extends Error {
-  constructor({
-    message = '',
-    hint = '',
-    debugInfo = '',
-    inspectOptions = null,
-  } = {}) {
-    const formattedMessage = _.compact([
-      message,
-      hint && `HINT: ${hint}`,
-      _.isObject(debugInfo)
-        ? DetoxRuntimeError.inspectObj(debugInfo, inspectOptions)
-        : debugInfo,
-    ]).join('\n\n');
-
-    super(formattedMessage);
+  constructor(options) {
+    super(formatOptions(options));
     this.name = 'DetoxRuntimeError';
   }
 
@@ -51,4 +38,25 @@ class DetoxRuntimeError extends Error {
   }
 }
 
+function formatOptions(options) {
+  if (_.isObject(options)) {
+    const {
+      message = '',
+      hint = '',
+      debugInfo = '',
+      inspectOptions = null,
+    } = options;
+
+    return _.compact([
+      message,
+      hint && `HINT: ${hint}`,
+      _.isObject(debugInfo)
+        ? DetoxRuntimeError.inspectObj(debugInfo, inspectOptions)
+        : debugInfo,
+    ]).join('\n\n');
+  }
+
+  return options;
+}
+
 module.exports = DetoxRuntimeError;
diff --git a/detox/src/utils/AsyncEmitter.js b/detox/src/utils/AsyncEmitter.js
index 709c29272..26be1a8b1 100644
--- a/detox/src/utils/AsyncEmitter.js
+++ b/detox/src/utils/AsyncEmitter.js
@@ -1,4 +1,5 @@
 const _ = require('lodash');
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 
 class AsyncEmitter {
   constructor({ events, onError }) {
@@ -35,7 +36,7 @@ class AsyncEmitter {
     if (this._listeners.hasOwnProperty(eventName)) {
       _.pull(this._listeners[eventName], callback);
     } else {
-      throw new Error('AsyncEmitter.off() failed to unsubscribe from a non-existent event: ' + eventName);
+      throw new DetoxRuntimeError('AsyncEmitter.off() failed to unsubscribe from a non-existent event: ' + eventName);
     }
   }
 
@@ -43,9 +44,9 @@ class AsyncEmitter {
     if (this._listeners.hasOwnProperty(eventName)) {
       this._listeners[eventName].push(callback);
     } else {
-      throw new Error('AsyncEmitter.on() failed to subscribe to a non-existent event: ' + eventName);
+      throw new DetoxRuntimeError('AsyncEmitter.on() failed to subscribe to a non-existent event: ' + eventName);
     }
   }
 }
 
-module.exports = AsyncEmitter;
\ No newline at end of file
+module.exports = AsyncEmitter;
diff --git a/detox/src/utils/ChromeTracingExporter.js b/detox/src/utils/ChromeTracingExporter.js
index 84e36fccf..d771e7ed5 100644
--- a/detox/src/utils/ChromeTracingExporter.js
+++ b/detox/src/utils/ChromeTracingExporter.js
@@ -1,4 +1,5 @@
 const _ = require('lodash');
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 
 class ChromeTracingExporter {
   constructor({
@@ -32,7 +33,7 @@ class ChromeTracingExporter {
           this._event('thread_name', 'M', ts, { name: this._thread.name }),
         ];
       default:
-        throw new Error(`Invalid type '${type}' in event: ${event}`);
+        throw new DetoxRuntimeError(`Invalid type '${type}' in event: ${event}`);
     }
   }
 
diff --git a/detox/src/utils/ExclusiveLockfile.js b/detox/src/utils/ExclusiveLockfile.js
index 80fc93b17..c1e209be0 100644
--- a/detox/src/utils/ExclusiveLockfile.js
+++ b/detox/src/utils/ExclusiveLockfile.js
@@ -1,6 +1,7 @@
 const _ = require('lodash');
 const fs = require('fs-extra');
 const plockfile = require('proper-lockfile');
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 const retry = require('./retry');
 
 const DEFAULT_OPTIONS = {
@@ -12,7 +13,7 @@ const DEFAULT_OPTIONS = {
 class ExclusiveLockfile {
   constructor(lockfile, options) {
     if (!lockfile) {
-      throw new Error('Path to the lockfile should be a non-empty string');
+      throw new DetoxRuntimeError('Path to the lockfile should be a non-empty string');
     }
 
     this._lockFilePath = lockfile;
@@ -46,7 +47,7 @@ class ExclusiveLockfile {
    */
   read() {
     if (!this._isLocked) {
-      throw new Error('Forbidden to read in unlocked mode');
+      throw new DetoxRuntimeError('Forbidden to read in unlocked mode');
     }
 
     if (!this._hasValue) {
@@ -62,7 +63,7 @@ class ExclusiveLockfile {
    */
   write(value) {
     if (!this._isLocked) {
-      throw new Error('Forbidden to write in unlocked mode');
+      throw new DetoxRuntimeError('Forbidden to write in unlocked mode');
     }
 
     this._value = value;
diff --git a/detox/src/utils/Timer.js b/detox/src/utils/Timer.js
index d0ec322a6..d6ad65b8b 100644
--- a/detox/src/utils/Timer.js
+++ b/detox/src/utils/Timer.js
@@ -1,3 +1,4 @@
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 const Deferred = require('./Deferred');
 
 class Timer {
@@ -23,7 +24,7 @@ class Timer {
   }
 
   async run(action) {
-    const error = new Error();
+    const error = new DetoxRuntimeError();
 
     return Promise.race([
       this._timeoutDeferred.promise.then(() => {
diff --git a/detox/src/utils/appdatapath.js b/detox/src/utils/appdatapath.js
index cb8e4d57b..4528e0ec3 100644
--- a/detox/src/utils/appdatapath.js
+++ b/detox/src/utils/appdatapath.js
@@ -1,5 +1,6 @@
 const os = require('os');
 const path = require('path');
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 
 function darwin() {
   return path.join(os.homedir(), 'Library');
@@ -30,7 +31,7 @@ function appDataPath() {
     case 'win32':
       return win32();
     default:
-      throw new Error(`${os.platform()} is not supported`);
+      throw new DetoxRuntimeError(`${os.platform()} is not supported`);
   }
 }
 
diff --git a/detox/src/utils/assertArgument.js b/detox/src/utils/assertArgument.js
index 6a0a08b53..8fadcca91 100644
--- a/detox/src/utils/assertArgument.js
+++ b/detox/src/utils/assertArgument.js
@@ -1,3 +1,5 @@
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
+
 function firstEntry(obj) {
   return Object.entries(obj)[0];
 }
@@ -7,7 +9,7 @@ function assertType(expectedType) {
     const [key, value] = firstEntry(arg);
 
     if (typeof value !== expectedType) {
-      throw new Error(`${key} should be a ${expectedType}, but got ${value} (${typeof value})`);
+      throw new DetoxRuntimeError(`${key} should be a ${expectedType}, but got ${value} (${typeof value})`);
     }
   };
 }
@@ -20,7 +22,7 @@ function assertNormalized(arg) {
 
   const [key, value] = firstEntry(arg);
   if (value < 0 || value > 1) {
-    throw new Error(`${key} should be a number [0.0, 1.0], but got ${value} (${typeof value})`);
+    throw new DetoxRuntimeError(`${key} should be a number [0.0, 1.0], but got ${value} (${typeof value})`);
   }
 }
 
@@ -29,7 +31,7 @@ function assertEnum(allowedValues) {
     const [key, value] = firstEntry(arg);
 
     if (allowedValues.indexOf(value) === -1) {
-      throw new Error(`${key} should be one of [${allowedValues.join(', ')}], but got ${value} (${typeof value})`);
+      throw new DetoxRuntimeError(`${key} should be one of [${allowedValues.join(', ')}], but got ${value} (${typeof value})`);
     }
   };
 }
diff --git a/detox/src/utils/environment.js b/detox/src/utils/environment.js
index ae05bf26a..bd6a2b804 100644
--- a/detox/src/utils/environment.js
+++ b/detox/src/utils/environment.js
@@ -7,6 +7,7 @@ const _which = require('which');
 const exec = require('child-process-promise').exec;
 const appdatapath = require('./appdatapath');
 const fsext = require('./fsext');
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 
 function which(executable, path) {
   return _which.sync(executable, {path, nothrow: true});
@@ -124,15 +125,15 @@ function getGmsaasPath() {
 }
 
 function throwMissingSdkError() {
-  throw new Error(MISSING_SDK_ERROR);
+  throw new DetoxRuntimeError(MISSING_SDK_ERROR);
 }
 
 function throwMissingAvdINIError(avdName, avdIniPath) {
-  throw new Error(`Failed to find INI file for ${avdName} at path: ${avdIniPath}`);
+  throw new DetoxRuntimeError(`Failed to find INI file for ${avdName} at path: ${avdIniPath}`);
 }
 
 function throwMissingAvdError(avdName, avdPath, avdIniPath) {
-  throw new Error(
+  throw new DetoxRuntimeError(
     `Failed to find AVD ${avdName} directory at path: ${avdPath}\n` +
     `Please verify "path" property in the INI file: ${avdIniPath}`
   );
@@ -143,14 +144,14 @@ function throwSdkIntegrityError(sdkRoot, relativeExecutablePath) {
   const name = path.basename(executablePath);
   const dir = path.dirname(executablePath);
 
-  throw new Error(
+  throw new DetoxRuntimeError(
     `There was no "${name}" executable file in directory: ${dir}.\n` +
     `Check integrity of your Android SDK.`
   );
 }
 
 function throwMissingGmsaasError() {
-  throw new Error(`Failed to locate Genymotion\'s gmsaas executable. Please add it to your $PATH variable!\nPATH is currently set to: ${process.env.PATH}`)
+  throw new DetoxRuntimeError(`Failed to locate Genymotion\'s gmsaas executable. Please add it to your $PATH variable!\nPATH is currently set to: ${process.env.PATH}`)
 }
 
 function getDetoxVersion() {
diff --git a/detox/src/utils/getAbsoluteBinaryPath.js b/detox/src/utils/getAbsoluteBinaryPath.js
index b5d32daf9..c72bde5ed 100644
--- a/detox/src/utils/getAbsoluteBinaryPath.js
+++ b/detox/src/utils/getAbsoluteBinaryPath.js
@@ -1,5 +1,6 @@
 const fs = require('fs');
 const path = require('path');
+const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 
 function getAbsoluteBinaryPath(appPath) {
   if (path.isAbsolute(appPath)) {
@@ -10,7 +11,7 @@ function getAbsoluteBinaryPath(appPath) {
   if (fs.existsSync(absPath)) {
     return absPath;
   } else {
-    throw new Error(`app binary not found at '${absPath}', did you build it?`);
+    throw new DetoxRuntimeError(`app binary not found at '${absPath}', did you build it?`);
   }
 }
 


Test patch:
diff --git a/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.js b/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.js
index b25ab7f80..66e4485c8 100644
--- a/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.js
+++ b/detox/src/artifacts/templates/plugin/TwoSnapshotsPerTestPlugin.js
@@ -1,3 +1,4 @@
+const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
 const ArtifactPlugin = require('./ArtifactPlugin');
 
 /***
@@ -72,7 +73,7 @@ class TwoSnapshotsPerTestPlugin extends ArtifactPlugin {
 
   async onCreateExternalArtifact(e) {
     if (!e.artifact) {
-      throw new Error('Internal error: expected Artifact instance in the event');
+      throw new DetoxRuntimeError('Internal error: expected Artifact instance in the event');
     }
 
     this._registerSnapshot(e.name, e.artifact);
diff --git a/detox/src/devices/drivers/ios/SimulatorDriver.test.js b/detox/src/devices/drivers/ios/SimulatorDriver.test.js
index a7e919ee5..e5dbcb9ae 100644
--- a/detox/src/devices/drivers/ios/SimulatorDriver.test.js
+++ b/detox/src/devices/drivers/ios/SimulatorDriver.test.js
@@ -79,6 +79,8 @@ describe('IOS simulator driver', () => {
   });
 
   describe('biometrics', () => {
+    let languageAndLocale;
+
     beforeEach(() => {
       languageAndLocale = '';
 
diff --git a/detox/src/errors/DetoxRuntimeError.test.js b/detox/src/errors/DetoxRuntimeError.test.js
index 44136b882..413326359 100644
--- a/detox/src/errors/DetoxRuntimeError.test.js
+++ b/detox/src/errors/DetoxRuntimeError.test.js
@@ -5,10 +5,16 @@ describe(DetoxRuntimeError, () => {
   it('should format all fields to a single message', () => {
     _.forEach(varietiesOfInstantiation(), (error, description) => {
       expect(error.toString()).toBeDefined();
-      expect(() => { throw error; }).toThrowError();
+      expect(() => { throw error; }).toThrowErrorMatchingSnapshot(description);
     });
   });
 
+  it('should format string as well, similar to Error', () => {
+    expect(new DetoxRuntimeError('Test')).toEqual(new DetoxRuntimeError({
+      message: 'Test'
+    }));
+  });
+
   it('should format any object to an error message', () => {
     expect(DetoxRuntimeError.format({})).toBe("{}");
 
@@ -35,6 +41,7 @@ describe(DetoxRuntimeError, () => {
   function varietiesOfInstantiation() {
     return {
       'no args': new DetoxRuntimeError(),
+      'plain string': new DetoxRuntimeError('A plain message'),
       'empty object': new DetoxRuntimeError({}),
       'only message': new DetoxRuntimeError({
         message: `The video is not being recorded on device (${'emulator-5554'}) at path: ${'/sdcard/712398.mp4'}`,
diff --git a/detox/src/errors/__snapshots__/DetoxRuntimeError.test.js.snap b/detox/src/errors/__snapshots__/DetoxRuntimeError.test.js.snap
new file mode 100644
index 000000000..aa34cf0e1
--- /dev/null
+++ b/detox/src/errors/__snapshots__/DetoxRuntimeError.test.js.snap
@@ -0,0 +1,45 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DetoxRuntimeError should format all fields to a single message: empty object 1`] = `""`;
+
+exports[`DetoxRuntimeError should format all fields to a single message: message with debug info 1`] = `
+"no filename was given to constructSafeFilename()
+
+the arguments were: {
+  \\"prefix\\": \\"detox - \\"
+}"
+`;
+
+exports[`DetoxRuntimeError should format all fields to a single message: message with debug info object 1`] = `
+"no filename was given to constructSafeFilename()
+
+{
+  prefix: 'detox - ',
+  trimmable: undefined,
+  suffix: undefined
+}"
+`;
+
+exports[`DetoxRuntimeError should format all fields to a single message: message with hint 1`] = `
+"Detox adapter to Jest is malfunctioning.
+
+HINT: Make sure you register it as Jasmine reporter inside init.js:
+-------------------------------------------------------------
+jasmine.getEnv().addReporter(adapter);"
+`;
+
+exports[`DetoxRuntimeError should format all fields to a single message: message with hint and debug info 1`] = `
+"Invalid test summary was passed to detox.beforeEach(testSummary)
+Expected to get an object of type: { title: string; fullName: string; status: \\"running\\" | \\"passed\\" | \\"failed\\"; }
+
+HINT: Maybe you are still using an old undocumented signature detox.beforeEach(string, string, string) in init.js ?
+See the article for the guidance: https://github.com/wix/detox/blob/master/docs/APIRef.TestLifecycle.md
+
+testSummary was: \\"test name\\""
+`;
+
+exports[`DetoxRuntimeError should format all fields to a single message: no args 1`] = `""`;
+
+exports[`DetoxRuntimeError should format all fields to a single message: only message 1`] = `"The video is not being recorded on device (emulator-5554) at path: /sdcard/712398.mp4"`;
+
+exports[`DetoxRuntimeError should format all fields to a single message: plain string 1`] = `"A plain message"`;
diff --git a/detox/src/ios/expectTwo.test.js b/detox/src/ios/expectTwo.test.js
index 2dd9df7d5..b338b9011 100644
--- a/detox/src/ios/expectTwo.test.js
+++ b/detox/src/ios/expectTwo.test.js
@@ -1,6 +1,8 @@
 const _ = require('lodash');
 
 describe('expectTwo', () => {
+  let e;
+
   beforeEach(() => {
     const IosExpect = require('./expectTwo');
     e = new IosExpect({