@voxpelli/node-test-pretty-reporter
Version:
Reporter for node:test that supports colorful diffs etc
159 lines (130 loc) • 4.58 kB
JavaScript
import { MarkdownOrChalk } from 'markdown-or-chalk';
import { formatErrorAndCauses } from './errors.js';
const slowIfAboveMs = 75;
/**
* @param {AsyncIterableIterator<import("./advanced-types.js").TestsStreamEventPayloads>} source
* @returns {AsyncGenerator<string, void>}
*/
export default async function * customReporter (source) {
const format = new MarkdownOrChalk(false);
/** @type {import("./advanced-types.js").TestDiagnosticData[]} */
const diagnostics = [];
/** @type {Array<{ data: import("./advanced-types.js").TestFail, parentStack: import("./advanced-types.js").TestFileEvent[] }>} */
const failures = [];
/** @type {import("./advanced-types.js").TestFileEvent[]} */
const parentStack = [];
/** @type {import("./advanced-types.js").TestFileEvent[]} */
let stack = [];
for await (const event of source) {
const { data, type } = event;
if (
type !== 'test:start' &&
type !== 'test:pass' &&
type !== 'test:fail' &&
type !== 'test:plan' &&
type !== 'test:diagnostic' &&
type !== 'test:stdout' &&
type !== 'test:stderr'
) {
continue;
}
switch (type) {
case 'test:start': {
// Emitted when a test starts reporting its own and its subtests status. This event is guaranteed to be emitted in the same order as the tests are defined.
stack.push(data);
let closestParent = parentStack.at(-1);
while (closestParent && closestParent.nesting >= data.nesting) {
parentStack.pop();
closestParent = parentStack.at(-1);
}
parentStack.push(data);
break;
}
case 'test:pass':
case 'test:fail':
// Emitted when a test passes / fails.
if (stack.length > 0) {
if (stack.length > 1) {
stack.pop();
if (stack[0]?.nesting === 0) {
yield '\n';
}
yield * printParentStack(stack);
}
stack = [];
// Skip empty top level tests
if (data.nesting > 0) {
yield ''.padEnd((data.nesting + 1) * 2);
if (type === 'test:pass') {
const successMsg = `${format.logSymbols.success} ${data.name}`;
yield format.chalk
? format.chalk.gray(successMsg)
: successMsg;
} else {
failures.push({
data,
parentStack: [...parentStack],
});
const failureIndexHumanized = failures.length;
const failMsg = `${failureIndexHumanized}) ${data.name}`;
yield format.chalk
? format.chalk.red(failMsg)
: failMsg;
}
if (data.details.duration_ms > slowIfAboveMs) {
const durationMsg = format.italic(`(${Math.floor(data.details.duration_ms)}ms)`);
yield ' ';
yield format.chalk
? format.chalk.red(durationMsg)
: durationMsg;
}
yield '\n';
}
}
break;
case 'test:plan':
// Emitted when all subtests have completed for a given test.
break;
case 'test:diagnostic':
// Emitted when context.diagnostic is called.
diagnostics.push(data);
break;
case 'test:stdout':
process.stdout.write(data.message);
break;
case 'test:stderr':
process.stderr.write(data.message);
break;
}
}
const diagnosticMessages = diagnostics.map(({ message }) => `${format.logSymbols.info} ${message}`).join('\n');
yield '\n';
yield format.chalk ? format.chalk.gray(diagnosticMessages) : diagnosticMessages;
yield '\n\n';
for (const [i, { data, parentStack }] of failures.entries()) {
yield * printParentStack(parentStack, `${i + 1}) `, ':');
yield '\n';
yield format.indent(formatErrorAndCauses(format, data.details.error), 3);
yield '\n\n';
}
}
/**
* @param {import("./advanced-types.js").TestFileEvent[]} parentStack
* @param {string} [prefix]
* @param {string} [suffix]
* @returns {Generator<string, void>}
*/
function * printParentStack (parentStack, prefix = '', suffix = '') {
const prefixLength = prefix.length;
for (const [i, parentTest] of parentStack.entries()) {
if (i !== 0) {
yield '\n';
}
yield ''.padEnd((parentTest.nesting + 1) * 2 + (i ? prefixLength : 0));
if (i === 0 && prefix) {
yield prefix;
}
yield parentTest.name;
}
yield suffix + '\n';
}