API Docs for: 0.0.9
Show:

File: lib/dalek.js

/*!
 *
 * Copyright (c) 2013 Sebastian Golasch
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

'use strict';

// ext. libs
var async = require('async');
var EventEmitter2 = require('eventemitter2').EventEmitter2;

// int. libs
var Driver = require('./dalek/driver');
var Reporter = require('./dalek/reporter');
var Timer = require('./dalek/timer');
var Config = require('./dalek/config');
var Host = require('./dalek/host');

/**
 * Default options
 * @type {Object}
 */

var defaults = {
  reporter: ['console'],
  driver: ['native'],
  browser: ['phantomjs'],
  viewport: {width: 1280, height: 1024},
  logLevel: 3
};

/**
 * Setup all the options needed to configure dalek
 *
 * @param {options} opts Configuration options
 * @constructor
 */

var Dalek = function (opts) {
  // setup instance
  this._initialize();

  // register exception handler
  this._registerExceptionHandler();

  // normalize options
  this.options = this.normalizeOptions(opts);

  // getting advanced options
  if (opts && opts.advanced) {
    this.advancedOptions = opts.advanced;
  }

  // initiate config
  this.config = new Config(defaults, this.options, this.advancedOptions);

  // override tests if provided on the commandline
  if (this.options.tests) {
    this.config.config.tests = this.options.tests;
  }

  // prepare and load reporter(s)
  this._setupReporters();

  // count all passed & failed assertions
  this.reporterEvents.on('report:assertion', this._onReportAssertion.bind(this));

  // init the timer instance
  this.timer = new Timer();

  // prepare driver event emitter instance
  this._setupDriverEmitter();

  // check for file option, throw error if none is given
  // only bypasses if dalek runs in the remote mode
  if (!Array.isArray(this.config.get('tests')) && !this.options.remote) {
    this.reporterEvents.emit('error', 'No test files given!');
    this.driverEmitter.emit('killAll');
    process.exit(127);
  }

  // init the driver instance
  this._initDriver();

  // check if dalek runs as a remote browser launcher
  if (this.options.remote) {
    var host = new Host({reporterEvents: this.reporterEvents, config: this.config});
    host.run({
      port: !isNaN(parseFloat(this.options.remote)) && isFinite(this.options.remote) ? this.options.remote : false
    });
  }
};

/**
 * Daleks base module
 * Used to configure all the things
 * and to start off the tests
 *
 * @module DalekJS
 * @class Dalek
 */

Dalek.prototype = {

  /**
   * Runs the configured testsuites
   *
   * @method run
   * @chainable
   */

  run: function () {
    // early return; in case of remote
    if (this.options.remote) {
      return this;
    }

    // start the timer to measure the execution time
    this.timer.start();

    // emit the runner started event
    this.reporterEvents.emit('report:runner:started');

    // execute all given drivers sequentially
    var drivers = this.driver.getDrivers();
    async.series(drivers, this.testsuitesFinished.bind(this));
    return this;
  },

  /**
   * Reports the all testsuites executed event
   *
   * @method testsuitesFinished
   * @chainable
   */

  testsuitesFinished: function () {
    this.driverEmitter.emit('tests:complete');
    setTimeout(this.reportRunFinished.bind(this), 0);
    return this;
  },

  /**
   * Reports the all testsuites executed event
   *
   * @method reportRunFinished
   * @chainable
   */

  reportRunFinished: function () {
    this.reporterEvents.emit('report:runner:finished', {
      elapsedTime: this.timer.stop().getElapsedTimeFormatted(),
      assertions: this.assertionsFailed + this.assertionsPassed,
      assertionsFailed: this.assertionsFailed,
      assertionsPassed: this.assertionsPassed,
      status: this.runnerStatus
    });

    //we want to exit process with code 1 to single that test did not pass
    if(this.runnerStatus !== true) {
      var processExitCaptured = false;

      process.on('exit', function() {
        if(processExitCaptured === false) {
          processExitCaptured = true;
          process.exit(1);
        }
      });
    }

    return this;
  },

  /**
   * Normalizes options
   *
   * @method normalizeOptions
   * @param {object} options Raw options
   * @return {object} Normalized options
   */

  normalizeOptions: function (options) {
    Object.keys(options).forEach(function (key) {
      if ({reporter: 1, driver: 1}[key]) {
        options[key] = options[key].map(function (input) { return input.trim(); });
      }
    });

    return options;
  },

  /**
   * Sets up system env properties
   *
   * @method _initialize
   * @chainable
   * @private
   */

  _initialize: function () {
    // prepare error data
    this.warnings = [];
    this.errors = [];

    // prepare state data for the complete test run
    this.runnerStatus = true;
    this.assertionsFailed = 0;
    this.assertionsPassed = 0;

    return this;
  },

  /**
   * Sets up all the reporters
   *
   * @method _setupReporters
   * @chainable
   * @private
   */

  _setupReporters: function () {
    this.reporters = [];
    this.reporterEvents = new EventEmitter2();
    this.reporterEvents.setMaxListeners(Infinity);
    this.options.reporter = this.config.verifyReporters(this.config.get('reporter'), Reporter);
    this.options.reporter.forEach(this._addReporter, this);
    return this;
  },

  /**
   * Adds a reporter
   *
   * @method _addReporter
   * @param {string} reporter Name of the reporter to add
   * @chainable
   * @private
   */

  _addReporter: function (reporter) {
    this.reporters.push(Reporter.loadReporter(reporter, {events: this.reporterEvents, config: this.config, logLevel: this.config.get('logLevel')}));
    return this;
  },

  /**
   * Updates the assertion state
   *
   * @method _onReportAssertion
   * @param {object} assertion Informations aout the runned assertions
   * @chainable
   * @private
   */

  _onReportAssertion: function (assertion) {
    if (assertion.success) {
      this.assertionsPassed++;
    } else {
      this.runnerStatus = false;
      this.assertionsFailed++;
    }
    return this;
  },

  /**
   * Initizializes the driver instances
   *
   * @method _initDriver
   * @chainable
   * @private
   */

  _initDriver: function () {
    this.driver = new Driver({
      config: this.config,
      driverEmitter: this.driverEmitter,
      reporterEvents: this.reporterEvents
    });

    this.options.driver = this.config.verifyDrivers(this.config.get('driver'), this.driver);
    return this;
  },

  /**
   * Sets up the event dispatcher for driver events
   *
   * @method _setupDriverEmitter
   * @chainable
   * @private
   */

  _setupDriverEmitter: function () {
    var driverEmitter = new EventEmitter2();
    driverEmitter.setMaxListeners(Infinity);
    this.driverEmitter = driverEmitter;
    return this;
  },

  /**
   * Make sure to shutdown dalek & its spawned
   * components, webservers gracefully if a
   * runtime error pops up
   *
   * @method _registerExceptionHandler
   * @private
   * @chainable
   */

  _registerExceptionHandler: function () {
    process.setMaxListeners(Infinity);
    process.on('uncaughtException', this._shutdown.bind(this));
    return this;
  },

  /**
   * Shutdown on uncaught exception
   *
   * @method _shutdown
   * @param {object} exception Runtime exception
   * @private
   */

  _shutdown: function (exception) {
    // ios emulator hack, needs to go in the future
    if (exception.message && exception.message.search('This socket has been ended by the other party') !== -1) {
      return false;
    }

    this.driverEmitter.emit('killAll');
    this.reporterEvents.emit('error', exception);
  }

};

// export dalek as a module
module.exports = Dalek;