mochawesome-report-generator
Version:
Generates gorgeous HTML reports from mochawesome reporter.
265 lines (216 loc) • 6.97 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const t = require('tcomb-validation');
const dateFormat = require('dateformat');
const report = require('../lib/main');
const types = require('./types');
const logger = require('./logger');
const JsonErrRegex = /Unexpected token/;
const JsonFileRegex = /\.json{1}$/;
const htmlJsonExtRegex = /\.(?:html|json)$/;
const mapJsonErrors = errors => errors.map(e => ` ${e.message}`).join('\n');
const ERRORS = {
NOT_FOUND: ' File not found.',
GENERIC: ' There was a problem loading mochawesome data.',
INVALID_JSON: errMsgs => mapJsonErrors(errMsgs)
};
let validFiles;
/**
* Validate the data file
*
* @typedef {Object} File
* @property {string} filename Name of the file
* @property {object} data JSON test data
* @property {object} err Error object
*
* @param {string} file File to load/validate
*
* @return {File} Validated file with test data, `err` will be null if valid
*/
function validateFile(file) {
let data;
let err = null; // Try to read and parse the file
try {
data = JSON.parse(fs.readFileSync(file, 'utf-8'));
} catch (e) {
if (e.code === 'ENOENT') {
err = ERRORS.NOT_FOUND;
} else if (JsonErrRegex.test(e.message)) {
err = ERRORS.INVALID_JSON([e]);
} else {
err = ERRORS.GENERIC;
}
} // If the file was loaded successfully,
// validate the json against the TestReport schema
if (data) {
const validationResult = t.validate(data, types.TestReport, {
strict: true
});
if (!validationResult.isValid()) {
err = ERRORS.INVALID_JSON(validationResult.errors);
} else {
validFiles += 1;
}
}
return {
filename: file,
data,
err
};
}
/**
* Set exit code and throw caught errors
*
* @param {Object|string} err Error object or error message
*
*/
function handleError(err) {
process.exitCode = 1;
throw new Error(err);
}
/**
* Loop through resolved promises to log the appropriate messages
*
* @param {Array} resolvedValues Result of promise.all
*
* @return {Array} Array of resolved promise values
*/
function handleResolved(resolvedValues) {
const saved = [];
const errors = [];
resolvedValues.forEach(value => {
if (value.err) {
errors.push(value);
} else {
saved.push(value[0]);
}
});
if (saved.length) {
logger.info(chalk.green('\n✓ Reports saved:'));
logger.info(saved.map(savedFile => `${chalk.underline(savedFile)}`).join('\n'));
}
if (errors.length) {
logger.info(chalk.red('\n✘ Some files could not be processed:'));
logger.info(errors.map(e => `${chalk.underline(e.filename)}\n${chalk.dim(e.err)}`).join('\n\n'));
process.exitCode = 1;
}
if (!validFiles && !errors.length) {
logger.info(chalk.yellow('\nDid not find any JSON files to process.'));
process.exitCode = 1;
}
return resolvedValues;
}
/**
* Get the dateformat format string based on the timestamp option
*
* @param {string|boolean} ts Timestamp option value
*
* @return {string} Valid dateformat format string
*/
function getTimestampFormat(ts) {
return ts === undefined || ts === true || ts === 'true' || ts === false || ts === 'false' ? 'isoDateTime' : ts;
}
/**
* Get the reportFilename option to be passed to `report.create`
*
* Returns the `reportFilename` option if provided otherwise
* it returns the base filename stripped of path and extension
*
* @param {Object} file File object
* @param {string} file.filename Name of file to be processed
* @param {Object} file.data JSON test data
* @param {Object} args CLI process arguments
*
* @return {string} Filename
*/
function getReportFilename({
filename,
data
}, {
reportFilename,
timestamp
}) {
const DEFAULT_FILENAME = filename.split(path.sep).pop().replace(JsonFileRegex, '');
const NAME_REPLACE = '[name]';
const STATUS_REPLACE = '[status]';
const DATETIME_REPLACE = '[datetime]';
const STATUSES = {
Pass: 'pass',
Fail: 'fail'
};
let outFilename = reportFilename || DEFAULT_FILENAME;
const hasDatetimeReplacement = outFilename.includes(DATETIME_REPLACE);
const tsFormat = getTimestampFormat(timestamp);
const ts = dateFormat(new Date(), tsFormat) // replace commas, spaces or comma-space combinations with underscores
.replace(/(,\s*)|,|\s+/g, '_') // replace forward and back slashes with hyphens
.replace(/\\|\//g, '-') // remove colons
.replace(/:/g, '');
if (timestamp) {
if (!hasDatetimeReplacement) {
outFilename = `${outFilename}_${DATETIME_REPLACE}`;
}
} // Special handling of replacement tokens
const status = data.stats.failures > 0 ? STATUSES.Fail : STATUSES.Pass;
outFilename = outFilename.replace(NAME_REPLACE, DEFAULT_FILENAME).replace(STATUS_REPLACE, status).replace(DATETIME_REPLACE, ts).replace(htmlJsonExtRegex, '');
return outFilename;
}
/**
* Process arguments, recursing through any directories,
* to find and validate JSON files
*
* @param {array} args Array of paths
* @param {array} files Array to populate
*
* @return {array} File objects to be processed
*/
function processArgs(args, files = []) {
return args.reduce((acc, arg) => {
let stats;
try {
stats = fs.statSync(arg);
} catch (err) {} // Do nothing
// If argument is a directory, process the files inside
if (stats && stats.isDirectory()) {
return processArgs(fs.readdirSync(arg).map(file => path.join(arg, file)), files);
} // If `statSync` failed, validating will handle the error
// If the argument is a file, check if its a JSON file before validating
if (!stats || JsonFileRegex.test(arg)) {
acc.push(validateFile(arg));
}
return acc;
}, files);
}
/**
* Main CLI Program
*
* @param {Object} args CLI arguments
*
* @return {Promise} Resolved promises with saved files or errors
*/
function marge(args) {
// Reset valid files count
validFiles = 0;
const newArgs = Object.assign({}, args); // Get the array of JSON files to process
const files = processArgs(args._); // When there are multiple valid files OR the timestamp option is set
// we must force `overwrite` to `false` to ensure all reports are created
/* istanbul ignore else */
if (validFiles > 1 || args.timestamp !== false) {
newArgs.overwrite = false;
}
const promises = files.map(file => {
// Files with errors we just resolve
if (file.err) {
return Promise.resolve(file);
} // Valid files get created but first we need to pass correct filename option
// Default value is name of file
// If a filename option was provided, all files get that name
const reportFilename = getReportFilename(file, newArgs);
return report.create(file.data, Object.assign({}, newArgs, {
reportFilename
}));
});
return Promise.all(promises).then(handleResolved).catch(handleError);
}
module.exports = marge;
;