Skip to content

Commit

Permalink
Custom driver plugin support(#1919)
Browse files Browse the repository at this point in the history
* Add dynamic require for driver plugins

* Push absolute path logic into driver layer

#1919 (comment)

* fix existing tests to use new internal implementation details in assertions

also fix code coverage

* Add test for support of driver plugin

* Add demo-puppeteer

* Fix tests

Need to maintain 100% coverage while not breaking prepare for drivers
whose binaryPath isn't an actual path on disk

* Create Guide.ThirdPartyDrivers.md

* Use headless driver for jenkins compatibility

* Add demo-plugin project

* Remove demo-puppeteer

* bump detox to v16 in examples/demo-plugin

* remove demo-puppeteer from lerna.json

* Update README with clarifications

* Add getAbsoluteBinaryPath.test.js to avoid testing it's logic in Device.test.js

* remove checkBinaryPath argument
  • Loading branch information
awinograd authored Apr 22, 2020
1 parent 0f9ceb2 commit c878093
Show file tree
Hide file tree
Showing 16 changed files with 374 additions and 45 deletions.
9 changes: 8 additions & 1 deletion detox/src/Detox.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,14 @@ class Detox {
this._client.setNonresponsivenessListener(this._onNonresnponsivenessEvent.bind(this));
await this._client.connect();

const DeviceDriverClass = DEVICE_CLASSES[this._deviceConfig.type];
let DeviceDriverClass = DEVICE_CLASSES[this._deviceConfig.type];
if (!DeviceDriverClass) {
try {
DeviceDriverClass = require(this._deviceConfig.type);
} catch (e) {
// noop, if we don't find a module to require, we'll hit the unsupported error below
}
}
if (!DeviceDriverClass) {
throw new Error(`'${this._deviceConfig.type}' is not supported`);
}
Expand Down
23 changes: 23 additions & 0 deletions detox/src/Detox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,29 @@ describe('Detox', () => {
}
});

it('properly instantiates configuration pointing to a plugin driver', async () => {
let instantiated = false;
class MockDriverPlugin {
constructor(config) {
instantiated = true;
}
on() {}
declareArtifactPlugins() {}
}
jest.mock('driver-plugin', () => MockDriverPlugin, { virtual: true });
const pluginDeviceConfig = {
"binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
"type": "driver-plugin",
"name": "MyPlugin"
};

Detox = require('./Detox');
detox = new Detox({deviceConfig: pluginDeviceConfig});
await detox.init();

expect(instantiated).toBe(true);
});

it(`should log EMIT_ERROR if the internal emitter throws an error`, async () => {
Detox = require('./Detox');
detox = new Detox({deviceConfig: validDeviceConfigWithSession});
Expand Down
27 changes: 6 additions & 21 deletions detox/src/devices/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const argparse = require('../utils/argparse');
const debug = require('../utils/debug'); //debug utils, leave here even if unused
const debug = require('../utils/debug'); // debug utils, leave here even if unused

class Device {
constructor({ deviceConfig, deviceDriver, emitter, sessionConfig }) {
Expand All @@ -16,16 +16,14 @@ class Device {
}

async prepare(params = {}) {
this._binaryPath = this._getAbsolutePath(this._deviceConfig.binaryPath);
this._testBinaryPath = this._deviceConfig.testBinaryPath ? this._getAbsolutePath(this._deviceConfig.testBinaryPath) : null;
this._deviceId = await this.deviceDriver.acquireFreeDevice(this._deviceConfig.device || this._deviceConfig.name);
this._bundleId = await this.deviceDriver.getBundleIdFromBinary(this._binaryPath);
this._bundleId = await this.deviceDriver.getBundleIdFromBinary(this._deviceConfig.binaryPath);

await this.deviceDriver.prepare();

if (!argparse.getArgValue('reuse') && !params.reuse) {
await this.deviceDriver.uninstallApp(this._deviceId, this._bundleId);
await this.deviceDriver.installApp(this._deviceId, this._binaryPath, this._testBinaryPath);
await this.deviceDriver.installApp(this._deviceId, this._deviceConfig.binaryPath, this._deviceConfig.testBinaryPath);
}

if (params.launchApp) {
Expand Down Expand Up @@ -184,8 +182,8 @@ class Device {
}

async installApp(binaryPath, testBinaryPath) {
const _binaryPath = binaryPath || this._binaryPath;
const _testBinaryPath = testBinaryPath || this._testBinaryPath;
const _binaryPath = binaryPath || this._deviceConfig.binaryPath;
const _testBinaryPath = testBinaryPath || this._deviceConfig.testBinaryPath;
await this.deviceDriver.installApp(this._deviceId, _binaryPath, _testBinaryPath);
}

Expand Down Expand Up @@ -306,27 +304,14 @@ class Device {
return launchArgs;
}

_getAbsolutePath(appPath) {
if (path.isAbsolute(appPath)) {
return appPath;
}

const absPath = path.join(process.cwd(), appPath);
if (fs.existsSync(absPath)) {
return absPath;
} else {
throw new Error(`app binary not found at '${absPath}', did you build it?`);
}
}

async _terminateApp() {
await this.deviceDriver.terminate(this._deviceId, this._bundleId);
this._processes[this._bundleId] = undefined;
}

async _reinstallApp() {
await this.deviceDriver.uninstallApp(this._deviceId, this._bundleId);
await this.deviceDriver.installApp(this._deviceId, this._binaryPath, this._testBinaryPath);
await this.deviceDriver.installApp(this._deviceId, this._deviceConfig.binaryPath, this._deviceConfig.testBinaryPath);
}
}

Expand Down
24 changes: 4 additions & 20 deletions detox/src/devices/Device.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,22 +125,6 @@ describe('Device', () => {
});

describe('prepare()', () => {
it(`valid scheme, no binary, should throw`, async () => {
const device = validDevice();
fs.existsSync.mockReturnValue(false);
try {
await device.prepare();
fail('should throw')
} catch (ex) {
expect(ex.message).toMatch(/app binary not found at/)
}
});

it(`valid scheme, no binary, should not throw`, async () => {
const device = validDevice();
await device.prepare();
});

it(`when reuse is enabled in CLI args should not uninstall and install`, async () => {
const device = validDevice();
argparse.getArgValue.mockReturnValue(true);
Expand Down Expand Up @@ -478,7 +462,7 @@ describe('Device', () => {

await device.installApp('newAppPath');

expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, 'newAppPath', undefined);
expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, 'newAppPath', device._deviceConfig.testBinaryPath);
});

it(`with a custom test app path should use custom test app path`, async () => {
Expand All @@ -494,7 +478,7 @@ describe('Device', () => {

await device.installApp();

expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, device._binaryPath, device._testBinaryPath);
expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, device._deviceConfig.binaryPath, device._deviceConfig.testBinaryPath);
});
});

Expand All @@ -512,7 +496,7 @@ describe('Device', () => {

await device.uninstallApp();

expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith(device._deviceId, device._binaryPath);
expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith(device._deviceId, device._bundleId);
});
});

Expand Down Expand Up @@ -759,7 +743,7 @@ describe('Device', () => {

it(`should accept relative path for binary`, async () => {
const actualPath = await launchAndTestBinaryPath('relativePath');
expect(actualPath).toEqual(path.join(process.cwd(), 'abcdef/123'));
expect(actualPath).toEqual('abcdef/123');
});

it(`pressBack() should invoke driver's pressBack()`, async () => {
Expand Down
6 changes: 4 additions & 2 deletions detox/src/devices/drivers/android/AndroidDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
const sleep = require('../../../utils/sleep');
const retry = require('../../../utils/retry');
const { interruptProcess, spawnAndLog } = require('../../../utils/exec');
const getAbsoluteBinaryPath = require('../../../utils/getAbsoluteBinaryPath');
const AndroidExpect = require('../../../android/expect');
const { InstrumentationLogsParser } = require('./InstrumentationLogsParser');

Expand Down Expand Up @@ -63,12 +64,13 @@ class AndroidDriver extends DeviceDriverBase {
}

async getBundleIdFromBinary(apkPath) {
return await this.aapt.getPackageName(apkPath);
return await this.aapt.getPackageName(getAbsoluteBinaryPath(apkPath));
}

async installApp(deviceId, binaryPath, testBinaryPath) {
binaryPath = getAbsoluteBinaryPath(binaryPath);
await this.adb.install(deviceId, binaryPath);
await this.adb.install(deviceId, testBinaryPath ? testBinaryPath : this.getTestApkPath(binaryPath));
await this.adb.install(deviceId, testBinaryPath ? getAbsoluteBinaryPath(testBinaryPath) : this.getTestApkPath(binaryPath));
}

async pressBack(deviceId) {
Expand Down
4 changes: 3 additions & 1 deletion detox/src/devices/drivers/ios/SimulatorDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const configuration = require('../../../configuration');
const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
const environment = require('../../../utils/environment');
const argparse = require('../../../utils/argparse');
const getAbsoluteBinaryPath = require('../../../utils/getAbsoluteBinaryPath');

class SimulatorDriver extends IosDriver {

Expand Down Expand Up @@ -56,6 +57,7 @@ class SimulatorDriver extends IosDriver {
}

async getBundleIdFromBinary(appPath) {
appPath = getAbsoluteBinaryPath(appPath);
try {
const result = await exec(`/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "${path.join(appPath, 'Info.plist')}"`);
const bundleId = _.trim(result.stdout);
Expand All @@ -75,7 +77,7 @@ class SimulatorDriver extends IosDriver {
}

async installApp(deviceId, binaryPath) {
await this.applesimutils.install(deviceId, binaryPath);
await this.applesimutils.install(deviceId, getAbsoluteBinaryPath(binaryPath));
}

async uninstallApp(deviceId, bundleId) {
Expand Down
17 changes: 17 additions & 0 deletions detox/src/utils/getAbsoluteBinaryPath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const fs = require('fs');
const path = require('path');

function getAbsoluteBinaryPath(appPath) {
if (path.isAbsolute(appPath)) {
return appPath;
}

const absPath = path.join(process.cwd(), appPath);
if (fs.existsSync(absPath)) {
return absPath;
} else {
throw new Error(`app binary not found at '${absPath}', did you build it?`);
}
}

module.exports = getAbsoluteBinaryPath;
19 changes: 19 additions & 0 deletions detox/src/utils/getAbsoluteBinaryPath.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const fs = require('fs');
const path = require('path');
const getAbsoluteBinaryPath = require('./getAbsoluteBinaryPath');

describe('getAbsoluteBinaryPath', () => {
it('should return the given path if it is already absolute', async () => {
expect(getAbsoluteBinaryPath('/my/absolute/path')).toEqual('/my/absolute/path');
});

it('should return an absolute path if a relative path is passed in', async () => {
expect(getAbsoluteBinaryPath('src/utils/getAbsoluteBinaryPath.js')).toEqual(path.join(process.cwd(), 'src/utils/getAbsoluteBinaryPath.js'));
});

it('should throw exception if resulting absolute path does not exist', async () => {
expect(() => getAbsoluteBinaryPath('my/relative/path'))
.toThrowError();
});
});

62 changes: 62 additions & 0 deletions docs/Guide.ThirdPartyDrivers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Third Party Drivers

Detox comes with built-in support for running on Android and iOS by choosing a driver type in your detox configurations. For example, the following configuration uses the "ios.simulator" driver.

```
"ios.sim": {
"binaryPath": "bin/YourApp.app",
"type": "ios.simulator",
}
```

While React Native officially supports Android and iOS, other platforms such as
[Web](https://github.com/necolas/react-native-web) and [Windows](https://github.com/microsoft/react-native-windows)
can be targeted. If your app targets a third party platform, you may which to use a [third party driver](#How-to-use-a-driver) to run your tests on said platform as of Detox v16.3.0. If one doesn't already exist you may want to [write your own](#Writing-a-new-driver)

## How to use a driver

Check to see if a [third party driver](#Existing-Third-party-drivers) already exists for the platform you wish to target. Mostly likely, the driver will have setup instructions. Overall the setup for any third party driver is fairly simple.

1. Add the driver to your `package.json` withh `npm install --save-dev detox-driver-package` or `yarn add --dev detox-driver-package`
1. Add a new detox configuration to your existing configurations with the `type` set to driver's package name.
```
"thirdparty.driver.config": {
"binaryPath": "bin/YourApp.app",
"type": "detox-driver-package",
}
```
3. Run detox while specifying the name of your new configuration `detox test --configuration detox-driver-package`

## Writing a new driver

### Anatomy of a driver

The architecture of a driver is split into a few different pieces. Understanding the [overall architecture of Detox](https://github.com/wix/Detox/blob/master/docs/Introduction.HowDetoxWorks.md#architecture) will help with this section

1. The Device Driver - code runs on the Detox tester, within the test runner context. It implements the details for the
[`device` object](https://github.com/wix/Detox/blob/master/docs/APIRef.DeviceObjectAPI.md) that is exposed in your detox tests. The implementation is responsible for managing device clients your tests will run on.
1. Matchers - code powering the `expect` `element` `waitFor` and `by` globals in your tests.
These helpers serialize your test code so they can be sent over the network to the device on which your tests are running.
1. Driver Client - code running on the device being tested. The driver client communicates with the server over
websocket where it receives information from the serialized matchers, and expectations, and also sends responses
back of whether each step of your test succeeds or fails. Typically a device client will use an underlying library specific
to the platform at hand to implement the expectations.

### Implementation details

You may want to read through the source of both the built-in, official drivers as well as
existing third party drivers to get a sense of how the code is structured. You can also look at
`examples/demo-plugin/driver.js` for a minimal driver implementation that doesn't really do anything
useful. Your driver should extend `DeviceDriverBase` and export as `module.exports`.

```
const DeviceDriverBase = require('detox/src/devices/drivers/DeviceDriverBase');
class MyNewDriver extends DeviceDriverBase {
// ...
}
module.exports = MyNewDriver;
```

## Existing Third party drivers

* [detox-puppeteer](https://github.com/ouihealth/detox-puppeteer)
Loading

0 comments on commit c878093

Please sign in to comment.