Repository: wix/Detox (js)

Issue:
[feature] Element Screenshot
**Is your feature request related to a problem? Please describe.**
Recently we used screenshot tests with detox to detect breaking UI changes in our components. 
In order to do that we compared screenshots of the entire screen that showcase the component. 
Unfortunately, our showcase screens contain other elements other than the tested component (titles, controls, etc..). 
If something has changed with those elements the screenshot test failed. 
It turned those tests to be very flaky so we turned them off for now. 


**Describe the solution you'd like**
A great solution will be to capture a screenshot of a specific element (by testID), this will help isolate to test case and prevent flakiness. 

**Describe alternatives you've considered**
none. 

**Additional context**
I discussed this some time ago with @amitdwix and @rotemmiz, I understand this is applicable. 
Would be very beneficial for UI testing. 


Pull request:
Bump top-level matchers api to upper Detox layer (where they should be)
- [ ] This is a small change 
- [ ] This change has been discussed in issue #<?> and the solution has been agreed upon with maintainers.

---

## Description

Relates to #2012: lays some preliminary ground work so that we could introduce it with proper API.

### Dive-in

Looking at the current software architecture of Detox at a high-level perspective, we see two main **distinct** units, top-to-bottom:
1. A framework for providing `expect` and element-interactions as a top-level, user-facing API (e.g. `element(by.id(testId)).tap()`), which in turn uses a distinct, platform-specific node-side (JS) implementation that eventually directly triggers device-side logic using `invoke`.
2. A framework for providing element-less, device-scope, top-level, user-facing APIs (e.g. `device.takeScreenshot()`), which are provided mainly by the (currently) singleton `Device` object, that in turns delegates platform-specific logic to Drivers.

While the two global _units_ are, in principle, agnostic, we've nevertheless created an artificial coupling between them, as the top-level implementation of unit 1 is created via the drivers of unit 2, due to convenience ([example](https://github.com/wix/Detox/blob/78bc945bc5dbf15c116cceb790c8ae5c9aac9af4/detox/src/devices/drivers/android/AndroidDriver.js#L36)). Unfortunately, this type of an unnecessary coupling - as a rule of thumb, damages flexibility, and we can see it in the case of elements' screenshot.

The pseudo-code implementation of taking an element screenshot is as follows:
a. `Invoke` an on-test-device, native method that creates an on-device `.png` file.
b. Pull the image back to the host through the driver (e.g. using `adb`) and provide the path back to the caller.

