Repository: wix/Detox (js)

Issue:
android.attached not working on latest Detox version
**Describe the bug**
Running tests on a launched emulator throws error

**To Reproduce**

Steps to reproduce:
1. Launch an emulator, run _adb devices_ and get the emulator name
2. Add the emulator name to the configuration:
```
"android.emu.debug": {
                    "binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk",
                    "build": "cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..",
                    "type": "android.attached",
                    "device": {
                        "adbName": "emulator-5554"
                    }
                }
```
3. Build the app using _detox build_ and then execute tests using _detox test_

**Expected behavior**
App is installed on the emulator and tests are executed

**Device and Verbose Detox Logs**

```
TypeError: Cannot read property 'includes' of undefined
    at FreeDeviceFinder._isDeviceFreeAndMatching (/node_modules/detox/src/devices/drivers/android/tools/FreeDeviceFinder.js:27:41)
    at FreeDeviceFinder.findFreeDevice (/node_modules/detox/src/devices/drivers/android/tools/FreeDeviceFinder.js:14:22)
    at async safeAsync (/node_modules/detox/src/utils/safeAsync.js:4:13)
    at async /node_modules/detox/src/devices/DeviceRegistry.js:30:24
    at async ExclusiveLockfile.exclusively (/node_modules/detox/src/utils/ExclusiveLockfile.js:38:15)
    at async AttachedAndroidDriver.acquireFreeDevice (/node_modules/detox/src/devices/drivers/android/attached/AttachedAndroidDriver.js:21:21)
    at async Device.prepare (/node_modules/detox/src/devices/Device.js:16:22)
    at async Detox._doInit (/node_modules/detox/src/Detox.js:166:5)

```
**Environment:**
 - Detox: 17.13.0
 - React Native: 0.63.2
 - Node: 14.5.1
 - OS: Android

Pull request description:
Fix attached-android device usage regression
## Description

Resolves #2478. Introduced due to some refactor work that included Attached devices, as well (and due to the lack of CI coverage over attached devices - all in due time).

Patch:
diff --git a/README.md b/README.md
index 258fea487..b5e39496e 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
 
 ---
 
