arrange-act-assert
Version:
The lightweight "Act-Arrange-Assert" oriented testing framework
492 lines (491 loc) • 18.6 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultFormatter = void 0;
const Util = __importStar(require("util"));
const Path = __importStar(require("path"));
const merge_1 = __importDefault(require("../coverage/merge"));
const utils_1 = require("../utils/utils");
const processCoverage_1 = require("../coverage/processCoverage");
class TestFormatter {
constructor(out, info, level, parent) {
this.out = out;
this.info = info;
this.level = level;
this.parent = parent;
this.children = [];
this._pendingShown = [];
this._shown = false;
this._pendingLogs = [];
this._pending = new Set;
this._next = null;
this._startLogged = false;
this.childrenOk = true;
this.ended = false;
this.error = null;
}
_setChildError() {
if (this.childrenOk) {
this.childrenOk = false;
if (this.parent) {
this.parent._setChildError();
}
}
}
_startChild(test) {
if (this._next === test) {
if (this._shown) {
test.show();
}
else {
this._pendingShown.push(test);
}
}
else {
this._pending.add(test);
}
}
_endChild(test) {
if (test.error) {
this._setChildError();
}
if (this._next === test) {
this._next = this.children[this.children.indexOf(test) + 1] || null;
test._logEnd();
if (this._next && this._pending.delete(this._next)) {
this._startChild(this._next);
if (this._next.ended) {
this._endChild(this._next);
}
}
}
else {
this._pending.add(test);
}
}
_logStart() {
if (!this._startLogged) {
this._startLogged = true;
this._log("\u001B[1m", "►", this.info.description);
}
}
_logEnd() {
if (this._startLogged) {
if (this.error) {
if (this.childrenOk) {
this._log("\u001B[91m", "X", `${this.info.description}:\n`, this.error);
}
else {
this._log("\u001B[93m", "►", this.info.description);
}
}
else {
this._log("", "√", this.info.description);
}
}
else if (this.error) {
this._log("\u001B[91m", "X", `${this.info.description}:\n`, this.error);
}
else if (this._startLogged) {
this._log("", "√", this.info.description);
}
else {
this._log("\u001B[92m", "√", this.info.description);
}
}
addChild(test) {
this.children.push(test);
if (!this._next) {
this._next = test;
}
}
start() {
if (this.parent) {
this.parent._logStart();
this.parent._startChild(this);
}
}
end(error) {
this.ended = true;
this.error = error;
if (this.parent) {
this.parent._endChild(this);
}
}
show() {
this._shown = true;
for (const child of this._pendingShown) {
child.show();
}
for (const [test, args] of this._pendingLogs.splice(0)) {
test._log(...args);
}
}
_log(style, icon, ...args) {
if (this._shown) {
if (this.out) {
const pad = " ".repeat((this.level * 2));
icon = icon ? `${icon} ` : icon;
const padIcon = " ".repeat((icon.length));
const formatted = args.map(arg => Util.format("%s", arg)).join("");
const lines = formatted.split("\n").map((line, i) => {
if (i === 0) {
return `${pad}${style}${icon}${line}${"\u001B[0m"}`;
}
else {
return `${pad + padIcon}${line}`;
}
});
this.out(lines.join("\n"));
}
}
else {
this._addLogs([style, icon, ...args]);
}
}
_addLogs(args, test = this) {
if (this.parent && !this.parent._shown) {
this.parent._addLogs(args, test);
}
else {
this._pendingLogs.push([test, args]);
}
}
}
class Root extends TestFormatter {
constructor(parent) {
super(null, {
parentId: -1,
description: "",
type: 1
}, -1, parent);
}
}
function getUid(fileId, testId) {
return `${fileId}_${testId}`;
}
function formatSummaryResult(title, result) {
const okColor = result.error > 0 ? "\u001B[93m" : "\u001B[92m";
let msg = `- ${title}: ${result.count === 0 ? "\u001B[91m" : okColor}${result.count}${"\u001B[0m"}`;
if (result.error > 0) {
msg += `\n · OK: ${result.ok === 0 ? "\u001B[91m" : okColor}${result.ok}${"\u001B[0m"}`;
msg += `\n · ERROR: ${"\u001B[91m"}${result.error}${"\u001B[0m"}`;
}
return msg;
}
function tryShrinkFolder(folder) {
if (folder.entries.files.length === 0 && folder.entries.folders.length === 1) {
const subFolder = folder.entries.folders[0];
folder.path.push(...subFolder.path);
folder.entries = subFolder.entries;
}
return folder;
}
function getFolderTree(coverage, parentPath = []) {
const tree = {
files: [],
folders: []
};
let pathCheck = null;
let preSlice = 0;
let i = 0;
while (i < coverage.length) {
const entry = coverage[i];
const isFile = entry.path.length - 1 <= parentPath.length;
if (isFile) {
tree.files.push({
...entry,
path: entry.path.slice(parentPath.length)
});
}
else {
preSlice = i;
pathCheck = entry.path[parentPath.length];
break;
}
i++;
}
if (pathCheck) {
while (i <= coverage.length) {
const entry = coverage[i];
const isIncluded = entry && entry.path[parentPath.length] === pathCheck;
if (!isIncluded || i === coverage.length) {
const subList = coverage.slice(preSlice, i);
const subTree = {
path: [pathCheck],
entries: getFolderTree(subList, [...parentPath, pathCheck])
};
tryShrinkFolder(subTree);
tree.folders.push(subTree);
if (entry) {
pathCheck = entry.path[parentPath.length];
preSlice = i;
}
}
i++;
}
}
return tree;
}
function groupCoverages(baseFolder, coverage) {
const pathCoverage = coverage.map(entry => ({
...entry,
path: entry.file.substring(baseFolder.length).split(Path.sep)
})).sort((a, b) => {
const maxPath = Math.min(a.path.length - 1, b.path.length - 1);
for (let i = 0; i < maxPath; i++) {
const res = a.path[i].localeCompare(b.path[i]);
if (res !== 0) {
return res;
}
}
if (a.path.length !== b.path.length) {
return a.path.length - b.path.length;
}
return a.path[maxPath].localeCompare(b.path[maxPath]);
});
return getFolderTree(pathCoverage);
}
class DefaultFormatter {
constructor(_out = console.log) {
this._out = _out;
this._root = new Root(null);
this.tests = new Map();
this.coverage = [];
this._root.show();
}
_processCoverageFile(padding, file, rows) {
const uncoveredLines = [];
let uncoveredLinesCount = 0;
let pendingUncovered = null;
for (let i = 0; i < file.lines.length; i++) {
const line = file.lines[i];
const uncoveredBranches = [];
let nextRange = 0;
for (const range of line.ranges) {
if (range.start === nextRange) {
nextRange = range.end;
}
else {
uncoveredBranches.push(nextRange + 1 !== range.start ? `${nextRange + 1}-${range.start}` : String(nextRange + 1));
nextRange = range.end;
}
}
if (line.length !== nextRange && uncoveredBranches.length === 0) {
uncoveredLinesCount++;
if (pendingUncovered == null) {
pendingUncovered = i;
}
}
else if (pendingUncovered != null) {
uncoveredLines.push(`${"\u001B[91m"}${pendingUncovered !== i - 1 ? `${pendingUncovered + 1}-${i}` : pendingUncovered + 1}${"\u001B[0m"}`);
pendingUncovered = null;
}
if (uncoveredBranches.length > 0) {
uncoveredLines.push(`${"\u001B[93m"}${i + 1}${"\u001B[0m"}:[${uncoveredBranches.join("|")}]`);
}
}
if (pendingUncovered != null) {
uncoveredLines.push(`${"\u001B[91m"}${pendingUncovered !== file.lines.length - 1 ? `${pendingUncovered + 1}-${file.lines.length}` : pendingUncovered + 1}${"\u001B[0m"}`);
}
rows.push({
padding: padding,
file: file.path.join(Path.sep),
lines: {
total: file.lines.length,
uncovered: uncoveredLinesCount,
ratio: (file.lines.length - uncoveredLinesCount) / file.lines.length
},
uncoveredLines: uncoveredLines.join(", ")
});
}
_processCoverageTree(prefix, root, tree, _rows = []) {
for (let i = 0; i < tree.files.length; i++) {
const file = tree.files[i];
const isLast = tree.folders.length === 0 && i === tree.files.length - 1;
const padding = `${prefix}${!root ? `${isLast ? "\u2514" : "\u251C"} ` : ""}`;
this._processCoverageFile(padding, file, _rows);
}
for (let i = 0; i < tree.folders.length; i++) {
const folder = tree.folders[i];
const isLast = i === tree.folders.length - 1;
const padding = `${prefix}${!root ? `${isLast ? "\u2514" : "\u251C"} ` : ""}`;
_rows.push({
padding: padding,
file: folder.path.join(Path.sep),
lines: null,
uncoveredLines: ""
});
this._processCoverageTree(`${prefix}${!root ? (isLast ? " " : "\u2502") : ""}${!root ? " " : ""}`, false, folder.entries, _rows);
}
return _rows;
}
async _formatCoverage(coverageOptions) {
const coverages = [];
for (const coverage of this.coverage) {
coverages.push(await (0, processCoverage_1.processCoverage)(coverage, coverageOptions));
}
const coverage = (0, merge_1.default)(coverages);
if (coverage.length > 0) {
const baseFolder = (0, utils_1.getCommonBasePath)(coverage.map(entry => entry.file));
const coverageTree = groupCoverages(baseFolder, coverage);
const rows = this._processCoverageTree("", true, coverageTree);
const maxLength = {
file: Math.max("File".length, "Total".length),
lines: "Lines".length
};
for (const row of rows) {
const fileLength = row.padding.length + row.file.length;
if (fileLength > maxLength.file) {
maxLength.file = fileLength;
}
}
this._out(`\n${"Coverage result"}:`);
this._out(`┏━${"━".repeat(maxLength.file)}━┳━${"━".repeat(maxLength.lines)}━┳━━━ ━━ ━━ ── ─`);
this._out(`┃ ${"File".padEnd(maxLength.file, " ")} ┃ ${"Lines".padEnd(maxLength.lines, " ")} ┃ ${"Uncovered lines"}`);
this._out(`┣━${"━".repeat(maxLength.file)}━╋━${"━".repeat(maxLength.lines)}━╋━━━ ━━ ━━ ── ─`);
let totalLines = 0;
let totalUncoveredLines = 0;
for (const row of rows) {
const color = row.lines ? row.lines.ratio >= 0.9 ? "\u001B[92m" : row.lines.ratio >= 0.5 ? "\u001B[93m" : "\u001B[91m" : "";
const lines = row.lines != null ? `${Math.floor(row.lines.ratio * 100)} %` : "";
if (row.lines != null) {
totalLines += row.lines.total;
totalUncoveredLines += row.lines.uncovered;
}
this._out(`┃ ${row.padding}${color}${row.file.padEnd(maxLength.file - row.padding.length, " ")}${"\u001B[0m"} ┃ ${color}${lines.padStart(maxLength.lines, " ")}${"\u001B[0m"} ┃ ${row.uncoveredLines}`);
}
this._out(`┣━${"━".repeat(maxLength.file)}━╋━${"━".repeat(maxLength.lines)}━╋━━━ ━━ ━━ ── ─`);
const lines = `${Math.floor(((totalLines - totalUncoveredLines) / totalLines) * 100)} %`;
this._out(`┃ ${"Total".padStart(maxLength.file, " ")} ┃ ${lines.padStart(maxLength.lines, " ")} ┃`);
this._out(`┗━${"━".repeat(maxLength.file)}━┻━${"━".repeat(maxLength.lines)}━┛`);
}
}
async formatSummary(summary, coverageOptions) {
this._out(`\n${"\u001B[1m"}Summary:${"\u001B[0m"}`);
this._out(formatSummaryResult("Asserts", summary.assert));
this._out(formatSummaryResult("Tests", summary.test));
if (summary.describe.count > 0) {
this._out(formatSummaryResult("Describes", summary.describe));
}
this._out(formatSummaryResult("Total", summary.total));
await this._formatCoverage(coverageOptions);
for (const { fileId, id, error } of summary.failed) {
const test = this.tests.get(getUid(fileId, id));
if (test && test.childrenOk) {
const lines = [];
let parent = test;
while (parent && !(parent instanceof Root)) {
const pad = " ".repeat((parent.level * 2));
if (parent === test) {
lines.unshift(`${pad}${"\u001B[91m"}X ${parent.info.description}:${"\u001B[0m"}`);
}
else {
lines.unshift(`${pad}${"\u001B[1m"}► ${parent.info.description}${"\u001B[0m"}`);
}
parent = parent.parent;
}
this._out(`${"\u001B[93m"}[X]----- - - - - - - -${"\u001B[0m"}`);
this._out(lines.join("\n"));
this._out(error);
}
}
if (summary.test.count === 0) {
throw new Error("No test run");
}
if (summary.assert.count === 0) {
throw new Error("No asserts run");
}
}
format(fileId, msg) {
switch (msg.type) {
case 5: {
this.coverage.push(msg.coverage);
break;
}
case 0: {
const root = new Root(this._root);
this._root.addChild(root);
root.start();
this.tests.set(fileId, root);
break;
}
case 1: {
const test = this.tests.get(fileId);
if (!test) {
throw new Error("Received file end without file start");
}
test.end(null);
break;
}
case 2: {
if (msg.test.description) {
const testId = getUid(fileId, msg.id);
let parent = this.tests.get(getUid(fileId, msg.test.parentId));
if (!parent) {
parent = this.tests.get(fileId);
if (!parent) {
parent = this._root;
}
}
const test = new TestFormatter(this._out, msg.test, parent.level + 1, parent);
parent.addChild(test);
this.tests.set(testId, test);
}
break;
}
case 3: {
const testId = getUid(fileId, msg.id);
const test = this.tests.get(testId);
if (test) {
test.start();
}
break;
}
case 4: {
const testId = getUid(fileId, msg.id);
const test = this.tests.get(testId);
if (test) {
test.end(msg.error || null);
}
break;
}
}
}
}
exports.DefaultFormatter = DefaultFormatter;