detox
Version:
E2E tests and automation for mobile
357 lines (300 loc) • 9.77 kB
JavaScript
const CAF = require('caf');
const _ = require('lodash');
const Client = require('./client/Client');
const environmentFactory = require('./environmentFactory');
const { DetoxRuntimeErrorComposer } = require('./errors');
const { InvocationManager } = require('./invoke');
const DetoxPilot = require('./pilot/DetoxPilot');
const symbols = require('./realms/symbols');
const AsyncEmitter = require('./utils/AsyncEmitter');
const uuid = require('./utils/uuid');
class DetoxWorker {
constructor(context) {
this._context = context;
this._injectedGlobalProperties = [];
this._config = context[symbols.config];
this._runtimeErrorComposer = new DetoxRuntimeErrorComposer(this._config);
this._client = null;
this._artifactsManager = null;
this._eventEmitter = new AsyncEmitter({
events: [
'bootDevice',
'beforeShutdownDevice',
'shutdownDevice',
'beforeTerminateApp',
'terminateApp',
'beforeUninstallApp',
'beforeLaunchApp',
'launchApp',
'appReady',
'createExternalArtifact',
],
onError: this._onEmitError.bind(this),
});
/** @type {DetoxInternals.RuntimeConfig['apps']} */
this._appsConfig = null;
/** @type {DetoxInternals.RuntimeConfig['artifacts']} */
this._artifactsConfig = null;
/** @type {DetoxInternals.RuntimeConfig['behavior']} */
this._behaviorConfig = null;
/** @type {DetoxInternals.RuntimeConfig['device']} */
this._deviceConfig = null;
/** @type {DetoxInternals.RuntimeConfig['session']} */
this._sessionConfig = null;
/** @type {string} */
this.id = 'worker';
/** @type {Detox.Device} */
this.device = null;
/** @type {Detox.ElementFacade} */
this.element = null;
/** @type {Detox.WaitForFacade} */
this.waitFor = null;
/** @type {Detox.ExpectFacade} */
this.expect = null;
/** @type {Detox.ByFacade} */
this.by = null;
/** @type {Detox.WebFacade} */
this.web = null;
/** @type {Detox.SystemFacade} */
this.system = null;
/** @type {Detox.PilotFacade} */
this.pilot = null;
/** @type {Detox.PilotFacade} */
this.copilot = null;
/** @type {Function} */
this.REPL = context.REPL;
this._deviceCookie = null;
this.trace = this._context.trace;
/** @deprecated */
this.traceCall = this._context.traceCall;
this._reinstallAppsOnDevice = CAF(this._reinstallAppsOnDevice.bind(this));
this._initToken = new CAF.cancelToken();
this._cafWrap([
'init',
'onRunDescribeStart',
'onTestStart',
'onHookFailure',
'onTestFnFailure',
'onTestDone',
'onRunDescribeFinish',
]);
}
/** @this {DetoxWorker} */
init = function* (signal) {
const {
apps: appsConfig,
artifacts: artifactsConfig,
behavior: behaviorConfig,
device: deviceConfig,
session: sessionConfig
} = this._config;
this._appsConfig = appsConfig;
this._artifactsConfig = artifactsConfig;
this._behaviorConfig = behaviorConfig;
this._deviceConfig = deviceConfig;
this._sessionConfig = sessionConfig;
// @ts-ignore
this._sessionConfig.sessionId = sessionConfig.sessionId || uuid.UUID();
this._runtimeErrorComposer.appsConfig = this._appsConfig;
this._client = new Client(sessionConfig);
this._client.terminateApp = async () => {
// @ts-ignore
if (this.device && this.device._isAppRunning()) {
await this.device.terminateApp();
}
};
this.pilot = new DetoxPilot();
Object.defineProperty(this, 'copilot', {
get: () => {
console.warn('Warning: "copilot" is deprecated. Please use "pilot" instead.');
return this.pilot;
},
configurable: true,
});
yield this._client.connect();
const invocationManager = new InvocationManager(this._client);
const {
// @ts-ignore
envValidatorFactory,
// @ts-ignore
artifactsManagerFactory,
// @ts-ignore
matchersFactory,
// @ts-ignore
runtimeDeviceFactory,
} = environmentFactory.createFactories(deviceConfig);
const envValidator = envValidatorFactory.createValidator();
yield envValidator.validate();
const commonDeps = {
invocationManager,
client: this._client,
eventEmitter: this._eventEmitter,
runtimeErrorComposer: this._runtimeErrorComposer,
};
this._artifactsManager = artifactsManagerFactory.createArtifactsManager(this._artifactsConfig, commonDeps);
this._deviceCookie = yield this._context[symbols.allocateDevice](this._deviceConfig);
this.device = runtimeDeviceFactory.createRuntimeDevice(
this._deviceCookie,
commonDeps,
{
appsConfig: this._appsConfig,
behaviorConfig: this._behaviorConfig,
deviceConfig: this._deviceConfig,
sessionConfig,
});
const matchers = matchersFactory.createMatchers({
invocationManager,
runtimeDevice: this.device,
eventEmitter: this._eventEmitter,
});
Object.assign(this, matchers);
yield this._eventEmitter.emit('bootDevice', { deviceId: this.device.id });
if (behaviorConfig.init.exposeGlobals) {
const injectedGlobals = {
...matchers,
device: this.device,
pilot: this.pilot,
REPL: this.REPL,
detox: this,
};
this._injectedGlobalProperties = Object.keys(injectedGlobals);
Object.assign(DetoxWorker.global, injectedGlobals);
Object.defineProperty(DetoxWorker.global, 'copilot', {
get: () => this.copilot,
configurable: true,
});
}
// @ts-ignore
yield this.device.installUtilBinaries();
if (behaviorConfig.init.reinstallApp) {
yield this._reinstallAppsOnDevice(signal);
}
const appAliases = Object.keys(this._appsConfig);
if (appAliases.length === 1) {
yield this.device.selectApp(appAliases[0]);
} else {
yield this.device.selectApp(null);
}
};
async cleanup() {
this._initToken.abort('CLEANUP');
for (const key of this._injectedGlobalProperties) {
delete DetoxWorker.global[key];
}
if (this._artifactsManager) {
await this._artifactsManager.onBeforeCleanup();
this._artifactsManager = null;
}
if (this._client) {
this._client.dumpPendingRequests();
await this._client.cleanup();
this._client = null;
}
if (this.device) {
// @ts-ignore
await this.device._cleanup();
}
if (this._deviceCookie) {
await this._context[symbols.deallocateDevice](this._deviceCookie);
}
this._deviceCookie = null;
this.device = null;
}
get log() {
return this._context.log;
}
onRunDescribeStart = function* (_signal, ...args) {
yield this._artifactsManager.onRunDescribeStart(...args);
};
onTestStart = function* (_signal, testSummary){
if (this.pilot.isInitialized()) {
this.pilot.start();
}
this._validateTestSummary('beforeEach', testSummary);
yield this._dumpUnhandledErrorsIfAny({
pendingRequests: false,
testName: testSummary.fullName,
});
yield this._artifactsManager.onTestStart(testSummary);
};
onHookFailure = function* (_signal, ...args) {
yield this._artifactsManager.onHookFailure(...args);
};
onTestFnFailure = function* (_signal, ...args) {
yield this._artifactsManager.onTestFnFailure(...args);
};
onTestDone = function* (_signal, testSummary) {
this._validateTestSummary('afterEach', testSummary);
yield this._artifactsManager.onTestDone(testSummary);
yield this._dumpUnhandledErrorsIfAny({
pendingRequests: testSummary.timedOut,
testName: testSummary.fullName,
});
if (this.pilot.isInitialized()) {
this.pilot.end(testSummary.status === 'passed');
}
};
onRunDescribeFinish = function* (_signal, ...args) {
yield this._artifactsManager.onRunDescribeFinish(...args);
};
*_reinstallAppsOnDevice(_signal) {
const appNames = _(this._appsConfig)
.map((config, key) => [key, `${config.binaryPath}:${config.testBinaryPath}`])
.uniqBy(1)
.map(0)
.value();
for (const appName of appNames) {
yield this.device.selectApp(appName);
yield this.device.uninstallApp();
}
for (const appName of appNames) {
yield this.device.selectApp(appName);
yield this.device.installApp();
}
}
_validateTestSummary(methodName, testSummary) {
if (!_.isPlainObject(testSummary)) {
throw this._runtimeErrorComposer.invalidTestSummary(methodName, testSummary);
}
switch (testSummary.status) {
case 'running':
case 'passed':
case 'failed':
break;
default:
throw this._runtimeErrorComposer.invalidTestSummaryStatus(methodName, testSummary);
}
}
async _dumpUnhandledErrorsIfAny({ testName, pendingRequests }) {
if (pendingRequests) {
this._client.dumpPendingRequests({ testName });
}
}
_onEmitError({ error, eventName, eventObj }) {
this.log.error(
{ event: 'EMIT_ERROR', fn: eventName },
`Caught an exception in: emitter.emit("${eventName}", ${JSON.stringify(eventObj)})\n\n`,
error
);
}
_cafWrap(methodNames) {
for (const methodName of methodNames) {
const cafMethod = CAF(this[methodName].bind(this));
this[methodName] = async (...args) => {
try {
await cafMethod(this._initToken.signal, ...args);
} catch (e) {
if (e !== 'CLEANUP') {
throw e;
}
}
return this;
};
}
}
}
/**
* @type {NodeJS.Global | {}}
*/
DetoxWorker.global = global;
module.exports = DetoxWorker;