diff --git a/src/classes/TestResult.js b/src/classes/TestResult.js index f045450..3363fe9 100644 --- a/src/classes/TestResult.js +++ b/src/classes/TestResult.js @@ -1,6 +1,6 @@ import BubblingEventTarget from "./BubblingEventTarget.js"; import format, { stripFormatting } from "../format-console.js"; -import { delay, formatDuration, stringify } from "../util.js"; +import { delay, formatDuration, interceptConsole, pluralize, stringify } from "../util.js"; /** * Represents the result of a test or group of tests. @@ -47,6 +47,10 @@ export default class TestResult extends BubblingEventTarget { this.stats.fail++; } + if (originalTarget.messages?.length > 0) { + this.stats.messages += originalTarget.messages.length; + } + this.timeTaken += originalTarget.timeTaken; if (originalTarget.timeTakenAsync) { @@ -75,25 +79,27 @@ export default class TestResult extends BubblingEventTarget { * Run the test(s) */ async run () { - let start = performance.now(); + this.messages = interceptConsole(async () => { + let start = performance.now(); - try { - this.actual = this.test.run ? this.test.run.apply(this.test, this.test.args) : this.test.args[0]; - this.timeTaken = performance.now() - start; + try { + this.actual = this.test.run ? this.test.run.apply(this.test, this.test.args) : this.test.args[0]; + this.timeTaken = performance.now() - start; - if (this.actual instanceof Promise) { - this.actual = await this.actual; - this.timeTakenAsync = performance.now() - start; + if (this.actual instanceof Promise) { + this.actual = await this.actual; + this.timeTakenAsync = performance.now() - start; + } } - } - catch (e) { - this.error = e; - } + catch (e) { + this.error = e; + } + }); this.evaluate(); } - static STATS_AVAILABLE = ["pass", "fail", "error", "skipped", "total", "totalTime", "totalTimeAsync"]; + static STATS_AVAILABLE = ["pass", "fail", "error", "skipped", "total", "totalTime", "totalTimeAsync", "messages"]; /** * Run all tests in the group @@ -301,9 +307,15 @@ ${ this.error.stack }`); let ret = [ ` ${ this.pass? "PASS" : "FAIL" } `, `${this.name ?? "(Anonymous)"}`, - `(${ formatDuration(this.timeTaken ?? 0) })`, ].join(" "); + if (this.messages?.length > 0) { + let suffix = pluralize(this.messages.length, "message", "messages"); + ret += ` ${ this.messages.length } ${ suffix }`; + } + + ret += ` (${ formatDuration(this.timeTaken ?? 0) })`; + if (this.details?.length > 0) { ret += ": " + this.details.join(", "); } @@ -339,6 +351,11 @@ ${ this.error.stack }`); ret.push(`${ stats.skipped }/${ stats.total } skipped`); } + if (stats.messages > 0) { + let suffix = pluralize(stats.messages, "message", "messages"); + ret.push(`${ stats.messages } ${suffix}`); + } + let icon = stats.fail > 0? "❌" : stats.pending > 0? "⏳" : "✅"; ret.splice(1, 0, icon); @@ -351,24 +368,47 @@ ${ this.error.stack }`); return o?.format === "rich" ? ret : stripFormatting(ret); } + /** + * Get a summary of console messages intercepted during the test run. + * @param {object} [o] Options + * @param {"rich" | "plain"} [o.format="rich"] Format to use for output. Defaults to "rich". + * @returns {string} + */ + getMessages (o = {}) { + let ret = new String("(Messages)"); + ret.children = this.messages.map(m =>`(${ m.method }) ${m.args.join(" ")}`); + + return o?.format === "rich" ? ret : stripFormatting(ret); + } + toString (o) { let ret = []; if (this.test.isGroup) { ret.push(this.getSummary(o)); } - else if (this.pass === false || o?.verbose) { + else if (this.pass === false || this.messages?.length > 0 || o?.verbose) { ret.push(this.getResult(o)); } ret = ret.join("\n"); - if (this.tests) { + if (this.tests || this.messages) { ret = new String(ret); - ret.children = this.tests.filter(t => t.stats.fail + t.stats.pending + t.stats.skipped > 0) + + if (this.tests) { + ret.children = this.tests.filter(t => t.stats.fail + t.stats.pending + t.stats.skipped + t.stats.messages > 0) .flatMap(t => t.toString(o)).filter(Boolean); - ret.collapsed = ret.children.length ? this.collapsed : undefined; - ret.highlighted = this.highlighted; + } + + if (this.messages?.length > 0) { + (ret.children ??= []).push(this.getMessages(o)); + } + + if (ret.children?.length > 0 || ret.messages?.length > 0) { + ret.collapsed = this.collapsed; + ret.highlighted = this.highlighted; + } } return ret; diff --git a/src/env/node.js b/src/env/node.js index 41f302a..0d09e32 100644 --- a/src/env/node.js +++ b/src/env/node.js @@ -10,20 +10,25 @@ import { globSync } from 'glob'; // Internal modules import format from "../format-console.js"; -import { getType, interceptConsole, restoreConsole } from '../util.js'; +import { getType } from '../util.js'; -// Recursively traverse a subtree starting from `node` and make (only) groups of tests collapsible +/** + * Recursively traverse a subtree starting from `node` and make groups of tests and test with console messages collapsible. + */ function makeCollapsible (node) { - if (node.tests?.length) { - node.collapsed = true; // all groups are collapsed by default + if (node.tests?.length || node.messages?.length) { + node.collapsed = true; // all groups and console messages are collapsed by default - for (let test of node.tests) { - makeCollapsible(test); + let nodes = [...(node.tests ?? []), ...(node.messages ?? [])]; + for (let node of nodes) { + makeCollapsible(node); } } } -// Recursively traverse a subtree starting from `node` and return all visible groups of tests +/** + * Recursively traverse a subtree starting from `node` and return all visible groups of tests or tests with console messages. + */ function getVisibleGroups (node, options, groups = []) { groups.push(node); @@ -119,24 +124,14 @@ export default { }, setup () { process.env.NODE_ENV = "test"; - interceptedConsole = interceptConsole(); }, done (result, options, event, root) { - makeCollapsible(root) + makeCollapsible(root); render(root, options); if (root.stats.pending === 0) { logUpdate.clear(); - let {messages, originalConsole} = interceptedConsole; - restoreConsole(originalConsole); - - // Replay all the suppressed messages from the tests - for (let message of messages) { - let {args, method} = message; - console[method](...args); - } - let hint = ` Use and arrow keys to navigate groups of tests, and to expand and collapse them respectively. Press ^C (Ctrl+C) or q to quit interactive mode. diff --git a/src/util.js b/src/util.js index 28c7418..615befc 100644 --- a/src/util.js +++ b/src/util.js @@ -183,26 +183,38 @@ export function subsetTests (test, path) { return tests; } -export function interceptConsole () { - let messages = []; - - function getOriginalConsole () { - const methods = ["log", "warn", "error", "info"]; - let originalConsole = {}; +/** + * Intercept console output while running a function. + * @param {Function} fn Function to run. + * @returns {Array<{args: Array, method: string}>} Array of intercepted messages containing the used console method and passed arguments. + */ +export function interceptConsole (fn) { + const methods = ["log", "warn", "error"]; - for (let method of methods) { - originalConsole[method] = console[method]; - console[method] = (...args) => messages.push({args, method}); - } + let originalConsole = {}; + let messages = []; - return originalConsole; + for (let method of methods) { + originalConsole[method] = console[method]; + console[method] = (...args) => messages.push({args, method}); } - return {messages, originalConsole: getOriginalConsole()}; -} + fn(); -export function restoreConsole (originalConsole) { for (let method in originalConsole) { console[method] = originalConsole[method]; } -} \ No newline at end of file + + return messages; +} + +/** + * Pluralize a word. + * @param {number} n Number to check. + * @param {string} singular Singular form of the word. + * @param {string} plural Plural form of the word. + * @returns {string} + */ +export function pluralize (n, singular, plural) { + return n === 1 ? singular : plural; +}