You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
309 lines
8.0 KiB
JavaScript
309 lines
8.0 KiB
JavaScript
5 years ago
|
'use strict';
|
||
|
var StringDecoder = require('string_decoder').StringDecoder;
|
||
|
var cliCursor = require('cli-cursor');
|
||
|
var lastLineTracker = require('last-line-stream/tracker');
|
||
|
var plur = require('plur');
|
||
|
var spinners = require('cli-spinners');
|
||
|
var chalk = require('chalk');
|
||
|
var cliTruncate = require('cli-truncate');
|
||
|
var cross = require('figures').cross;
|
||
|
var repeating = require('repeating');
|
||
|
var objectAssign = require('object-assign');
|
||
|
var colors = require('../colors');
|
||
|
|
||
|
chalk.enabled = true;
|
||
|
Object.keys(colors).forEach(function (key) {
|
||
|
colors[key].enabled = true;
|
||
|
});
|
||
|
|
||
|
function MiniReporter(options) {
|
||
|
if (!(this instanceof MiniReporter)) {
|
||
|
return new MiniReporter(options);
|
||
|
}
|
||
|
|
||
|
var spinnerDef = spinners[process.platform === 'win32' ? 'line' : 'dots'];
|
||
|
this.spinnerFrames = spinnerDef.frames.map(function (c) {
|
||
|
return chalk.gray.dim(c);
|
||
|
});
|
||
|
this.spinnerInterval = spinnerDef.interval;
|
||
|
|
||
|
this.options = objectAssign({}, options);
|
||
|
|
||
|
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.knownFailureCount = 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) {
|
||
|
if (test.todo) {
|
||
|
this.todoCount++;
|
||
|
} else if (test.skip) {
|
||
|
this.skipCount++;
|
||
|
} else if (test.error) {
|
||
|
this.failCount++;
|
||
|
} else {
|
||
|
this.passCount++;
|
||
|
if (test.failing) {
|
||
|
this.knownFailureCount++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (test.todo || test.skip) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
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 SPINNER_WIDTH = 3;
|
||
|
var PADDING = 1;
|
||
|
var title = cliTruncate(test.title, process.stdout.columns - SPINNER_WIDTH - PADDING);
|
||
|
|
||
|
if (test.error || test.failing) {
|
||
|
title = colors.error(test.title);
|
||
|
}
|
||
|
|
||
|
return title + '\n' + this.reportCounts();
|
||
|
};
|
||
|
|
||
|
MiniReporter.prototype.unhandledError = function (err) {
|
||
|
if (err.type === 'exception') {
|
||
|
this.exceptionCount++;
|
||
|
} else {
|
||
|
this.rejectionCount++;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
MiniReporter.prototype.reportCounts = function (time) {
|
||
|
var lines = [
|
||
|
this.passCount > 0 ? '\n ' + colors.pass(this.passCount, 'passed') : '',
|
||
|
this.knownFailureCount > 0 ? '\n ' + colors.error(this.knownFailureCount, plur('known failure', this.knownFailureCount)) : '',
|
||
|
this.failCount > 0 ? '\n ' + colors.error(this.failCount, 'failed') : '',
|
||
|
this.skipCount > 0 ? '\n ' + colors.skip(this.skipCount, 'skipped') : '',
|
||
|
this.todoCount > 0 ? '\n ' + colors.todo(this.todoCount, 'todo') : ''
|
||
|
].filter(Boolean);
|
||
|
|
||
|
if (time && lines.length > 0) {
|
||
|
lines[0] += ' ' + time;
|
||
|
}
|
||
|
|
||
|
return lines.join('');
|
||
|
};
|
||
|
|
||
|
MiniReporter.prototype.finish = function (runStatus) {
|
||
|
this.clearInterval();
|
||
|
var time;
|
||
|
|
||
|
if (this.options.watching) {
|
||
|
time = chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']');
|
||
|
}
|
||
|
|
||
|
var status = this.reportCounts(time);
|
||
|
|
||
|
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));
|
||
|
}
|
||
|
|
||
|
if (runStatus.previousFailCount > 0) {
|
||
|
status += '\n ' + colors.error(runStatus.previousFailCount, 'previous', plur('failure', runStatus.previousFailCount), 'in test files that were not rerun');
|
||
|
}
|
||
|
|
||
|
var i = 0;
|
||
|
|
||
|
if (this.knownFailureCount > 0) {
|
||
|
runStatus.knownFailures.forEach(function (test) {
|
||
|
i++;
|
||
|
|
||
|
var title = test.title;
|
||
|
|
||
|
status += '\n\n\n ' + colors.error(i + '.', title);
|
||
|
// TODO output description with link
|
||
|
// status += colors.stack(description);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (this.failCount > 0) {
|
||
|
runStatus.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).trimRight();
|
||
|
} else {
|
||
|
description = JSON.stringify(test);
|
||
|
}
|
||
|
|
||
|
status += '\n\n\n ' + colors.error(i + '.', title) + '\n';
|
||
|
status += colors.stack(description);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (this.rejectionCount > 0 || this.exceptionCount > 0) {
|
||
|
runStatus.errors.forEach(function (err) {
|
||
|
if (err.title) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
i++;
|
||
|
|
||
|
if (err.type === 'exception' && err.name === 'AvaError') {
|
||
|
status += '\n\n\n ' + colors.error(cross + ' ' + err.message);
|
||
|
} else {
|
||
|
var title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception';
|
||
|
var description = err.stack ? err.stack.trimRight() : JSON.stringify(err);
|
||
|
|
||
|
status += '\n\n\n ' + colors.error(i + '.', title) + '\n';
|
||
|
status += ' ' + colors.stack(description);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return status + '\n';
|
||
|
};
|
||
|
|
||
|
MiniReporter.prototype.section = function () {
|
||
|
return '\n' + chalk.gray.dim(repeating('\u2500', process.stdout.columns || 80));
|
||
|
};
|
||
|
|
||
|
MiniReporter.prototype.clear = function () {
|
||
|
return '';
|
||
|
};
|
||
|
|
||
|
MiniReporter.prototype.write = function (str) {
|
||
|
cliCursor.hide();
|
||
|
this.currentStatus = str;
|
||
|
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/, '');
|
||
|
}
|