diff --git a/detox/package.json b/detox/package.json index 0c0cf0f712..8a3b79d4c2 100644 --- a/detox/package.json +++ b/detox/package.json @@ -88,6 +88,7 @@ "src/devices/ios/AppleSimUtils.js", "src/server/DetoxServer.js", ".*Driver.js", + "MissingDetox.js", "EmulatorTelnet.js", "Emulator.js", "DeviceDriverBase.js", diff --git a/detox/runners/jest/WorkerAssignReporterImpl.js b/detox/runners/jest/WorkerAssignReporterImpl.js index 3643f9b63c..23a76bfdc8 100644 --- a/detox/runners/jest/WorkerAssignReporterImpl.js +++ b/detox/runners/jest/WorkerAssignReporterImpl.js @@ -1,3 +1,4 @@ +const _ = require('lodash'); const chalk = require('chalk').default; const ReporterBase = require('./ReporterBase'); const log = require('../../src/utils/logger').child(); @@ -9,7 +10,12 @@ class WorkerAssignReporterImpl extends ReporterBase { } report(workerName) { - log.info({event: 'WORKER_ASSIGN'}, `${chalk.whiteBright(workerName)} assigned to ${chalk.blueBright(this.device.name)}`); + const deviceName = _.attempt(() => this.device.name); + const formattedDeviceName = _.isError(deviceName) + ? chalk.redBright('undefined') + : chalk.blueBright(deviceName); + + log.info({event: 'WORKER_ASSIGN'}, `${chalk.whiteBright(workerName)} is assigned to ${formattedDeviceName}`); } } diff --git a/detox/src/Detox.js b/detox/src/Detox.js index 5b7f4eeefb..f3181ce28a 100644 --- a/detox/src/Detox.js +++ b/detox/src/Detox.js @@ -9,6 +9,7 @@ const EmulatorDriver = require('./devices/drivers/EmulatorDriver'); const AttachedAndroidDriver = require('./devices/drivers/AttachedAndroidDriver'); const DetoxRuntimeError = require('./errors/DetoxRuntimeError'); const argparse = require('./utils/argparse'); +const MissingDetox = require('./utils/MissingDetox'); const configuration = require('./configuration'); const Client = require('./client/Client'); const DetoxServer = require('./server/DetoxServer'); @@ -177,4 +178,6 @@ class Detox { } } +Detox.none = new MissingDetox(); + module.exports = Detox; diff --git a/detox/src/DetoxExportWrapper.js b/detox/src/DetoxExportWrapper.js index a8c6939977..58e598d72c 100644 --- a/detox/src/DetoxExportWrapper.js +++ b/detox/src/DetoxExportWrapper.js @@ -10,7 +10,7 @@ const _detox = Symbol('detox'); class DetoxExportWrapper { constructor() { - this[_detox] = null; + this[_detox] = Detox.none; this.init = this.init.bind(this); this.cleanup = this.cleanup.bind(this); @@ -29,33 +29,39 @@ class DetoxExportWrapper { } async init(config, params) { + if (!params || params.initGlobals !== false) { + Detox.none.initContext(global); + } + this[_detox] = await DetoxExportWrapper._initializeInstance(config, params); return this[_detox]; } async cleanup() { - if (this[_detox]) { + Detox.none.cleanupContext(global); + + if (this[_detox] !== Detox.none) { await this[_detox].cleanup(); - this[_detox] = null; + this[_detox] = Detox.none; } } _definePassthroughMethod(name) { this[name] = (...args) => { - if (this[_detox]) { - return this[_detox][name](...args); - } + return this[_detox][name](...args); }; } _defineProxy(name) { - this[name] = funpermaproxy(() => (this[_detox] && this[_detox][name])); + this[name] = funpermaproxy(() => this[_detox][name]); } static async _initializeInstance(detoxConfig, params) { let instance = null; try { + Detox.none.setError(null); + if (!detoxConfig) { throw new Error(`No configuration was passed to detox, make sure you pass a detoxConfig when calling 'detox.init(detoxConfig)'`); } @@ -81,6 +87,7 @@ class DetoxExportWrapper { await instance.init(params); return instance; } catch (err) { + Detox.none.setError(err); log.error({ event: 'DETOX_INIT_ERROR' }, '\n', err); if (instance) { diff --git a/detox/src/index.test.js b/detox/src/index.test.js index 1b1d3f2e8b..e037b321c4 100644 --- a/detox/src/index.test.js +++ b/detox/src/index.test.js @@ -1,4 +1,5 @@ const schemes = require('./configurations.mock'); + describe('index', () => { let detox; let mockDevice; @@ -24,7 +25,13 @@ describe('index', () => { .mock('./devices/Device') .mock('./utils/logger') .mock('./client/Client') - .mock('./Detox', () => jest.fn(() => mockDetox)) + .mock('./Detox', () => { + const MissingDetox = require('./utils/MissingDetox'); + const none = new MissingDetox(); + none.cleanupContext = () => {}; // avoid ruining global state + + return Object.assign(jest.fn(() => mockDetox), { none }); + }); process.env.DETOX_UNIT_TEST = true; detox = require('./index'); @@ -39,6 +46,14 @@ describe('index', () => { .unmock('./Detox') }); + it(`constructs detox without globals if initGlobals = false`, async () => { + const Detox = require('./Detox'); + + await detox.init(schemes.validOneDeviceNoSession, { initGlobals: false }); + + expect('by' in global).toBe(false); + }); + it(`throws if there was no config passed`, async () => { const logger = require('./utils/logger'); let exception = undefined; @@ -185,8 +200,9 @@ describe('index', () => { it(`Basic usage with memorized exported objects`, async() => { const { device, by } = detox; - expect(device.launchApp).toBe(undefined); - expect(by.id).toBe(undefined); + expect(() => { device, by }).not.toThrowError(); + expect(() => { device.launchApp }).toThrowError(); + expect(() => { by.id }).toThrowError(); await detox.init(schemes.validOneDeviceNoSession); @@ -195,8 +211,9 @@ describe('index', () => { await detox.cleanup(); - expect(device.launchApp).toBe(undefined); - expect(by.id).toBe(undefined); + expect(() => { device, by }).not.toThrowError(); + expect(() => { device.launchApp }).toThrowError(); + expect(() => { by.id }).toThrowError(); }); it(`Basic usage, do not throw an error if cleanup is done twice`, async() => { @@ -221,7 +238,14 @@ describe('index', () => { }); it(`beforeEach() should be covered - with detox not initialized`, async() => { - await detox.beforeEach(); + await expect(detox.beforeEach()).rejects.toThrow(/Make sure to call/); + }); + + it(`error message should be covered - with detox failed to initialize`, async() => { + mockDetox.init.mockReturnValue(Promise.reject(new Error('Test error'))); + + await expect(detox.init(schemes.validOneDeviceNoSession)).rejects.toThrow(/Test error/); + await expect(detox.beforeEach()).rejects.toThrow(/There was an error/); }); it(`afterEach() should be covered - with detox initialized`, async() => { @@ -230,7 +254,6 @@ describe('index', () => { }); it(`afterEach() should be covered - with detox not initialized`, async() => { - await detox.afterEach(); + await expect(detox.afterEach()).rejects.toThrow(/Make sure to call/); }); - }); diff --git a/detox/src/utils/MissingDetox.js b/detox/src/utils/MissingDetox.js new file mode 100644 index 0000000000..23c097baa3 --- /dev/null +++ b/detox/src/utils/MissingDetox.js @@ -0,0 +1,77 @@ +const DetoxRuntimeError = require('../errors/DetoxRuntimeError'); + +class MissingDetox { + constructor() { + this.throwError = this.throwError.bind(this); + this.initContext(this); + + this._defineRequiredProperty(this, 'beforeEach', async () => this.throwError(), true); + this._defineRequiredProperty(this, 'afterEach', async () => this.throwError(), true); + } + + initContext(context) { + const readonly = context === this; + + this._defineRequiredProperty(context, 'by', undefined, readonly); + this._defineRequiredProperty(context, 'device', undefined, readonly); + this._defineRequiredProperty(context, 'element', this.throwError, readonly); + this._defineRequiredProperty(context, 'expect', this.throwError, readonly); + this._defineRequiredProperty(context, 'waitFor', this.throwError, readonly); + } + + cleanupContext(context) { + this._cleanupProperty(context, 'by'); + this._cleanupProperty(context, 'device'); + this._cleanupProperty(context, 'element'); + this._cleanupProperty(context, 'expect'); + this._cleanupProperty(context, 'waitFor'); + } + + _cleanupProperty(context, name) { + if (context.hasOwnProperty(name)) { + context[name] = undefined; + } + } + + _defineRequiredProperty(context, name, initialValue, readonly) { + if (context.hasOwnProperty(name)) { + return; + } + + let _value = initialValue; + + const descriptor = { + get: () => { + if (_value === undefined) { + this.throwError(); + } + + return _value; + }, + }; + + if (!readonly) { + descriptor.set = (value) => { + _value = value; + }; + } + + Object.defineProperty(context, name, descriptor); + } + + setError(err) { + this._lastError = err; + } + + throwError() { + throw new DetoxRuntimeError({ + message: 'Detox instance has not been initialized', + hint: this._lastError + ? 'There was an error on attempt to call detox.init()' + : 'Make sure to call detox.init() before your test begins', + debugInfo: this._lastError && this._lastError.stack || '', + }); + } +} + +module.exports = MissingDetox;