aboutsummaryrefslogtreecommitdiffstats
path: root/src/scenario/Scenario.js
blob: e93f6b2e414e2ea65b0105644c406403f5826e42 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/**
 * Setup file for the Scenario. 
 * Must be first in the compilation/bootstrap list.
 */

// Public namespace
angular.scenario = {};

// Namespace for the UI
angular.scenario.ui = {};

/**
 * Defines a new DSL statement. If your factory function returns a Future
 * it's returned, otherwise the result is assumed to be a map of functions
 * for chaining. Chained functions are subject to the same rules.
 *
 * Note: All functions on the chain are bound to the chain scope so values
 *   set on "this" in your statement function are available in the chained
 *   functions.
 *
 * @param {String} The name of the statement
 * @param {Function} Factory function(application), return a function for 
 *  the statement.
 */
angular.scenario.dsl = function(name, fn) {
  angular.scenario.dsl[name] = function() {
    function executeStatement(statement, args) {
      var result = statement.apply(this, args);
      if (angular.isFunction(result) || result instanceof angular.scenario.Future)
        return result;
      var self = this;
      var chain = angular.extend({}, result);
      angular.foreach(chain, function(value, name) {
        if (angular.isFunction(value)) {
          chain[name] = angular.bind(self, function() {
            return executeStatement.call(self, value, arguments);
          });
        } else {
          chain[name] = value;
        }
      });
      return chain;
    }
    var statement = fn.apply(this, arguments);
    return function() {
      return executeStatement.call(this, statement, arguments);
    };
  };
};

/**
 * Defines a new matcher for use with the expects() statement. The value
 * this.actual (like in Jasmine) is available in your matcher to compare 
 * against. Your function should return a boolean. The future is automatically
 * created for you.
 *
 * @param {String} The name of the matcher
 * @param {Function} The matching function(expected).
 */
angular.scenario.matcher = function(name, fn) {
  angular.scenario.matcher[name] = function(expected) {
    var prefix = 'expect ' + this.future.name + ' ';
    if (this.inverse) {
      prefix += 'not ';
    }
    this.addFuture(prefix + name + ' ' + angular.toJson(expected),
      angular.bind(this, function(done) {
        this.actual = this.future.value;
        if ((this.inverse && fn.call(this, expected)) ||
            (!this.inverse && !fn.call(this, expected))) {
          this.error = 'expected ' + angular.toJson(expected) +
            ' but was ' + angular.toJson(this.actual);
        }
        done(this.error);
      })
    );
  };
};

/**
 * Iterates through list with iterator function that must call the
 * continueFunction to continute iterating.
 *
 * @param {Array} list to iterate over
 * @param {Function} Callback function(value, continueFunction)
 * @param {Function} Callback function(error, result) called when iteration 
 *   finishes or an error occurs.
 */
function asyncForEach(list, iterator, done) {
  var i = 0;
  function loop(error) {
    if (error || i >= list.length) {
      done(error);
    } else {
      try {
        iterator(list[i++], loop);
      } catch (e) {
        done(e);
      }
    }
  }
  loop();
}