Skip to content

Commit

Permalink
Merge pull request #128 from gemini-testing/fix-hooks-handling
Browse files Browse the repository at this point in the history
fix: add correct error handling in before and beforeEach hooks
  • Loading branch information
tormozz48 authored Apr 22, 2017
2 parents e84a5f1 + a925eab commit 72bc643
Show file tree
Hide file tree
Showing 6 changed files with 641 additions and 257 deletions.
56 changes: 48 additions & 8 deletions lib/runner/mocha-runner/mocha-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ module.exports = class MochaAdapter {
this._currentRunnable = null;

this._injectBrowser();
this._injectBeforeHookErrorHandling();
this._injectBeforeEachHookErrorHandling();
this._injectPretestFailVerification();
this._injectRunnableSpy();
this._injectSkip();

Expand Down Expand Up @@ -154,21 +157,58 @@ module.exports = class MochaAdapter {

this._addEventHandler(
['beforeAll', 'beforeEach', 'test', 'afterEach', 'afterAll'],
(runnable) => {
const baseFn = runnable.fn;
if (!baseFn) {
return;
}

this._overrideRunnableFn((runnable, baseFn) => {
const _this = this;
runnable.fn = function() {
return function() {
_this._currentRunnable = _.extend(runnable, {browserId});
return baseFn.apply(this, arguments);
};
}
})
);
}

_injectBeforeHookErrorHandling() {
this._injectHookErrorHandling('beforeAll', (error, hook) => {
hook.parent.fail = error;
hook.parent.eachTest((test) => test.fail = error);
});
}

_injectBeforeEachHookErrorHandling() {
this._injectHookErrorHandling('beforeEach', (error, hook) => {
hook.ctx.currentTest.fail = error;
});
}

_injectHookErrorHandling(event, onError) {
this._addEventHandler(event, this._overrideRunnableFn((hook, baseFn) => {
return function() {
return hook.parent.fail
? q.reject(hook.parent.fail)
: q(baseFn).apply(this, arguments).catch((error) => onError(error, hook));
};
}));
}

_injectPretestFailVerification() {
this._addEventHandler('test', this._overrideRunnableFn((test, baseFn) => {
return function() {
return test.fail
? q.reject(test.fail)
: baseFn.apply(this, arguments);
};
}));
}

_overrideRunnableFn(overrideFn) {
return (runnable) => {
const baseFn = runnable.fn;
if (baseFn) {
runnable.fn = overrideFn(runnable, baseFn);
}
};
}

// Set recursive handler for events triggered by mocha while parsing test file
_addEventHandler(events, cb) {
events = [].concat(events);
Expand Down
45 changes: 45 additions & 0 deletions test/lib/_mocha/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict';

const Suite = require('./suite');
const Test = require('./test');

class Mocha {
constructor(options) {
this._suite = Suite.create();
this.constructor._instance = this;

this.addFile = sinon.stub();
this.loadFiles = sinon.stub();
this.reporter = sinon.stub();
this.fullTrace = sinon.stub();

this.constructorArgs = options;
}

static get lastInstance() {
return this._instance;
}

static get Test() {
return Test;
}

static get Suite() {
return Suite;
}

run(cb) {
return this.suite.run().then(cb);
}

get suite() {
return this._suite;
}

updateSuiteTree(cb) {
this._suite = cb(this._suite);
return this;
}
}

module.exports = Mocha;
22 changes: 22 additions & 0 deletions test/lib/_mocha/runnable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

module.exports = class Runnable {
constructor(parent, options) {
this.title = options.title;
this.fn = options.fn;
this.parent = parent;
this.ctx = {};
}

static create(parent, options) {
return new this(parent, options);
}

fullTitle() {
return `${this.parent.title} ${this.title}`;
}

run() {
return this.fn();
}
};
174 changes: 174 additions & 0 deletions test/lib/_mocha/suite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
'use strict';

const q = require('q');
const EventEmitter = require('events').EventEmitter;
const Runnable = require('./runnable');
const Test = require('./test');

const EVENTS = {
TEST_BEGIN: 'test',
FAIL: 'fail'
};

module.exports = class Suite extends EventEmitter {
constructor(parent) {
super();

this.parent = parent;
this.title = 'suite-title';

this._beforeAll = [];
this._beforeEach = [];
this._afterEach = [];
this._afterAll = [];
this._tests = [];
this._suites = [];

this.ctx = {};

this.enableTimeouts = sinon.stub().returns(true);
}

static create(parent) {
return new this(parent);
}

get tests() {
return this._tests;
}

get suites() {
return this._suites;
}

get beforeAllHooks() {
return this._beforeAll;
}

get beforeEachHooks() {
return this._beforeEach;
}

get afterEachHooks() {
return this._afterEach;
}

get afterAllHooks() {
return this._afterAll;
}

fullTitle() {
return `${this.parent.title} ${this.title}`;
}

beforeAll(fn) {
return this._createHook({
title: 'before all',
collection: this._beforeAll,
event: 'beforeAll',
fn
});
}

beforeEach(fn) {
return this._createHook({
title: 'before each',
collection: this._beforeEach,
event: 'beforeEach',
fn
});
}

afterEach(fn) {
return this._createHook({
title: 'after each',
collection: this._afterEach,
event: 'afterEach',
fn
});
}

afterAll(fn) {
return this._createHook({
title: 'after all',
collection: this._afterAll,
event: 'afterAll',
fn
});
}

_createHook(options) {
const hook = Runnable.create(this, options);
options.collection.push(hook);
this.emit(options.event, hook);
return this;
}

addTest(options) {
let test;

if (options instanceof Test) {
test = options;
test.parent = this;
} else {
test = Test.create(this, options);
}

this.tests.push(test);
this.emit('test', test);

return this;
}

addSuite(suite) {
suite.parent = this;
this.suites.push(suite);
this.emit('suite', suite);
return this;
}

onTestBegin(cb) {
this.on(EVENTS.TEST_BEGIN, cb);
return this;
}

onFail(cb) {
this.on(EVENTS.FAIL, cb);
return this;
}

eachTest(fn) {
this.tests.forEach(fn);
}

run() {
return q()
.then(this._execRunnables(this.beforeAllHooks))
.then(() => this.tests.reduce((acc, test) => {
return acc
.then(() => {
const setContextToHook = (hook) => hook.ctx.currentTest = test;

this.beforeEachHooks.forEach(setContextToHook);
this.afterEachHooks.forEach(setContextToHook);
})
.then(this._execRunnables(this.beforeEachHooks))
.then(() => {
this.emit(EVENTS.TEST_BEGIN, test);
return test.run();
})
.catch((error) => this.emit(EVENTS.FAIL, {error, test}))
.then(this._execRunnables(this.afterEachHooks));
}, q()))
.then(this._execRunnables(this.suites, []))
.then(this._execRunnables(this.afterAllHooks));
}

_execRunnables(runnables) {
return () => runnables.reduce((acc, runnable) => {
return acc
.then(() => runnable.run())
.catch((error) => this.emit(EVENTS.FAIL, {error, runnable}));
}, q());
}
};
17 changes: 17 additions & 0 deletions test/lib/_mocha/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

const _ = require('lodash');
const Runnable = require('./runnable');

module.exports = class Test extends Runnable {
constructor(parent, options) {
options = options || {};

super(parent, options);

this.title = options.title || 'some-test';
this.fn = options.fn || _.noop;
this.file = options.file || null;
this.pending = options.skipped || false;
}
};
Loading

0 comments on commit 72bc643

Please sign in to comment.