Skip to content

Commit c878093

Browse files
authored
Custom driver plugin support(wix#1919)
* Add dynamic require for driver plugins * Push absolute path logic into driver layer wix#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
1 parent 0f9ceb2 commit c878093

File tree

16 files changed

+374
-45
lines changed

16 files changed

+374
-45
lines changed

detox/src/Detox.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,14 @@ class Detox {
152152
this._client.setNonresponsivenessListener(this._onNonresnponsivenessEvent.bind(this));
153153
await this._client.connect();
154154

155-
const DeviceDriverClass = DEVICE_CLASSES[this._deviceConfig.type];
155+
let DeviceDriverClass = DEVICE_CLASSES[this._deviceConfig.type];
156+
if (!DeviceDriverClass) {
157+
try {
158+
DeviceDriverClass = require(this._deviceConfig.type);
159+
} catch (e) {
160+
// noop, if we don't find a module to require, we'll hit the unsupported error below
161+
}
162+
}
156163
if (!DeviceDriverClass) {
157164
throw new Error(`'${this._deviceConfig.type}' is not supported`);
158165
}

detox/src/Detox.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,29 @@ describe('Detox', () => {
259259
}
260260
});
261261

262+
it('properly instantiates configuration pointing to a plugin driver', async () => {
263+
let instantiated = false;
264+
class MockDriverPlugin {
265+
constructor(config) {
266+
instantiated = true;
267+
}
268+
on() {}
269+
declareArtifactPlugins() {}
270+
}
271+
jest.mock('driver-plugin', () => MockDriverPlugin, { virtual: true });
272+
const pluginDeviceConfig = {
273+
"binaryPath": "ios/build/Build/Products/Release-iphonesimulator/example.app",
274+
"type": "driver-plugin",
275+
"name": "MyPlugin"
276+
};
277+
278+
Detox = require('./Detox');
279+
detox = new Detox({deviceConfig: pluginDeviceConfig});
280+
await detox.init();
281+
282+
expect(instantiated).toBe(true);
283+
});
284+
262285
it(`should log EMIT_ERROR if the internal emitter throws an error`, async () => {
263286
Detox = require('./Detox');
264287
detox = new Detox({deviceConfig: validDeviceConfigWithSession});

detox/src/devices/Device.js

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const _ = require('lodash');
22
const fs = require('fs');
33
const path = require('path');
44
const argparse = require('../utils/argparse');
5-
const debug = require('../utils/debug'); //debug utils, leave here even if unused
5+
const debug = require('../utils/debug'); // debug utils, leave here even if unused
66

77
class Device {
88
constructor({ deviceConfig, deviceDriver, emitter, sessionConfig }) {
@@ -16,16 +16,14 @@ class Device {
1616
}
1717

1818
async prepare(params = {}) {
19-
this._binaryPath = this._getAbsolutePath(this._deviceConfig.binaryPath);
20-
this._testBinaryPath = this._deviceConfig.testBinaryPath ? this._getAbsolutePath(this._deviceConfig.testBinaryPath) : null;
2119
this._deviceId = await this.deviceDriver.acquireFreeDevice(this._deviceConfig.device || this._deviceConfig.name);
22-
this._bundleId = await this.deviceDriver.getBundleIdFromBinary(this._binaryPath);
20+
this._bundleId = await this.deviceDriver.getBundleIdFromBinary(this._deviceConfig.binaryPath);
2321

2422
await this.deviceDriver.prepare();
2523

2624
if (!argparse.getArgValue('reuse') && !params.reuse) {
2725
await this.deviceDriver.uninstallApp(this._deviceId, this._bundleId);
28-
await this.deviceDriver.installApp(this._deviceId, this._binaryPath, this._testBinaryPath);
26+
await this.deviceDriver.installApp(this._deviceId, this._deviceConfig.binaryPath, this._deviceConfig.testBinaryPath);
2927
}
3028

3129
if (params.launchApp) {
@@ -184,8 +182,8 @@ class Device {
184182
}
185183

186184
async installApp(binaryPath, testBinaryPath) {
187-
const _binaryPath = binaryPath || this._binaryPath;
188-
const _testBinaryPath = testBinaryPath || this._testBinaryPath;
185+
const _binaryPath = binaryPath || this._deviceConfig.binaryPath;
186+
const _testBinaryPath = testBinaryPath || this._deviceConfig.testBinaryPath;
189187
await this.deviceDriver.installApp(this._deviceId, _binaryPath, _testBinaryPath);
190188
}
191189

@@ -306,27 +304,14 @@ class Device {
306304
return launchArgs;
307305
}
308306

309-
_getAbsolutePath(appPath) {
310-
if (path.isAbsolute(appPath)) {
311-
return appPath;
312-
}
313-
314-
const absPath = path.join(process.cwd(), appPath);
315-
if (fs.existsSync(absPath)) {
316-
return absPath;
317-
} else {
318-
throw new Error(`app binary not found at '${absPath}', did you build it?`);
319-
}
320-
}
321-
322307
async _terminateApp() {
323308
await this.deviceDriver.terminate(this._deviceId, this._bundleId);
324309
this._processes[this._bundleId] = undefined;
325310
}
326311

327312
async _reinstallApp() {
328313
await this.deviceDriver.uninstallApp(this._deviceId, this._bundleId);
329-
await this.deviceDriver.installApp(this._deviceId, this._binaryPath, this._testBinaryPath);
314+
await this.deviceDriver.installApp(this._deviceId, this._deviceConfig.binaryPath, this._deviceConfig.testBinaryPath);
330315
}
331316
}
332317

detox/src/devices/Device.test.js

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -125,22 +125,6 @@ describe('Device', () => {
125125
});
126126

127127
describe('prepare()', () => {
128-
it(`valid scheme, no binary, should throw`, async () => {
129-
const device = validDevice();
130-
fs.existsSync.mockReturnValue(false);
131-
try {
132-
await device.prepare();
133-
fail('should throw')
134-
} catch (ex) {
135-
expect(ex.message).toMatch(/app binary not found at/)
136-
}
137-
});
138-
139-
it(`valid scheme, no binary, should not throw`, async () => {
140-
const device = validDevice();
141-
await device.prepare();
142-
});
143-
144128
it(`when reuse is enabled in CLI args should not uninstall and install`, async () => {
145129
const device = validDevice();
146130
argparse.getArgValue.mockReturnValue(true);
@@ -478,7 +462,7 @@ describe('Device', () => {
478462

479463
await device.installApp('newAppPath');
480464

481-
expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, 'newAppPath', undefined);
465+
expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, 'newAppPath', device._deviceConfig.testBinaryPath);
482466
});
483467

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

495479
await device.installApp();
496480

497-
expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, device._binaryPath, device._testBinaryPath);
481+
expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._deviceId, device._deviceConfig.binaryPath, device._deviceConfig.testBinaryPath);
498482
});
499483
});
500484

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

513497
await device.uninstallApp();
514498

515-
expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith(device._deviceId, device._binaryPath);
499+
expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith(device._deviceId, device._bundleId);
516500
});
517501
});
518502

