diff options
35 files changed, 1034 insertions, 121 deletions
| diff --git a/CHANGELOG.md b/CHANGELOG.md index daa7537d..1856124a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@  <a name="0.9.16"><a/>  # <angular/> 0.9.16 weather-control (in-progress) # +### Features +- we can run scenario tests with jstd (from command line and in multiple browsers) + +### Breaking changes +- html scenario runner requires ng:autotest option to start tests automatically  <a name="0.9.15"><a/> @@ -53,7 +53,7 @@ ANGULAR_SCENARIO = [    'src/scenario/output/Html.js',    'src/scenario/output/Json.js',    'src/scenario/output/Xml.js', -  'src/scenario/output/Object.js', +  'src/scenario/output/Object.js'  ]  BUILD_DIR = 'build' @@ -94,6 +94,30 @@ task :compile_scenario => :init do    end  end +desc 'Compile JSTD Scenario Adapter' +task :compile_jstd_scenario_adapter => :init do + +  deps = [ +      'src/jstd-scenario-adapter/angular.prefix', +      'src/jstd-scenario-adapter/Adapter.js', +      'src/jstd-scenario-adapter/angular.suffix', +  ] + +  concat = 'cat ' + deps.flatten.join(' ') + +  File.open(path_to('jstd-scenario-adapter.js'), 'w') do |f| +    f.write(%x{#{concat}}) +  end + +  # TODO(vojta) use jstd configuration when implemented +  # (instead of including jstd-adapter-config.js) +  File.open(path_to('jstd-scenario-adapter-config.js'), 'w') do |f| +    f.write("/**\r\n" + +            " * Configuration for jstd scenario adapter \n */\n" + +            "var jstdScenarioAdapter = {\n  relativeUrlPrefix: '/build/docs/'\n};\n") +  end +end +  desc 'Generate IE css js patch'  task :generate_ie_compat => :init do @@ -152,7 +176,7 @@ end  desc 'Compile JavaScript' -task :compile => [:init, :compile_scenario, :generate_ie_compat] do +task :compile => [:init, :compile_scenario, :compile_jstd_scenario_adapter, :generate_ie_compat] do    deps = [        'src/angular.prefix', @@ -195,7 +219,9 @@ task :package => [:clean, :compile, :docs] do      path_to('angular.js'),      path_to('angular.min.js'),      path_to('angular-ie-compat.js'), -    path_to('angular-scenario.js') +    path_to('angular-scenario.js'), +    path_to('jstd-scenario-adapter.js'), +    path_to('jstd-scenario-adapter-config.js'),    ].each do |src|      dest = src.gsub(/^[^\/]+\//, '').gsub(/((\.min)?\.js)$/, "-#{version}\\1")      FileUtils.cp(src, pkg_dir + '/' + dest) diff --git a/docs/cookbook.deeplinking.ngdoc b/docs/cookbook.deeplinking.ngdoc index 5270eb16..7d69ee84 100644 --- a/docs/cookbook.deeplinking.ngdoc +++ b/docs/cookbook.deeplinking.ngdoc @@ -34,8 +34,8 @@ In this example we have a simple app which consist of two screens:  The two partials are defined in the following URLs: -* {@link ./static/settings.html} -* {@link ./static/welcome.html} +* {@link ./examples/settings.html} +* {@link ./examples/welcome.html}  <doc:example> @@ -44,8 +44,8 @@ The two partials are defined in the following URLs:       AppCntl.$inject = ['$route']       function AppCntl($route) {        // define routes -      $route.when("",          {template:'./static/welcome.html',  controller:WelcomeCntl}); -      $route.when("/settings", {template:'./static/settings.html', controller:SettingsCntl}); +      $route.when("",          {template:'./examples/welcome.html',  controller:WelcomeCntl}); +      $route.when("/settings", {template:'./examples/settings.html', controller:SettingsCntl});        $route.parent(this);        // initialize the model to something useful diff --git a/docs/static/settings.html b/docs/examples/settings.html index 2fa5dca8..2fa5dca8 100644 --- a/docs/static/settings.html +++ b/docs/examples/settings.html diff --git a/docs/static/welcome.html b/docs/examples/welcome.html index b085123d..b085123d 100644 --- a/docs/static/welcome.html +++ b/docs/examples/welcome.html diff --git a/docs/src/gen-docs.js b/docs/src/gen-docs.js index 83e33942..464916b1 100644 --- a/docs/src/gen-docs.js +++ b/docs/src/gen-docs.js @@ -25,22 +25,22 @@ var writes = callback.chain(function(){    var metadata = ngdoc.metadata(docs);    writer.output('docs-keywords.js', ['NG_PAGES=', JSON.stringify(metadata).replace(/{/g, '\n{'), ';'], writes.waitFor());    writer.copyDir('img', writes.waitFor()); -  writer.copyDir('static', writes.waitFor()); -  writer.copy('index.html', writes.waitFor()); -  writer.copy('docs.js', writes.waitFor()); -  writer.copy('docs.css', writes.waitFor()); -  writer.copy('doc_widgets.js', writes.waitFor()); -  writer.copy('doc_widgets.css', writes.waitFor()); -  writer.copy('docs-scenario.html', writes.waitFor()); +  writer.copyDir('examples', writes.waitFor()); +  writer.copyTpl('index.html', writes.waitFor()); +  writer.copyTpl('docs.js', writes.waitFor()); +  writer.copyTpl('docs.css', writes.waitFor()); +  writer.copyTpl('doc_widgets.js', writes.waitFor()); +  writer.copyTpl('doc_widgets.css', writes.waitFor()); +  writer.copyTpl('docs-scenario.html', writes.waitFor());    writer.output('docs-scenario.js', ngdoc.scenarios(docs), writes.waitFor());    writer.output('sitemap.xml', new SiteMap(docs).render(), writes.waitFor());    writer.output('robots.txt', 'Sitemap: http://docs.angularjs.org/sitemap.xml\n', writes.waitFor()); -  writer.copy('syntaxhighlighter/shBrushJScript.js', writes.waitFor()); -  writer.copy('syntaxhighlighter/shBrushXml.js', writes.waitFor()); -  writer.copy('syntaxhighlighter/shCore.css', writes.waitFor()); -  writer.copy('syntaxhighlighter/shCore.js', writes.waitFor()); -  writer.copy('syntaxhighlighter/shThemeDefault.css', writes.waitFor()); -  writer.copy('jquery.min.js', writes.waitFor()); +  writer.copyTpl('syntaxhighlighter/shBrushJScript.js', writes.waitFor()); +  writer.copyTpl('syntaxhighlighter/shBrushXml.js', writes.waitFor()); +  writer.copyTpl('syntaxhighlighter/shCore.css', writes.waitFor()); +  writer.copyTpl('syntaxhighlighter/shCore.js', writes.waitFor()); +  writer.copyTpl('syntaxhighlighter/shThemeDefault.css', writes.waitFor()); +  writer.copyTpl('jquery.min.js', writes.waitFor());  });  writes.onDone(function(){    console.log('DONE. Generated ' + docs.length + ' pages in ' + diff --git a/docs/src/templates/docs-scenario.html b/docs/src/templates/docs-scenario.html index bc244d5d..fcc70431 100644 --- a/docs/src/templates/docs-scenario.html +++ b/docs/src/templates/docs-scenario.html @@ -2,7 +2,7 @@  <html xmlns:ng="http://angularjs.org" wiki:ng="http://angularjs.org">  <head>    <title><angular/> Docs Scenario Runner</title> -  <script type="text/javascript" src="../angular-scenario.js" ng:autobind></script> +  <script type="text/javascript" src="../angular-scenario.js" ng:autotest></script>    <script type="text/javascript" src="docs-scenario.js"></script>  </head>  <body> diff --git a/docs/src/writer.js b/docs/src/writer.js index 3251b9cd..cf54e1a3 100644 --- a/docs/src/writer.js +++ b/docs/src/writer.js @@ -49,7 +49,7 @@ exports.makeDir = function (path, callback) {    })();  }; -exports.copy = function(filename, callback){ +exports.copyTpl = function(filename, callback) {    copy('docs/src/templates/' + filename, OUTPUT_DIR + filename, callback);  }; diff --git a/example/personalLog/scenario/runner.html b/example/personalLog/scenario/runner.html index 7129c228..2dd776db 100644 --- a/example/personalLog/scenario/runner.html +++ b/example/personalLog/scenario/runner.html @@ -2,7 +2,7 @@  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">    <head>      <title>Personal Log Scenario Runner</title> -    <script type="text/javascript" src="../../../src/scenario/angular-bootstrap.js"></script> +    <script type="text/javascript" src="../../../src/scenario/angular-bootstrap.js" ng:autotest></script>      <script type="text/javascript" src="personalLogScenario.js"></script>    </head>    <body> diff --git a/jsTestDriver-scenario.conf b/jsTestDriver-scenario.conf new file mode 100644 index 00000000..1ad7d32f --- /dev/null +++ b/jsTestDriver-scenario.conf @@ -0,0 +1,10 @@ +server: http://localhost:9877 + +load: +  - build/angular-scenario.js +  - build/jstd-scenario-adapter-config.js +  - build/jstd-scenario-adapter.js +  - build/docs/docs-scenario.js + +proxy: + - {matcher: "*", server: "http://localhost:8000"} diff --git a/jsTestDriver.conf b/jsTestDriver.conf index 204594d4..901803b7 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -13,11 +13,13 @@ load:    - test/testabilityPatch.js    - src/scenario/Scenario.js    - src/scenario/output/*.js +  - src/jstd-scenario-adapter/*.js    - src/scenario/*.js    - src/angular-mocks.js    - test/mocks.js    - test/scenario/*.js    - test/scenario/output/*.js +  - test/jstd-scenario-adapter/*.js    - test/*.js    - test/service/*.js    - example/personalLog/test/*.js diff --git a/scenario/Runner-compiled.html b/scenario/Runner-compiled.html index f5f76fde..78cd7e57 100644 --- a/scenario/Runner-compiled.html +++ b/scenario/Runner-compiled.html @@ -1,7 +1,7 @@  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">    <head> -    <script type="text/javascript" src="../build/angular-scenario.js"></script> +    <script type="text/javascript" src="../build/angular-scenario.js" ng:autotest></script>      <script type="text/javascript" src="widgets-scenario.js"></script>    </head>    <body> diff --git a/scenario/Runner.html b/scenario/Runner.html index f715b8e5..fa3ccf23 100644 --- a/scenario/Runner.html +++ b/scenario/Runner.html @@ -1,7 +1,7 @@  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">    <head> -    <script type="text/javascript" src="../src/scenario/angular-bootstrap.js"></script> +    <script type="text/javascript" src="../src/scenario/angular-bootstrap.js" ng:autotest></script>      <script type="text/javascript" src="widgets-scenario.js"></script>    </head>    <body> diff --git a/server-scenario.sh b/server-scenario.sh new file mode 100755 index 00000000..3f7c42d6 --- /dev/null +++ b/server-scenario.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +java -jar lib/jstestdriver/JsTestDriver.jar --port 9877 --browserTimeout 90000 --config jsTestDriver-scenario.conf diff --git a/src/jstd-scenario-adapter/Adapter.js b/src/jstd-scenario-adapter/Adapter.js new file mode 100644 index 00000000..fd9674e1 --- /dev/null +++ b/src/jstd-scenario-adapter/Adapter.js @@ -0,0 +1,175 @@ +/** + * JSTestDriver adapter for angular scenario tests + * + * Example of jsTestDriver.conf for running scenario tests with JSTD: +  <pre> +    server: http://localhost:9877 + +    load: +      - lib/angular-scenario.js +      - lib/jstd-scenario-adapter-config.js +      - lib/jstd-scenario-adapter.js +      # your test files go here # + +    proxy: +     - {matcher: "/your-prefix/*", server: "http://localhost:8000/"} +  </pre> + * + * For more information on how to configure jstd proxy, see {@link http://code.google.com/p/js-test-driver/wiki/Proxy} + * Note the order of files - it's important ! + * + * Example of jstd-scenario-adapter-config.js +  <pre> +    var jstdScenarioAdapter = { +      relativeUrlPrefix: '/your-prefix/' +    }; +  </pre> + * + * Whenever you use <code>browser().navigateTo('relativeUrl')</code> in your scenario test, the relativeUrlPrefix will be prepended. + * You have to configure this to work together with JSTD proxy. + * + * Let's assume you are using the above configuration (jsTestDriver.conf and jstd-scenario-adapter-config.js): + * Now, when you call <code>browser().navigateTo('index.html')</code> in your scenario test, the browser will open /your-prefix/index.html. + * That matches the proxy, so JSTD will proxy this request to http://localhost:8000/index.html. + */ + +/** + * Custom type of test case + * + * @const + * @see jstestdriver.TestCaseInfo + */ +var SCENARIO_TYPE = 'scenario'; + +/** + * Plugin for JSTestDriver + * Connection point between scenario's jstd output and jstestdriver. + * + * @see jstestdriver.PluginRegistrar + */ +function JstdPlugin() { +  var nop = function() {}; + +  this.reportResult = nop; +  this.reportEnd = nop; +  this.runScenario = nop; + +  this.name = 'Angular Scenario Adapter'; + +  /** +   * Called for each JSTD TestCase +   * +   * Handles only SCENARIO_TYPE test cases. There should be only one fake TestCase. +   * Runs all scenario tests (under one fake TestCase) and report all results to JSTD. +   * +   * @param {jstestdriver.TestRunConfiguration} configuration +   * @param {Function} onTestDone +   * @param {Function} onAllTestsComplete +   * @returns {boolean} True if this type of test is handled by this plugin, false otherwise +   */ +  this.runTestConfiguration = function(configuration, onTestDone, onAllTestsComplete) { +    if (configuration.getTestCaseInfo().getType() != SCENARIO_TYPE) return false; + +    this.reportResult = onTestDone; +    this.reportEnd = onAllTestsComplete; +    this.runScenario(); + +    return true; +  }; + +  this.getTestRunsConfigurationFor = function(testCaseInfos, expressions, testRunsConfiguration) { +    testRunsConfiguration.push( +        new jstestdriver.TestRunConfiguration( +            new jstestdriver.TestCaseInfo( +                'Angular Scenario Tests', function() {}, SCENARIO_TYPE), [])); + +    return true; +  }; +} + +/** + * Singleton instance of the plugin + * Accessed using closure by: + *  - jstd output (reports to this plugin) + *  - initScenarioAdapter (register the plugin to jstd) + */ +var plugin = new JstdPlugin(); + +/** + * Initialise scenario jstd-adapter + * (only if jstestdriver is defined) + * + * @param {Object} jstestdriver Undefined when run from browser (without jstd) + * @param {Function} initScenarioAndRun Function that inits scenario and runs all the tests + * @param {Object=} config Configuration object, supported properties: + *  - relativeUrlPrefix: prefix for all relative links when navigateTo() + */ +function initScenarioAdapter(jstestdriver, initScenarioAndRun, config) { +  if (jstestdriver) { +    // create and register ScenarioPlugin +    jstestdriver.pluginRegistrar.register(plugin); +    plugin.runScenario = initScenarioAndRun; + +    /** +     * HACK (angular.scenario.Application.navigateTo) +     * +     * We need to navigate to relative urls when running from browser (without JSTD), +     * because we want to allow running scenario tests without creating its own virtual host. +     * For example: http://angular.local/build/docs/docs-scenario.html +     * +     * On the other hand, when running with JSTD, we need to navigate to absolute urls, +     * because of JSTD proxy. (proxy, because of same domain policy) +     * +     * So this hack is applied only if running with JSTD and change all relative urls to absolute. +     */ +    var appProto = angular.scenario.Application.prototype, +        navigateTo = appProto.navigateTo, +        relativeUrlPrefix = config && config.relativeUrlPrefix || '/'; + +    appProto.navigateTo = function(url, loadFn, errorFn) { +      if (url.charAt(0) != '/' && url.charAt(0) != '#' && +          url != 'about:blank' && !url.match(/^https?/)) { +        url = relativeUrlPrefix + url; +      } + +      return navigateTo.call(this, url, loadFn, errorFn); +    }; +  } +} + +/** + * Builds proper TestResult object from given model spec + * + * TODO(vojta) report error details + * + * @param {angular.scenario.ObjectModel.Spec} spec + * @returns {jstestdriver.TestResult} + */ +function createTestResultFromSpec(spec) { +  var map = { +    success: 'PASSED', +    error:   'ERROR', +    failure: 'FAILED' +  }; + +  return new jstestdriver.TestResult( +    spec.fullDefinitionName, +    spec.name, +    jstestdriver.TestResult.RESULT[map[spec.status]], +    spec.error || '', +    spec.line || '', +    spec.duration); +} + +/** + * Generates JSTD output (jstestdriver.TestResult) + */ +angular.scenario.output('jstd', function(context, runner, model) { +  model.on('SpecEnd', function(spec) { +    plugin.reportResult(createTestResultFromSpec(spec)); +  }); + +  model.on('RunnerEnd', function() { +    plugin.reportEnd(); +  }); +}); diff --git a/src/jstd-scenario-adapter/angular.prefix b/src/jstd-scenario-adapter/angular.prefix new file mode 100644 index 00000000..ab8a7152 --- /dev/null +++ b/src/jstd-scenario-adapter/angular.prefix @@ -0,0 +1,24 @@ +/** + * The MIT License + * + * Copyright (c) 2010 Adam Abrons and Misko Hevery http://getangular.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +(function(window) { diff --git a/src/jstd-scenario-adapter/angular.suffix b/src/jstd-scenario-adapter/angular.suffix new file mode 100644 index 00000000..6134fb01 --- /dev/null +++ b/src/jstd-scenario-adapter/angular.suffix @@ -0,0 +1,2 @@ +initScenarioAdapter(window.jstestdriver, angular.scenario.setUpAndRun, window.jstdScenarioAdapter); +})(window); diff --git a/src/scenario/Describe.js b/src/scenario/Describe.js index 50cfded6..c6484f2f 100644 --- a/src/scenario/Describe.js +++ b/src/scenario/Describe.js @@ -37,6 +37,9 @@ angular.scenario.Describe = function(descName, parent) {  // Shared Unique ID generator for every describe block  angular.scenario.Describe.id = 0; +// Shared Unique ID generator for every it (spec) +angular.scenario.Describe.specId = 0; +  /**   * Defines a block to execute before each it or nested describe.   * @@ -93,6 +96,7 @@ angular.scenario.Describe.prototype.xdescribe = angular.noop;   */  angular.scenario.Describe.prototype.it = function(name, body) {    this.its.push({ +    id: angular.scenario.Describe.specId++,      definition: this,      only: this.only,      name: name, diff --git a/src/scenario/ObjectModel.js b/src/scenario/ObjectModel.js index 263aa5f9..bff14461 100644 --- a/src/scenario/ObjectModel.js +++ b/src/scenario/ObjectModel.js @@ -4,21 +4,26 @@   * @param {Object} runner The scenario Runner instance to connect to.   *   * TODO(esprehn): Every output type creates one of these, but we probably - *  want one glonal shared instance. Need to handle events better too + *  want one global shared instance. Need to handle events better too   *  so the HTML output doesn't need to do spec model.getSpec(spec.id)   *  silliness. + * + * TODO(vojta) refactor on, emit methods (from all objects) - use inheritance   */  angular.scenario.ObjectModel = function(runner) {    var self = this;    this.specMap = {}; +  this.listeners = [];    this.value = {      name: '',      children: {}    };    runner.on('SpecBegin', function(spec) { -    var block = self.value; +    var block = self.value, +        definitions = []; +      angular.forEach(self.getDefinitionPath(spec), function(def) {        if (!block.children[def.name]) {          block.children[def.name] = { @@ -29,49 +34,78 @@ angular.scenario.ObjectModel = function(runner) {          };        }        block = block.children[def.name]; +      definitions.push(def.name);      }); -    self.specMap[spec.id] = block.specs[spec.name] = -      new angular.scenario.ObjectModel.Spec(spec.id, spec.name); + +    var it = self.specMap[spec.id] = +             block.specs[spec.name] = +             new angular.scenario.ObjectModel.Spec(spec.id, spec.name, definitions); + +    // forward the event +    self.emit('SpecBegin', it);    });    runner.on('SpecError', function(spec, error) {      var it = self.getSpec(spec.id);      it.status = 'error';      it.error = error; + +    // forward the event +    self.emit('SpecError', it, error);    });    runner.on('SpecEnd', function(spec) {      var it = self.getSpec(spec.id);      complete(it); + +    // forward the event +    self.emit('SpecEnd', it);    });    runner.on('StepBegin', function(spec, step) {      var it = self.getSpec(spec.id); -    it.steps.push(new angular.scenario.ObjectModel.Step(step.name)); +    var step = new angular.scenario.ObjectModel.Step(step.name); +    it.steps.push(step); + +    // forward the event +    self.emit('StepBegin', it, step);    });    runner.on('StepEnd', function(spec, step) {      var it = self.getSpec(spec.id); -    if (it.getLastStep().name !== step.name) -      throw 'Events fired in the wrong order. Step names don\' match.'; -    complete(it.getLastStep()); +    var step = it.getLastStep(); +    if (step.name !== step.name) +      throw 'Events fired in the wrong order. Step names don\'t match.'; +    complete(step); + +    // forward the event +    self.emit('StepEnd', it, step);    });    runner.on('StepFailure', function(spec, step, error) { -    var it = self.getSpec(spec.id); -    var item = it.getLastStep(); -    item.error = error; -    if (!it.status) { -      it.status = item.status = 'failure'; -    } +    var it = self.getSpec(spec.id), +        modelStep = it.getLastStep(); + +    modelStep.setErrorStatus('failure', error, step.line()); +    it.setStatusFromStep(modelStep); + +    // forward the event +    self.emit('StepFailure', it, modelStep, error);    });    runner.on('StepError', function(spec, step, error) { -    var it = self.getSpec(spec.id); -    var item = it.getLastStep(); -    it.status = 'error'; -    item.status = 'error'; -    item.error = error; +    var it = self.getSpec(spec.id), +        modelStep = it.getLastStep(); + +    modelStep.setErrorStatus('error', error, step.line()); +    it.setStatusFromStep(modelStep); + +    // forward the event +    self.emit('StepError', it, modelStep, error); +  }); + +  runner.on('RunnerEnd', function() { +    self.emit('RunnerEnd');    });    function complete(item) { @@ -82,6 +116,36 @@ angular.scenario.ObjectModel = function(runner) {  };  /** + * Adds a listener for an event. + * + * @param {string} eventName Name of the event to add a handler for + * @param {Function} listener Function that will be called when event is fired + */ +angular.scenario.ObjectModel.prototype.on = function(eventName, listener) { +  eventName = eventName.toLowerCase(); +  this.listeners[eventName] = this.listeners[eventName] || []; +  this.listeners[eventName].push(listener); +}; + +/** + * Emits an event which notifies listeners and passes extra + * arguments. + * + * @param {string} eventName Name of the event to fire. + */ +angular.scenario.ObjectModel.prototype.emit = function(eventName) { +  var self = this, +      args = Array.prototype.slice.call(arguments, 1), +      eventName = eventName.toLowerCase(); + +  if (this.listeners[eventName]) { +    angular.forEach(this.listeners[eventName], function(listener) { +      listener.apply(self, args); +    }); +  } +}; + +/**   * Computes the path of definition describe blocks that wrap around   * this spec.   * @@ -113,12 +177,14 @@ angular.scenario.ObjectModel.prototype.getSpec = function(id) {   *   * @param {string} id Id of the spec   * @param {string} name Name of the spec + * @param {Array<string>=} definitionNames List of all describe block names that wrap this spec   */ -angular.scenario.ObjectModel.Spec = function(id, name) { +angular.scenario.ObjectModel.Spec = function(id, name, definitionNames) {    this.id = id;    this.name = name;    this.startTime = new Date().getTime();    this.steps = []; +  this.fullDefinitionName = (definitionNames || []).join(' ');  };  /** @@ -143,6 +209,19 @@ angular.scenario.ObjectModel.Spec.prototype.getLastStep = function() {  };  /** + * Set status of the Spec from given Step + * + * @param {angular.scenario.ObjectModel.Step} step + */ +angular.scenario.ObjectModel.Spec.prototype.setStatusFromStep = function(step) { +  if (!this.status || step.status == 'error') { +    this.status = step.status; +    this.error = step.error; +    this.line = step.line; +  } +}; + +/**   * A single step inside a Spec.   *   * @param {string} step Name of the step @@ -151,3 +230,16 @@ angular.scenario.ObjectModel.Step = function(name) {    this.name = name;    this.startTime = new Date().getTime();  }; + +/** + * Helper method for setting all error status related properties + * + * @param {string} status + * @param {string} error + * @param {string} line + */ +angular.scenario.ObjectModel.Step.prototype.setErrorStatus = function(status, error, line) { +  this.status = status; +  this.error = error; +  this.line = line; +}; diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js index 583c8beb..51a81d6a 100644 --- a/src/scenario/Runner.js +++ b/src/scenario/Runner.js @@ -1,5 +1,8 @@  /** - * Runner for scenarios. + * Runner for scenarios + * + * Has to be initialized before any test is loaded, + * because it publishes the API into window (global space).   */  angular.scenario.Runner = function($window) {    this.listeners = []; diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js index 87dad5b5..6879d787 100644 --- a/src/scenario/Scenario.js +++ b/src/scenario/Scenario.js @@ -87,17 +87,20 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {  };  /** - * Initialization function for the scenario runner. + * Initialize the scenario runner and run !   * - * @param {angular.scenario.Runner} $scenario The runner to setup - * @param {Object} config Config options + * Access global window and document object + * Access $runner through closure + * + * @param {Object=} config Config options   */ -function angularScenarioInit($scenario, config) { +angular.scenario.setUpAndRun = function (config) {    var href = window.location.href;    var body = _jQuery(document.body);    var output = []; +  var objModel = new angular.scenario.ObjectModel($runner); -  if (config.scenario_output) { +  if (config && config.scenario_output) {      output = config.scenario_output.split(',');    } @@ -105,7 +108,7 @@ function angularScenarioInit($scenario, config) {      if (!output.length || indexOf(output,name) != -1) {        var context = body.append('<div></div>').find('div:last');        context.attr('id', name); -      fn.call({}, context, $scenario); +      fn.call({}, context, $runner, objModel);      }    }); @@ -121,12 +124,12 @@ function angularScenarioInit($scenario, config) {    var appFrame = body.append('<div id="application"></div>').find('#application');    var application = new angular.scenario.Application(appFrame); -  $scenario.on('RunnerEnd', function() { +  $runner.on('RunnerEnd', function() {      appFrame.css('display', 'none');      appFrame.find('iframe').attr('src', 'about:blank');    }); -  $scenario.on('RunnerError', function(error) { +  $runner.on('RunnerError', function(error) {      if (window.console) {        console.log(formatException(error));      } else { @@ -135,8 +138,8 @@ function angularScenarioInit($scenario, config) {      }    }); -  $scenario.run(application); -} +  $runner.run(application); +};  /**   * Iterates through list with iterator function that must call the diff --git a/src/scenario/angular-bootstrap.js b/src/scenario/angular-bootstrap.js index 68dc393e..264ce718 100644 --- a/src/scenario/angular-bootstrap.js +++ b/src/scenario/angular-bootstrap.js @@ -23,7 +23,8 @@      try {        if (previousOnLoad) previousOnLoad();      } catch(e) {} -    angularScenarioInit($scenario, angularJsConfig(document)); +    var config = angularJsConfig(document); +    if (config.autotest) angular.scenario.setUpAndRun(config);    };    addCSS("../../css/angular-scenario.css"); @@ -52,8 +53,7 @@    // Create the runner (which also sets up the global API)    document.write(      '<script type="text/javascript">' + -    'var $scenario = new angular.scenario.Runner(window, angular.scenario.SpecRunner);' + -    '</script>' -  ); +    '  var $runner = new angular.scenario.Runner(window);' + +    '</script>');  })(window.onload); diff --git a/src/scenario/angular.suffix b/src/scenario/angular.suffix index 3ab796d2..014c2cc9 100644 --- a/src/scenario/angular.suffix +++ b/src/scenario/angular.suffix @@ -1,7 +1,10 @@ -  var $scenario = new angular.scenario.Runner(window); +var $runner = new angular.scenario.Runner(window), +    config = angularJsConfig(document); +if (config.autotest) {    jqLiteWrap(document).ready(function() { -    angularScenarioInit($scenario, angularJsConfig(document)); +    angular.scenario.setUpAndRun(config);    }); - +}  })(window, document); + diff --git a/src/scenario/output/Html.js b/src/scenario/output/Html.js index 6e2e20f3..ccf7db6f 100644 --- a/src/scenario/output/Html.js +++ b/src/scenario/output/Html.js @@ -4,8 +4,9 @@   * TODO(esprehn): This should be refactored now that ObjectModel exists   *  to use angular bindings for the UI.   */ -angular.scenario.output('html', function(context, runner) { -  var model = new angular.scenario.ObjectModel(runner); +angular.scenario.output('html', function(context, runner, model) { +  var specUiMap = {}, +      lastStepUiMap = {};    context.append(      '<div id="header">' + @@ -22,7 +23,7 @@ angular.scenario.output('html', function(context, runner) {    );    runner.on('InteractiveWait', function(spec, step) { -    var ui = model.getSpec(spec.id).getLastStep().ui; +    var ui = lastStepUiMap[spec.id];      ui.find('.test-title').        html('waiting for you to <a href="javascript:resume()">resume</a>.');    }); @@ -58,59 +59,62 @@ angular.scenario.output('html', function(context, runner) {          name.removeClass('closed').addClass('open');        }      }); -    model.getSpec(spec.id).ui = ui; + +    specUiMap[spec.id] = ui;    });    runner.on('SpecError', function(spec, error) { -    var ui = model.getSpec(spec.id).ui; +    var ui = specUiMap[spec.id];      ui.append('<pre></pre>');      ui.find('> pre').text(formatException(error));    });    runner.on('SpecEnd', function(spec) { +    var ui = specUiMap[spec.id];      spec = model.getSpec(spec.id); -    spec.ui.removeClass('status-pending'); -    spec.ui.addClass('status-' + spec.status); -    spec.ui.find("> .test-info .timer-result").text(spec.duration + "ms"); +    ui.removeClass('status-pending'); +    ui.addClass('status-' + spec.status); +    ui.find("> .test-info .timer-result").text(spec.duration + "ms");      if (spec.status === 'success') { -      spec.ui.find('> .test-info .test-name').addClass('closed'); -      spec.ui.find('> .scrollpane .test-actions').hide(); +      ui.find('> .test-info .test-name').addClass('closed'); +      ui.find('> .scrollpane .test-actions').hide();      }      updateTotals(spec.status);    });    runner.on('StepBegin', function(spec, step) { +    var ui = specUiMap[spec.id];      spec = model.getSpec(spec.id);      step = spec.getLastStep(); -    spec.ui.find('> .scrollpane .test-actions'). -      append('<li class="status-pending"></li>'); -    step.ui = spec.ui.find('> .scrollpane .test-actions li:last'); -    step.ui.append( +    ui.find('> .scrollpane .test-actions').append('<li class="status-pending"></li>'); +    var stepUi = lastStepUiMap[spec.id] = ui.find('> .scrollpane .test-actions li:last'); +    stepUi.append(        '<div class="timer-result"></div>' +        '<div class="test-title"></div>'      ); -    step.ui.find('> .test-title').text(step.name); -    var scrollpane = step.ui.parents('.scrollpane'); +    stepUi.find('> .test-title').text(step.name); +    var scrollpane = stepUi.parents('.scrollpane');      scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));    });    runner.on('StepFailure', function(spec, step, error) { -    var ui = model.getSpec(spec.id).getLastStep().ui; +    var ui = lastStepUiMap[spec.id];      addError(ui, step.line, error);    });    runner.on('StepError', function(spec, step, error) { -    var ui = model.getSpec(spec.id).getLastStep().ui; +    var ui = lastStepUiMap[spec.id];      addError(ui, step.line, error);    });    runner.on('StepEnd', function(spec, step) { +    var stepUi = lastStepUiMap[spec.id];      spec = model.getSpec(spec.id);      step = spec.getLastStep(); -    step.ui.find('.timer-result').text(step.duration + 'ms'); -    step.ui.removeClass('status-pending'); -    step.ui.addClass('status-' + step.status); -    var scrollpane = spec.ui.find('> .scrollpane'); +    stepUi.find('.timer-result').text(step.duration + 'ms'); +    stepUi.removeClass('status-pending'); +    stepUi.addClass('status-' + step.status); +    var scrollpane = specUiMap[spec.id].find('> .scrollpane');      scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));    }); diff --git a/src/scenario/output/Json.js b/src/scenario/output/Json.js index 2c852496..1e3bef6b 100644 --- a/src/scenario/output/Json.js +++ b/src/scenario/output/Json.js @@ -1,10 +1,8 @@  /**   * Generates JSON output into a context.   */ -angular.scenario.output('json', function(context, runner) { -  var model = new angular.scenario.ObjectModel(runner); - -  runner.on('RunnerEnd', function() { +angular.scenario.output('json', function(context, runner, model) { +  model.on('RunnerEnd', function() {      context.text(angular.toJson(model.value));    });  }); diff --git a/src/scenario/output/Object.js b/src/scenario/output/Object.js index 3257cfd7..1f29af9c 100644 --- a/src/scenario/output/Object.js +++ b/src/scenario/output/Object.js @@ -1,6 +1,6 @@  /**   * Creates a global value $result with the result of the runner.   */ -angular.scenario.output('object', function(context, runner) { -  runner.$window.$result = new angular.scenario.ObjectModel(runner).value; +angular.scenario.output('object', function(context, runner, model) { +  runner.$window.$result = model.value;  }); diff --git a/src/scenario/output/Xml.js b/src/scenario/output/Xml.js index e8c7f0e3..1a4c362c 100644 --- a/src/scenario/output/Xml.js +++ b/src/scenario/output/Xml.js @@ -1,10 +1,9 @@  /**   * Generates XML output into a context.   */ -angular.scenario.output('xml', function(context, runner) { -  var model = new angular.scenario.ObjectModel(runner); +angular.scenario.output('xml', function(context, runner, model) {    var $ = function(args) {return new context.init(args);}; -  runner.on('RunnerEnd', function() { +  model.on('RunnerEnd', function() {      var scenario = $('<scenario></scenario>');      context.append(scenario);      serializeXml(scenario, model.value); diff --git a/test-scenario.sh b/test-scenario.sh new file mode 100755 index 00000000..8ecf8b03 --- /dev/null +++ b/test-scenario.sh @@ -0,0 +1,7 @@ +#!/bin/bash +tests=$1 +if [[ $tests = "" ]]; then +  tests="all" +fi + +java -jar lib/jstestdriver/JsTestDriver.jar --tests "$tests" --config jsTestDriver-scenario.conf --reset diff --git a/test/jstd-scenario-adapter/AdapterSpecs.js b/test/jstd-scenario-adapter/AdapterSpecs.js new file mode 100644 index 00000000..ccf08e3d --- /dev/null +++ b/test/jstd-scenario-adapter/AdapterSpecs.js @@ -0,0 +1,320 @@ +describe('jstd-adapter', function() { +  var fakeJSTD = { pluginRegistrar: { register: function() {} } }, +      originalNavigateTo = angular.scenario.Application.prototype.navigateTo; + +  /** +   * Reverts hack on angular.scenario.Application.navigateTo +   * We should revert this hack after any single call of initScenarioAdapter, +   * so that it doesn't influence other tests... +   */ +  function revertNavigateToHack() { +    angular.scenario.Application.prototype.navigateTo = originalNavigateTo; +  } + +  /** +   * Helper for building angular.scenario.ObjectModel.Spec +   * @returns {angular.scenario.ObjectModel.Spec} +   */ +  function buildSpec(status, name, duration, definitions, error, line) { +    var spec = new angular.scenario.ObjectModel.Spec( +        'fake-id', name || 'name', definitions || ['desc1', 'desc2']); +    spec.duration = duration || 10; +    spec.status = status || 'success'; +    spec.error = error || ''; +    spec.line = line || ''; + +    return spec; +  } + +  /** +   * Helper for building angular.scenario.ObjectModel.Spec with error and error line +   * @returns {angular.scenario.ObjectModel.Spec} +   */ +  function buildErrorSpec(error, line, status, name) { +    return buildSpec(status || 'error', name, null, null, error, line); +  } + +  /** +   * Helper for building TestConfiguration +   * @returns {jstestdriver.TestRunConfiguration} +   */ +  function buildTestConf(type) { +    return new jstestdriver.TestRunConfiguration( +      new jstestdriver.TestCaseInfo('Fake test - ' + Math.random(), function(){}, type), null); +  } + +  /** +   * Helper for building SCENARIO TestConfiguration +   * @returns {jstestdriver.TestRunConfiguration} +   */ +  function buildScenarioTestConf() { +    return buildTestConf(SCENARIO_TYPE); +  } + +  describe('initScenarioAdapter', function() { +    afterEach(revertNavigateToHack); + +    it('should create and register plugin if jstestdriver defined', function() { +      spyOn(fakeJSTD.pluginRegistrar, 'register'); +      initScenarioAdapter(fakeJSTD); +      expect(fakeJSTD.pluginRegistrar.register).toHaveBeenCalled(); +      expect(fakeJSTD.pluginRegistrar.register.mostRecentCall.args[0] instanceof JstdPlugin); +    }); + +    it('should do nothing if jstestdriver not defined', function() { +      expect(function() { +        initScenarioAdapter(undefined); +      }).not.toThrow(); +    }); + +    it('should set setUpAndRun callback to plugin', function() { +      var runFn = jasmine.createSpy('setUpAndRun'); +      plugin.runScenario = null; + +      initScenarioAdapter(fakeJSTD, runFn); +      expect(plugin.runScenario).toBe(runFn); +    }); + +    describe('navigateTo', function() { +      var fakeJSTD = { pluginRegistrar: { register: function() {} } }, +          app = new angular.scenario.Application(_jQuery('<div></div>')), +          navigateSpy; + +      beforeEach(function() { +        navigateSpy = spyOn(angular.scenario.Application.prototype, 'navigateTo'); +      }); + +      it('should add url prefix when jstd defined', function() { +        initScenarioAdapter(fakeJSTD, null, {relativeUrlPrefix: '/prefix/'}); + +        app.navigateTo('test.html'); +        expect(navigateSpy).toHaveBeenCalled(); +        expect(navigateSpy.mostRecentCall.args[0]).toEqual('/prefix/test.html'); +      }); + +      it('should add forward-slash as default url prefix when jstd defined', function() { +        initScenarioAdapter(fakeJSTD); + +        app.navigateTo('test.html'); +        expect(navigateSpy).toHaveBeenCalled(); +        expect(navigateSpy.mostRecentCall.args[0]).toEqual('/test.html'); +      }); + +      it('should not change url when jstd not defined', function() { +        initScenarioAdapter(null); + +        app.navigateTo('test.html'); +        expect(navigateSpy).toHaveBeenCalled(); +        expect(navigateSpy.mostRecentCall.args[0]).toEqual('test.html'); +      }); + +      it('should not change hash url', function() { +        initScenarioAdapter(fakeJSTD); + +        app.navigateTo('#/index.html/a'); +        expect(navigateSpy).toHaveBeenCalled(); +        expect(navigateSpy.mostRecentCall.args[0]).toEqual('#/index.html/a'); +      }); + +      it('should not change absolute url', function() { +        initScenarioAdapter(fakeJSTD); + +        app.navigateTo('/index.html/a'); +        expect(navigateSpy).toHaveBeenCalled(); +        expect(navigateSpy.mostRecentCall.args[0]).toEqual('/index.html/a'); +      }); + +      it('should not change "about:blank" url', function() { +        initScenarioAdapter(fakeJSTD); + +        app.navigateTo('about:blank'); +        expect(navigateSpy).toHaveBeenCalled(); +        expect(navigateSpy.mostRecentCall.args[0]).toEqual('about:blank'); +      }); + +      it('should not change url with domain', function() { +        initScenarioAdapter(fakeJSTD); + +        app.navigateTo('http://www.google.com'); +        expect(navigateSpy).toHaveBeenCalled(); +        expect(navigateSpy.mostRecentCall.args[0]).toEqual('http://www.google.com'); +      }); +    }); +  }); + +  describe('JstdPlugin', function() { +    var p; + +    beforeEach(function() { +      p = new JstdPlugin(); +    }); + +    describe('runTestConfiguration', function() { +      var initScenarioSpy, onTestSpy, onAllTestsSpy, spec, modelSpec; + +      beforeEach(function() { +        initScenarioSpy = jasmine.createSpy('initScenarioAndRun'); +        onTestSpy = jasmine.createSpy('onOneTest'); +        onAllTestsSpy = jasmine.createSpy('onAllTests'); + +        p.runScenario = initScenarioSpy; +        spec = {id: 'fake', name: 'Spec Name'}; +        modelSpec = new angular.scenario.ObjectModel.Spec(spec.id, spec.name); +      }); + +      it('should ignore non scenario test cases', function() { +        expect(p.runTestConfiguration(buildTestConf(), onTestSpy, onAllTestsSpy)).toBe(false); +        expect(p.runTestConfiguration(buildTestConf('async'), onTestSpy, onAllTestsSpy)).toBe(false); +        expect(initScenarioSpy).not.toHaveBeenCalled(); +        expect(onTestSpy).not.toHaveBeenCalled(); +        expect(onAllTestsSpy).not.toHaveBeenCalled(); +      }); + +      it('should return true when scenario test case', function() { +        expect(p.runTestConfiguration(buildScenarioTestConf(), onTestSpy, onAllTestsSpy)).toBe(true); +      }); + +      it('should call initAndRunTests when scenario test case', function() { +        p.runTestConfiguration(buildScenarioTestConf(), onTestSpy, onAllTestsSpy); +        expect(initScenarioSpy).toHaveBeenCalled(); +      }); +    }); + +    describe('getTestRunsConfigurationFor', function() { +      it('should add TestRunConfiguration with SCENARIO_TYPE TestCase', function() { +        var configurations = []; +        p.getTestRunsConfigurationFor(null, null, configurations); + +        expect(configurations.length).toBe(1); +        expect(configurations[0] instanceof jstestdriver.TestRunConfiguration).toBe(true); +        expect(configurations[0].getTestCaseInfo().getType()).toEqual(SCENARIO_TYPE); +      }); + +      it('should always return true', function() { +        expect(p.getTestRunsConfigurationFor(null, null, [])).toBe(true); +      }); +    }); +  }); + +  describe('createTestResultFromSpec', function() { +    it('should return jstestdriver.TestResult instance', function() { +      expect(createTestResultFromSpec(buildSpec()) instanceof jstestdriver.TestResult).toBe(true); +    }); + +    it('should set proper test name', function() { +      expect(createTestResultFromSpec(buildSpec()).testName).toEqual('name'); +    }); + +    it('should set duration', function() { +      expect(createTestResultFromSpec(buildSpec()).time).toEqual(10); +    }); + +    it('should set test case - full definition name', function() { +      var spec = buildSpec(); +      expect(createTestResultFromSpec(spec).testCaseName).toEqual(spec.fullDefinitionName); +    }); + +    it('should set passed result when success', function() { +      expect(createTestResultFromSpec(buildSpec('success')).result) +        .toEqual(jstestdriver.TestResult.RESULT.PASSED); +    }); + +    it('should set error result when error', function() { +      expect(createTestResultFromSpec(buildSpec('error')).result) +        .toEqual(jstestdriver.TestResult.RESULT.ERROR); +    }); + +    it('should set failed result when failure', function() { +      expect(createTestResultFromSpec(buildSpec('failure')).result) +        .toEqual(jstestdriver.TestResult.RESULT.FAILED); +    }); + +    it('should set error message when error/failure', function() { +      expect(createTestResultFromSpec(buildErrorSpec('error-message')).message) +      .toEqual('error-message'); +    }); + +    it('should log line number when error/failure', function() { +      expect(createTestResultFromSpec(buildErrorSpec('msg', 'line-number')).log) +      .toEqual('line-number'); +    }); +  }); + +  describe('angular.scenario.output.jstd', function() { +    var model; + +    beforeEach(function() { +      var runner = new angular.scenario.testing.MockRunner(), +          context = _jQuery("<div></div>"); + +      plugin = new JstdPlugin(); +      model = new angular.scenario.ObjectModel(runner); +      angular.scenario.output.jstd(context, runner, model); + +      spyOn(plugin, 'reportEnd'); +      spyOn(plugin, 'reportResult'); +    }); + +    it('should report end of all tests', function() { +      model.emit('RunnerEnd'); +      expect(plugin.reportEnd).toHaveBeenCalled(); +    }); + +    it('should report jstestdriver.TestResult', function() { +      model.emit('SpecEnd', buildSpec()); +      expect(plugin.reportResult).toHaveBeenCalled(); +      expect(plugin.reportResult.argsForCall[0][0] instanceof jstestdriver.TestResult).toBe(true); +    }); +  }); + +  // couple of higher level tests (wiring objects together) +  describe('HIGHER LEVEL', function() { +    var initScenarioSpy, onTestSpy, onAllTestsSpy, model; + +    beforeEach(function() { +      plugin = new JstdPlugin(); +      initScenarioSpy = jasmine.createSpy('initScenarioAndRun'); +      onTestSpy = jasmine.createSpy('onOneTest'); +      onAllTestsSpy = jasmine.createSpy('onAllTests'); + +      var runner = new angular.scenario.testing.MockRunner(), +          context = _jQuery("<div></div>"); + +      model = new angular.scenario.ObjectModel(runner); +      angular.scenario.output.jstd(context, runner, model); + +      initScenarioAdapter(fakeJSTD, initScenarioSpy); +      plugin.runTestConfiguration(buildScenarioTestConf(), onTestSpy, onAllTestsSpy); +    }); + +    afterEach(revertNavigateToHack); + +    it('should report and of test suite', function() { +      model.emit('RunnerEnd'); +      expect(onAllTestsSpy).toHaveBeenCalled(); +    }); + +    it('should report success test result', function() { +      model.emit('SpecEnd', buildSpec('success', 'name')); +      expect(onTestSpy).toHaveBeenCalled(); +      var result = onTestSpy.argsForCall[0][0]; +      expect(result instanceof jstestdriver.TestResult).toBe(true); +      expect(result.testName).toEqual('name'); +      expect(result.result).toEqual(jstestdriver.TestResult.RESULT.PASSED); +    }); + +    it('should report error test result', function() { +      model.emit('SpecEnd', buildSpec('error')); +      expect(onTestSpy).toHaveBeenCalled(); +      var result = onTestSpy.argsForCall[0][0]; +      expect(result.result).toEqual(jstestdriver.TestResult.RESULT.ERROR); +    }); + +    it('should report failed test result', function() { +      model.emit('SpecEnd', buildSpec('failure')); +      expect(onTestSpy).toHaveBeenCalled(); +      var result = onTestSpy.argsForCall[0][0]; +      expect(result.result).toEqual(jstestdriver.TestResult.RESULT.FAILED); +    }); +  }); +}); diff --git a/test/scenario/DescribeSpec.js b/test/scenario/DescribeSpec.js index 6fcee731..0322b2d4 100644 --- a/test/scenario/DescribeSpec.js +++ b/test/scenario/DescribeSpec.js @@ -107,4 +107,14 @@ describe('angular.scenario.Describe', function() {      var b = new angular.scenario.Describe();      expect(a.id).toNotEqual(b.id);    }); + +  it('should create uniqueIds for each spec', function() { +    var d = new angular.scenario.Describe(); +    d.it('fake', function() {}); +    d.it('fake', function() {}); + +    expect(d.its[0].id).toBeDefined(); +    expect(d.its[1].id).toBeDefined(); +    expect(d.its[0].id).not.toEqual(d.its[1].id); +  });  }); diff --git a/test/scenario/ObjectModelSpec.js b/test/scenario/ObjectModelSpec.js index 8b83a52f..cb84b33f 100644 --- a/test/scenario/ObjectModelSpec.js +++ b/test/scenario/ObjectModelSpec.js @@ -3,18 +3,36 @@ describe('angular.scenario.ObjectModel', function() {    var runner;    var spec, step; -  beforeEach(function() { -    spec = { -      name: 'test spec', +  function buildSpec(id, name, definitions) { +    var spec = { +      id: id, +      name: name,        definition: { -        id: 10, -        name: 'describe 1' +        name: definitions.shift()        }      }; -    step = { -      name: 'test step', -      line: function() { return ''; } +    var currentDef = spec.definition; + +    forEach(definitions, function(defName) { +      currentDef.parent = { +        name: defName +      }; +      currentDef = currentDef.parent; +    }); + +    return spec; +  } + +  function buildStep(name, line) { +    return { +      name: name || 'test step', +      line: function() { return line || ''; }      }; +  } + +  beforeEach(function() { +    spec = buildSpec(1, 'test spec', ['describe 1']); +    step = buildStep();      runner = new angular.scenario.testing.MockRunner();      model = new angular.scenario.ObjectModel(runner);    }); @@ -27,23 +45,28 @@ describe('angular.scenario.ObjectModel', function() {    });    it('should add spec and create describe blocks on SpecBegin event', function() { -    runner.emit('SpecBegin', { -      name: 'test spec', -      definition: { -        id: 10, -        name: 'describe 2', -        parent: { -          id: 12, -          name: 'describe 1' -        } -      } -    }); +    runner.emit('SpecBegin', buildSpec(1, 'test spec', ['describe 2', 'describe 1']));      expect(model.value.children['describe 1']).toBeDefined();      expect(model.value.children['describe 1'].children['describe 2']).toBeDefined();      expect(model.value.children['describe 1'].children['describe 2'].specs['test spec']).toBeDefined();    }); +  it('should set fullDefinitionName on SpecBegin event', function() { +    runner.emit('SpecBegin', buildSpec(1, 'fake spec', ['describe 2'])); +    var spec = model.getSpec(1); + +    expect(spec.fullDefinitionName).toBeDefined(); +    expect(spec.fullDefinitionName).toEqual('describe 2'); +  }); + +  it('should set fullDefinitionName on SpecBegin event (join more names by space)', function() { +    runner.emit('SpecBegin', buildSpec(1, 'fake spec', ['describe 2', 'describe 1'])); +    var spec = model.getSpec(1); + +    expect(spec.fullDefinitionName).toEqual('describe 1 describe 2'); +  }); +    it('should add step to spec on StepBegin', function() {      runner.emit('SpecBegin', spec);      runner.emit('StepBegin', spec, step); @@ -109,4 +132,200 @@ describe('angular.scenario.ObjectModel', function() {      expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('error');    }); + +  describe('events', function() { +    var Spec = angular.scenario.ObjectModel.Spec, +        Step = angular.scenario.ObjectModel.Step, +        callback; + +    beforeEach(function() { +      callback = jasmine.createSpy('listener'); +    }); + +    it('should provide method for registering a listener', function() { +      expect(model.on).toBeDefined(); +      expect(model.on instanceof Function).toBe(true); +    }); + +    it('should forward SpecBegin event', function() { +      model.on('SpecBegin', callback); +      runner.emit('SpecBegin', spec); + +      expect(callback).toHaveBeenCalled(); +    }); + +    it('should forward SpecBegin event with ObjectModel.Spec as a param', function() { +      model.on('SpecBegin', callback); +      runner.emit('SpecBegin', spec); + +      expect(callback.mostRecentCall.args[0] instanceof Spec).toBe(true); +      expect(callback.mostRecentCall.args[0].name).toEqual(spec.name); +    }); + +    it('should forward SpecError event', function() { +      model.on('SpecError', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('SpecError', spec, {}); + +      expect(callback).toHaveBeenCalled(); +    }); + +    it('should forward SpecError event with ObjectModel.Spec and error as a params', function() { +      var error = {}; +      model.on('SpecError', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('SpecError', spec, error); + +      var param = callback.mostRecentCall.args[0]; +      expect(param instanceof Spec).toBe(true); +      expect(param.name).toEqual(spec.name); +      expect(param.status).toEqual('error'); +      expect(param.error).toBe(error); +    }); + +    it('should forward SpecEnd event', function() { +      model.on('SpecEnd', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('SpecEnd', spec); + +      expect(callback).toHaveBeenCalled(); +    }); + +    it('should forward SpecEnd event with ObjectModel.Spec as a param', function() { +      model.on('SpecEnd', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('SpecEnd', spec); + +      expect(callback.mostRecentCall.args[0] instanceof Spec).toBe(true); +      expect(callback.mostRecentCall.args[0].name).toEqual(spec.name); +    }); + +    it('should forward StepBegin event', function() { +      model.on('StepBegin', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('StepBegin', spec, step); + +      expect(callback).toHaveBeenCalled(); +    }); + +    it('should forward StepBegin event with Spec and Step as params', function() { +      model.on('StepBegin', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('StepBegin', spec, step); + +      var params = callback.mostRecentCall.args; +      expect(params[0] instanceof Spec).toBe(true); +      expect(params[0].name).toEqual(spec.name); +      expect(params[1] instanceof Step).toBe(true); +    }); + +    it('should forward StepError event', function() { +      model.on('StepError', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('StepBegin', spec, step); +      runner.emit('StepError', spec, step, {}); + +      expect(callback).toHaveBeenCalled(); +    }); + +    it('should forward StepError event with Spec, Step and error as params', function() { +      var error = {}; +      model.on('StepError', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('StepBegin', spec, step); +      runner.emit('StepError', spec, step, error); + +      var params = callback.mostRecentCall.args; +      expect(params[0] instanceof Spec).toBe(true); +      expect(params[0].name).toEqual(spec.name); +      expect(params[1] instanceof Step).toBe(true); +      expect(params[1].status).toEqual('error'); +      expect(params[2]).toBe(error); +    }); + +    it('should forward StepFailure event', function() { +      model.on('StepFailure', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('StepBegin', spec, step); +      runner.emit('StepFailure', spec, step, {}); + +      expect(callback).toHaveBeenCalled(); +    }); + +    it('should forward StepFailure event with Spec, Step and error as params', function() { +      var error = {}; +      model.on('StepFailure', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('StepBegin', spec, step); +      runner.emit('StepFailure', spec, step, error); + +      var params = callback.mostRecentCall.args; +      expect(params[0] instanceof Spec).toBe(true); +      expect(params[0].name).toEqual(spec.name); +      expect(params[1] instanceof Step).toBe(true); +      expect(params[1].status).toEqual('failure'); +      expect(params[2]).toBe(error); +    }); + +    it('should forward StepEnd event', function() { +      model.on('StepEnd', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('StepBegin', spec, step); +      runner.emit('StepEnd', spec, step); + +      expect(callback).toHaveBeenCalled(); +    }); + +    it('should forward StepEnd event with Spec and Step as params', function() { +      model.on('StepEnd', callback); +      runner.emit('SpecBegin', spec); +      runner.emit('StepBegin', spec, step); +      runner.emit('StepEnd', spec, step); + +      var params = callback.mostRecentCall.args; +      expect(params[0] instanceof Spec).toBe(true); +      expect(params[0].name).toEqual(spec.name); +      expect(params[1] instanceof Step).toBe(true); +    }); + +    it('should forward RunnerEnd event', function() { +      model.on('RunnerEnd', callback); +      runner.emit('RunnerEnd'); +      expect(callback).toHaveBeenCalled(); +    }); + +    it('should set error of first failure', function() { +      var error = 'first-error', +          step2 = buildStep(); + +      model.on('SpecEnd', function(spec) { +        expect(spec.error).toBeDefined(); +        expect(spec.error).toBe(error); +      }); + +      runner.emit('SpecBegin', spec); +      runner.emit('StepBegin', spec, step); +      runner.emit('StepFailure', spec, step, error); +      runner.emit('StepBegin', spec, step2); +      runner.emit('StepFailure', spec, step2, 'second-error'); +      runner.emit('SpecEnd', spec); +    }); + +    it('should set line number of first failure', function() { +      var step = buildStep('fake', 'first-line'), +          step2 = buildStep('fake2', 'second-line'); + +      model.on('SpecEnd', function(spec) { +        expect(spec.line).toBeDefined(); +        expect(spec.line).toBe('first-line'); +      }); + +      runner.emit('SpecBegin', spec); +      runner.emit('StepBegin', spec, step); +      runner.emit('StepFailure', spec, step, null); +      runner.emit('StepBegin', spec, step2); +      runner.emit('StepFailure', spec, step2, null); +      runner.emit('SpecEnd', spec); +    }); +  });  }); diff --git a/test/scenario/output/HtmlSpec.js b/test/scenario/output/HtmlSpec.js index f973397e..6694bb45 100644 --- a/test/scenario/output/HtmlSpec.js +++ b/test/scenario/output/HtmlSpec.js @@ -1,5 +1,5 @@  describe('angular.scenario.output.html', function() { -  var runner, spec, listeners; +  var runner, model, spec, listeners;    var ui, context;    beforeEach(function() { @@ -22,8 +22,9 @@ describe('angular.scenario.output.html', function() {        line: function() { return 'unknown:-1'; }      };      runner = new angular.scenario.testing.MockRunner(); +    model = new angular.scenario.ObjectModel(runner);      context = _jQuery("<div></div>"); -    ui = angular.scenario.output.html(context, runner); +    ui = angular.scenario.output.html(context, runner, model);    });    it('should create nested describe context', function() { diff --git a/test/scenario/output/jsonSpec.js b/test/scenario/output/jsonSpec.js index afc74a21..2c56b297 100644 --- a/test/scenario/output/jsonSpec.js +++ b/test/scenario/output/jsonSpec.js @@ -1,13 +1,14 @@  describe('angular.scenario.output.json', function() {    var output, context; -  var runner, $window; +  var runner, model, $window;    var spec, step;    beforeEach(function() {      $window = {};      context = _jQuery('<div></div>');      runner = new angular.scenario.testing.MockRunner(); -    output = angular.scenario.output.json(context, runner); +    model = new angular.scenario.ObjectModel(runner); +    output = angular.scenario.output.json(context, runner, model);      spec = {        name: 'test spec',        definition: { diff --git a/test/scenario/output/objectSpec.js b/test/scenario/output/objectSpec.js index 73c3dcf9..9fc2f7d4 100644 --- a/test/scenario/output/objectSpec.js +++ b/test/scenario/output/objectSpec.js @@ -1,13 +1,14 @@  describe('angular.scenario.output.object', function() {    var output; -  var runner, $window; +  var runner, model, $window;    var spec, step;    beforeEach(function() {      $window = {};      runner = new angular.scenario.testing.MockRunner(); +    model = new angular.scenario.ObjectModel(runner);      runner.$window = $window; -    output = angular.scenario.output.object(null, runner); +    output = angular.scenario.output.object(null, runner, model);      spec = {        name: 'test spec',        definition: { diff --git a/test/scenario/output/xmlSpec.js b/test/scenario/output/xmlSpec.js index fbfabcc4..a0e92639 100644 --- a/test/scenario/output/xmlSpec.js +++ b/test/scenario/output/xmlSpec.js @@ -1,13 +1,14 @@  describe('angular.scenario.output.json', function() {    var output, context; -  var runner, $window; +  var runner, model, $window;    var spec, step;    beforeEach(function() {      $window = {};      context = _jQuery('<div></div>');      runner = new angular.scenario.testing.MockRunner(); -    output = angular.scenario.output.xml(context, runner); +    model = new angular.scenario.ObjectModel(runner); +    output = angular.scenario.output.xml(context, runner, model);      spec = {        name: 'test spec',        definition: { | 
