detox
Version:
E2E tests and automation for mobile
242 lines (204 loc) • 6.72 kB
JavaScript
const path = require('path');
const WithEmitter = require('jest-environment-emit').default;
const resolveFrom = require('resolve-from');
const maybeNodeEnvironment = require(resolveFrom(process.cwd(), 'jest-environment-node'));
/** @type {typeof import('@jest/environment').JestEnvironment} */
const NodeEnvironment = maybeNodeEnvironment.default || maybeNodeEnvironment;
const detox = require('../../../internals');
const Timer = require('../../../src/utils/Timer');
const {
DetoxCoreListener,
DetoxInitErrorListener,
DetoxPlatformFilterListener,
REPLListener,
SpecReporter,
WorkerAssignReporter
} = require('./listeners');
const assertExistingContext = require('./utils/assertExistingContext');
const { validateAndPatchProjectConfig } = require('./utils/validateAndPatchProjectConfig');
const SYNC_CIRCUS_EVENTS = new Set([
'start_describe_definition',
'finish_describe_definition',
'add_hook',
'add_test',
'error',
]);
const log = detox.log.child({ cat: 'lifecycle,jest-environment' });
/**
* @see https://www.npmjs.com/package/jest-circus#overview
*/
class DetoxCircusEnvironment extends WithEmitter(NodeEnvironment) {
constructor(config, context) {
super(validateAndPatchProjectConfig(config), assertExistingContext(context));
/** @private */
this._shouldManageDetox = detox.getStatus() === 'inactive';
/** @private */
this._timer = new Timer();
/** @internal */
this.testPath = path.relative(process.cwd(), context.testPath);
/** @protected */
this.testEventListeners = [];
/** @protected */
this.setupTimeout = detox.config.testRunner.jest.setupTimeout;
/** @protected */
this.teardownTimeout = detox.config.testRunner.jest.teardownTimeout;
log.trace.begin(this.testPath);
this.handleTestEvent = this.handleTestEvent.bind(this);
this.setup = this._wrapSetup(this.setup);
this.teardown = this._wrapTeardown(this.teardown);
this.registerListeners({
DetoxInitErrorListener,
DetoxPlatformFilterListener,
DetoxCoreListener,
SpecReporter,
WorkerAssignReporter,
REPLListener,
});
// Artifacts flushing should be delayed to avoid conflicts with third-party reporters
this.testEvents.on('*', this._onTestEvent.bind(this), 1e6);
}
/** @override */
async setup() {
await super.setup();
await this.initDetox();
}
// @ts-expect-error TS2425
async handleTestEvent(event, state) {
// @ts-expect-error TS2855
await super.handleTestEvent(event, state);
if (detox.session.unsafe_earlyTeardown) {
if (event.name === 'test_fn_start' || event.name === 'hook_start') {
throw new Error('Detox halted test execution due to an early teardown request');
}
}
}
/** @override */
async teardown() {
try {
await this.cleanupDetox();
} finally {
await super.teardown();
}
}
/** @protected */
registerListeners(map) {
for (const Listener of Object.values(map)) {
this.testEventListeners.push(new Listener({
env: this,
}));
}
}
/**
* @protected
*/
async initDetox() {
if (detox.session.unsafe_earlyTeardown) {
throw new Error('Detox halted test execution due to an early teardown request');
}
const opts = {
global: this.global,
workerId: `w${process.env.JEST_WORKER_ID}`,
};
if (this._shouldManageDetox) {
await detox.init(opts);
} else {
await detox.installWorker(opts);
}
detox.worker.pilot.setDefaults({
testContext: {
getCurrentTestFilePath: () => path.resolve(this.testPath),
},
});
return detox.worker;
}
/** @protected */
async cleanupDetox() {
if (this._shouldManageDetox) {
await detox.cleanup();
} else {
await detox.uninstallWorker();
}
}
/** @private */
_handleTestEventSync(event, state) {
const { name } = event;
for (const listener of this.testEventListeners) {
if (typeof listener[name] === 'function') {
listener[name](event, state);
}
}
}
/** @private */
_onTestEvent({ type, event, state }) {
const timeout = state && state.testTimeout != null ? state.testTimeout : this.setupTimeout;
this._timer.schedule(timeout);
if (event) {
if (SYNC_CIRCUS_EVENTS.has(event.name)) {
this._handleTestEventSync(event, state);
} else {
return this._handleTestEventAsync(event, state);
}
} else {
return this._handleTestEventAsync({ name: type }, null);
}
}
/** @private */
async _handleTestEventAsync(event, state = null) {
const description = `handling ${state ? 'jest-circus' : 'jest-environment'} "${event.name}" event`;
for (const listener of this.testEventListeners) {
if (typeof listener[event.name] !== 'function') {
continue;
}
try {
await this._timer.run(description, () => listener[event.name](event, state));
} catch (listenerError) {
log.error(listenerError);
if (this._timer.expired) {
break;
}
}
}
}
_wrapSetup(fn) {
const _setup = fn.bind(this);
return async () => {
await log.trace.complete('set up environment', async () => {
try {
this._timer.schedule(this.setupTimeout);
await this._handleTestEventAsync({ name: 'environment_setup_start' });
await this._timer.run(`setting up Detox environment`, _setup);
await this._handleTestEventAsync({ name: 'environment_setup_success' });
} catch (error) {
this._timer.schedule(this.teardownTimeout);
await this._handleTestEventAsync({ name: 'environment_setup_failure', error });
throw error;
} finally {
this._timer.clear();
}
});
};
}
_wrapTeardown(fn) {
const _teardown = fn.bind(this);
return async () => {
await log.trace.complete('tear down environment', async () => {
try {
this._timer.schedule(this.teardownTimeout);
await this._handleTestEventAsync({ name: 'environment_teardown_start' });
await this._timer.run(`tearing down Detox environment`, _teardown);
await this._handleTestEventAsync({ name: 'environment_teardown_success' });
} catch (error) {
if (this._timer.expired) {
this._timer.schedule(this.teardownTimeout);
}
await this._handleTestEventAsync({ name: 'environment_teardown_failure', error });
throw error;
} finally {
this._timer.clear();
log.trace.end();
}
});
};
}
}
module.exports = DetoxCircusEnvironment;