diff options
| -rw-r--r-- | css/angular-scenario.css | 35 | ||||
| -rw-r--r-- | scenario/widgets-scenario.js | 1 | ||||
| -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 | ||||
| -rw-r--r-- | test/scenario/DescribeSpec.js | 8 | ||||
| -rw-r--r-- | test/scenario/FutureSpec.js | 3 | ||||
| -rw-r--r-- | test/scenario/HtmlUISpec.js | 41 | ||||
| -rw-r--r-- | test/scenario/RunnerSpec.js | 22 | ||||
| -rw-r--r-- | test/scenario/SpecRunnerSpec.js | 73 | ||||
| -rw-r--r-- | test/scenario/dslSpec.js | 33 | 
15 files changed, 429 insertions, 138 deletions
diff --git a/css/angular-scenario.css b/css/angular-scenario.css index 3462ecef..adadebb0 100644 --- a/css/angular-scenario.css +++ b/css/angular-scenario.css @@ -89,6 +89,20 @@ body {    border-radius: 8px 0 0 8px;    -webkit-border-radius: 8px 0 0 8px;    -moz-border-radius: 8px 0 0 8px; +  cursor: pointer; +} + +.test-info:hover .test-name { +  text-decoration: underline; +} + +.test-info .closed:before { +  content: '\25b8\00A0'; +} + +.test-info .open:before { +  content: '\25be\00A0'; +  font-weight: bold;  }  .test-it ol { @@ -111,6 +125,21 @@ body {    padding: 4px;  } +.test-actions .test-title, +.test-actions .test-result { +  display: table-cell; +  padding-left: 0.5em; +  padding-right: 0.5em; +} + +.test-actions { +  display: table; +} + +.test-actions li { +  display: table-row; +} +  .timer-result {    width: 4em;    padding: 0 10px; @@ -121,6 +150,7 @@ body {  .test-it pre,  .test-actions pre {    clear: left; +  color: black;    margin-left: 6em;  } @@ -132,6 +162,11 @@ body {    content: '\00bb\00A0';  } +.scrollpane { +   max-height: 20em; +   overflow: auto; +} +  /** Colors */  #header { diff --git a/scenario/widgets-scenario.js b/scenario/widgets-scenario.js index 0cb189f7..0d604fc9 100644 --- a/scenario/widgets-scenario.js +++ b/scenario/widgets-scenario.js @@ -23,7 +23,6 @@ describe('widgets', function() {      select('select').option('B');      expect(binding('select')).toEqual('B'); -      select('multiselect').options('A', 'C');      expect(binding('multiselect').fromJson()).toEqual(['A', 'C']); 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; diff --git a/test/scenario/DescribeSpec.js b/test/scenario/DescribeSpec.js index 05129cfe..417a0d2e 100644 --- a/test/scenario/DescribeSpec.js +++ b/test/scenario/DescribeSpec.js @@ -38,12 +38,16 @@ describe('angular.scenario.Describe', function() {      expect(specs.length).toEqual(2);      expect(specs[0].name).toEqual('2'); -    specs[0].fn(); +    specs[0].before(); +    specs[0].body(); +    specs[0].after();      expect(log.text).toEqual('{(2)}');      log.reset();      expect(specs[1].name).toEqual('1'); -    specs[1].fn(); +    specs[1].before(); +    specs[1].body(); +    specs[1].after();      expect(log.text).toEqual('{1}');    }); diff --git a/test/scenario/FutureSpec.js b/test/scenario/FutureSpec.js index 1e6af7a1..52bd9c66 100644 --- a/test/scenario/FutureSpec.js +++ b/test/scenario/FutureSpec.js @@ -3,9 +3,10 @@ describe('angular.scenario.Future', function() {    it('should set the sane defaults', function() {      var behavior = function() {}; -    var future = new angular.scenario.Future('test name', behavior); +    var future = new angular.scenario.Future('test name', behavior, 'foo');      expect(future.name).toEqual('test name');      expect(future.behavior).toEqual(behavior); +    expect(future.line).toEqual('foo');      expect(future.value).toBeUndefined();      expect(future.fulfilled).toBeFalsy();      expect(future.parser).toEqual(angular.identity); diff --git a/test/scenario/HtmlUISpec.js b/test/scenario/HtmlUISpec.js index 5b800fca..9357e00b 100644 --- a/test/scenario/HtmlUISpec.js +++ b/test/scenario/HtmlUISpec.js @@ -2,6 +2,8 @@ describe('angular.scenario.HtmlUI', function() {    var ui;    var context;    var spec; +   +  function line() { return 'unknown:-1'; }    beforeEach(function() {      spec = { @@ -35,44 +37,44 @@ describe('angular.scenario.HtmlUI', function() {    it('should update totals when steps complete', function() {      // Error      ui.addSpec(spec).error('error'); -    // Error -    specUI = ui.addSpec(spec); -    specUI.addStep('some step').finish(); -    specUI.finish('error');      // Failure      specUI = ui.addSpec(spec); -    specUI.addStep('some step').finish('failure'); -    specUI.finish('failure'); +    specUI.addStep('some step', line).finish('failure'); +    specUI.finish();      // Failure      specUI = ui.addSpec(spec); -    specUI.addStep('some step').finish('failure'); -    specUI.finish('failure'); +    specUI.addStep('some step', line).finish('failure'); +    specUI.finish();      // Failure      specUI = ui.addSpec(spec); -    specUI.addStep('some step').finish('failure'); -    specUI.finish('failure'); +    specUI.addStep('some step', line).finish('failure'); +    specUI.finish();      // Success      specUI = ui.addSpec(spec); -    specUI.addStep('some step').finish(); +    specUI.addStep('some step', line).finish(); +    specUI.finish(); +    // Success +    specUI = ui.addSpec(spec); +    specUI.addStep('another step', line).finish();      specUI.finish();      expect(parseInt(context.find('#status-legend .status-failure').text(), 10)).        toEqual(3); -    expect(parseInt(context.find('#status-legend .status-error').text(), 10)). -      toEqual(2);      expect(parseInt(context.find('#status-legend .status-success').text(), 10)). +      toEqual(2); +    expect(parseInt(context.find('#status-legend .status-error').text(), 10)).        toEqual(1);    });    it('should update timer when test completes', function() {      // Success      specUI = ui.addSpec(spec); -    specUI.addStep('some step').finish(); +    specUI.addStep('some step', line).finish();      specUI.finish();      // Failure      specUI = ui.addSpec(spec); -    specUI.addStep('some step').finish('failure'); +    specUI.addStep('some step', line).finish('failure');      specUI.finish('failure');      // Error @@ -83,5 +85,14 @@ describe('angular.scenario.HtmlUI', function() {          expect(timer.innerHTML).toMatch(/ms$/);      });    }); +   +  it('should include line if provided', function() { +    specUI = ui.addSpec(spec); +    specUI.addStep('some step', line).finish('error!'); +    specUI.finish(); + +    var errorHtml = context.find('#describe-10 .tests li pre').html(); +    expect(errorHtml.indexOf('unknown:-1')).toEqual(0); +  });  }); diff --git a/test/scenario/RunnerSpec.js b/test/scenario/RunnerSpec.js index 43d97257..d34a228c 100644 --- a/test/scenario/RunnerSpec.js +++ b/test/scenario/RunnerSpec.js @@ -3,16 +3,28 @@   */  function MockSpecRunner() {}  MockSpecRunner.prototype.run = function(ui, spec, specDone) { -  spec.fn.call(this); +  spec.before.call(this); +  spec.body.call(this); +  spec.after.call(this);    specDone();  }; +MockSpecRunner.prototype.addFuture = function(name, fn, line) { +  return {name: name, fn: fn, line: line}; +}; +  describe('angular.scenario.Runner', function() {    var $window;    var runner;    beforeEach(function() {      // Trick to get the scope out of a DSL statement +    angular.scenario.dsl('dslAddFuture', function() { +      return function() { +        return this.addFuture('future name', angular.noop); +      }; +    }); +    // Trick to get the scope out of a DSL statement      angular.scenario.dsl('dslScope', function() {        var scope = this;        return function() { return scope; }; @@ -25,7 +37,9 @@ describe('angular.scenario.Runner', function() {          return this;        };      }); -    $window = {}; +    $window = { +      location: {} +    };      runner = new angular.scenario.Runner($window);    }); @@ -63,7 +77,9 @@ describe('angular.scenario.Runner', function() {        });      });      var specs = runner.rootDescribe.getSpecs(); -    specs[0].fn(); +    specs[0].before(); +    specs[0].body(); +    specs[0].after();      expect(before).toEqual(['A', 'B', 'C']);      expect(after).toEqual(['C', 'B', 'A']);      expect(specs[2].definition.parent).toEqual(runner.rootDescribe); diff --git a/test/scenario/SpecRunnerSpec.js b/test/scenario/SpecRunnerSpec.js index e62bb392..dd7a1b72 100644 --- a/test/scenario/SpecRunnerSpec.js +++ b/test/scenario/SpecRunnerSpec.js @@ -49,11 +49,24 @@ ApplicationMock.prototype = {  describe('angular.scenario.SpecRunner', function() {    var $window;    var runner; +   +  function createSpec(name, body) { +    return { +      name: name, +      before: angular.noop, +      body: body || angular.noop, +      after: angular.noop +    }; +  }    beforeEach(function() {      $window = {}; +    $window.setTimeout = function(fn, timeout) { +      fn(); +    };      runner = angular.scope();      runner.application = new ApplicationMock($window); +    runner.$window = $window;      runner.$become(angular.scenario.SpecRunner);    }); @@ -78,11 +91,11 @@ describe('angular.scenario.SpecRunner', function() {    });    it('should execute spec function and notify UI', function() { -    var finished = false; +    var finished;      var ui = new UIMock(); -    var spec = {name: 'test spec', fn: function() { -      this.test = 'some value'; -    }}; +    var spec = createSpec('test spec', function() {  +      this.test = 'some value';  +    });      runner.addFuture('test future', function(done) {        done();      }); @@ -100,11 +113,11 @@ describe('angular.scenario.SpecRunner', function() {    });    it('should execute notify UI on spec setup error', function() { -    var finished = false; +    var finished;      var ui = new UIMock(); -    var spec = {name: 'test spec', fn: function() { +    var spec = createSpec('test spec', function() {         throw 'message'; -    }}; +    });      runner.run(ui, spec, function() {        finished = true;      }); @@ -116,9 +129,9 @@ describe('angular.scenario.SpecRunner', function() {    });    it('should execute notify UI on step failure', function() { -    var finished = false; +    var finished;      var ui = new UIMock(); -    var spec = {name: 'test spec', fn: angular.noop}; +    var spec = createSpec('test spec');      runner.addFuture('test future', function(done) {        done('failure message');      }); @@ -130,16 +143,17 @@ describe('angular.scenario.SpecRunner', function() {        'addSpec:test spec',        'addStep:test future',        'step finish:failure message', -      'spec finish:failure message' +      'spec finish:'      ]);    });    it('should execute notify UI on step error', function() { -    var finished = false; +    var finished;      var ui = new UIMock(); -    var spec = {name: 'test spec', fn: angular.noop}; -    runner.addFuture('test future', function(done) { -      throw 'error message'; +    var spec = createSpec('test spec', function() { +      this.addFuture('test future', function(done) { +        throw 'error message'; +      });      });      runner.run(ui, spec, function() {        finished = true; @@ -149,7 +163,36 @@ describe('angular.scenario.SpecRunner', function() {        'addSpec:test spec',        'addStep:test future',        'step error:error message', -      'spec finish:error message' +      'spec finish:' +    ]); +  }); +   +  it('should run after handlers even if error in body of spec', function() { +    var finished, after; +    var ui = new UIMock(); +    var spec = createSpec('test spec', function() { +      this.addFuture('body', function(done) { +        throw 'error message'; +      }); +    }); +    spec.after = function() { +      this.addFuture('after', function(done) { +        after = true; +        done(); +      }); +    }; +    runner.run(ui, spec, function() { +      finished = true; +    }); +    expect(finished).toBeTruthy(); +    expect(after).toBeTruthy(); +    expect(ui.log).toEqual([ +      'addSpec:test spec', +      'addStep:body', +      'step error:error message', +      'addStep:after', +      'step finish:', +      'spec finish:'      ]);    }); diff --git a/test/scenario/dslSpec.js b/test/scenario/dslSpec.js index dd489d86..0d523ad3 100644 --- a/test/scenario/dslSpec.js +++ b/test/scenario/dslSpec.js @@ -35,13 +35,16 @@ describe("angular.scenario.dsl", function() {        document: _jQuery("<div></div>"),        angular: new AngularMock()      }; -    $root = angular.scope({}, angular.service); +    $root = angular.scope();      $root.futures = []; +    $root.futureLog = []; +    $root.$window = $window;      $root.addFuture = function(name, fn) {        this.futures.push(name);        fn.call(this, function(error, result) {          $root.futureError = error;          $root.futureResult = result; +        $root.futureLog.push(name);        });      };      $root.dsl = {}; @@ -63,6 +66,18 @@ describe("angular.scenario.dsl", function() {        SpecRunner.prototype.addFutureAction;    }); +  describe('Wait', function() { +    it('should wait until resume to complete', function() { +      expect($window.resume).toBeUndefined(); +      $root.dsl.wait(); +      expect(angular.isFunction($window.resume)).toBeTruthy(); +      expect($root.futureLog).toEqual([]); +      $window.resume(); +      expect($root.futureLog). +        toEqual(['waiting for you to call resume() in the console']); +    }); +  }); +    describe('Pause', function() {      beforeEach(function() {        $root.setTimeout = function(fn, value) { @@ -99,10 +114,11 @@ describe("angular.scenario.dsl", function() {      });      it('should allow a future url', function() { -      var future = {name: 'future name', value: 'http://myurl'}; -      $root.dsl.navigateTo(future); -      expect($window.location).toEqual('http://myurl'); -      expect($root.futureResult).toEqual('http://myurl'); +      $root.dsl.navigateTo('http://myurl', function() { +        return 'http://futureUrl/'; +      }); +      expect($window.location).toEqual('http://futureUrl/'); +      expect($root.futureResult).toEqual('http://futureUrl/');      });      it('should complete if angular is missing from app frame', function() { @@ -205,6 +221,13 @@ describe("angular.scenario.dsl", function() {          expect(doc.find('input').val()).toEqual('baz');        }); +      it('should execute custom query', function() { +        doc.append('<a id="test" href="myUrl"></a>'); +        $root.dsl.element('#test').query(function(elements, done) { +          done(null, elements.attr('href')); +        }); +        expect($root.futureResult).toEqual('myUrl'); +      });      });      describe('Repeater', function() {  | 
