mochawesome
Version:
A gorgeous reporter for Mocha.js
226 lines (201 loc) • 7.09 kB
JavaScript
const baseReporter = require('mocha/lib/reporters/base');
const Base = baseReporter.Base || baseReporter;
const mochaPkg = require('mocha/package.json');
const { randomUUID } = require('node:crypto');
const marge = require('mochawesome-report-generator');
const margePkg = require('mochawesome-report-generator/package.json');
const conf = require('./config');
const utils = require('./utils');
const pkg = require('../package.json');
const Mocha = require('mocha');
const { EVENT_SUITE_END } = Mocha.Runner.constants;
// Import the utility functions
const { log, mapSuites, getFinalizedStats } = utils;
// Track the total number of tests registered/skipped
const testTotals = {
registered: 0,
skipped: 0,
};
/**
* Done function gets called before mocha exits
*
* Creates and saves the report HTML and JSON files
*
* @param {Object} output Final report object
* @param {Object} options Options to pass to report generator
* @param {Object} config Reporter config object
* @param {Number} failures Number of reported failures
* @param {Function} exit
*
* @return {Promise} Resolves with successful report creation
*/
async function done(output, options, config, failures, exit) {
try {
const [htmlFile, jsonFile] = await marge.create(output, options);
if (!htmlFile && !jsonFile) {
log('No files were generated', 'warn', config);
} else {
jsonFile && log(`Report JSON saved to ${jsonFile}`, null, config);
htmlFile && log(`Report HTML saved to ${htmlFile}`, null, config);
}
} catch (err) {
log(err, 'error', config);
}
exit && exit(failures > 0 ? 1 : 0);
}
/**
* Get the class of the configured console reporter. This reporter outputs
* test results to the console while mocha is running, and before
* mochawesome generates its own report.
*
* Defaults to 'spec'.
*
* @param {String} reporter Name of reporter to use for console output
*
* @return {Object} Reporter class object
*/
function consoleReporter(reporter) {
// Mocha <= 11 ships CommonJS reporters (`module.exports = ReporterClass`),
// while Mocha >= 12 ships ES modules that expose the reporter as a named
// export (e.g. `export { Spec }`). Rather than trusting the first function
// found on the namespace object, match a class that actually derives from
// the Base reporter so an unexpected Mocha internals change surfaces as a
// thrown error here instead of silently selecting the wrong export.
const getReporter = reporterModule =>
typeof reporterModule === 'function'
? reporterModule
: Object.values(reporterModule).find(
value =>
typeof value === 'function' &&
(value === Base || value.prototype instanceof Base)
);
if (reporter) {
try {
const reporterModule = require(`mocha/lib/reporters/${reporter}`);
return getReporter(reporterModule);
} catch {
log(`Unknown console reporter '${reporter}', defaulting to spec`);
}
}
const specReporter = require('mocha/lib/reporters/spec');
return getReporter(specReporter);
}
/**
* Initialize a new reporter.
*
* @param {Runner} runner
* @api public
*/
function Mochawesome(runner, options) {
// Set the config options
this.config = conf(options);
// Ensure stats collector has been initialized
if (!runner.stats) {
const statsCollector = require('mocha/lib/stats-collector');
const createStatsCollector =
statsCollector.createStatsCollector || statsCollector;
createStatsCollector(runner);
}
// Reporter options
const reporterOptions = {
...options.reporterOptions,
reportFilename: this.config.reportFilename,
saveHtml: this.config.saveHtml,
saveJson: this.config.saveJson,
};
// Done function will be called before mocha exits
// This is where we will save JSON and generate the HTML report
this.done = (failures, exit) =>
done(this.output, reporterOptions, this.config, failures, exit);
// Reset total tests counters
testTotals.registered = 0;
testTotals.skipped = 0;
// Call the Base mocha reporter
Object.assign(this, new Base(runner));
const reporterName = reporterOptions.consoleReporter;
if (reporterName !== 'none') {
const ConsoleReporter = consoleReporter(reporterName);
new ConsoleReporter(runner);
}
let endCalled = false;
// Add a unique identifier to each suite/test/hook
['suite', 'test', 'hook', 'pending'].forEach(type => {
runner.on(type, item => {
item.uuid = randomUUID();
});
});
// Handle events from workers in parallel mode
if (runner.constructor.name === 'ParallelBufferedRunner') {
const setSuiteDefaults = suite => {
[
'suites',
'tests',
'_beforeAll',
'_beforeEach',
'_afterEach',
'_afterAll',
].forEach(field => {
suite[field] = suite[field] || [];
});
suite.suites.forEach(it => setSuiteDefaults(it));
};
runner.on(EVENT_SUITE_END, function (suite) {
if (suite.root) {
setSuiteDefaults(suite);
runner.suite.suites.push(...suite.suites);
}
});
}
// Process the full suite
runner.on('end', () => {
try {
/* c8 ignore next */
if (!endCalled) {
// end gets called more than once for some reason
// so we ensure the suite is processed only once
endCalled = true;
const rootSuite = mapSuites(this.runner.suite, testTotals, this.config);
// Attempt to set a filename for the root suite to
// support `reportFilename` [name] replacement token
if (rootSuite) {
if (rootSuite.suites.length === 1) {
const firstSuite = rootSuite.suites[0];
rootSuite.file = firstSuite.file || rootSuite.file;
rootSuite.fullFile = firstSuite.fullFile || rootSuite.fullFile;
} else if (!rootSuite.suites.length && rootSuite.tests.length) {
const firstTest = this.runner.suite.tests[0];
rootSuite.file = firstTest.file || rootSuite.file;
rootSuite.fullFile = firstTest.fullFile || rootSuite.fullFile;
}
}
const obj = {
stats: this.stats,
results: [rootSuite],
meta: {
mocha: {
version: mochaPkg.version,
},
mochawesome: {
options: this.config,
version: pkg.version,
},
marge: {
options: options.reporterOptions,
version: margePkg.version,
},
},
};
obj.stats = getFinalizedStats(obj.stats, this.failures, testTotals);
// Save the final output to be used in the done function
this.output = obj;
}
/* c8 ignore start */
} catch (e) {
// required because thrown errors are not handled directly in the
// event emitter pattern and mocha does not have an "on error"
log(`Problem with mochawesome: ${e.stack}`, 'error');
}
/* c8 ignore stop */
});
}
module.exports = Mochawesome;