Ideally, this top-level API for doing this would be amazing:
```js
const screenshotFilePath = await element(by.id('fancy-element-id')).takeScreenshot()`
```

But due to the coupling mentioned earlier, this cannot be done, as the underlying `Element.takeScreenshot()` function that would implement this -- originally generated by driver, cannot access the `device` in _part b_ of the pseudo-code (!) It more so artificially forces us onto this kind of an API, which is largely undesired as it is both not friendly and would encourage code duplication and single-responsibility breakage in Detox itself:

```js
const screenshotFilePath = await device.takeScreenshot( element(by.id('fancy-element-id')) );
```

If we want to nevertheless stick to the API that we really want - and with that, empower high flexibility in Detox (yes, gimme all those things!), we need to break the coupling.

**This is the scope of this PR:** 100% Separating drivers and matchers, and their creation. Instead of having the matchers created by the drivers, we delegate the proper matcher resolution to a new util called `matchersRegistry` (equilvant to `driverRegistry`), which is utilized in the Detox class (aka our Detox _main_ layer).

Unfortunately, the main drawback is a minor breakage of external-drivers implementation: Whichever plugin-driver implementations that exist out there in the wild -- they _still provide the matchers through the driver._ Fortunately, I've made the migration pretty easy. Given a plugin `driver.js`, users will have to apply these minor changes:
```diff
class Expect {
-  constructor(invocationManager) {
+  constructor({ invocationManager }) {
     this._invocationManager = invocationManager;
  }
}

class PluginDriver {
  constructor() {
-    this.matchers = new Expect(new invocationManager());
+    // no this.matchers init here...
  }
}

-module.exports = PluginDriver;
+module.exports = {
+  DriverClass: PluginDriver,
+  ExpectClass: Expect,
+}
```

Patch:
diff --git a/detox/src/Detox.js b/detox/src/Detox.js
index 350cf5af8..ffcd8bf91 100644
--- a/detox/src/Detox.js
+++ b/detox/src/Detox.js
@@ -8,11 +8,12 @@ const DetoxRuntimeError = require('./errors/DetoxRuntimeError');
 const AsyncEmitter = require('./utils/AsyncEmitter');
 const MissingDetox = require('./utils/MissingDetox');
 const Client = require('./client/Client');
+const { InvocationManager } = require('./invoke');
 const DetoxServer = require('./server/DetoxServer');
 const ArtifactsManager = require('./artifacts/ArtifactsManager');
-
 const log = logger.child({ __filename });
 const driverRegistry = require('./devices/DriverRegistry').default;
+const matchersRegistry = require('./matchersRegistry');
 
 const _initHandle = Symbol('_initHandle');
 const _assertNoPendingInit = Symbol('_assertNoPendingInit');
@@ -144,13 +145,13 @@ class Detox {
     this._client.setNonresponsivenessListener(this._onNonresnponsivenessEvent.bind(this));
     await this._client.connect();
 
+    const invocationManager = new InvocationManager(this._client);
     const deviceDriver = driverRegistry.resolve(this._deviceConfig.type, {
       client: this._client,
+      invocationManager,
       emitter: this._eventEmitter,
     });
 
-    Object.assign(this, deviceDriver.matchers);
-
     this.device = new Device({
       deviceConfig: this._deviceConfig,
       emitter: this._eventEmitter,
@@ -158,19 +159,24 @@ class Detox {
       sessionConfig,
     });
 
-    if (behaviorConfig.exposeGlobals) {
-      Object.assign(Detox.global, {
-        ...deviceDriver.matchers,
-        device: this.device,
-      });
-    }
-
     this._artifactsManager = new ArtifactsManager(this._artifactsConfig);
     this._artifactsManager.subscribeToDeviceEvents(this._eventEmitter);
     this._artifactsManager.registerArtifactPlugins(deviceDriver.declareArtifactPlugins());
 
     await this.device.prepare();
 
+    const matchers = matchersRegistry.resolve(this.device, {
+      invocationManager,
+    });
+    Object.assign(this, matchers);
+
+    if (behaviorConfig.exposeGlobals) {
+      Object.assign(Detox.global, {
+        ...matchers,
+        device: this.device,
+      });
+    }
+
     if (behaviorConfig.reinstallApp) {
       await this.device.uninstallApp();
       await this.device.installApp();
diff --git a/detox/src/android/expect.js b/detox/src/android/expect.js
index 007ad44b6..c5b7820da 100644
--- a/detox/src/android/expect.js
+++ b/detox/src/android/expect.js
@@ -403,7 +403,7 @@ class WaitForElement extends WaitFor {
 }
 
 class AndroidExpect {
-  constructor(invocationManager) {
+  constructor({ invocationManager }) {
     this._invocationManager = invocationManager;
 
     this.by = {
diff --git a/detox/src/devices/Device.js b/detox/src/devices/Device.js
index 35928a3f3..48450e8d9 100644
--- a/detox/src/devices/Device.js
+++ b/detox/src/devices/Device.js
@@ -89,6 +89,10 @@ class Device {
     return this.deviceDriver.name;
   }
 
+  get type() {
+    return this._deviceConfig.type;
+  }
+
   async takeScreenshot(name) {
     if (!name) {
       throw new Error('Cannot take a screenshot with an empty name.');
diff --git a/detox/src/devices/DriverRegistry.js b/detox/src/devices/DriverRegistry.js
index 756d91761..9e63691ab 100644
--- a/detox/src/devices/DriverRegistry.js
+++ b/detox/src/devices/DriverRegistry.js
@@ -10,7 +10,7 @@ class DriverRegistry {
 
     if (!DeviceDriverClass) {
       try {
-        DeviceDriverClass = resolveModuleFromPath(deviceType);
+        DeviceDriverClass = resolveModuleFromPath(deviceType).DriverClass;
       } catch (e) {
         // noop, if we don't find a module to require, we'll hit the unsupported error below
       }
@@ -31,4 +31,4 @@ DriverRegistry.default = new DriverRegistry({
   'android.attached': require('./drivers/android/AttachedAndroidDriver'),
 });
 
-module.exports = DriverRegistry;
\ No newline at end of file
+module.exports = DriverRegistry;
diff --git a/detox/src/devices/drivers/DeviceDriverBase.js b/detox/src/devices/drivers/DeviceDriverBase.js
index 47d56d03e..d1215d8a7 100644
--- a/detox/src/devices/drivers/DeviceDriverBase.js
+++ b/detox/src/devices/drivers/DeviceDriverBase.js
@@ -8,7 +8,6 @@ class DeviceDriverBase {
   constructor({ client, emitter }) {
     this.client = client;
     this.emitter = emitter;
-    this.matchers = null;
   }
 
   get name() {
diff --git a/detox/src/devices/drivers/android/AndroidDriver.js b/detox/src/devices/drivers/android/AndroidDriver.js
index 628005506..8cbcdad65 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.js
@@ -4,8 +4,6 @@ const _ = require('lodash');
 const DeviceDriverBase = require('../DeviceDriverBase');
 const logger = require('../../../utils/logger');
 const log = logger.child({ __filename });
-const invoke = require('../../../invoke');
-const InvocationManager = invoke.InvocationManager;
 const ADB = require('./exec/ADB');
 const AAPT = require('./exec/AAPT');
 const APKPath = require('./tools/APKPath');
@@ -26,14 +24,12 @@ const temporaryPath = require('../../../artifacts/utils/temporaryPath');
 const sleep = require('../../../utils/sleep');
 const retry = require('../../../utils/retry');
 const getAbsoluteBinaryPath = require('../../../utils/getAbsoluteBinaryPath');
-const AndroidExpect = require('../../../android/expect');
 
 class AndroidDriver extends DeviceDriverBase {
   constructor(config) {
     super(config);
 
-    this.invocationManager = new InvocationManager(this.client);
-    this.matchers = new AndroidExpect(this.invocationManager);
+    this.invocationManager = config.invocationManager;
     this.uiDevice = new UiDeviceProxy(this.invocationManager).getUIDevice();
 
     this.adb = new ADB();
diff --git a/detox/src/devices/drivers/ios/IosDriver.js b/detox/src/devices/drivers/ios/IosDriver.js
index b46031b66..e359be5ac 100644
--- a/detox/src/devices/drivers/ios/IosDriver.js
+++ b/detox/src/devices/drivers/ios/IosDriver.js
@@ -1,8 +1,6 @@
 const path = require('path');
 const fs = require('fs');
-const log = require('../../../utils/logger').child({ __filename });
 const DeviceDriverBase = require('../DeviceDriverBase');
-const InvocationManager = require('../../../invoke').InvocationManager;
 const AppleSimUtils = require('./tools/AppleSimUtils');
 
 const SimulatorLogPlugin = require('../../../artifacts/log/ios/SimulatorLogPlugin');
@@ -10,16 +8,12 @@ const SimulatorScreenshotPlugin = require('../../../artifacts/screenshot/Simulat
 const SimulatorRecordVideoPlugin = require('../../../artifacts/video/SimulatorRecordVideoPlugin');
 const SimulatorInstrumentsPlugin = require('../../../artifacts/instruments/ios/SimulatorInstrumentsPlugin');
 const TimelineArtifactPlugin = require('../../../artifacts/timeline/TimelineArtifactPlugin');
-const IosExpectTwo = require('../../../ios/expectTwo');
-
 
 class IosDriver extends DeviceDriverBase {
   constructor(config) {
     super(config);
 
     this.applesimutils = new AppleSimUtils();
-    this.matchers = new IosExpectTwo(new InvocationManager(this.client));
-    // this.matchers = new IosExpect(new InvocationManager(this.client));
   }
 
   declareArtifactPlugins() {
diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js
index a9b6aac4a..f308ec1d5 100644
--- a/detox/src/ios/expectTwo.js
+++ b/detox/src/ios/expectTwo.js
@@ -540,7 +540,7 @@ function waitFor(element) {
 let _invocationManager;
 
 class IosExpect {
-  constructor(invocationManager) {
+  constructor({ invocationManager }) {
     _invocationManager = invocationManager;
     this.element = this.element.bind(this);
     this.expect = this.expect.bind(this);
diff --git a/detox/src/matchersRegistry.js b/detox/src/matchersRegistry.js
new file mode 100644
index 000000000..ab9877b14
--- /dev/null
+++ b/detox/src/matchersRegistry.js
@@ -0,0 +1,15 @@
+const resolveModuleFromPath = require('./utils/resolveModuleFromPath');
+
+function resolve(device, opts) {
+  let MatcherClass;
+  switch (device.getPlatform()) {
+    case 'android': MatcherClass = require('./android/expect'); break;
+    case 'ios': MatcherClass = require('./ios/expectTwo'); break;
+    default: MatcherClass = resolveModuleFromPath(device.type).ExpectClass; break;
+  }
+  return new MatcherClass(opts);
+}
+
+module.exports = {
+  resolve,
+};
diff --git a/examples/demo-plugin/driver.js b/examples/demo-plugin/driver.js
index 7964bef3b..16cc90747 100644
--- a/examples/demo-plugin/driver.js
+++ b/examples/demo-plugin/driver.js
@@ -2,7 +2,7 @@ const DeviceDriverBase = require('detox/src/devices/drivers/DeviceDriverBase');
 const Client = require('detox/src/client/Client');
 
 class Expect {
-  constructor(invocationManager) {
+  constructor({ invocationManager }) {
     this._invocationManager = invocationManager;
 
     this.by = {
@@ -106,9 +106,8 @@ class PluginTestee {
 class PluginDriver extends DeviceDriverBase {
   constructor(config) {
     super(config);
-    this.matchers = {};
+
     this.testee = new PluginTestee(config);
-    this.matchers = new Expect();
   }
 
   async launchApp(deviceId, bundleId, launchArgs, languageAndLocale) {
@@ -146,4 +145,7 @@ class PluginDriver extends DeviceDriverBase {
   }
 }
 
-module.exports = PluginDriver;
+module.exports = {
+  DriverClass: PluginDriver,
+  ExpectClass: Expect,
+};


Test patch:
diff --git a/detox/src/Detox.test.js b/detox/src/Detox.test.js
index 85735e16a..4b3bd4b95 100644
--- a/detox/src/Detox.test.js
+++ b/detox/src/Detox.test.js
@@ -7,6 +7,8 @@ jest.mock('./devices/DriverRegistry');
 jest.mock('./artifacts/ArtifactsManager');
 jest.mock('./client/Client');
 jest.mock('./devices/Device');
+jest.mock('./matchersRegistry');
+jest.mock('./invoke');
 
 jest.mock('./server/DetoxServer', () => {
   const FakeServer = jest.genMockFromModule('./server/DetoxServer');
@@ -21,7 +23,9 @@ describe('Detox', () => {
   let Device;
   let Client;
   let DetoxServer;
+  let matchersRegistry;
   let ArtifactsManager;
+  let invoke;
   let Detox;
   let detox;
   let lifecycleSymbols;
@@ -55,26 +59,29 @@ describe('Detox', () => {
     Device = require('./devices/Device');
     FakeDriverRegistry = require('./devices/DriverRegistry');
     ArtifactsManager = require('./artifacts/ArtifactsManager');
+    invoke = require('./invoke');
     Client = require('./client/Client');
     DetoxServer = require('./server/DetoxServer');
+    matchersRegistry = require('./matchersRegistry');
     Detox = require('./Detox');
     lifecycleSymbols = require('../runners/integration').lifecycle;
   });
 
   describe('when detox.init() is called', () => {
-    let globalMatcher;
+    let mockGlobalMatcher;
 
     const init = async () => {
       detox = await new Detox(detoxConfig).init();
     };
 
     beforeEach(() => {
-      FakeDriverRegistry.FakeDriver.matchers.globalMatcher = globalMatcher = jest.fn();
+      mockGlobalMatcher = jest.fn();
+      matchersRegistry.resolve.mockReturnValue({
+        globalMatcher: mockGlobalMatcher,
+      });
     });
 
     afterEach(() => {
-      delete FakeDriverRegistry.FakeDriver.matchers.globalMatcher;
-
       // cleanup spilled globals after detox.init()
       delete global.device;
       delete global.globalMatcher;
@@ -94,6 +101,9 @@ describe('Detox', () => {
           sessionId: expect.any(String),
         })));
 
+      it('should create an invocation manager', () =>
+        expect(invoke.InvocationManager).toHaveBeenCalledWith(client()));
+
       it('should add a non-responsiveness listener to client ', () =>
         expect(client().setNonresponsivenessListener).toHaveBeenCalledWith(
           expect.any(Function)
@@ -106,13 +116,19 @@ describe('Detox', () => {
         expect(FakeDriverRegistry.default.resolve).toHaveBeenCalledWith(
           detoxConfig.deviceConfig.type,
           {
-            client: expect.anything(),
+            client: client(),
+            invocationManager: invoke.InvocationManager.mock.instances[0],
             emitter: expect.anything(),
           }
         ));
 
-      it('should take device driver matchers to Detox', () =>
-        expect(detox.globalMatcher).toBe(globalMatcher));
+      it('should resolve matchers implementation', () =>
+        expect(matchersRegistry.resolve).toHaveBeenCalledWith(device(), {
+          invocationManager: invoke.InvocationManager.mock.instances[0],
+        }));
+
+      it('should take the matchers from the matchers-registry to Detox', () =>
+        expect(detox.globalMatcher).toBe(mockGlobalMatcher));
 
       it('should take device to Detox', () =>
         expect(detox.device).toBe(device()));
@@ -128,8 +144,8 @@ describe('Detox', () => {
       it('should expose device to global', () =>
         expect(global.device).toBe(device()));
 
-      it('should expose device driver matchers to global', () =>
-        expect(global.globalMatcher).toBe(globalMatcher));
+      it('should expose matchers to global', () =>
+        expect(global.globalMatcher).toBe(mockGlobalMatcher));
 
       it('should create artifacts manager', () =>
         expect(ArtifactsManager).toHaveBeenCalledWith(detoxConfig.artifactsConfig));
@@ -190,8 +206,8 @@ describe('Detox', () => {
 
       beforeEach(init);
 
-      it('should take device driver matchers to Detox', () =>
-        expect(detox.globalMatcher).toBe(globalMatcher));
+      it('should take the matchers from the matchers-registry to Detox', () =>
+        expect(detox.globalMatcher).toBe(mockGlobalMatcher));
 
       it('should take device to Detox', () =>
         expect(detox.device).toBe(device()));
@@ -199,7 +215,7 @@ describe('Detox', () => {
       it('should not expose device to globals', () =>
         expect(global.device).toBe(undefined));
 
-      it('should not expose globalMatcher to globals', () =>
+      it('should not expose matchers to globals', () =>
         expect(global.globalMatcher).toBe(undefined));
     });
 
diff --git a/detox/src/android/expect.test.js b/detox/src/android/expect.test.js
index 1d68913a5..c13d40dca 100644
--- a/detox/src/android/expect.test.js
+++ b/detox/src/android/expect.test.js
@@ -3,7 +3,9 @@ describe('expect', () => {
 
   beforeEach(() => {
     const AndroidExpect = require('./expect');
-    e = new AndroidExpect(new MockExecutor());
+    e = new AndroidExpect({
+      invocationManager: new MockExecutor(),
+    });
   });
 
   it(`element by accessibilityLabel`, async () => {
diff --git a/detox/src/devices/Device.test.js b/detox/src/devices/Device.test.js
index 2e6a3bb53..4e8bbec26 100644
--- a/detox/src/devices/Device.test.js
+++ b/detox/src/devices/Device.test.js
@@ -111,6 +111,11 @@ describe('Device', () => {
     expect(device.name).toEqual('mock-device-name-from-driver');
   });
 
+  it('should return the type from the configuration', async () => {
+    const device = validDevice();
+    expect(device.type).toEqual('ios.simulator');
+  });
+
   it('should return an undefined ID for an unprepared device', async() => {
     const device = validDevice();
     expect(device.id).toBeUndefined();
diff --git a/detox/src/devices/DriverRegistry.test.js b/detox/src/devices/DriverRegistry.test.js
index 7cdae0a21..06983c393 100644
--- a/detox/src/devices/DriverRegistry.test.js
+++ b/detox/src/devices/DriverRegistry.test.js
@@ -56,10 +56,12 @@ describe('DriverRegistry', () => {
 
     it('should try to resolve unknown driver as a node.js dependency', () => {
       require('../utils/resolveModuleFromPath').mockImplementation(() => {
-        return require('./drivers/__mocks__/FakeDriver');
+        return {
+          DriverClass: require('./drivers/__mocks__/FakeDriver'),
+        };
       });
 
-      const FakeDriver = require('./drivers/__mocks__/FakeDriver')
+      const FakeDriver = require('./drivers/__mocks__/FakeDriver');
       const driver = registry.resolve('fake-driver', opts);
 
       expect(driver).toBeInstanceOf(FakeDriver);
diff --git a/detox/src/devices/drivers/android/AndroidDriver.test.js b/detox/src/devices/drivers/android/AndroidDriver.test.js
index ed5efed82..e7cec4e38 100644
--- a/detox/src/devices/drivers/android/AndroidDriver.test.js
+++ b/detox/src/devices/drivers/android/AndroidDriver.test.js
@@ -18,6 +18,7 @@ describe('Android driver', () => {
   let exec;
   let emitter;
   let detoxApi;
+  let invocationManager;
   beforeEach(() => {
     jest.mock('fs', () => ({
       existsSync: jest.fn(),
@@ -69,6 +70,9 @@ describe('Android driver', () => {
 
     jest.mock('../../../android/espressoapi/Detox');
     detoxApi = require('../../../android/espressoapi/Detox');
+
+    const InvocationManager = jest.genMockFromModule('../../../invoke').InvocationManager;
+    invocationManager = new InvocationManager();
   });
 
   let instrumentation;
@@ -139,21 +143,6 @@ describe('Android driver', () => {
     jest.mock('./tools/TempFileXfer', () => MockTempFileXferClass);
   });
 
-  let invocationManager;
-  beforeEach(() => {
-    class MockInvocationManagerClass {
-      constructor() {
-        Object.assign(this, invocationManager);
-      }
-    }
-
-    const InvocationManager = jest.genMockFromModule('../../../invoke').InvocationManager;
-    invocationManager = new InvocationManager();
-    jest.mock('../../../invoke', () => ({
-      InvocationManager: MockInvocationManagerClass,
-    }))
-  });
-
   let appInstallHelper;
   beforeEach(() => {
     class MockAppInstallHelperClass {
@@ -174,6 +163,7 @@ describe('Android driver', () => {
     const AndroidDriver = require('./AndroidDriver');
     uut = new AndroidDriver({
       client,
+      invocationManager,
       emitter,
     });
   });
diff --git a/detox/src/ios/expectTwo.test.js b/detox/src/ios/expectTwo.test.js
index edc24b305..b88651bfb 100644
--- a/detox/src/ios/expectTwo.test.js
+++ b/detox/src/ios/expectTwo.test.js
@@ -3,7 +3,9 @@ const _ = require('lodash');
 describe('expectTwo', () => {
   beforeEach(() => {
     const IosExpect = require('./expectTwo');
-    e = new IosExpect(new MockExecutor());
+    e = new IosExpect({
+      invocationManager: new MockExecutor(),
+    });
   });
 
   it(`element(by.text('tapMe')).tap()`, () => {
diff --git a/detox/src/ios/expectTwoApiCoverage.test.js b/detox/src/ios/expectTwoApiCoverage.test.js
index 484cfef49..573d0e8d0 100644
--- a/detox/src/ios/expectTwoApiCoverage.test.js
+++ b/detox/src/ios/expectTwoApiCoverage.test.js
@@ -3,7 +3,9 @@ describe('expectTwo API Coverage', () => {
 
   beforeEach(() => {
     const IosExpect = require('./expectTwo');
-    e = new IosExpect(new MockExecutor());
+    e = new IosExpect({
+      invocationManager: new MockExecutor(),
+    });
   });
 
 
diff --git a/detox/src/matchersRegistry.test.js b/detox/src/matchersRegistry.test.js
new file mode 100644
index 000000000..d884bc168
--- /dev/null
+++ b/detox/src/matchersRegistry.test.js
@@ -0,0 +1,94 @@
+describe('Detox matchers registry', () => {
+
+  const opts = {
+    some: 'object',
+  };
+
+  let androidExpect;
+  let iosExpect;
+  let device;
+  let resolveModuleFromPath;
+  let uut;
+  beforeEach(() => {
+    androidExpect = {
+      ctor: jest.fn(),
+    };
+
+    class MockAndroidExpect {
+      constructor(...args) {
+        this.mockName = 'mock-android-expect';
+        androidExpect.ctor(...args);
+      }
+    }
+    jest.mock('./android/expect', () => MockAndroidExpect);
+
+    iosExpect = {
+      ctor: jest.fn(),
+    }
+    class MockIosExpect {
+      constructor(...args) {
+        this.mockName = 'mock-ios-expect';
+        iosExpect.ctor(...args);
+      }
+    }
+    jest.mock('./ios/expectTwo', () => MockIosExpect);
+
+    class MockExternalModuleExpect {
+      constructor() {
+        this.mockName = 'external-module-expect';
+      }
+    }
+    jest.mock('./utils/resolveModuleFromPath');
+    resolveModuleFromPath = require('./utils/resolveModuleFromPath');
+    resolveModuleFromPath.mockReturnValue({
+      ExpectClass: MockExternalModuleExpect,
+    });
+
+    device = {
+      getPlatform: jest.fn(),
+    };
+
+    uut = require('./matchersRegistry');
+  });
+
+  const withAndroidDevice = () => device.getPlatform.mockReturnValue('android');
+  const withIosDevice = () => device.getPlatform.mockReturnValue('ios');
+  const withUnknownDevice = (deviceType) => {
+    device.getPlatform.mockReturnValue(undefined);
+    device.type = deviceType;
+  }
+
+  it('should resolve the Android matchers', () => {
+    withAndroidDevice();
+    const result = uut.resolve(device);
+    expect(result.mockName).toEqual('mock-android-expect');
+  });
+
+  it('should init the matchers with opts', () => {
+    withAndroidDevice();
+
+    uut.resolve(device, opts);
+    expect(androidExpect.ctor).toHaveBeenCalledWith(opts);
+  });
+
+  it('should resolve the iOS matchers', () => {
+    withIosDevice();
+    const result = uut.resolve(device);
+    expect(result.mockName).toEqual('mock-ios-expect');
+  });
+
+  it('should init the ios-matchers with opts', () => {
+    withIosDevice();
+    uut.resolve(device, opts);
+    expect(iosExpect.ctor).toHaveBeenCalledWith(opts);
+  });
+
+  it('should resort to type-as-path based resolution if platform is not recognized', () => {
+    const deviceType = './path/to/external/module/index.js';
+    withUnknownDevice(deviceType);
+
+    const result = uut.resolve(device, opts);
+    expect(result.mockName).toEqual('external-module-expect');
+    expect(resolveModuleFromPath).toHaveBeenCalledWith(deviceType);
+  });
+});