diff options
| author | Elliott Sprehn | 2010-10-08 16:43:40 -0700 | 
|---|---|---|
| committer | Elliott Sprehn | 2010-10-14 09:47:39 -0700 | 
| commit | 03df6cbddbb80186caf571e29957370b2ef9881c (patch) | |
| tree | d5a321c8b207b464a5c8a300c422186e20e8ae31 | |
| parent | 0f104317dff5628765e26cc68df7dd1175b2aa5e (diff) | |
| download | angular.js-03df6cbddbb80186caf571e29957370b2ef9881c.tar.bz2 | |
New Angular Scenario runner and DSL system with redesigned HTML UI.
Uses the Jasmine syntax for tests, ex:
describe('widgets', function() {
  it('should verify that basic widgets work', function(){
    navigateTo('widgets.html');
    input('text.basic').enter('Carlos');
    expect(binding('text.basic')).toEqual('Carlos');
    input('text.basic').enter('Carlos Santana');
    expect(binding('text.basic')).not().toEqual('Carlos Boozer');
    input('text.password').enter('secret');
    expect(binding('text.password')).toEqual('secret');
    expect(binding('text.hidden')).toEqual('hiddenValue');
    expect(binding('gender')).toEqual('male');
    input('gender').select('female');
    expect(binding('gender')).toEqual('female');
  });
});
Note: To create new UI's implement the interface shown in angular.scenario.ui.Html.
32 files changed, 1979 insertions, 900 deletions
| @@ -1,5 +1,46 @@  include FileUtils +ANGULAR = [ +  'src/Angular.js', +  'src/JSON.js', +  'src/Compiler.js', +  'src/Scope.js', +  'src/Injector.js', +  'src/Parser.js', +  'src/Resource.js', +  'src/Browser.js', +  'src/jqLite.js', +  'src/apis.js', +  'src/filters.js', +  'src/formatters.js', +  'src/validators.js', +  'src/services.js', +  'src/directives.js', +  'src/markups.js', +  'src/widgets.js', +  'src/AngularPublic.js', +] + +ANGULAR_SCENARIO = [ +  'src/scenario/Scenario.js', +  'src/scenario/Application.js', +  'src/scenario/Describe.js', +  'src/scenario/Future.js', +  'src/scenario/HtmlUI.js', +  'src/scenario/Describe.js', +  'src/scenario/Runner.js', +  'src/scenario/SpecRunner.js', +  'src/scenario/dsl.js', +  'src/scenario/matchers.js', +] + +GENERATED_FILES = [ +  'angular-debug.js', +  'angular-minified.js', +  'angular-minified.map', +  'angular-scenario.js', +] +  task :default => [:compile, :test]  desc 'Generate Externs' @@ -20,31 +61,27 @@ task :compile_externs do    out.close  end +desc 'Clean Generated Files' +task :clean do   +  GENERATED_FILES.each do |file| +    `rm #{file}` +  end +end +  desc 'Compile Scenario'  task :compile_scenario do -  concat = %x(cat \ -      lib/jquery/jquery-1.4.2.js \ -      src/scenario/angular.prefix \ -      src/Angular.js \ -      src/jqLite.js \ -      src/JSON.js \ -      src/Scope.js \ -      src/Injector.js \ -      src/Parser.js \ -      src/Resource.js \ -      src/Browser.js \ -      src/apis.js \ -      src/services.js \ -      src/AngularPublic.js \ -      src/scenario/DSL.js \ -      src/scenario/Future.js \ -      src/scenario/Matcher.js \ -      src/scenario/Runner.js \ -      src/scenario/angular.suffix \ -    ) +   +  deps = [ +      'lib/jquery/jquery-1.4.2.js', +      'src/scenario/angular.prefix', +      ANGULAR, +      ANGULAR_SCENARIO, +      'src/scenario/angular.suffix', +  ]    css = %x(cat css/angular-scenario.css) +  concat = 'cat ' + deps.flatten.join(' ')    f = File.new("angular-scenario.js", 'w') -  f.write(concat) +  f.write(%x{#{concat}})    f.write('document.write(\'<style type="text/css">\n')    f.write(css.gsub(/'/, "\\'").gsub(/\n/, "\\n"));    f.write('\n</style>\');') @@ -54,30 +91,14 @@ end  desc 'Compile JavaScript'  task :compile => [:compile_externs, :compile_scenario] do -  concat = %x(cat \ -      src/angular.prefix \ -      src/Angular.js \ -      src/JSON.js \ -      src/Compiler.js \ -      src/Scope.js \ -      src/Injector.js \ -      src/Parser.js \ -      src/Resource.js \ -      src/Browser.js \ -      src/jqLite.js \ -      src/apis.js \ -      src/filters.js \ -      src/formatters.js \ -      src/validators.js \ -      src/services.js \ -      src/directives.js \ -      src/markups.js \ -      src/widgets.js \ -      src/AngularPublic.js \ -      src/angular.suffix \ -    ) +  deps = [ +      'src/angular.prefix', +      ANGULAR, +      'src/angular.suffix', +  ]    f = File.new("angular-debug.js", 'w') -  f.write(concat) +  concat = 'cat ' + deps.flatten.join(' ') +  f.write(%x{#{concat}})    f.close    %x(java -jar lib/compiler-closure/compiler.jar \ diff --git a/css/angular-scenario.css b/css/angular-scenario.css index 3960c357..2cf24b19 100644 --- a/css/angular-scenario.css +++ b/css/angular-scenario.css @@ -1,76 +1,199 @@  @charset "UTF-8";  /* CSS Document */ -#runner { -  position: absolute; -  top:5px; -  left:10px; -  right:10px; -  height: 200px; +/** Structure */ +body { +  font-family: Arial, sans-serif; +  margin: 0; +  font-size: 14px;  } -.console { -  display: block; -  overflow: scroll; -  height: 200px; -  border: 1px solid black; +#header { +  position: fixed; +  width: 100%; +} + +#specs { +  padding-top: 50px; +} + +#header .angular { +  font-family: Courier New, monospace; +  font-weight: bold; +} + +#header h1 { +  font-weight: normal; +  float: left; +  font-size: 30px; +  line-height: 30px; +  margin: 0; +  padding: 10px 10px; +  height: 30px; +} + +#frame h2, +#specs h2 { +  margin: 0; +  padding: 0.5em; +  font-size: 1.1em; +} +       +#status-legend { +  margin-top: 10px; +  margin-right: 10px; +} + +#header,  +#frame, +.test-info,  +.test-actions li { +  overflow: hidden;  } -#testView { -  position: absolute; -  bottom:10px; -  top:230px; -  left:10px; -  right:10px; +#frame { +  margin: 10px;  } -#testView iframe { +#frame iframe {    width: 100%; -  height: 100%; +  height: 758px; +} + +#frame .popout { +  float: right;  } -li.running > span { -  background-color: yellow; +#frame iframe { +  border: none; +} +       +.tests li,  +.test-actions li, +.test-it li,  +.test-it ol, +.status-display { +  list-style-type: none;  } -#runner span { -  background-color: green; +.tests, +.test-it ol, +.status-display { +  margin: 0; +  padding: 0;  } -#runner .fail > span { -  background-color: red; +.test-info {  +  margin-left: 1em; +  margin-top: 0.5em; +  border-radius: 8px 0 0 8px; +  -webkit-border-radius: 8px 0 0 8px; +  -moz-border-radius: 8px 0 0 8px; +} +                   +.test-it ol { +  margin-left: 2.5em;  } -.collapsed > ul { -  display: none; +.status-display, +.status-display li { +  float: right;  } -////// +.status-display li { +  padding: 5px 10px; +} -.run, .info, .error { -  display: block; -  padding: 0 1em; +.timer-result,  +.test-title { +  display: inline-block; +  margin: 0; +  padding: 4px; +} + +.timer-result { +  width: 4em; +  padding: 0 10px; +  text-align: right;    font-family: monospace; -  white-space: pre;  } -.run { -  background-color: lightgrey; -  padding: 0 .2em; +.test-it pre, +.test-actions pre { +  clear: left; +  margin-left: 6em;  } -.run.pass { -  background-color: lightgreen; +.test-describe .test-describe { +  margin: 5px 5px 10px 2em;  } -.run.fail { -  background-color: lightred; +.test-actions .status-pending .test-title:before { +  content: 'ยป '; +} + +/** Colors */ + +#header { +  background-color: #F2C200;  } -.name, .time, .state { -  padding-right: 2em; +#specs h2 { +  border-top: 2px solid #BABAD1;  } -error { -  color: red; -}
\ No newline at end of file +#specs h2, +#frame h2 { +  background-color: #efefef; +} + +#frame { +  border: 1px solid #BABAD1; +} +       +.test-describe .test-describe { +  border-left: 1px solid #BABAD1; +  border-right: 1px solid #BABAD1; +  border-bottom: 1px solid #BABAD1; +} + +.status-display { +  border: 1px solid #777; +} + +.status-display .status-pending, +.status-pending .test-info { +  background-color: #F9EEBC; +} + +.status-display .status-success, +.status-success .test-info { +  background-color: #B1D7A1;  +} +       +.status-display .status-failure, +.status-failure .test-info {  +  background-color: #FF8286;  +} + +.status-display .status-error, +.status-error .test-info {  +  background-color: black; +  color: white; +} + +.test-actions .status-success .test-title {  +  color: #30B30A;  +} + +.test-actions .status-failure .test-title {  +  color: #DF0000; +} + +.test-actions .status-error .test-title {  +  color: black; +} + +.test-actions .timer-result { +  color: #888; +} diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf index ed58d269..26bdd614 100644 --- a/jsTestDriver-jquery.conf +++ b/jsTestDriver-jquery.conf @@ -9,7 +9,7 @@ load:    - src/JSON.js    - src/*.js    - test/testabilityPatch.js -  - src/scenario/Runner.js +  - src/scenario/Scenario.js    - src/scenario/*.js    - test/angular-mocks.js    - test/scenario/*.js diff --git a/jsTestDriver.conf b/jsTestDriver.conf index c7d74b75..9d3d980d 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -9,7 +9,7 @@ load:    - src/JSON.js    - src/*.js    - test/testabilityPatch.js -  - src/scenario/Runner.js +  - src/scenario/Scenario.js    - src/scenario/*.js    - test/angular-mocks.js    - test/scenario/*.js diff --git a/scenario/style.css b/scenario/style.css index 956bdc52..43690e2c 100644 --- a/scenario/style.css +++ b/scenario/style.css @@ -5,3 +5,7 @@ th {  tr {    border: 1px solid black;  } + +.redbox { +  background-color: red; +}
\ No newline at end of file diff --git a/scenario/widgets-scenario.js b/scenario/widgets-scenario.js index f4488190..69fdc10e 100644 --- a/scenario/widgets-scenario.js +++ b/scenario/widgets-scenario.js @@ -1,25 +1,58 @@ -describe('widgets', function(){ +describe('widgets', function() {    it('should verify that basic widgets work', function(){ -    browser.navigateTo('widgets.html'); - -    expect('{{text.basic}}').toEqual(''); -    input('text.basic').enter('John'); -    expect('{{text.basic}}').toEqual('John'); - -    expect('{{text.password}}').toEqual(''); +    navigateTo('widgets.html'); +    input('text.basic').enter('Carlos'); +    expect(binding('text.basic')).toEqual('Carlos'); +    pause(2); +    input('text.basic').enter('Carlos Santana'); +    pause(2); +    expect(binding('text.basic')).not().toEqual('Carlos Boozer'); +    pause(2);      input('text.password').enter('secret'); -    expect('{{text.password}}').toEqual('secret'); - -    expect('{{text.hidden}}').toEqual('hiddenValue'); - -    expect('{{gender}}').toEqual('male'); +    expect(binding('text.password')).toEqual('secret'); +    expect(binding('text.hidden')).toEqual('hiddenValue'); +    expect(binding('gender')).toEqual('male'); +    pause(2);      input('gender').select('female'); -    input('gender').isChecked('female'); -    expect('{{gender}}').toEqual('female'); - -//    expect('{{tea}}').toBeChecked(); -//    input('gender').select('female'); -//    expect('{{gender}}').toEqual('female'); - +    expect(binding('gender')).toEqual('female'); +    pause(2); +  }); +  describe('do it again', function() { +    it('should verify that basic widgets work', function(){ +      navigateTo('widgets.html'); +      input('text.basic').enter('Carlos'); +      expect(binding('text.basic')).toEqual('Carlos'); +      pause(2); +      input('text.basic').enter('Carlos Santana'); +      pause(2); +      expect(binding('text.basic')).toEqual('Carlos Santana'); +      pause(2); +      input('text.password').enter('secret'); +      expect(binding('text.password')).toEqual('secret'); +      expect(binding('text.hidden')).toEqual('hiddenValue'); +      expect(binding('gender')).toEqual('male'); +      pause(2); +      input('gender').select('female'); +      expect(binding('gender')).toEqual('female'); +      pause(2); +    }); +  }); +  it('should verify that basic widgets work', function(){ +    navigateTo('widgets.html'); +    input('text.basic').enter('Carlos'); +    expect(binding('text.basic')).toEqual('Carlos'); +    pause(2); +    input('text.basic').enter('Carlos Santana'); +    pause(2); +    expect(binding('text.basic')).toEqual('Carlos Santana'); +    pause(2); +    input('text.password').enter('secret'); +    expect(binding('text.password')).toEqual('secret'); +    expect(binding('text.hidden')).toEqual('hiddenValue'); +    expect(binding('gender')).toEqual('male'); +    pause(2); +    input('gender').select('female'); +    expect(binding('gender')).toEqual('female'); +    pause(2);    });  }); diff --git a/src/Angular.js b/src/Angular.js index 95970850..72f341f3 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -270,11 +270,11 @@ function equals(o1, o2) {      } else {        keySet = {};        for(key in o1) { -        if (key.charAt(0) !== '$' && !equals(o1[key], o2[key])) return false; +        if (key.charAt(0) !== '$' && !isFunction(o1[key]) && !equals(o1[key], o2[key])) return false;          keySet[key] = true;        }        for(key in o2) { -        if (key.charAt(0) !== '$' && keySet[key] !== true) return false; +        if (!keySet[key] && key.charAt(0) !== '$' && !isFunction(o2[key])) return false;        }        return true;      } diff --git a/src/scenario/Application.js b/src/scenario/Application.js new file mode 100644 index 00000000..24ae99e9 --- /dev/null +++ b/src/scenario/Application.js @@ -0,0 +1,51 @@ +/** + * Represents the application currently being tested and abstracts usage + * of iframes or separate windows. + */ +angular.scenario.Application = function(context) { +  this.context = context; +  context.append('<h2>Current URL: <a href="about:blank">None</a></h2>'); +}; + +/** + * Gets the jQuery collection of frames. Don't use this directly because + * frames may go stale. + * + * @return {Object} jQuery collection + */ +angular.scenario.Application.prototype.getFrame = function() { +  return this.context.find('> iframe'); +}; + +/** + * Gets the window of the test runner frame. Always favor executeAction()  + * instead of this method since it prevents you from getting a stale window. + * + * @return {Object} the window of the frame + */ +angular.scenario.Application.prototype.getWindow = function() { +  var contentWindow = this.getFrame().attr('contentWindow'); +  if (!contentWindow) +    throw 'No window available because frame not loaded.'; +  return contentWindow; +}; + +/** + * Changes the location of the frame. + */ +angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) { +  this.getFrame().remove(); +  this.context.append('<iframe src=""></iframe>'); +  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. + * + * @param {Function} The callback to execute. function($window, $document) + */ +angular.scenario.Application.prototype.executeAction = function(action) { +  var $window = this.getWindow(); +  return action.call($window, _jQuery($window.document), $window); +}; diff --git a/src/scenario/DSL.js b/src/scenario/DSL.js index dc85ea45..a7571afe 100644 --- a/src/scenario/DSL.js +++ b/src/scenario/DSL.js @@ -1,131 +1,134 @@ -angular.scenario.dsl.browser = { -  navigateTo: function(url){ -    var location = this.location; -    return $scenario.addFuture('Navigate to: ' + url, function(done){ -      var self = this; -      this.testFrame.load(function(){ -        self.testFrame.unbind(); -        self.testWindow = self.testFrame[0].contentWindow; -        self.testDocument = self.jQuery(self.testWindow.document); -        self.$browser = self.testWindow.angular.service.$browser(); -        self.notifyWhenNoOutstandingRequests = -          bind(self.$browser, self.$browser.notifyWhenNoOutstandingRequests); -        self.notifyWhenNoOutstandingRequests(done); -      }); -      if (this.testFrame.attr('src') == url) { -        this.testFrame[0].contentWindow.location.reload(); -      } else { -        this.testFrame.attr('src', url); -        location.setLocation(url); -      } -    }); -  }, -  location: { -    href: "", -    hash: "", -    toEqual: function(url) { -      return (this.hash === "" ? (url == this.href) : -        (url == (this.href + "/#/" + this.hash))); -    }, -    setLocation: function(url) { -      var urlParts = url.split("/#/"); -      this.href = urlParts[0] || ""; -      this.hash = urlParts[1] || ""; -    } -  } -}; - -angular.scenario.dsl.input = function(selector) { -  var namePrefix = "input '" + selector + "'"; -  return { -    enter: function(value) { -      return $scenario.addFuture(namePrefix + " enter '" + value + "'", function(done) { -        var input = this.testDocument.find('input[name=' + selector + ']'); -        input.val(value); -        this.testWindow.angular.element(input[0]).trigger('change'); -        done(); -      }); -    }, -    select: function(value) { -      return $scenario.addFuture(namePrefix + " select '" + value + "'", function(done) { -        var input = this.testDocument. -          find(':radio[name$=@' + selector + '][value=' + value + ']'); -        jqLiteWrap(input[0]).trigger('click'); -        input[0].checked = true; -        done(); -      }); -    } -  }; -}; +/** + * Shared DSL statements that are useful to all scenarios. + */ -angular.scenario.dsl.NG_BIND_PATTERN =/\{\{[^\}]+\}\}/; +/** +* Usage: +*    pause(seconds) pauses the test for specified number of seconds +*/ +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); +   }); + }; +}); -angular.scenario.dsl.repeater = function(selector) { -  var namePrefix = "repeater '" + selector + "'"; -  return { -    count: function() { -      return $scenario.addFuture(namePrefix + ' count', function(done) { -          done(this.testDocument.find(selector).size()); -      }); -    }, -    collect: function(collectSelector) { -      return $scenario.addFuture( -          namePrefix + " collect '" + collectSelector + "'", -          function(done) { -        var self = this; -        var doCollect = bind(this, function() { -          var repeaterArray = [], ngBindPattern; -          var startIndex = collectSelector.search( -              angular.scenario.dsl.NG_BIND_PATTERN); -          if (startIndex >= 0) { -            ngBindPattern = collectSelector.substring( -                startIndex + 2, collectSelector.length - 2); -            collectSelector = '*'; -             -          } -          this.testDocument.find(selector).each(function() { -            var element = self.jQuery(this); -            element.find(collectSelector). -              each(function() { -                var foundElem = self.jQuery(this); -                if (foundElem.attr('ng:bind') == ngBindPattern) { -                  repeaterArray.push(foundElem.text()); -                } -            }); -          }); -          return repeaterArray; -        }); -        done(doCollect()); -      }); -    } +/** + * Usage: + *    expect(future).{matcher} where matcher is one of the matchers defined + *    with angular.scenario.matcher + * + * ex. expect(binding("name")).toEqual("Elliott") + */ +angular.scenario.dsl('expect', function() { +  var chain = angular.extend({}, angular.scenario.matcher); +   +  chain.not = function() { +    this.inverse = true; +    return chain; +  }; +   +  return function(future) { +    this.future = future; +    return chain;    }; -}; +}); -angular.scenario.dsl.element = function(selector) { -  var namePrefix = "Element '" + selector + "'"; -  var futureJquery = {}; -  for (key in (jQuery || _jQuery).fn) { -    (function(){ -      var jqFnName = key; -      var jqFn = (jQuery || _jQuery).fn[key]; -      futureJquery[key] = function() { -        var jqArgs = arguments; -        return $scenario.addFuture(namePrefix + "." + jqFnName + "()", -                function(done) { -          var self = this, repeaterArray = [], ngBindPattern; -          var startIndex = selector.search(angular.scenario.dsl.NG_BIND_PATTERN); -          if (startIndex >= 0) { -            ngBindPattern = selector.substring(startIndex + 2, selector.length - 2); -            var element = this.testDocument.find('*').filter(function() { -              return self.jQuery(this).attr('ng:bind') == ngBindPattern; +/** + * Usage: + *    navigateTo(future|string) where url a string or future with a value  + *    of a  URL to navigate to + */ +angular.scenario.dsl('navigateTo', function() { +  return function(url) { +    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() { +        application.executeAction(function() { +          if (this.angular) { +            var $browser = this.angular.service.$browser(); +            $browser.poll(); +            $browser.notifyWhenNoOutstandingRequests(function() { +              done(null, url.value || url);              }); -            done(jqFn.apply(element, jqArgs));            } else { -            done(jqFn.apply(this.testDocument.find(selector), jqArgs)); +            done(null, url.value || url);            }          }); -      }; -    })(); -  } -  return futureJquery; -}; +      }); +    }); +  }; +}); + +/** + * Usage: + *    input(name).enter(value) enters value in input with specified name + *    input(name).check() checks checkbox + *    input(name).select(value) selects the readio button with specified name/value + */ +angular.scenario.dsl('input', function() { +  var chain = {}; +   +  chain.enter = function(value) { +    var spec = this; +    return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function(done) { +      var input = _jQuery(this.document).find('input[name=' + spec.name + ']'); +      if (!input.length) +        return done("Input named '" + spec.name + "' does not exist."); +      input.val(value); +      this.angular.element(input[0]).trigger('change'); +      done(); +    }); +  }; +   +  chain.check = function() { +    var spec = this; +    return this.addFutureAction("checkbox '" + this.name + "' toggle", function(done) { +      var input = _jQuery(this.document). +        find('input:checkbox[name=' + spec.name + ']'); +      if (!input.length) +        return done("Input named '" + spec.name + "' does not exist."); +      this.angular.element(input[0]).trigger('click'); +      input.attr('checked', !input.attr('checked')); +      done(); +    }); +  }; +   +  chain.select = function(value) { +    var spec = this; +    return this.addFutureAction("radio button '" + this.name + "' toggle '" + value + "'", function(done) { +      var input = _jQuery(this.document). +        find('input:radio[name$="@' + spec.name + '"][value="' + value + '"]'); +      if (!input.length) +        return done("Input named '" + spec.name + "' does not exist."); +      this.angular.element(input[0]).trigger('click'); +      input.attr('checked', !input.attr('checked')); +      done(); +    }); +  }; +   +  return function(name) { +    this.name = name; +    return chain; +  }; +}); + +/** + * Usage: + *    binding(name) returns the value of a binding + */ +angular.scenario.dsl('binding', function() { +  return function(name) { +    return this.addFutureAction("select binding '" + name + "'", function(done) { +      var element = _jQuery(this.document).find('[ng\\:bind="' + name + '"]'); +      if (!element.length) +        return done("Binding named '" + name + "' does not exist."); +      done(null, element.text()); +    }); +  }; +}); diff --git a/src/scenario/Describe.js b/src/scenario/Describe.js new file mode 100644 index 00000000..896b337f --- /dev/null +++ b/src/scenario/Describe.js @@ -0,0 +1,108 @@ +/** + * The representation of define blocks. Don't used directly, instead use + * define() in your tests. + */ +angular.scenario.Describe = function(descName, parent) { +  this.beforeEachFns = []; +  this.afterEachFns = []; +  this.its = []; +  this.children = []; +  this.name = descName; +  this.parent = parent; +  this.id = angular.scenario.Describe.id++; +   +  /** +   * Calls all before functions. +   */ +  var beforeEachFns = this.beforeEachFns; +  this.setupBefore = function() { +    if (parent) parent.setupBefore.call(this); +    angular.foreach(beforeEachFns, function(fn) { fn.call(this); }, this); +  }; + +  /** +   * Calls all after functions. +   */ +  var afterEachFns = this.afterEachFns; +  this.setupAfter  = function() { +    angular.foreach(afterEachFns, function(fn) { fn.call(this); }, this); +    if (parent) parent.setupAfter.call(this); +  }; +}; + +// Shared Unique ID generator for every describe block +angular.scenario.Describe.id = 0; + +/** + * Defines a block to execute before each it or nested describe. + * + * @param {Function} Body of the block. + */ +angular.scenario.Describe.prototype.beforeEach = function(body) { +  this.beforeEachFns.push(body); +}; + +/** + * Defines a block to execute after each it or nested describe. + * + * @param {Function} Body of the block. + */ +angular.scenario.Describe.prototype.afterEach = function(body) { +  this.afterEachFns.push(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. + */ +angular.scenario.Describe.prototype.describe = function(name, body) { +  var child = new angular.scenario.Describe(name, this); +  this.children.push(child); +  body.call(child); +}; + +/** + * Use to disable a describe block. + */ +angular.scenario.Describe.prototype.xdescribe = angular.noop; + +/** + * Defines a test. + * + * @param {String} Name of the test. + * @param {Function} Body of the block. + */ +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); +    } +  }); +}; + +/** + * Use to disable a test block. + */ +angular.scenario.Describe.prototype.xit = angular.noop; + +/** + * Gets an array of functions representing all the tests (recursively). + * that can be executed with SpecRunner's. + */ +angular.scenario.Describe.prototype.getSpecs = function() { +  var specs = arguments[0] || []; +  angular.foreach(this.children, function(child) { +    child.getSpecs(specs); +  }); +  angular.foreach(this.its, function(it) { +    specs.push(it); +  }); +  return specs; +}; diff --git a/src/scenario/Future.js b/src/scenario/Future.js index cc40eff0..60fad9c5 100644 --- a/src/scenario/Future.js +++ b/src/scenario/Future.js @@ -1,13 +1,22 @@ -function Future(name, behavior) { +/** + * A future action in a spec. + */ +angular.scenario.Future = function(name, behavior) {    this.name = name;    this.behavior = behavior;    this.fulfilled = false; -  this.value = _undefined; -} +  this.value = undefined; +}; -Future.prototype = { -  fulfill: function(value) { +/** + * Executes the behavior of the closure. + * + * @param {Function} Callback function(error, result) + */ +angular.scenario.Future.prototype.execute = function(doneFn) { +  this.behavior(angular.bind(this, function(error, result) {      this.fulfilled = true; -    this.value = value; -  } +    this.value = error || result; +    doneFn(error, result); +  }));  }; diff --git a/src/scenario/HtmlUI.js b/src/scenario/HtmlUI.js new file mode 100644 index 00000000..46c88837 --- /dev/null +++ b/src/scenario/HtmlUI.js @@ -0,0 +1,204 @@ +/** + * 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>' +  ); +}; + +/** + * 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 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) { +      var status = this.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 path = []; +  var currentContext = this.context.find('#specs'); +  var currentDefinition = definition; +  while (currentDefinition && currentDefinition.name) { +    path.unshift(currentDefinition); +    currentDefinition = currentDefinition.parent; +  } +  angular.foreach(path, angular.bind(this, function(defn) { +    var id = 'describe-' + defn.id; +    if (!this.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>' +      ); +      this.context.find('#' + id).find('> h2').text('describe: ' + defn.name); +    } +    currentContext = this.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>' + +    '<ol class="test-actions">' + +    '</ol>'     +  ); +  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. + */ +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'); +  var self = this; +  return new angular.scenario.ui.Html.Step(stepContext, name, function(status) { +    self.status = status; +  }); +}; + +/** + * 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"); +}; + +/** + * Finishes the spec, possibly with an error. + * + * @param {Object} An optional error + */ +angular.scenario.ui.Html.Spec.prototype.finish = function(error) { +  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); +}; + +/** + * Finishes the spec, but with a Fatal Error. + * + * @param {Object} Required error + */ +angular.scenario.ui.Html.Spec.prototype.error = function(error) { +  this.finish(error); +}; + +/** + * 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} Callback function(status) to call when complete. + */ +angular.scenario.ui.Html.Step = function(context, name, doneFn) { +  this.context = context; +  this.name = name; +  this.startTime = new Date().getTime(); +  this.doneFn = doneFn; +  context.append( +    '<span class="timer-result"></span>' + +    '<span class="test-title"></span>' +  ); +  context.find('> .test-title').text(name); +}; + +/** + * Completes the step and sets the timer value. + */ +angular.scenario.ui.Html.Step.prototype.complete = function() { +  this.context.removeClass('status-pending'); +  var endTime = new Date().getTime(); +  this.context.find(".timer-result") +    .text((endTime - this.startTime) + "ms"); +}; + +/** + * Finishes the step, possibly with an error. + * + * @param {Object} An optional error + */ +angular.scenario.ui.Html.Step.prototype.finish = function(error) { +  this.complete(); +  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(); +  this.context.addClass('status-error'); +  this.doneFn('error'); +}; diff --git a/src/scenario/Matcher.js b/src/scenario/Matcher.js deleted file mode 100644 index a9c86571..00000000 --- a/src/scenario/Matcher.js +++ /dev/null @@ -1,21 +0,0 @@ -function Matcher(scope, future, logger) { -  var self = scope.$scenario = this; -  this.logger = logger; -  this.future = future; -} - -Matcher.addMatcher = function(name, matcher) { -  Matcher.prototype[name] = function(expected) { -    var future = this.future; -    $scenario.addFuture( -      'expect ' + future.name + ' ' + name + ' ' + expected, -      function(done){ -        if (!matcher(future.value, expected)) -          throw "Expected " + expected + ' but was ' + future.value; -        done(); -      } -    ); -  }; -}; - -Matcher.addMatcher('toEqual', angular.equals); diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js index 77969618..0267bb2d 100644 --- a/src/scenario/Runner.js +++ b/src/scenario/Runner.js @@ -1,183 +1,95 @@ -angular['scenario'] = angular['scenario'] || (angular['scenario'] = {}); -angular.scenario['dsl'] = angular.scenario['dsl'] || (angular.scenario['dsl'] = {}); - -angular.scenario.Runner = function(scope, jQuery){ -  var self = scope.$scenario = this; -  this.scope = scope; -  this.jQuery = jQuery; -  this.scope.$testrun = {done: false, results: []}; - -  var specs = this.specs = {}; -  this.currentSpec = {name: '', futures: []}; -  var path = []; -  this.scope.describe = function(name, body){ -    path.push(name); -    body(); -    path.pop(); -  }; -  var beforeEach = noop; -  var afterEach = noop; -  this.scope.beforeEach = function(body) { -    beforeEach = body; -  }; -  this.scope.afterEach = function(body) { -    afterEach = body; -  }; -  this.scope.expect = function(future) { -    return new Matcher(self, future, self.logger); +/** + * Runner for scenarios. + */ +angular.scenario.Runner = function($window) { +  this.$window = $window; +  this.rootDescribe = new angular.scenario.Describe(); +  this.currentDescribe = this.rootDescribe; +  this.api = { +    it: this.it, +    xit: angular.noop, +    describe: this.describe, +    xdescribe: angular.noop, +    beforeEach: this.beforeEach, +    afterEach: this.afterEach    }; -  this.scope.it = function(name, body) { -    var specName = path.join(' ') + ': it ' + name; -    self.currentSpec = specs[specName] = { -        name: specName, -        futures: [] -     }; +  angular.foreach(this.api, angular.bind(this, function(fn, key) { +    this.$window[key] = angular.bind(this, fn); +  })); +}; + +/** + * Defines a describe block of a spec. + * + * @param {String} Name of the block + * @param {Function} Body of the block + */ +angular.scenario.Runner.prototype.describe = function(name, body) { +  var self = this; +  this.currentDescribe.describe(name, function() { +    var parentDescribe = self.currentDescribe; +    self.currentDescribe = this;      try { -      beforeEach(); -      body(); -    } catch(err) { -      self.addFuture(err.message || 'ERROR', function(){ -        throw err; -      }); +      body.call(this);      } finally { -      afterEach(); +      self.currentDescribe = parentDescribe;      } -    self.currentSpec = _null; -  }; -  this.logger = function returnNoop(){ -    return extend(returnNoop, {close:noop, fail:noop}); -  }; +  });  }; -angular.scenario.Runner.prototype = { -  run: function(body){ -    var jQuery = this.jQuery; -    body.append( -      '<div id="runner">' + -        '<div class="console"></div>' + -      '</div>' + -      '<div id="testView">' + -        '<iframe></iframe>' + -      '</div>'); -    var console = body.find('#runner .console'); -    console.find('li').live('click', function(){ -      jQuery(this).toggleClass('collapsed'); -    }); -    this.testFrame = body.find('#testView iframe'); -    function logger(parent) { -      var container; -      return function(type, text) { -        if (!container) { -          container = jQuery('<ul></ul>'); -          parent.append(container); -        } -        var element = jQuery('<li class="running '+type+'"><span></span></li>'); -        element.find('span').text(text); -        container.append(element); -        return extend(logger(element), { -          close: function(){ -            element.removeClass('running'); -            if(!element.hasClass('fail')) -              element.addClass('collapsed'); -            console.scrollTop(console[0].scrollHeight); -          }, -          fail: function(){ -            element.removeClass('running'); -            var current = element; -            while (current[0] != console[0]) { -              if (current.is('li')) -                current.addClass('fail'); -              current = current.parent(); -            } -          } -        }); -      }; -    } -    this.logger = logger(console); -    var specNames = []; -    foreach(this.specs, function(spec, name){ -      specNames.push(name); -    }, this); -    specNames.sort(); -    var self = this; -    function callback(){ -      var next = specNames.shift(); -      if(next) { -        self.execute(next, callback); -      } else { -        self.scope.$testrun.done = true; -      } -    } -    callback(); -  }, +/** + * Defines a test in a describe block of a spec. + * + * @param {String} Name of the block + * @param {Function} Body of the block + */ +angular.scenario.Runner.prototype.it = function(name, body) {  +  this.currentDescribe.it(name, body);  +}; -  addFuture: function(name, behavior) { -    var future = new Future(name, behavior); -    this.currentSpec.futures.push(future); -    return future; -  }, +/** + * Defines a function to be called before each it block in the describe + * (and before all nested describes). + * + * @param {Function} Callback to execute + */ +angular.scenario.Runner.prototype.beforeEach = function(body) { +  this.currentDescribe.beforeEach(body);  +}; -  execute: function(name, callback) { -   var spec = this.specs[name], -       self = this, -       futuresFulfilled = [], -       result = { -         passed: false, -         failed: false, -         finished: false, -         fail: function(error) { -           result.passed = false; -           result.failed = true; -           result.error = error; -           result.log('fail', isString(error) ? error : toJson(error)).fail(); -         } -       }, -       specThis = createScope({ -         result: result, -         jQuery: this.jQuery, -         testFrame: this.testFrame, -         testWindow: this.testWindow -       }, angularService, {}); -   this.self = specThis; -   var futureLogger = this.logger('spec', name); -   spec.nextFutureIndex = 0; -   function done() { -     result.finished = true; -     futureLogger.close(); -     self.self = _null; -     (callback||noop).call(specThis); -   } -   function next(value){ -     if (spec.nextFutureIndex > 0) { -       spec.futures[spec.nextFutureIndex - 1].fulfill(value); -     } -     var future = spec.futures[spec.nextFutureIndex]; -     (result.log || {close:noop}).close(); -     result.log = _null; -     if (future) { -       spec.nextFutureIndex ++; -       result.log = futureLogger('future', future.name); -       futuresFulfilled.push(future.name); -       try { -         future.behavior.call(specThis, next); -       } catch (e) { -         console.error(e); -         result.fail(e); -         self.scope.$testrun.results.push( -           {name: name, passed: false, error: e, steps: futuresFulfilled}); -         done(); -       } -     } else { -       result.passed = !result.failed; -       self.scope.$testrun.results.push({ -         name: name, -         passed: !result.failed, -         error: result.error, -         steps: futuresFulfilled}); -       done(); -     } -   } -   next(); -   return specThis; -  } -};
\ No newline at end of file +/** + * Defines a function to be called after each it block in the describe + * (and before all nested describes). + * + * @param {Function} Callback to execute + */ +angular.scenario.Runner.prototype.afterEach = function(body) { +  this.currentDescribe.afterEach(body);  +}; + +/** + * Defines a function to be called before each it block in the describe + * (and before all nested describes). + * + * @param {Function} Callback to execute + */ +angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClass, specsDone) { +  var $root = angular.scope({}, angular.service); +  var self = this; +  var specs = this.rootDescribe.getSpecs(); +  $root.application = application; +  $root.ui = ui; +  $root.setTimeout = function() {  +    return self.$window.setTimeout.apply(self.$window, arguments); +  }; +  asyncForEach(specs, angular.bind(this, function(spec, specDone) { +    var runner = angular.scope($root); +    runner.$become(specRunnerClass); +    angular.foreach(angular.scenario.dsl, angular.bind(this, function(fn, key) { +      this.$window[key] = function() { +        return fn.call($root).apply(angular.scope(runner), arguments); +      } +    })); +    runner.run(ui, spec, specDone); +  }), specsDone || angular.noop); +}; diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js new file mode 100644 index 00000000..e93f6b2e --- /dev/null +++ b/src/scenario/Scenario.js @@ -0,0 +1,103 @@ +/** + * Setup file for the Scenario.  + * Must be first in the compilation/bootstrap list. + */ + +// Public namespace +angular.scenario = {}; + +// Namespace for the UI +angular.scenario.ui = {}; + +/** + * Defines a new DSL statement. If your factory function returns a Future + * it's returned, otherwise the result is assumed to be a map of functions + * for chaining. Chained functions are subject to the same rules. + * + * Note: All functions on the chain are bound to the chain scope so values + *   set on "this" in your statement function are available in the chained + *   functions. + * + * @param {String} The name of the statement + * @param {Function} Factory function(application), return a function for  + *  the statement. + */ +angular.scenario.dsl = function(name, fn) { +  angular.scenario.dsl[name] = function() { +    function executeStatement(statement, args) { +      var result = statement.apply(this, args); +      if (angular.isFunction(result) || result instanceof angular.scenario.Future) +        return result; +      var self = this; +      var chain = angular.extend({}, result); +      angular.foreach(chain, function(value, name) { +        if (angular.isFunction(value)) { +          chain[name] = angular.bind(self, function() { +            return executeStatement.call(self, value, arguments); +          }); +        } else { +          chain[name] = value; +        } +      }); +      return chain; +    } +    var statement = fn.apply(this, arguments); +    return function() { +      return executeStatement.call(this, statement, arguments); +    }; +  }; +}; + +/** + * Defines a new matcher for use with the expects() statement. The value + * this.actual (like in Jasmine) is available in your matcher to compare  + * against. Your function should return a boolean. The future is automatically + * created for you. + * + * @param {String} The name of the matcher + * @param {Function} The matching function(expected). + */ +angular.scenario.matcher = function(name, fn) { +  angular.scenario.matcher[name] = function(expected) { +    var prefix = 'expect ' + this.future.name + ' '; +    if (this.inverse) { +      prefix += 'not '; +    } +    this.addFuture(prefix + name + ' ' + angular.toJson(expected), +      angular.bind(this, function(done) { +        this.actual = this.future.value; +        if ((this.inverse && fn.call(this, expected)) || +            (!this.inverse && !fn.call(this, expected))) { +          this.error = 'expected ' + angular.toJson(expected) + +            ' but was ' + angular.toJson(this.actual); +        } +        done(this.error); +      }) +    ); +  }; +}; + +/** + * Iterates through list with iterator function that must call the + * continueFunction to continute iterating. + * + * @param {Array} list to iterate over + * @param {Function} Callback function(value, continueFunction) + * @param {Function} Callback function(error, result) called when iteration  + *   finishes or an error occurs. + */ +function asyncForEach(list, iterator, done) { +  var i = 0; +  function loop(error) { +    if (error || i >= list.length) { +      done(error); +    } else { +      try { +        iterator(list[i++], loop); +      } catch (e) { +        done(e); +      } +    } +  } +  loop(); +} diff --git a/src/scenario/SpecRunner.js b/src/scenario/SpecRunner.js new file mode 100644 index 00000000..8b6d4ef1 --- /dev/null +++ b/src/scenario/SpecRunner.js @@ -0,0 +1,78 @@ +/** + * This class is the "this" of the it/beforeEach/afterEach method. + * Responsibilities: + *   - "this" for it/beforeEach/afterEach + *   - keep state for single it/beforeEach/afterEach execution + *   - keep track of all of the futures to execute + *   - run single spec (execute each future) + */ +angular.scenario.SpecRunner = function() { +  this.futures = []; +}; + +/** + * 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 {Function} Callback function that is called when the  spec finshes. + */ +angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) { +  var specUI = ui.addSpec(spec); +   +  try { +    spec.fn.call(this); +  } catch (e) { +    specUI.error(e); +    specDone(); +    return; +  } +   +  asyncForEach( +    this.futures,  +    function(future, futureDone) { +      var stepUI = specUI.addStep(future.name); +      try { +        future.execute(function(error) { +          stepUI.finish(error); +          futureDone(error); +        }); +      } catch (e) { +        stepUI.error(e); +        rethrow(e); +      } +    },  +    function(e) { +      specUI.finish(e);  +      specDone();  +    } +  ); +}; + +/** + * Adds a new future action. + * + * @param {String} Name of the future + * @param {Function} Behavior of the future + */ +angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior) { +  var future = new angular.scenario.Future(name, angular.bind(this, behavior)); +  this.futures.push(future); +  return future; +}; + +/** + * Adds a new future action to be executed on the application window. + * + * @param {String} Name of the future + * @param {Function} Behavior of the future + */ +angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior) { +  return this.addFuture(name, function(done) { +    this.application.executeAction(function() { +      behavior.call(this, done); +    }); +  }); +}; diff --git a/src/scenario/angular.prefix b/src/scenario/angular.prefix index 5b44e17c..a1b4e151 100644 --- a/src/scenario/angular.prefix +++ b/src/scenario/angular.prefix @@ -22,9 +22,3 @@   * THE SOFTWARE.   */  (function(window, document, previousOnLoad){ -  window.angular = { -    scenario: { -      dsl: window -    } -  }; - diff --git a/src/scenario/angular.suffix b/src/scenario/angular.suffix index fc861cbf..53d99dd2 100644 --- a/src/scenario/angular.suffix +++ b/src/scenario/angular.suffix @@ -1,11 +1,31 @@ +  var $scenario = new angular.scenario.Runner(window); -  var $scenarioRunner = new angular.scenario.Runner(window, jQuery); - -  window.onload = function(){ +  window.onload = function() {      try {        if (previousOnLoad) previousOnLoad();      } catch(e) {} -    $scenarioRunner.run(jQuery(window.document.body)); +    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, function(error) { +      frame.remove(); +      if (error) { +        if (window.console) { +          console.log(error); +          if (error.stack) { +            console.log(error.stack); +          } +        } else { +          // Do something for IE +          alert(error); +        } +      } +    });    };  })(window, document, window.onload); diff --git a/src/scenario/bootstrap.js b/src/scenario/bootstrap.js index f74305c3..014c636d 100644 --- a/src/scenario/bootstrap.js +++ b/src/scenario/bootstrap.js @@ -1,4 +1,4 @@ -(function(onLoadDelegate){ +(function(previousOnLoad){    var prefix = (function(){      var filename = /(.*\/)bootstrap.js(#(.*))?/;      var scripts = document.getElementsByTagName("script"); @@ -10,6 +10,7 @@        }      }    })(); +      function addScript(path) {      document.write('<script type="text/javascript" src="' + prefix + path + '"></script>');    } @@ -18,26 +19,51 @@      document.write('<link rel="stylesheet" type="text/css" href="' + prefix + path + '"/>');    } -  window.angular = { -    scenario: { -      dsl: window -    } -  }; -    window.onload = function(){ -    setTimeout(function(){ -      $scenarioRunner.run(jQuery(window.document.body)); -    }, 0); -    (onLoadDelegate||function(){})(); +    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); +        } +      } +    });    }; +    addCSS("../../css/angular-scenario.css");    addScript("../../lib/jquery/jquery-1.4.2.js"); +  addScript("../angular-bootstrap.js"); + +  addScript("Scenario.js"); +  addScript("Application.js"); +  addScript("Describe.js"); +  addScript("Future.js"); +  addScript("HtmlUI.js");    addScript("Runner.js"); -  addScript("../Angular.js"); -  addScript("../JSON.js"); -  addScript("DSL.js"); -  document.write('<script type="text/javascript">' + -    '$scenarioRunner = new angular.scenario.Runner(window, jQuery);' + -    '</script>'); -})(window.onload); +  addScript("SpecRunner.js"); +  addScript("dsl.js"); +  addScript("matchers.js"); +  // Create the runner (which also sets up the global API) +  document.write( +    '<script type="text/javascript">' + +    'var _jQuery = jQuery.noConflict(true);' + +    'var $scenario = new angular.scenario.Runner(window);' + +    '</script>' +  ); + +})(window.onload); diff --git a/src/scenario/matchers.js b/src/scenario/matchers.js new file mode 100644 index 00000000..0dfbc455 --- /dev/null +++ b/src/scenario/matchers.js @@ -0,0 +1,39 @@ +/** + * Matchers for implementing specs. Follows the Jasmine spec conventions. + */ + +angular.scenario.matcher('toEqual', function(expected) { +  return angular.equals(this.actual, expected); +}); + +angular.scenario.matcher('toBeDefined', function() { +  return angular.isDefined(this.actual); +}); + +angular.scenario.matcher('toBeTruthy', function() { +  return this.actual; +}); + +angular.scenario.matcher('toBeFalsy', function() { +  return !this.actual; +}); + +angular.scenario.matcher('toMatch', function(expected) { +  return new RegExp(expected).test(this.actual); +}); + +angular.scenario.matcher('toBeNull', function() { +  return this.actual === null; +}); + +angular.scenario.matcher('toContain', function(expected) { +  return includes(this.actual, expected); +}); + +angular.scenario.matcher('toBeLessThan', function(expected) { +  return this.actual < expected; +}); + +angular.scenario.matcher('toBeGreaterThan', function(expected) { +  return this.actual > expected; +}); diff --git a/test/AngularSpec.js b/test/AngularSpec.js index e0228e16..6faed707 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -86,6 +86,10 @@ describe('equals', function(){      expect(equals({name:'misko'}, {name:'misko', $id:2})).toEqual(true);      expect(equals({name:'misko', $id:1}, {name:'misko'})).toEqual(true);    }); + +  it('should ignore functions', function(){ +    expect(equals({func: function() {}}, {bar: function() {}})).toEqual(true); +  });  });  describe('parseKeyValue', function() { diff --git a/test/scenario/ApplicationSpec.js b/test/scenario/ApplicationSpec.js new file mode 100644 index 00000000..706fbc36 --- /dev/null +++ b/test/scenario/ApplicationSpec.js @@ -0,0 +1,75 @@ +describe('angular.scenario.Application', function() { +  var app, frames; + +  beforeEach(function() { +    frames = _jQuery("<div></div>"); +    app = new angular.scenario.Application(frames); +  }); + +  it('should return new $window and $document after navigate', function() { +    var testWindow, testDocument, counter = 0; +    app.navigateTo = noop; +    app.getWindow = function() {  +      return {x:counter++, document:{x:counter++}};  +    }; +    app.navigateTo('http://www.google.com/'); +    app.executeAction(function($document, $window) { +      testWindow = $window; +      testDocument = $document; +    }); +    app.navigateTo('http://www.google.com/'); +    app.executeAction(function($document, $window) { +      expect($window).not.toEqual(testWindow); +      expect($document).not.toEqual(testDocument); +    }); +  }); + +  it('should execute callback on $window of frame', function() { +    var testWindow = {document: {}}; +    app.getWindow = function() {  +      return testWindow;  +    }; +    app.executeAction(function($document, $window) { +      expect(this).toEqual($window); +      expect(this).toEqual(testWindow); +    }); +  }); +   +  it('should create a new iframe each time', function() { +    app.navigateTo('about:blank'); +    var frame = app.getFrame(); +    frame.attr('test', true); +    app.navigateTo('about:blank'); +    expect(app.getFrame().attr('test')).toBeFalsy(); +  }); +   +  it('should URL description bar', function() { +    app.navigateTo('about:blank'); +    var anchor = frames.find('> h2 a'); +    expect(anchor.attr('href')).toEqual('about:blank'); +    expect(anchor.text()).toEqual('about:blank'); +  }); +   +  it('should call onload handler when frame loads', function() { +    var called; +    app.getFrame = function() {  +      // Mock a little jQuery +      var result = { +        remove: function() {  +          return result;  +        }, +        attr: function(key, value) {  +          return (!value) ? 'attribute value' : result; +        }, +        load: function() {  +          called = true;  +        } +      }; +      return result; +    }; +    app.navigateTo('about:blank', function() { +      called = true; +    }); +    expect(called).toBeTruthy(); +  }); +}); diff --git a/test/scenario/DSLSpec.js b/test/scenario/DSLSpec.js index 7a8e2e3b..9b011847 100644 --- a/test/scenario/DSLSpec.js +++ b/test/scenario/DSLSpec.js @@ -1,181 +1,232 @@ -describe("DSL", function() { +/** + * Very basic Mock of angular. + */ +function AngularMock() { +  this.reset(); +  this.service = this; +} -  var lastDocument, executeFuture, Expect; +AngularMock.prototype.reset = function() { +  this.log = []; +}; -  beforeEach(function() { -    setUpContext(); -    executeFuture = function(future, html, callback) { -      lastDocument = _jQuery('<div>' + html + '</div>'); -      lastFrame = _jQuery('<iframe>' + lastDocument + '</iframe>'); -      _jQuery(document.body).append(lastDocument); -      var specThis = { -        testWindow: window, -        testDocument: lastDocument, -        testFrame: lastFrame, -        jQuery: _jQuery -      }; -      future.behavior.call(specThis, callback || noop); -    }; -    Expect = _window.expect; -  }); - -  describe("input", function() { +AngularMock.prototype.element = function(node) { +  this.log.push('element(' + node.nodeName.toLowerCase() + ')'); +  return this; +}; -    var input = angular.scenario.dsl.input; +AngularMock.prototype.trigger = function(value) { +  this.log.push('element().trigger(' + value + ')'); +}; -    it('should enter', function() { -      var future = input('name').enter('John'); -      expect(future.name).toEqual("input 'name' enter 'John'"); -      executeFuture(future, '<input type="text" name="name" />'); -      expect(lastDocument.find('input').val()).toEqual('John'); -    }); +AngularMock.prototype.$browser = function() { +  this.log.push('$brower()'); +  return this; +}; -    it('should select', function() { -      var future = input('gender').select('female'); -      expect(future.name).toEqual("input 'gender' select 'female'"); -      executeFuture(future, -        '<input type="radio" name="0@gender" value="male" checked/>' + -        '<input type="radio" name="0@gender" value="female"/>'); -      expect(lastDocument.find(':radio:checked').length).toEqual(1); -      expect(lastDocument.find(':radio:checked').val()).toEqual('female'); -    }); -  }); +AngularMock.prototype.poll = function() { +  this.log.push('$brower.poll()'); +  return this; +}; -  describe('browser', function() { -    var browser = angular.scenario.dsl.browser; -    it('shoud return true if location with empty hash provided is same ' + -        'as location of the page', function() { -      browser.location.href = "http://server"; -      expect(browser.location.toEqual("http://server")).toEqual(true); -    }); -    it('shoud return true if location with hash provided is same ' + -        'as location of the page', function() { -      browser.location.href = "http://server"; -      browser.location.hash = "hashPath"; -      expect(browser.location.toEqual("http://server/#/hashPath")).toEqual(true); -    }); -    it('should return true if the location provided is the same as which ' + -        'browser navigated to', function() { -      var future = browser.navigateTo("http://server/#/hashPath"); -      expect(future.name).toEqual("Navigate to: http://server/#/hashPath"); -      executeFuture(future, '<input type="text" name="name" />'); -      expect(browser.location.toEqual("http://server/#/hashPath")).toEqual(true); -      expect(browser.location.toEqual("http://server/")).toEqual(false); +AngularMock.prototype.notifyWhenNoOutstandingRequests = function(fn) { +  this.log.push('$brower.notifyWhenNoOutstandingRequests()'); +  fn(); +}; -      future = browser.navigateTo("http://server/"); -      expect(future.name).toEqual("Navigate to: http://server/"); -      executeFuture(future, '<input type="text" name="name" />'); -      expect(browser.location.toEqual("http://server/")).toEqual(true); -    }); +describe("angular.scenario.dsl", function() { +  var $window; +  var $root; +  var application; +   +  beforeEach(function() { +    $window = { +      document: _jQuery("<div></div>"), +      angular: new AngularMock() +    }; +    $root = angular.scope({}, angular.service); +    $root.futures = []; +    $root.addFuture = function(name, fn) { +      this.futures.push(name); +      fn.call(this, function(error, result) { +        $root.futureError = error; +        $root.futureResult = result; +      }); +    }; +    $root.application = new angular.scenario.Application($window.document); +    $root.application.getWindow = function() { +      return $window;  +    }; +    $root.application.navigateTo = function(url, callback) {  +      $window.location = url; +      callback(); +    }; +    // Just use the real one since it delegates to this.addFuture +    $root.addFutureAction = angular.scenario. +      SpecRunner.prototype.addFutureAction;    }); - -  describe('repeater', function() { - -    var repeater = angular.scenario.dsl.repeater; -    var html; +   +  describe('Pause', function() {      beforeEach(function() { -      html = "<table>" + -          "<tr class='epic'>" + -            "<td class='hero-name'>" + -              "<span ng:bind='hero'>John Marston</span>" + -            "</td>" + -            "<td class='game-name'>" + -              "<span ng:bind='game'>Red Dead Redemption</span>" + -            "</td>" + -          "</tr>" + -          "<tr class='epic'>" + -            "<td class='hero-name'>" + -              "<span ng:bind='hero'>Nathan Drake</span>" + -            "</td>" + -            "<td class='game-name'>" + -              "<span ng:bind='game'>Uncharted</span>" + -            "</td>" + -          "</tr>" + -        "</table>"; +      $root.setTimeout = function(fn, value) { +        $root.timerValue = value; +        fn(); +      };      }); -    it('should count', function() { -      var future = repeater('.repeater-row').count(); -      expect(future.name).toEqual("repeater '.repeater-row' count"); -      executeFuture(future, -        "<div class='repeater-row'>a</div>" + -        "<div class='repeater-row'>b</div>", -        function(value) { -          future.fulfill(value); -      }); -      expect(future.fulfilled).toBeTruthy(); -      expect(future.value).toEqual(2); +     +    it('should pause for specified seconds', function() { +      angular.scenario.dsl.pause.call($root).call($root, 10); +      expect($root.timerValue).toEqual(10000); +      expect($root.futureResult).toEqual(10000); +    });     +  }); +   +  describe('Expect', function() { +    it('should chain and execute matcher', function() { +      var future = {value: 10}; +      var result = angular.scenario.dsl.expect.call($root).call($root, future); +      result.toEqual(10); +      expect($root.futureError).toBeUndefined(); +      expect($root.futureResult).toBeUndefined(); +      var result = angular.scenario.dsl.expect.call($root).call($root, future); +      result.toEqual(20); +      expect($root.futureError).toBeDefined();      }); - -    function assertFutureState(future, expectedName, expectedValue) { -      expect(future.name).toEqual(expectedName); -      executeFuture(future, html, function(value) { -        future.fulfill(value); -      }); -      expect(future.fulfilled).toBeTruthy(); -      expect(future.value).toEqual(expectedValue); -    } -    it('should collect bindings', function() { -      assertFutureState(repeater('.epic').collect('{{hero}}'), -        "repeater '.epic' collect '{{hero}}'", -        ['John Marston', 'Nathan Drake']); -      assertFutureState(repeater('.epic').collect('{{game}}'), -        "repeater '.epic' collect '{{game}}'", -        ['Red Dead Redemption', 'Uncharted']); +  }); +   +  describe('NavigateTo', function() {     +    it('should allow a string url', function() { +      angular.scenario.dsl.navigateTo.call($root).call($root, 'http://myurl'); +      expect($window.location).toEqual('http://myurl'); +      expect($root.futureResult).toEqual('http://myurl'); +    }); +     +    it('should allow a future url', function() { +      var future = {name: 'future name', value: 'http://myurl'}; +      angular.scenario.dsl.navigateTo.call($root).call($root, future); +      expect($window.location).toEqual('http://myurl'); +      expect($root.futureResult).toEqual('http://myurl');      }); -    it('should collect normal selectors', function() { -      assertFutureState(repeater('.epic').collect('.hero-name'), -        "repeater '.epic' collect '.hero-name'", -        ['John Marston', 'Nathan Drake']); -      assertFutureState(repeater('.epic').collect('.game-name'), -        "repeater '.epic' collect '.game-name'", -        ['Red Dead Redemption', 'Uncharted']); +     +    it('should complete if angular is missing from app frame', function() { +      delete $window.angular; +      angular.scenario.dsl.navigateTo.call($root).call($root, 'http://myurl'); +      expect($window.location).toEqual('http://myurl'); +      expect($root.futureResult).toEqual('http://myurl');      }); -    it('should collect normal attributes', function() { -      //TODO(shyamseshadri) : Left as an exercise to the user +     +    it('should wait for angular notify when no requests pending', function() { +      angular.scenario.dsl.navigateTo.call($root).call($root, 'url'); +      expect($window.angular.log).toContain('$brower.poll()'); +      expect($window.angular.log) +        .toContain('$brower.notifyWhenNoOutstandingRequests()');      });    }); - -  describe('element', function() { -    var element = angular.scenario.dsl.element; -    var html; +   +  describe('Element Finding', function() { +    var doc; +    //TODO(esprehn): Work around a bug in jQuery where attribute selectors +    //  only work if they are executed on a real document, not an element. +    // +    //  ex. jQuery('#foo').find('[name="bar"]') // fails +    //  ex. jQuery('#foo [name="bar"]') // works, wtf? +    //      beforeEach(function() { -      html = '<div class="container">' + -          '<div class="reports-detail">' + -            '<span class="desc">Description : ' + -              '<span ng:bind="report.description">Details...</span>' + -            '</span>' + -            '<span>Date created: ' + -              '<span ng:bind="report.creationDate">01/01/01</span>' + -            '</span>' + -          '</div>' + -        '</div>'; +      doc = _jQuery('<div id="angular-scenario-binding"></div>'); +      _jQuery(document.body).append(doc); +     $window.document = window.document;      }); -    function timeTravel(future) { -      executeFuture(future, html, function(value) { future.fulfill(value); }); -      expect(future.fulfilled).toBeTruthy(); -    } -    it('should find elements on the page and provide jquery api', function() { -      var future = element('.reports-detail').text(); -      expect(future.name).toEqual("Element '.reports-detail'.text()"); -      timeTravel(future); -      expect(future.value). -        toEqual('Description : Details...Date created: 01/01/01'); -//      expect(future.value.find('.desc').text()). -//        toEqual('Description : Details...'); +     +    afterEach(function() { +      _jQuery(document.body) +        .find('#angular-scenario-binding') +        .remove();      }); -    it('should find elements with angular syntax', function() { -      var future = element('{{report.description}}').text(); -      expect(future.name).toEqual("Element '{{report.description}}'.text()"); -      timeTravel(future); -      expect(future.value).toEqual('Details...'); -//      expect(future.value.attr('ng:bind')).toEqual('report.description'); + +    describe('Binding', function() { +      it('should select binding by name', function() { +        doc.append('<span ng:bind="foo.bar">some value</span>'); +        angular.scenario.dsl.binding.call($root).call($root, 'foo.bar'); +        expect($root.futureResult).toEqual('some value'); +      }); +     +      it('should return error if no binding exists', function() { +        angular.scenario.dsl.binding.call($root).call($root, 'foo.bar'); +        expect($root.futureError).toMatch(/does not exist/); +      });      }); -    it('should be able to click elements', function(){ -      var future = element('.link-class').click(); -      expect(future.name).toEqual("Element '.link-class'.click()"); -      executeFuture(future, html, function(value) { future.fulfill(value); }); -      expect(future.fulfilled).toBeTruthy(); -      // TODO(rajat): look for some side effect from click happening? +   +    describe('Input', function() { +      it('should change value in text input', function() { +        doc.append('<input name="test.input" value="something">'); +        var chain = angular.scenario.dsl.input +          .call($root).call($root, 'test.input'); +        chain.enter('foo'); +        expect($window.angular.log).toContain('element(input)'); +        expect($window.angular.log).toContain('element().trigger(change)'); +        expect(_jQuery('input[name="test.input"]').val()).toEqual('foo'); +      }); +       +      it('should return error if no input exists', function() { +        var chain = angular.scenario.dsl.input +          .call($root).call($root, 'test.input'); +        chain.enter('foo'); +        expect($root.futureError).toMatch(/does not exist/); +      }); +       +      it('should toggle checkbox state', function() { +        doc.append('<input type="checkbox" name="test.input" checked>'); +        expect(_jQuery('input[name="test.input"]') +          .attr('checked')).toBeTruthy(); +        var chain = angular.scenario.dsl.input +          .call($root).call($root, 'test.input'); +        chain.check(); +        expect($window.angular.log).toContain('element(input)'); +        expect($window.angular.log).toContain('element().trigger(click)'); +        expect(_jQuery('input[name="test.input"]') +          .attr('checked')).toBeFalsy(); +        $window.angular.reset(); +        chain.check(); +        expect($window.angular.log).toContain('element(input)'); +        expect($window.angular.log).toContain('element().trigger(click)'); +        expect(_jQuery('input[name="test.input"]') +          .attr('checked')).toBeTruthy(); +      }); +       +      it('should return error if checkbox does not exist', function() { +        var chain = angular.scenario.dsl.input +          .call($root).call($root, 'test.input'); +        chain.check(); +        expect($root.futureError).toMatch(/does not exist/); +      }); + +      it('should select option from radio group', function() { +        doc.append( +          '<input type="radio" name="0@test.input" value="foo">' + +          '<input type="radio" name="0@test.input" value="bar" checked>' +        ); +        expect(_jQuery('input[name="0@test.input"][value="bar"]') +          .attr('checked')).toBeTruthy(); +        expect(_jQuery('input[name="0@test.input"][value="foo"]') +          .attr('checked')).toBeFalsy(); +        var chain = angular.scenario.dsl.input +          .call($root).call($root, 'test.input'); +        chain.select('foo'); +        expect($window.angular.log).toContain('element(input)'); +        expect($window.angular.log).toContain('element().trigger(click)'); +        expect(_jQuery('input[name="0@test.input"][value="bar"]') +          .attr('checked')).toBeFalsy(); +        expect(_jQuery('input[name="0@test.input"][value="foo"]') +          .attr('checked')).toBeTruthy(); +      }); +       +      it('should return error if radio button does not exist', function() { +        var chain = angular.scenario.dsl.input +          .call($root).call($root, 'test.input'); +        chain.select('foo'); +        expect($root.futureError).toMatch(/does not exist/); +      });      });    }); +    }); diff --git a/test/scenario/DescribeSpec.js b/test/scenario/DescribeSpec.js new file mode 100644 index 00000000..05129cfe --- /dev/null +++ b/test/scenario/DescribeSpec.js @@ -0,0 +1,85 @@ +describe('angular.scenario.Describe', function() { +  var log; +  var root; + +  beforeEach(function() { +    root = new angular.scenario.Describe(); +     +    /** +     * Simple callback logging system. Use to assert proper order of calls. +     */ +    log = function(text) {  +      log.text = log.text + text;  +    }; +    log.fn = function(text) { +      return function(done){  +        log(text);  +        (done || angular.noop)();  +      }; +    }; +    log.reset = function() {  +      log.text = '';  +    }; +    log.reset(); +  }); + +  it('should handle basic nested case', function() { +    root.describe('A', function(){ +      this.beforeEach(log.fn('{')); +      this.afterEach(log.fn('}')); +      this.it('1', log.fn('1')); +      this.describe('B', function(){ +        this.beforeEach(log.fn('(')); +        this.afterEach(log.fn(')')); +        this.it('2', log.fn('2')); +      }); +    }); +    var specs = root.getSpecs(); +    expect(specs.length).toEqual(2); + +    expect(specs[0].name).toEqual('2'); +    specs[0].fn(); +    expect(log.text).toEqual('{(2)}'); + +    log.reset(); +    expect(specs[1].name).toEqual('1'); +    specs[1].fn(); +    expect(log.text).toEqual('{1}'); +  }); +     +  it('should link nested describe blocks with parent and children', function() { +    root.describe('A', function() { +      this.it('1', angular.noop); +      this.describe('B', function() { +        this.it('2', angular.noop); +        this.describe('C', function() { +          this.it('3', angular.noop); +        }); +      }); +    }); +    var specs = root.getSpecs(); +    expect(specs[2].definition.parent).toEqual(root); +    expect(specs[0].definition.parent).toEqual(specs[2].definition.children[0]); +  }); +   +  it('should not process xit and xdescribe', function() { +    root.describe('A', function() { +      this.xit('1', angular.noop); +      this.xdescribe('B', function() { +        this.it('2', angular.noop); +        this.describe('C', function() { +          this.it('3', angular.noop); +        }); +      }); +    }); +    var specs = root.getSpecs(); +    expect(specs.length).toEqual(0); +  }); +   +  it('should create uniqueIds in the tree', function() { +    angular.scenario.Describe.id = 0; +    var a = new angular.scenario.Describe(); +    var b = new angular.scenario.Describe(); +    expect(a.id).toNotEqual(b.id); +  }); +}); diff --git a/test/scenario/FutureSpec.js b/test/scenario/FutureSpec.js new file mode 100644 index 00000000..ae475779 --- /dev/null +++ b/test/scenario/FutureSpec.js @@ -0,0 +1,38 @@ +describe('angular.scenario.Future', function() { +  var future; +   +  it('should set the name and behavior', function() { +    var behavior = function() {}; +    var future = new angular.scenario.Future('test name', behavior); +    expect(future.name).toEqual('test name'); +    expect(future.behavior).toEqual(behavior); +    expect(future.value).toBeUndefined(); +    expect(future.fulfilled).toBeFalsy(); +  }); +   +  it('should be fulfilled after execution and done callback', function() { +    var future = new angular.scenario.Future('test name', function(done) { +      done(); +    }); +    future.execute(angular.noop); +    expect(future.fulfilled).toBeTruthy(); +  }); +   +  it('should take callback with (error, result) and forward', function() { +    var future = new angular.scenario.Future('test name', function(done) { +      done(10, 20); +    }); +    future.execute(function(error, result) { +      expect(error).toEqual(10); +      expect(result).toEqual(20); +    }); +  }); +   +  it('should use error as value if provided', function() { +    var future = new angular.scenario.Future('test name', function(done) { +      done(10, 20); +    }); +    future.execute(angular.noop); +    expect(future.value).toEqual(10); +  }); +}); diff --git a/test/scenario/HtmlUISpec.js b/test/scenario/HtmlUISpec.js new file mode 100644 index 00000000..b2e2652f --- /dev/null +++ b/test/scenario/HtmlUISpec.js @@ -0,0 +1,87 @@ +describe('angular.scenario.HtmlUI', function() { +  var ui; +  var context; +  var spec; +   +  beforeEach(function() { +    spec = { +      name: 'test spec', +      definition: { +        id: 10, +        name: 'child', +        children: [], +        parent: { +          id: 20, +          name: 'parent', +          children: [] +        } +      } +    }; +    context = _jQuery("<div></div>"); +    ui = new angular.scenario.ui.Html(context); +  }); +   +  it('should create nested describe context', function() { +    ui.addSpec(spec); +    expect(context.find('#describe-20 #describe-10 > h2').text()) +      .toEqual('describe: child'); +    expect(context.find('#describe-20 > h2').text()).toEqual('describe: parent'); +    expect(context.find('#describe-10 .tests > li .test-info .test-name').text()) +      .toEqual('it test spec'); +    expect(context.find('#describe-10 .tests > li').hasClass('status-pending')) +      .toBeTruthy(); +  }); +   +  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'); +    // Failure +    specUI = ui.addSpec(spec); +    specUI.addStep('some step').finish('failure'); +    specUI.finish('failure'); +    // Failure +    specUI = ui.addSpec(spec); +    specUI.addStep('some step').finish('failure'); +    specUI.finish('failure'); +    // Success +    specUI = ui.addSpec(spec); +    specUI.addStep('some step').finish(); +    specUI.finish(); +     +    expect(parseInt(context.find('#status-legend .status-failure').text())) +      .toEqual(3); +    expect(parseInt(context.find('#status-legend .status-error').text())) +      .toEqual(2); +    expect(parseInt(context.find('#status-legend .status-success').text())) +      .toEqual(1); +  }); +   +  it('should update timer when test completes', function() { +    // Success +    specUI = ui.addSpec(spec); +    specUI.addStep('some step').finish(); +    specUI.finish(); +     +    // Failure +    specUI = ui.addSpec(spec); +    specUI.addStep('some step').finish('failure'); +    specUI.finish('failure'); +     +    // Error +    specUI = ui.addSpec(spec).error('error'); +     +    context.find('#describe-10 .tests > li .test-info .timer-result') +      .each(function(index, timer) { +        expect(timer.innerHTML).toMatch(/ms$/); +    }); +  }); + +}); diff --git a/test/scenario/MatcherSpec.js b/test/scenario/MatcherSpec.js deleted file mode 100644 index 2eddd2bc..00000000 --- a/test/scenario/MatcherSpec.js +++ /dev/null @@ -1,38 +0,0 @@ -describe('Matcher', function () { -  function executeFutures() { -    for(var i in $scenario.currentSpec.futures) { -      var future = $scenario.currentSpec.futures[i]; -      future.behavior.call({}, function(value) { future.fulfill(value); }); -    } -  } -  var matcher; -  beforeEach(function() { -    setUpContext(); -    var future = $scenario.addFuture('Calculate first future', function(done) { -      done(123); -    }); -    matcher = new Matcher(this, future); - -  }); -  it('should correctly match toEqual', function() { -    matcher.toEqual(123); -    executeFutures(); -  }); -  it('should throw an error when incorrect match toEqual', function() { -    matcher.toEqual(456); -    try { -      executeFutures(); -      fail(); -    } catch (e) { -      expect(e).toEqual('Expected 456 but was 123'); -    } -  }); -  it('should correctly match arrays', function() { -    var future = $scenario.addFuture('Calculate first future', function(done) { -      done(['a', 'b']); -    }); -    matcher = new Matcher(this, future); -    matcher.toEqual(['a', 'b']); -    executeFutures(); -  }); -});
\ No newline at end of file diff --git a/test/scenario/RunnerSpec.js b/test/scenario/RunnerSpec.js index 2986add6..43d97257 100644 --- a/test/scenario/RunnerSpec.js +++ b/test/scenario/RunnerSpec.js @@ -1,238 +1,96 @@ -describe('Runner', function() { - -  var Describe, It, BeforeEach, AfterEach, body; - +/** + * Mock spec runner. + */ +function MockSpecRunner() {} +MockSpecRunner.prototype.run = function(ui, spec, specDone) { +  spec.fn.call(this); +  specDone(); +}; + +describe('angular.scenario.Runner', function() { +  var $window; +  var runner; +      beforeEach(function() { -    setUpContext(); -    Describe = _window.describe; -    It = _window.it; -    BeforeEach = _window.beforeEach; -    AfterEach = _window.afterEach; -    body = _jQuery('<div></div>'); -  }); - -  describe('describe', function() { -    it('should consume the describe functions', function() { -      Describe('describe name',  logger('body')); -      expect(log).toEqual('body'); +    // Trick to get the scope out of a DSL statement +    angular.scenario.dsl('dslScope', function() { +      var scope = this; +      return function() { return scope; };      }); - -    describe('it', function() { -      it('should consume it', function() { -        Describe('describe name', function() { -          It('should text', logger('body')); -        }); -        expect(log).toEqual('body'); -        var spec = $scenario.specs['describe name: it should text']; -        expect(spec.futures).toEqual([]); -        expect(spec.name).toEqual('describe name: it should text'); -      }); - -      it('should complain on duplicate it', function() { -        // WRITE ME!!!! -      }); - -      it('should create a failing future if there is a javascript error', function() { -        var spec; -        Describe('D1', function() { -          It('I1', function() { -            spec = $scenario.currentSpec; -            throw {message: 'blah'}; -          }); -        }); -        var future = spec.futures[0]; -        expect(future.name).toEqual('blah'); -        try { -          future.behavior(); -          fail(); -        } catch (e) { -          expect(e.message).toEqual('blah'); -        } -      }); -    }); - -    describe('beforeEach', function() { -      it('should execute beforeEach before every it', function() { -        Describe('describe name', function() { -          BeforeEach(logger('before;')); -          It('should text', logger('body;')); -          It('should text2', logger('body2;')); -        }); -        expect(log).toEqual('before;body;before;body2;'); -      }); +    // Trick to get the scope out of a DSL statement +    angular.scenario.dsl('dslChain', function() { +      return function() { +        this.chained = 0; +        this.chain = function() { this.chained++; return this; }; +        return this; +      };      }); -    describe('afterEach', function() { -      it('should execute afterEach after every it', function() { -        Describe('describe name', function() { -          AfterEach(logger('after;')); -          It('should text1', logger('body1;')); -          It('should text2', logger('body2;')); -        }); -        expect(log).toEqual('body1;after;body2;after;'); -      }); - -      it('should always execute afterEach after every it', function() { -        Describe('describe name', function() { -          AfterEach(logger('after;')); -          It('should text', function() { -            logger('body1;')(); -            throw "MyError"; -          }); -          It('should text2', logger('body2;')); -        }); -        expect(log).toEqual('body1;after;body2;after;'); -      }); - -      it('should report an error if afterEach fails', function() { -        var next; -        Describe('describe name', function() { -          AfterEach(function() { -            $scenario.addFuture('afterEachLog', logger('after;')); -            $scenario.addFuture('afterEachThrow', function() { -              throw "AfterError"; -            }); -          }); -          It('should text1', function() { -            $scenario.addFuture('future1', logger('future1;')); -          }); -          It('should text2', function() { -            $scenario.addFuture('future2', logger('future2;')); -          }); -        }); -        $scenario.run(body); -        expect(log).toEqual('future1;after;future2;after;'); -        expect(_window.$testrun.results).toEqual([ -          { name : 'describe name: it should text1', -            passed : false, -            error : 'AfterError', -            steps : [ 'future1', 'afterEachLog', 'afterEachThrow' ] }, -          { name : 'describe name: it should text2', -            passed : false, -            error : 'AfterError', -            steps : [ 'future2', 'afterEachLog', 'afterEachThrow' ] }]); -      }); +    $window = {}; +    runner = new angular.scenario.Runner($window); +  }); +   +  afterEach(function() { +    delete angular.scenario.dsl.dslScope; +    delete angular.scenario.dsl.dslChain; +  }); +   +  it('should publish the functions in the public API', function() { +    angular.foreach(runner.api, function(fn, name) { +      var func; +      if (name in $window) { +        func = $window[name]; +      } +      expect(angular.isFunction(func)).toBeTruthy();      });    }); - -  describe('future building', function() { -    it('should queue futures', function() { -      function behavior(){} -      Describe('name', function() { -        It('should', function() { -          $scenario.addFuture('futureName', behavior); +   +  it('should construct valid describe trees with public API', function() { +    var before = []; +    var after = []; +    $window.describe('A', function() { +      $window.beforeEach(function() { before.push('A'); }); +      $window.afterEach(function() { after.push('A'); }); +      $window.it('1', angular.noop); +      $window.describe('B', function() { +        $window.beforeEach(function() { before.push('B'); }); +          $window.afterEach(function() { after.push('B'); }); +        $window.it('2', angular.noop); +        $window.describe('C', function() { +          $window.beforeEach(function() { before.push('C'); }); +          $window.afterEach(function() { after.push('C'); }); +          $window.it('3', angular.noop);          });        }); -      expect($scenario.specs['name: it should'].futures[0].name). -        toEqual('futureName');      }); +    var specs = runner.rootDescribe.getSpecs(); +    specs[0].fn(); +    expect(before).toEqual(['A', 'B', 'C']); +    expect(after).toEqual(['C', 'B', 'A']); +    expect(specs[2].definition.parent).toEqual(runner.rootDescribe); +    expect(specs[0].definition.parent).toEqual(specs[2].definition.children[0]);    }); - -  describe('execution', function() { -    it('should execute the queued futures', function() { -      var next, firstThis, secondThis, doneThis, spec; -      $scenario.specs['spec'] = { -        futures: [ -            new Future('future1', function(done) { -              next = done; -              log += 'first;'; -              firstThis = this; -            }), -            new Future('future2', function(done) { -              next = done; -              log += 'second;'; -              secondThis = this; -            }) -        ] -      }; - -      spec = $scenario.execute('spec', function(done){ -        log += 'done;'; -        doneThis = this; +   +  it('should publish the DSL statements to the $window', function() { +    $window.describe('describe', function() { +      $window.it('1', function() { +        expect($window.dslScope).toBeDefined();        }); -      expect(log).toEqual('first;'); -      next(); -      expect(log).toEqual('first;second;'); -      next(); -      expect(log).toEqual('first;second;done;'); -      expect(spec === window).toEqual(false); -      expect(spec).toEqual(firstThis); -      expect(spec).toEqual(secondThis); -      expect(spec).toEqual(doneThis); - -      expect(spec.result.failed).toEqual(false); -      expect(spec.result.finished).toEqual(true); -      expect(spec.result.error).toBeUndefined(); -      expect(spec.result.passed).toEqual(true); -    }); - -    it('should handle exceptions in a future', function() { -      $scenario.specs['spec'] = { -          futures: [ -            new Future('first future', function(done) { -              done(); -            }), -            new Future('error', function(done) { -              throw "MyError"; -            }), -            new Future('should not execute', function(done) { -              done(); -            }) -          ] -        }; - -        var spec = $scenario.execute('spec'); - -        expect(spec.result.passed).toEqual(false); -        expect(spec.result.failed).toEqual(true); -        expect(spec.result.finished).toEqual(true); -        expect(spec.result.error).toEqual("MyError"); -        expect(_window.$testrun.results).toEqual([{ -          name: 'spec', -          passed: false, -          error: 'MyError', -          steps: ['first future', 'error']}]);      }); +    runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow);    }); - -  describe('run', function() { -    var next; -    beforeEach(function() { -      Describe('d1', function() { -        It('it1', function() { $scenario.addFuture('s1', logger('s1,')); }); -        It('it2', function() { -          $scenario.addFuture('s2', logger('s2,')); -          $scenario.addFuture('s2.2', function(done){ next = done; }); -        }); +   +  it('should create a new scope for each DSL chain', function() { +    $window.describe('describe', function() { +      $window.it('1', function() { +        var scope = $window.dslScope(); +        scope.test = "foo"; +        expect($window.dslScope().test).toBeUndefined();        }); -      Describe('d2', function() { -        It('it3', function() { $scenario.addFuture('s3', logger('s3,')); }); -        It('it4', function() { $scenario.addFuture('s4', logger('s4,')); }); +      $window.it('2', function() { +        var scope = $window.dslChain().chain().chain(); +        expect(scope.chained).toEqual(2);        });      }); -    it('should execute all specs', function() { -      $scenario.run(body); - -      expect(log).toEqual('s1,s2,'); -      next(); -      expect(log).toEqual('s1,s2,s3,s4,'); -    }); -    it('should publish done state and results as tests are run', function() { -      expect(_window.$testrun.done).toBeFalsy(); -      expect(_window.$testrun.results).toEqual([]); -      $scenario.run(body); -      expect(_window.$testrun.done).toBeFalsy(); -      expect(_window.$testrun.results).toEqual([ -        {name: 'd1: it it1', passed: true, error: undefined, steps: ['s1']} -      ]); -      next(); -      expect(_window.$testrun.done).toBeTruthy(); -      expect(_window.$testrun.results).toEqual([ -        {name: 'd1: it it1', passed: true, error: undefined, steps: ['s1']}, -        {name: 'd1: it it2', passed: true, error: undefined, steps: ['s2', 's2.2']}, -        {name: 'd2: it it3', passed: true, error: undefined, steps: ['s3']}, -        {name: 'd2: it it4', passed: true, error: undefined, steps: ['s4']} -      ]); -    }); +    runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow);    }); - -});
\ No newline at end of file +}); diff --git a/test/scenario/SpecRunnerSpec.js b/test/scenario/SpecRunnerSpec.js new file mode 100644 index 00000000..81b66956 --- /dev/null +++ b/test/scenario/SpecRunnerSpec.js @@ -0,0 +1,165 @@ +/** + * Mock of all required UI classes/methods. (UI, Spec, Step). + */ +function UIMock() { +  this.log = []; +} +UIMock.prototype = { +  addSpec: function(spec) { +    var log = this.log; +    log.push('addSpec:' + spec.name); +    return { +      addStep: function(name) { +        log.push('addStep:' + name); +        return { +          finish: function(e) { +            log.push('step finish:' + (e ? e : '')); +            return this; +          }, +          error: function(e) { +            log.push('step error:' + (e ? e : '')); +            return this; +          } +        }; +      }, +      finish: function(e) { +        log.push('spec finish:' + (e ? e : '')); +        return this; +      }, +      error: function(e) { +        log.push('spec error:' + (e ? e : '')); +        return this; +      } +    };  +  }, +}; + +/** + * Mock Application + */ +function ApplicationMock($window) { +  this.$window = $window; +} +ApplicationMock.prototype = { +  executeAction: function(callback) { +    callback.call(this.$window); +  } +}; + +describe('angular.scenario.SpecRunner', function() { +  var $window; +  var runner; + +  beforeEach(function() { +    $window = {}; +    runner = angular.scope(); +    runner.application = new ApplicationMock($window); +    runner.$become(angular.scenario.SpecRunner); +  }); +   +  it('should bind futures to the spec', function() { +    runner.addFuture('test future', function(done) { +      this.application.value = 10; +      done(); +    }); +    runner.futures[0].execute(angular.noop); +    expect(runner.application.value).toEqual(10); +  }); +   +  it('should pass done to future action behavior', function() { +    runner.addFutureAction('test future', function(done) { +      expect(angular.isFunction(done)).toBeTruthy(); +      done(10, 20); +    }); +    runner.futures[0].execute(function(error, result) { +      expect(error).toEqual(10); +      expect(result).toEqual(20); +    }); +  }); +   +  it('should pass execute future action on the $window', function() { +    runner.addFutureAction('test future', function(done) { +      this.test = 'test value'; +      done(); +    }); +    runner.futures[0].execute(angular.noop); +    expect($window.test).toEqual('test value'); +  }); + +  it('should execute spec function and notify UI', function() { +    var finished = false; +    var ui = new UIMock(); +    var spec = {name: 'test spec', fn: function() { +      this.test = 'some value'; +    }}; +    runner.addFuture('test future', function(done) { +      done(); +    }); +    runner.run(ui, spec, function() { +      finished = true; +    }); +    expect(runner.test).toEqual('some value'); +    expect(finished).toBeTruthy(); +    expect(ui.log).toEqual([ +      'addSpec:test spec', +      'addStep:test future', +      'step finish:', +      'spec finish:' +    ]); +  }); + +  it('should execute notify UI on spec setup error', function() { +    var finished = false; +    var ui = new UIMock(); +    var spec = {name: 'test spec', fn: function() { +      throw 'message'; +    }}; +    runner.run(ui, spec, function() { +      finished = true; +    }); +    expect(finished).toBeTruthy(); +    expect(ui.log).toEqual([ +      'addSpec:test spec', +      'spec error:message' +    ]); +  }); +   +  it('should execute notify UI on step failure', function() { +    var finished = false; +    var ui = new UIMock(); +    var spec = {name: 'test spec', fn: angular.noop}; +    runner.addFuture('test future', function(done) { +      done('failure message'); +    }); +    runner.run(ui, spec, function() { +      finished = true; +    }); +    expect(finished).toBeTruthy(); +    expect(ui.log).toEqual([ +      'addSpec:test spec', +      'addStep:test future', +      'step finish:failure message', +      'spec finish:failure message' +    ]); +  }); + +  it('should execute notify UI on step error', function() { +    var finished = false; +    var ui = new UIMock(); +    var spec = {name: 'test spec', fn: angular.noop}; +    runner.addFuture('test future', function(done) { +      throw 'error message'; +    }); +    runner.run(ui, spec, function() { +      finished = true; +    }); +    expect(finished).toBeTruthy(); +    expect(ui.log).toEqual([ +      'addSpec:test spec', +      'addStep:test future', +      'step error:error message', +      'spec finish:error message' +    ]); +  }); + +}); diff --git a/test/scenario/TestContext.js b/test/scenario/TestContext.js deleted file mode 100644 index 0c8e6143..00000000 --- a/test/scenario/TestContext.js +++ /dev/null @@ -1,15 +0,0 @@ -var _window, runner, log, $scenario; - -function logger(text) { -  return function(done){ -    log += text; -    (done||noop)(); -  }; -} - -function setUpContext() { -  _window = {}; -  runner = new angular.scenario.Runner(_window, _jQuery); -  $scenario = _window.$scenario; -  log = ''; -} diff --git a/test/scenario/matchersSpec.js b/test/scenario/matchersSpec.js new file mode 100644 index 00000000..faabd1a2 --- /dev/null +++ b/test/scenario/matchersSpec.js @@ -0,0 +1,43 @@ +describe('angular.scenario.matchers', function () { +  var matchers; +   +  function expectMatcher(value, test) { +    delete matchers.error; +    delete matchers.future.value; +    if (value !== undefined) { +      matchers.future.value = value; +    } +    test(); +    expect(matchers.error).toBeUndefined(); +  } +   +  beforeEach(function() { +    /**  +     * Mock up the future system wrapped around matchers. +     * +     * @see Scenario.js#angular.scenario.matcher +     */ +    matchers = { +      future: { name: 'test' } +    }; +    matchers.addFuture = function(name, callback) { +      callback(function(error) { +        matchers.error = error; +      }); +    }; +    angular.extend(matchers, angular.scenario.matcher); +  }); +   +  it('should handle basic matching', function() { +    expectMatcher(10, function() { matchers.toEqual(10); }); +    expectMatcher('value', function() { matchers.toBeDefined(); }); +    expectMatcher([1], function() { matchers.toBeTruthy(); }); +    expectMatcher("", function() { matchers.toBeFalsy(); }); +    expectMatcher(0, function() { matchers.toBeFalsy(); }); +    expectMatcher('foo', function() { matchers.toMatch('.o.'); }); +    expectMatcher(null, function() { matchers.toBeNull(); }); +    expectMatcher([1, 2, 3], function() { matchers.toContain(2); }); +    expectMatcher(3, function() { matchers.toBeLessThan(10); }); +    expectMatcher(3, function() { matchers.toBeGreaterThan(-5); }); +  }); +}); diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index 955dccfa..47bc0d0d 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -22,6 +22,19 @@ beforeEach(function(){          return "Expected to not have class 'ng-validation-error' but found.";        };        return !hasClass; +    }, + +    toEqualData: function(expected) { +      return equals(this.actual, expected); +    }, + +    toHaveClass: function(clazz) { +      this.message = function(){ +        return "Expected '" + sortedHtml(this.actual) + "' to have class '" + clazz + "'."; +      }; +      return this.actual.hasClass ?  +              this.actual.hasClass(clazz) : +              jqLite(this.actual).hasClass(clazz);      }    });  }); @@ -194,3 +207,9 @@ function click(element) {      JQLite.prototype.trigger.call(element, 'click');    }  } + +function rethrow(e) {  +  if(e) { +    throw e;  +  } +} | 
