Repository: wix/Detox (js)

Issue:
[SimulatorDriver] semantics of device.name has been unexpectedly changed since 7.4.0
#### Description
After upgrading to Detox 8.0.0 from 7.3.3, detox is not able to find the same iOS simulator using the name of the simulator. After downgrading, it works again, with no changes to the simulator config in between.

```
Error: a BeforeAll hook errored, process exiting: e2e/support/index.js:48: Can't find a simulator to match with "DETOX", run 'xcrun simctl list' to list your supported devices.
      It is advised to only state a device type, and not to state iOS version, e.g. "iPhone 7"
```

 Output of `xcrun simctl list`
```
== Devices ==
-- iOS 11.2 --
    DETOX (4E4CECC2-AA47-437F-B9B6-EC5917751329) (Booted)
-- iOS 11.3 --
    iPhone 7 (621A1BEF-EEF1-4017-BE73-A46DDAC3A0DF) (Shutdown)
-- tvOS 11.3 --
-- watchOS 4.3 --
== Device Pairs ==

``` Detox config in package.json
"detox": {
    "test-runner": "cucumber",
    "configurations": {
      "iphone": {
        "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/xx.app",
        "build": "yarn ios-detox-build",
        "type": "ios.simulator",
        "name": "DETOX"
      },
      "android": {
        "binaryPath": "./android/app/build/outputs/apk/debug/app-debug.apk",
        "build": "yarn android-detox-build",
        "type": "android.emulator",
        "name": "Nexus_5X_API_27"
      }
    }
  }
```
#### Steps to Reproduce
1. Create a working detox configuration with version `7.3.3` 
2. Upgrade to 8.0.0
3. Run tests

#### Detox, Node, Device, Xcode and macOS Versions

* Detox: 8.0.0
* React Native: 0.55
* Node: v8.11.3
* Device: iOS 11.2 simulator 
* Xcode: 9.3
* macOS: 10.13.4

#### Device and verbose Detox logs
```
$ yarn e2e-test e2e/features/
$ ENVFILE=.env.e2e export $(cat .env.e2e | xargs -I{} echo 'DETOX_{}') && ./node_modules/.bin/cucumber-js --require-module babel-core/register --require-module babel-preset-stage-2 -f node_modules/cucumber-pretty --artifacts-location ./artifacts --configuration iphone e2e/features/
Started mock server on PORT 9000
detox info 13:03:50: server listening on localhost:64969...
detox info 2: Searching for device matching DETOX... 
VError: a BeforeAll hook errored, process exiting: e2e/support/index.js:48: Can't find a simulator to match with "DETOX", run 'xcrun simctl list' to list your supported devices.
      It is advised to only state a device type, and not to state iOS version, e.g. "iPhone 7"
    at /Users/xx/dev/proj/x/mobile-app/node_modules/cucumber/src/runtime/index.js:36:15
    at Generator.next (<anonymous>)
    at Generator.tryCatcher (/Users/xx/dev/proj/x/mobile-app/node_modules/bluebird/js/release/util.js:16:23)
    at PromiseSpawn._promiseFulfilled (/Users/xx/dev/proj/xx/mobile-app/node_modules/bluebird/js/release/generators.js:97:49)
    at Promise._settlePromise (/Users/xx/dev/proj/xx/mobile-app/node_modules/bluebird/js/release/promise.js:574:26)
    at Promise._settlePromise0 (/Users/xx/dev/proj/xx/mobile-app/node_modules/bluebird/js/release/promise.js:614:10)
    at Promise._settlePromises (/Users/xx/dev/proj/xx/mobile-app/node_modules/bluebird/js/release/promise.js:693:18)
    at Async._drainQueue (/Users/xx/dev/proj/xx/mobile-app/node_modules/bluebird/js/release/async.js:133:16)
    at Async._drainQueues (/Users/xx/dev/proj/xx/mobile-app/node_modules/bluebird/js/release/async.js:143:10)
    at Immediate.Async.drainQueues (/Users/xx/dev/proj/xx/mobile-app/node_modules/bluebird/js/release/async.js:17:14)
    at runCallback (timers.js:810:20)
    at tryOnImmediate (timers.js:768:5)
    at processImmediate [as _immediateCallback] (timers.js:745:5)
caused by: Error: Can't find a simulator to match with "DETOX", run 'xcrun simctl list' to list your supported devices.
      It is advised to only state a device type, and not to state iOS version, e.g. "iPhone 7"
    at AppleSimUtils.findDevicesUDID (/Users/xx/dev/proj/xx/mobile-app/node_modules/detox/src/devices/AppleSimUtils.js:50:13)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)
