Repository: wix/Detox
Pull request #2688 (2021-03-14 14:51:33)

Integrate genymotion SaaS into Detox' CI
## Description

Integrate Genymotion cloud into Detox' CI.

Following this change, instead of running the full demo-app test suite over 3 local emulators, we will:
- Execute the entire suite using 5 (not 3) cloud instances.
- Execute ~5 suites using 3 local (Google) emulators.

Everything else stays the same.


Patch:
diff --git a/detox/src/devices/drivers/android/AndroidDriver.js b/detox/src/devices/drivers/android/AndroidDriver.js
index 5fa52e73b..bcf36b849 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.js
@@ -161,7 +161,8 @@ class AndroidDriver extends DeviceDriverBase {
   }
 
   async typeText(deviceId, text) {
-    await this.adb.typeText(deviceId, text);
+    const adbName = this._getAdbName(deviceId);
+    await this.adb.typeText(adbName, text);
   }
 
   async terminate(deviceId, bundleId) {
diff --git a/detox/src/utils/exec.js b/detox/src/utils/exec.js
index 964625551..8159b38a4 100644
--- a/detox/src/utils/exec.js
+++ b/detox/src/utils/exec.js
@@ -4,7 +4,6 @@ const execLogger = require('./logger').child({ __filename });
 const retry = require('./retry');
 const { escape } = require('./pipeCommands');
 const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
-const { traceCall } = require('./trace');
 
 let _operationCounter = 0;
 
@@ -33,12 +32,12 @@ async function execWithRetriesAndLogs(bin, options = {}) {
       } else if (statusLogs.retrying) {
         _logRetrying(log, cmd, retryNumber, lastError);
       }
-      result = await traceCall(`cmd: ${cmd}`, () => exec(cmd, { timeout }));
+      result = await exec(cmd, { timeout });
     });
   } catch (err) {
     const _failReason = err.code == null && timeout > 0
       ? `timeout = ${timeout}ms`
-      : `code = ${err.code}`;
+      : `error = ${err} (code=${err.code})`;
 
     const level = (verbosity === 'low' ? 'debug' : 'error');
 
diff --git a/scripts/ci.android.sh b/scripts/ci.android.sh
index a0f8000ac..11208f9ba 100755
--- a/scripts/ci.android.sh
+++ b/scripts/ci.android.sh
@@ -10,11 +10,13 @@ run_f "npm run integration"
 popd
 
 pushd detox/android
-run_f "./gradlew test"
+run_f "./gradlew testFullRelease"
 popd
 
 mkdir -p coverage
 
+run_f "scripts/ci.genycloud-login.sh"
+
 pushd detox/test
 
 # Workaround until react android issue will be fixed - react-native: 0.55
@@ -24,9 +26,11 @@ cp extras/release.gradle node_modules/react-native/ReactAndroid/
 run_f "npm run build:android"
 cp ../coverage/lcov.info ../../coverage/unit.lcov
 
-run_f "npm run e2e:android-ci"
+run_f "npm run e2e:android-ci-genycloud"
 cp coverage/lcov.info ../../coverage/e2e-android-ci.lcov
 
+run_f "npm run e2e:android-ci-google -- e2e/01* e2e/02* e2e/03*"
+
 run_f "scripts/ci_unhappy.sh android"
 
 # run_f "npm run verify-artifacts:android"
diff --git a/scripts/ci.genycloud-login.sh b/scripts/ci.genycloud-login.sh
new file mode 100755
index 000000000..a4ffe82a4
--- /dev/null
+++ b/scripts/ci.genycloud-login.sh
@@ -0,0 +1,5 @@
+#!/bin/bash -ex
+
+which gmsaas
+gmsaas --version
+gmsaas auth login "$GENYCLOUD_USERNAME" "$GENYCLOUD_PASSWORD"


Test patch:
diff --git a/detox/src/devices/drivers/android/AndroidDriver.test.js b/detox/src/devices/drivers/android/AndroidDriver.test.js
index ac562aaf6..f8d96b593 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.test.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.test.js
@@ -4,6 +4,8 @@ const latestInstanceOf = (clazz) => _.last(clazz.mock.instances);
 
 describe('Android driver', () => {
   const deviceId = 'device-id-mock';
+  const adbName = 'device-adb-name';
+  const deviceHandle = { adbName };
   const bundleId = 'bundle-id-mock';
   const detoxServerPort = 1234;
   const mockNotificationDataTargetPath = '/ondevice/path/to/notification.json';
@@ -452,7 +454,6 @@ describe('Android driver', () => {
   });
 
   describe('net-port reversing', () => {
-    const deviceId = 1010;
     const port = 1337;
 
     it(`should invoke ADB's reverse`, async () => {
@@ -460,10 +461,34 @@ describe('Android driver', () => {
       expect(adbObj().reverse).toHaveBeenCalledWith(deviceId, port);
     });
 
+    it(`should invoke ADB's reverse, given a device handle`, async () => {
+      await uut.reverseTcpPort(deviceHandle, port);
+      expect(adbObj().reverse).toHaveBeenCalledWith(adbName, port);
+    });
+
     it(`should invoke ADB's reverse-remove`, async () => {
       await uut.unreverseTcpPort(deviceId, port);
       expect(adbObj().reverseRemove).toHaveBeenCalledWith(deviceId, port);
     });
+
+    it(`should invoke ADB's reverse-remove, given a device handle`, async () => {
+      await uut.unreverseTcpPort(deviceHandle, port);
+      expect(adbObj().reverseRemove).toHaveBeenCalledWith(adbName, port);
+    });
+  });
+
+  describe('text-typing (global)', () => {
+    const text = 'text to type';
+
+    it(`should invoke ADB's text typing`, async () => {
+      await uut.typeText(deviceId, text);
+      expect(adbObj().typeText).toHaveBeenCalledWith(deviceId, text);
+    });
+
+    it(`should invoke ADB's text typing, given a device handle`, async () => {
+      await uut.typeText(deviceHandle, text);
+      expect(adbObj().typeText).toHaveBeenCalledWith(adbName, text);
+    });
   });
 
   const setUpModuleDepMocks = () => {
diff --git a/detox/src/devices/drivers/android/exec/ADB.test.js b/detox/src/devices/drivers/android/exec/ADB.test.js
index 2f0d31436..98fa32e89 100644
--- a/detox/src/devices/drivers/android/exec/ADB.test.js
+++ b/detox/src/devices/drivers/android/exec/ADB.test.js
@@ -1,4 +1,5 @@
 describe('ADB', () => {
+  const deviceId = 'mockEmulator';
   const adbBinPath = `/Android/sdk-mock/platform-tools/adb`;
 
   let ADB;
@@ -113,7 +114,6 @@ describe('ADB', () => {
   });
 
   it('push', async () => {
-    const deviceId = 'mockEmulator';
     const sourceFile = '/mock-source/file.xyz';
     const destFile = '/sdcard/file.abc';
     await adb.push(deviceId, sourceFile, destFile);
@@ -124,7 +124,6 @@ describe('ADB', () => {
   });
 
   it('remote-install', async () => {
-    const deviceId = 'mockEmulator';
     const binaryPath = '/mock-path/filename.mock';
     await adb.remoteInstall(deviceId, binaryPath);
 
@@ -133,9 +132,16 @@ describe('ADB', () => {
       expect.anything());
   });
 
-  describe('unlockScreen', () => {
-    const deviceId = 'mockEmulator';
+  it('global text-typing', async () => {
+    const text = 'some-text-with spaces';
+    const expectedText = 'some-text-with%sspaces';
+    await adb.typeText(deviceId, text);
+    expect(exec).toHaveBeenCalledWith(
+      expect.stringContaining(`-s mockEmulator shell "input text ${expectedText}"`),
+      expect.anything());
+  });
 
+  describe('unlockScreen', () => {
     async function unlockScreenWithPowerStatus(mWakefulness, mUserActivityTimeoutOverrideFromWindowManager) {
       jest.spyOn(adb, 'shell').mockImplementation(async () => `
         mWakefulness=${mWakefulness}
@@ -193,7 +199,6 @@ describe('ADB', () => {
   });
 
   describe('spawnInstrumentation', () => {
-    const deviceId = 'aDeviceId';
     const testRunner = 'aTestRunner';
 
     it('should spawn instrumentation', async () => {
diff --git a/detox/test/e2e/28.webview.test.js b/detox/test/e2e/29.webview.test.js
similarity index 100%
rename from detox/test/e2e/28.webview.test.js
rename to detox/test/e2e/29.webview.test.js
diff --git a/detox/test/package.json b/detox/test/package.json
index 489cd7e02..d5cb19621 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -12,10 +12,11 @@
     "packager": "react-native start",
     "detox-server": "detox run-server",
     "e2e:ios": "detox test -c ios.sim.release",
+    "e2e:ios-ci": "npm run e2e:ios -- --workers 3 --retries 1",
     "e2e:android": "detox test -c android.emu.release",
+    "e2e:android-ci-genycloud": "detox test -c android.genycloud.release --workers 5 --retries 1 --jest-report-specs --loglevel verbose",
+    "e2e:android-ci-google": "detox test -c android.emu.release --workers 3 --retries 1 --jest-report-specs --loglevel verbose --headless",
     "e2e:android-debug": "detox test -c android.emu.debug",
-    "e2e:ios-ci": "npm run e2e:ios -- --workers 3 --retries 1",
-    "e2e:android-ci": "npm run e2e:android -- --workers 3 --retries 1 --headless --jest-report-specs --loglevel verbose",
     "integration": "jest --clearCache && jest unit --config integration/config.js -w 1 .",
     "integration-update-snapshots": "npm run integration -- -u",
     "build:ios": "detox build -c ios.sim.release",