UNPKG

cucumber-html-report

Version:

Convert cucumber json reports to a pretty html report.

457 lines (393 loc) 14.2 kB
'use strict'; 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); }