newman-reporter-htmlextra-onexit
Version:
A newman reporter with added handlebars helpers and separated request iterations
514 lines (443 loc) • 20.8 kB
JavaScript
/* eslint-disable max-len */
/* eslint-disable no-console */
/* eslint-disable arrow-body-style */
/* eslint-disable padding-line-between-statements */
/* eslint-disable semi */
/* eslint-disable arrow-parens */
/* eslint-disable block-scoped-var */
/* eslint-disable no-redeclare */
/* eslint-disable one-var */
/* eslint-disable no-unused-vars */
var fs = require('fs'),
path = require('path'),
_ = require('lodash'),
moment = require('moment-timezone'),
handlebars = require('handlebars'),
helpers = require('handlebars-helpers')({
handlebars: handlebars
}),
version = require('../package.json').version,
chalk = require('chalk'),
util = require('./util'),
progress = require('cli-progress'),
exportFile = require('newman/lib/run/export-file.js'),
/**
* An object of the default file read preferences.
*
* @type {Object}
*/
FILE_READ_OPTIONS = { encoding: 'utf8' },
/**
* The default Handlebars template to use when no user specified template is provided.
*
* @type {String}
*/
DEFAULT_TEMPLATE = 'dashboard-template.hbs',
/**
/**
* The show only fails Handlebars template is used when the arg is passed in the cli.
*
* @type {String}
*/
SHOW_ONLY_FAILS = 'only-failures-dashboard.hbs',
/**
/**
* The list of execution data fields that are aggregated over multiple requests for the collection run
*
* @type {String[]}
*/
AGGREGATED_FIELDS = ['cursor', 'item', 'request', 'response', 'requestError'],
PostmanHTMLExtraReporter;
/**
* A function that creates raw markup to be written to Newman HTML reports.
*
* @param {Object} newman - The collection run object, with a event handler setter, used to enable event wise reporting.
* @param {Object} options - The set of HTML reporter run options.
* @param {String=} options.template - Optional path to the custom user defined HTML report template (Handlebars).
* @param {String=} options.export - Optional custom path to create the HTML report at.
* @param {String=} options.title - Optional flag to set a custom title on the report.
* @param {String=} options.paging - Optional flag to add paging to the tests on the request view.
* @param {String=} options.showOnlyFails - Optional flag to create a report showing only failed tests.
* @param {String=} options.logs - Optional flag to show the console logs in the report.
* @param {Object} collectionRunOptions - The set of all the collection run options.
* @returns {*}
*/
function tryParseJson (data) {
var parsedData;
try {
parsedData = JSON.parse(data);
}
catch (e) {
parsedData = null;
}
return parsedData;
}
function getContentTypeFromHeaders (headers) {
var networkHeaders = [];
if (headers && headers.members) {
networkHeaders = headers.members;
}
else if (headers) {
networkHeaders = headers;
}
for (let i = 0; i < networkHeaders.length; i++) {
let currentHeader = networkHeaders[i];
if (currentHeader.key.toLowerCase() === 'content-type') {
return currentHeader.value;
}
}
return null;
}
PostmanHTMLExtraReporter = function (newman, options, collectionRunOptions) {
handlebars.registerHelper('checklength', function (v1, v2, options) {
if (v1.length > v2) {
// eslint-disable-next-line no-invalid-this
return options.fn(this);
}
// eslint-disable-next-line no-invalid-this
return options.inverse(this);
});
handlebars.registerHelper('prettyPrint', function (text, headers) {
let contentType = getContentTypeFromHeaders(headers);
let formattedContent = '';
if (!contentType) {
formattedContent = '--- reporter:: Content type not recognized ---';
}
else if (contentType.includes('json')) {
let jsonData = tryParseJson(text);
if (jsonData) {
formattedContent = JSON.stringify(jsonData, null, 2);
}
else {
formattedContent = '--- reporter:: JSON parse error ---'
}
}
else if (contentType.includes('text')) {
formattedContent = text;
}
else {
formattedContent = '--- reporter:: ' + contentType + ' not shown.'
}
return formattedContent.trim().slice(0, 10000);
});
// Helper for calculating pass percentage
handlebars.registerHelper('percent', function (passed, failed) {
return (passed * 100 / (passed + failed)).toFixed(0);
});
// Helper for converting object to json
handlebars.registerHelper('formdata', function (context) {
let formdata = {};
context.forEach(function (value, key) {
if (value.src) {
formdata[value.key] = value.src;
}
else {
formdata[value.key] = value.value;
}
});
return JSON.stringify(formdata);
});
// Helper for simple converting object to json when context.forEach returns empty value
handlebars.registerHelper('object', function (context) {
return JSON.stringify(context);
});
// increment helper for zero index
handlebars.registerHelper('inc', function (value) {
return parseInt(value) + 1;
});
// Sums the total tests by 'assertions - skipped tests'
handlebars.registerHelper('totalTests', function (assertions, skippedTests) {
return skippedTests ? parseInt(assertions) - parseInt(skippedTests) : parseInt(assertions);
});
// Adds the moment helper module
handlebars.registerHelper('paging', function () {
var paging = options.testPaging || false;
return paging;
});
handlebars.registerHelper('logs', function () {
var logs = options.logs || false;
return logs;
});
handlebars.registerHelper('isTheSame', function (lvalue, rvalue, options) {
if (arguments.length < 3) {
throw new Error('Handlebars Helper equal needs 2 parameters');
}
// eslint-disable-next-line no-negated-condition
// eslint-disable-next-line eqeqeq
// eslint-disable-next-line no-negated-condition
if (lvalue !== rvalue) {
return options.inverse(this);
}
// eslint-disable-next-line no-else-return
else {
return options.fn(this);
}
});
handlebars.registerHelper('isNotIn', function (elem, list, options) {
if (typeof (list) === 'object') {
list = list.map(v => v.toLowerCase())
}
else if (list.length !== undefined) {
list = list.toLowerCase()
}
if (elem === null) {
return;
}
// eslint-disable-next-line lodash/prefer-is-nil
if (elem !== undefined && elem !== null) {
var convertedElem = elem.toLowerCase()
}
if (_.includes(list, convertedElem)) {
return options.inverse(this);
}
return options.fn(this);
});
handlebars.registerHelper('totalFolders', function (aggregations) {
return aggregations.length;
});
handlebars.registerHelper('totalFailedFolders', function (aggregations) {
let failedFolders = 0;
aggregations.forEach(aggregation => {
aggregation.executions.forEach(execution => {
if (execution.cumulativeTests.failed > 0) {
failedFolders++;
}
});
});
return failedFolders;
});
// eslint-disable-next-line one-var
// eslint-disable-next-line block-scoped-var
var htmlTemplate = options.template || path.join(__dirname, DEFAULT_TEMPLATE);
var compiler = handlebars.compile(fs.readFileSync(htmlTemplate, FILE_READ_OPTIONS), {
preventIndent: true
});
// Handle the skipped tests
newman.on('assertion', function (err, o) {
if (err) { return; }
if (o.skipped) {
this.summary.skippedTests = this.summary.skippedTests || [];
this.summary.skippedTests.push({
cursor: {
ref: o.cursor.ref,
iteration: o.cursor.iteration,
scriptId: o.cursor.scriptId
},
assertion: o.assertion,
skipped: o.skipped,
error: o.error,
item: {
id: o.item.id,
name: o.item.name
}
});
}
});
// Add progress feedback for the reporter
if (_.includes(collectionRunOptions.reporters, 'cli') || _.get(collectionRunOptions.reporter, 'cli') || _.includes(collectionRunOptions.reporters, 'json') || _.get(collectionRunOptions.reporter, 'json') || _.includes(collectionRunOptions.reporters, 'progress') || _.get(collectionRunOptions.reporter, 'progress')) {
newman.on('start', function (err, o) {
if (err) { return; }
console.log(`Using ${chalk.green('htmlextra-onexit')} version ${chalk.green(version)}`)
});
}
if (!_.includes(collectionRunOptions.reporters, 'progress') && !_.get(collectionRunOptions.reporter, 'progress') && !_.includes(collectionRunOptions.reporters, 'cli') && !_.get(collectionRunOptions.reporter, 'cli') && !_.includes(collectionRunOptions.reporters, 'json') && !_.get(collectionRunOptions.reporter, 'json')) {
var bar = new progress.Bar({
format: 'Newman Run Progress |' + chalk.green('{bar}') + '| {percentage}% || Requests: {value}/{total} || ETA: {eta}s',
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
hideCursor: true
});
newman.on('start', function (err, o) {
if (err) { return; }
console.log(`Using ${chalk.green('htmlextra-onexit')} version ${chalk.green(version)}`)
bar.start(o.cursor.length * o.cursor.cycles, 0);
});
newman.on('item', function () {
bar.increment();
});
newman.on('done', function () {
bar.stop();
console.log(`Created the ${chalk.green('htmlextra-onexit')} report.`);
});
}
newman.on('console', function (err, o) {
if (err) { return; }
if (options.logs) {
this.summary.consoleLogs = this.summary.consoleLogs || {};
this.summary.consoleLogs[o.cursor.ref] = this.summary.consoleLogs[o.cursor.ref] || [];
this.summary.consoleLogs[o.cursor.ref].push(o);
}
});
// Prevent node-js exiting automatically
process.stdin.resume();
var isReportCreated = false;
function exitHandler () {
console.log(`Task is exiting!!! Exit code isReportCreated (var) is ${isReportCreated}`);
// When report is created, all good just exit
if (isReportCreated) {
console.log('Report was created, exiting process!')
process.exit();
return;
}
console.log('Creating report before exit!');
newman.emit('beforeDone');
newman.exports.forEach(function (data) {
exportFile(data, function (err) {
newman.emit('done', err, newman.summary);
process.exit();
})
});
}
process.on('exit', exitHandler);
process.on('SIGTERM', exitHandler);
// process.on('SIGKILL', exitHandler);
process.on('SIGINT', exitHandler);
process.on('SIGUSR1', exitHandler);
process.on('SIGUSR2', exitHandler);
process.on('uncaughtException', exitHandler);
newman.on('beforeDone', function () {
isReportCreated = true;
var items = {},
executionMeans = {},
netTestCounts = {},
aggregations = [],
traversedRequests = {},
aggregatedExecutions = {},
consoleLogs = this.summary.consoleLogs || {},
executions = _.get(this, 'summary.run.executions'),
assertions = _.transform(executions, function (result, currentExecution) {
var stream,
reducedExecution,
executionId = currentExecution.cursor.ref;
if (!_.has(traversedRequests, executionId)) {
// mark the current request instance as traversed
_.set(traversedRequests, executionId, 1);
// set the base assertion and cumulative test details for the current request instance
_.set(result, executionId, {});
_.set(netTestCounts, executionId, { passed: 0, failed: 0, skipped: 0 });
// set base values for overall response size and time values
_.set(executionMeans, executionId, { time: { sum: 0, count: 0 }, size: { sum: 0, count: 0 } });
reducedExecution = _.pick(currentExecution, AGGREGATED_FIELDS);
if (reducedExecution.response && _.isFunction(reducedExecution.response.toJSON)) {
reducedExecution.response = reducedExecution.response.toJSON();
stream = reducedExecution.response.stream;
reducedExecution.response.body = Buffer.from(stream).toString();
}
// set sample request and response details for the current request
items[reducedExecution.cursor.ref] = reducedExecution;
}
executionMeans[executionId].time.sum += _.get(currentExecution, 'response.responseTime', 0);
executionMeans[executionId].size.sum += _.get(currentExecution, 'response.responseSize', 0);
++executionMeans[executionId].time.count;
++executionMeans[executionId].size.count;
_.forEach(currentExecution.assertions, function (assertion) {
var aggregationResult,
assertionName = assertion.assertion,
testName = _.get(assertion, 'error.test') || undefined,
errorMessage = _.get(assertion, 'error.message') || undefined,
isError = _.get(assertion, 'error') !== undefined,
isSkipped = _.get(assertion, 'skipped');
result[executionId][assertionName] = result[executionId][assertionName] || {
name: assertionName,
testFailure: { test: testName, message: errorMessage },
passed: 0,
failed: 0,
skipped: 0
};
aggregationResult = result[executionId][assertionName];
if (isError && isSkipped !== true) {
aggregationResult.failed++;
netTestCounts[executionId].failed++;
}
else if (isSkipped) {
aggregationResult.skipped++;
netTestCounts[executionId].skipped++;
}
else if (isError === false && isSkipped === false) {
aggregationResult.passed++;
netTestCounts[executionId].passed++;
}
});
}, {}),
aggregator = function (execution) {
// fetch aggregated run times and response sizes for items, (0 for failed requests)
var aggregationMean = executionMeans[execution.cursor.ref],
meanTime = _.get(aggregationMean, 'time', 0),
meanSize = _.get(aggregationMean, 'size', 0),
parent = execution.item.parent(),
iteration = execution.cursor.iteration,
previous = _.last(aggregations),
current = _.merge(items[execution.cursor.ref], {
assertions: _.values(assertions[execution.cursor.ref]),
mean: {
time: util.prettyms(meanTime.sum / meanTime.count),
size: util.filesize(meanSize.sum / meanSize.count)
},
cumulativeTests: netTestCounts[execution.cursor.ref],
consoleLogs: consoleLogs[execution.cursor.ref]
});
if (aggregatedExecutions[execution.cursor.ref]) { return; }
aggregatedExecutions[execution.cursor.ref] = true;
if (previous && parent.id === previous.parent.id && previous.parent.iteration === iteration) {
previous.executions.push(current);
}
else {
aggregations.push({
parent: {
id: parent.id,
name: util.getFullName(parent),
description: parent.description,
iteration: iteration
},
executions: [current]
});
}
};
_.forEach(this.summary.run.executions, aggregator);
// File name validation regex from owasp https://owasp.org/www-community/OWASP_Validation_Regex_Repository
var pattern = new RegExp('^(([a-zA-Z]:|\\\\)\\\\)?(((\\.)|' +
'(\\.\\.)|([^\\\\/:*?"|<>. ](([^\\\\/:*?"|<>. ])|' +
'([^\\\\/:*?"|<>]*[^\\\\/:*?"|<>. ]))?))' +
'\\\\)*[^\\\\/:*?"|<>. ](([^\\\\/:*?"|<>. ])' +
'|([^\\\\/:*?"|<>]*[^\\\\/:*?"|<>. ]))?$');
let timezone = options.timezone || moment.tz.guess(true);
this.exports.push({
name: 'html-reporter-htmlextra-onexit',
default: (this.summary.collection.name).match(pattern) ?
`${this.summary.collection.name}.html` : 'newman_htmlextra.html',
path: options.export,
content: compiler({
skipHeaders: options.skipHeaders || [],
skipEnvironmentVars: options.skipEnvironmentVars || [],
skipGlobalVars: options.skipGlobalVars || [],
hideRequestBody: options.hideRequestBody || [],
hideResponseBody: options.hideResponseBody || [],
showEnvironmentData: options.showEnvironmentData || false,
showGlobalData: options.showGlobalData || false,
skipSensitiveData: options.skipSensitiveData || false,
omitHeaders: options.omitHeaders || false,
showMarkdownLinks: options.showMarkdownLinks || false,
showFolderDescription: options.showFolderDescription || false,
browserTitle: options.browserTitle || 'Newman Summary Report',
title: options.title || 'Newman Run Dashboard',
titleSize: options.titleSize || 2,
timestamp: moment().tz(timezone).format('dddd, DD MMMM YYYY HH:mm:ss'),
version: collectionRunOptions.newmanVersion,
folders: collectionRunOptions.folder,
aggregations: aggregations,
summary: {
stats: this.summary.run.stats,
collection: this.summary.collection,
globals: _.isObject(this.summary.globals) ? this.summary.globals : undefined,
environment: _.isObject(this.summary.environment) ? this.summary.environment : undefined,
failures: this.summary.run.failures,
responseTotal: util.filesize(this.summary.run.transfers.responseTotal),
responseAverage: util.prettyms(this.summary.run.timings.responseAverage),
duration: util.prettyms(this.summary.run.timings.completed - this.summary.run.timings.started),
skippedTests: _.isObject(this.summary.skippedTests) ? this.summary.skippedTests : undefined
}
})
});
});
};
module.exports = PostmanHTMLExtraReporter;