mirror of https://github.com/lukechilds/ava.git
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.
373 lines
8.1 KiB
373 lines
8.1 KiB
'use strict';
|
|
const inspect = require('util').inspect;
|
|
const isGeneratorFn = require('is-generator-fn');
|
|
const maxTimeout = require('max-timeout');
|
|
const Promise = require('bluebird');
|
|
const fnName = require('fn-name');
|
|
const co = require('co-with-promise');
|
|
const observableToPromise = require('observable-to-promise');
|
|
const isPromise = require('is-promise');
|
|
const isObservable = require('is-observable');
|
|
const plur = require('plur');
|
|
const assert = require('./assert');
|
|
const enhanceAssert = require('./enhance-assert');
|
|
const globals = require('./globals');
|
|
const throwsHelper = require('./throws-helper');
|
|
|
|
const formatter = enhanceAssert.formatter();
|
|
|
|
class SkipApi {
|
|
constructor(test) {
|
|
this._test = test;
|
|
}
|
|
}
|
|
|
|
function skipFn() {
|
|
return this._test._assert(null);
|
|
}
|
|
|
|
Object.keys(assert).forEach(el => {
|
|
SkipApi.prototype[el] = skipFn;
|
|
});
|
|
|
|
class PublicApi {
|
|
constructor(test) {
|
|
this._test = test;
|
|
this.skip = new SkipApi(test);
|
|
}
|
|
plan(ct) {
|
|
const limitBefore = Error.stackTraceLimit;
|
|
Error.stackTraceLimit = 1;
|
|
const obj = {};
|
|
Error.captureStackTrace(obj, this.plan);
|
|
Error.stackTraceLimit = limitBefore;
|
|
this._test.plan(ct, obj.stack);
|
|
}
|
|
get context() {
|
|
const contextRef = this._test.contextRef;
|
|
return contextRef && contextRef.context;
|
|
}
|
|
set context(context) {
|
|
const 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 onAssertionEvent(event) {
|
|
if (event.assertionThrew) {
|
|
if (event.powerAssertContext) {
|
|
event.error.message = formatter(event.powerAssertContext);
|
|
if (event.originalMessage) {
|
|
event.error.message = event.originalMessage + ' ' + event.error.message;
|
|
}
|
|
}
|
|
this._test._setAssertError(event.error);
|
|
this._test._assert(null);
|
|
return null;
|
|
}
|
|
|
|
let ret = event.returnValue;
|
|
|
|
if (isObservable(ret)) {
|
|
ret = observableToPromise(ret);
|
|
}
|
|
|
|
if (isPromise(ret)) {
|
|
const promise = ret.then(null, err => {
|
|
err.originalMessage = event.originalMessage;
|
|
throw err;
|
|
});
|
|
|
|
this._test._assert(promise);
|
|
|
|
return promise;
|
|
}
|
|
|
|
this._test._assert(null);
|
|
|
|
return ret;
|
|
}
|
|
|
|
Object.assign(PublicApi.prototype, enhanceAssert({
|
|
assert,
|
|
onSuccess: onAssertionEvent,
|
|
onError: onAssertionEvent
|
|
}));
|
|
|
|
// Getters
|
|
[
|
|
'assertCount',
|
|
'title',
|
|
'end'
|
|
]
|
|
.forEach(name => {
|
|
Object.defineProperty(PublicApi.prototype, name, {
|
|
get() {
|
|
return this._test[name];
|
|
}
|
|
});
|
|
});
|
|
|
|
Object.defineProperty(PublicApi.prototype, 'context', {enumerable: true});
|
|
|
|
class Test {
|
|
constructor(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;
|
|
this.threwSync = false;
|
|
|
|
// 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]';
|
|
}
|
|
}
|
|
get assertCount() {
|
|
return this.assertions.length;
|
|
}
|
|
_assert(promise) {
|
|
if (isPromise(promise)) {
|
|
this.sync = false;
|
|
}
|
|
|
|
this.assertions.push(promise);
|
|
}
|
|
_setAssertError(err) {
|
|
throwsHelper(err);
|
|
|
|
if (this.assertError !== undefined) {
|
|
return;
|
|
}
|
|
|
|
this.assertError = err;
|
|
}
|
|
plan(count, planStack) {
|
|
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 = planStack;
|
|
}
|
|
_run() {
|
|
let ret;
|
|
|
|
try {
|
|
ret = this.fn(this._publicApi());
|
|
} catch (err) {
|
|
this.threwSync = true;
|
|
|
|
if (err instanceof Error) {
|
|
this._setAssertError(err);
|
|
} else {
|
|
this._setAssertError(new assert.AssertionError({
|
|
actual: err,
|
|
message: `Non-error thrown with value: ${inspect(err, {depth: null})}`,
|
|
operator: 'catch'
|
|
}));
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
promise() {
|
|
if (!this._promise) {
|
|
this._promise = {};
|
|
|
|
this._promise.promise = new Promise((resolve, reject) => {
|
|
this._promise.resolve = resolve;
|
|
this._promise.reject = reject;
|
|
}).tap(result => {
|
|
if (this.report) {
|
|
this.report(result);
|
|
}
|
|
});
|
|
}
|
|
|
|
return this._promise;
|
|
}
|
|
run() {
|
|
if (this.metadata.callback) {
|
|
this.sync = false;
|
|
}
|
|
|
|
this._timeStart = globals.now();
|
|
|
|
// Wait until all assertions are complete
|
|
this._timeout = globals.setTimeout(() => {}, maxTimeout);
|
|
|
|
let ret = this._run();
|
|
let asyncType = 'promises';
|
|
|
|
if (isObservable(ret)) {
|
|
asyncType = 'observables';
|
|
ret = observableToPromise(ret);
|
|
}
|
|
|
|
if (isPromise(ret)) {
|
|
this.sync = false;
|
|
|
|
if (this.metadata.callback) {
|
|
this._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(
|
|
() => {
|
|
this.exit();
|
|
},
|
|
err => {
|
|
if (!(err instanceof Error)) {
|
|
err = new assert.AssertionError({
|
|
actual: err,
|
|
message: `Promise rejected with: ${inspect(err, {depth: null})}`,
|
|
operator: 'promise'
|
|
});
|
|
}
|
|
|
|
this._setAssertError(err);
|
|
this.exit();
|
|
});
|
|
|
|
return this.promise().promise;
|
|
}
|
|
|
|
if (this.metadata.callback && !this.threwSync) {
|
|
return this.promise().promise;
|
|
}
|
|
|
|
return this.exit();
|
|
}
|
|
_result() {
|
|
let reason = this.assertError;
|
|
let passed = reason === undefined;
|
|
|
|
if (this.metadata.failing) {
|
|
passed = !passed;
|
|
|
|
if (passed) {
|
|
reason = undefined;
|
|
} else {
|
|
reason = new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing');
|
|
}
|
|
}
|
|
|
|
return {
|
|
passed,
|
|
result: this,
|
|
reason
|
|
};
|
|
}
|
|
get end() {
|
|
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)`');
|
|
}
|
|
_end(err) {
|
|
if (err) {
|
|
if (!(err instanceof Error)) {
|
|
err = new assert.AssertionError({
|
|
actual: err,
|
|
message: 'Callback called with an error: ' + inspect(err, {depth: null}),
|
|
operator: 'callback'
|
|
});
|
|
}
|
|
|
|
this._setAssertError(err);
|
|
this.exit();
|
|
|
|
return;
|
|
}
|
|
|
|
if (this.endCalled) {
|
|
this._setAssertError(new Error('.end() called more than once'));
|
|
return;
|
|
}
|
|
|
|
this.endCalled = true;
|
|
this.exit();
|
|
}
|
|
_checkPlanCount() {
|
|
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: `Planned for ${this.planCount} ${plur('assertion', this.planCount)}, but got ${this.assertions.length}.`,
|
|
operator: 'plan'
|
|
}));
|
|
|
|
this.assertError.stack = this.planStack;
|
|
}
|
|
}
|
|
exit() {
|
|
this._checkPlanCount();
|
|
|
|
if (this.sync || this.threwSync) {
|
|
this.duration = globals.now() - this._timeStart;
|
|
globals.clearTimeout(this._timeout);
|
|
|
|
const result = this._result();
|
|
|
|
if (this.report) {
|
|
this.report(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Promise.all(this.assertions)
|
|
.catch(err => {
|
|
this._setAssertError(err);
|
|
})
|
|
.finally(() => {
|
|
// Calculate total time spent in test
|
|
this.duration = globals.now() - this._timeStart;
|
|
|
|
// Stop infinite timer
|
|
globals.clearTimeout(this._timeout);
|
|
|
|
this._checkPlanCount();
|
|
|
|
this.promise().resolve(this._result());
|
|
});
|
|
|
|
return this.promise().promise;
|
|
}
|
|
_publicApi() {
|
|
return new PublicApi(this);
|
|
}
|
|
}
|
|
|
|
module.exports = Test;
|
|
|