Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug-report---android.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ Device logs can be retrieved from the device using `adb logcat`, or if recorded,
- Node:
- Device:
- OS:
- Test-runner (select one): Mocha | Jest+jasmine | Jest+jest-circus
- Test-runner (select one): `jest-circus` | `jest-jasmine2` (deprecated) | `mocha`

<!-- Note: Test-runner is set in Detox.test-runner in your package.json -->
4 changes: 2 additions & 2 deletions detox/local-cli/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ function createMochaFolderE2E() {
function createJestFolderE2E() {
createFolder('e2e', {
'config.json': jestTemplates.runnerConfig,
'init.js': jestTemplates.initjs,
'firstTest.spec.js': jestTemplates.firstTest
'environment.js': jestTemplates.environment,
'firstTest.e2e.js': jestTemplates.firstTest,
});

createFile('.detoxrc.json', JSON.stringify({
Expand Down
64 changes: 27 additions & 37 deletions detox/local-cli/templates/jest.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,40 @@
const firstTestContent = require('./firstTestContent');

const runnerConfig = `{
"setupFilesAfterEnv": ["./init.js"],
"testEnvironment": "node",
"testEnvironment": "./environment",
"testRunner": "jest-circus/runner",
"testTimeout": 120000,
"testRegex": "\\\\.e2e\\\\.js$",
"reporters": ["detox/runners/jest/streamlineReporter"],
"verbose": true
}
`;

const initjsContent = `const detox = require('detox');
const adapter = require('detox/runners/jest/adapter');
const specReporter = require('detox/runners/jest/specReporter');

// Set the default timeout
jest.setTimeout(120000);

jasmine.getEnv().addReporter(adapter);

// This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level.
// This is strictly optional.
jasmine.getEnv().addReporter(specReporter);

beforeAll(async () => {
await detox.init();
}, 300000);

beforeEach(async () => {
try {
await adapter.beforeEach();
} catch (err) {
// Workaround for the 'jest-jasmine' runner (default one): if 'beforeAll' hook above fails with a timeout,
// unfortunately, 'jest' might continue running other hooks and test suites. To prevent that behavior,
// adapter.beforeEach() will throw if detox.init() is still running; that allows us to run detox.cleanup()
// in that emergency case and disable calling 'device', 'element', 'expect', 'by' and other Detox globals.
// If you switch to 'jest-circus' runner, you can omit this try-catch workaround at all.

await detox.cleanup();
throw err;
const environmentJsContent = `const {
DetoxCircusEnvironment,
SpecReporter,
WorkerAssignReporter,
} = require('detox/runners/jest-circus');

class CustomDetoxEnvironment extends DetoxCircusEnvironment {
constructor(config) {
super(config);

// Can be safely removed, if you are content with the default value (=300000ms)
this.initTimeout = 300000;

// This takes care of generating status logs on a per-spec basis. By default, Jest only reports at file-level.
// This is strictly optional.
this.registerListeners({
SpecReporter,
WorkerAssignReporter,
});
}
});
}

afterAll(async () => {
await adapter.afterAll();
await detox.cleanup();
});
module.exports = CustomDetoxEnvironment;
`;

exports.initjs = initjsContent;
exports.environment = environmentJsContent;
exports.firstTest = firstTestContent;
exports.runnerConfig = runnerConfig;
3 changes: 2 additions & 1 deletion detox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"devDependencies": {
"eslint": "^4.11.0",
"eslint-plugin-node": "^6.0.1",
"jest": "25.3.x",
"jest": "26.x.x",
"mockdate": "^2.0.1",
"prettier": "1.7.0",
"react-native": "0.62.x",
Expand All @@ -53,6 +53,7 @@
"proper-lockfile": "^3.0.2",
"sanitize-filename": "^1.6.1",
"shell-utils": "^1.0.9",
"signal-exit": "^3.0.3",
"tail": "^2.0.0",
"telnet-client": "1.2.8",
"tempfile": "^2.0.0",
Expand Down
16 changes: 16 additions & 0 deletions detox/runners/integration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
lifecycle: {
onRunStart: Symbol('run_start'),
onRunDescribeStart: Symbol('run_describe_start'),
onTestStart: Symbol('test_start'),
onHookStart: Symbol('hook_start'),
onHookFailure: Symbol('hook_failure'),
onHookSuccess: Symbol('hook_success'),
onTestFnStart: Symbol('test_fn_start'),
onTestFnFailure: Symbol('test_fn_failure'),
onTestFnSuccess: Symbol('test_fn_success'),
onTestDone: Symbol('test_done'),
onRunDescribeFinish: Symbol('run_describe_finish'),
onRunFinish: Symbol('run_finish'),
},
};
111 changes: 111 additions & 0 deletions detox/runners/jest-circus/environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const _ = require('lodash');
const NodeEnvironment = require('jest-environment-node'); // eslint-disable-line node/no-extraneous-require
const DetoxCoreListener = require('./listeners/DetoxCoreListener');
const DetoxInitErrorListener = require('./listeners/DetoxInitErrorListener');
const assertJestCircus26 = require('./utils/assertJestCircus26');
const wrapErrorWithNoopLifecycle = require('./utils/wrapErrorWithNoopLifecycle');
const timely = require('../../src/utils/timely');

/**
* @see https://www.npmjs.com/package/jest-circus#overview
*/
class DetoxCircusEnvironment extends NodeEnvironment {
constructor(config) {
super(assertJestCircus26(config));

/** @private */
this._listenerFactories = {
DetoxInitErrorListener,
DetoxCoreListener,
};
/** @private */
this._hookTimeout = undefined;
/** @protected */
this.testEventListeners = [];
/** @protected */
this.initTimeout = 300000;
}

get detox() {
return require('../../src')._setGlobal(this.global);
}

async handleTestEvent(event, state) {
const { name } = event;

if (name === 'setup') {
await this._onSetup(state);
}

await this._timely(async () => {
for (const listener of this.testEventListeners) {
if (typeof listener[name] === 'function') {
await listener[name](event, state);
}
}
});

if (name === 'teardown') {
await this._onTeardown();
}
}

_timely(fn) {
const ms = this._hookTimeout === undefined ? this.initTimeout : this._hookTimeout;
return timely(fn, ms, () => {
return new Error(`Exceeded timeout of ${ms}ms.`);
})();
}

async _onSetup(state) {
let detox = null;

try {
try {
detox = await this._timely(() => this.initDetox());
} finally {
this._hookTimeout = state.testTimeout;
}
} catch (initError) {
state.unhandledErrors.push(initError);
detox = wrapErrorWithNoopLifecycle(initError);
await this._onTeardown();
}

this._instantiateListeners(detox);
}

_instantiateListeners(detoxInstance) {
for (const Listener of Object.values(this._listenerFactories)) {
this.testEventListeners.push(new Listener({
detox: detoxInstance,
env: this,
}));
}
}

async _onTeardown() {
try {
await this._timely(() => this.cleanupDetox());
} catch (cleanupError) {
state.unhandledErrors.push(cleanupError);
}
}

/** @protected */
async initDetox() {
return this.detox.init();
}

/** @protected */
async cleanupDetox() {
return this.detox.cleanup();
}

/** @protected */
registerListeners(map) {
Object.assign(this._listenerFactories, map);
}
}

module.exports = DetoxCircusEnvironment;
9 changes: 9 additions & 0 deletions detox/runners/jest-circus/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const DetoxCircusEnvironment = require('./environment');
const WorkerAssignReporterCircus = require('../jest/WorkerAssignReporterCircus');
const SpecReporterCircus = require('../jest/SpecReporterCircus');

module.exports = {
DetoxCircusEnvironment,
SpecReporter: SpecReporterCircus,
WorkerAssignReporter: WorkerAssignReporterCircus,
};
82 changes: 82 additions & 0 deletions detox/runners/jest-circus/listeners/DetoxCoreListener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const _ = require('lodash');
const {getFullTestName, hasTimedOut} = require('../../jest/utils');
const {
onRunDescribeStart,
onTestStart,
onHookFailure,
onTestFnFailure,
onTestDone,
onRunDescribeFinish,
} = require('../../integration').lifecycle;

class DetoxCoreListener {
constructor({ detox }) {
this._startedTests = new WeakSet();
this._testsFailedBeforeStart = new WeakSet();
this.detox = detox;
}

async run_describe_start({describeBlock: {name, children}}) {
if (children.length) {
await this.detox[onRunDescribeStart]({ name });
}
}

async run_describe_finish({describeBlock: {name, children}}) {
if (children.length) {
await this.detox[onRunDescribeFinish]({ name });
}
}

async test_start({ test }) {
if (!_.isEmpty(test.errors)) {
this._testsFailedBeforeStart.add(test);
}
}

async hook_start(_event, state) {
await this._onBeforeActualTestStart(state.currentlyRunningTest);
}

async hook_failure({ error, hook }) {
await this.detox[onHookFailure]({
error,
hook: hook.type,
});
}

async test_fn_start({ test }) {
await this._onBeforeActualTestStart(test);
}

async test_fn_failure({ error }) {
await this.detox[onTestFnFailure]({ error });
}

async _onBeforeActualTestStart(test) {
if (!test || test.status === 'skip' || this._startedTests.has(test) || this._testsFailedBeforeStart.has(test)) {
return;
}

this._startedTests.add(test);

await this.detox[onTestStart]({
title: test.name,
fullName: getFullTestName(test),
status: 'running',
});
}

async test_done({ test }) {
if (this._startedTests.has(test)) {
await this.detox[onTestDone]({
title: test.name,
fullName: getFullTestName(test),
status: test.errors.length ? 'failed' : 'passed',
timedOut: hasTimedOut(test)
});
}
}
}

module.exports = DetoxCoreListener;
21 changes: 21 additions & 0 deletions detox/runners/jest-circus/listeners/DetoxInitErrorListener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const _ = require('lodash');

class DetoxInitErrorListener {
setup(event, { unhandledErrors, rootDescribeBlock }) {
if (unhandledErrors.length > 0) {
rootDescribeBlock.mode = 'skip';
}
}

add_test(event, { currentDescribeBlock, rootDescribeBlock }) {
if (currentDescribeBlock === rootDescribeBlock && rootDescribeBlock.mode === 'skip') {
const currentTest = _.last(currentDescribeBlock.children);

if (currentTest) {
currentTest.mode = 'skip';
}
}
}
}

module.exports = DetoxInitErrorListener;
26 changes: 26 additions & 0 deletions detox/runners/jest-circus/utils/assertJestCircus26.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const fs = require('fs');
const path = require('path');

function assertJestCircus26(config) {
if (!/jest-circus/.test(config.testRunner)) {
throw new Error('Cannot run tests without "jest-circus" npm package, exiting.');
}

const circusPackageJson = path.join(path.dirname(config.testRunner), 'package.json');
if (!fs.existsSync(circusPackageJson)) {
throw new Error('Check that you have an installed copy of "jest-circus" npm package, exiting.');
}

const circusVersion = require(circusPackageJson).version || '';
const [major] = circusVersion.split('.');
if (major < 26) {
throw new Error(
`Cannot use older versions of "jest-circus", exiting.\n` +
`You have jest-circus@${circusVersion}. Update to ^26.0.0 or newer.`
);
}

return config;
}

module.exports = assertJestCircus26;
13 changes: 13 additions & 0 deletions detox/runners/jest-circus/utils/wrapErrorWithNoopLifecycle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const _ = require('lodash');
const lifecycleSymbols = require('../../integration').lifecycle;

function wrapErrorWithNoopLifecycle(rawError) {
const error = _.isError(rawError) ? rawError : new Error(rawError);
for (const symbol of Object.values(lifecycleSymbols)) {
error[symbol] = _.noop;
}

return error;
}

module.exports = wrapErrorWithNoopLifecycle;
Loading