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;
+}