error Command failed with exit code 1.
```


Pull request:
feat(configuration): name string -> device object
- [x] This change has been discussed in issues #853  and pull requests #905, #1404 .

### Issues resolved

Resolves #853 (to query a simulator specifically by name, use appropriately):

```
"device": { name: "Simulator name" }
```

### Summary
It is recommended to change `"name"` string to `"device"` object in your configurations, like shown below:

Before:
```json
{
  "ios.sim.debug": {
    "type": "ios.simulator",
    "name": "iPhone 11 Pro"
  },
  "android.emu.release": {
    "type": "android.emulator",
    "name": "Nexus_5X_API_29"
  },
  "android.att.release": {
    "type": "android.attached",
    "name": "YOGAA1BBB412"
  }
}
```

After:

```js
{
  "ios.sim.debug": {
    "type": "ios.simulator",
    "device": { // one of these or a combination of them
      "id": "D53474CF-7DD1-4673-8517-E75DAD6C34D6",
      "type": "iPhone 11 Pro",
      "name": "MySim",
      "os": "iOS 13.0",
    }
  },
  "android.emu.release": {
    "type": "android.emulator",
    "device": { // only avdName is supported at the moment
      "avdName": "Nexus_5X_API_29",
    }
  },
  "android.att.release": {
    "type": "android.attached",
    "device": { // only adbName is supported at the moment
      "adbName": "YOGAA1BBB412",
    }
  }
}
```

Patch:
diff --git a/detox/src/DetoxExportWrapper.js b/detox/src/DetoxExportWrapper.js
index 3a021cbb8..701f92aba 100644
--- a/detox/src/DetoxExportWrapper.js
+++ b/detox/src/DetoxExportWrapper.js
@@ -95,16 +95,15 @@ class DetoxExportWrapper {
                         ${Object.keys(configurations)}`);
     }
 
-    if (deviceOverride) {
-      deviceConfig.name = deviceOverride;
-    }
-
     if (!deviceConfig.type) {
       configuration.throwOnEmptyType();
     }
 
-    if (!deviceConfig.name) {
-      configuration.throwOnEmptyName();
+    deviceConfig.device = deviceOverride || deviceConfig.device || deviceConfig.name;
+    delete deviceConfig.name;
+
+    if (_.isEmpty(deviceConfig.device)) {
+      configuration.throwOnEmptyDevice();
     }
 
     return deviceConfig;
diff --git a/detox/src/configuration.js b/detox/src/configuration.js
index ac7dabd98..00647a0c3 100644
--- a/detox/src/configuration.js
+++ b/detox/src/configuration.js
@@ -23,12 +23,12 @@ function validateSession(session) {
   }
 }
 
