index.js 4.15 KB
var extend = Object.assign || require('util')._extend // eslint-disable-line node/no-deprecated-api
var Path = require('path')

/*
 * store original global keys
 */

var blacklist = Object.keys(global)
blacklist.push('constructor')

/*
 * default config
 */

var defaults = {
  globalize: true,
  console: true,
  useEach: false,
  skipWindowCheck: false,
  html:
    "<!doctype html><html><head><meta charset='utf-8'></head>" +
    '<body></body></html>'
}

/*
 * simple jsdom integration.
 * You can pass jsdom options in, too:
 *
 *     require('./support/jsdom')({
 *       src: [ jquery ]
 *     })
 */

module.exports = function (_options) {
  var options = extend(extend({}, defaults), _options)

  var keys = []

  var before = options.useEach ? global.beforeEach : global.before
  var after = options.useEach ? global.afterEach : global.after

  /*
   * register jsdom before the entire test suite
   */

  before(function (next) {
    if (global.window && !options.skipWindowCheck) {
      throw new Error(
        'mocha-jsdom: already a browser environment, or mocha-jsdom invoked ' +
          "twice. use 'skipWindowCheck' to disable this check."
      )
    }
    require('jsdom/lib/old-api').env(
      extend(extend({}, options), { done: done })
    )

    function done (errors, window) {
      if (options.globalize) {
        propagateToGlobal(window)
      } else {
        global.window = window
      }

      if (options.console) {
        window.console = global.console
      }

      if (errors) {
        return next(getError(errors))
      }

      next(null)
    }
  })

  /*
   * undo keys from being propagated to global after the test suite
   */

  after(function () {
    if (options.globalize) {
      keys.forEach(function (key) {
        delete global[key]
      })
    } else {
      delete global.window
    }
  })

  /*
   * propagate keys from `window` to `global`
   */

  function propagateToGlobal (window) {
    for (var key in window) {
      if (!window.hasOwnProperty(key)) continue
      if (~blacklist.indexOf(key)) continue
      if (global[key]) {
        if (process.env.JSDOM_VERBOSE) {
          console.warn(
            "[jsdom] Warning: skipping cleanup of global['" + key + "']"
          )
        }
        continue
      }

      keys.push(key)
      global[key] = window[key]
    }
  }

  /*
   * re-throws jsdom errors
   */

  function getError (errors) {
    var data = errors[0].data
    var err = data.error
    err.message = err.message + ' [jsdom]'

    // clean up stack trace
    if (err.stack) {
      err.stack = err.stack
        .split('\n')
        .reduce(function (list, line) {
          if (line.match(/node_modules.+(jsdom|mocha)/)) {
            return list
          }

          line = line
            .replace(/file:\/\/.*<script>/g, '<script>')
            .replace(/:undefined:undefined/g, '')
          list.push(line)
          return list
        }, [])
        .join('\n')
    }

    return err
  }
}

module.exports.rerequire = rerequire

/**
 * Requires a module via `require()`, but invalidates the cache so it may be
 * called again in the future. Useful for `mocha --watch`.
 *
 *     var rerequire = require('mocha-jsdom').rerequire
 *     var $ = rerequire('jquery')
 */

function rerequire (module) {
  if (module[0] === '.') {
    module = Path.join(Path.dirname(getCaller()), module)
  }

  var oldkeys = Object.keys(require.cache)
  var result = require(module)
  var newkeys = Object.keys(require.cache)
  newkeys.forEach(function (newkey) {
    if (!~oldkeys.indexOf(newkey)) {
      delete require.cache[newkey]
    }
  })
  return result
}

/**
 * Internal: gets the filename of the caller function. The `offset` defines how
 * many hops away it's expected to be from in the stack trace.
 *
 * See: http://stackoverflow.com/questions/16697791/nodejs-get-filename-of-caller-function
 */

function getCaller (offset) {
  /* eslint-disable handle-callback-err */
  if (typeof offset !== 'number') offset = 1
  var old = Error.prepareStackTrace
  var err = new Error()
  Error.prepareStackTrace = function (err, stack) {
    return stack
  }
  var fname = err.stack[1 + offset].getFileName()
  Error.prepareStackTrace = old
  return fname
}