238aad2e06
Allow for a customized junitTemplate using options.junit.template.
390 lines
11 KiB
JavaScript
390 lines
11 KiB
JavaScript
/*
|
|
* grunt-contrib-jasmine
|
|
* http://gruntjs.com/
|
|
*
|
|
* Licensed under the MIT license.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
module.exports = function(grunt) {
|
|
|
|
// node api
|
|
var fs = require('fs'),
|
|
path = require('path');
|
|
|
|
// npm lib
|
|
var phantomjs = require('grunt-lib-phantomjs').init(grunt),
|
|
chalk = require('chalk'),
|
|
_ = require('lodash');
|
|
|
|
// local lib
|
|
var jasmine = require('./lib/jasmine').init(grunt, phantomjs);
|
|
|
|
var junitTemplate = __dirname + '/jasmine/templates/JUnit.tmpl';
|
|
|
|
var status = {};
|
|
|
|
var summary = [];
|
|
|
|
var symbols = {
|
|
short : {
|
|
check : '.',
|
|
error : 'X',
|
|
splat : '*'
|
|
},
|
|
full : {
|
|
check : '✓',
|
|
error : 'X',
|
|
splat : '*'
|
|
}
|
|
};
|
|
|
|
//With node.js on Windows: use symbols available in terminal default fonts
|
|
//https://github.com/visionmedia/mocha/pull/641
|
|
if (process && process.platform === 'win32') {
|
|
symbols = {
|
|
short : {
|
|
check : '.',
|
|
error : '\u00D7',
|
|
splat : '*'
|
|
},
|
|
full : {
|
|
check : '\u221A',
|
|
error : '\u00D7',
|
|
splat : '*'
|
|
}
|
|
};
|
|
}
|
|
|
|
grunt.registerMultiTask('jasmine', 'Run jasmine specs headlessly through PhantomJS.', function() {
|
|
|
|
// Merge task-specific options with these defaults.
|
|
var options = this.options({
|
|
version : '2.0.0',
|
|
timeout : 10000,
|
|
styles : [],
|
|
specs : [],
|
|
helpers : [],
|
|
vendor : [],
|
|
outfile : '_SpecRunner.html',
|
|
host : '',
|
|
template : __dirname + '/jasmine/templates/DefaultRunner.tmpl',
|
|
templateOptions : {},
|
|
junit : {},
|
|
ignoreEmpty: grunt.option('force') === true,
|
|
display: 'full',
|
|
summary: false,
|
|
});
|
|
|
|
if (grunt.option('debug')) {
|
|
grunt.log.debug(options);
|
|
}
|
|
|
|
setup(options);
|
|
|
|
// The filter returned no spec files so skip phantom.
|
|
if (!jasmine.buildSpecrunner(this.filesSrc, options)) {
|
|
return removePhantomListeners();
|
|
}
|
|
|
|
// If we're just building (e.g. for web), skip phantom.
|
|
if (this.flags.build) return;
|
|
|
|
var done = this.async();
|
|
phantomRunner(options, function(err, status) {
|
|
var success = !err && status.failed === 0;
|
|
|
|
if (err) {
|
|
grunt.log.error(err);
|
|
}
|
|
if (status.failed === 0) {
|
|
grunt.log.ok('0 failures');
|
|
} else {
|
|
grunt.log.error(status.failed + ' failures');
|
|
}
|
|
|
|
teardown(options, function() {
|
|
done(success);
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
function phantomRunner(options, cb){
|
|
var file = options.outfile;
|
|
|
|
if (options.host) {
|
|
if (!(/\/$/).test(options.host)) options.host = options.host + '/';
|
|
file = options.host + options.outfile;
|
|
}
|
|
|
|
grunt.verbose.subhead('Testing jasmine specs via phantom').or.writeln('Testing jasmine specs via PhantomJS');
|
|
grunt.log.writeln('');
|
|
|
|
phantomjs.spawn(file, {
|
|
failCode : 90,
|
|
options : options,
|
|
done : function(err){
|
|
cb(err,status);
|
|
}
|
|
});
|
|
}
|
|
|
|
function teardown(options, cb) {
|
|
removePhantomListeners();
|
|
|
|
if (!options.keepRunner && fs.statSync(options.outfile).isFile()) {
|
|
fs.unlink(options.outfile);
|
|
}
|
|
|
|
if (!options.keepRunner) {
|
|
jasmine.cleanTemp(cb);
|
|
} else {
|
|
cb();
|
|
}
|
|
}
|
|
|
|
function removePhantomListeners() {
|
|
phantomjs.removeAllListeners();
|
|
phantomjs.listenersAny().length = 0;
|
|
}
|
|
|
|
function setup(options) {
|
|
var indentLevel = 1,
|
|
tabstop = 2,
|
|
thisRun = {},
|
|
suites = {},
|
|
currentSuite;
|
|
|
|
status = {
|
|
failed : 0
|
|
};
|
|
|
|
function indent(times) {
|
|
return new Array(+times * tabstop).join(' ');
|
|
}
|
|
|
|
phantomjs.on('fail.timeout', function() {
|
|
grunt.log.writeln();
|
|
grunt.warn('PhantomJS timed out, possibly due to an unfinished async spec.', 90);
|
|
});
|
|
|
|
phantomjs.on('console', function(msg) {
|
|
thisRun.cleanConsole = false;
|
|
if(options.display === 'full') {
|
|
grunt.log.writeln('\n' + chalk.yellow('log: ') + msg);
|
|
}
|
|
});
|
|
|
|
phantomjs.on('error.onError', function(string, trace){
|
|
if (trace && trace.length) {
|
|
grunt.log.error(chalk.red(string) + ' at ');
|
|
trace.forEach(function(line) {
|
|
var file = line.file.replace(/^file:/,'');
|
|
var message = grunt.util._('%s:%d %s').sprintf(path.relative('.',file), line.line, line.function);
|
|
grunt.log.error(chalk.red(message));
|
|
});
|
|
} else {
|
|
grunt.log.error("Error caught from PhantomJS. More info can be found by opening the Spec Runner in a browser.");
|
|
grunt.warn(string);
|
|
}
|
|
});
|
|
|
|
phantomjs.onAny(function() {
|
|
var args = [this.event].concat(grunt.util.toArray(arguments));
|
|
grunt.event.emit.apply(grunt.event, args);
|
|
});
|
|
|
|
phantomjs.on('jasmine.jasmineStarted', function() {
|
|
grunt.verbose.writeln('Jasmine Runner Starting...');
|
|
thisRun.startTime = (new Date()).getTime();
|
|
thisRun.executedSpecs = 0;
|
|
thisRun.passedSpecs = 0;
|
|
thisRun.failedSpecs = 0;
|
|
thisRun.skippedSpecs = 0;
|
|
});
|
|
|
|
phantomjs.on('jasmine.suiteStarted', function(suiteMetaData) {
|
|
currentSuite = suiteMetaData.id;
|
|
suites[currentSuite] = {
|
|
name : suiteMetaData.fullName,
|
|
timestamp : new Date(suiteMetaData.startTime),
|
|
errors : 0,
|
|
tests : 0,
|
|
failures : 0,
|
|
testcases : []
|
|
};
|
|
if(options.display === 'full') {
|
|
grunt.log.write(indent(indentLevel++));
|
|
grunt.log.writeln(chalk.bold(suiteMetaData.description));
|
|
}
|
|
});
|
|
|
|
phantomjs.on('jasmine.suiteDone', function(suiteMetaData) {
|
|
suites[currentSuite].time = suiteMetaData.duration / 1000;
|
|
|
|
if(indentLevel > 1) {
|
|
indentLevel--;
|
|
}
|
|
});
|
|
|
|
phantomjs.on('jasmine.specStarted', function(specMetaData) {
|
|
thisRun.executedSpecs++;
|
|
thisRun.cleanConsole = true;
|
|
if(options.display === 'full') {
|
|
grunt.log.write(indent(indentLevel) + '- ' + chalk.grey(specMetaData.description) + '...');
|
|
} else if(options.display === 'short' ) {
|
|
grunt.log.write(chalk.grey('.'));
|
|
}
|
|
});
|
|
|
|
phantomjs.on('jasmine.specDone', function(specMetaData) {
|
|
var specSummary = {
|
|
assertions : 0,
|
|
classname : suites[currentSuite].name,
|
|
name : specMetaData.description,
|
|
time : specMetaData.duration / 1000,
|
|
failureMessages : []
|
|
};
|
|
|
|
suites[currentSuite].tests++;
|
|
|
|
var color = 'yellow',
|
|
symbol = 'splat';
|
|
if (specMetaData.status === "passed") {
|
|
thisRun.passedSpecs++;
|
|
color = 'green';
|
|
symbol = 'check';
|
|
} else if (specMetaData.status === "failed") {
|
|
thisRun.failedSpecs++;
|
|
status.failed++;
|
|
color = 'red';
|
|
symbol = 'error';
|
|
suites[currentSuite].failures++;
|
|
suites[currentSuite].errors += specMetaData.failedExpectations.length;
|
|
specSummary.failureMessages = specMetaData.failedExpectations.map(function(error){
|
|
return error.message;
|
|
});
|
|
summary.push({
|
|
suite: suites[currentSuite].name,
|
|
name: specMetaData.description,
|
|
errors: specMetaData.failedExpectations.map(function(error){
|
|
return {
|
|
message: error.message
|
|
};
|
|
})
|
|
});
|
|
} else {
|
|
thisRun.skippedSpecs++;
|
|
}
|
|
|
|
suites[currentSuite].testcases.push(specSummary);
|
|
|
|
// If we're writing to a proper terminal, make it fancy.
|
|
if (process.stdout.clearLine) {
|
|
if(options.display === 'full') {
|
|
process.stdout.clearLine();
|
|
process.stdout.cursorTo(0);
|
|
grunt.log.writeln(
|
|
indent(indentLevel) +
|
|
chalk[color].bold(symbols.full[symbol]) + ' ' +
|
|
chalk.grey(specMetaData.description)
|
|
);
|
|
} else if(options.display === 'short') {
|
|
process.stdout.moveCursor(-1);
|
|
grunt.log.write(chalk[color].bold(symbols.short[symbol]));
|
|
}
|
|
} else {
|
|
// If we haven't written out since we've started
|
|
if (thisRun.cleanConsole) {
|
|
// then append to the current line.
|
|
grunt.log.writeln('...' + symbols[options.display][symbol]);
|
|
} else {
|
|
// Otherwise reprint the current spec and status.
|
|
grunt.log.writeln(
|
|
indent(indentLevel) + '...' +
|
|
chalk.grey(specMetaData.description) + '...' +
|
|
symbols[options.display][symbol]
|
|
);
|
|
}
|
|
}
|
|
|
|
specMetaData.failedExpectations.forEach(function(error, i){
|
|
var specIndex = ' ('+(i+1)+')';
|
|
if(options.display === 'full') {
|
|
grunt.log.writeln(indent(indentLevel + 1) + chalk.red(error.message + specIndex));
|
|
}
|
|
phantomjs.emit('onError', error.message, error.stack);
|
|
});
|
|
|
|
});
|
|
|
|
phantomjs.on('jasmine.jasmineDone', function(){
|
|
var dur = (new Date()).getTime() - thisRun.startTime;
|
|
var specQuantity = thisRun.executedSpecs + (thisRun.executedSpecs === 1 ? " spec " : " specs ");
|
|
|
|
grunt.verbose.writeln('Jasmine runner finished');
|
|
|
|
if (thisRun.executedSpecs === 0) {
|
|
// log.error will print the message but not fail the task, warn will do both.
|
|
var log = (options.ignoreEmpty) ? grunt.log.error : grunt.warn;
|
|
|
|
log('No specs executed, is there a configuration error?');
|
|
}
|
|
|
|
if(options.display === 'short') {
|
|
grunt.log.writeln();
|
|
}
|
|
|
|
if(options.summary && summary.length) {
|
|
grunt.log.writeln();
|
|
logSummary(summary);
|
|
}
|
|
|
|
if (options.junit && options.junit.path) {
|
|
writeJunitXml(suites);
|
|
}
|
|
|
|
grunt.log.writeln('\n' + specQuantity + 'in ' + (dur / 1000) + "s.");
|
|
});
|
|
|
|
function logSummary(tests) {
|
|
grunt.log.writeln('Summary (' + tests.length + ' tests failed)');
|
|
_.forEach(tests, function(test){
|
|
grunt.log.writeln(chalk.red(symbols[options.display]['error']) + ' ' + test.suite + ' ' + test.name);
|
|
_.forEach(test.errors, function(error){
|
|
grunt.log.writeln(' ' + chalk.red(error.message));
|
|
});
|
|
});
|
|
}
|
|
|
|
function writeJunitXml(testsuites){
|
|
var template = grunt.file.read(options.junit.template || junitTemplate);
|
|
if (options.junit.consolidate) {
|
|
var xmlFile = path.join(options.junit.path, 'TEST-' + testsuites.suite1.name.replace(/[^\w]/g, '') + '.xml');
|
|
grunt.file.write(xmlFile, grunt.util._.template(template, { testsuites: _.values(testsuites)}));
|
|
} else {
|
|
_.forEach(testsuites, function(suiteData){
|
|
var xmlFile = path.join(options.junit.path, 'TEST-' + suiteData.name.replace(/[^\w]/g, '') + '.xml');
|
|
grunt.file.write(xmlFile, _.template(template, { testsuites: [suiteData] }));
|
|
});
|
|
}
|
|
}
|
|
|
|
phantomjs.on('jasmine.done', function(elapsed) {
|
|
phantomjs.halt();
|
|
});
|
|
|
|
phantomjs.on('jasmine.done.PhantomReporter', function() {
|
|
phantomjs.emit('jasmine.done');
|
|
});
|
|
|
|
phantomjs.on('jasmine.done_fail', function(url) {
|
|
grunt.log.error();
|
|
grunt.warn('PhantomJS unable to load "' + url + '" URI.', 90);
|
|
});
|
|
}
|
|
|
|
};
|