256 lines
7.8 KiB
JavaScript
256 lines
7.8 KiB
JavaScript
//
|
|
// Vows.js - asynchronous event-based BDD for node.js
|
|
//
|
|
// usage:
|
|
//
|
|
// var vows = require('vows');
|
|
//
|
|
// vows.describe('Deep Thought').addBatch({
|
|
// "An instance of DeepThought": {
|
|
// topic: new DeepThought,
|
|
//
|
|
// "should know the answer to the ultimate question of life": function (deepThought) {
|
|
// assert.equal (deepThought.question('what is the answer to the universe?'), 42);
|
|
// }
|
|
// }
|
|
// }).run();
|
|
//
|
|
var util = require('util'),
|
|
path = require('path'),
|
|
events = require('events'),
|
|
vows = exports;
|
|
|
|
// Options
|
|
vows.options = {
|
|
Emitter: events.EventEmitter,
|
|
reporter: require('./vows/reporters/dot-matrix'),
|
|
matcher: /.*/,
|
|
error: true // Handle "error" event
|
|
};
|
|
|
|
vows.__defineGetter__('reporter', function () {
|
|
return vows.options.reporter;
|
|
});
|
|
|
|
var stylize = require('./vows/console').stylize;
|
|
var console = vows.console = require('./vows/console');
|
|
|
|
vows.inspect = require('./vows/console').inspect;
|
|
vows.prepare = require('./vows/extras').prepare;
|
|
vows.tryEnd = require('./vows/suite').tryEnd;
|
|
|
|
//
|
|
// Assertion Macros & Extensions
|
|
//
|
|
require('./assert/error');
|
|
require('./assert/macros');
|
|
|
|
//
|
|
// Suite constructor
|
|
//
|
|
var Suite = require('./vows/suite').Suite;
|
|
|
|
//
|
|
// This function gets added to events.EventEmitter.prototype, by default.
|
|
// It's essentially a wrapper around `on`, which adds all the specification
|
|
// goodness.
|
|
//
|
|
function addVow(vow) {
|
|
var batch = vow.batch,
|
|
event = vow.binding.context.event || 'success',
|
|
self = this;
|
|
|
|
batch.total ++;
|
|
batch.vows.push(vow);
|
|
|
|
// always set a listener on the event
|
|
this.on(event, function () {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
// If the vow is a sub-event then we know it is an
|
|
// emitted event. So I don't muck with the arguments
|
|
// However the legacy behavior:
|
|
// If the callback is expecting two or more arguments,
|
|
// pass the error as the first (null) and the result after.
|
|
if (!(this.ctx && this.ctx.isEvent) &&
|
|
vow.callback.length >= 2 && batch.suite.options.error) {
|
|
args.unshift(null);
|
|
}
|
|
runTest(args, this.ctx);
|
|
vows.tryEnd(batch);
|
|
});
|
|
|
|
if (event !== 'error') {
|
|
this.on("error", function (err) {
|
|
if (vow.callback.length >= 2 || !batch.suite.options.error) {
|
|
runTest(arguments, this.ctx);
|
|
} else {
|
|
output('errored', { type: 'promise', error: err.stack ||
|
|
err.message || JSON.stringify(err) });
|
|
}
|
|
vows.tryEnd(batch);
|
|
});
|
|
}
|
|
|
|
// in case an event fired before we could listen
|
|
if (this._vowsEmitedEvents &&
|
|
this._vowsEmitedEvents.hasOwnProperty(event)) {
|
|
// make sure no one is messing with me
|
|
if (Array.isArray(this._vowsEmitedEvents[event])) {
|
|
// I don't think I need to optimize for one event,
|
|
// I think it is more important to make sure I check the vow n times
|
|
self._vowsEmitedEvents[event].forEach(function(args) {
|
|
runTest(args, self.ctx);
|
|
});
|
|
} else {
|
|
// initial conditions problem
|
|
throw new Error('_vowsEmitedEvents[' + event + '] is not an Array')
|
|
}
|
|
vows.tryEnd(batch);
|
|
}
|
|
|
|
return this;
|
|
|
|
function runTest(args, ctx) {
|
|
if (vow.callback instanceof String) {
|
|
return output('pending');
|
|
}
|
|
|
|
if (vow.binding.context.isEvent && vow.binding.context.after) {
|
|
var after = vow.binding.context.after;
|
|
// only need to check order. I won't get here if the after event
|
|
// has never been emitted
|
|
if (self._vowsEmitedEventsOrder.indexOf(after) >
|
|
self._vowsEmitedEventsOrder.indexOf(event)) {
|
|
output('broken', event + ' emitted before ' + after);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Run the test, and try to catch `AssertionError`s and other exceptions;
|
|
// increment counters accordingly.
|
|
try {
|
|
vow.callback.apply(ctx === global || !ctx ? vow.binding : ctx, args);
|
|
output('honored');
|
|
} catch (e) {
|
|
if (e.name && e.name.match(/AssertionError/)) {
|
|
output('broken', e.toString().replace(/\`/g, '`'));
|
|
} else {
|
|
output('errored', e.stack || e.message || e);
|
|
}
|
|
}
|
|
}
|
|
|
|
function output(status, exception) {
|
|
batch[status] ++;
|
|
vow.status = status;
|
|
|
|
if (vow.context && batch.lastContext !== vow.context) {
|
|
batch.lastContext = vow.context;
|
|
batch.suite.report(['context', vow.context]);
|
|
}
|
|
batch.suite.report(['vow', {
|
|
title: vow.description,
|
|
context: vow.context,
|
|
status: status,
|
|
exception: exception || null
|
|
}]);
|
|
}
|
|
};
|
|
|
|
//
|
|
// On exit, check that all promises have been fired.
|
|
// If not, report an error message.
|
|
//
|
|
process.on('exit', function () {
|
|
var results = { honored: 0, broken: 0, errored: 0, pending: 0, total: 0 },
|
|
failure;
|
|
|
|
vows.suites.forEach(function (s) {
|
|
if ((s.results.total > 0) && (s.results.time === null)) {
|
|
s.reporter.print('\n\n');
|
|
s.reporter.report(['error', { error: "Asynchronous Error", suite: s }]);
|
|
}
|
|
s.batches.forEach(function (b) {
|
|
var unFired = [];
|
|
|
|
b.vows.forEach(function (vow) {
|
|
if (! vow.status) {
|
|
if (unFired.indexOf(vow.context) === -1) {
|
|
unFired.push(vow.context);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (unFired.length > 0) { util.print('\n') }
|
|
|
|
unFired.forEach(function (title) {
|
|
s.reporter.report(['error', {
|
|
error: "callback not fired",
|
|
context: title,
|
|
batch: b,
|
|
suite: s
|
|
}]);
|
|
});
|
|
|
|
if (b.status === 'begin') {
|
|
failure = true;
|
|
results.errored ++;
|
|
results.total ++;
|
|
}
|
|
Object.keys(results).forEach(function (k) { results[k] += b[k] });
|
|
});
|
|
});
|
|
if (failure) {
|
|
util.puts(console.result(results));
|
|
}
|
|
});
|
|
|
|
vows.suites = [];
|
|
|
|
// We need the old emit function so we can hook it
|
|
// and do magic to deal with events that have fired
|
|
var oldEmit = vows.options.Emitter.prototype.emit;
|
|
|
|
//
|
|
// Create a new test suite
|
|
//
|
|
vows.describe = function (subject) {
|
|
var suite = new(Suite)(subject);
|
|
|
|
this.options.Emitter.prototype.addVow = addVow;
|
|
// just in case someone emit's before I get to it
|
|
this.options.Emitter.prototype.emit = function (event) {
|
|
this._vowsEmitedEvents = this._vowsEmitedEvents || {};
|
|
this._vowsEmitedEventsOrder = this._vowsEmitedEventsOrder || [];
|
|
// slice off the event
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
// if multiple events are fired, add or push
|
|
if (this._vowsEmitedEvents.hasOwnProperty(event)) {
|
|
this._vowsEmitedEvents[event].push(args);
|
|
} else {
|
|
this._vowsEmitedEvents[event] = [args];
|
|
}
|
|
|
|
// push the event onto a stack so I have an order
|
|
this._vowsEmitedEventsOrder.push(event);
|
|
return oldEmit.apply(this, arguments);
|
|
}
|
|
this.suites.push(suite);
|
|
|
|
//
|
|
// Add any additional arguments as batches if they're present
|
|
//
|
|
if (arguments.length > 1) {
|
|
for (var i = 1, l = arguments.length; i < l; ++i) {
|
|
suite.addBatch(arguments[i]);
|
|
}
|
|
}
|
|
|
|
return suite;
|
|
};
|
|
|
|
|
|
vows.version = JSON.parse(require('fs')
|
|
.readFileSync(path.join(__dirname, '..', 'package.json')))
|
|
.version
|