-<img src="https://user-images.githubusercontent.com/1962469/89655670-1c235c80-d8d3-11ea-9320-0f865767ef5d.png" alt="" height=24 width=1> [![NPM Version](https://img.shields.io/npm/v/detox.svg?style=flat)](https://www.npmjs.com/package/detox) [![NPM Downloads](https://img.shields.io/npm/dm/detox.svg?style=flat)](https://www.npmjs.com/package/detox) [![Build Status](https://img.shields.io/jenkins/s/http/jenkins-oss.wixpress.com:8080/job/multi-detox-master.svg)](https://jenkins-oss.wixpress.com/job/multi-detox-master/) [![Coverage Status](https://coveralls.io/repos/github/wix/Detox/badge.svg?branch=master)](https://coveralls.io/github/wix/Detox?branch=master) [![Detox is released under the MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![PR's welcome!](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](docs/Guide.Contributing.md)
+<img src="https://user-images.githubusercontent.com/1962469/89655670-1c235c80-d8d3-11ea-9320-0f865767ef5d.png" alt="" height=24 width=1> [![NPM Version](https://img.shields.io/npm/v/detox.svg?style=flat)](https://www.npmjs.com/package/detox) [![NPM Downloads](https://img.shields.io/npm/dm/detox.svg?style=flat)](https://www.npmjs.com/package/detox) [![Build Status](https://img.shields.io/jenkins/s/http/jenkins-oss.wixpress.com:8080/job/multi-detox-master.svg)](https://jenkins-oss.wixpress.com/job/multi-detox-master/) [![Coverage Status](https://coveralls.io/repos/github/wix/Detox/badge.svg?branch=master)](https://coveralls.io/github/wix/Detox?branch=master) [![Detox is released under the MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![PR's welcome!](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](docs/Guide.Contributing.md) [![Twitter Follow](https://img.shields.io/twitter/follow/detoxe2e?label=Follow&style=social)](https://twitter.com/detoxe2e)
 
 ## What does a Detox test look like?
 
diff --git a/detox/package.json b/detox/package.json
index 482d7cb6c..ba4d4d0b5 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -98,12 +98,10 @@
       "src/server/DetoxServer.js",
       "DeviceDriverBase.js",
       "AndroidDriver.js",
-      "AttachedAndroidDriver.js",
       "EmulatorDriver.js",
       "IosDriver.js",
       "SimulatorDriver.js",
       "AndroidDeviceAllocator.js",
-      "AttachedDeviceAllocator.js",
       "MissingDetox.js",
       "EmulatorTelnet.js",
       "EmulatorExec.js",
diff --git a/detox/src/devices/drivers/android/attached/AttachedAndroidDriver.js b/detox/src/devices/drivers/android/attached/AttachedAndroidDriver.js
index 03b280042..44a2c0f71 100644
--- a/detox/src/devices/drivers/android/attached/AttachedAndroidDriver.js
+++ b/detox/src/devices/drivers/android/attached/AttachedAndroidDriver.js
@@ -8,8 +8,8 @@ class AttachedAndroidDriver extends AndroidDriver {
     super(config);
     this._name = 'Unnamed Android Device';
 
-    this._freeDeviceFinder = new FreeDeviceFinder(this.adb, this.deviceRegistry);
     this._deviceRegistry = DeviceRegistry.forAndroid();
+    this._freeDeviceFinder = new FreeDeviceFinder(this.adb, this._deviceRegistry);
   }
 
   get name() {
@@ -28,9 +28,9 @@ class AttachedAndroidDriver extends AndroidDriver {
     return adbName;
   }
 
-  async cleanup(deviceId, bundleId) {
-    await this._deviceRegistry.disposeDevice(deviceId);
-    await super.cleanup(deviceId, bundleId);
+  async cleanup(adbName, bundleId) {
+    await this._deviceRegistry.disposeDevice(adbName);
+    await super.cleanup(adbName, bundleId);
   }
 }
 


Test patch:
diff --git a/detox/src/devices/drivers/android/attached/AttachedAndroidDriver.test.js b/detox/src/devices/drivers/android/attached/AttachedAndroidDriver.test.js
new file mode 100644
index 000000000..8e489e2c2
--- /dev/null
+++ b/detox/src/devices/drivers/android/attached/AttachedAndroidDriver.test.js
@@ -0,0 +1,148 @@
+const _ = require('lodash');
+const latestInstanceOf = (clazz) => _.last(clazz.mock.instances);
+
+const mockBaseClassesDependencies = () => {
+  jest.mock('../exec/ADB');
+  jest.mock('../exec/AAPT');
+  jest.mock('../tools/APKPath');
+  jest.mock('../tools/TempFileXfer');
+  jest.mock('../tools/AppInstallHelper');
+  jest.mock('../tools/AppUninstallHelper');
+  jest.mock('../tools/MonitoredInstrumentation');
+  jest.mock('../../../../artifacts/utils/AndroidDevicePathBuilder');
+  jest.mock('../../../../android/espressoapi/UiDeviceProxy');
+  jest.mock('../../../../utils/logger');
+};
+
+const mockDirectDependencies = () => {
+  jest.mock('../../../DeviceRegistry');
+  jest.mock('../tools/FreeDeviceFinder');
+};
+
+describe('Attached android device driver', () => {
+  beforeEach(mockBaseClassesDependencies);
+  beforeEach(mockDirectDependencies);
+
+  const adbNamePattern = '9A291FFAZ005S9';
+  const deviceConfig = {
+    adbName: adbNamePattern,
+  };
+
+  let emitter;
+  let adbObj;
+  let deviceRegistry;
+  let FreeDeviceFinder;
+  let freeDeviceFinderObj;
+  let uut;
+  beforeEach(() => {
+    const Emitter = jest.genMockFromModule('../../../../utils/AsyncEmitter');
+    emitter = new Emitter();
+
+    const { InvocationManager } = jest.genMockFromModule('../../../../invoke');
+    const invocationManager = new InvocationManager();
+
+    const ADB = require('../exec/ADB');
+    adbObj = () => latestInstanceOf(ADB);
+
+    const DeviceRegistry = require('../../../DeviceRegistry');
+    deviceRegistry = new DeviceRegistry();
+    deviceRegistry.allocateDevice.mockImplementation((doAllocateFn) => doAllocateFn());
+    DeviceRegistry.forAndroid.mockReturnValue(deviceRegistry);
+
+    FreeDeviceFinder = require('../tools/FreeDeviceFinder');
+    freeDeviceFinderObj = () => latestInstanceOf(FreeDeviceFinder);
+
+    const AttachedAndroidDriver = require('./AttachedAndroidDriver');
+    uut = new AttachedAndroidDriver({
+      invocationManager,
+      emitter,
+      client: {},
+    });
+  });
+
+  describe('device acquiring', () => {
+    it('should allocate a device', async () => {
+      deviceRegistry.allocateDevice.mockImplementation(async (userFn) => {
+        try {
+          return await userFn();
+        } finally {
+          expect(freeDeviceFinderObj().findFreeDevice).toHaveBeenCalledWith(adbNamePattern);
+        }
+      });
+
+      await uut.acquireFreeDevice(adbNamePattern);
+      expect(deviceRegistry.allocateDevice).toHaveBeenCalled();
+    });
+
+    it('should allocate a device based on an object-like configuration', async () => {
+      deviceRegistry.allocateDevice.mockImplementation(async (userFn) => {
+        try {
+          return await userFn();
+        } finally {
+          expect(freeDeviceFinderObj().findFreeDevice).toHaveBeenCalledWith(adbNamePattern);
+        }
+      });
+
+      await uut.acquireFreeDevice(deviceConfig);
+      expect(deviceRegistry.allocateDevice).toHaveBeenCalled();
+    });
+
+    it('should create a device finder', async () => {
+      expect(FreeDeviceFinder).toHaveBeenCalledWith(adbObj(), deviceRegistry);
+    });
+
+    it('should emit a bootDevice event', async () => {
+      const adbName = `${adbNamePattern}_allocated`;
+      deviceRegistry.allocateDevice.mockReturnValue(adbName);
+
+      await uut.acquireFreeDevice(adbNamePattern);
+
+      expect(emitter.emit).toHaveBeenCalledWith('bootDevice', { coldBoot: false, deviceId: adbName, type: 'device' });
+    });
+
+    it('should init ADB\'s API-level', async () => {
+      const adbName = `${adbNamePattern}_allocated`;
+      deviceRegistry.allocateDevice.mockReturnValue(adbName);
+
+      await uut.acquireFreeDevice(adbNamePattern);
+      expect(adbObj().apiLevel).toHaveBeenCalledWith(adbName);
+    });
+
+    it('should unlock the screen using ADB', async () => {
+      const adbName = `${adbNamePattern}_allocated`;
+      deviceRegistry.allocateDevice.mockReturnValue(adbName);
+
+      await uut.acquireFreeDevice(adbNamePattern);
+      expect(adbObj().unlockScreen).toHaveBeenCalledWith(adbName);
+    });
+
+    it('should initialize the name', async () => {
+      const adbName = `${adbNamePattern}_allocated`;
+      deviceRegistry.allocateDevice.mockReturnValue(adbName);
+
+      expect(uut.name).toEqual('Unnamed Android Device');
+      await uut.acquireFreeDevice(adbNamePattern);
+      expect(uut.name).toEqual(adbName);
+    });
+  });
+
+  describe('clean-up', () => {
+    const adbName = 'mock-adb-name';
+    const instrumentationObj = () => latestInstanceOf(Instrumentation);
+
+    let Instrumentation;
+    beforeEach(() => {
+      Instrumentation = require('../tools/MonitoredInstrumentation');
+    });
+
+    it('should dispose an instance based on its UUID', async () => {
+      await uut.cleanup(adbName, 'bundle-id');
+      expect(deviceRegistry.disposeDevice).toHaveBeenCalledWith(adbName);
+    });
+
+    it('should kill instrumentation', async () => {
+      await uut.cleanup(adbName, 'bundle-id');
+      expect(instrumentationObj().terminate).toHaveBeenCalled();
+    });
+  });
+});
diff --git a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
index 5a7241de7..95e44ea80 100644
--- a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
+++ b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
@@ -45,7 +45,7 @@ describe('Genymotion-cloud driver', () => {
 
   let logger;
   let emitter;
-  let adb;
+  let adbObj;
   let deviceQueryHelper;
   let deviceRegistry;
   let deviceCleanupRegistry;
@@ -63,7 +63,7 @@ describe('Genymotion-cloud driver', () => {
     const invocationManager = new InvocationManager();
 
     const ADB = require('../exec/ADB');
-    adb = () => latestInstanceOf(ADB);
+    adbObj = () => latestInstanceOf(ADB);
 
     const GenyDeviceRegistryFactory = require('./GenyDeviceRegistryFactory');
     const DeviceRegistry = jest.genMockFromModule('../../../DeviceRegistry');
@@ -187,7 +187,7 @@ describe('Genymotion-cloud driver', () => {
 
       await uut.acquireFreeDevice(aDeviceQuery());
 
-      expect(adb().apiLevel).toHaveBeenCalledWith(instance.adbName);
+      expect(adbObj().apiLevel).toHaveBeenCalledWith(instance.adbName);
     });
 
     it('should disable native animations', async () => {
@@ -197,7 +197,7 @@ describe('Genymotion-cloud driver', () => {
 
       await uut.acquireFreeDevice(aDeviceQuery());
 
-      expect(adb().disableAndroidAnimations).toHaveBeenCalledWith(instance.adbName);
+      expect(adbObj().disableAndroidAnimations).toHaveBeenCalledWith(instance.adbName);
     });
   });