Mailspring/app/spec/spec-runner/gui-reporter.tsx

360 lines
9.4 KiB
TypeScript

/*
* decaffeinate suggestions:
* DS001: Remove Babel/TypeScript constructor workaround
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import path from 'path';
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
const formatStackTrace = function(spec, message, stackTrace, indent) {
if (message == null) {
message = '';
}
if (indent == null) {
indent = '';
}
if (!stackTrace) {
return stackTrace;
}
const jasminePattern = /^\s*at\s+.*\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/;
const firstJasmineLinePattern = /^\s*at [/\\].*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/;
const convertedLines = [];
for (var line of stackTrace.split('\n')) {
if (!jasminePattern.test(line)) {
convertedLines.push(line);
}
if (firstJasmineLinePattern.test(line)) {
break;
}
}
stackTrace = convertedLines.join('\n');
let lines = stackTrace.split('\n');
// Remove first line of stack when it is the same as the error message
const errorMatch = lines[0] != null ? lines[0].match(/^Error: (.*)/) : undefined;
if (errorMatch && message.trim() === errorMatch[1].trim()) {
lines.shift();
}
for (let index = 0; index < lines.length; index++) {
// Remove prefix of lines matching: at [object Object].<anonymous> (path:1:2)
line = lines[index];
const prefixMatch = line.match(/at \[object Object\]\.<anonymous> \(([^)]+)\)/);
if (prefixMatch) {
line = `at ${prefixMatch[1]}`;
}
// Relativize locations to spec directory
lines[index] = line.replace(`at ${spec.specDirectory}${path.sep}`, 'at ');
}
lines = lines.map(line => indent + line.trim());
return lines.join('\n');
};
function indentationString(suite, plus) {
if (plus == null) {
plus = 0;
}
let rootSuite = suite;
let indentLevel = 0 + plus;
while (rootSuite.parentSuite) {
rootSuite = rootSuite.parentSuite;
indentLevel += 1;
}
return __range__(0, indentLevel, false)
.map(() => ' ')
.join('');
}
function suiteString(spec) {
const descriptions = [spec.suite.description];
let rootSuite = spec.suite;
while (rootSuite.parentSuite) {
const indent = indentationString(rootSuite);
descriptions.unshift(indent + rootSuite.description);
rootSuite = rootSuite.parentSuite;
}
return descriptions.join('\n');
}
class N1GuiReporter extends React.Component {
render() {
return (
<div className="spec-reporter">
<button className="btn reload-button" onClick={this.onReloadSpecs}>
Reload Specs
</button>
<div className="symbol-area">
<div className="symbol-header">Core</div>
<div className="symbol-summary list-unstyled">{this._renderSpecsOfType('core')}</div>
</div>
<div className="symbol-area">
<div className="symbol-header">Packages</div>
<div className="symbol-summary list-unstyled">{this._renderSpecsOfType(undefined)}</div>
</div>
{this._renderStatus()}
<div className="results">{this._renderFailures()}</div>
<div className="plain-text-output">{this.props.plainTextOutput}</div>
</div>
);
}
_renderSpecsOfType = type => {
const items = [];
this.props.specs.forEach((spec, idx) => {
if (spec.specType !== type) {
return;
}
let statusClass = 'pending';
let title = undefined;
const results = spec.results();
if (results) {
if (results.skipped) {
statusClass = 'skipped';
} else if (results.failedCount > 0) {
statusClass = 'failed';
title = spec.getFullName();
} else if (spec.endedAt) {
statusClass = 'passed';
}
}
return items.push(<li key={idx} title={title} className={`spec-summary ${statusClass}`} />);
});
return items;
};
_renderFailures = () => {
// We have an array of specs with `suite` and potentially N `parentSuite` from there.
// Create a tree instead.
const topLevelSuites = [];
const failedSpecs = this.props.specs.filter(
spec => spec.endedAt && spec.results().failedCount > 0
);
for (let spec of failedSpecs) {
let { suite } = spec;
while (suite.parentSuite) {
suite = suite.parentSuite;
}
if (topLevelSuites.indexOf(suite) === -1) {
topLevelSuites.push(suite);
}
}
return topLevelSuites.map((suite, idx) => (
<SuiteResultView suite={suite} key={idx} allSpecs={failedSpecs} />
));
};
_renderStatus = () => {
let message, specCount;
let failedCount = 0;
let skippedCount = 0;
let completeCount = 0;
for (let spec of this.props.specs) {
const results = spec.results();
if (!spec.endedAt) {
continue;
}
if (results.failedCount > 0) {
failedCount += 1;
}
if (results.skipped) {
skippedCount += 1;
}
if (results.passedCount > 0 && results.failedCount === 0) {
completeCount += 1;
}
}
if (failedCount === 1) {
message = `${failedCount} failure`;
} else {
message = `${failedCount} failures`;
}
if (skippedCount) {
specCount = `${completeCount - skippedCount}/${this.props.specs.length -
skippedCount} (${skippedCount} skipped)`;
} else {
specCount = `${completeCount}/${this.props.specs.length}`;
}
return (
<div className="status alert alert-info">
<div className="time" />
<div className="spec-count">{specCount}</div>
<div className="message">{message}</div>
</div>
);
};
onReloadSpecs = () => {
require('electron')
.remote.getCurrentWindow()
.reload();
};
}
class SuiteResultView extends React.Component {
static propTypes = {
suite: PropTypes.object,
allSpecs: PropTypes.array,
};
render() {
let items = [];
let subsuites = [];
this.props.allSpecs.forEach(spec => {
if (spec.suite === this.props.suite) {
return items.push(spec);
} else {
let { suite } = spec;
while (suite.parentSuite) {
if (suite.parentSuite === this.props.suite) {
subsuites.push(suite);
return;
}
suite = suite.parentSuite;
}
}
});
items = items.map((spec, idx) => <SpecResultView key={idx} spec={spec} />);
subsuites = subsuites.map((suite, idx) => (
<SuiteResultView key={idx} suite={suite} allSpecs={this.props.allSpecs} />
));
return (
<div className="suite">
<div className="description">{this.props.suite.description}</div>
<div className="results">
{items}
{subsuites}
</div>
</div>
);
}
}
class SpecResultView extends React.Component {
static propTypes = {
spec: PropTypes.object,
};
render() {
let { description } = this.props.spec;
const resultItems = this.props.spec.results().getItems();
if (description.indexOf('it ') !== 0) {
description = `it ${description}`;
}
const failures = [];
for (let idx = 0; idx < resultItems.length; idx++) {
const result = resultItems[idx];
if (result.passed()) {
continue;
}
const stackTrace = formatStackTrace(this.props.spec, result.message, result.trace.stack);
failures.push(
<div key={idx}>
<div className="result-message fail">{result.message}</div>
<div className="stack-trace padded">{stackTrace}</div>
</div>
);
}
return (
<div className="spec">
<div className="description">{description}</div>
<div className="spec-failures">{failures}</div>
</div>
);
}
}
const el = document.createElement('div');
document.body.appendChild(el);
let startedAt = null;
let specs = [];
let plainTextOutput = '';
const update = () => {
const component = <N1GuiReporter startedAt={startedAt} specs={specs} />;
return ReactDOM.render(component, el);
};
export function reportRunnerStarting(runner) {
specs = runner.specs();
startedAt = Date.now();
update();
}
export function reportRunnerResults(runner) {
update();
}
export function reportSuiteResults(suite) {}
export function reportSpecResults(spec) {
spec.endedAt = Date.now();
update();
}
export function reportPlainTextSpecResult(spec) {
let str = '';
if (spec.results().failedCount > 0) {
str += suiteString(spec) + '\n';
const indent = indentationString(spec.suite, 1);
const stackIndent = indentationString(spec.suite, 2);
let { description } = spec;
if (description.indexOf('it ') !== 0) {
description = `it ${description}`;
}
str += indent + description + '\n';
for (let result of spec.results().getItems()) {
if (result.passed()) {
continue;
}
str += indent + result.message + '\n';
const stackTrace = formatStackTrace(spec, result.message, result.trace.stack, stackIndent);
str += stackTrace + '\n';
}
str += '\n\n';
}
plainTextOutput = plainTextOutput + str;
update();
}
export function reportSpecStarting(spec) {
update();
}
function __range__(left, right, inclusive) {
let range = [];
let ascending = left < right;
let end = !inclusive ? right : ascending ? right + 1 : right - 1;
for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
range.push(i);
}
return range;
}