Repository: wix/Detox
Pull request #4005 (2023-04-10 11:16:03)

feat(drivers): add postAllocate() method
## Description

- This pull request addresses the issue described here: #3655

In this pull request, I have added a new method, `postAllocate` to the device allocation drivers, which is an optional method to handle operations after the device is allocated, such as waiting until the boot animation is finished, configuring the device, etc.

```diff
class MyNewAllocationDriver {
  constructor(deps) {
    this.emitter = deps.eventEmitter;
  }
  async allocate(deviceConfig) {
    // ...
    return new Cookie(id); // This is where a cookie is formed once for the entire process
  }
+  async postAllocate(cookie) {
+   // Optional method to handle operations after the device is allocated
+    // like waiting until the boot animation is finished, configuring the device, etc.
+  }
  async free(cookie, options) {
    // ...
  }
}
```
This helps to separate "acquiring a _cookie_" from "finishing the preparation" so that even in case the preparation goes wrong, the layer responsible for the teardown is guaranteed to receive the cookie and be able to clean up.

Previously, a broken post-cookie preparation resulted in us not having a cookie to use when freeing the device – i.e., not freeing the device. So, I rewrote our allocation drivers to leverage this method. This simplified a few `try-catch` places where we were aware of this problem yet applied a workaround (and missed this problem with Genymotion drivers). 

