ava
Version:
Futuristic test runner 🚀
203 lines (161 loc) • 4.51 kB
JavaScript
'use strict';
var isGeneratorFn = require('is-generator-fn');
var Promise = require('bluebird');
var setImmediate = require('set-immediate-shim');
var fnName = require('fn-name');
var co = require('co-with-promise');
var maxTimeout = require('max-timeout');
var observableSymbol = require('./symbol')('observable');
var assert = require('./assert');
function noop() {}
function Test(title, fn) {
if (!(this instanceof Test)) {
return new Test(title, fn);
}
if (typeof title === 'function') {
fn = title;
title = null;
}
assert.is(typeof fn, 'function', 'you must provide a callback');
this.title = title || fnName(fn) || '[anonymous]';
this.fn = isGeneratorFn(fn) ? co.wrap(fn) : fn;
this.assertCount = 0;
this.planCount = null;
this.duration = null;
this.context = {};
// test type, can be: test, hook, eachHook
this.type = 'test';
// store the time point before test execution
// to calculate the total time spent in test
this._timeStart = null;
// workaround for Babel giving anonymous functions a name
if (this.title === 'callee$0$0') {
this.title = '[anonymous]';
}
Object.keys(Test.prototype).forEach(function (key) {
this[key] = this[key].bind(this);
}, this);
}
module.exports = Test;
Test.prototype._assert = function () {
this.assertCount++;
if (this.assertCount === this.planCount) {
setImmediate(this.exit.bind(this));
}
};
Object.keys(assert).forEach(function (el) {
Test.prototype[el] = function () {
var self = this;
try {
var fn = assert[el].apply(assert, arguments);
if (fn && fn[observableSymbol]) {
fn = fn[observableSymbol]().forEach(noop);
}
if (fn && fn.then) {
return fn
.then(function () {
self._assert();
})
.catch(function (err) {
self.assertError = err;
self._assert();
});
}
this._assert();
} catch (err) {
this.assertError = err;
this._assert();
}
};
});
// Workaround for power-assert
// `t` must be capturable for decorated assert output
Test.prototype._capt = assert._capt;
Test.prototype._expr = assert._expr;
Test.prototype.plan = function (count) {
if (typeof count !== 'number') {
throw new TypeError('Expected a number');
}
this.planCount = count;
// in case the `planCount` doesn't match `assertCount,
// we need the stack of this function to throw with a useful stack
this.planStack = new Error().stack;
};
Test.prototype.run = function () {
this.promise = {};
// TODO(vdemedes): refactor this to avoid storing the promise
return new Promise(function (resolve, reject) {
this.promise.resolve = resolve;
this.promise.reject = reject;
if (!this.fn) {
return this.exit();
}
this._timeStart = Date.now();
// wait until all assertions are complete
this._timeout = setTimeout(function () {}, maxTimeout);
try {
var ret = this.fn(this);
if (ret && ret[observableSymbol]) {
ret = ret[observableSymbol]().forEach(noop);
}
if (ret && typeof ret.then === 'function') {
ret
.then(function () {
if (!this.planCount || this.planCount === this.assertCount) {
this.exit();
}
}.bind(this))
.catch(function (err) {
this.assertError = new assert.AssertionError({
actual: err,
message: 'Promise rejected → ' + err,
operator: 'promise'
});
this.exit();
}.bind(this));
}
} catch (err) {
this.assertError = err;
this.exit();
}
}.bind(this));
};
Test.prototype.end = function (err) {
if (err) {
this.assertError = new assert.AssertionError({
actual: err,
message: 'Callback called with an error → ' + err,
operator: 'callback'
});
return this.exit();
}
if (this.endCalled) {
throw new Error('.end() called more than once');
}
this.endCalled = true;
this.exit();
};
Test.prototype.exit = function () {
// calculate total time spent in test
this.duration = Date.now() - this._timeStart;
// stop infinite timer
clearTimeout(this._timeout);
if (!this.assertError && this.planCount !== null && this.planCount !== this.assertCount) {
this.assertError = new assert.AssertionError({
actual: this.assertCount,
expected: this.planCount,
message: 'Assertion count does not match planned',
operator: 'plan'
});
this.assertError.stack = this.planStack;
}
if (!this.ended) {
this.ended = true;
setImmediate(function () {
if (this.assertError) {
return this.promise.reject(this.assertError);
}
this.promise.resolve(this);
}.bind(this));
}
};