adamgruber/mochawesome-report-generator

View on GitHub
src/lib/main.js

Summary

Maintainability
A
35 mins
Test Coverage
const fs = require('fs-extra');
const fsu = require('fsu');
const path = require('path');
const opener = require('opener');
const dateFormat = require('dateformat');
const renderMainHTML = require('./main-html');
const pkg = require('../package.json');
const { getMergedOptions } = require('./options');

const distDir = path.join(__dirname, '..', 'dist');
const fileExtRegex = /\.[^.]*?$/;
const htmlJsonExtRegex = /\.(?:html|json)$/;
const semverRegex = /\d+\.\d+\.\d+(?:-(alpha|beta)\.\d+)?/;

/**
 * Saves a file
 *
 * @param {string} filename Name of file to save
 * @param {string} data Data to be saved
 * @param {boolean} overwrite Overwrite existing files (default: true)
 *
 * @return {Promise} Resolves with filename if successfully saved
 */
function saveFile(filename, data, overwrite) {
  if (overwrite) {
    return fs.outputFile(filename, data).then(() => filename);
  }

  return new Promise((resolve, reject) => {
    fsu.writeFileUnique(
      filename.replace(fileExtRegex, '{_###}$&'),
      data,
      { force: true },
      (err, savedFile) => (err === null ? resolve(savedFile) : reject(err))
    );
  });
}

/**
 * Opens a file
 *
 * @param {string} filename Name of file to open
 *
 * @return {Promise} Resolves with filename if successfully opened
 */
function openFile(filename) {
  return new Promise((resolve, reject) => {
    opener(filename, null, err =>
      err === null ? resolve(filename) : reject(err)
    );
  });
}

/**
 * Synchronously loads a file with utf8 encoding
 *
 * @param {string} filename Name of file to load
 *
 * @return {string} File data as string
 */
function loadFile(filename) {
  return fs.readFileSync(filename, 'utf8');
}

/**
 * Get the dateformat format string based on the timestamp option
 *
 * @param {string|boolean} ts Timestamp option value
 *
 * @return {string} Valid dateformat format string
 */
function getTimestampFormat(ts) {
  return ts === '' ||
    ts === true ||
    ts === 'true' ||
    ts === false ||
    ts === 'false'
    ? 'isoDateTime'
    : ts;
}

/**
 * Construct the path/name of the HTML/JSON file to be saved
 *
 * @param {object} reportOptions Options object
 * @param {string} reportOptions.reportDir Directory to save report to
 * @param {string} reportOptions.reportFilename Filename to save report to
 * @param {string} reportOptions.timestamp Timestamp format to be appended to the filename
 * @param {object} reportData JSON test data
 *
 * @return {string} Fully resolved path without extension
 */
