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

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/, '');
}