UNPKG

protractor-beautiful-reporter

Version:

An npm module and which generates your Protractor test reports in HTML (angular) with screenshots

325 lines (287 loc) 10.5 kB
const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const CircularJSON = require('circular-json'); const fse = require('fs-extra'); function getTag(value) { if (value == null) { return value === undefined ? '[object Undefined]' : '[object Null]'; } return toString.call(value); } /** * checks if the given argument is a real string * @param value the value to check * @returns {boolean} true if its a string * @private */ function _isString(value) { const type = typeof value; return type === 'string' || (type === 'object' && value != null && !Array.isArray(value) && getTag(value) === '[object String]'); } /** Function: storeScreenShot * Stores base64 encoded PNG data to the file at the given path. * * Parameters: * (String) data - PNG data, encoded in base64 * (String) file - Target file path */ function storeScreenShot(data, file) { try { fse.outputFileSync(file, data, {encoding: 'base64'}); } catch(e) { console.error(e); console.error('Could not save image: ', file); } } function cleanArray(actual) { const newArray = []; for (let i = 0; i < actual.length; i++) { if (actual[i]) { newArray.push(actual[i]); } } return newArray; } function stringifyHtml(source) { var quote = '\''; var res = source.split(/^/gm).map(function (line) { //var quote = '"'; line = line.replace(/\\/g, '\\\\'); line = line.replace(/\r/g, ''); line = line.replace(/\n/g, ''); // line = line.replace(/\n/g, '\\n'); // line = line.replace(/\r/g, '\\r'); var quoteRegExp = new RegExp(quote, 'g'); line = line.replace(quoteRegExp, '\\' + quote); if (line.length > 0) { return quote + line + "\\n" + quote + " +\n "; } return ""; }).join('') || '""'; return res + " " + quote + quote; } function buildTemplateJs(file) { var templateName = path.basename(file); var template = fs.readFileSync(file) .toString(); var stemplate = stringifyHtml(template); return "\n $templateCache.put('" + templateName + "',\n " + stemplate + "\n );\n"; } /** * Writes a new _report.html file from the jsonData. * @param jsonData combined.json * @param baseName * @param options */ function addHTMLReport(jsonData, baseName, options) { const basePath = path.dirname(baseName); // Output files const htmlFile = path.join(basePath, options.docName); const jsFile = path.join(basePath, 'app.js'); // Input files const htmlInFile = path.join(__dirname, 'lib', 'index.html'); const jsTemplate = path.join(__dirname, 'lib', 'app.js'); let streamJs; let streamHtml; let cssLink = path.join('assets', 'bootstrap.css').replace(/\\/g, '/'); try { if (options.cssOverrideFile) { cssLink = options.cssOverrideFile; } if (options.prepareAssets) { var cssInsert = `<link rel="stylesheet" href="${cssLink}">`; if (options.customCssInline) { cssInsert += ` <style type="text/css">${options.customCssInline}</style>`; } //copy assets fse.copySync(path.join(__dirname, 'lib', 'assets'), path.join(basePath, 'assets')); //copy bootstrap fonts fse.copySync(path.join(__dirname, 'lib', 'fonts'), path.join(basePath, 'fonts')); if (options.clientDefaults && options.clientDefaults.useAjax) { //copy templates fs.copyFileSync(path.join(__dirname, 'lib', 'pbr-screenshot-modal.html'), path.join(basePath, 'pbr-screenshot-modal.html')); fs.copyFileSync(path.join(__dirname, 'lib', 'pbr-stack-modal.html'), path.join(basePath, 'pbr-stack-modal.html')); } // Construct index.html streamHtml = fs.createWriteStream(htmlFile); streamHtml.write( fs.readFileSync(htmlInFile) .toString() .replace('<!-- Here will be CSS placed -->', cssInsert) .replace('<!-- Here goes title -->', options.docTitle) ); streamHtml.end(); } // Construct app.js streamJs = fs.createWriteStream(jsFile); //prepare clientDefaults for serializations var jsonDataString; //templates replacement var templater; if (options.clientDefaults && options.clientDefaults.useAjax) { jsonDataString = "[]"; templater = ""; } else { jsonDataString = JSON.stringify(jsonData, null, 4); templater = ""; templater += buildTemplateJs(path.join(__dirname, 'lib', 'pbr-screenshot-modal.html')); templater += buildTemplateJs(path.join(__dirname, 'lib', 'pbr-stack-modal.html')); } streamJs.write( fs.readFileSync(jsTemplate) .toString() .replace('\[\];//\'<Results Replacement>\'', jsonDataString) .replace('defaultSortFunction/*<Sort Function Replacement>*/', options.sortFunction.toString()) .replace('{};//\'<Client Defaults Replacement>\'', JSON.stringify(options.clientDefaults, null, 4)) .replace("//'<templates replacement>';", templater) ); streamJs.end(); } catch(e) { console.error(e); console.error('Could not save combined.js for data: ' + jsonData); } } /** * Adds the metaData JSON to combined.json * combined.json is a JSON list, containing all metaData. * @param test the metaData to add to the combined JSON list * @param baseName base directory name * @param options reporter options passed through */ function addMetaData(test, baseName, options) { const basePath = path.dirname(baseName); const file = path.join(basePath, 'combined.json'); const lock = path.join(basePath, '.lock'); let data = []; try { try { fs.mkdirSync(lock); } catch(e) { // delay if one write operation is pending if (e.code === 'EEXIST') { setTimeout(function () { addMetaData(test, baseName, options); }, 200); return; } // something else happened (e.g. file access permissions) throw e; } //concat all tests if (fse.pathExistsSync(file)) { data = JSON.parse(fse.readJsonSync(file), {encoding: 'utf8'}); } else { fse.ensureFileSync(file); fse.outputJsonSync(file, CircularJSON.stringify([])); data = JSON.parse(fse.readJsonSync(file), {encoding: 'utf8'}); } data.push(test); fse.outputJsonSync(file, CircularJSON.stringify(data)); addHTMLReport(data, baseName, options); fs.rmdirSync(lock); } catch(e) { console.error(e); console.error('Could not save JSON for data: ' + test); } } /** Function: storeMetaData * Converts the metaData object to a JSON string and stores it to the file at * the given path. * * Parameters: * (Object) metaData - Object to save as JSON * (String) file - Target file path */ function storeMetaData(metaData, file, descriptions) { try { metaData.description = cleanArray(descriptions).join('|'); fse.outputJsonSync(file, metaData); } catch(e) { console.error(e); console.error('Could not save meta data for ' + file); } } /** Function: gatherDescriptions * Traverses the parent suites of a test spec recursivly and gathers all * descriptions. Finally returns them as an array. * * Example: * If your test file has the following structure, this function returns an * array like ['My Tests', 'Module 1', 'Case A'] when executed for `Case A`: * * describe('My Tests', function() { * describe('Module 1', function() { * it('Case A', function() { /* ... * / }); * }); * }); * * Parameters: * (Object) suite - Test suite * (Array) soFar - Already gathered descriptions. On first call, pass an * array containing the specs description itself. * * Returns: * (Array) containing the descriptions of all parental suites and the suite * itself. */ function gatherDescriptions(suite, soFar) { if (suite.description != null) { // jshint ignore:line //the != above is intentional: !== would distinguish between null and undefined (and we do not want this) soFar.push(suite.description); } if (suite.parentSuite) { return gatherDescriptions(suite.parentSuite, soFar); } else { return soFar; } } /** Function: generateGuid * Generates a GUID using node.js' crypto module. * * Returns: * (String) containing a guid */ function generateGuid() { let buf = new Uint16Array(8); buf = crypto.randomBytes(8); const S4 = function (num) { let ret = num.toString(16); while (ret.length < 4) { ret = "0" + ret; } return ret; }; return ( S4(buf[0]) + S4(buf[1]) + "-" + S4(buf[2]) + "-" + S4(buf[3]) + "-" + S4(buf[4]) + "-" + S4(buf[5]) + S4(buf[6]) + S4(buf[7]) ); } function removeDirectory(dirPath) { let files; try { files = fs.readdirSync(dirPath); } catch(e) { return; } if (files.length > 0) { for (let i = 0; i < files.length; i++) { const filePath = dirPath + '/' + files[i]; if (fs.statSync(filePath).isFile()) { fs.unlinkSync(filePath); } else { removeDirectory(filePath); } } } fs.rmdirSync(dirPath); } module.exports = { storeScreenShot: storeScreenShot , storeMetaData: storeMetaData , gatherDescriptions: gatherDescriptions , generateGuid: generateGuid , addMetaData: addMetaData , removeDirectory: removeDirectory , _isString: _isString };