diff options
Diffstat (limited to 'src/scenario')
| -rw-r--r-- | src/scenario/Application.js | 61 | ||||
| -rw-r--r-- | src/scenario/Describe.js | 65 | ||||
| -rw-r--r-- | src/scenario/Future.js | 8 | ||||
| -rw-r--r-- | src/scenario/HtmlUI.js | 244 | ||||
| -rw-r--r-- | src/scenario/ObjectModel.js | 153 | ||||
| -rw-r--r-- | src/scenario/Runner.js | 128 | ||||
| -rw-r--r-- | src/scenario/Scenario.js | 77 | ||||
| -rw-r--r-- | src/scenario/SpecRunner.js | 57 | ||||
| -rw-r--r-- | src/scenario/angular-bootstrap.js (renamed from src/scenario/bootstrap.js) | 38 | ||||
| -rw-r--r-- | src/scenario/angular.prefix | 2 | ||||
| -rw-r--r-- | src/scenario/angular.suffix | 20 | ||||
| -rw-r--r-- | src/scenario/dsl.js | 56 | ||||
| -rw-r--r-- | src/scenario/matchers.js | 4 | ||||
| -rw-r--r-- | src/scenario/output/Html.js | 165 | ||||
| -rw-r--r-- | src/scenario/output/Json.js | 10 | ||||
| -rw-r--r-- | src/scenario/output/Object.js | 6 | ||||
| -rw-r--r-- | src/scenario/output/Xml.js | 48 | 
17 files changed, 747 insertions, 395 deletions
| diff --git a/src/scenario/Application.js b/src/scenario/Application.js index 4ee0dd03..e2d34551 100644 --- a/src/scenario/Application.js +++ b/src/scenario/Application.js @@ -1,51 +1,84 @@  /**   * Represents the application currently being tested and abstracts usage   * of iframes or separate windows. + * + * @param {Object} context jQuery wrapper around HTML context.   */  angular.scenario.Application = function(context) {    this.context = context; -  context.append('<h2>Current URL: <a href="about:blank">None</a></h2>'); +  context.append( +    '<h2>Current URL: <a href="about:blank">None</a></h2>' + +    '<div id="test-frames"></div>' +  );  };  /**   * Gets the jQuery collection of frames. Don't use this directly because   * frames may go stale.   * + * @private   * @return {Object} jQuery collection   */ -angular.scenario.Application.prototype.getFrame = function() { -  return this.context.find('> iframe'); +angular.scenario.Application.prototype.getFrame_ = function() { +  return this.context.find('#test-frames iframe:last');  };  /** - * Gets the window of the test runner frame. Always favor executeAction()  + * Gets the window of the test runner frame. Always favor executeAction()   * instead of this method since it prevents you from getting a stale window.   * + * @private   * @return {Object} the window of the frame   */ -angular.scenario.Application.prototype.getWindow = function() { -  var contentWindow = this.getFrame().attr('contentWindow'); +angular.scenario.Application.prototype.getWindow_ = function() { +  var contentWindow = this.getFrame_().attr('contentWindow');    if (!contentWindow) -    throw 'No window available because frame not loaded.'; +    throw 'Frame window is not accessible.';    return contentWindow;  };  /**   * Changes the location of the frame. + * + * @param {string} url The URL. If it begins with a # then only the  + *   hash of the page is changed. + * @param {Function} onloadFn function($window, $document)   */  angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) { -  this.getFrame().remove(); -  this.context.append('<iframe src=""></iframe>'); +  var self = this; +  var frame = this.getFrame_(); +  if (url.charAt(0) === '#') { +    url = frame.attr('src').split('#')[0] + url; +    frame.attr('src', url); +    this.executeAction(onloadFn); +  } else { +    frame.css('display', 'none').attr('src', 'about:blank'); +    this.context.find('#test-frames').append('<iframe>'); +    frame = this.getFrame_(); +    frame.load(function() { +      self.executeAction(onloadFn); +      frame.unbind(); +    }).attr('src', url); +  }    this.context.find('> h2 a').attr('href', url).text(url); -  this.getFrame().attr('src', url).load(onloadFn);  };  /** - * Executes a function in the context of the tested application. + * Executes a function in the context of the tested application. Will wait + * for all pending angular xhr requests before executing.   * - * @param {Function} The callback to execute. function($window, $document) + * @param {Function} action The callback to execute. function($window, $document) + *  $document is a jQuery wrapped document.   */  angular.scenario.Application.prototype.executeAction = function(action) { -  var $window = this.getWindow(); -  return action.call(this, $window, _jQuery($window.document)); +  var self = this; +  var $window = this.getWindow_(); +  if (!$window.angular) { +    return action.call(this, $window, _jQuery($window.document)); +  } +  var $browser = $window.angular.service.$browser(); +  $browser.poll(); +  $browser.notifyWhenNoOutstandingRequests(function() { +    action.call(self, $window, _jQuery($window.document)); +  });  }; diff --git a/src/scenario/Describe.js b/src/scenario/Describe.js index f6a52f1e..69ed8238 100644 --- a/src/scenario/Describe.js +++ b/src/scenario/Describe.js @@ -1,8 +1,12 @@  /**   * The representation of define blocks. Don't used directly, instead use   * define() in your tests. + * + * @param {string} descName Name of the block + * @param {Object} parent describe or undefined if the root.   */  angular.scenario.Describe = function(descName, parent) { +  this.only = parent && parent.only;    this.beforeEachFns = [];    this.afterEachFns = [];    this.its = []; @@ -10,7 +14,7 @@ angular.scenario.Describe = function(descName, parent) {    this.name = descName;    this.parent = parent;    this.id = angular.scenario.Describe.id++; -   +    /**     * Calls all before functions.     */ @@ -36,7 +40,7 @@ angular.scenario.Describe.id = 0;  /**   * Defines a block to execute before each it or nested describe.   * - * @param {Function} Body of the block. + * @param {Function} body Body of the block.   */  angular.scenario.Describe.prototype.beforeEach = function(body) {    this.beforeEachFns.push(body); @@ -45,7 +49,7 @@ angular.scenario.Describe.prototype.beforeEach = function(body) {  /**   * Defines a block to execute after each it or nested describe.   * - * @param {Function} Body of the block. + * @param {Function} body Body of the block.   */  angular.scenario.Describe.prototype.afterEach = function(body) {    this.afterEachFns.push(body); @@ -54,8 +58,8 @@ angular.scenario.Describe.prototype.afterEach = function(body) {  /**   * Creates a new describe block that's a child of this one.   * - * @param {String} Name of the block. Appended to the parent block's name. - * @param {Function} Body of the block. + * @param {string} name Name of the block. Appended to the parent block's name. + * @param {Function} body Body of the block.   */  angular.scenario.Describe.prototype.describe = function(name, body) {    var child = new angular.scenario.Describe(name, this); @@ -64,6 +68,19 @@ angular.scenario.Describe.prototype.describe = function(name, body) {  };  /** + * Same as describe() but makes ddescribe blocks the only to run. + * + * @param {string} name Name of the test. + * @param {Function} body Body of the block. + */ +angular.scenario.Describe.prototype.ddescribe = function(name, body) { +  var child = new angular.scenario.Describe(name, this); +  child.only = true; +  this.children.push(child); +  body.call(child); +}; + +/**   * Use to disable a describe block.   */  angular.scenario.Describe.prototype.xdescribe = angular.noop; @@ -71,21 +88,32 @@ angular.scenario.Describe.prototype.xdescribe = angular.noop;  /**   * Defines a test.   * - * @param {String} Name of the test. - * @param {Function} Body of the block. + * @param {string} name Name of the test. + * @param {Function} vody Body of the block.   */  angular.scenario.Describe.prototype.it = function(name, body) { -  var self = this;    this.its.push({      definition: this, +    only: this.only,      name: name, -    before: self.setupBefore, +    before: this.setupBefore,      body: body, -    after: self.setupAfter +    after: this.setupAfter    });  };  /** + * Same as it() but makes iit tests the only test to run. + * + * @param {string} name Name of the test. + * @param {Function} body Body of the block. + */ +angular.scenario.Describe.prototype.iit = function(name, body) { +  this.it.apply(this, arguments); +  this.its[this.its.length-1].only = true; +}; + +/**   * Use to disable a test block.   */  angular.scenario.Describe.prototype.xit = angular.noop; @@ -93,6 +121,15 @@ angular.scenario.Describe.prototype.xit = angular.noop;  /**   * Gets an array of functions representing all the tests (recursively).   * that can be executed with SpecRunner's. + * + * @return {Array<Object>} Array of it blocks { + *   definition : Object // parent Describe + *   only: boolean + *   name: string + *   before: Function + *   body: Function + *   after: Function + *  }   */  angular.scenario.Describe.prototype.getSpecs = function() {    var specs = arguments[0] || []; @@ -102,5 +139,11 @@ angular.scenario.Describe.prototype.getSpecs = function() {    angular.foreach(this.its, function(it) {      specs.push(it);    }); -  return specs; +  var only = []; +  angular.foreach(specs, function(it) { +    if (it.only) { +      only.push(it); +    } +  }); +  return (only.length && only) || specs;  }; diff --git a/src/scenario/Future.js b/src/scenario/Future.js index 8853aa3f..f545c721 100644 --- a/src/scenario/Future.js +++ b/src/scenario/Future.js @@ -1,9 +1,9 @@  /**   * A future action in a spec.   * - * @param {String} name of the future action + * @param {string} name of the future action   * @param {Function} future callback(error, result) - * @param {String} Optional. function that returns the file/line number. + * @param {Function} Optional. function that returns the file/line number.   */  angular.scenario.Future = function(name, behavior, line) {    this.name = name; @@ -17,7 +17,7 @@ angular.scenario.Future = function(name, behavior, line) {  /**   * Executes the behavior of the closure.   * - * @param {Function} Callback function(error, result) + * @param {Function} doneFn Callback function(error, result)   */  angular.scenario.Future.prototype.execute = function(doneFn) {    var self = this; @@ -37,6 +37,8 @@ angular.scenario.Future.prototype.execute = function(doneFn) {  /**   * Configures the future to convert it's final with a function fn(value) + * + * @param {Function} fn function(value) that returns the parsed value   */  angular.scenario.Future.prototype.parsedWith = function(fn) {    this.parser = fn; diff --git a/src/scenario/HtmlUI.js b/src/scenario/HtmlUI.js deleted file mode 100644 index 78fe8c33..00000000 --- a/src/scenario/HtmlUI.js +++ /dev/null @@ -1,244 +0,0 @@ -/** - * User Interface for the Scenario Runner. - * - * @param {Object} The jQuery UI object for the UI. - */ -angular.scenario.ui.Html = function(context) { -  this.context = context; -  context.append( -    '<div id="header">' + -    '  <h1><span class="angular"><angular/></span>: Scenario Test Runner</h1>' + -    '  <ul id="status-legend" class="status-display">' + -    '    <li class="status-error">0 Errors</li>' + -    '    <li class="status-failure">0 Failures</li>' + -    '    <li class="status-success">0 Passed</li>' + -    '  </ul>' + -    '</div>' + -    '<div id="specs">' + -    '  <div class="test-children"></div>' + -    '</div>' -  ); -}; - -/** - * 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,  -    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]); -    } -  ); -}; - -/** - * Finds the context of a spec block defined by the passed definition. - * - * @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; -  while (currentDefinition && currentDefinition.name) { -    path.unshift(currentDefinition); -    currentDefinition = currentDefinition.parent; -  } -  angular.foreach(path, function(defn) { -    var id = 'describe-' + defn.id; -    if (!self.context.find('#' + id).length) { -      currentContext.find('> .test-children').append( -        '<div class="test-describe" id="' + id + '">' + -        '  <h2></h2>' + -        '  <div class="test-children"></div>' + -        '  <ul class="tests"></ul>' + -        '</div>' -      ); -      self.context.find('#' + id).find('> h2').text('describe: ' + defn.name); -    } -    currentContext = self.context.find('#' + id); -  }); -  return this.context.find('#describe-' + definition.id); -}; - -/** - * A spec block in the UI. - * - * @param {Object} The jQuery object for the context of the spec. - * @param {String} The name of the spec. - * @param {Function} Callback function(status) to call when complete. - */ -angular.scenario.ui.Html.Spec = function(context, name, doneFn) { -  this.status = 'pending'; -  this.context = context; -  this.startTime = new Date().getTime(); -  this.doneFn = doneFn; -  context.append( -    '<div class="test-info">' + -    '  <p class="test-title">' + -    '    <span class="timer-result"></span>' + -    '    <span class="test-name"></span>' + -    '  </p>' + -    '</div>' + -    '<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); -}; - -/** - * 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, 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, 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')); -  }); -}; - -/** - * Completes the spec and sets the timer value. - */ -angular.scenario.ui.Html.Spec.prototype.complete = function() { -  this.context.removeClass('status-pending'); -  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(); -  } -}; - -/** - * Finishes the spec, possibly with an error. - * - * @param {Object} An optional error - */ -angular.scenario.ui.Html.Spec.prototype.finish = function() { -  this.complete(); -  this.context.addClass('status-' + this.status); -  this.doneFn(this.status); -}; - -/** - * Finishes the spec, but with a Fatal Error. - * - * @param {Object} Required error - */ -angular.scenario.ui.Html.Spec.prototype.error = function(error) { -  this.status = 'error'; -  this.context.append('<pre></pre>'); -  this.context.find('> pre').text(formatException(error)); -  this.finish(); -}; - -/** - * A single step inside an it block (or a before/after function). - * - * @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, location, doneFn) { -  this.context = context; -  this.name = name; -  this.location = location; -  this.startTime = new Date().getTime(); -  this.doneFn = doneFn; -  context.append( -    '<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(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); -  } -}; - -/** - * Finishes the step, possibly with an error. - * - * @param {Object} An optional error - */ -angular.scenario.ui.Html.Step.prototype.finish = function(error) { -  this.complete(error); -  if (error) { -    this.context.addClass('status-failure'); -    this.doneFn('failure'); -  } else { -    this.context.addClass('status-success'); -    this.doneFn('success'); -  } -}; - -/** - * Finishes the step, but with a Fatal Error. - * - * @param {Object} Required error - */ -angular.scenario.ui.Html.Step.prototype.error = function(error) { -  this.complete(error); -  this.context.addClass('status-error'); -  this.doneFn('error'); -}; diff --git a/src/scenario/ObjectModel.js b/src/scenario/ObjectModel.js new file mode 100644 index 00000000..e9125e03 --- /dev/null +++ b/src/scenario/ObjectModel.js @@ -0,0 +1,153 @@ +/** + * Maintains an object tree from the runner events. + * + * @param {Object} runner The scenario Runner instance to connect to. + * + * TODO(esprehn): Every output type creates one of these, but we probably + *  want one glonal shared instance. Need to handle events better too + *  so the HTML output doesn't need to do spec model.getSpec(spec.id) + *  silliness. + */ +angular.scenario.ObjectModel = function(runner) { +  var self = this; + +  this.specMap = {}; +  this.value = { +    name: '', +    children: {} +  }; + +  runner.on('SpecBegin', function(spec) { +    var block = self.value; +    angular.foreach(self.getDefinitionPath(spec), function(def) { +      if (!block.children[def.name]) { +        block.children[def.name] = { +          id: def.id, +          name: def.name, +          children: {}, +          specs: {} +        }; +      } +      block = block.children[def.name]; +    }); +    self.specMap[spec.id] = block.specs[spec.name] = +      new angular.scenario.ObjectModel.Spec(spec.id, spec.name); +  }); + +  runner.on('SpecError', function(spec, error) { +    var it = self.getSpec(spec.id); +    it.status = 'error'; +    it.error = error; +  }); + +  runner.on('SpecEnd', function(spec) { +    var it = self.getSpec(spec.id); +    complete(it); +  }); + +  runner.on('StepBegin', function(spec, step) { +    var it = self.getSpec(spec.id); +    it.steps.push(new angular.scenario.ObjectModel.Step(step.name)); +  }); + +  runner.on('StepEnd', function(spec, step) { +    var it = self.getSpec(spec.id); +    if (it.getLastStep().name !== step.name) +      throw 'Events fired in the wrong order. Step names don\' match.'; +    complete(it.getLastStep()); +  }); + +  runner.on('StepFailure', function(spec, step, error) { +    var it = self.getSpec(spec.id); +    var item = it.getLastStep(); +    item.error = error; +    if (!it.status) { +      it.status = item.status = 'failure'; +    } +  }); + +  runner.on('StepError', function(spec, step, error) { +    var it = self.getSpec(spec.id); +    var item = it.getLastStep(); +    it.status = 'error'; +    item.status = 'error'; +    item.error = error; +  }); + +  function complete(item) { +    item.endTime = new Date().getTime(); +    item.duration = item.endTime - item.startTime; +    item.status = item.status || 'success'; +  } +}; + +/** + * Computes the path of definition describe blocks that wrap around + * this spec. + * + * @param spec Spec to compute the path for. + * @return {Array<Describe>} The describe block path + */ +angular.scenario.ObjectModel.prototype.getDefinitionPath = function(spec) { +  var path = []; +  var currentDefinition = spec.definition; +  while (currentDefinition && currentDefinition.name) { +    path.unshift(currentDefinition); +    currentDefinition = currentDefinition.parent; +  } +  return path; +}; + +/** + * Gets a spec by id. + * + * @param {string} The id of the spec to get the object for. + * @return {Object} the Spec instance + */ +angular.scenario.ObjectModel.prototype.getSpec = function(id) { +  return this.specMap[id]; +}; + +/** + * A single it block. + * + * @param {string} id Id of the spec + * @param {string} name Name of the spec + */ +angular.scenario.ObjectModel.Spec = function(id, name) { +  this.id = id; +  this.name = name; +  this.startTime = new Date().getTime(); +  this.steps = []; +}; + +/** + * Adds a new step to the Spec. + * + * @param {string} step Name of the step (really name of the future) + * @return {Object} the added step + */ +angular.scenario.ObjectModel.Spec.prototype.addStep = function(name) { +  var step = new angular.scenario.ObjectModel.Step(name); +  this.steps.push(step); +  return step; +}; + +/** + * Gets the most recent step. + * + * @return {Object} the step + */ +angular.scenario.ObjectModel.Spec.prototype.getLastStep = function() { +  return this.steps[this.steps.length-1]; +}; + +/** + * A single step inside a Spec. + * + * @param {string} step Name of the step + */ +angular.scenario.ObjectModel.Step = function(name) { +  this.name = name; +  this.startTime = new Date().getTime(); +}; diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js index a8b23f83..f628eb04 100644 --- a/src/scenario/Runner.js +++ b/src/scenario/Runner.js @@ -2,13 +2,16 @@   * Runner for scenarios.   */  angular.scenario.Runner = function($window) { +  this.listeners = [];    this.$window = $window;    this.rootDescribe = new angular.scenario.Describe();    this.currentDescribe = this.rootDescribe;    this.api = {      it: this.it, +    iit: this.iit,      xit: angular.noop,      describe: this.describe, +    ddescribe: this.ddescribe,      xdescribe: angular.noop,      beforeEach: this.beforeEach,      afterEach: this.afterEach @@ -19,10 +22,41 @@ angular.scenario.Runner = function($window) {  };  /** + * Emits an event which notifies listeners and passes extra + * arguments. + * + * @param {string} eventName Name of the event to fire. + */ +angular.scenario.Runner.prototype.emit = function(eventName) { +  var self = this; +  var args = Array.prototype.slice.call(arguments, 1); +  eventName = eventName.toLowerCase(); +  if (!this.listeners[eventName]) +    return; +  angular.foreach(this.listeners[eventName], function(listener) { +    listener.apply(self, args); +  }); +}; + +/** + * Adds a listener for an event. + * + * @param {string} eventName The name of the event to add a handler for + * @param {string} listener The fn(...) that takes the extra arguments from emit() + */ +angular.scenario.Runner.prototype.on = function(eventName, listener) { +  eventName = eventName.toLowerCase(); +  this.listeners[eventName] = this.listeners[eventName] || []; +  this.listeners[eventName].push(listener); +}; + +/**   * Defines a describe block of a spec.   * - * @param {String} Name of the block - * @param {Function} Body of the block + * @see Describe.js + * + * @param {string} name Name of the block + * @param {Function} body Body of the block   */  angular.scenario.Runner.prototype.describe = function(name, body) {    var self = this; @@ -38,19 +72,56 @@ angular.scenario.Runner.prototype.describe = function(name, body) {  };  /** + * Same as describe, but makes ddescribe the only blocks to run. + * + * @see Describe.js + * + * @param {string} name Name of the block + * @param {Function} body Body of the block + */ +angular.scenario.Runner.prototype.ddescribe = function(name, body) { +  var self = this; +  this.currentDescribe.ddescribe(name, function() { +    var parentDescribe = self.currentDescribe; +    self.currentDescribe = this; +    try { +      body.call(this); +    } finally { +      self.currentDescribe = parentDescribe; +    } +  }); +}; + +/**   * Defines a test in a describe block of a spec.   * - * @param {String} Name of the block - * @param {Function} Body of the block + * @see Describe.js + * + * @param {string} name Name of the block + * @param {Function} body Body of the block   */  angular.scenario.Runner.prototype.it = function(name, body) {    this.currentDescribe.it(name, body);  };  /** + * Same as it, but makes iit tests the only tests to run. + * + * @see Describe.js + * + * @param {string} name Name of the block + * @param {Function} body Body of the block + */ +angular.scenario.Runner.prototype.iit = function(name, body) { +  this.currentDescribe.iit(name, body); +}; + +/**   * Defines a function to be called before each it block in the describe   * (and before all nested describes).   * + * @see Describe.js + *   * @param {Function} Callback to execute   */  angular.scenario.Runner.prototype.beforeEach = function(body) { @@ -61,6 +132,8 @@ angular.scenario.Runner.prototype.beforeEach = function(body) {   * Defines a function to be called after each it block in the describe   * (and before all nested describes).   * + * @see Describe.js + *   * @param {Function} Callback to execute   */  angular.scenario.Runner.prototype.afterEach = function(body) { @@ -68,24 +141,29 @@ angular.scenario.Runner.prototype.afterEach = function(body) {  };  /** - * Defines a function to be called before each it block in the describe - * (and before all nested describes). + * Creates a new spec runner.   * - * @param {Function} Callback to execute + * @private + * @param {Object} scope parent scope + */ +angular.scenario.Runner.prototype.createSpecRunner_ = function(scope) { +  return scope.$new(angular.scenario.SpecRunner); +}; + +/** + * Runs all the loaded tests with the specified runner class on the + * provided application. + * + * @param {angular.scenario.Application} application App to remote control.   */ -angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClass, specsDone) { -  var $root = angular.scope({}, angular.service); +angular.scenario.Runner.prototype.run = function(application) {    var self = this; -  var specs = this.rootDescribe.getSpecs(); +  var $root = angular.scope(this);    $root.application = application; -  $root.ui = ui; -  $root.setTimeout = function() { -    return self.$window.setTimeout.apply(self.$window, arguments); -  }; -  asyncForEach(specs, function(spec, specDone) { +  this.emit('RunnerBegin'); +  asyncForEach(this.rootDescribe.getSpecs(), function(spec, specDone) {      var dslCache = {}; -    var runner = angular.scope($root); -    runner.$become(specRunnerClass); +    var runner = self.createSpecRunner_($root);      angular.foreach(angular.scenario.dsl, function(fn, key) {        dslCache[key] = fn.call($root);      }); @@ -105,16 +183,24 @@ angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClas          // Make these methods work on the current chain          scope.addFuture = function() {            Array.prototype.push.call(arguments, line); -          return specRunnerClass.prototype.addFuture.apply(scope, arguments); +          return angular.scenario.SpecRunner. +            prototype.addFuture.apply(scope, arguments);          };          scope.addFutureAction = function() {            Array.prototype.push.call(arguments, line); -          return specRunnerClass.prototype.addFutureAction.apply(scope, arguments); +          return angular.scenario.SpecRunner. +            prototype.addFutureAction.apply(scope, arguments);          };          return scope.dsl[key].apply(scope, arguments);        };      }); -    runner.run(ui, spec, specDone); -  }, specsDone || angular.noop); +    runner.run(spec, specDone); +  }, +  function(error) { +    if (error) { +      self.emit('RunnerError', error); +    } +    self.emit('RunnerEnd'); +  });  }; diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js index c00ed3dd..f2ebc640 100644 --- a/src/scenario/Scenario.js +++ b/src/scenario/Scenario.js @@ -6,8 +6,15 @@  // Public namespace  angular.scenario = angular.scenario || {}; -// Namespace for the UI -angular.scenario.ui = angular.scenario.ui || {}; +/** + * Defines a new output format. + * + * @param {string} name the name of the new output format + * @param {Function} fn function(context, runner) that generates the output + */ +angular.scenario.output = angular.scenario.output || function(name, fn) { +  angular.scenario.output[name] = fn; +};  /**   * Defines a new DSL statement. If your factory function returns a Future @@ -18,8 +25,8 @@ angular.scenario.ui = angular.scenario.ui || {};   *   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 + * @param {string} name The name of the statement + * @param {Function} fn Factory function(), return a function for   *  the statement.   */  angular.scenario.dsl = angular.scenario.dsl || function(name, fn) { @@ -54,8 +61,8 @@ angular.scenario.dsl = angular.scenario.dsl || function(name, fn) {   * 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). + * @param {string} name The name of the matcher + * @param {Function} fn The matching function(expected).   */  angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {    angular.scenario.matcher[name] = function(expected) { @@ -79,13 +86,55 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {  };  /** + * Initialization function for the scenario runner. + * + * @param {angular.scenario.Runner} $scenario The runner to setup + * @param {Object} config Config options + */ +function angularScenarioInit($scenario, config) { +  var body = _jQuery(document.body); +  var output = []; + +  if (config.scenario_output) { +    output = config.scenario_output.split(','); +  } + +  angular.foreach(angular.scenario.output, function(fn, name) { +    if (!output.length || indexOf(output,name) != -1) { +      var context = body.append('<div></div>').find('div:last'); +      context.attr('id', name); +      fn.call({}, context, $scenario); +    } +  }); + +  var appFrame = body.append('<div id="application"></div>').find('#application'); +  var application = new angular.scenario.Application(appFrame); + +  $scenario.on('RunnerEnd', function() { +    appFrame.css('display', 'none'); +    appFrame.find('iframe').attr('src', 'about:blank'); +  }); + +  $scenario.on('RunnerError', function(error) { +    if (window.console) { +      console.log(formatException(error)); +    } else { +      // Do something for IE +      alert(error); +    } +  }); + +  $scenario.run(application); +} + +/**   * 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. + * @param {Array} list list to iterate over + * @param {Function} iterator Callback function(value, continueFunction) + * @param {Function} done Callback function(error, result) called when  + *   iteration finishes or an error occurs.   */  function asyncForEach(list, iterator, done) {    var i = 0; @@ -110,8 +159,8 @@ function asyncForEach(list, iterator, done) {   * 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 + * @param {Object} error The exception to format, can be anything throwable + * @param {Number} maxStackLines Optional. max lines of the stack trace to include   *  default is 5.   */  function formatException(error, maxStackLines) { @@ -134,6 +183,8 @@ function formatException(error, maxStackLines) {   *   * Note: this returns another function because accessing .stack is very   * expensive in Chrome. + * + * @param {Number} offset Number of stack lines to skip   */  function callerFile(offset) {    var error = new Error(); @@ -161,7 +212,7 @@ function callerFile(offset) {   * not specified.   *   * @param {Object} Either a wrapped jQuery/jqLite node or a DOMElement - * @param {String} Optional event type. + * @param {string} Optional event type.   */  function browserTrigger(element, type) {    if (element && !element.nodeName) element = element[0]; diff --git a/src/scenario/SpecRunner.js b/src/scenario/SpecRunner.js index 98ce4b53..fb7d5c02 100644 --- a/src/scenario/SpecRunner.js +++ b/src/scenario/SpecRunner.js @@ -15,14 +15,16 @@ angular.scenario.SpecRunner = function() {   * Executes a spec which is an it block with associated before/after functions   * based on the describe nesting.   * - * @param {Object} An angular.scenario.UI implementation - * @param {Object} A spec object - * @param {Object} An angular.scenario.Application instance + * @param {Object} spec A spec object + * @param {Object} specDone An angular.scenario.Application instance   * @param {Function} Callback function that is called when the  spec finshes.   */ -angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) { +angular.scenario.SpecRunner.prototype.run = function(spec, specDone) {    var self = this; -  var specUI = ui.addSpec(spec); +  var count = 0; +  this.spec = spec; + +  this.emit('SpecBegin', spec);    try {      spec.before.call(this); @@ -30,7 +32,8 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {      this.afterIndex = this.futures.length;      spec.after.call(this);    } catch (e) { -    specUI.error(e); +    this.emit('SpecError', spec, e); +    this.emit('SpecEnd', spec);      specDone();      return;    } @@ -42,32 +45,40 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {      self.error = true;      done(null, self.afterIndex);    }; -   -  var spec = this; +    asyncForEach(      this.futures,      function(future, futureDone) { -      var stepUI = specUI.addStep(future.name, future.line); +      self.step = future; +      self.emit('StepBegin', spec, future);        try {          future.execute(function(error) { -          stepUI.finish(error);            if (error) { +            self.emit('StepFailure', spec, future, error); +            self.emit('StepEnd', spec, future);              return handleError(error, futureDone);            } -          spec.$window.setTimeout( function() { futureDone(); }, 0); +          self.emit('StepEnd', spec, future); +          if ((count++) % 10 === 0) { +            self.$window.setTimeout(function() { futureDone(); }, 0); +          } else { +            futureDone(); +          }          });        } catch (e) { -        stepUI.error(e); +        self.emit('StepError', spec, future, e); +        self.emit('StepEnd', spec, future);          handleError(e, futureDone);        }      },      function(e) {        if (e) { -        specUI.error(e); -      } else { -        specUI.finish(); +        self.emit('SpecError', spec, e);        } -      specDone(); +      self.emit('SpecEnd', spec); +      // Call done in a timeout so exceptions don't recursively +      // call this function +      self.$window.setTimeout(function() { specDone(); }, 0);      }    );  }; @@ -77,9 +88,9 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {   *   * 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 + * @param {string} name Name of the future + * @param {Function} behavior Behavior of the future + * @param {Function} line fn() that returns file/line number   */  angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line) {    var future = new angular.scenario.Future(name, angular.bind(this, behavior), line); @@ -92,14 +103,16 @@ angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line)   *   * 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  + * @param {string} name Name of the future + * @param {Function} behavior Behavior of the future + * @param {Function} line fn() that returns file/line number   */  angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior, line) {    var self = this;    return this.addFuture(name, function(done) {      this.application.executeAction(function($window, $document) { + +      //TODO(esprehn): Refactor this so it doesn't need to be in here.        $document.elements = function(selector) {          var args = Array.prototype.slice.call(arguments, 1);          if (self.selector) { diff --git a/src/scenario/bootstrap.js b/src/scenario/angular-bootstrap.js index 4661bfb2..68dc393e 100644 --- a/src/scenario/bootstrap.js +++ b/src/scenario/angular-bootstrap.js @@ -1,6 +1,6 @@  (function(previousOnLoad){    var prefix = (function(){ -    var filename = /(.*\/)bootstrap.js(#(.*))?/; +    var filename = /(.*\/)angular-bootstrap.js(#(.*))?/;      var scripts = document.getElementsByTagName("script");      for(var j = 0; j < scripts.length; j++) {        var src = scripts[j].src; @@ -23,50 +23,36 @@      try {        if (previousOnLoad) previousOnLoad();      } catch(e) {} -    _jQuery(document.body).append( -      '<div id="runner"></div>' + -      '<div id="frame"></div>' -    ); -    var frame = _jQuery('#frame'); -    var runner = _jQuery('#runner'); -    var application = new angular.scenario.Application(frame); -    var ui = new angular.scenario.ui.Html(runner); -    $scenario.run(ui, application, angular.scenario.SpecRunner, function(error) { -      frame.remove(); -      if (error) { -        if (window.console) { -          console.log(error.stack || error); -        } else { -          // Do something for IE -          alert(error); -        } -      } -    }); +    angularScenarioInit($scenario, angularJsConfig(document));    };    addCSS("../../css/angular-scenario.css");    addScript("../../lib/jquery/jquery-1.4.2.js");    document.write( -      '<script type="text/javascript">' + -      'var _jQuery = jQuery.noConflict(true);' + -      '</script>' -    ); +    '<script type="text/javascript">' + +    'var _jQuery = jQuery.noConflict(true);' + +    '</script>' +  );    addScript("../angular-bootstrap.js");    addScript("Scenario.js");    addScript("Application.js");    addScript("Describe.js");    addScript("Future.js"); -  addScript("HtmlUI.js");    addScript("Runner.js");    addScript("SpecRunner.js");    addScript("dsl.js");    addScript("matchers.js"); +  addScript("ObjectModel.js"); +  addScript("output/Html.js"); +  addScript("output/Json.js"); +  addScript("output/Object.js"); +  addScript("output/Xml.js");    // Create the runner (which also sets up the global API)    document.write(      '<script type="text/javascript">' + -    'var $scenario = new angular.scenario.Runner(window);' + +    'var $scenario = new angular.scenario.Runner(window, angular.scenario.SpecRunner);' +      '</script>'    ); diff --git a/src/scenario/angular.prefix b/src/scenario/angular.prefix index d6660d61..fb9ae147 100644 --- a/src/scenario/angular.prefix +++ b/src/scenario/angular.prefix @@ -22,4 +22,4 @@   * THE SOFTWARE.   */  (function(window, document, previousOnLoad){ -  var _jQuery = window.jQuery.noConflict(true);
\ No newline at end of file +  var _jQuery = window.jQuery.noConflict(true); diff --git a/src/scenario/angular.suffix b/src/scenario/angular.suffix index c38f0ab5..66843013 100644 --- a/src/scenario/angular.suffix +++ b/src/scenario/angular.suffix @@ -4,25 +4,7 @@      try {        if (previousOnLoad) previousOnLoad();      } catch(e) {} -    _jQuery(document.body).append( -      '<div id="runner"></div>' + -      '<div id="frame"></div>' -    ); -    var frame = _jQuery('#frame'); -    var runner = _jQuery('#runner'); -    var application = new angular.scenario.Application(frame); -    var ui = new angular.scenario.ui.Html(runner); -    $scenario.run(ui, application, angular.scenario.SpecRunner, function(error) { -      frame.remove(); -      if (error) { -        if (window.console) { -          console.log(error.stack || error); -        } else { -          // Do something for IE -          alert(error); -        } -      } -    }); +    angularScenarioInit($scenario, angularJsConfig(document));    };  })(window, document, window.onload); diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js index f4484df8..1ae26db8 100644 --- a/src/scenario/dsl.js +++ b/src/scenario/dsl.js @@ -1,18 +1,19 @@  /**   * Shared DSL statements that are useful to all scenarios.   */ -  +   /**   * Usage:   *    wait() waits until you call resume() in the console   */ - angular.scenario.dsl('wait', function() { +angular.scenario.dsl('wait', function() {    return function() { -    return this.addFuture('waiting for you to call resume() in the console', function(done) { +    return this.addFuture('waiting for you to resume', function(done) { +      this.emit('InteractiveWait', this.spec, this.step);        this.$window.resume = function() { done(); };      });    }; - }); +});  /**  * Usage: @@ -21,7 +22,7 @@  angular.scenario.dsl('pause', function() {   return function(time) {     return this.addFuture('pause for ' + time + ' seconds', function(done) { -     this.setTimeout(function() { done(null, time * 1000); }, time * 1000); +     this.$window.setTimeout(function() { done(null, time * 1000); }, time * 1000);     });   };  }); @@ -49,8 +50,8 @@ angular.scenario.dsl('expect', function() {  /**   * Usage: - *    navigateTo(future|string) where url a string or future with a value - *    of a  URL to navigate to + *    navigateTo(url) Loads the url into the frame + *    navigateTo(url, fn) where fn(url) is called and returns the URL to navigate to   */  angular.scenario.dsl('navigateTo', function() {    return function(url, delegate) { @@ -60,17 +61,7 @@ angular.scenario.dsl('navigateTo', function() {          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); -            }); -          } else { -            done(null, url); -          } -        }); +        done(null, url);        });      });    }; @@ -162,7 +153,11 @@ angular.scenario.dsl('repeater', function() {    chain.count = function() {      return this.addFutureAction('repeater ' + this.selector + ' count', function($window, $document, done) { -      done(null, $document.elements().size()); +      try { +        done(null, $document.elements().length); +      } catch (e) { +        done(null, 0); +      }      });    }; @@ -238,6 +233,7 @@ angular.scenario.dsl('select', function() {  /**   * Usage: + *    element(selector).count() get the number of elements that match selector   *    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 @@ -248,10 +244,28 @@ angular.scenario.dsl('select', function() {  angular.scenario.dsl('element', function() {    var chain = {}; +  chain.count = function() { +    return this.addFutureAction('element ' + this.selector + ' count', function($window, $document, done) { +      try { +        done(null, $document.elements().length); +      } catch (e) { +        done(null, 0); +      } +    }); +  }; +    chain.click = function() {      return this.addFutureAction('element ' + this.selector + ' click', function($window, $document, done) { -      $document.elements().trigger('click'); -      done(); +      var elements = $document.elements(); +      var href = elements.attr('href'); +      elements.trigger('click'); +      if (href && elements[0].nodeName.toUpperCase() === 'A') { +        this.application.navigateTo(href, function() { +          done(); +        }); +      } else { +        done(); +      }      });    }; diff --git a/src/scenario/matchers.js b/src/scenario/matchers.js index 0dfbc455..8ef154e9 100644 --- a/src/scenario/matchers.js +++ b/src/scenario/matchers.js @@ -6,6 +6,10 @@ angular.scenario.matcher('toEqual', function(expected) {    return angular.equals(this.actual, expected);  }); +angular.scenario.matcher('toBe', function(expected) { +  return this.actual === expected; +}); +  angular.scenario.matcher('toBeDefined', function() {    return angular.isDefined(this.actual);  }); diff --git a/src/scenario/output/Html.js b/src/scenario/output/Html.js new file mode 100644 index 00000000..4a682b9a --- /dev/null +++ b/src/scenario/output/Html.js @@ -0,0 +1,165 @@ +/** + * User Interface for the Scenario Runner. + * + * TODO(esprehn): This should be refactored now that ObjectModel exists + *  to use angular bindings for the UI. + */ +angular.scenario.output('html', function(context, runner) { +  var model = new angular.scenario.ObjectModel(runner); + +  context.append( +    '<div id="header">' + +    '  <h1><span class="angular"><angular/></span>: Scenario Test Runner</h1>' + +    '  <ul id="status-legend" class="status-display">' + +    '    <li class="status-error">0 Errors</li>' + +    '    <li class="status-failure">0 Failures</li>' + +    '    <li class="status-success">0 Passed</li>' + +    '  </ul>' + +    '</div>' + +    '<div id="specs">' + +    '  <div class="test-children"></div>' + +    '</div>' +  ); + +  runner.on('InteractiveWait', function(spec, step) { +    var ui = model.getSpec(spec.id).getLastStep().ui; +    ui.find('.test-title'). +      html('waiting for you to <a href="javascript:resume()">resume</a>.'); +  }); + +  runner.on('SpecBegin', function(spec) { +    var ui = findContext(spec); +    ui.find('> .tests').append( +      '<li class="status-pending test-it"></li>' +    ); +    ui = ui.find('> .tests li:last'); +    ui.append( +      '<div class="test-info">' + +      '  <p class="test-title">' + +      '    <span class="timer-result"></span>' + +      '    <span class="test-name"></span>' + +      '  </p>' + +      '</div>' + +      '<div class="scrollpane">' + +      '  <ol class="test-actions"></ol>' + +      '</div>' +    ); +    ui.find('> .test-info .test-name').text(spec.name); +    ui.find('> .test-info').click(function() { +      var scrollpane = ui.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'); +      } +    }); +    model.getSpec(spec.id).ui = ui; +  }); + +  runner.on('SpecError', function(spec, error) { +    var ui = model.getSpec(spec.id).ui; +    ui.append('<pre></pre>'); +    ui.find('> pre').text(formatException(error)); +  }); + +  runner.on('SpecEnd', function(spec) { +    spec = model.getSpec(spec.id); +    spec.ui.removeClass('status-pending'); +    spec.ui.addClass('status-' + spec.status); +    spec.ui.find("> .test-info .timer-result").text(spec.duration + "ms"); +    if (spec.status === 'success') { +      spec.ui.find('> .test-info .test-name').addClass('closed'); +      spec.ui.find('> .scrollpane .test-actions').hide(); +    } +    updateTotals(spec.status); +  }); + +  runner.on('StepBegin', function(spec, step) { +    spec = model.getSpec(spec.id); +    step = spec.getLastStep(); +    spec.ui.find('> .scrollpane .test-actions'). +      append('<li class="status-pending"></li>'); +    step.ui = spec.ui.find('> .scrollpane .test-actions li:last'); +    step.ui.append( +      '<div class="timer-result"></div>' + +      '<div class="test-title"></div>' +    ); +    step.ui.find('> .test-title').text(step.name); +    var scrollpane = step.ui.parents('.scrollpane'); +    scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight')); +  }); + +  runner.on('StepFailure', function(spec, step, error) { +    var ui = model.getSpec(spec.id).getLastStep().ui; +    addError(ui, step.line, error); +  }); + +  runner.on('StepError', function(spec, step, error) { +    var ui = model.getSpec(spec.id).getLastStep().ui; +    addError(ui, step.line, error); +  }); + +  runner.on('StepEnd', function(spec, step) { +    spec = model.getSpec(spec.id); +    step = spec.getLastStep(); +    step.ui.find('.timer-result').text(step.duration + 'ms'); +    step.ui.removeClass('status-pending'); +    step.ui.addClass('status-' + step.status); +    var scrollpane = spec.ui.find('> .scrollpane'); +    scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight')); +  }); + +  /** +   * Finds the context of a spec block defined by the passed definition. +   * +   * @param {Object} The definition created by the Describe object. +   */ +  function findContext(spec) { +    var currentContext = context.find('#specs'); +    angular.foreach(model.getDefinitionPath(spec), function(defn) { +      var id = 'describe-' + defn.id; +      if (!context.find('#' + id).length) { +        currentContext.find('> .test-children').append( +          '<div class="test-describe" id="' + id + '">' + +          '  <h2></h2>' + +          '  <div class="test-children"></div>' + +          '  <ul class="tests"></ul>' + +          '</div>' +        ); +        context.find('#' + id).find('> h2').text('describe: ' + defn.name); +      } +      currentContext = context.find('#' + id); +    }); +    return context.find('#describe-' + spec.definition.id); +  }; + +  /** +   * Updates the test counter for the status. +   * +   * @param {string} the status. +   */ +  function updateTotals(status) { +    var legend = context.find('#status-legend .status-' + status); +    var parts = legend.text().split(' '); +    var value = (parts[0] * 1) + 1; +    legend.text(value + ' ' + parts[1]); +  } + +  /** +   * Add an error to a step. +   * +   * @param {Object} The JQuery wrapped context +   * @param {Function} fn() that should return the file/line number of the error +   * @param {Object} the error. +   */ +  function addError(context, line, error) { +    context.find('.test-title').append('<pre></pre>'); +    var message = _jQuery.trim(line() + '\n\n' + formatException(error)); +    context.find('.test-title pre:last').text(message); +  }; +}); diff --git a/src/scenario/output/Json.js b/src/scenario/output/Json.js new file mode 100644 index 00000000..94212301 --- /dev/null +++ b/src/scenario/output/Json.js @@ -0,0 +1,10 @@ +/** + * Generates JSON output into a context. + */ +angular.scenario.output('json', function(context, runner) { +  var model = new angular.scenario.ObjectModel(runner); +   +  runner.on('RunnerEnd', function() { +    context.text(angular.toJson(model.value)); +  }); +}); diff --git a/src/scenario/output/Object.js b/src/scenario/output/Object.js new file mode 100644 index 00000000..3257cfd7 --- /dev/null +++ b/src/scenario/output/Object.js @@ -0,0 +1,6 @@ +/** + * Creates a global value $result with the result of the runner. + */ +angular.scenario.output('object', function(context, runner) { +  runner.$window.$result = new angular.scenario.ObjectModel(runner).value; +}); diff --git a/src/scenario/output/Xml.js b/src/scenario/output/Xml.js new file mode 100644 index 00000000..47d98c78 --- /dev/null +++ b/src/scenario/output/Xml.js @@ -0,0 +1,48 @@ +/** + * Generates XML output into a context. + */ +angular.scenario.output('xml', function(context, runner) { +  var model = new angular.scenario.ObjectModel(runner); + +  runner.on('RunnerEnd', function() { +    context.append('<scenario></scenario>'); +    serializeXml(context.find('> scenario'), model.value); +  }); + +  /** +   * Convert the tree into XML. +   * +   * @param {Object} context jQuery context to add the XML to. +   * @param {Object} tree node to serialize +   */ +  function serializeXml(context, tree) { +     angular.foreach(tree.children, function(child) { +       context.append('<describe></describe>'); +       var describeContext = context.find('> describe:last'); +       describeContext.attr('id', child.id); +       describeContext.attr('name', child.name); +       serializeXml(describeContext, child); +     }); +     context.append('<its></its>'); +     context = context.find('> its'); +     angular.foreach(tree.specs, function(spec) { +       context.append('<it></it>') +       var specContext = context.find('> it:last'); +       specContext.attr('id', spec.id); +       specContext.attr('name', spec.name); +       specContext.attr('duration', spec.duration); +       specContext.attr('status', spec.status); +       angular.foreach(spec.steps, function(step) { +         specContext.append('<step></step>'); +         var stepContext = specContext.find('> step:last'); +         stepContext.attr('name', step.name); +         stepContext.attr('duration', step.duration); +         stepContext.attr('status', step.status); +         if (step.error) { +           stepContext.append('<error></error'); +           stepContext.find('error').text(formatException(step.error)); +         } +       }); +     }); +   } +}); | 
