UNPKG

@testim/testim-cli

Version:

Command line interface for running Testing on you CI

294 lines (248 loc) 7.27 kB
var tty = require('tty'), util = require('util'), events = require('events'), supportsColor = require('supports-color'), isatty = tty.isatty(1) && tty.isatty(2); function Base() { var stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0, runner: {} }, failures = this.failures = []; events.EventEmitter.call(this); this.printEpilogue = true; this.on('start', function() { stats.start = new Date(); }); this.on('runner:start', function(runner) { stats.runner[runner.pid] = { start: new Date(), capabilities: runner.capabilities, config: runner.config, tests: [] }; }); this.on('runner:init', function(runner) { stats.runner[runner.pid].sessionID = runner.sessionID; }); this.on('suite:start', function(suite) { suite.root || stats.suites++; }); this.on('test:end', function() { stats.tests++; }); this.on('test:pass', function(test) { stats.runner[test.pid].tests.push(null); stats.passes++; }); this.on('test:fail', function(test) { stats.failures++; stats.runner[test.pid].tests.push(test.err); /** * check if error also happened in other runners */ var duplicateError = false; failures.forEach(function(failure) { if(test.err.message !== failure.err.message || failure.title !== test.title) { return; } duplicateError = true; failure.runner[test.pid] = test.runner[test.pid]; }); if(!duplicateError) { failures.push(test); } }); this.on('test:pending', function() { stats.pending++; }); this.on('runner:end', function(runner) { stats.runner[runner.pid].end = new Date(); }); this.on('end', function(args) { stats.end = new Date(); stats.duration = new Date() - stats.start; this.printEpilogue = this.printEpilogue && !args.sigint; }); this.on('error', function(m) { this.printEpilogue = false; var fmt = this.color('error message', 'ERROR: %s'); console.log(fmt, m.error.message); var sanitizedCaps = []; Object.keys(m.capabilities).forEach(function(capability) { /** * we don't need all capability types to recognise a vm */ if(['browserName', 'platform', 'version', 'platformVersion', 'deviceName', 'app'].indexOf(capability) === -1) { return; } var val = m.capabilities[capability]; sanitizedCaps.push(capability + ': ' + JSON.stringify(val)); }); fmt = this.color('bright yellow', sanitizedCaps.join(', ')); console.log(fmt); if(m.error.stack) { fmt = this.color('error stack', m.error.stack.replace('Error: ' + m.error.message + '\n', '')); } else { fmt = this.color('error stack', ' no stack available'); } console.log(fmt); }); } /** * Inherit from EventEmitter */ util.inherits(Base, events.EventEmitter); /** * Expose some basic cursor interactions * that are common among reporters. */ Base.prototype.cursor = { hide: function() { isatty && process.stdout.write('\u001b[?25l'); }, show: function() { isatty && process.stdout.write('\u001b[?25h'); }, deleteLine: function() { isatty && process.stdout.write('\u001b[2K'); }, beginningOfLine: function() { isatty && process.stdout.write('\u001b[0G'); }, CR: function() { if (isatty) { this.deleteLine(); this.beginningOfLine(); } else { process.stdout.write('\r'); } }, isatty: isatty }; /** * Default color map. */ Base.prototype.colors = { 'pass': 90, 'fail': 31, 'bright pass': 92, 'bright fail': 91, 'bright yellow': 93, 'pending': 36, 'suite': 0, 'error title': 0, 'error message': 31, 'error stack': 90, 'checkmark': 32, 'fast': 90, 'medium': 33, 'slow': 31, 'green': 32, 'light': 90, 'diff gutter': 90, 'diff added': 32, 'diff removed': 31 }; /** * Default symbol map. */ Base.prototype.symbols = { ok: '✓', err: '✖', dot: '․', error: 'F' }; /** * With node.js on Windows: use symbols available in terminal default fonts */ if ('win32' == process.platform) { Base.prototype.symbols.ok = '\u221A'; Base.prototype.symbols.err = '\u00D7'; Base.prototype.symbols.dot = '.'; } /** * Color `str` with the given `type`, * allowing colors to be disabled, * as well as user-defined color * schemes. * * @param {String} type * @param {String} str * @return {String} * @api private */ Base.prototype.color = function(type, str) { if (!supportsColor) return String(str); return '\u001b[' + this.colors[type] + 'm' + str + '\u001b[0m'; }; /** * Output common epilogue used by many of * the bundled reporters. * * @api public */ Base.prototype.epilogue = function() { var stats = this.stats, fmt; if(!this.printEpilogue) { return; } console.log('\n'); // passes fmt = this.color('green', '%d passing') + this.color('light', ' (%ss)'); console.log(fmt, stats.passes || 0, ((Math.round(stats.duration/100)) / 10).toFixed(2)); // pending if (stats.pending) { fmt = this.color('pending', '%d pending'); console.log(fmt, stats.pending); } // failures if (stats.failures) { fmt = this.color('fail', '%d failing'); console.log(fmt, stats.failures); this.listFailures(); } console.log(); }; /** * Outut the given failures as a list */ Base.prototype.listFailures = function() { var self = this; console.log(); this.failures.forEach(function(test, i) { var runningBrowser = ''; Object.keys(test.runner).forEach(function(pid) { var caps = test.runner[pid]; runningBrowser += '\nrunning'; if(caps.browserName) { runningBrowser += ' ' + caps.browserName; } if(caps.version) { runningBrowser += ' (v' + caps.version + ')'; } if(caps.platform) { runningBrowser += ' on ' + caps.platform; } var host = self.stats.runner[pid].config.host; if(host && host.indexOf('saucelabs') > -1) { runningBrowser += '\nCheck out job at https://saucelabs.com/tests/' + self.stats.runner[pid].sessionID; } }); // format var fmt = self.color('error title', '%s) %s:\n') + self.color('error message', '%s') + self.color('bright yellow', '%s') + self.color('error stack', '\n%s\n'); console.log(fmt, (i + 1), test.title, test.err.message, runningBrowser, test.err.stack); }); }; /** * Expose `Base`. */ exports = module.exports = Base;