diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/scenario/Describe.js | 10 | ||||
| -rw-r--r-- | src/scenario/Future.js | 18 | ||||
| -rw-r--r-- | src/scenario/HtmlUI.js | 102 | ||||
| -rw-r--r-- | src/scenario/Runner.js | 7 | ||||
| -rw-r--r-- | src/scenario/Scenario.js | 97 | ||||
| -rw-r--r-- | src/scenario/SpecRunner.js | 61 | ||||
| -rw-r--r-- | src/scenario/dsl.js | 56 | 
7 files changed, 255 insertions, 96 deletions
diff --git a/src/scenario/Describe.js b/src/scenario/Describe.js index 896b337f..f6a52f1e 100644 --- a/src/scenario/Describe.js +++ b/src/scenario/Describe.js @@ -78,12 +78,10 @@ angular.scenario.Describe.prototype.it = function(name, body) {    var self = this;    this.its.push({      definition: this, -    name: name,  -    fn: function() { -      self.setupBefore.call(this); -      body.call(this); -      self.setupAfter.call(this); -    } +    name: name, +    before: self.setupBefore, +    body: body, +    after: self.setupAfter    });  }; diff --git a/src/scenario/Future.js b/src/scenario/Future.js index 30c2d902..8853aa3f 100644 --- a/src/scenario/Future.js +++ b/src/scenario/Future.js @@ -1,12 +1,17 @@  /**   * A future action in a spec. + * + * @param {String} name of the future action + * @param {Function} future callback(error, result) + * @param {String} Optional. function that returns the file/line number.   */ -angular.scenario.Future = function(name, behavior) { +angular.scenario.Future = function(name, behavior, line) {    this.name = name;    this.behavior = behavior;    this.fulfilled = false;    this.value = undefined;    this.parser = angular.identity; +  this.line = line || function() { return ''; };  };  /** @@ -15,18 +20,19 @@ angular.scenario.Future = function(name, behavior) {   * @param {Function} Callback function(error, result)   */  angular.scenario.Future.prototype.execute = function(doneFn) { -  this.behavior(angular.bind(this, function(error, result) { -    this.fulfilled = true; +  var self = this; +  this.behavior(function(error, result) { +    self.fulfilled = true;      if (result) {        try { -        result = this.parser(result); +        result = self.parser(result);        } catch(e) {          error = e;        }      } -    this.value = error || result; +    self.value = error || result;      doneFn(error, result); -  })); +  });  };  /** diff --git a/src/scenario/HtmlUI.js b/src/scenario/HtmlUI.js index a93ed1e3..78fe8c33 100644 --- a/src/scenario/HtmlUI.js +++ b/src/scenario/HtmlUI.js @@ -21,23 +21,29 @@ angular.scenario.ui.Html = function(context) {  };  /** + * The severity order of an error. + */ +angular.scenario.ui.Html.SEVERITY = ['pending', 'success', 'failure', 'error']; + +/**   * Adds a new spec to the UI.   *   * @param {Object} The spec object created by the Describe object.   */  angular.scenario.ui.Html.prototype.addSpec = function(spec) { +  var self = this;    var specContext = this.findContext(spec.definition);    specContext.find('> .tests').append(      '<li class="status-pending test-it"></li>'    );    specContext = specContext.find('> .tests li:last'); -  return new angular.scenario.ui.Html.Spec(specContext, spec.name, -    angular.bind(this, function(status) { -      status = this.context.find('#status-legend .status-' + status); +  return new angular.scenario.ui.Html.Spec(specContext, spec.name,  +    function(status) { +      status = self.context.find('#status-legend .status-' + status);        var parts = status.text().split(' ');        var value = (parts[0] * 1) + 1;        status.text(value + ' ' + parts[1]); -    }) +    }    );  }; @@ -47,6 +53,7 @@ angular.scenario.ui.Html.prototype.addSpec = function(spec) {   * @param {Object} The definition created by the Describe object.   */  angular.scenario.ui.Html.prototype.findContext = function(definition) { +  var self = this;    var path = [];    var currentContext = this.context.find('#specs');    var currentDefinition = definition; @@ -54,9 +61,9 @@ angular.scenario.ui.Html.prototype.findContext = function(definition) {      path.unshift(currentDefinition);      currentDefinition = currentDefinition.parent;    } -  angular.foreach(path, angular.bind(this, function(defn) { +  angular.foreach(path, function(defn) {      var id = 'describe-' + defn.id; -    if (!this.context.find('#' + id).length) { +    if (!self.context.find('#' + id).length) {        currentContext.find('> .test-children').append(          '<div class="test-describe" id="' + id + '">' +          '  <h2></h2>' + @@ -64,10 +71,10 @@ angular.scenario.ui.Html.prototype.findContext = function(definition) {          '  <ul class="tests"></ul>' +          '</div>'        ); -      this.context.find('#' + id).find('> h2').text('describe: ' + defn.name); +      self.context.find('#' + id).find('> h2').text('describe: ' + defn.name);      } -    currentContext = this.context.find('#' + id); -  })); +    currentContext = self.context.find('#' + id); +  });    return this.context.find('#describe-' + definition.id);  }; @@ -90,9 +97,24 @@ angular.scenario.ui.Html.Spec = function(context, name, doneFn) {      '    <span class="test-name"></span>' +      '  </p>' +      '</div>' + -    '<ol class="test-actions">' + -    '</ol>' +    '<div class="scrollpane">' + +    '  <ol class="test-actions">' + +    '  </ol>' + +    '</div>'    ); +  context.find('> .test-info').click(function() { +    var scrollpane = context.find('> .scrollpane'); +    var actions = scrollpane.find('> .test-actions'); +    var name = context.find('> .test-info .test-name'); +    if (actions.find(':visible').length) { +      actions.hide(); +      name.removeClass('open').addClass('closed'); +    } else { +      actions.show(); +      scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight')); +      name.removeClass('closed').addClass('open'); +    } +  });    context.find('> .test-info .test-name').text('it ' + name);  }; @@ -100,13 +122,20 @@ angular.scenario.ui.Html.Spec = function(context, name, doneFn) {   * Adds a new Step to this spec and returns it.   *   * @param {String} The name of the step. + * @param {Function} function() that returns a string with the file/line number + *  where the step was added from.   */ -angular.scenario.ui.Html.Spec.prototype.addStep = function(name) { -  this.context.find('> .test-actions').append('<li class="status-pending"></li>'); -  var stepContext = this.context.find('> .test-actions li:last'); +angular.scenario.ui.Html.Spec.prototype.addStep = function(name, location) { +  this.context.find('> .scrollpane .test-actions').append('<li class="status-pending"></li>'); +  var stepContext = this.context.find('> .scrollpane .test-actions li:last');    var self = this; -  return new angular.scenario.ui.Html.Step(stepContext, name, function(status) { -    self.status = status; +  return new angular.scenario.ui.Html.Step(stepContext, name, location, function(status) { +    if (indexOf(angular.scenario.ui.Html.SEVERITY, status) > +      indexOf(angular.scenario.ui.Html.SEVERITY, self.status)) { +      self.status = status; +    } +    var scrollpane = self.context.find('> .scrollpane'); +    scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));    });  }; @@ -118,6 +147,10 @@ angular.scenario.ui.Html.Spec.prototype.complete = function() {    var endTime = new Date().getTime();    this.context.find("> .test-info .timer-result").      text((endTime - this.startTime) + "ms"); +  if (this.status === 'success') { +    this.context.find('> .test-info .test-name').addClass('closed'); +    this.context.find('> .scrollpane .test-actions').hide(); +  }  };  /** @@ -125,15 +158,8 @@ angular.scenario.ui.Html.Spec.prototype.complete = function() {   *   * @param {Object} An optional error   */ -angular.scenario.ui.Html.Spec.prototype.finish = function(error) { +angular.scenario.ui.Html.Spec.prototype.finish = function() {    this.complete(); -  if (error) { -    if (this.status !== 'failure') { -      this.status = 'error'; -    } -    this.context.append('<pre></pre>'); -    this.context.find('pre:first').text(error.stack || error.toString()); -  }    this.context.addClass('status-' + this.status);    this.doneFn(this.status);  }; @@ -144,7 +170,10 @@ angular.scenario.ui.Html.Spec.prototype.finish = function(error) {   * @param {Object} Required error   */  angular.scenario.ui.Html.Spec.prototype.error = function(error) { -  this.finish(error); +  this.status = 'error'; +  this.context.append('<pre></pre>'); +  this.context.find('> pre').text(formatException(error)); +  this.finish();  };  /** @@ -152,28 +181,39 @@ angular.scenario.ui.Html.Spec.prototype.error = function(error) {   *   * @param {Object} The jQuery object for the context of the step.   * @param {String} The name of the step. + * @param {Function} function() that returns file/line number of step.   * @param {Function} Callback function(status) to call when complete.   */ -angular.scenario.ui.Html.Step = function(context, name, doneFn) { +angular.scenario.ui.Html.Step = function(context, name, location, doneFn) {    this.context = context;    this.name = name; +  this.location = location;    this.startTime = new Date().getTime();    this.doneFn = doneFn;    context.append( -    '<span class="timer-result"></span>' + -    '<span class="test-title"></span>' +    '<div class="timer-result"></div>' + +    '<div class="test-title"></div>'    );    context.find('> .test-title').text(name); +  var scrollpane = context.parents('.scrollpane'); +  scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));  };  /**   * Completes the step and sets the timer value.   */ -angular.scenario.ui.Html.Step.prototype.complete = function() { +angular.scenario.ui.Html.Step.prototype.complete = function(error) {    this.context.removeClass('status-pending');    var endTime = new Date().getTime();    this.context.find(".timer-result").      text((endTime - this.startTime) + "ms"); +  if (error) { +    if (!this.context.find('.test-title pre').length) { +      this.context.find('.test-title').append('<pre></pre>'); +    } +    var message = _jQuery.trim(this.location() + '\n\n' + formatException(error)); +    this.context.find('.test-title pre').text(message); +  }  };  /** @@ -182,7 +222,7 @@ angular.scenario.ui.Html.Step.prototype.complete = function() {   * @param {Object} An optional error   */  angular.scenario.ui.Html.Step.prototype.finish = function(error) { -  this.complete(); +  this.complete(error);    if (error) {      this.context.addClass('status-failure');      this.doneFn('failure'); @@ -198,7 +238,7 @@ angular.scenario.ui.Html.Step.prototype.finish = function(error) {   * @param {Object} Required error   */  angular.scenario.ui.Html.Step.prototype.error = function(error) { -  this.complete(); +  this.complete(error);    this.context.addClass('status-error');    this.doneFn('error');  }; diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js index 55360592..a8b23f83 100644 --- a/src/scenario/Runner.js +++ b/src/scenario/Runner.js @@ -91,6 +91,7 @@ angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClas      });      angular.foreach(angular.scenario.dsl, function(fn, key) {        self.$window[key] = function() { +        var line = callerFile(3);          var scope = angular.scope(runner);          // Make the dsl accessible on the current chain @@ -103,10 +104,12 @@ angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClas          // Make these methods work on the current chain          scope.addFuture = function() { -          return angular.scenario.SpecRunner.prototype.addFuture.apply(scope, arguments); +          Array.prototype.push.call(arguments, line); +          return specRunnerClass.prototype.addFuture.apply(scope, arguments);          };          scope.addFutureAction = function() { -          return angular.scenario.SpecRunner.prototype.addFutureAction.apply(scope, arguments); +          Array.prototype.push.call(arguments, line); +          return specRunnerClass.prototype.addFutureAction.apply(scope, arguments);          };          return scope.dsl[key].apply(scope, arguments); diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js index 979210e5..b1782cf2 100644 --- a/src/scenario/Scenario.js +++ b/src/scenario/Scenario.js @@ -32,9 +32,9 @@ angular.scenario.dsl = angular.scenario.dsl || function(name, fn) {        var chain = angular.extend({}, result);        angular.foreach(chain, function(value, name) {          if (angular.isFunction(value)) { -          chain[name] = angular.bind(self, function() { +          chain[name] = function() {              return executeStatement.call(self, value, arguments); -          }); +          };          } else {            chain[name] = value;          } @@ -63,17 +63,18 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {      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); +    var self = this; +    this.addFuture(prefix + name + ' ' + angular.toJson(expected),  +      function(done) { +        var error; +        self.actual = self.future.value; +        if ((self.inverse && fn.call(self, expected)) || +            (!self.inverse && !fn.call(self, expected))) { +          error = 'expected ' + angular.toJson(expected) + +            ' but was ' + angular.toJson(self.actual);          } -        done(this.error); -      }) -    ); +        done(error); +    });    };  }; @@ -88,7 +89,10 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {   */  function asyncForEach(list, iterator, done) {    var i = 0; -  function loop(error) { +  function loop(error, index) { +    if (index && index > i) { +      i = index; +    }      if (error || i >= list.length) {        done(error);      } else { @@ -102,7 +106,63 @@ function asyncForEach(list, iterator, done) {    loop();  } +/** + * Formats an exception into a string with the stack trace, but limits + * to a specific line length. + * + * @param {Object} the exception to format, can be anything throwable + * @param {Number} Optional. max lines of the stack trace to include + *  default is 5. + */ +function formatException(error, maxStackLines) { +  maxStackLines = maxStackLines || 5; +  var message = error.toString(); +  if (error.stack) { +    var stack = error.stack.split('\n'); +    if (stack[0].indexOf(message) === -1) { +      maxStackLines++; +      stack.unshift(error.message); +    } +    message = stack.slice(0, maxStackLines).join('\n'); +  } +  return message; +} +/** + * Returns a function that gets the file name and line number from a  + * location in the stack if available based on the call site. + * + * Note: this returns another function because accessing .stack is very + * expensive in Chrome. + */ +function callerFile(offset) { +  var error = new Error(); +   +  return function() { +    var line = (error.stack || '').split('\n')[offset]; +   +    // Clean up the stack trace line +    if (line) { +      if (line.indexOf('@') !== -1) { +        // Firefox +        line = line.substring(line.indexOf('@')+1); +      } else { +        // Chrome +        line = line.substring(line.indexOf('(')+1).replace(')', ''); +      } +    } +     +    return line || ''; +  }; +} + +/** + * Triggers a browser event. Attempts to choose the right event if one is + * not specified. + * + * @param {Object} Either a wrapped jQuery/jqLite node or a DOMElement + * @param {String} Optional event type. + */  function browserTrigger(element, type) {    if (element && !element.nodeName) element = element[0];    if (!element) return; @@ -136,10 +196,17 @@ function browserTrigger(element, type) {    }  } +/** + * Don't use the jQuery trigger method since it works incorrectly. + * + * jQuery notifies listeners and then changes the state of a checkbox and + * does not create a real browser event. A real click changes the state of  + * the checkbox and then notifies listeners. + *  + * To work around this we instead use our own handler that fires a real event. + */  _jQuery.fn.trigger = function(type) {    return this.each(function(index, node) {      browserTrigger(node, type);    });  }; - - diff --git a/src/scenario/SpecRunner.js b/src/scenario/SpecRunner.js index 26fa9b91..98ce4b53 100644 --- a/src/scenario/SpecRunner.js +++ b/src/scenario/SpecRunner.js @@ -8,6 +8,7 @@   */  angular.scenario.SpecRunner = function() {    this.futures = []; +  this.afterIndex = 0;  };  /** @@ -20,32 +21,52 @@ angular.scenario.SpecRunner = function() {   * @param {Function} Callback function that is called when the  spec finshes.   */  angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) { +  var self = this;    var specUI = ui.addSpec(spec);    try { -    spec.fn.call(this); +    spec.before.call(this); +    spec.body.call(this); +    this.afterIndex = this.futures.length; +    spec.after.call(this);    } catch (e) {      specUI.error(e);      specDone();      return;    } +  var handleError = function(error, done) { +    if (self.error) { +      return done(); +    } +    self.error = true; +    done(null, self.afterIndex); +  }; +   +  var spec = this;    asyncForEach(      this.futures,      function(future, futureDone) { -      var stepUI = specUI.addStep(future.name); +      var stepUI = specUI.addStep(future.name, future.line);        try {          future.execute(function(error) {            stepUI.finish(error); -          futureDone(error); +          if (error) { +            return handleError(error, futureDone); +          } +          spec.$window.setTimeout( function() { futureDone(); }, 0);          });        } catch (e) {          stepUI.error(e); -        throw e; +        handleError(e, futureDone);        }      },      function(e) { -      specUI.finish(e); +      if (e) { +        specUI.error(e); +      } else { +        specUI.finish(); +      }        specDone();      }    ); @@ -54,11 +75,14 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {  /**   * Adds a new future action.   * + * Note: Do not pass line manually. It happens automatically. + *   * @param {String} Name of the future   * @param {Function} Behavior of the future + * @param {Function} fn() that returns file/line number   */ -angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior) { -  var future = new angular.scenario.Future(name, angular.bind(this, behavior)); +angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line) { +  var future = new angular.scenario.Future(name, angular.bind(this, behavior), line);    this.futures.push(future);    return future;  }; @@ -66,17 +90,20 @@ angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior) {  /**   * Adds a new future action to be executed on the application window.   * + * Note: Do not pass line manually. It happens automatically. + *   * @param {String} Name of the future   * @param {Function} Behavior of the future + * @param {Function} fn() that returns file/line number    */ -angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior) { +angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior, line) { +  var self = this;    return this.addFuture(name, function(done) { -    this.application.executeAction(angular.bind(this, function($window, $document) { - -      $document.elements = angular.bind(this, function(selector) { +    this.application.executeAction(function($window, $document) { +      $document.elements = function(selector) {          var args = Array.prototype.slice.call(arguments, 1); -        if (this.selector) { -          selector = this.selector + ' ' + (selector || ''); +        if (self.selector) { +          selector = self.selector + ' ' + (selector || '');          }          angular.foreach(args, function(value, index) {            selector = selector.replace('$' + (index + 1), value); @@ -90,10 +117,10 @@ angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior)          }          return result; -      }); +      };        try { -        behavior.call(this, $window, $document, done); +        behavior.call(self, $window, $document, done);        } catch(e) {          if (e.type && e.type === 'selector') {            done(e.message); @@ -101,6 +128,6 @@ angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior)            throw e;          }        } -    })); -  }); +    }); +  }, line);  }; diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js index 69af39db..f4484df8 100644 --- a/src/scenario/dsl.js +++ b/src/scenario/dsl.js @@ -1,6 +1,18 @@  /**   * Shared DSL statements that are useful to all scenarios.   */ +  + /** + * Usage: + *    wait() waits until you call resume() in the console + */ + angular.scenario.dsl('wait', function() { +  return function() { +    return this.addFuture('waiting for you to call resume() in the console', function(done) { +      this.$window.resume = function() { done(); }; +    }); +  }; + });  /**  * Usage: @@ -41,23 +53,22 @@ angular.scenario.dsl('expect', function() {   *    of a  URL to navigate to   */  angular.scenario.dsl('navigateTo', function() { -  return function(url) { +  return function(url, delegate) {      var application = this.application; -    var name = url; -    if (url.name) { -      name = ' value of ' + url.name; -    } -    return this.addFuture('navigate to ' + name, function(done) { -      application.navigateTo(url.value || url, function() { +    return this.addFuture('navigate to ' + url, function(done) { +      if (delegate) { +        url = delegate.call(this, url); +      } +      application.navigateTo(url, function() {          application.executeAction(function($window) {            if ($window.angular) {              var $browser = $window.angular.service.$browser();              $browser.poll();              $browser.notifyWhenNoOutstandingRequests(function() { -              done(null, url.value || url); +              done(null, url);              });            } else { -            done(null, url.value || url); +            done(null, url);            }          });        }); @@ -142,9 +153,9 @@ angular.scenario.dsl('input', function() {  /**   * Usage: - *    repeater('#products table').count() // number of rows - *    repeater('#products table').row(1) // all bindings in row as an array - *    repeater('#products table').column('product.name') // all values across all rows in an array + *    repeater('#products table').count() number of rows + *    repeater('#products table').row(1) all bindings in row as an array + *    repeater('#products table').column('product.name') all values across all rows in an array   */  angular.scenario.dsl('repeater', function() {    var chain = {}; @@ -194,8 +205,8 @@ angular.scenario.dsl('repeater', function() {  /**   * Usage: - *    select(selector).option('value') // select one option - *    select(selector).options('value1', 'value2', ...) // select options from a multi select + *    select(selector).option('value') select one option + *    select(selector).options('value1', 'value2', ...) select options from a multi select   */  angular.scenario.dsl('select', function() {    var chain = {}; @@ -227,11 +238,12 @@ angular.scenario.dsl('select', function() {  /**   * Usage: - *    element(selector).click() // clicks an element - *    element(selector).attr(name) // gets the value of an attribute - *    element(selector).attr(name, value) // sets the value of an attribute - *    element(selector).val() // gets the value (as defined by jQuery) - *    element(selector).val(value) // sets the value (as defined by jQuery) + *    element(selector).click() clicks an element + *    element(selector).attr(name) gets the value of an attribute + *    element(selector).attr(name, value) sets the value of an attribute + *    element(selector).val() gets the value (as defined by jQuery) + *    element(selector).val(value) sets the value (as defined by jQuery) + *    element(selector).query(fn) executes fn(selectedElements, done)   */  angular.scenario.dsl('element', function() {    var chain = {}; @@ -263,6 +275,12 @@ angular.scenario.dsl('element', function() {      });    }; +  chain.query = function(fn) { +    return this.addFutureAction('element ' + this.selector + ' custom query', function($window, $document, done) { +      fn.call(this, $document.elements(), done); +    }); +  }; +    return function(selector) {      this.dsl.using(selector);      return chain;  | 
