platform-helpers/wdio-html-generator.js

'use strict';

const Handlebars = require('handlebars');
const fs = require('fs-extra');
const lodash = require('lodash');
const Moment = require('moment');
const open = require('open');
const Duration = require('moment-duration-format');
const titleCase = require('title-case').titleCase;
const rootRequire = require('rpcm-root-require');
const LoggerWrapper = rootRequire('/platform-helpers/log4js-wrapper');
const logger = LoggerWrapper.getLogger();
rootRequire('/platform-helpers/string-extensions');

/**
 * Ported from @rpii/wdio-html-reporter, we don't want to base64 encode our images (we already need
 * a folder for the summary).  Besides, the way he's doing it is causing npm install errors for
 * prototype pollution.
 *
 * He uses TypeScript and we don't, so this seems easier than a fork.
 */
class HtmlGenerator {
  static async htmlOutput (reportOptions, callback = () => {}) {
    try {
      Duration(Moment);
      logger.info('wdio-html-generator.js --> htmlOutput --> Html Generation started');
      const templateFile = fs.readFileSync(reportOptions.templateFilename, 'utf8');

      Handlebars.registerHelper('prettyCapitalization', function (str, hbopts) {
        if (typeof str !== 'string') return str;
        const knownLookup = {
          windows: 'Windows',
          msedge: 'MSEdge',
          macos: 'MacOS',
          microsoftedge: 'MicrosoftEdge',
          'mac os x': 'Mac OS X'
        };
        const lookup = knownLookup[str.toLowerCase()];
        if (typeof lookup === 'string') {
          return lookup;
        }
        // otherwise, split and capitalize
        return str.toLowerCase().split(' ').map(function (word) {
          return (word.charAt(0).toUpperCase() + word.slice(1));
        }).join(' ');
      });

      Handlebars.registerHelper('isValidReport', function (suites, hbopts) {
        if (suites && suites.length > 0) {
          return hbopts.fn(this);
        }

        return hbopts.inverse(this);
      });

      Handlebars.registerHelper('isValidSuite', function (suite, hbopts) {
        if (suite.title.length > 0 && suite.type === 'suite' && suite.tests.length > 0) {
          return hbopts.fn(this);
        }

        return hbopts.inverse(this);
      });

      Handlebars.registerHelper('testStateColour', function (state, hbopts) {
        if (state === 'passed') {
          return 'test-pass';
        } else if (state === 'failed') {
          return 'test-fail';
        } else if (state === 'pending') {
          return 'test-pending';
        } else if (state === 'skipped') {
          return 'test-skipped';
        }
      });

      Handlebars.registerHelper('testStateIcon', function (state, hbopts) {
        if (state === 'passed') {
          return '<span class="success">&#10004;</span>';
        } else if (state === 'failed') {
          return '<span class="error">&#10006;</span>';
        } else if (state === 'pending') {
          return '<span class="pending">&#10004;</span>';
        } else if (state === 'skipped') {
          return '<span class="skipped">&#10034;</span>';
        }
      });

      Handlebars.registerHelper('suiteStateColour', function (tests, hbopts) {
        const numTests = Object.keys(tests).length;

        const fail = lodash.values(tests).find(test => {
          return test.state === 'failed';
        });

        if (fail != null) {
          return 'suite-fail';
        }

        const passes = lodash.values(tests).filter(test => {
          return test.state === 'passed';
        });

        if (passes.length === numTests && numTests > 0) {
          return 'suite-pass';
        } // skipped is the lowest priority check

        const skipped = lodash.values(tests).find(test => {
          return test.state === 'skipped';
        });

        if (skipped != null) {
          return 'suite-pending';
        }

        return 'suite-unknown';
      });

      Handlebars.registerHelper('humanizeDuration', function (duration, hbopts) {
        return Moment.duration(duration, 'milliseconds').format('hh:mm:ss', { trim: false });
      });

      Handlebars.registerHelper('ifSuiteHasTests', function (testsHash, hbopts) {
        if (Object.keys(testsHash).length > 0) {
          return hbopts.fn(this);
        }

        return hbopts.inverse(this);
      });

      Handlebars.registerHelper('ifEventIsError', function (event, hbopts) {
        if (event.type.includes('Error')) {
          return hbopts.fn(this);
        }

        return hbopts.inverse(this);
      });

      Handlebars.registerHelper('ifEventIsScreenshot', function (event, hbopts) {
        if (event.type === 'screenshot') {
          return hbopts.fn(this);
        }

        return hbopts.inverse(this);
      });

      Handlebars.registerHelper('ifEventIsLogMessage', function (event, hbopts) {
        if (event.type === 'log') {
          return hbopts.fn(this);
        }

        return hbopts.inverse(this);
      });

      Handlebars.registerHelper('logClass', function (text, hbopts) {
        if (text.includes('Test Iteration')) {
          return 'test-iteration';
        } else {
          return 'log-output';
        }
      });

      Handlebars.registerHelper('fixScreenshotPath', function (path) {
        path = path || '';
        return (rootRequire.rootPath + '/' + path).replace(/\\/gi, '/');
      });

      Handlebars.registerHelper('fixRootPath', function (file) {
        return '{0}/{1}/{2}'
          .format(rootRequire.rootPath, reportOptions.data.options.outputDir, file)
          .replace(/\\/gi, '/')
          .replace(/\.\//gi, '/')
          .replace(/\/\//gi, '/');
      });

      Handlebars.registerHelper('addOne', function (number, hbopts) {
        if (typeof number !== 'number') return number;
        return number + 1;
      });

      Handlebars.registerHelper('shortenPath', function (path) {
        path = path || '';
        const splitPath = path.split('/');
        return splitPath[splitPath.length - 1];
      });

      Handlebars.registerHelper('convertPathToTitle', function (path) {
        const shortPath = Handlebars.helpers.shortenPath(path);
        const title = shortPath.replace(/-/g, ' ').replace(/_/g, ' - ').replace(/\.js$/, '');
        return titleCase(title);
      });

      Handlebars.registerHelper('debug', function (optionalValue) {
        logger.trace('wdio-html-generator.js --> htmlOutput --> debug --> Handlebars.registerHelper --> Current Context');
        logger.trace('wdio-html-generator.js --> htmlOutput --> debug --> Handlebars.registerHelper --> {0}'.format(this));

        if (optionalValue) {
          logger.trace('wdio-html-generator.js --> htmlOutput --> debug --> Handlebars.registerHelper --> Value');
          logger.trace('wdio-html-generator.js --> htmlOutput --> debug --> Handlebars.registerHelper --> {0}'.format(optionalValue));
        }
      });

      Object.keys(reportOptions.templateFuncs).forEach(key => {
        Handlebars.registerHelper(key, reportOptions.templateFuncs[key]);
      });

      if (fs.pathExistsSync(reportOptions.outputDir)) {
        const jsonFile = reportOptions.reportFile.replace('.html', '.json');
        fs.outputFileSync(jsonFile, JSON.stringify(reportOptions.data));
      }

      const template = Handlebars.compile(templateFile);
      const html = template(reportOptions.data);

      if (fs.pathExistsSync(reportOptions.outputDir)) {
        fs.outputFileSync(reportOptions.reportFile, html);

        try {
          if (reportOptions.showInBrowser) {
            await open(reportOptions.reportFile);
            logger.trace('wdio-html-generator.js --> htmlOutput --> browser launched');
          }
        } catch (ex) {
          logger.error('wdio-html-generator.js --> htmlOutput --> showInBrowser error spawning: {0} {1}'.format(reportOptions.reportFile, ex.toString()));
        }
      }
      logger.info('wdio-html-generator.js --> htmlOutput --> Html Generation completed');
      logger.trace('wdio-html-generator.js --> htmlOutput --> processing callback method');
      callback(null, true);
    } catch (ex) {
      logger.error('wdio-html-generator.js --> htmlOutput --> Html Generation processing ended in error: {0}'.format(ex.toString()));
      logger.trace('wdio-html-generator.js --> htmlOutput --> processing callback method');
      callback(ex);
    }
  }
}

module.exports = HtmlGenerator;