cucumber-html-report
Version:
Convert cucumber json reports to a pretty html report.
457 lines (393 loc) • 14.2 kB
JavaScript
;
var R = require('ramda');
var fs = require('fs');
var path = require('path');
var atob = require('atob');
var moment = require('moment');
var Mustache = require('mustache');
var Directory = require('./directory');
var Summary = require('./summary');
var Template = require('./template');
var Duration = require('./duration');
if (!Object.assign) {
Object.assign = require('object-assign');
}
exports.validate = function (options) {
return new Promise(function (resolve, reject) {
if (!fs.existsSync(options.source) || typeof options.source === 'undefined') {
reject('Input file ' + options.source + ' does not exist! Aborting');
}
if (options.hasOwnProperty('template') && !fs.existsSync(options.template)) {
reject('Template file ' + options.template + ' does not exist! Aborting');
}
if (!options.hasOwnProperty('name') || typeof options.name === 'undefined') {
options.name = 'index.html';
}
if (!options.hasOwnProperty('dest') || typeof options.dest === 'undefined') {
options.dest = './reports';
}
if (!options.hasOwnProperty('logo') || options.logo.length < 3) {
options.logo = path.join(__dirname, '..', 'logos', 'cucumber-logo.svg');
}
if (!options.hasOwnProperty('screenshots') || typeof options.screenshots === 'undefined') {
options.screenshots = false;
}
if (!options.hasOwnProperty('maxScreenshots')) {
options.maxScreenshots = 1000;
}
if (!options.hasOwnProperty('sortReport') || typeof options.sortReport === 'undefined') {
options.sortReport = true;
}
options.timestamp = moment().format(options.dateformat || 'YYYY-MM-DD hh:mm:ss');
resolve(options);
});
};
exports.createDirectory = function (options) {
return new Promise(function (resolve, reject) {
// Create output directory if not exists
if (!fs.existsSync(options.dest)) {
Directory.mkdirpSync(options.dest);
console.log('Created directory: %s', options.dest);
} else {
console.log('Directory already exists: %s', options.dest);
}
resolve(options);
});
};
exports.writeReport = function (mustacheOptions) {
var template = Template.load(mustacheOptions.template || Template.defaultTemplate);
var partials = mustacheOptions.template ? mustacheOptions.partialsDir ? Template.getTemplatePartials(mustacheOptions.partialsDir) : {} : Template.getTemplatePartials(path.join(__dirname, '..', 'templates', 'partials'));
var html = Mustache.render(template, mustacheOptions, partials);
return writeHTML(mustacheOptions.dest, mustacheOptions.name, html);
};
/**
* Create a report data structure that we can pass to Mustache.
* @param options the configuration options
* @returns {Promise}
*/
exports.createReport = function (options) {
var features = parseFeatures(options, loadCucumberJson(options.source));
var stepsSummary = [];
var scenarios = [];
var isCucumber2 = features.every(function (feature) {
return feature.elements.every(function (scenario) {
return scenario.type === undefined;
});
});
durationCounter(features, isCucumber2);
// Extracts steps from the features.
features.map(function (feature, index) {
feature.index = index;
var steps = R.compose(R.flatten(), R.map(function (scenario) {
return scenario.steps;
}), R.filter(isScenarioType))(feature.elements);
stepsSummary.push({
'all': 0,
'passed': 0,
'skipped': 0,
'failed': 0
});
// Counts the steps based on their status.
steps.map(function (step) {
switch (step.result.status) {
case 'passed':
stepsSummary[index].all++;
stepsSummary[index].passed++;
break;
case 'skipped':
stepsSummary[index].all++;
stepsSummary[index].skipped++;
break;
default:
stepsSummary[index].all++;
stepsSummary[index].failed++;
break;
}
stepDurationConverter(step, isCucumber2);
});
scenarios.push({
all: 0,
passed: 0,
failed: 0
});
R.compose(R.map(function (status) {
scenarios[index].all++;
scenarios[index][status]++;
}), R.flatten(), R.map(function (scenario) {
return scenario.status;
}), R.filter(isScenarioType))(feature.elements);
});
var scenariosSummary = R.compose(R.filter(isScenarioType), R.flatten(), R.map(function (feature) {
return feature.elements;
}))(features);
var summary = Summary.calculateSummary(features);
var tags = mappingTags(features);
var tagsArray = createTagsArray(tags, isCucumber2);
var mustacheOptions = Object.assign({}, options, {
features: features,
featuresJson: JSON.stringify(R.pluck('name', scenariosSummary)),
stepsSummary: stepsSummary,
scenariosSummary: JSON.stringify(scenariosSummary),
stepsJson: JSON.stringify(stepsSummary),
scenarios: scenarios,
scenariosJson: JSON.stringify(scenarios),
summary: summary,
logo: encodeLogo(options.logo),
screenshots: encodeScreenshot(options),
tags: tagsArray,
tagsJson: JSON.stringify(tagsArray),
image: mustacheImageFormatter,
duration: mustacheDurationFormatter
});
return Promise.resolve(mustacheOptions);
};
function durationCounter(features, isCucumber2) {
R.map(function (feature) {
var duration = R.compose(R.reduce(function (accumulator, current) {
return accumulator + current;
}, 0), R.flatten(), R.map(function (step) {
return step.result.duration ? step.result.duration : 0;
}), R.flatten(), R.map(function (element) {
return element.steps;
}))(feature.elements);
if (Duration.isMinuteOrMore(duration, isCucumber2)) {
// If the test ran for more than a minute, also display minutes.
feature.duration = Duration.formatDurationInMinutesAndSeconds(duration, isCucumber2);
} else if (Duration.isMinuteOrLess(duration, isCucumber2)) {
// If the test ran for less than a minute, display only seconds.
feature.duration = Duration.formatDurationInSeconds(duration, isCucumber2);
}
})(features);
}
function createTagsArray(tags, isCucumber2) {
return function (tags) {
var array = [];
for (var tag in tags) {
if (tags.hasOwnProperty(tag)) {
// Converts the duration from nanoseconds to seconds and minutes (if any)
var duration = tags[tag].duration;
if (Duration.isMinuteOrMore(duration, isCucumber2)) {
// If the test ran for more than a minute, also display minutes.
tags[tag].duration = Duration.formatDurationInMinutesAndSeconds(duration, isCucumber2);
} else if (Duration.isMinuteOrLess(duration, isCucumber2)) {
// If the test ran for less than a minute, display only seconds.
tags[tag].duration = Duration.formatDurationInSeconds(duration, isCucumber2);
}
array.push(tags[tag]);
}
}
return array;
}(tags);
}
function stepDurationConverter(step, isCucumber2) {
// Converts the duration from nanoseconds to seconds and minutes (if any)
var duration = step.result.duration;
if (Duration.isMinuteOrMore(duration, isCucumber2)) {
// If the test ran for more than a minute, also display minutes.
step.result.convertedDuration = Duration.formatDurationInMinutesAndSeconds(duration, isCucumber2);
} else if (Duration.isMinuteOrLess(duration, isCucumber2)) {
// If the test ran for less than a minute, display only seconds.
step.result.convertedDuration = Duration.formatDurationInSeconds(duration, isCucumber2);
}
}
function mappingTags(features) {
var tags = {};
features.map(function (feature) {
[].concat(feature.tags).map(function (tag) {
if (!(tag in tags)) {
tags[tag] = {
name: tag,
scenarios: {
all: 0,
passed: 0,
failed: 0
},
steps: {
all: 0,
passed: 0,
failed: 0,
skipped: 0
},
duration: 0,
status: 'passed'
};
}
feature.elements.map(function (element) {
if (isScenarioType(element)) {
tags[tag].scenarios.all++;
tags[tag].scenarios[element.status]++;
}
element.steps.map(function (step) {
if (step.result.duration) {
tags[tag].duration += step.result.duration;
}
tags[tag].steps.all++;
tags[tag].steps[step.result.status]++;
});
});
if (tags[tag].scenarios.failed > 0) {
tags[tag].status = 'failed';
}
});
});
return tags;
}
function isValidStep(step) {
return step.hidden === undefined || step.result.status.toLocaleLowerCase() === 'failed';
}
function loadCucumberJson(fileName) {
return JSON.parse(fs.readFileSync(fileName, 'utf-8').toString());
}
function sortByStatusAndName(list, options) {
var sortArray = [R.ascend(R.prop('status')), R.ascend(R.prop('name'))];
/* If the option sortReport is false leave the report sorted by execution chronology */
if (!options.sortReport) {
sortArray = [R.ascend(R.prop('line'))];
}
return R.sortWith(sortArray, list);
}
function sortScenariosForFeature(feature) {
feature.elements = sortByStatusAndName(feature.elements, this);
return feature;
}
function parseFeatures(options, features) {
return sortByStatusAndName(features.map(getFeatureStatus).map(parseTags).map(processScenarios(options)).map(sortScenariosForFeature, options), options);
}
function createFileName(name) {
/* eslint-disable no-control-regex */
return name.replace(/[^\x00-\x7F]/g, '').replace(/\s/g, '_').toLowerCase();
}
function writeHTML(targetDirectory, reportName, html) {
return new Promise(function (resolve, reject) {
fs.writeFile(path.join(targetDirectory, reportName || 'index.html'), html, function (error) {
if (error) {
reject(error);
} else {
resolve('Report created successfully!');
}
});
});
}
function writeImage(fileName, data) {
// fs.writeFileSync(fileName, new Buffer(data, 'base64'))
fs.writeFile(fileName, new Buffer(data, 'base64'), function (error) {
if (error) {
console.log('Error writing', fileName, error);
} else {
console.log('Wrote', fileName);
}
});
}
function getFeatureStatus(feature) {
feature.status = Summary.getFeatureStatus(feature);
return feature;
}
function getScenarioStatus(scenario) {
return Summary.getScenarioStatus(scenario);
}
function parseTags(feature) {
feature.tags = (feature.tags || []).map(function (tag) {
return tag.name;
}).join(', ');
return feature;
}
function isScenarioType(scenario) {
return scenario.type === 'scenario' || scenario.keyword === 'Scenario';
}
function processScenario(options) {
return function (scenario) {
scenario.status = getScenarioStatus(scenario);
saveEmbeddedMetadata(options.dest, scenario, scenario.steps, options.maxScreenshots);
scenario.steps = scenario.steps.filter(isValidStep);
};
}
function processScenarios(options) {
return function (feature) {
var scenarios = (feature.elements || []).filter(isScenarioType);
scenarios.forEach(processScenario(options));
return feature;
};
}
function saveEmbeddedMetadata(destPath, element, steps, maxScreenshots) {
steps = steps || [];
steps.forEach(function (step) {
if (step.embeddings) {
var imgCount = 1;
step.embeddings.forEach(function (embedding) {
if (embedding.mime_type === 'image/png' || embedding.media && embedding.media.type === 'image/png') {
if (imgCount <= maxScreenshots) {
handleEmbeddingPng(embedding, element, destPath, imgCount);
}
++imgCount;
} else if (embedding.mime_type === 'text/plain') {
handleEmbeddingPlainText(embedding, element);
} else if (embedding.mime_type === 'text/log') {
handleEmbeddingBrowserLog(embedding, element);
}
});
}
});
}
function handleEmbeddingPng(embedding, element, destPath, imgCount) {
var imageName = createFileName(element.name + '-' + element.line + '-' + imgCount) + '.png';
var fileName = path.join(destPath, imageName);
// Save imageName on element so we use it in HTML
element.imageName = element.imageName || [];
element.imageName.push(imageName);
writeImage(fileName, embedding.data);
}
function handleEmbeddingPlainText(embedding, element) {
// Save plain text on element so we use it in HTML
element.plainTextMetadata = element.plainTextMetadata || [];
var decodedText = atob(embedding.data);
element.plainTextMetadata.push(decodedText);
}
function handleEmbeddingBrowserLog(embedding, element) {
element.logs = embedding.data.split('\n');
}
function mustacheImageFormatter() {
return function (text, render) {
var imgResult = '';
var src = render(text);
var imgList = src.split(',');
if (src.length > 0 && imgList.length > 0) {
imgResult = imgList.map(function (image) {
return '<img src="' + image + '" />';
}).join('');
}
return imgResult;
};
}
function mustacheDurationFormatter() {
return function (text, render) {
return render(text);
};
}
function getDataUri(file) {
var bitmap = fs.readFileSync(file);
return new Buffer(bitmap).toString('base64');
}
function encodeScreenshot(options) {
if (!options.screenshots) {
return undefined;
}
return fs.readdirSync(options.screenshots).map(function (file) {
if (file[0] === '.') {
return undefined;
}
var name = file.split('.');
var extension = name.pop();
extension === 'svg' ? extension = 'svg+xml' : false;
return {
name: name.join('.').replace(/\s/, '_'),
url: 'data:image/' + extension + ';base64,' + getDataUri(options.screenshots + '/' + file)
};
}).filter(function (image) {
return image;
}); // why?
}
function encodeLogo(logoPath) {
var logoExtension = logoPath.split('.').pop();
var extension = logoExtension === 'svg' ? 'svg+xml' : logoExtension;
return 'data:image/' + extension + ';base64,' + getDataUri(logoPath);
}