API Docs for: 0.0.9
Show:

File: lib/dalek/suite.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 _ = require('lodash');
var fs = require('fs');
var EventEmitter2 = require('eventemitter2').EventEmitter2;

// int. libs
var unit = require('./unit');

/**
 * @constructor
 * @param {object} options
 */

var Suite = function (options) {
  this.emitter = new EventEmitter2();
  this.emitter.setMaxListeners(Infinity);
  this.initialize(options);
  this.suite = this.loadTestsuite(options.file);
};

/**
 * Suite (Testsuite)
 *
 * @module DalekJS
 * @class Suite
 * @namespace Dalek
 * @part Testsuite
 * @api
 */

Suite.prototype = {

  /**
   * Assigns the initial options
   * driverEmitter -> the drivers event dispatcher
   * reporterEmitter -> the reporters event dispatcher
   * driver -> the driver instance (e.g. native webdriver, selenium, etc.)
   * name -> the suites filename (default suite name)
   *
   * @method initialize
   * @param {object} options
   * @chainable
   */

  initialize: function (options) {
    this.driverEmitter = options.driverEmitter;
    this.reporterEmitter = options.reporterEmitter;
    this.driver = options.driver;
    this.name = options.file;
    this.numberOfSuites = options.numberOfSuites;
    this.error = null;
    return this;
  },

  /**
   * Loads the testsuite that should be executed
   *
   * @method loadTestsuite
   * @param {string} testfile
   * @return {object} testsuite
   */

  loadTestsuite: function (testfile) {
    var suite = {};

    // if the tests were passed in *as* a list of tests, just use them
    if (testfile && typeof testfile === 'object') {
      var allAreTestFunctions = true;
      for (var testname in testfile) {
        if (typeof testfile[testname] !== 'function') { allAreTestFunctions = false; }
      }
      if (allAreTestFunctions) {
        return testfile;
      }
    }

    // catch any errors, like falsy requires & stuff
    try {

      if (fs.existsSync(process.cwd() + '/' + testfile)) {
        suite = require(process.cwd() + '/' + testfile.replace('.js', ''));
      } else {
        this.error = 'Suite "' + testfile + '" does not exist. Skipping!';
        return suite;
      }
    } catch (e) {
      this.error = '\n' + e.name + ': ' + e.message + '\nFailure loading suite "' + testfile + '". Skipping!' + e;
      return suite;
    }

    suite._uid = _.uniqueId('Suite');
    return suite;
  },

  /**
   * Checks if all tests from the testsuite are executed.
   * Runs the next test if not.
   * Triggers `asyncs` callback if the suite is finished.
   * Decrements the `testsToBeExecuted` counter
   *
   * @method testFinished
   * @param {function} callback
   * @param {array} tests
   * @param {object} test
   * @param {string} event
   * @chainable
   */

  testFinished: function (callback, tests) {
    var complete = function() {
      // check if there are still tests that should be executed in this suite,
      // if so, run them
      if (this.decrementTestsToBeExecuted() > 1) {
        this.executeNextTest(tests);
        return this;
      }

      // run a function after the testsuite, if given
      if (this.options.teardown) {
        this.options.teardown();
      }

      // emit the suite finished event
      this.reporterEmitter.emit('report:testsuite:finished', this.name);

      // move on to the next suite
      callback();
      return this;
    }.bind(this);

    // run a function after the test, if given

    if (typeof this.options.afterEach === 'function') {
      // If there is an argument, assume async.
      if (this.options.afterEach.length === 1) {
        this.options.afterEach(function() {
          return complete();
        }.bind(this));
      } else {
        // no argument, assume sync.
        this.options.afterEach();
        return complete();
      }
    } else {
        return complete();
    }

  },

  /**
   * Decrements number of tests that should be executed in this suite
   *
   * @method decrementTestsToBeExecuted
   * @return {integer} numberOfTestsToBeExecuted
   */

  decrementTestsToBeExecuted: function () {
    return (this.testsToBeExecuted--) -1;
  },

  /**
   * Returns the name of the testsuite
   * If the suite has no name, it will return the testsuites filename
   *
   * @method getName
   * @return {string} name
   */

  getName: function () {
    if (this.suite.name && _.isString(this.suite.name)) {
      var name = this.suite.name;
      delete this.suite.name;
      return name;
    }

    return this.name;
  },

  /**
   * Returns the options of the testsuite
   * If the suite has no options, it will return an empty object
   *
   * @method getOptions
   * @return {object} options Suite options
   */

  getOptions: function () {
    if (this.suite.options && _.isObject(this.suite.options)) {
      var options = this.suite.options;
      delete this.suite.options;
      return options;
    }

    return {};
  },

  /**
   * Returns all names (aka. object keys) the tests that should be executed
   *
   * @method getTests
   * @return {array} test
   */

  getTests: function () {
    return Object.keys(this.suite);
  },

  /**
   * Returns the number of tests to be executed
   *
   * @method getNumberOfTests
   * @param {array} tests
   * @return {integer} numberOfTests
   */

  getNumberOfTests: function (tests) {
    return tests.length;
  },

  /**
   * Returns the next test, that should be executed
   *
   * @method getNextTest
   * @return {string} testName
   */

  getNextTest: function (tests) {
    return tests.shift();
  },

  /**
   * Executes the next test in the sequence
   *
   * @method executeNextTest
   * @param {array} tests
   * @return {mixed} testValue
   */

  executeNextTest: function (tests, callback) {
    var cb = callback || function() {};
    // grab the next test in the queue
    var testName = this.getNextTest(tests);
    // get the next test function
    var testFunction = this.getTest(testName);
    // generate an instance of the test
    var test = this.getTestInstance(testName);
    // run a setup function before the test, if given
    if (typeof this.options.beforeEach !== 'function') {
      cb(null, null);
      testFunction.apply(test,[test]);
      return this;
    }
    if (this.options.beforeEach.length === 1) {
      // if function takes an argument, assume async
      this.options.beforeEach(function() {
        // start it
        testFunction.apply(test,[test]);
        cb(null, null);
      });
    } else {
      // otherwise, assume sync
      this.options.beforeEach();
      testFunction.apply(test,[test]);
      cb(null, null);
    }
    return this;
  },

  /**
   * Generates a new test instance
   *
   * @method getTestInstance
   * @param {string} name
   * @return {Dalek.Test} test
   */

  getTestInstance: function (name) {
    return unit({events: this.emitter, driver: this.driver, reporter: this.reporterEmitter, name: name});
  },

  /**
   * Returns a test function by its name
   *
   * @method getTest
   * @param {string} name
   * @return {function} test
   */

  getTest: function (name) {
    return this.suite[name] && _.isFunction(this.suite[name]) ? this.suite[name] : this.testDoesNotExist;
  },

  /**
   * Will be executed if a test is started, that does not exist
   *
   * @method testDoesNotExist
   * @param {object} options
   */

  testDoesNotExist: function (options) {
    if (options.name) {
      this.reporterEmitter.emit('warning', 'Test "' + options.name + '" does not exist! Skipping.');
    }
    return this;
  },

  /**
   * Runs any tests from this testsuite in sequence
   *
   * @method run
   * @param {function} callback
   * @chainable
   */

  run: function (callback) {
    var tests = [];

    // check if the suite is
    if (this.error) {
      this.reporterEmitter.emit('report:testsuite:started', null);
      // emit a warning notice
      this.reporterEmitter.emit('warning', this.error);
      // emit the suite finished event
      this.reporterEmitter.emit('report:testsuite:finished', null);
      // move on to the next suite
      callback();
    }

    // extract suite name
    this.name = this.getName();
    // extract suite options
    this.options = this.getOptions();

    // extract tests
    tests = this.getTests();
    this.testsToBeExecuted = this.numberOfTests = this.getNumberOfTests(tests);

    // run a function before the testsuite has been launched, if given
    if (this.options.setup) {
      this.options.setup();
    }

    // kickstart the test execution
    this.executeNextTest(tests, function() {
      // emit the suite started event
      this.reporterEmitter.emit('report:testsuite:started', this.name);
      // listen to the test:finished event & then start the next test
      // if there are no tests in this suite left,
      // run the async callback & mark this suite as finished
      this.emitter.onAny(this.testFinished.bind(this, callback, tests));
    }.bind(this));

    return this;
  }
};

// export the testuite instance
module.exports = Suite;