index.js

'use strict'

var isNil = require('lodash/isNil')
var Differ = require('webjerk-image-set-diff')
var fs = require('fs-extra')
var debug = require('debug')('webjerk-snaps')
var path = require('path')

var DEFAULT_WINDOW_EXEC =  function () { return {}; } // eslint-disable-line

/**
 * Run arbitrary code before `snapDefinition.selector` is
 * captured.
 *
 * **WARNING** this code must be serializable, or serialized in
 * advanced.  This is because this code is run by a different process--a docker
 * processes.  Because we execute in a docker context, the docker process is
 * responsible for calling the method.  Therefore, make this code as simple
 * as possible, or if you absolutely must have complexity here (please don't),
 * you can bundle your JS into a string and eval it in.  PRs welcome if folks want
 * to build in rollup/webpack/etc to do this automatically.
 * @callback onPreSnap
 * @param {SnapDefinition} snapDefinition snap definition for element about to be captured
 * @param {string} browserName chrome, firefox, etc
 * @param {*} browserDriver browser adapter used to perform screenshots
 * @returns {Promise} can be async or sync.
 * @example
 * // example: run code in the browser context to ready it for snapping
 * // https://github.com/GoogleChrome/puppeteer/blob/6512ce768ddce790095e2201d8ada3c24407fc57/docs/api.md#pageevaluatepagefunction-args
 * {
 *   selector: '#test',
 *   name: 'test',
 *   onPreSnap: function revealElement (snapDefinition, browserName, browserDriver) {
 *     if (browserName.match(/chrome/i)) {
 *       browserDriver.evaluate(function revealElement (selector) {
 *         return window.myApp.show(selector)
 *       }, snapDefinition.selector)
 *     }
 *   }
 * }
 */

/**
 * Run code after a snap has been captured.
 * See {@link onPreSnap}.
 * @see {@link onPreSnap}
 * @callback onPostSnap
 */

/**
 * @typedef SnapDefinition
 * @property {string} selector css selector for single element to capture
 * @property {string} name basename for .png file
 * @property {onPreSnap} [onPreSnap] run code before a capture
 * @property {onPostSnap} [onPostSnap] run code after a capture
 */

/**
 * Enables executing code at to compute {@link SnapDefintion}s at runtime.
 * @function onSnapDefinitionsFromWindow
 * @returns {Promise}
 */

/**
 * @typedef SnapsConfig
 * @property {string} staticDirectory
 * @property {Number} [runId=Date.now()]
 * @property {string} [url]
 * @property {SnapDefinition[]} [snapDefinitions]
 * @property {onSnapDefinitionsFromWindow} [snapDefinitionsFromWindow]
 * @property {string} [snapRunRoot=`pwd`/snaps/run]
 * @property {string} [snapRefRoot=`pwd`/snaps/ref]
 * @property {boolean} [report=true] generate a static web-application to
 * highlight image changes. report will be placed into snaps/run/<run-id>-diff
 */

/**
 * @module webjerk-snaps
 * @description website visual regression testing plugin.  on webjerk's `main` cycle,
 * webjerk-snaps launches an adapter to _capture_ snaps. after snaps are captured,
 * the image directories (this run's image dir & the reference image dir) are sent to
 * `webjerk-image-set-diff` for executing the comparison algorithm.
 */
module.exports = function registerSnaps () {
  return Object.assign({}, {
    name: 'snaps',
    /**
     * webjerk main hook
     * @param {SnapsConfig} pluginConfig
     * @param {*} webjerkconfig
     */
    async main (pluginConfig, webjerkconfig) {
      var runId = Date.now().toString()
      this.conf = Object.assign(
        {
          adapter: 'webjerk-snaps-adapter-puppeteer',
          runId,
          snapRunRoot: path.join(process.cwd(), 'snaps', 'run', runId),
          snapRefRoot: path.join(process.cwd(), 'snaps', 'ref')
        },
        pluginConfig
      )
      await Promise.all([
        fs.mkdirp(this.conf.snapRunRoot),
        fs.mkdirp(this.conf.snapRefRoot)
      ])
      debug(`launching adapter ${this.conf.adapter}`)
      return require(this.conf.adapter).capture(this.conf)
    },
    /**
     * webjerk post hook
     * @param {*} pluginConfig
     * @param {*} webjerkconfig
     * @param {*} results
     */
    async post (pluginConfig, webjerkconfig, results) {
      if (!this.conf.snapRunRoot || !(await fs.exists(this.conf.snapRunRoot))) {
        throw new Error('unable to find run images')
      }
      var differ = Differ.factory({
        refDir: this.conf.snapRefRoot,
        runDir: this.conf.snapRunRoot,
        report: isNil(pluginConfig.report) ? true : pluginConfig.report
      })
      return differ.run()
    }
  })
}