-function throwOnEmptyName() {
-  throw new DetoxConfigError(`'name' property is missing, should hold the device name to run on (e.g. "iPhone 11 Pro", "iPhone 11 Pro, iOS 13.0"`);
+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 throwOnEmptyType() {
-  throw new DetoxConfigError(`'type' property is missing, should hold the device type to test on (currently only simulator is supported: ios.simulator or ios.none)`);
+  throw new DetoxConfigError(`'type' property is missing, should hold the device type to test on (e.g. "ios.simulator" or "android.emulator")`);
 }
 
 function throwOnEmptyBinaryPath() {
@@ -38,7 +38,7 @@ function throwOnEmptyBinaryPath() {
 module.exports = {
   defaultSession,
   validateSession,
-  throwOnEmptyName,
+  throwOnEmptyDevice,
   throwOnEmptyType,
   throwOnEmptyBinaryPath
 };
diff --git a/detox/src/configurations.mock.js b/detox/src/configurations.mock.js
index a2dfa3fa7..ee9891e3b 100644
--- a/detox/src/configurations.mock.js
+++ b/detox/src/configurations.mock.js
@@ -171,6 +171,31 @@ const validOneEmulator = {
   }
 };
 
+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 = {
   validOneDeviceNoSession,
   validOneIosNoneDeviceNoSession,
@@ -186,5 +211,7 @@ module.exports = {
   sessionPerConfiguration,
   sessionInCommonAndInConfiguration,
   validOneEmulator,
-  pathsTests
+  pathsTests,
+  deviceObjectEmulator,
+  deviceObjectSimulator,
 };
diff --git a/detox/src/devices/Device.js b/detox/src/devices/Device.js
index 1e7051091..3410cc997 100644
--- a/detox/src/devices/Device.js
+++ b/detox/src/devices/Device.js
@@ -17,7 +17,7 @@ class Device {
   async prepare(params = {}) {
     this._binaryPath = this._getAbsolutePath(this._deviceConfig.binaryPath);
     this._testBinaryPath = this._deviceConfig.testBinaryPath ? this._getAbsolutePath(this._deviceConfig.testBinaryPath) : null;
-    this._deviceId = await this.deviceDriver.acquireFreeDevice(this._deviceConfig.name);
+    this._deviceId = await this.deviceDriver.acquireFreeDevice(this._deviceConfig.device || this._deviceConfig.name);
     this._bundleId = await this.deviceDriver.getBundleIdFromBinary(this._binaryPath);
 
     await this.deviceDriver.prepare();
diff --git a/detox/src/devices/drivers/AttachedAndroidDriver.js b/detox/src/devices/drivers/AttachedAndroidDriver.js
index dfbeb6604..da6a5e5e2 100644
--- a/detox/src/devices/drivers/AttachedAndroidDriver.js
+++ b/detox/src/devices/drivers/AttachedAndroidDriver.js
@@ -1,3 +1,4 @@
+const _ = require('lodash');
 const AndroidDriver = require('./AndroidDriver');
 const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
 
@@ -12,7 +13,8 @@ class AttachedAndroidDriver extends AndroidDriver {
     return this._name;
   }
 
-  async acquireFreeDevice(adbName) {
+  async acquireFreeDevice(deviceQuery) {
+    const adbName = _.isPlainObject(deviceQuery) ? deviceQuery.adbName : deviceQuery;
     const { devices, stdout } = await this.adb.devices();
 
     if (!devices.some(d => d.adbName === adbName)) {
diff --git a/detox/src/devices/drivers/DeviceDriverBase.js b/detox/src/devices/drivers/DeviceDriverBase.js
index 9f4944c9e..c43e4f3d0 100644
--- a/detox/src/devices/drivers/DeviceDriverBase.js
+++ b/detox/src/devices/drivers/DeviceDriverBase.js
@@ -40,7 +40,7 @@ class DeviceDriverBase {
     return {};
   }
 
-  async acquireFreeDevice(name) {
+  async acquireFreeDevice(deviceQuery) {
     await Promise.resolve('');
   }
 
diff --git a/detox/src/devices/drivers/EmulatorDriver.js b/detox/src/devices/drivers/EmulatorDriver.js
index ab1046c9d..0977aef95 100644
--- a/detox/src/devices/drivers/EmulatorDriver.js
+++ b/detox/src/devices/drivers/EmulatorDriver.js
@@ -32,7 +32,9 @@ class EmulatorDriver extends AndroidDriver {
     return this._name
   }
 
-  async acquireFreeDevice(avdName) {
+  async acquireFreeDevice(deviceQuery) {
+    const avdName = _.isPlainObject(deviceQuery) ? deviceQuery.avdName : deviceQuery;
+
     await this._validateAvd(avdName);
     await this._fixEmulatorConfigIniSkinNameIfNeeded(avdName);
 
diff --git a/detox/src/devices/drivers/SimulatorDriver.js b/detox/src/devices/drivers/SimulatorDriver.js
index 6a0e0137d..28164a11d 100644
--- a/detox/src/devices/drivers/SimulatorDriver.js
+++ b/detox/src/devices/drivers/SimulatorDriver.js
@@ -39,13 +39,18 @@ class SimulatorDriver extends IosDriver {
     await super.cleanup(deviceId, bundleId);
   }
 
-  async acquireFreeDevice(query) {
+  async acquireFreeDevice(deviceQuery) {
     return this.deviceRegistry.allocateDevice(async () => {
-      const deviceId = await this._findOrCreateDevice(query);
-      await this._boot(deviceId);
+      const udid = await this._findOrCreateDevice(deviceQuery);
+      const deviceComment = this._commentDevice(deviceQuery);
 
-      this._name = `${deviceId} (${query})`;
-      return deviceId;
+      if (!udid) {
+        throw new Error(`Failed to find device matching ${deviceComment}`);
+      }
+
+      await this._boot(udid);
+      this._name = `${udid} ${deviceComment}`;
+      return udid;
     });
   }
 
@@ -142,10 +147,6 @@ class SimulatorDriver extends IosDriver {
     if (!deviceConfig.binaryPath) {
       configuration.throwOnEmptyBinaryPath();
     }
-
-    if (!deviceConfig.name) {
-      configuration.throwOnEmptyName();
-    }
   }
 
   getLogsPaths(deviceId) {
@@ -162,12 +163,13 @@ class SimulatorDriver extends IosDriver {
 
   /***
    * @private
-   * @param deviceQuery
+   * @param {String | Object} rawDeviceQuery
    * @returns {Promise<String>}
    */
-  async _findOrCreateDevice(deviceQuery) {
+  async _findOrCreateDevice(rawDeviceQuery) {
     let udid;
 
+    const deviceQuery = this._adaptQuery(rawDeviceQuery);
     const { free, busy } = await this._groupDevicesByStatus(deviceQuery);
 
     if (_.isEmpty(free)) {
@@ -177,15 +179,11 @@ class SimulatorDriver extends IosDriver {
       udid = free[0].udid;
     }
 
-    if (!udid) {
-      throw new Error(`Failed to find device matching "${deviceQuery}"`);
-    }
-
     return udid;
   }
 
-  async _groupDevicesByStatus(query) {
-    const searchResults = await this._queryDevices(query);
+  async _groupDevicesByStatus(deviceQuery) {
+    const searchResults = await this._queryDevices(deviceQuery);
 
     const { busy, free}  = _.groupBy(searchResults, device => {
       return this.deviceRegistry.isDeviceBusy(device.udid)
@@ -202,25 +200,15 @@ class SimulatorDriver extends IosDriver {
     }
   }
 
-  async _queryDevices(query) {
-    let byType, byOS, searchCriteria;
-
-    if (_.includes(query, ',')) {
-      [byType, byOS] = _.split(query, /\s*,\s*/);
-      searchCriteria = `type "${byType}" with "${byOS}"`;
-    } else {
-      byType = query;
-      searchCriteria = `type "${byType}"`;
-    }
-
+  async _queryDevices(deviceQuery) {
     const result = await this.applesimutils.list(
-      { byType, byOS },
-      `Searching for device of ${searchCriteria}...`
+      deviceQuery,
+      `Searching for device ${this._commentQuery(deviceQuery)} ...`
     );
 
     if (_.isEmpty(result)) {
       throw new DetoxRuntimeError({
-        message: `Failed to find a device of ${searchCriteria}`,
+        message: `Failed to find a device ${this._commentQuery(deviceQuery)}`,
         hint: `Run 'applesimutils --list' to list your supported devices. ` +
               `It is advised only to specify a device type, e.g., "iPhone Xʀ" and avoid explicit search by OS version.`
       });
@@ -228,6 +216,45 @@ class SimulatorDriver extends IosDriver {
 
     return result;
   }
+
+  _adaptQuery(rawDeviceQuery) {
+    let byId, byName, byOS, byType;
+
+    if (_.isPlainObject(rawDeviceQuery)) {
+      byId = rawDeviceQuery.id;
+      byName = rawDeviceQuery.name;
+      byOS = rawDeviceQuery.os;
+      byType = rawDeviceQuery.type;
+    } else {
+      if (_.includes(rawDeviceQuery, ',')) {
+        [byType, byOS] = _.split(rawDeviceQuery, /\s*,\s*/);
+      } else {
+        byType = rawDeviceQuery;
+      }
+    }
+
+    return _.omitBy({
+      byId,
+      byName,
+      byOS,
+      byType,
+    }, _.isUndefined);
+  }
+
+  _commentQuery({ byId, byName, byOS, byType }) {
+    return _.compact([
+      byId && `by UDID = ${JSON.stringify(byId)}`,
+      byName && `by name = ${JSON.stringify(byName)}`,
+      byType && `by type = ${JSON.stringify(byType)}`,
+      byOS && `by OS = ${JSON.stringify(byOS)}`,
+    ]).join(' and ');
+  }
+
+  _commentDevice(rawDeviceQuery) {
+    return _.isPlainObject(rawDeviceQuery)
+      ? JSON.stringify(rawDeviceQuery)
+      : `(${rawDeviceQuery})`;
+  }
 }
 
 module.exports = SimulatorDriver;
diff --git a/docs/APIRef.Configuration.md b/docs/APIRef.Configuration.md
index adad4b38e..b0d2b1aeb 100644
--- a/docs/APIRef.Configuration.md
+++ b/docs/APIRef.Configuration.md
@@ -11,23 +11,47 @@
 |`type`| Device type, available options are `ios.simulator`, `ios.none`, `android.emulator`, and `android.attached`. |
 |`binaryPath`| Relative path to the ipa/app due to be  tested (make sure you build the app in a project relative path)|
 |`testBinaryPath`| (optional, Android only): relative path to the test app (apk) |
-|`name`| Device name, aligns to the device list avaliable through `xcrun simctl list` for example, this is one line of the output of `xcrun simctl list`: `A3C93900-6D17-4830-8FBE-E102E4BBCBB9  iPhone 7  Shutdown  iPhone 7  iOS 10.2`, in order to choose the first `iPhone 7` regardless of OS version, use `iPhone 7`. To be OS specific use `iPhone 7, iOS 10.2`|
+|`device`| Device query, e.g. `{ "byType": "iPhone 11 Pro" }` for iOS simulator or `{ "avdName": "Nexus_5X_API_29" }` |
 |`build`| **[optional]** Build command (either `xcodebuild`, `react-native run-ios`, etc...), will be later available through detox CLI tool.|
 	
 **Example:**
 
-```json
+```js
+{
+  // ...
   "detox": {
-	...
+    // ...
     "configurations": {
       "ios.sim.debug": {
         "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/example.app",
         "build": "xcodebuild -project ios/example.xcodeproj -scheme example -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
         "type": "ios.simulator",
-        "name": "iPhone 7 Plus"
-      }
+        "device": { /* one of these or a combination of them */
+          "id": "D53474CF-7DD1-4673-8517-E75DAD6C34D6",
+          "type": "iPhone 11 Pro",
+          "name": "MySim",
+          "os": "iOS 13.0",
+        }
+      },
+      "android.emu.release": {
+        "binaryPath": "...",
+        "build": "...",
+        "type": "android.emulator",
+        "device": {
+          "avdName": "Nexus_5X_API_29",
+        }
+      },
+      "android.att.release": {
+        "binaryPath": "...",
+        "build": "...",
+        "type": "android.attached",
+        "device": {
+          "adbName": "YOGAA1BBB412",
+        }
+      },
     }
   }
+}
 ```	
 	
 ### Server Configuration
@@ -52,7 +76,7 @@ Session can also be set per configuration:
     "configurations": {
       "ios.sim.debug": {
         ...
-	"session": {
+        "session": {
           "server": "ws://localhost:8099",
           "sessionId": "YourProjectSessionId"
         }
diff --git a/docs/Guide.DebuggingInXcode.md b/docs/Guide.DebuggingInXcode.md
index 13433b867..3a7670f93 100644
--- a/docs/Guide.DebuggingInXcode.md
+++ b/docs/Guide.DebuggingInXcode.md
@@ -16,13 +16,15 @@ Edit the Detox section in `package.json` to add the following configuration:
 
 ```json
 "ios.none": {
-    "binaryPath": "ios",
-    "type": "ios.none",
-    "name": "iPhone 8 Plus",
-    "session": {
-        "server": "ws://localhost:8099",
-        "sessionId": "<your app's bundle identifier>"
-    }
+  "binaryPath": "ios",
+  "type": "ios.none",
+  "device": {
+    "type": "iPhone 8 Plus"
+  },
+  "session": {
+    "server": "ws://localhost:8099",
+    "sessionId": "<your app's bundle identifier>"
+  }
 }
 ```
 
diff --git a/docs/Guide.Migration.md b/docs/Guide.Migration.md
index a17c6fcea..f1c3403f6 100644
--- a/docs/Guide.Migration.md
+++ b/docs/Guide.Migration.md
@@ -2,6 +2,56 @@
 
 We are improving detox API as we go along, sometimes these changes require us to break the API in order for it to make more sense. These migration guides refer to breaking changes. If a newer version has no entries in this document, it means it does not require special migration steps. Refer to the release notes of the later builds to learn about their improvements and changes.
 
+## 14.5.0
+
+It is recommended to change "name" string to "device" object in your configurations, like shown below:
+
+Before:
+```json
+{
+  "ios.sim.debug": {
+    "type": "ios.simulator",
+    "name": "iPhone 11 Pro"
+  },
+  "android.emu.release": {
+    "type": "android.emulator",
+    "name": "Nexus_5X_API_29"
+  },
+  "android.att.release": {
+    "type": "android.attached",
+    "name": "YOGAA1BBB412"
+  }
+}
+```
+
+After:
+
+```js
+{
+  "ios.sim.debug": {
+    "type": "ios.simulator",
+    "device": { // one of these or a combination of them
+      "id": "D53474CF-7DD1-4673-8517-E75DAD6C34D6",
+      "type": "iPhone 11 Pro",
+      "name": "MySim",
+      "os": "iOS 13.0",
+    }
+  },
+  "android.emu.release": {
+    "type": "android.emulator",
+    "device": { // only avdName is supported at the moment
+      "avdName": "Nexus_5X_API_29",
+    }
+  },
+  "android.att.release": {
+    "type": "android.attached",
+    "device": { // only adbName is supported at the moment
+      "adbName": "YOGAA1BBB412",
+    }
+  }
+}
+```
+
 ## 14.0.0
 
 Detox 14.0.0 drops support for iOS 9.x simulators, and thus it also drops support for any API that is deprecated in iOS 10 and above. This includes legacy [remote](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623013-application?language=objc) and [local](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622930-application?language=objc) notifications handling API. These APIs have been deprecated since iOS 10, and we believe we've given app developers enough time to use the modern APIs. Make sure you transition to the [UserNotifications framework](https://developer.apple.com/documentation/usernotifications?language=objc) as soon as possible.
diff --git a/docs/Guide.RunningOnCI.md b/docs/Guide.RunningOnCI.md
index c73ef55a2..9c18a20b6 100644
--- a/docs/Guide.RunningOnCI.md
+++ b/docs/Guide.RunningOnCI.md
@@ -19,7 +19,9 @@ We will need to create a [release device configuration for Detox](/docs/APIRef.C
       "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
       "build": "xcodebuild -project ios/example.xcodeproj -scheme example -configuration Release -sdk iphonesimulator -derivedDataPath ios/build",
       "type": "ios.simulator",
-      "name": "iPhone 7"
+      "device": {
+        "type": "iPhone 11 Pro"
+      }
     }
   }
 }
diff --git a/docs/Introduction.Android.md b/docs/Introduction.Android.md
index ebcf4f1ed..9e5ea38c9 100644
--- a/docs/Introduction.Android.md
+++ b/docs/Introduction.Android.md
@@ -120,14 +120,17 @@ Add this part to your `package.json`:
             "build":
             "cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..",
             "type": "android.emulator",
-            "name": "Nexus_5X_API_24"
+            "device": {
+              "avdName": "Nexus_5X_API_24"
+            }
         },
         "android.emu.release": {
             "binaryPath": "android/app/build/outputs/apk/release/app-release.apk",
-            "build":
-            "cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ..",
+            "build": "cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ..",
             "type": "android.emulator",
-            "name": "Nexus_5X_API_26"
+            "device": {
+              "avdName": "Nexus_5X_API_26"
+            }
         }
     }
 }
diff --git a/docs/Introduction.GettingStarted.md b/docs/Introduction.GettingStarted.md
index fc9d8e563..e82b22545 100644
--- a/docs/Introduction.GettingStarted.md
+++ b/docs/Introduction.GettingStarted.md
@@ -92,7 +92,9 @@ The basic configuration for Detox should be in your `package.json` file under th
       "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/example.app",
       "build": "xcodebuild -project ios/example.xcodeproj -scheme example -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
       "type": "ios.simulator",
-      "name": "iPhone 7"
+      "device": {
+        "type": "iPhone 11 Pro"
+      }
     }
   }
 }
@@ -102,7 +104,7 @@ In the above configuration example, change `example` to your actual project name
 
 For React Native 0.60 or above, or any other iOS apps in a workspace (eg: CocoaPods) use `-workspace ios/example.xcworkspace` instead of `-project`.
 
-Also make sure the simulator model specified under the key `"name"` (`iPhone 7` above) is actually available on your machine (it was installed by Xcode). Check this by typing `xcrun simctl list` in terminal to display all available simulators.
+Also make sure the simulator model specified under the key `device.type` (e.g. `iPhone 11 Pro` above) is actually available on your machine (it was installed by Xcode). Check this by typing `applesimutils --list` in terminal to display all available simulators.
 
 > TIP: To test a release version, replace 'Debug' with 'Release' in the binaryPath and build properties. For full configuration options see Configuration under the API Reference.
 
diff --git a/docs/More.AndroidSupportStatus.md b/docs/More.AndroidSupportStatus.md
index a8c2048d9..021470c9b 100644
--- a/docs/More.AndroidSupportStatus.md
+++ b/docs/More.AndroidSupportStatus.md
@@ -62,7 +62,9 @@ To utilize Genymotion you should use 'android.attached' as configuration type pa
     "binaryPath": "./android/app/build/outputs/apk/app-debug.apk",
     "build": "pushd ./android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && popd",
     "type": "android.attached",
-    "name": "192.168.57.101:5555"
+    "device": {
+      "adbName": "192.168.57.101:5555"
+    }
 }
 ```
 
diff --git a/examples/demo-react-native-jest/package.json b/examples/demo-react-native-jest/package.json
index 0883f2eee..e5de22c60 100644
--- a/examples/demo-react-native-jest/package.json
+++ b/examples/demo-react-native-jest/package.json
@@ -23,12 +23,16 @@
       "ios.sim.release": {
         "binaryPath": "../demo-react-native/ios/build/Build/Products/Release-iphonesimulator/example.app",
         "type": "ios.simulator",
-        "name": "iPhone 11 Pro"
+        "device": {
+          "type": "iPhone 11 Pro"
+        }
       },
       "android.emu.release": {
         "binaryPath": "../demo-react-native/android/app/build/outputs/apk/release/app-release.apk",
         "type": "android.emulator",
-        "name": "Nexus_5X_API_26"
+        "device": {
+          "avdName": "Nexus_5X_API_26"
+        }
       }
     }
   }
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index 213546dd4..8be9806c9 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -31,19 +31,25 @@
         "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
         "build": "export RCT_NO_LAUNCH_PACKAGER=true && xcodebuild -project ios/example.xcodeproj -UseNewBuildSystem=NO -scheme example -configuration Release -sdk iphonesimulator -derivedDataPath ios/build -quiet",
         "type": "ios.simulator",
-        "name": "iPhone 11 Pro"
+        "device": {
+          "type": "iPhone 11 Pro"
+        }
       },
       "ios.sim.debug": {
         "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/example.app",
         "build": "xcodebuild -project ios/example.xcodeproj -UseNewBuildSystem=NO -scheme example -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
         "type": "ios.simulator",
-        "name": "iPhone 11 Pro"
+        "device": {
+          "type": "iPhone 11 Pro"
+        }
       },
       "ios.none": {
         "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/example.app",
         "build": "xcodebuild -project ios/example.xcodeproj -UseNewBuildSystem=NO -scheme example -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
         "type": "ios.none",
-        "name": "iPhone 11 Pro",
+        "device": {
+          "type": "iPhone 11 Pro"
+        },
         "session": {
           "server": "ws://localhost:8099",
           "sessionId": "com.wix.demo.react.native"
@@ -53,13 +59,17 @@
         "binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk",
         "build": "cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -",
         "type": "android.emulator",
-        "name": "Nexus_5X_API_26"
+        "device": {
+          "avdName": "Nexus_5X_API_26"
+        }
       },
       "android.emu.release": {
         "binaryPath": "android/app/build/outputs/apk/release/app-release.apk",
         "build": "cd android ; ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd -",
         "type": "android.emulator",
-        "name": "Nexus_5X_API_26"
+        "device": {
+          "avdName": "Nexus_5X_API_26"
+        }
       }
     }
   }


Test patch:
diff --git a/detox/src/devices/Device.test.js b/detox/src/devices/Device.test.js
index 82dedf24d..229647ef2 100644
--- a/detox/src/devices/Device.test.js
+++ b/detox/src/devices/Device.test.js
@@ -644,6 +644,7 @@ describe('Device', () => {
   });
 
   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({
       deviceConfig: invalidDeviceNoBinary.configurations['ios.sim.release'],
       deviceDriver: new SimulatorDriver(client),
@@ -651,14 +652,6 @@ describe('Device', () => {
     })).toThrowError(/binaryPath.* is missing/);
   });
 
-  it(`new Device() with invalid device config (no device name) should throw`, () => {
-    expect(() => new Device({
-      deviceConfig: invalidDeviceNoDeviceName.configurations['ios.sim.release'],
-      deviceDriver: new SimulatorDriver(client),
-      sessionConfig: validScheme.session,
-    })).toThrowError(/name.* is missing/);
-  });
-
   it(`should accept absolute path for binary`, async () => {
     const actualPath = await launchAndTestBinaryPath('absolutePath');
     expect(actualPath).toEqual(process.platform === 'win32' ? 'C:\\Temp\\abcdef\\123' : '/tmp/abcdef/123');
diff --git a/detox/src/devices/drivers/SimulatorDriver.test.js b/detox/src/devices/drivers/SimulatorDriver.test.js
index 14b7f3952..57c0071ec 100644
--- a/detox/src/devices/drivers/SimulatorDriver.test.js
+++ b/detox/src/devices/drivers/SimulatorDriver.test.js
@@ -79,6 +79,72 @@ describe('IOS simulator driver', () => {
       expect(sim.applesimutils.unmatchBiometric).toHaveBeenCalledWith(deviceId, 'Finger');
     })
   });
+
+  describe('acquireFreeDevice', () => {
+    let applesimutils;
+
+    beforeEach(() => {
+      const SimulatorDriver = require('./SimulatorDriver');
+      uut = new SimulatorDriver({ client: {} });
+      jest.spyOn(uut.deviceRegistry, 'isDeviceBusy').mockReturnValue(false);
+      applesimutils = uut.applesimutils;
+      applesimutils.list.mockImplementation(async () => require('../ios/applesimutils.mock')['--list']);
+    });
+
+    it('should accept string as device type', async () => {
+      await uut.acquireFreeDevice('iPhone X');
+
+      expect(applesimutils.list).toHaveBeenCalledWith(
+        { byType: 'iPhone X' },
+        'Searching for device by type = "iPhone X" ...'
+      );
+    });
+
+    it('should accept string with comma as device type and OS version', async () => {
+      await uut.acquireFreeDevice('iPhone X, iOS 12.0');
+
+      expect(applesimutils.list).toHaveBeenCalledWith(
+        { byType: 'iPhone X', byOS: 'iOS 12.0' },
+        'Searching for device by type = "iPhone X" and by OS = "iOS 12.0" ...'
+      );
+    });
+
+    it('should accept { byId } as matcher', async () => {
+      await uut.acquireFreeDevice({ id: 'C6EC2279-A6EB-40BE-99D2-5F11949F25E5' });
+
+      expect(applesimutils.list).toHaveBeenCalledWith(
+        { byId: 'C6EC2279-A6EB-40BE-99D2-5F11949F25E5' },
+        'Searching for device by UDID = "C6EC2279-A6EB-40BE-99D2-5F11949F25E5" ...'
+      );
+    });
+
+    it('should accept { byName } as matcher', async () => {
+      await uut.acquireFreeDevice({ name: 'Chika' });
+
+      expect(applesimutils.list).toHaveBeenCalledWith(
+        { byName: 'Chika' },
+        'Searching for device by name = "Chika" ...'
+      );
+    });
+
+    it('should accept { byType } as matcher', async () => {
+      await uut.acquireFreeDevice({ type: 'iPad Air' });
+
+      expect(applesimutils.list).toHaveBeenCalledWith(
+        { byType: 'iPad Air' },
+        'Searching for device by type = "iPad Air" ...'
+      );
+    });
+
+    it('should accept { byType, byOS } as matcher', async () => {
+      await uut.acquireFreeDevice({ type: 'iPad 2', os: 'iOS 9.3.6' });
+
+      expect(applesimutils.list).toHaveBeenCalledWith(
+        { byType: 'iPad 2', byOS: 'iOS 9.3.6' },
+        'Searching for device by type = "iPad 2" and by OS = "iOS 9.3.6" ...'
+      );
+    });
+  });
 });
 
 class mockAppleSimUtils {
@@ -87,5 +153,7 @@ class mockAppleSimUtils {
     this.setBiometricEnrollment = jest.fn();
     this.matchBiometric = jest.fn();
     this.unmatchBiometric = jest.fn();
+    this.boot = jest.fn();
+    this.list = jest.fn();
   }
 }
diff --git a/detox/src/index.test.js b/detox/src/index.test.js
index b2a93b0c8..522838500 100644
--- a/detox/src/index.test.js
+++ b/detox/src/index.test.js
@@ -109,7 +109,7 @@ describe('index', () => {
 
     const expectedConfig = {
       ...schemes.validOneDeviceNoSession.configurations['ios.sim.release'],
-      name: 'iPhone X'
+      device: 'iPhone X'
     }
 
     expect(Detox).toHaveBeenCalledWith({
diff --git a/detox/test/package.json b/detox/test/package.json
index 957343997..799f08932 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -48,18 +48,24 @@
         "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/example.app",
         "build": "set -o pipefail && xcodebuild -project ios/example.xcodeproj -UseNewBuildSystem=NO -scheme example_ci -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
         "type": "ios.simulator",
-        "name": "iPhone 11 Pro"
+        "device": {
+          "type": "iPhone 11 Pro"
+        }
       },
       "ios.sim.release": {
         "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
         "build": "set -o pipefail && export CODE_SIGNING_REQUIRED=NO && export RCT_NO_LAUNCH_PACKAGER=true && xcodebuild -project ios/example.xcodeproj  -UseNewBuildSystem=NO -scheme example_ci -configuration Release -sdk iphonesimulator -derivedDataPath ios/build",
         "type": "ios.simulator",
-        "name": "iPhone 11 Pro"
+        "device": {
+          "type": "iPhone 11 Pro"
+        }
       },
       "ios.none": {
         "binaryPath": "ios",
         "type": "ios.none",
-        "name": "iPhone 11 Pro",
+        "device": {
+          "type": "iPhone 11 Pro"
+        },
         "session": {
           "server": "ws://localhost:8099",
           "sessionId": "com.wix.detox-example"
@@ -69,19 +75,25 @@
         "binaryPath": "android/app/build/outputs/apk/fromBin/debug/app-fromBin-debug.apk",
         "build": "cd android && ./gradlew assembleFromBinDebug assembleFromBinDebugAndroidTest -DtestBuildType=debug && cd ..",
         "type": "android.emulator",
-        "name": "Nexus_5X_API_26"
+        "device": {
+          "avdName": "Nexus_5X_API_26"
+        }
       },
       "android.emu.release": {
         "binaryPath": "android/app/build/outputs/apk/fromBin/release/app-fromBin-release.apk",
         "build": "cd android && ./gradlew assembleFromBinRelease assembleFromBinReleaseAndroidTest -DtestBuildType=release && cd ..",
         "type": "android.emulator",
-        "name": "Nexus_5X_API_26"
+        "device": {
+          "avdName": "Nexus_5X_API_26"
+        }
       },
       "android.emu.debug.fromSource": {
         "binaryPath": "android/app/build/outputs/apk/fromSource/debug/app-fromSource-debug.apk",
         "build": "cd android && ./gradlew assembleFromSourceDebug assembleFromSourceDebugAndroidTest -DtestBuildType=debug && cd ..",
         "type": "android.emulator",
-        "name": "Nexus_5X_API_26"
+        "device": {
+          "avdName": "Nexus_5X_API_26"
+        }
       }
     }
   },