ava
Version:
Futuristic test runner 🚀
262 lines (209 loc) • 6.63 kB
JavaScript
'use strict';
var cliCursor = require('cli-cursor');
var lastLineTracker = require('last-line-stream/tracker');
var StringDecoder = require('string_decoder').StringDecoder;
var plur = require('plur');
var spinners = require('cli-spinners');
var chalk = require('chalk');
var colors = require('../colors');
function MiniReporter() {
if (!(this instanceof MiniReporter)) {
return new MiniReporter();
}
var spinnerDef = spinners.dots;
this.spinnerFrames = spinnerDef.frames.map(function (c) {
return chalk.gray.dim(c);
});
this.spinnerInterval = spinnerDef.interval;
this.reset();
this.stream = process.stderr;
this.stringDecoder = new StringDecoder();
}
module.exports = MiniReporter;
MiniReporter.prototype.start = function () {
var self = this;
this.interval = setInterval(function () {
self.spinnerIndex = (self.spinnerIndex + 1) % self.spinnerFrames.length;
self.write(self.prefix());
}, this.spinnerInterval);
return this.prefix('');
};
MiniReporter.prototype.reset = function () {
this.clearInterval();
this.passCount = 0;
this.failCount = 0;
this.skipCount = 0;
this.todoCount = 0;
this.rejectionCount = 0;
this.exceptionCount = 0;
this.currentStatus = '';
this.currentTest = '';
this.statusLineCount = 0;
this.spinnerIndex = 0;
this.lastLineTracker = lastLineTracker();
};
MiniReporter.prototype.spinnerChar = function () {
return this.spinnerFrames[this.spinnerIndex];
};
MiniReporter.prototype.clearInterval = function () {
clearInterval(this.interval);
this.interval = null;
};
MiniReporter.prototype.test = function (test) {
return this.prefix(this._test(test));
};
MiniReporter.prototype.prefix = function (str) {
str = str || this.currentTest;
this.currentTest = str;
// The space before the newline is required for proper formatting. (Not sure why).
return ' \n ' + this.spinnerChar() + ' ' + str;
};
MiniReporter.prototype._test = function (test) {
var title = test.title;
if (test.todo) {
this.todoCount++;
} else if (test.skip) {
this.skipCount++;
} else if (test.error) {
title = colors.error(test.title);
this.failCount++;
} else {
this.passCount++;
}
return title + '\n' + this.reportCounts();
};
MiniReporter.prototype.unhandledError = function (err) {
if (err.type === 'exception') {
this.exceptionCount++;
} else {
this.rejectionCount++;
}
};
MiniReporter.prototype.reportCounts = function () {
var status = '';
if (this.passCount > 0) {
status += '\n ' + colors.pass(this.passCount, 'passed');
}
if (this.skipCount > 0) {
status += '\n ' + colors.skip(this.skipCount, 'skipped');
}
if (this.todoCount > 0) {
status += '\n ' + colors.todo(this.todoCount, 'todo');
}
if (this.failCount > 0) {
status += '\n ' + colors.error(this.failCount, 'failed');
}
return status.length ? status : '\n';
};
MiniReporter.prototype.finish = function () {
this.clearInterval();
var status = this.reportCounts();
if (this.rejectionCount > 0) {
status += '\n ' + colors.error(this.rejectionCount, plur('rejection', this.rejectionCount));
}
if (this.exceptionCount > 0) {
status += '\n ' + colors.error(this.exceptionCount, plur('exception', this.exceptionCount));
}
var i = 0;
if (this.failCount > 0) {
this.api.errors.forEach(function (test) {
if (!test.error || !test.error.message) {
return;
}
i++;
var title = test.error ? test.title : 'Unhandled Error';
var description;
if (test.error) {
description = ' ' + test.error.message + '\n ' + stripFirstLine(test.error.stack);
} else {
description = JSON.stringify(test);
}
status += '\n\n ' + colors.error(i + '.', title) + '\n';
status += colors.stack(description);
});
}
if (this.rejectionCount > 0 || this.exceptionCount > 0) {
this.api.errors.forEach(function (err) {
if (err.title) {
return;
}
i++;
if (err.type === 'exception' && err.name === 'AvaError') {
status += '\n\n ' + colors.error(i + '. ' + err.message) + '\n';
} else {
var title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception';
var description = err.stack ? err.stack : JSON.stringify(err);
status += '\n\n ' + colors.error(i + '.', title) + '\n';
status += ' ' + colors.stack(description);
}
});
}
if (this.failCount === 0 && this.rejectionCount === 0 && this.exceptionCount === 0) {
status += '\n';
}
return status;
};
MiniReporter.prototype.write = function (str) {
cliCursor.hide();
this.currentStatus = str + '\n';
this._update();
this.statusLineCount = this.currentStatus.split('\n').length;
};
MiniReporter.prototype.stdout = MiniReporter.prototype.stderr = function (data) {
this._update(data);
};
MiniReporter.prototype._update = function (data) {
var str = '';
var ct = this.statusLineCount;
var columns = process.stdout.columns;
var lastLine = this.lastLineTracker.lastLine();
// Terminals automatically wrap text. We only need the last log line as seen on the screen.
lastLine = lastLine.substring(lastLine.length - (lastLine.length % columns));
// Don't delete the last log line if it's completely empty.
if (lastLine.length) {
ct++;
}
// Erase the existing status message, plus the last log line.
str += eraseLines(ct);
// Rewrite the last log line.
str += lastLine;
if (str.length) {
this.stream.write(str);
}
if (data) {
// send new log data to the terminal, and update the last line status.
this.lastLineTracker.update(this.stringDecoder.write(data));
this.stream.write(data);
}
var currentStatus = this.currentStatus;
if (currentStatus.length) {
lastLine = this.lastLineTracker.lastLine();
// We need a newline at the end of the last log line, before the status message.
// However, if the last log line is the exact width of the terminal a newline is implied,
// and adding a second will cause problems.
if (lastLine.length % columns) {
currentStatus = '\n' + currentStatus;
}
// rewrite the status message.
this.stream.write(currentStatus);
}
};
// TODO(@jamestalamge): This should be fixed in log-update and ansi-escapes once we are confident it's a good solution.
var CSI = '\u001b[';
var ERASE_LINE = CSI + '2K';
var CURSOR_TO_COLUMN_0 = CSI + '0G';
var CURSOR_UP = CSI + '1A';
// Returns a string that will erase `count` lines from the end of the terminal.
function eraseLines(count) {
var clear = '';
for (var i = 0; i < count; i++) {
clear += ERASE_LINE + (i < count - 1 ? CURSOR_UP : '');
}
if (count) {
clear += CURSOR_TO_COLUMN_0;
}
return clear;
}
function stripFirstLine(message) {
return message.replace(/^[^\n]*\n/, '');
}