Repository: wix/Detox
Pull request #1547 (2019-07-22 22:57:04)

Add Support for Biometrics on SimulatorDriver
**Description:**

This is a small change (which was requested in #958).

It adds support for Biometrics by adding the following methods to DeviceDriver:

- `enrollBiometrics` and `disenrollBiometrics` - for setting up biometrics on a Simulator
- `matchFace` and `unmatchFace` - for matching a face on a Simulator
- `matchFinger` and `unmatchFinger` - for matching a finger on a Simulator

It uses an underlying call in `AppleSimUtils.js` to three new methods:

- `setBiometricEnrollment` - which accepts a UDID and either `"YES"` or `"NO"`
- `matchBiometric` - which accepts a UDID and either `"Face"` or `"Finger"`
- `unmatchBiometric` - which accepts a UDID and either `"Face"` or `"Finger"`

I believe I added the appropriate tests so I think this should be good to go - however, I'm happy to change if the core maintainers are not content with my proposed changes. Please provide some suggestions on how it can be improved. Thank you!

LeoNatan: Let's wait for build to finish and I'll merge. Thank you!
abunsen: @LeoNatan my pleasure. Thank you & Wix for making such an awesome Library.

Patch:
diff --git a/detox/src/devices/Device.js b/detox/src/devices/Device.js
index 1d33561e6..45e932901 100644
--- a/detox/src/devices/Device.js
+++ b/detox/src/devices/Device.js
@@ -135,6 +135,32 @@ class Device {
     await this.deviceDriver.waitForBackground();
   }
 
+  async setBiometricEnrollment(toggle) {
+    let yesOrNo = toggle ? 'YES' : 'NO'
+    await this.deviceDriver.setBiometricEnrollment(this._deviceId, yesOrNo);
+    await this.deviceDriver.waitForBackground();
+  }
+
+  async matchFace() {
+    await this.deviceDriver.matchFace(this._deviceId);
+    await this.deviceDriver.waitForBackground();
+  }
+
+  async unmatchFace() {
+    await this.deviceDriver.unmatchFace(this._deviceId);
+    await this.deviceDriver.waitForBackground();
+  }
+
+  async matchFinger() {
+    await this.deviceDriver.matchFinger(this._deviceId);
+    await this.deviceDriver.waitForBackground();
+  }
+
+  async unmatchFinger() {
+    await this.deviceDriver.unmatchFinger(this._deviceId);
+    await this.deviceDriver.waitForBackground();
+  }
+
   async shake() {
     await this.deviceDriver.shake(this._deviceId);
   }
diff --git a/detox/src/devices/drivers/DeviceDriverBase.js b/detox/src/devices/drivers/DeviceDriverBase.js
index 7bb4477a2..b8a9ae55b 100644
--- a/detox/src/devices/drivers/DeviceDriverBase.js
+++ b/detox/src/devices/drivers/DeviceDriverBase.js
@@ -63,6 +63,26 @@ class DeviceDriverBase {
     return await Promise.resolve('');
   }
 
+  async setBiometricEnrollment() {
+    return await Promise.resolve('');
+  }
+
+  async matchFace() {
+    return await Promise.resolve('');
+  }
+
+  async unmatchFace() {
+    return await Promise.resolve('');
+  }
+
+  async matchFinger() {
+    return await Promise.resolve('');
+  }
+  
+  async unmatchFinger() {
+    return await Promise.resolve('');
+  }
+
   async shake() {
     return await Promise.resolve('');
   }
diff --git a/detox/src/devices/drivers/SimulatorDriver.js b/detox/src/devices/drivers/SimulatorDriver.js
index 6e34f6847..af254d6f8 100644
--- a/detox/src/devices/drivers/SimulatorDriver.js
+++ b/detox/src/devices/drivers/SimulatorDriver.js
@@ -82,6 +82,26 @@ class SimulatorDriver extends IosDriver {
     await this.applesimutils.terminate(deviceId, bundleId);
   }
 
+  async setBiometricEnrollment(deviceId, yesOrNo) {
+    await this.applesimutils.setBiometricEnrollment(deviceId, yesOrNo);
+  }
+
+  async matchFace(deviceId) {
+    await this.applesimutils.matchBiometric(deviceId, 'Face');
+  }
+
+  async unmatchFace(deviceId) {
+    await this.applesimutils.unmatchBiometric(deviceId, 'Face');
+  }
+
+  async matchFinger(deviceId) {
+    await this.applesimutils.matchBiometric(deviceId, 'Finger');
+  }
+
+  async unmatchFinger(deviceId) {
+    await this.applesimutils.unmatchBiometric(deviceId, 'Finger');
+  }
+
   async sendToHome(deviceId) {
     await this.applesimutils.sendToHome(deviceId);
   }
diff --git a/detox/src/devices/ios/AppleSimUtils.js b/detox/src/devices/ios/AppleSimUtils.js
index bbd757f7d..f9476efd3 100644
--- a/detox/src/devices/ios/AppleSimUtils.js
+++ b/detox/src/devices/ios/AppleSimUtils.js
@@ -137,6 +137,42 @@ class AppleSimUtils {
     await this._execSimctl({ cmd: `launch ${udid} com.apple.springboard`, retries: 10 });
   }
 
+  async matchBiometric(udid, matchType) {
+    if (!['Face', 'Finger'].include(matchType)) {
+      return;
+    }
+    const statusLogs = {
+      trying: `Trying to match ${matchType}...`,
+      successful: `Matched ${matchType}!`
+    };
+
+    await this._execAppleSimUtils({ args: `--byId ${udid} --${matchType}}` }, statusLogs, 1);
+  }
+
+  async unmatchBiometric(udid, matchType) {
+    if (!['Face', 'Finger'].include(matchType)) {
+      return;
+    }
+    const statusLogs = {
+      trying: `Trying to unmatch ${matchType}...`,
+      successful: `Unmatched ${matchType}!`
+    };
+
+    await this._execAppleSimUtils({ args: `--byId ${udid} --unmatch${matchType}}` }, statusLogs, 1);
+  }
+
+  async setBiometricEnrollment(udid, yesOrNo) {
+    if (!['YES', 'NO'].include(yesOrNo)) {
+      return;
+    }
+    let toggle = yesOrNo === 'YES'
+    const statusLogs = {
+      trying: `Turning ${toggle ? 'on' : 'off'} biometric enrollment...`,
+      successful: toggle ? 'Activated!' : 'Deactivated!'
+    };
+    await this._execAppleSimUtils({ args: `--byId ${udid} --biometricEnrollment ${yesOrNo}` }, statusLogs, 1);
+  }
+
   async getAppContainer(udid, bundleId) {
     return _.trim((await this._execSimctl({ cmd: `get_app_container ${udid} ${bundleId}` })).stdout);
   }
diff --git a/docs/APIRef.DeviceObjectAPI.md b/docs/APIRef.DeviceObjectAPI.md
index a3c15527b..d7bc46449 100644
--- a/docs/APIRef.DeviceObjectAPI.md
+++ b/docs/APIRef.DeviceObjectAPI.md
@@ -23,6 +23,11 @@
 - [`device.getPlatform()`](#devicegetplatform)
 - [`device.takeScreenshot(name)`](#devicetakescreenshotname)
 - [`device.shake()` **iOS Only**](#deviceshake-ios-only)
+- [`device.setBiometricEnrollment(bool)` **iOS Only**](#devicesetbiometricenrolmmentbool-ios-only)
+- [`device.matchFace()` **iOS Only**](#devicematchface-ios-only)
+- [`device.unmatchFace()` **iOS Only**](#deviceunmatchface-ios-only)
+- [`device.matchFinger()` **iOS Only**](#devicematchfinger-ios-only)
+- [`device.unmatchFinger()` **iOS Only**](#deviceunmatchfinger-ios-only)
 - [`device.pressBack()` **Android Only**](#devicepressback-android-only)
 - [`device.getUIDevice()` **Android Only**](#devicegetuidevice-android-only)
 
@@ -341,6 +346,27 @@ describe('Menu items', () => {
 ### `device.shake()` **iOS Only**
 Simulate shake
 
+### `device.setBiometricEnrollment(bool)` **iOS Only**
+Toggles device enrollment in biometric auth (TouchID or FaceID).
+
+```js
+await device.setBiometricEnrollment(true);
+// or
+await device.setBiometricEnrollment(false);
+```
+
+### `device.matchFace()` **iOS Only**
+Simulates the success of a face match via FaceID
+
+### `device.unmatchFace()` **iOS Only**
+Simulates the failure of face match via FaceID
+
+### `device.matchFinger()` **iOS Only**
+Simulates the success of a finger match via TouchID
+
+### `device.unmatchFinger()` **iOS Only**
+Simulates the failure of a finger match via TouchID
+
 ### `device.pressBack()` **Android Only**
 Simulate press back button.
 


Test patch:
diff --git a/detox/src/devices/Device.test.js b/detox/src/devices/Device.test.js
index d8a867af5..08204e001 100644
--- a/detox/src/devices/Device.test.js
+++ b/detox/src/devices/Device.test.js
@@ -476,6 +476,50 @@ describe('Device', () => {
     expect(driverMock.driver.sendToHome).toHaveBeenCalledTimes(1);
   });
 
+  it(`setBiometricEnrollment(true) should pass YES to device driver`, async () => {
+    const device = validDevice();
+    await device.setBiometricEnrollment(true);
+
+    expect(driverMock.driver.setBiometricEnrollment).toHaveBeenCalledWith(device._deviceId, 'YES');
+    expect(driverMock.driver.setBiometricEnrollment).toHaveBeenCalledTimes(1);
+  });
+
+  it(`setBiometricEnrollment(false) should pass NO to device driver`, async () => {
+    const device = validDevice();
+    await device.setBiometricEnrollment(false);
+
+    expect(driverMock.driver.setBiometricEnrollment).toHaveBeenCalledWith(device._deviceId, 'NO');
+    expect(driverMock.driver.setBiometricEnrollment).toHaveBeenCalledTimes(1);
+  });
+
+  it(`matchFace() should pass to device driver`, async () => {
+    const device = validDevice();
+    await device.matchFace();
+
+    expect(driverMock.driver.matchFace).toHaveBeenCalledTimes(1);
+  });
+
+  it(`unmatchFace() should pass to device driver`, async () => {
+    const device = validDevice();
+    await device.unmatchFace();
+
+    expect(driverMock.driver.unmatchFace).toHaveBeenCalledTimes(1);
+  });
+
+  it(`matchFinger() should pass to device driver`, async () => {
+    const device = validDevice();
+    await device.matchFinger();
+
+    expect(driverMock.driver.matchFinger).toHaveBeenCalledTimes(1);
+  });
+
+  it(`unmatchFinger() should pass to device driver`, async () => {
+    const device = validDevice();
+    await device.unmatchFinger();
+
+    expect(driverMock.driver.unmatchFinger).toHaveBeenCalledTimes(1);
+  });
+
   it(`shake() should pass to device driver`, async () => {
     const device = validDevice();
     await device.shake();
diff --git a/detox/src/devices/drivers/SimulatorDriver.test.js b/detox/src/devices/drivers/SimulatorDriver.test.js
index 20958a751..ccd83ccd9 100644
--- a/detox/src/devices/drivers/SimulatorDriver.test.js
+++ b/detox/src/devices/drivers/SimulatorDriver.test.js
@@ -1,5 +1,5 @@
 describe('IOS simulator driver', () => {
-  let uut;
+  let uut, sim;
 
   const deviceId = 'device-id-mock';
   const bundleId = 'bundle-id-mock';
@@ -40,10 +40,52 @@ describe('IOS simulator driver', () => {
       }, '');
     });
   });
+
+  describe('biometrics', () => {
+    beforeEach(() => {
+      languageAndLocale = '';
+
+      const SimulatorDriver = require('./SimulatorDriver');
+      sim = new SimulatorDriver({ client: {} });
+    });
+
+    it('enrolls in biometrics by passing to AppleSimUtils', async () => {
+      await sim.setBiometricEnrollment(deviceId, 'YES');
+      expect(sim.applesimutils.setBiometricEnrollment).toHaveBeenCalledWith(deviceId, 'YES');
+    })
+
+    it('disenrolls in biometrics by passing to AppleSimUtils', async () => {
+      await sim.setBiometricEnrollment(deviceId, 'NO');
+      expect(sim.applesimutils.setBiometricEnrollment).toHaveBeenCalledWith(deviceId, 'NO');
+    })
+
+    it('matches a face by passing to AppleSimUtils', async () => {
+      await sim.matchFace(deviceId);
+      expect(sim.applesimutils.matchBiometric).toHaveBeenCalledWith(deviceId, 'Face');
+    })
+
+    it('fails to match a face by passing to AppleSimUtils', async () => {
+      await sim.unmatchFace(deviceId);
+      expect(sim.applesimutils.unmatchBiometric).toHaveBeenCalledWith(deviceId, 'Face');
+    })
+
+    it('matches a face by passing to AppleSimUtils', async () => {
+      await sim.matchFinger(deviceId);
+      expect(sim.applesimutils.matchBiometric).toHaveBeenCalledWith(deviceId, 'Finger');
+    })
+
+    it('fails to match a face by passing to AppleSimUtils', async () => {
+      await sim.unmatchFinger(deviceId);
+      expect(sim.applesimutils.unmatchBiometric).toHaveBeenCalledWith(deviceId, 'Finger');
+    })
+  });
 });
 
 class mockAppleSimUtils {
   constructor() {
     this.launch = jest.fn();
+    this.setBiometricEnrollment = jest.fn();
+    this.matchBiometric = jest.fn();
+    this.unmatchBiometric = jest.fn();
   }
 }