API Docs for: 0.0.9
Show:

File: lib/dalek/unit.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 Q = require('q');

// Int. libs
var actions = require('./actions');
var assertions = require('./assertions');

// Default timeout for calling done
var DONE_TIMEOUT = 10000;

/**
 * Prepares the test instance values
 *
 * @param {object} opts Options like the tests name, etc.
 * @constructor
 */

var Unit = function (opts) {
  // prepare meta queue data
  this.actionPromiseQueue = [];

  // prepare assertion data
  this.expectation = null;
  this.runnedExpactations = 0;
  this.failedAssertions = 0;

  // prepare test specific data
  this.name = opts.name;
  this.lastChain = [];
  this.uuids = {};
  this.contextVars = {};

  if (this.name) {
    this.timeoutForDone = setTimeout(function () {
      this.done();
      this.reporter.emit('warning', 'done() not called before timeout!');
    }.bind(this), DONE_TIMEOUT);
  }
};

/**
 * Generates an test instance
 *
 * @module DalekJS
 * @class Unit
 * @namespace Dalek
 * @part test
 * @api
 */

Unit.prototype = {

  /**
   * Specify how many assertions are expected to run within a test.
   * Very useful for ensuring that all your callbacks and assertions are run.
   *
   * @method expect
   * @param {Integer} expecatation Number of assertions that should be run
   * @chainable
   */

  expect: function (expectation) {
    this.expectation = parseInt(expectation, 10);
    return this;
  },

  /**
   * Global data store (works between the node & browser envs)
   *
   * @method data
   * @param {string|number} key Key to store or fetch data
   * @param {mixed} value *optional* Data that should be stored
   * @return {mixed} Data that has been stored
   * @chainable
   */

  data: function (key, value) {
    if (value) {
      this.contextVars[key] = value;
      return this;
    }

    return this.contextVars[key];
  },

  /**
   * Increment the number of executed assertions
   *
   * @method incrementExpectations
   * @chainable
   */

  incrementExpectations: function () {
    this.runnedExpactations++;
    return this;
  },

  /**
   * Increment the number of failed assertions
   *
   * @method incrementFailedAssertions
   * @chainable
   */

  incrementFailedAssertions: function () {
    this.failedAssertions++;
    return this;
  },

  /**
   * Checks if the runned tests fullfill the set expectations
   * or if no expectations were raised
   *
   * @method checkExpectations
   * @return {bool} checkedExpectations Expectations match
   */

  checkExpectations: function () {
    return (this.expectation === null || !this.expectation || (this.runnedExpactations === this.expectation));
  },

  /**
   * Checks if all runned assertions passed
   *
   * @method checkAssertions
   * @return {bool} assertionFailed Any expectation failed
   */

  checkAssertions: function () {
    return this.failedAssertions === 0;
  },

  /**
   * Sets up all the bindings needed for a test to run
   *
   * @method done
   * @return {object} result A promise
   * @private
   */

  done: function () {
    var result = Q.resolve();
    // clear the done error timeout
    clearTimeout(this.timeoutForDone);
    // remove all previously attached event listeners to clear the message queue
    this.driver.events.removeAllListeners('driver:message');
    // resolve the deferred when the test is finished
    Unit.testStarted.fin(this._testFinished.bind(this, result));
    return result;
  },

  /**
   * Allow to use custom functions in order to embrace code reuse across
   * multiple files (for example for use in Page Objects).
   *
   * @method andThen
   * @param {function} a function, where 'this' references the test
   * @chainable
   */
  andThen: function(func) {
    return func.call(this);
  },

  /**
   * Adds a node style function (with node err callback) style to the test.
   *
   * @method node
   * @param {function} a node function that is executed in the context of the test.
   * @chainable
   */
  node: function(nodeFunction) {
    var deferred = Q.defer(),
      that = this;
    nodeFunction.call(this, function(err) {
      if (typeof err !== 'undefined') {
        that.reporter.emit('error', err);
        that.incrementFailedAssertions();
        deferred.reject();
      } else {
        deferred.resolve();
      }
    });
    this.promise(deferred);
    return this;
  },

  /**
   * Adds a promise to the chain of tests.
   *
   * @method promise
   * @param {promise} a q promise
   * @chainable
   */
  promise: function(deferred) {
    this.actionPromiseQueue.push(deferred);
    return this;
  },

  /**
   * Emits the test finished events & resolves all promises
   * when its done
   *
   * @method _testFinished
   * @param {object} result Promised result var
   * @return {object} result Promised result var
   * @private
   */

  _testFinished: function (result) {
    // add a last deferred function on the end of the action queue,
    // to tell that this test is finished
    this.actionPromiseQueue.push(this._testFin.bind(this));

    // initialize all of the event receiver functions,
    // that later take the driver result
    this.actionPromiseQueue.forEach(function (f) {
      result = result.then(f).fail(function () {
        console.error(arguments);
        process.exit(0);
      });
    }.bind(this));

    // run the driver when all actions are stored in the queue
    Q.allSettled(this.actionPromiseQueue)
      .then(this.driver.run.bind(this.driver));

    return result;
  },

  /**
   * Emits the test started event
   *
   * @method _reportTestStarted
   * @param {string} name Name of the test
   * @chainable
   * @private
   */

  _reportTestStarted: function (name) {
    this.reporter.emit('report:test:started', {name: name});
    return this;
  },

  /**
   * Checks if the test run is complete & emits/resolves
   * all the needed events/promises when the run is complete
   *
   * @method _onDriverMessage
   * @param {object} data Data that is returned by the driver:message event
   * @chainable
   * @private
   */

  _onDriverMessage: function (data) {
    // check if the test run is complete
    if (data && data.key === 'run.complete') {
      // emit the test finish events & resolve the deferred
      this._emitConcreteTestFinished();
      this._emitAssertionStatus();
      this._emitTestFinished();
      this.deferred.resolve();
    }

    return this;
  },

  /**
   * Emits an event, that the current test run has been finished
   *
   * @method _emitConcreteTestFinished
   * @chainable
   * @private
   */

  _emitConcreteTestFinished: function () {
    this.events.emit('test:' + this._uid + ':finished', 'test:finished', this);
    return this;
  },

  /**
   * Emits an event that describes the current state of all assertions
   *
   * @method _emitAssertionStatus
   * @chainable
   * @private
   */

  _emitAssertionStatus: function () {
    this.reporter.emit('report:assertion:status', {
      expected: (this.expectation ? this.expectation : this.runnedExpactations),
      run: this.runnedExpactations,
      status: this._testStatus()
    });
    return this;
  },

  /**
   * Get the overall test status (assertions & expectation)
   *
   * @method _testStatus
   * @return {bool} status The test status
   * @chainable
   * @private
   */

  _testStatus: function () {
    return this.checkExpectations() && this.checkAssertions();
  },

  /**
   * Emits an event that describes the current state of all assertions.
   * The event should be fired when a test is finished
   *
   * @method _emitTestFinished
   * @chainable
   * @private
   */

  _emitTestFinished: function () {
    this.reporter.emit('report:test:finished', {
      name: this.name,
      id: this._uid,
      passedAssertions: this.runnedExpactations - this.failedAssertions,
      failedAssertions: this.failedAssertions,
      runnedExpactations: this.runnedExpactations,
      status: this._testStatus(),
      nl: true
    });

    return this;
  },

  /**
   * Kicks off the test & binds all promises/events
   *
   * @method _testFin
   * @return {object} promise A promise
   * @private
   */

  _testFin: function () {
    this.deferred = Q.defer();

    if (_.isFunction(this.driver.end)) {
      this.driver.end();
    }

    // emit report startet event
    this._reportTestStarted(this.name);

    // listen to all the messages from the driver
    this.driver.events.on('driver:message', this._onDriverMessage.bind(this));
    return this.deferred.promise;
  },

  /**
   * Copies assertion methods
   *
   * @method _inheritAssertions
   * @param {Test} test Instacne of test
   * @chainable
   * @private
   */

  _inheritAssertions: function (test) {
    ['is'].forEach(function (method) {
      test[method] = test.assert[method].bind(test.assert);
    });
    return test;
  },

  /**
   * Copies assertion helper methods
   *
   * @method _inheritAssertions
   * @param {Test} test Instacne of test
   * @chainable
   * @private
   */

  _inheritAssertionHelpers: function (test) {
    ['not', 'between', 'gt', 'gte', 'lt', 'lte', 'equalsCaseInsensitive'].forEach(function (method) {
      test.is[method] = test.assert[method].bind(test.assert);
      test.assert.is[method] = test.assert[method].bind(test.assert);
    });
    ['contain', 'match'].forEach(function (method) {
      test.to = test.to || {};
      test.assert.to = test.assert.to || {};

      test.to[method] = test.assert[method].bind(test.assert);
      test.assert.to[method] = test.assert[method].bind(test.assert);
    });
    ['notContain'].forEach(function (method) {
      var apiName = method.substr(3, 1).toLowerCase() + method.substr(4);
      test.to.not = test.to.not || {};
      test.assert.to.not = test.assert.to.not || {};

      test.to.not[apiName] = test.assert[method].bind(test.assert);
      test.assert.to.not[apiName] = test.assert[method].bind(test.assert);
    });
    return test;
  },

  /**
   * Set up the instance
   *
   * @method _inheritAssertions
   * @param {Test} test Instacne of test
   * @param {object} opts Options
   * @chainable
   * @private
   */

  _initialize: function (test, opts) {
    test._uid = _.uniqueId('test');
    test.events = opts.events;
    test.driver = opts.driver;
    test.reporter = opts.reporter;
    return test;
  }

};

/**
 * Alias for 'andThen'; use if it is the first function called in the test case.
 *
 * @method start
 * @chainable
 */
Unit.prototype.start = Unit.prototype.andThen;

// export a function that generates a new test instance
module.exports = function (opts) {
  // mixin assertions, actions & getters
  Unit.prototype = _.extend(Unit.prototype, actions({reporter: opts.reporter}).prototype);
  var unit = new Unit(opts);
  unit.assert = new (assertions())({test: unit});
  unit.assert.done = unit.done.bind(this);
  unit.assert.query = unit.query.bind(unit.assert);
  unit.assert.$ = unit.query.bind(unit.assert);
  unit.end = unit.assert.end.bind(unit.assert);

  // copy log methods
  unit.log = {};
  unit.log.dom = unit.logger.dom.bind(unit);
  unit.log.message = unit.logger.message.bind(unit);

  // copy assertions methods
  unit = unit._inheritAssertions(unit);

  // copy assertion helper methods
  unit = unit._inheritAssertionHelpers(unit);

  // initialize the instance
  unit = unit._initialize(unit, opts);

  // TODO: Promise driver start
  // so that we can reexecute them and clean the env between tests
  Unit.testStarted = unit.driver.start(Q);
  return unit;
};