/*!
*
* Copyright (c) 2013 Sebastian Golasch
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
'use strict';
// ext. libs
var Handlebars = require('handlebars');
var stylus = require('stylus');
var fs = require('fs');
// int. globals
var reporter = null;
/**
* The HTML reporter can produce a set of HTML files with the results of your testrun.
*
* The reporter can be installed with the following command:
*
* ```bash
* $ npm install dalek-reporter-html --save-dev
* ```
*
* By default the files will be written to the `report/dalek/` folder,
* you can change this by adding a config option to the your Dalekfile
*
* ```javascript
* "html-reporter": {
* "dest": "your/folder"
* }
* ```
*
* If you would like to use the reporter (in addition to the std. console reporter),
* you can start dalek with a special command line argument
*
* ```bash
* $ dalek your_test.js -r console,html
* ```
*
* or you can add it to your Dalekfile
*
* ```javascript
* "reporter": ["console", "html"]
* ```
*
* @class Reporter
* @constructor
* @part html
* @api
*/
function Reporter (opts) {
this.events = opts.events;
this.config = opts.config;
this.temporaryAssertions = [];
this.temp = {};
var defaultReportFolder = 'report/dalek';
this.dest = this.config.get('html-reporter') && this.config.get('html-reporter').dest ? this.config.get('html-reporter').dest : defaultReportFolder;
this.loadTemplates();
this.initOutputHandlers();
this.startListening();
}
/**
* @module Reporter
*/
module.exports = function (opts) {
if (reporter === null) {
reporter = new Reporter(opts);
}
return reporter;
};
Reporter.prototype = {
/**
* Inits the html buffer objects
*
* @method initOutputHandlers
* @chainable
*/
initOutputHandlers: function () {
this.output = {};
this.output.test = {};
return this;
},
/**
* Loads and prepares all the templates for
* CSS, JS & HTML
*
* @method loadTemplates
* @chainable
*/
loadTemplates: function () {
// render stylesheets
var precss = fs.readFileSync(__dirname + '/themes/default/styl/default.styl', 'utf8');
stylus.render(precss, { filename: 'default.css' }, function(err, css){
if (err) {
throw err;
}
this.styles = css;
}.bind(this));
// collect client js (to be inined later)
this.js = fs.readFileSync(__dirname + '/themes/default/js/default.js', 'utf8');
// register handlebars helpers
Handlebars.registerHelper('roundNumber', function (number) {
return Math.round(number * Math.pow(10, 2)) / Math.pow(10, 2);
});
// collect & compile templates
this.templates = {};
this.templates.test = Handlebars.compile(fs.readFileSync(__dirname + '/themes/default/hbs/test.hbs', 'utf8'));
this.templates.wrapper = Handlebars.compile(fs.readFileSync(__dirname + '/themes/default/hbs/wrapper.hbs', 'utf8'));
this.templates.testresult = Handlebars.compile(fs.readFileSync(__dirname + '/themes/default/hbs/tests.hbs', 'utf8'));
this.templates.banner = Handlebars.compile(fs.readFileSync(__dirname + '/themes/default/hbs/banner.hbs', 'utf8'));
this.templates.detail = Handlebars.compile(fs.readFileSync(__dirname + '/themes/default/hbs/detail.hbs', 'utf8'));
return this;
},
/**
* Connects to all the event listeners
*
* @method startListening
* @chainable
*/
startListening: function () {
// index page
this.events.on('report:assertion', this.outputAssertionResult.bind(this));
this.events.on('report:test:finished', this.outputTestFinished.bind(this));
this.events.on('report:runner:finished', this.outputRunnerFinished.bind(this));
this.events.on('report:run:browser', this.outputRunBrowser.bind(this));
// detail page
this.events.on('report:test:started', this.startDetailPage.bind(this));
this.events.on('report:action', this.addActionToDetailPage.bind(this));
this.events.on('report:assertion', this.addAssertionToDetailPage.bind(this));
this.events.on('report:test:finished', this.finishDetailPage.bind(this));
return this;
},
/**
* Prepares the output for a test detail page
*
* @method startDetailPage
* @chainable
*/
startDetailPage: function () {
this.detailContents = {};
this.detailContents.eventLog = [];
return this;
},
/**
* Adds an action output to the detail page
*
* @method addActionToDetailPage
* @param {object} data Event data
* @chainable
*/
addActionToDetailPage: function (data) {
data.isAction = true;
this.detailContents.eventLog.push(data);
return this;
},
/**
* Adds an assertion result to the detail page
*
* @method addAssertionToDetailPage
* @param {object} data Event data
* @chainable
*/
addAssertionToDetailPage: function (data) {
data.isAssertion = true;
this.detailContents.eventLog.push(data);
return this;
},
/**
* Writes a detail page to the file system
*
* @method finishDetailPage
* @param {object} data Event data
* @chainable
*/
finishDetailPage: function (data) {
this.detailContents.testResult = data;
this.detailContents.styles = this.styles;
this.detailContents.js = this.js;
fs.writeFileSync(this.dest + '/details/' + data.id + '.html', this.templates.detail(this.detailContents), 'utf8');
return this;
},
/**
* Stores the current browser name
*
* @method outputRunBrowser
* @param {string} browser Browser name
* @chainable
*/
outputRunBrowser: function (browser) {
this.temp.browser = browser;
return this;
},
/**
* Writes the index page to the filesystem
*
* @method outputRunnerFinished
* @param {object} data Event data
* @chainable
*/
outputRunnerFinished: function (data) {
var body = '';
var contents = '';
var tests = '';
var banner = '';
// add test results
var keys = Object.keys(this.output.test);
keys.forEach(function (key) {
tests += this.output.test[key];
}.bind(this));
// compile the test result template
body = this.templates.testresult({result: data, tests: tests});
// compile the banner
banner = this.templates.banner({status: data.status});
// compile the contents within the wrapper template
contents = this.templates.wrapper({styles: this.styles, js: this.js, banner: banner, body: body});
// save the main test output file
this.events.emit('report:written', {type: 'html', dest: this.dest});
this._recursiveMakeDirSync(this.dest + '/details');
fs.writeFileSync(this.dest + '/index.html', contents, 'utf8');
return this;
},
/**
* Pushes an assertion result to the index output queue
*
* @method outputAssertionResult
* @param {object} data Event data
* @chainable
*/
outputAssertionResult: function (data) {
this.temporaryAssertions.push(data);
return this;
},
/**
* Pushes an test result to the index output queue
*
* @method outputTestFinished
* @param {object} data Event data
* @chainable
*/
outputTestFinished: function (data) {
data.assertionInfo = this.temporaryAssertions;
data.browser = this.temp.browser;
this.output.test[data.id] = this.templates.test(data);
this.temporaryAssertions = [];
return this;
},
/**
* Helper method to generate deeper nested directory structures
*
* @method _recursiveMakeDirSync
* @param {string} path PAth to create
*/
_recursiveMakeDirSync: function (path) {
var pathSep = require('path').sep;
var dirs = path.split(pathSep);
var root = '';
while (dirs.length > 0) {
var dir = dirs.shift();
if (dir === '') {
root = pathSep;
}
if (!fs.existsSync(root + dir)) {
fs.mkdirSync(root + dir);
}
root += dir + pathSep;
}
}
};