> _For features/enhancements:_
 - [x] I have added/updated the relevant references in the [documentation](https://github.com/wix/Detox/tree/master/docs) files.


Patch:
diff --git a/detox/src/DetoxWorker.js b/detox/src/DetoxWorker.js
index ce8ead075..56cc795c6 100644
--- a/detox/src/DetoxWorker.js
+++ b/detox/src/DetoxWorker.js
@@ -138,6 +138,8 @@ class DetoxWorker {
     this._deviceAllocator = deviceAllocatorFactory.createDeviceAllocator(commonDeps);
     this._deviceCookie = yield this._deviceAllocator.allocate(this._deviceConfig);
 
+    yield this._deviceAllocator.postAllocate(this._deviceCookie);
+
     this.device = runtimeDeviceFactory.createRuntimeDevice(
       this._deviceCookie,
       commonDeps,
@@ -200,9 +202,12 @@ class DetoxWorker {
     }
 
     if (this.device) {
-      const shutdown = this._behaviorConfig ? this._behaviorConfig.cleanup.shutdownDevice : false;
       // @ts-ignore
       await this.device._cleanup();
+    }
+
+    if (this._deviceCookie) {
+      const shutdown = this._behaviorConfig ? this._behaviorConfig.cleanup.shutdownDevice : false;
       await this._deviceAllocator.free(this._deviceCookie, { shutdown });
     }
 
diff --git a/detox/src/devices/allocation/DeviceAllocator.js b/detox/src/devices/allocation/DeviceAllocator.js
index 5cd689865..091c7f582 100644
--- a/detox/src/devices/allocation/DeviceAllocator.js
+++ b/detox/src/devices/allocation/DeviceAllocator.js
@@ -8,7 +8,7 @@ class DeviceAllocator {
    */
   constructor(allocationDriver) {
     this._driver = allocationDriver;
-    traceMethods(log, this, ['allocate', 'free']);
+    traceMethods(log, this, ['allocate', 'postAllocate', 'free']);
   }
 
   /**
@@ -19,6 +19,18 @@ class DeviceAllocator {
     return this._driver.allocate(deviceConfig);
   }
 
+  /**
+   * @param {DeviceCookie} deviceCookie
+   * @return {Promise<unknown>}
+   */
+  postAllocate(deviceCookie) {
+    if (typeof this._driver.postAllocate !== 'function') {
+      return Promise.resolve();
+    }
+
+    return this._driver.postAllocate(deviceCookie);
+  }
+
   /**
    * @param cookie { DeviceCookie }
    * @param options { DeallocOptions }
diff --git a/detox/src/devices/allocation/drivers/AllocationDriverBase.js b/detox/src/devices/allocation/drivers/AllocationDriverBase.js
index 542eab8f9..09f7ce4d3 100644
--- a/detox/src/devices/allocation/drivers/AllocationDriverBase.js
+++ b/detox/src/devices/allocation/drivers/AllocationDriverBase.js
@@ -13,6 +13,12 @@ class AllocationDriverBase {
    */
   async allocate(deviceConfig) {}
 
+  /**
+   * @param {DeviceCookie} deviceCookie
+   * @return {Promise<void>}
+   */
+  async postAllocate(deviceCookie) {}
+
   /**
    * @param cookie { DeviceCookie }
    * @param options { DeallocOptions }
diff --git a/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.js b/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.js
index 89eb805ae..0e011092c 100644
--- a/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.js
+++ b/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.js
@@ -25,11 +25,20 @@ class AttachedAndroidAllocDriver extends AllocationDriverBase {
     const adbNamePattern = deviceConfig.device.adbName;
     const adbName = await this._deviceRegistry.allocateDevice(() => this._freeDeviceFinder.findFreeDevice(adbNamePattern));
 
+    return new AttachedAndroidDeviceCookie(adbName);
+  }
+
+  /**
+   * @param {AttachedAndroidDeviceCookie} deviceCookie
+   * @returns {Promise<void>}
+   */
+  async postAllocate(deviceCookie) {
+    const { adbName } = deviceCookie;
+
     // TODO Also disable native animations?
     await this._adb.apiLevel(adbName);
     await this._adb.unlockScreen(adbName);
     await this._attachedAndroidLauncher.notifyLaunchCompleted(adbName);
-    return new AttachedAndroidDeviceCookie(adbName);
   }
 
   /**
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
index fcc9b5111..c61294e05 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
@@ -1,8 +1,6 @@
 // @ts-nocheck
 const _ = require('lodash');
 
-const log = require('../../../../../utils/logger').child({ cat: 'device' });
-const traceMethods = require('../../../../../utils/traceMethods');
 const AndroidEmulatorCookie = require('../../../../cookies/AndroidEmulatorCookie');
 const AllocationDriverBase = require('../../AllocationDriverBase');
 
@@ -23,8 +21,7 @@ class EmulatorAllocDriver extends AllocationDriverBase {
     this._emulatorVersionResolver = emulatorVersionResolver;
     this._emulatorLauncher = emulatorLauncher;
     this._allocationHelper = allocationHelper;
-
-    traceMethods(log, this, ['_launchEmulator']);
+    this._launchInfo = {};
   }
 
   /**
@@ -38,21 +35,37 @@ class EmulatorAllocDriver extends AllocationDriverBase {
     await this._fixAvdConfigIniSkinNameIfNeeded(avdName, deviceConfig.headless);
 
     const allocResult = await this._allocationHelper.allocateDevice(avdName);
-    const { adbName, placeholderPort, isRunning } = allocResult;
-    const launchOptions = {
-      bootArgs: deviceConfig.bootArgs,
-      gpuMode: deviceConfig.gpuMode,
-      headless: deviceConfig.headless,
-      readonly: deviceConfig.readonly,
-      port: placeholderPort,
+    const { adbName } = allocResult;
+
+    this._launchInfo[adbName] = {
+      avdName,
+      isRunning: allocResult.isRunning,
+      launchOptions: {
+        bootArgs: deviceConfig.bootArgs,
+        gpuMode: deviceConfig.gpuMode,
+        headless: deviceConfig.headless,
+        readonly: deviceConfig.readonly,
+        port: allocResult.placeholderPort,
+      },
     };
 
-    await this._launchEmulator(avdName, adbName, isRunning, launchOptions);
-    await this._prepareEmulator(adbName);
-
     return new AndroidEmulatorCookie(adbName);
   }
 
+  /**
+   * @param {AndroidEmulatorCookie} deviceCookie
+   * @returns {Promise<void>}
+   */
+  async postAllocate(deviceCookie) {
+    const { adbName } = deviceCookie;
+    const { avdName, isRunning, launchOptions } = this._launchInfo[adbName];
+
+    await this._emulatorLauncher.launch(avdName, adbName, isRunning, launchOptions);
+    await this._adb.apiLevel(adbName);
+    await this._adb.disableAndroidAnimations(adbName);
+    await this._adb.unlockScreen(adbName);
+  }
+
   /**
    * @param cookie { AndroidEmulatorCookie }
    * @param options { DeallocOptions }
@@ -73,21 +86,6 @@ class EmulatorAllocDriver extends AllocationDriverBase {
     const binaryVersion = _.get(rawBinaryVersion, 'major');
     return await patchAvdSkinConfig(avdName, binaryVersion);
   }
-
-  async _launchEmulator(avdName, adbName, isRunning, options) {
-    try {
-      await this._emulatorLauncher.launch(avdName, adbName, isRunning, options);
-    } catch (e) {
-      await this._allocationHelper.deallocateDevice(adbName);
-      throw e;
-    }
-  }
-
-  async _prepareEmulator(adbName) {
-    await this._adb.apiLevel(adbName);
-    await this._adb.disableAndroidAnimations(adbName);
-    await this._adb.unlockScreen(adbName);
-  }
 }
 
 module.exports = EmulatorAllocDriver;
diff --git a/detox/src/devices/allocation/drivers/android/genycloud/GenyAllocDriver.js b/detox/src/devices/allocation/drivers/android/genycloud/GenyAllocDriver.js
index c8fba7980..dfd490c11 100644
--- a/detox/src/devices/allocation/drivers/android/genycloud/GenyAllocDriver.js
+++ b/detox/src/devices/allocation/drivers/android/genycloud/GenyAllocDriver.js
@@ -17,6 +17,7 @@ class GenyAllocDriver extends AllocationDriverBase {
     this._recipeQuerying = recipeQuerying;
     this._instanceLauncher = instanceLauncher;
     this._instanceAllocationHelper = allocationHelper;
+    this._launchInfo = {};
   }
 
   /**
@@ -28,21 +29,24 @@ class GenyAllocDriver extends AllocationDriverBase {
     const recipe = await this._recipeQuerying.getRecipeFromQuery(deviceQuery);
     this._assertRecipe(deviceQuery, recipe);
 
-    const allocResult = await this._instanceAllocationHelper.allocateDevice(recipe);
-    let { instance, isNew } = allocResult;
+    const { instance, isNew } = await this._instanceAllocationHelper.allocateDevice(recipe);
+    this._launchInfo[instance.uuid] = { isNew };
+    return new GenycloudEmulatorCookie(instance);
+  }
 
-    try {
-      instance = await this._instanceLauncher.launch(instance, isNew);
-    } catch (e) {
-      await this._instanceAllocationHelper.deallocateDevice(instance.uuid);
-      throw e;
-    }
-    const { adbName } = instance;
+  /**
+   * @param {GenycloudEmulatorCookie} cookie
+   * @returns {Promise<void>}
+   */
+  async postAllocate(cookie) {
+    const { instance } = cookie;
+    const { isNew } = this._launchInfo[instance.uuid];
+    const readyInstance = cookie.instance = await this._instanceLauncher.launch(instance, isNew);
 
+    const { adbName } = readyInstance;
     await this._adb.disableAndroidAnimations(adbName);
     await this._adb.setWiFiToggle(adbName, true);
     await this._adb.apiLevel(adbName);
-    return new GenycloudEmulatorCookie(instance);
   }
 
   /**
diff --git a/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js b/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js
index e473c22a8..e00791c3d 100644
--- a/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js
+++ b/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js
@@ -16,6 +16,7 @@ class SimulatorAllocDriver extends AllocationDriverBase {
     this._deviceRegistry = deviceRegistry;
     this._applesimutils = applesimutils;
     this._simulatorLauncher = simulatorLauncher;
+    this._launchInfo = {};
   }
 
   /**
@@ -35,16 +36,20 @@ class SimulatorAllocDriver extends AllocationDriverBase {
       throw new DetoxRuntimeError(`Failed to find device matching ${deviceComment}`);
     }
 
-    try {
-      await this._simulatorLauncher.launch(udid, deviceConfig.type, deviceConfig.bootArgs, deviceConfig.headless);
-    } catch (e) {
-      await this._deviceRegistry.disposeDevice(udid);
-      throw e;
-    }
-
+    this._launchInfo[udid] = { deviceConfig };
     return new IosSimulatorCookie(udid);
   }
 
+  /**
+   * @param {IosSimulatorCookie} deviceCookie
+   * @returns {Promise<void>}
+   */
+  async postAllocate(deviceCookie) {
+    const { udid } = deviceCookie;
+    const { deviceConfig } = this._launchInfo[udid];
+    await this._simulatorLauncher.launch(udid, deviceConfig.type, deviceConfig.bootArgs, deviceConfig.headless);
+  }
+
   /**
    * @param cookie { IosSimulatorCookie }
    * @param options { DeallocOptions }
diff --git a/docs/articles/third-party-drivers.md b/docs/articles/third-party-drivers.md
index 09a82b7ca..a0deff089 100644
--- a/docs/articles/third-party-drivers.md
+++ b/docs/articles/third-party-drivers.md
@@ -102,6 +102,11 @@ class MyNewAllocationDriver {
     return new Cookie(id); // This is where a cookie is formed once for the entire process
   }
 
+  async postAllocate(cookie) {
+    // Optional method to handle operations after the device is allocated
+    // like waiting until the boot animation is finished, configuring the device, etc.
+  }
+
   async free(cookie, options) {
     // ...
   }


Test patch:
diff --git a/detox/src/devices/allocation/DeviceAllocator.test.js b/detox/src/devices/allocation/DeviceAllocator.test.js
index 7815633e3..8bb852165 100644
--- a/detox/src/devices/allocation/DeviceAllocator.test.js
+++ b/detox/src/devices/allocation/DeviceAllocator.test.js
@@ -7,6 +7,7 @@ describe('Device allocator', () => {
   beforeEach(() => {
     allocDriver = {
       allocate: jest.fn(),
+      postAllocate: jest.fn(),
       free: jest.fn(),
     };
 
@@ -27,6 +28,18 @@ describe('Device allocator', () => {
     expect(allocDriver.allocate).toHaveBeenCalledWith(query);
   });
 
+  it('should post-allocate via driver', async () => {
+    const cookie = {};
+
+    const result = await deviceAllocator.postAllocate(cookie);
+    expect(allocDriver.postAllocate).toHaveBeenCalledWith(cookie);
+  });
+
+  it('should not post-allocate if the driver does not implement this function', async () => {
+    delete allocDriver.postAllocate;
+    await expect(deviceAllocator.postAllocate({})).resolves.not.toThrow();
+  });
+
   it('should deallocate via driver', async () => {
     const cookie = { cookie: 'mock' };
     const options = { shutdown: true };
diff --git a/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.test.js b/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.test.js
index ea9b0b3e7..e3fb92a81 100644
--- a/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.test.js
+++ b/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.test.js
@@ -62,27 +62,6 @@ describe('Allocation driver for attached Android devices', () => {
       await expect(allocDriver.allocate(deviceConfig)).rejects.toThrowError('mock error');
     });
 
-    it('should init ADB\'s API-level', async () => {
-      givenDeviceAllocationResult(adbName);
-
-      await allocDriver.allocate(deviceConfig);
-      expect(adb.apiLevel).toHaveBeenCalledWith(adbName);
-    });
-
-    it('should unlock the screen using ADB', async () => {
-      givenDeviceAllocationResult(adbName);
-
-      await allocDriver.allocate(deviceConfig);
-      expect(adb.unlockScreen).toHaveBeenCalledWith(adbName);
-    });
-
-    it('should report boot-event via launcher', async () => {
-      givenDeviceAllocationResult(adbName);
-
-      await allocDriver.allocate(deviceConfig);
-      expect(attachedAndroidLauncher.notifyLaunchCompleted).toHaveBeenCalledWith(adbName);
-    });
-
     it('should return a valid cookie', async () => {
       const Cookie = require('../../../../cookies/AttachedAndroidDeviceCookie');
 
@@ -92,6 +71,28 @@ describe('Allocation driver for attached Android devices', () => {
       expect(result.constructor.name).toEqual('AttachedAndroidDeviceCookie');
       expect(Cookie).toHaveBeenCalledWith(adbName);
     });
+
+    describe('post-allocation', () => {
+      beforeEach(async () => {
+        givenDeviceAllocationResult(adbName);
+        await allocDriver.allocate(deviceConfig);
+      });
+
+      it('should init ADB\'s API-level', async () => {
+        await allocDriver.postAllocate({ adbName });
+        expect(adb.apiLevel).toHaveBeenCalledWith(adbName);
+      });
+
+      it('should unlock the screen using ADB', async () => {
+        await allocDriver.postAllocate({ adbName });
+        expect(adb.unlockScreen).toHaveBeenCalledWith(adbName);
+      });
+
+      it('should report boot-event via launcher', async () => {
+        await allocDriver.postAllocate({ adbName });
+        expect(attachedAndroidLauncher.notifyLaunchCompleted).toHaveBeenCalledWith(adbName);
+      });
+    });
   });
 
   describe('deallocation', () => {
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js
index ddd596820..ff653ded3 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.test.js
@@ -72,8 +72,6 @@ describe('Allocation driver for Google emulators', () => {
 
   describe('allocation', () => {
     beforeEach(() => {
-      jest.mock('../../../../cookies/AndroidEmulatorCookie');
-
       givenAllocationOfRunningEmulator();
     });
 
@@ -88,69 +86,6 @@ describe('Allocation driver for Google emulators', () => {
       await expect(allocDriver.allocate(deviceConfig)).rejects.toThrowError();
     });
 
-    describe('given an allocated emulator that is not currently running', () => {
-      beforeEach(() => {
-        givenAllocationOfPlaceholderEmulator();
-      });
-
-      it('should launch it', async () => {
-        await allocDriver.allocate(deviceConfig);
-        expect(emulatorLauncher.launch).toHaveBeenCalledWith(avdName, adbName, false, expect.objectContaining({ port: placeholderPort }));
-      });
-
-      it('should pass-through various launch options', async () => {
-        await allocDriver.allocate(extendedDeviceConfig);
-        expect(emulatorLauncher.launch).toHaveBeenCalledWith(
-          expect.anything(),
-          expect.anything(),
-          expect.anything(),
-          expect.objectContaining(launchOptions),
-        );
-      });
-
-      it('should resort to undefined launch options', async () => {
-        const launchOptions = {
-          headless: undefined,
-          readonly: undefined,
-          gpuMode: undefined,
-          bootArgs: undefined,
-        };
-
-        await allocDriver.allocate(deviceConfig);
-        expect(emulatorLauncher.launch).toHaveBeenCalledWith(
-          expect.anything(),
-          expect.anything(),
-          expect.anything(),
-          expect.objectContaining(launchOptions),
-        );
-      });
-
-      it('should deallocate it, if launching fails', async () => {
-        givenEmulatorLaunchError();
-
-        try {
-          await allocDriver.allocate(deviceConfig);
-        } catch (e) {}
-        expect(allocationHelper.deallocateDevice).toHaveBeenCalledWith(adbName);
-      });
-
-      it('should rethrow the error, if launching fails', async () => {
-        givenEmulatorLaunchError();
-        await expect(allocDriver.allocate(deviceConfig)).rejects.toThrowError();
-      });
-    });
-
-    describe('given an allocated emulator that is already running', () => {
-      beforeEach(() => {
-        givenAllocationOfRunningEmulator();
-      });
-
-      it('should launch it with isRunning=true', async () => {
-        await allocDriver.allocate(deviceConfig);
-        expect(emulatorLauncher.launch).toHaveBeenCalledWith(avdName, adbName, true, expect.objectContaining({ port: placeholderPort }));
-      });
-    });
-
     it('should pre-validate proper AVD configuration', async () => {
       givenValidAVD();
       await allocDriver.allocate(deviceConfig);
@@ -181,29 +116,94 @@ describe('Allocation driver for Google emulators', () => {
       expect(patchAvdSkinConfig).toHaveBeenCalledWith(avdName, majorVersion);
     });
 
-    it('should prepare the emulators itself', async () => {
-      givenAllocationOfRunningEmulator();
+    it('should return an Android emulator handle', async () => {
+      const AndroidEmulatorCookie = require('../../../../cookies/AndroidEmulatorCookie');
 
-      await allocDriver.allocate(deviceConfig);
+      const handle = await allocDriver.allocate(deviceConfig);
+      expect(handle).toBeInstanceOf(AndroidEmulatorCookie);
+      expect(handle.adbName).toBe(adbName);
+    });
+
+    describe('post-allocation', () => {
+      describe('given an allocated emulator that is not currently running', () => {
+        beforeEach(() => {
+          givenAllocationOfPlaceholderEmulator();
+        });
+
+        it('should launch it', async () => {
+          const cookie = await allocDriver.allocate(deviceConfig);
+          await allocDriver.postAllocate(cookie);
+
+          expect(emulatorLauncher.launch).toHaveBeenCalledWith(avdName, adbName, false, expect.objectContaining({ port: placeholderPort }));
+        });
+
+        it('should pass-through various launch options', async () => {
+          const cookie = await allocDriver.allocate(extendedDeviceConfig);
+          await allocDriver.postAllocate(cookie);
+
+          expect(emulatorLauncher.launch).toHaveBeenCalledWith(
+            expect.anything(),
+            expect.anything(),
+            expect.anything(),
+            expect.objectContaining(launchOptions),
+          );
+        });
+
+        it('should resort to undefined launch options', async () => {
+          const launchOptions = {
+            headless: undefined,
+            readonly: undefined,
+            gpuMode: undefined,
+            bootArgs: undefined,
+          };
+
+          const cookie = await allocDriver.allocate(deviceConfig);
+          await allocDriver.postAllocate(cookie);
+          expect(emulatorLauncher.launch).toHaveBeenCalledWith(
+            expect.anything(),
+            expect.anything(),
+            expect.anything(),
+            expect.objectContaining(launchOptions),
+          );
+        });
+
+        it('should rethrow the error, if launching fails', async () => {
+          givenEmulatorLaunchError();
+          const cookie = await allocDriver.allocate(deviceConfig);
+          await expect(allocDriver.postAllocate(cookie)).rejects.toThrowError();
+        });
+      });
 
-      expect(adb.disableAndroidAnimations).toHaveBeenCalledWith(adbName);
-      expect(adb.unlockScreen).toHaveBeenCalledWith(adbName);
-    });
+      describe('given an allocated emulator that is already running', () => {
+        beforeEach(() => {
+          givenAllocationOfRunningEmulator();
+        });
 
-    it('should inquire the API level', async () => {
-      givenAllocationOfRunningEmulator();
+        it('should launch it with isRunning=true', async () => {
+          const cookie = await allocDriver.allocate(deviceConfig);
+          await allocDriver.postAllocate(cookie);
+          expect(emulatorLauncher.launch).toHaveBeenCalledWith(avdName, adbName, true, expect.objectContaining({ port: placeholderPort }));
+        });
+      });
 
-      await allocDriver.allocate(deviceConfig);
+      it('should prepare the emulators itself', async () => {
+        givenAllocationOfRunningEmulator();
 
-      expect(adb.apiLevel).toHaveBeenCalledWith(adbName);
-    });
+        const cookie = await allocDriver.allocate(deviceConfig);
+        await allocDriver.postAllocate(cookie);
 
-    it('should return an Android emulator handle', async () => {
-      const AndroidEmulatorCookie = require('../../../../cookies/AndroidEmulatorCookie');
+        expect(adb.disableAndroidAnimations).toHaveBeenCalledWith(adbName);
+        expect(adb.unlockScreen).toHaveBeenCalledWith(adbName);
+      });
 
-      const handle = await allocDriver.allocate(deviceConfig);
-      expect(handle.constructor.name).toEqual('AndroidEmulatorCookie');
-      expect(AndroidEmulatorCookie).toHaveBeenCalledWith(adbName);
+      it('should inquire the API level', async () => {
+        givenAllocationOfRunningEmulator();
+
+        const cookie = await allocDriver.allocate(deviceConfig);
+        await allocDriver.postAllocate(cookie);
+
+        expect(adb.apiLevel).toHaveBeenCalledWith(adbName);
+      });
     });
   });
 
diff --git a/detox/src/devices/allocation/drivers/android/genycloud/GenyAllocDriver.test.js b/detox/src/devices/allocation/drivers/android/genycloud/GenyAllocDriver.test.js
index 6cbce1dad..de94421a8 100644
--- a/detox/src/devices/allocation/drivers/android/genycloud/GenyAllocDriver.test.js
+++ b/detox/src/devices/allocation/drivers/android/genycloud/GenyAllocDriver.test.js
@@ -10,6 +10,7 @@ describe('Allocation driver for Genymotion SaaS emulators', () => {
   let instanceLauncher;
   let GenyInstance;
   let adb;
+
   beforeEach(() => {
     jest.mock('../../../../common/drivers/android/genycloud/services/GenyInstanceLookupService');
     jest.mock('../../../../common/drivers/android/genycloud/services/GenyInstanceLifecycleService');
@@ -33,8 +34,6 @@ describe('Allocation driver for Genymotion SaaS emulators', () => {
 
   let allocDriver;
   beforeEach(() => {
-    jest.mock('../../../../cookies/GenycloudEmulatorCookie');
-
     const GenyAllocDriver = require('./GenyAllocDriver');
     allocDriver = new GenyAllocDriver({ recipeQuerying, allocationHelper, instanceLauncher, adb });
   });
@@ -69,7 +68,6 @@ describe('Allocation driver for Genymotion SaaS emulators', () => {
   const givenLaunchResult = (instance) => instanceLauncher.launch.mockResolvedValue(instance);
 
   describe('allocation', () => {
-
     it('should obtain recipe from recipes service', async () => {
       givenRecipe(aRecipe());
       givenReallocationResult(anInstance());
@@ -103,85 +101,82 @@ describe('Allocation driver for Genymotion SaaS emulators', () => {
       expect(allocationHelper.allocateDevice).toHaveBeenCalledWith(recipe);
     });
 
-    describe('given an allocation of a fresh cloud instance', () => {
-      it('should launch it', async () => {
-        const expectedIsNew = false;
-        const instance = anInstance();
-        givenRecipe(aRecipe());
-        givenReallocationResult(instance);
+    describe('post-allocation', () => {
+      describe('given a fresh cloud instance', () => {
+        it('should launch it', async () => {
+          const expectedIsNew = true;
+          const instance = anInstance();
+          givenRecipe(aRecipe());
+          givenFreshAllocationResult(instance);
 
-        await allocDriver.allocate(deviceConfig);
+          const cookie = await allocDriver.allocate(deviceConfig);
+          await allocDriver.postAllocate(cookie);
 
-        expect(instanceLauncher.launch).toHaveBeenCalledWith(instance, expectedIsNew);
-      });
+          expect(instanceLauncher.launch).toHaveBeenCalledWith(instance, expectedIsNew);
+        });
 
-      it('should fail if launch fails', async () => {
-        givenRecipe(aRecipe());
-        givenFreshAllocationResult(anInstance());
-        givenLaunchError('alloc error mock');
+        it('should fail if launch fails', async () => {
+          givenRecipe(aRecipe());
+          givenFreshAllocationResult(anInstance());
+          givenLaunchError('alloc error mock');
 
-        await expect(allocDriver.allocate(deviceConfig)).rejects.toThrowError('alloc error mock');
+          const cookie = await allocDriver.allocate(deviceConfig);
+          await expect(allocDriver.postAllocate(cookie)).rejects.toThrowError('alloc error mock');
+        });
       });
 
-      it('should deallocate the instance if launch fails', async () => {
-        const instance = anInstance();
+      describe('given an existing cloud instance (reused)', () => {
+        it('should launch it', async () => {
+          const expectedIsNew = false;
+          const instance = anInstance();
+          givenRecipe(aRecipe());
+          givenReallocationResult(instance);
 
-        givenRecipe(aRecipe());
-        givenFreshAllocationResult(instance);
-        givenLaunchError('alloc error mock');
+          const cookie = await allocDriver.allocate(deviceConfig);
+          await allocDriver.postAllocate(cookie);
 
-        try {
-          await allocDriver.allocate(deviceConfig);
-        } catch (e) {}
-        expect(allocationHelper.deallocateDevice).toHaveBeenCalledWith(instance.uuid);
+          expect(instanceLauncher.launch).toHaveBeenCalledWith(instance, expectedIsNew);
+        });
       });
-    });
 
-    describe('given an allocation of an existing cloud instance (reused)', () => {
-      it('should launch it', async () => {
-        const expectedIsNew = true;
+      it('should return a cookie based on the launched instance and recipe', async () => {
+        const GenycloudEmulatorCookie = require('../../../../cookies/GenycloudEmulatorCookie');
         const instance = anInstance();
+        const launchedInstance = aLaunchedInstance();
         givenRecipe(aRecipe());
-        givenFreshAllocationResult(instance);
+        givenReallocationResult(instance);
+        givenLaunchResult(launchedInstance);
 
-        await allocDriver.allocate(deviceConfig);
+        const result = await allocDriver.allocate(deviceConfig);
+        expect(result).toBeInstanceOf(GenycloudEmulatorCookie);
+        expect(result.instance).toBe(instance);
+        expect(result.adbName).toBeUndefined();
 
-        expect(instanceLauncher.launch).toHaveBeenCalledWith(instance, expectedIsNew);
+        await allocDriver.postAllocate(result);
+        expect(result.adbName).toBe('localhost:1234');
       });
-    });
-
-    it('should return a cookie based on the launched instance and recipe', async () => {
-      const GenycloudEmulatorCookie = require('../../../../cookies/GenycloudEmulatorCookie');
-      const instance = anInstance();
-      const launchedInstance = aLaunchedInstance();
-      givenRecipe(aRecipe());
-      givenReallocationResult(instance);
-      givenLaunchResult(launchedInstance);
 
-      const result = await allocDriver.allocate(deviceConfig);
-      expect(result.constructor.name).toEqual('GenycloudEmulatorCookie');
-      expect(GenycloudEmulatorCookie).toHaveBeenCalledWith(launchedInstance);
-    });
-
-    it('should prepare the emulators itself', async () => {
-      const instance = anInstance();
-      givenRecipe(aRecipe());
-      givenReallocationResult(instance);
-
-      await allocDriver.allocate(deviceConfig);
+      it('should prepare the emulators itself', async () => {
+        const instance = anInstance();
+        givenRecipe(aRecipe());
+        givenReallocationResult(instance);
 
-      expect(adb.disableAndroidAnimations).toHaveBeenCalledWith(instance.adbName);
-      expect(adb.setWiFiToggle).toHaveBeenCalledWith(instance.adbName, true);
-    });
+        const cookie = await allocDriver.allocate(deviceConfig);
+        await allocDriver.postAllocate(cookie);
 
-    it('should inquire the API level', async () => {
-      const instance = anInstance();
-      givenRecipe(aRecipe());
-      givenReallocationResult(instance);
+        expect(adb.disableAndroidAnimations).toHaveBeenCalledWith(instance.adbName);
+        expect(adb.setWiFiToggle).toHaveBeenCalledWith(instance.adbName, true);
+      });
 
-      await allocDriver.allocate(deviceConfig);
+      it('should inquire the API level', async () => {
+        const instance = anInstance();
+        givenRecipe(aRecipe());
+        givenReallocationResult(instance);
 
-      expect(adb.apiLevel).toHaveBeenCalledWith(instance.adbName);
+        const cookie = await allocDriver.allocate(deviceConfig);
+        await allocDriver.postAllocate(cookie);
+        expect(adb.apiLevel).toHaveBeenCalledWith(instance.adbName);
+      });
     });
   });
 
diff --git a/detox/test/e2e/detox.config.js b/detox/test/e2e/detox.config.js
index 3b6ae8162..1c22b8593 100644
--- a/detox/test/e2e/detox.config.js
+++ b/detox/test/e2e/detox.config.js
@@ -200,6 +200,10 @@ const config = {
       device: 'android.emulator',
       apps: ['android.release', 'android.release.withArgs'],
     },
+    'android.genycloud.debug': {
+      device: 'android.genycloud.uuid',
+      apps: ['android.debug'],
+    },
     'android.genycloud.release': {
       device: 'android.genycloud.uuid',
       apps: ['android.release', 'android.release.withArgs'],