@@ -759,7 +743,7 @@ describe('Device', () => {
759743

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

765749
it(`pressBack() should invoke driver's pressBack()`, async () => {

detox/src/devices/drivers/android/AndroidDriver.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
2424
const sleep = require('../../../utils/sleep');
2525
const retry = require('../../../utils/retry');
2626
const { interruptProcess, spawnAndLog } = require('../../../utils/exec');
27+
const getAbsoluteBinaryPath = require('../../../utils/getAbsoluteBinaryPath');
2728
const AndroidExpect = require('../../../android/expect');
2829
const { InstrumentationLogsParser } = require('./InstrumentationLogsParser');
2930

@@ -63,12 +64,13 @@ class AndroidDriver extends DeviceDriverBase {
6364
}
6465

6566
async getBundleIdFromBinary(apkPath) {
66-
return await this.aapt.getPackageName(apkPath);
67+
return await this.aapt.getPackageName(getAbsoluteBinaryPath(apkPath));
6768
}
6869

6970
async installApp(deviceId, binaryPath, testBinaryPath) {
71+
binaryPath = getAbsoluteBinaryPath(binaryPath);
7072
await this.adb.install(deviceId, binaryPath);
71-
await this.adb.install(deviceId, testBinaryPath ? testBinaryPath : this.getTestApkPath(binaryPath));
73+
await this.adb.install(deviceId, testBinaryPath ? getAbsoluteBinaryPath(testBinaryPath) : this.getTestApkPath(binaryPath));
7274
}
7375

7476
async pressBack(deviceId) {

detox/src/devices/drivers/ios/SimulatorDriver.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const configuration = require('../../../configuration');
99
const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
1010
const environment = require('../../../utils/environment');
1111
const argparse = require('../../../utils/argparse');
12+
const getAbsoluteBinaryPath = require('../../../utils/getAbsoluteBinaryPath');
1213

1314
class SimulatorDriver extends IosDriver {
1415

@@ -56,6 +57,7 @@ class SimulatorDriver extends IosDriver {
5657
}
5758

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

7779
async installApp(deviceId, binaryPath) {
78-
await this.applesimutils.install(deviceId, binaryPath);
80+
await this.applesimutils.install(deviceId, getAbsoluteBinaryPath(binaryPath));
7981
}
8082

8183
async uninstallApp(deviceId, bundleId) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
function getAbsoluteBinaryPath(appPath) {
5+
if (path.isAbsolute(appPath)) {
6+
return appPath;
7+
}
8+
9+
const absPath = path.join(process.cwd(), appPath);
10+
if (fs.existsSync(absPath)) {
11+
return absPath;
12+
} else {
13+
throw new Error(`app binary not found at '${absPath}', did you build it?`);
14+
}
15+
}
16+
17+
module.exports = getAbsoluteBinaryPath;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const getAbsoluteBinaryPath = require('./getAbsoluteBinaryPath');
4+
5+
describe('getAbsoluteBinaryPath', () => {
6+
it('should return the given path if it is already absolute', async () => {
7+
expect(getAbsoluteBinaryPath('/my/absolute/path')).toEqual('/my/absolute/path');
8+
});
9+
10+
it('should return an absolute path if a relative path is passed in', async () => {
11+
expect(getAbsoluteBinaryPath('src/utils/getAbsoluteBinaryPath.js')).toEqual(path.join(process.cwd(), 'src/utils/getAbsoluteBinaryPath.js'));
12+
});
13+
14+
it('should throw exception if resulting absolute path does not exist', async () => {
15+
expect(() => getAbsoluteBinaryPath('my/relative/path'))
16+
.toThrowError();
17+
});
18+
});
19+

docs/Guide.ThirdPartyDrivers.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Third Party Drivers
2+
3+
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.
4+
5+
```
6+
"ios.sim": {
7+
"binaryPath": "bin/YourApp.app",
8+
"type": "ios.simulator",
9+
}
10+
```
11+
12+
While React Native officially supports Android and iOS, other platforms such as
13+
[Web](https://github.com/necolas/react-native-web) and [Windows](https://github.com/microsoft/react-native-windows)
14+
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)
15+
16+
## How to use a driver
17+
18+
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.
19+
20+
1. Add the driver to your `package.json` withh `npm install --save-dev detox-driver-package` or `yarn add --dev detox-driver-package`
21+
1. Add a new detox configuration to your existing configurations with the `type` set to driver's package name.
22+
```
23+
"thirdparty.driver.config": {
24+
"binaryPath": "bin/YourApp.app",
25+
"type": "detox-driver-package",
26+
}
27+
```
28+
3. Run detox while specifying the name of your new configuration `detox test --configuration detox-driver-package`
29+
30+
## Writing a new driver
31+
32+
### Anatomy of a driver
33+
34+
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
35+
36+
1. The Device Driver - code runs on the Detox tester, within the test runner context. It implements the details for the
37+
[`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.
38+
1. Matchers - code powering the `expect` `element` `waitFor` and `by` globals in your tests.
39+
These helpers serialize your test code so they can be sent over the network to the device on which your tests are running.
40+
1. Driver Client - code running on the device being tested. The driver client communicates with the server over
41+
websocket where it receives information from the serialized matchers, and expectations, and also sends responses
42+
back of whether each step of your test succeeds or fails. Typically a device client will use an underlying library specific
43+
to the platform at hand to implement the expectations.
44+
45+
### Implementation details
46+
47+
You may want to read through the source of both the built-in, official drivers as well as
48+
existing third party drivers to get a sense of how the code is structured. You can also look at
49+
`examples/demo-plugin/driver.js` for a minimal driver implementation that doesn't really do anything
50+
useful. Your driver should extend `DeviceDriverBase` and export as `module.exports`.
51+
52+
```
53+
const DeviceDriverBase = require('detox/src/devices/drivers/DeviceDriverBase');
54+
class MyNewDriver extends DeviceDriverBase {
55+
// ...
56+
}
57+
module.exports = MyNewDriver;
58+
```
59+
60+
## Existing Third party drivers
61+
62+
* [detox-puppeteer](https://github.com/ouihealth/detox-puppeteer)

0 commit comments

Comments
 (0)