Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First stab at handling console messages while a test runs #35

Merged
merged 5 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 59 additions & 19 deletions src/classes/TestResult.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -301,9 +307,15 @@ ${ this.error.stack }`);
let ret = [
`<b><bg ${color}><c white> ${ this.pass? "PASS" : "FAIL" } </c></bg></b>`,
`<c light${color}>${this.name ?? "(Anonymous)"}</c>`,
`<dim>(${ formatDuration(this.timeTaken ?? 0) })</dim>`,
].join(" ");

if (this.messages?.length > 0) {
let suffix = pluralize(this.messages.length, "message", "messages");
ret += ` <dim><b>${ this.messages.length }</b> ${ suffix }</dim>`;
}

ret += ` <dim>(${ formatDuration(this.timeTaken ?? 0) })</dim>`;

if (this.details?.length > 0) {
ret += ": " + this.details.join(", ");
}
Expand Down Expand Up @@ -339,6 +351,11 @@ ${ this.error.stack }`);
ret.push(`<dim><b>${ stats.skipped }</b>/${ stats.total } skipped</dim>`);
}

if (stats.messages > 0) {
let suffix = pluralize(stats.messages, "message", "messages");
ret.push(`<dim><b>${ stats.messages }</b> ${suffix}</dim>`);
}

let icon = stats.fail > 0? "❌" : stats.pending > 0? "⏳" : "✅";
ret.splice(1, 0, icon);

Expand All @@ -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("<c yellow><b><i>(Messages)</i></b></c>");
ret.children = this.messages.map(m =>`<dim>(${ m.method })</dim> ${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;
Expand Down
31 changes: 13 additions & 18 deletions src/env/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 <b>↑</b> and <b>↓</b> arrow keys to navigate groups of tests, <b>→</b> and <b>←</b> to expand and collapse them respectively.
Press <b>^C</b> (<b>Ctrl+C</b>) or <b>q</b> to quit interactive mode.
Expand Down
42 changes: 27 additions & 15 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>, 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];
}
}

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