web-component-tester
Version:
web-component-tester makes testing your web components a breeze!
349 lines (296 loc) • 9.41 kB
text/typescript
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as chalk from 'chalk';
import * as cleankill from 'cleankill';
import * as events from 'events';
import * as _ from 'lodash';
import * as stacky from 'stacky';
import * as tty from 'tty';
import * as util from 'util';
import {BrowserDef, Stats} from './browserrunner';
import * as config from './config';
const STACKY_CONFIG = {
indent: ' ',
locationStrip: [
/^https?:\/\/[^\/]+/,
/\?[\d\.]+$/,
],
unimportantLocation: [
/^\/web-component-tester\//,
]
};
export type State = 'passing'|'pending'|'failing'|'unknown'|'error';
export type CompletedState = 'passing'|'failing'|'pending'|'unknown';
type Formatter = (value: string) => string;
const STATE_ICONS = {
passing: '✓',
pending: '✖',
failing: '✖',
unknown: '?',
};
const STATE_COLORS: {[state: string]: Formatter} = {
passing: chalk.green,
pending: chalk.yellow,
failing: chalk.red,
unknown: chalk.red,
error: chalk.red,
};
const SHORT = {
'internet explorer': 'IE',
};
const BROWSER_PAD = 24;
const STATUS_PAD = 38;
export interface TestEndData {
state: CompletedState;
/**
* The titles of the tests that ran.
*/
test: string[];
duration: number;
error: any;
}
export class CliReporter {
prettyBrowsers: {[id: number]: string} = {};
browserStats: {[id: number]: Stats} = {};
emitter: events.EventEmitter;
stream: NodeJS.WritableStream;
options: config.Config;
/**
* The number of lines written the last time writeLines was called.
*/
private linesWritten: number;
constructor(
emitter: events.EventEmitter, stream: NodeJS.WritableStream,
options: config.Config) {
this.emitter = emitter;
this.stream = stream;
this.options = options;
cleankill.onInterrupt(() => {
return new Promise((resolve) => {
this.flush();
resolve();
});
});
emitter.on('log:error', this.log.bind(this, chalk.red));
if (!this.options.quiet) {
emitter.on('log:warn', this.log.bind(this, chalk.yellow));
emitter.on('log:info', this.log.bind(this));
if (this.options.verbose) {
emitter.on('log:debug', this.log.bind(this, chalk.dim));
}
}
emitter.on('browser-init', (browser: BrowserDef, stats: Stats) => {
this.browserStats[browser.id] = stats;
this.prettyBrowsers[browser.id] = this.prettyBrowser(browser);
this.updateStatus();
});
emitter.on(
'browser-start',
(browser: BrowserDef, data: {url: string}, stats: Stats) => {
this.browserStats[browser.id] = stats;
this.log(browser, 'Beginning tests via', chalk.magenta(data.url));
this.updateStatus();
});
emitter.on(
'test-end', (browser: BrowserDef, data: TestEndData, stats: Stats) => {
this.browserStats[browser.id] = stats;
if (data.state === 'failing') {
this.writeTestError(browser, data);
} else if (this.options.expanded || this.options.verbose) {
this.log(
browser, this.stateIcon(data.state), this.prettyTest(data));
}
this.updateStatus();
});
emitter.on(
'browser-end', (browser: BrowserDef, error: any, stats: Stats) => {
this.browserStats[browser.id] = stats;
if (error) {
this.log(chalk.red, browser, 'Tests failed:', error);
} else {
this.log(chalk.green, browser, 'Tests passed');
}
});
emitter.on('run-end', (error: any) => {
if (error) {
this.log(chalk.red, 'Test run ended in failure:', error);
} else {
this.log(chalk.green, 'Test run ended with great success');
}
if (!this.options.ttyOutput) {
this.updateStatus(true);
}
});
}
// Specialized Reporting
updateStatus(force?: boolean) {
if (!this.options.ttyOutput && !force) {
return;
}
// EXTREME TERMINOLOGY FAIL, but here's a glossary:
//
// stats: An object containing test stats (total, passing, failing, etc).
// state: The state that the run is in (running, etc).
// status: A string representation of above.
const statuses = Object.keys(this.browserStats).map((browserIdStr) => {
const browserId = parseInt(browserIdStr, 10);
const pretty = this.prettyBrowsers[browserId];
const stats = this.browserStats[browserId];
let status = '';
const counts = [stats.passing, stats.pending, stats.failing];
if (counts[0] > 0 || counts[1] > 0 || counts[2] > 0) {
if (counts[0] > 0) {
counts[0] = <any>chalk.green(counts[0].toString());
}
if (counts[1] > 0) {
counts[1] = <any>chalk.yellow(counts[1].toString());
}
if (counts[2] > 0) {
counts[2] = <any>chalk.red(counts[2].toString());
}
status = counts.join('/');
}
if (stats.status === 'error') {
status = status + (status === '' ? '' : ' ') + chalk.red('error');
}
return padRight(pretty + ' (' + status + ')', STATUS_PAD);
});
this.writeWrapped(statuses, ' ');
}
writeTestError(browser: BrowserDef, data: TestEndData) {
this.log(browser, this.stateIcon(data.state), this.prettyTest(data));
const error = data.error || {};
this.write('\n');
let prettyMessage = error.message || error;
if (typeof prettyMessage !== 'string') {
prettyMessage = util.inspect(prettyMessage);
}
this.write(chalk.red(' ' + prettyMessage));
if (error.stack) {
try {
this.write(stacky.pretty(data.error.stack, STACKY_CONFIG));
} catch (err) {
// If we couldn't extract a stack (i.e. there was no stack), the message
// is enough.
}
}
this.write('\n');
}
// Object Formatting
stateIcon(state: State) {
const color = STATE_COLORS[state] || STATE_COLORS['unknown'];
return color(STATE_ICONS[state] || STATE_ICONS.unknown);
}
prettyTest(data: TestEndData) {
const color = STATE_COLORS[data.state] || STATE_COLORS['unknown'];
return color(data.test.join(' » ') || '<unknown test>');
}
prettyBrowser(browser: BrowserDef) {
const parts: string[] = [];
if (browser.platform && !browser.deviceName) {
parts.push(browser.platform);
}
const name = browser.deviceName || browser.browserName;
parts.push(SHORT[name] || name);
if (browser.version) {
parts.push(browser.version);
}
if (browser.variant) {
parts.push(`[${browser.variant}]`);
}
return chalk.blue(parts.join(' '));
}
// General Output Formatting
log(...values: any[]): void;
log() {
let values = Array.from(arguments);
let format: (line: string) => string;
if (_.isFunction(values[0])) {
format = values[0];
values = values.slice(1);
}
if (values[0] && values[0].browserName) {
values[0] = padRight(this.prettyBrowser(values[0]), BROWSER_PAD);
}
let line =
_.toArray(values)
.map((value) => _.isString(value) ? value : util.inspect(value))
.join(' ');
line = line.replace(/[\s\n\r]+$/, '');
if (format) {
line = format(line);
}
this.write(line);
}
writeWrapped(blocks: string[], separator: string) {
if (blocks.length === 0) {
return;
}
const lines = [''];
const width = (<tty.WriteStream>this.stream).columns || 0;
for (const block of blocks) {
const line = lines[lines.length - 1];
const combined = line + separator + block;
if (line === '') {
lines[lines.length - 1] = block;
} else if (chalk.stripColor(combined).length <= width) {
lines[lines.length - 1] = combined;
} else {
lines.push(block);
}
}
this.writeLines(['\n'].concat(lines));
if (this.options.ttyOutput) {
this.stream.write('\r');
this.stream.write('\u001b[' + (lines.length + 1) + 'A');
}
}
write(line: string) {
this.writeLines([line]);
this.updateStatus();
}
writeLines(lines: string[]) {
for (let line of lines) {
if (line[line.length - 1] !== '\n') {
line = line + '\n';
}
if (this.options.ttyOutput) {
line = '\u001b[J' + line;
}
this.stream.write(line);
}
this.linesWritten = lines.length;
}
flush() {
if (!this.options.ttyOutput) {
return;
}
// Add an extra line for padding.
for (let i = 0; i <= this.linesWritten; i++) {
this.stream.write('\n');
}
}
// HACK
static CliReporter = CliReporter;
}
// Yeah, yeah.
function padRight(str: string, length: number) {
let currLength = chalk.stripColor(str).length;
while (currLength < length) {
currLength = currLength + 1;
str = str + ' ';
}
return str;
}
module.exports = CliReporter;