UNPKG

arrange-act-assert

Version:

The lightweight "Act-Arrange-Assert" oriented testing framework

492 lines (491 loc) 18.6 kB
"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;