You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

350 lines
7.4 KiB

'use strict';
var isGeneratorFn = require('is-generator-fn');
var maxTimeout = require('max-timeout');
var Promise = require('bluebird');
var fnName = require('fn-name');
var co = require('co-with-promise');
var observableToPromise = require('observable-to-promise');
var isPromise = require('is-promise');
var isObservable = require('is-observable');
var assert = require('./assert');
var enhanceAssert = require('./enhance-assert');
var globals = require('./globals');
function Test(title, fn, contextRef, report) {
if (!(this instanceof Test)) {
return new Test(title, fn, contextRef, report);
}
if (typeof title === 'function') {
contextRef = fn;
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.assertions = [];
this.planCount = null;
this.duration = null;
this.assertError = undefined;
this.sync = true;
this.contextRef = contextRef;
this.report = report;
// TODO(jamestalmage): make this an optional constructor arg instead of having Runner set it after the fact.
// metadata should just always exist, otherwise it requires a bunch of ugly checks all over the place.
this.metadata = {};
// 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]';
}
}
module.exports = Test;
Object.defineProperty(Test.prototype, 'assertCount', {
enumerable: true,
get: function () {
return this.assertions.length;
}
});
Test.prototype._assert = function (promise) {
if (isPromise(promise)) {
this.sync = false;
}
this.assertions.push(promise);
};
Test.prototype._setAssertError = function (err) {
if (this.assertError !== undefined) {
return;
}
if (err === undefined) {
err = 'undefined';
}
this.assertError = err;
};
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 () {
var ret;
try {
ret = this.fn(this._publicApi());
} catch (err) {
this._setAssertError(err);
}
return ret;
};
Test.prototype.promise = function () {
var self = this;
if (!this._promise) {
this._promise = {};
this._promise.promise = new Promise(function (resolve, reject) { // eslint-disable-line
self._promise.resolve = resolve;
self._promise.reject = reject;
}).tap(function (result) {
if (self.report) {
self.report(result);
}
});
}
return this._promise;
};
Test.prototype.run = function () {
if (this.metadata.callback) {
this.sync = false;
}
var self = this;
this._timeStart = globals.now();
// wait until all assertions are complete
this._timeout = globals.setTimeout(function () {}, maxTimeout);
var ret = this._run();
var asyncType = 'promises';
if (isObservable(ret)) {
asyncType = 'observables';
ret = observableToPromise(ret);
}
if (isPromise(ret)) {
this.sync = false;
if (this.metadata.callback) {
self._setAssertError(new Error('Do not return ' + asyncType + ' from tests declared via `test.cb(...)`, if you want to return a promise simply declare the test via `test(...)`'));
}
ret.then(
function () {
self.exit();
},
function (err) {
if (!(err instanceof Error)) {
err = new assert.AssertionError({
actual: err,
message: 'Promise rejected with "' + err + '"',
operator: 'promise'
});
}
self._setAssertError(err);
self.exit();
});
return this.promise().promise;
}
if (this.metadata.callback) {
return this.promise().promise;
}
return this.exit();
};
Test.prototype._result = function () {
var passed = this.assertError === undefined;
return {passed: passed, result: this, reason: this.assertError};
};
Object.defineProperty(Test.prototype, 'end', {
get: function () {
if (this.metadata.callback) {
return this._end.bind(this);
}
throw new Error('t.end is not supported in this context. To use t.end as a callback, you must use "callback mode" via `test.cb(testName, fn)` ');
}
});
Test.prototype._end = function (err) {
if (err) {
this._setAssertError(new assert.AssertionError({
actual: err,
message: 'Callback called with an error → ' + err,
operator: 'callback'
}));
this.exit();
return;
}
if (this.endCalled) {
throw new Error('.end() called more than once');
}
this.endCalled = true;
this.exit();
};
Test.prototype._checkPlanCount = function () {
if (this.assertError === undefined && this.planCount !== null && this.planCount !== this.assertions.length) {
this._setAssertError(new assert.AssertionError({
actual: this.assertions.length,
expected: this.planCount,
message: 'Assertion count does not match planned',
operator: 'plan'
}));
this.assertError.stack = this.planStack;
}
};
Test.prototype.exit = function () {
var self = this;
this._checkPlanCount();
if (this.sync) {
self.duration = globals.now() - self._timeStart;
globals.clearTimeout(self._timeout);
var result = this._result();
if (this.report) {
this.report(result);
}
return result;
}
Promise.all(this.assertions)
.catch(function (err) {
self._setAssertError(err);
})
.finally(function () {
// calculate total time spent in test
self.duration = globals.now() - self._timeStart;
// stop infinite timer
globals.clearTimeout(self._timeout);
self._checkPlanCount();
self.promise().resolve(self._result());
});
return self.promise().promise;
};
Test.prototype._publicApi = function () {
return new PublicApi(this);
};
function PublicApi(test) {
this._test = test;
this.plan = test.plan.bind(test);
this.skip = new SkipApi(test);
}
function onAssertionEvent(event) {
var promise = null;
if (event.assertionThrew) {
event.error.powerAssertContext = event.powerAssertContext;
event.error.originalMessage = event.originalMessage;
this._test._setAssertError(event.error);
} else {
var ret = event.returnValue;
if (isObservable(ret)) {
ret = observableToPromise(ret);
}
if (isPromise(ret)) {
promise = ret
.then(null, function (err) {
err.originalMessage = event.originalMessage;
throw err;
});
}
}
this._test._assert(promise);
return promise;
}
PublicApi.prototype = enhanceAssert({
assert: assert,
onSuccess: onAssertionEvent,
onError: onAssertionEvent
});
// Getters
[
'assertCount',
'title',
'end'
]
.forEach(function (name) {
Object.defineProperty(PublicApi.prototype, name, {
enumerable: false,
get: function () {
return this._test[name];
}
});
});
// Get / Set
Object.defineProperty(PublicApi.prototype, 'context', {
enumerable: true,
get: function () {
var contextRef = this._test.contextRef;
return contextRef && contextRef.context;
},
set: function (context) {
var contextRef = this._test.contextRef;
if (!contextRef) {
this._test._setAssertError(new Error('t.context is not available in ' + this._test.metadata.type + ' tests'));
return;
}
contextRef.context = context;
}
});
function skipFn() {
return this._test._assert(null);
}
function SkipApi(test) {
this._test = test;
}
Object.keys(assert).forEach(function (el) {
SkipApi.prototype[el] = skipFn;
});