UNPKG

mochawesome-local-screenshots

Version:

A Gorgeous HTML/CSS Reporter for Mocha.js - updated with local screenshot management

338 lines (287 loc) 8.97 kB
/*jshint loopfunc: true */ var mocha = require('mocha'), _ = require('lodash'), uuid = require('node-uuid'), chalk = require('chalk'), Highlight = require('highlight.js'), reportGen = require('./reportGenerator'), stringify = require('json-stringify-safe'), conf = require('./config'), templates = require('./templates.js'); var Base = mocha.reporters.Base, generateReport = reportGen.generateReport, saveToFile = reportGen.saveToFile, clearScreenshots = reportGen.clearScreenshots, totalTestsRegistered; Highlight.configure({ useBR: true, languages: ['javascript'] }); module.exports = Mochawesome; /** * Initialize a new reporter. * * @param {Runner} runner * @api public */ function Mochawesome (runner, options) { // Reset total tests counter totalTestsRegistered = 0; // Create/Save necessary report dirs/files var reporterOpts = options.reporterOptions || {}, config = conf(reporterOpts); if (config.clearOldScreenshots) { clearScreenshots(config); } generateReport(config); var self = this; Base.call(self, runner); // Show the Spec Reporter in the console new mocha.reporters.Spec(runner); var allSuites = {}, allTests = [], allHooks = [], allPending = [], allFailures = [], allPasses = [], endCalled = false; runner.on('test end', function (test) { allTests.push(test); }); runner.on('hook end', function (hook) { allHooks.push(hook); }); runner.on('pending', function (test) { allPending.push(test); }); runner.on('pass', function (test) { allPasses.push(test); }); runner.on('fail', function(test) { allFailures.push(test); }); runner.on('end', function () { try { if (!endCalled) { endCalled = true; // end gets called more than once for some reason so this ensures we only do this once allSuites = self.runner.suite; traverseSuites(allSuites); var obj = { reportTitle: config.reportTitle || process.cwd().split(config.splitChar).pop(), inlineAssets: config.inlineAssets, stats: self.stats, suites: allSuites, allTests: allTests.map(cleanTest), allHooks: allHooks, allPending: allPending, allPasses: allPasses.map(cleanTest), allFailures: allFailures.map(cleanTest), copyrightYear: new Date().getFullYear(), passedScr: config.passedScreenshot }; obj.stats.testsRegistered = totalTestsRegistered; var passPercentage = Math.round((obj.stats.passes / (obj.stats.testsRegistered - obj.stats.pending))*1000)/10; var pendingPercentage = Math.round((obj.stats.pending / obj.stats.testsRegistered)*1000)/10; obj.stats.passPercent = passPercentage; obj.stats.pendingPercent = pendingPercentage; obj.stats.other = (obj.stats.passes + obj.stats.failures + obj.stats.pending) - obj.stats.tests; obj.stats.hasOther = obj.stats.other > 0; obj.stats.skipped = obj.stats.testsRegistered - obj.stats.tests; obj.stats.hasSkipped = obj.stats.skipped > 0; obj.stats.failures = obj.stats.failures - obj.stats.other; obj.stats.passPercentClass = _getPercentClass(passPercentage); obj.stats.pendingPercentClass = _getPercentClass(pendingPercentage); if (!templates.mochawesome) { console.error('Mochawesome was unable to load the template.'); } saveToFile(stringify(obj, null, 2), config.reportJsonFile, function(){}); saveToFile(templates.mochawesome(obj), config.reportHtmlFile, function() { console.log('\n[' + chalk.gray('mochawesome') + '] Report saved to ' + config.reportHtmlFile + '\n\n'); }); } } catch (e) { //required because thrown errors are not handled directly in the event emitter pattern and mocha does not have an "on error" console.error('Problem with mochawesome: %s', e.stack); } }); } /** * HELPER FUNCTIONS */ /** * Do a breadth-first search to find * and format all nested 'suite' objects. * * @param {Object} suite * @api private */ function traverseSuites (suite) { var queue = [], next = suite; while (next) { if (next.root) { cleanSuite(next); } if (next.suites.length) { _.each(next.suites, function(suite, i) { cleanSuite(suite); queue.push(suite); }); } next = queue.shift(); } } /** * Modify the suite object to add properties needed to render * the template and remove properties we do not need. * * @param {Object} suite * @api private */ function cleanSuite (suite) { suite.uuid = uuid.v4(); var cleanTests = _.map(suite.tests, cleanTest); var passingTests = _.where(cleanTests, {state: 'passed'}); var failingTests = _.where(cleanTests, {state: 'failed'}); var pendingTests = _.where(cleanTests, {pending: true}); var skippedTests = _.where(cleanTests, {skipped: true}); var duration = 0; _.each(cleanTests, function (test) { duration += test.duration; }); totalTestsRegistered += suite.tests ? suite.tests.length : 0; suite.tests = cleanTests; suite.fullFile = suite.file || ''; suite.file = suite.file ? suite.file.replace(process.cwd(), '') : ''; suite.passes = passingTests; suite.failures = failingTests; suite.pending = pendingTests; suite.skipped = skippedTests; suite.hasTests = suite.tests.length > 0; suite.hasSuites = suite.suites.length > 0; suite.totalTests = suite.tests.length; suite.totalPasses = passingTests.length; suite.totalFailures = failingTests.length; suite.totalPending = pendingTests.length; suite.totalSkipped = skippedTests.length; suite.hasPasses = passingTests.length > 0; suite.hasFailures = failingTests.length > 0; suite.hasPending = pendingTests.length > 0; suite.hasSkipped = suite.skipped.length > 0; suite.duration = duration; if (suite.root) { suite.rootEmpty = suite.totalTests === 0; } removeAllPropsFromObjExcept(suite, [ 'title', 'fullFile', 'file', 'tests', 'suites', 'passes', 'failures', 'pending', 'skipped', 'hasTests', 'hasSuites', 'totalTests', 'totalPasses', 'totalFailures', 'totalPending', 'totalSkipped', 'hasPasses', 'hasFailures', 'hasPending', 'hasSkipped', 'root', 'uuid', 'duration', 'rootEmpty', '_timeout' ]); } /** * Return a plain-object representation of `test` * free of cyclic properties etc. * * @param {Object} test * @return {Object} * @api private */ function cleanTest (test) { var code = test.fn ? test.fn.toString() : test.body, err = test.err ? _.pick( test.err, ['name', 'message', 'expected', 'actual', 'stack', 'scr'] ) : test.err, scr = '<img src="screenshots/' + test.fullTitle().replace(/[^\w_\-]/g, '_').toLowerCase().substr(0,process.env['MOCHAWESOME_MAXFILELENGTH']) + '.png">'; if (code) { code = cleanCode(code); code = Highlight.fixMarkup(Highlight.highlightAuto(code).value); } if (err && err.stack) { err.scr = scr; err.stack = Highlight.fixMarkup(Highlight.highlightAuto(err.stack).value); } var cleaned = { title: test.title, fullTitle: test.fullTitle(), timedOut: test.timedOut, duration: test.duration || 0, state: test.state, speed: test.speed, pass: test.state === 'passed', fail: test.state === 'failed', pending: test.pending, code: code, scr: scr, err: err, isRoot: test.parent.root, uuid: uuid.v4(), parentUUID: test.parent.uuid }; cleaned.skipped = (!cleaned.pass && !cleaned.fail && !cleaned.pending); return cleaned; } /** * Strip the function definition from `str`, * and re-indent for pre whitespace. */ function cleanCode (str) { str = str .replace(/\r\n?|[\n\u2028\u2029]/g, '\n').replace(/^\uFEFF/, '') .replace(/^function *\(.*\) *{|\(.*\) *=> *{?/, '') .replace(/\s+\}$/, ''); var spaces = str.match(/^\n?( *)/)[1].length, tabs = str.match(/^\n?(\t*)/)[1].length, re = new RegExp('^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs ? tabs : spaces) + '}', 'gm'); str = str.replace(re, ''); str = str.replace(/^\s+|\s+$/g, ''); return str; } /** * Remove all properties from an object except * those that are in the propsToKeep array. * * @param {Object} obj * @param {Array} propsToKeep * @api private */ function removeAllPropsFromObjExcept (obj, propsToKeep) { _.forOwn(obj, function(val, prop) { if (propsToKeep.indexOf(prop) === -1) { delete obj[prop]; } }); } /** * Return a classname based on percentage * * @param {Integer} pct * @api private */ function _getPercentClass (pct) { if (pct <= 50) { return 'danger'; } else if (pct > 50 && pct < 80) { return 'warning'; } else { return 'success'; } }