function getFilename({ reportDir, reportFilename, timestamp }, reportData) {
  const DEFAULT_FILENAME = 'mochawesome';
  const NAME_REPLACE = '[name]';
  const STATUS_REPLACE = '[status]';
  const DATETIME_REPLACE = '[datetime]';
  const STATUSES = {
    Pass: 'pass',
    Fail: 'fail',
  };

  let filename = reportFilename || DEFAULT_FILENAME;

  const hasDatetimeReplacement = filename.includes(DATETIME_REPLACE);
  const tsFormat = getTimestampFormat(timestamp);
  const ts = dateFormat(new Date(), tsFormat)
    // replace commas, spaces or comma-space combinations with underscores
    .replace(/(,\s*)|,|\s+/g, '_')
    // replace forward and back slashes with hyphens
    .replace(/\\|\//g, '-')
    // remove colons
    .replace(/:/g, '');

  if (timestamp !== false && timestamp !== 'false') {
    if (!hasDatetimeReplacement) {
      filename = `${filename}_${DATETIME_REPLACE}`;
    }
  }

  const specFilename = path
    .basename(reportData.results[0].file || '')
    .replace(/\..+/, '');

  const status = reportData.stats.failures > 0 ? STATUSES.Fail : STATUSES.Pass;

  filename = filename
    .replace(NAME_REPLACE, specFilename || DEFAULT_FILENAME)
    .replace(STATUS_REPLACE, status)
    .replace(DATETIME_REPLACE, ts)
    .replace(htmlJsonExtRegex, '');

  return path.resolve(process.cwd(), reportDir, filename);
}

/**
 * Get report options by extending base options
 * with user provided options
 *
 * @param {object} opts Report options
 * @param {object} reportData JSON test data
 *
 * @return {object} User options merged with default options
 */
function getOptions(opts, reportData) {
  const mergedOptions = getMergedOptions(opts || {});
  const filename = getFilename(mergedOptions, reportData);

  // For saving JSON from mochawesome reporter
  if (mergedOptions.saveJson) {
    mergedOptions.jsonFile = `${filename}.json`;
  }

  mergedOptions.htmlFile = `${filename}.html`;
  return mergedOptions;
}

/**
 * Determine if assets should be copied following below logic:
 * - Assets folder does not exist -> copy assets
 * - Assets folder exists -> load the css asset to inspect the banner
 * - Error loading css file -> copy assets
 * - Read the package version from the css asset
 * - Asset version is not found -> copy assets
 * - Asset version differs from current version -> copy assets
 *
 * @param {string} assetsDir Directory where assets should be saved
 *
 * @return {boolean} Should assets be copied
 */
function _shouldCopyAssets(assetsDir) {
  if (!fs.existsSync(assetsDir)) {
    return true;
  }

  try {
    const appCss = loadFile(path.join(assetsDir, 'app.css'));
    const appCssVersion = semverRegex.exec(appCss);
    if (!appCssVersion || appCssVersion[0] !== pkg.version) {
      return true;
    }
  } catch (e) {
    return true;
  }

  return false;
}

/**
 * Copy the report assets to the report dir, ignoring inline assets
 *
 * @param {object} opts Report options
 */
function copyAssets({ assetsDir }) {
  if (_shouldCopyAssets(assetsDir)) {
    fs.copySync(distDir, assetsDir, {
      filter: src => !/inline/.test(src),
    });
  }
}

/**
 * Get the report assets object
 *
 * @param {object} reportOptions Options
 * @return {object} Object with assets props
 */
function getAssets(reportOptions) {
  const { assetsDir, cdn, dev, inlineAssets, reportDir } = reportOptions;
  const relativeAssetsDir = path.relative(reportDir, assetsDir);

  // Default URLs to assets path
  const assets = {
    inlineScripts: null,
    inlineStyles: null,
    scriptsUrl: path.join(relativeAssetsDir, 'app.js'),
    stylesUrl: path.join(relativeAssetsDir, 'app.css'),
  };

  // If using inline assets, load files and strings
  if (inlineAssets) {
    assets.inlineScripts = loadFile(path.join(distDir, 'app.js'));
    assets.inlineStyles = loadFile(path.join(distDir, 'app.inline.css'));
  }

  // If using CDN, return remote urls
  if (cdn) {
    assets.scriptsUrl = `https://unpkg.com/mochawesome-report-generator@${
      pkg.version
    }/dist/app.js`;
    assets.stylesUrl = `https://unpkg.com/mochawesome-report-generator@${
      pkg.version
    }/dist/app.css`;
  }

  // In DEV mode, return local urls
  if (dev) {
    assets.scriptsUrl = 'http://localhost:8080/app.js';
    assets.stylesUrl = 'http://localhost:8080/app.css';
  }

  // Copy the assets if needed
  if (!dev && !cdn && !inlineAssets) {
    copyAssets(reportOptions);
  }

  return assets;
}

/**
 * Prepare options, assets, and html for saving
 *
 * @param {object} reportData JSON test data
 * @param {object} opts Report options
 *
 * @return {object} Prepared data for saving
 */
function prepare(reportData, opts) {
  // Get the options
  const reportOptions = getOptions(opts, reportData);

  // Stop here if we're not generating an HTML report
  if (!reportOptions.saveHtml) {
    return { reportOptions };
  }

  // Get the assets
  const assets = getAssets(reportOptions);

  // Render basic template to string
  const renderedHtml = renderMainHTML({
    data: JSON.stringify(reportData),
    options: reportOptions,
    title: reportOptions.reportPageTitle,
    useInlineAssets: reportOptions.inlineAssets && !reportOptions.cdn,
    ...assets,
  });

  const html = `<!doctype html>\n${renderedHtml}`;
  return { html, reportOptions };
}

/**
 * Create the report
 *
 * @param {object} data JSON test data
 * @param {object} opts Report options
 *
 * @return {Promise} Resolves if report was created successfully
 */
function create(data, opts) {
  const { html, reportOptions } = prepare(data, opts);
  const {
    saveJson,
    saveHtml,
    autoOpen,
    overwrite,
    jsonFile,
    htmlFile,
  } = reportOptions;

  const savePromises = [];

  savePromises.push(
    saveHtml !== false
      ? saveFile(htmlFile, html, overwrite).then(
          savedHtml => (autoOpen && openFile(savedHtml)) || savedHtml
        )
      : null
  );

  savePromises.push(
    saveJson
      ? saveFile(
          jsonFile,
          // Preserve `undefined` values as `null` when stringifying
          JSON.stringify(data, (k, v) => (v === undefined ? null : v), 2),
          overwrite
        )
      : null
  );

  return Promise.all(savePromises);
}

/**
 * Create the report synchronously
 *
 * @param {object} data JSON test data
 * @param {object} opts Report options
 *
 */
function createSync(data, opts) {
  const { html, reportOptions } = prepare(data, opts);
  const { autoOpen, htmlFile } = reportOptions;
  fs.outputFileSync(htmlFile, html);
  if (autoOpen) opener(htmlFile);
}

/**
 * Expose functions
 *
 */
module.exports = { create, createSync };