From 1abdc097b235366759a889bdcc68359653a9b8a3 Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Thu, 19 May 2011 17:33:25 +0200 Subject: JSTD adapter for running e2e tests Couple of changes into angular.scenario runner: - add autotest config (runs tests when document ready) - update ObjectModel (forwards events) - use only one ObjectModel instance for all outputters - expose error msg and line number in ObjectModel.Spec and ObjectModel.Step - fix generating spec.ids - fix 'html' output so that it does not mutate ObjectModel Couple of changes into docs / generator: - rename copy -> copyTpl - move docs/static into docs/examples (to avoid conflict with jstd proxy) Running all docs e2e tests: ======================================================== 1/ compile angular-scenario, jstd-scenario-adapter >> rake compile 2/ build docs >> rake docs 3/ start jstd server >> ./server-scenario.sh 4/ capture some browser 5/ run node server to serve static content >> node ../lib/nodeserver/server.js 6/ run tests >> ./test-scenario.sh --- CHANGELOG.md | 5 + Rakefile | 32 ++- docs/cookbook.deeplinking.ngdoc | 8 +- docs/examples/settings.html | 18 ++ docs/examples/welcome.html | 5 + docs/src/gen-docs.js | 26 +-- docs/src/templates/docs-scenario.html | 2 +- docs/src/writer.js | 2 +- docs/static/settings.html | 18 -- docs/static/welcome.html | 5 - example/personalLog/scenario/runner.html | 2 +- jsTestDriver-scenario.conf | 10 + jsTestDriver.conf | 2 + scenario/Runner-compiled.html | 2 +- scenario/Runner.html | 2 +- server-scenario.sh | 3 + src/jstd-scenario-adapter/Adapter.js | 175 ++++++++++++++++ src/jstd-scenario-adapter/angular.prefix | 24 +++ src/jstd-scenario-adapter/angular.suffix | 2 + src/scenario/Describe.js | 4 + src/scenario/ObjectModel.js | 132 ++++++++++-- src/scenario/Runner.js | 5 +- src/scenario/Scenario.js | 23 ++- src/scenario/angular-bootstrap.js | 8 +- src/scenario/angular.suffix | 9 +- src/scenario/output/Html.js | 48 +++-- src/scenario/output/Json.js | 6 +- src/scenario/output/Object.js | 4 +- src/scenario/output/Xml.js | 5 +- test-scenario.sh | 7 + test/jstd-scenario-adapter/AdapterSpecs.js | 320 +++++++++++++++++++++++++++++ test/scenario/DescribeSpec.js | 10 + test/scenario/ObjectModelSpec.js | 257 +++++++++++++++++++++-- test/scenario/output/HtmlSpec.js | 5 +- test/scenario/output/jsonSpec.js | 5 +- test/scenario/output/objectSpec.js | 5 +- test/scenario/output/xmlSpec.js | 5 +- 37 files changed, 1057 insertions(+), 144 deletions(-) create mode 100644 docs/examples/settings.html create mode 100644 docs/examples/welcome.html delete mode 100644 docs/static/settings.html delete mode 100644 docs/static/welcome.html create mode 100644 jsTestDriver-scenario.conf create mode 100755 server-scenario.sh create mode 100644 src/jstd-scenario-adapter/Adapter.js create mode 100644 src/jstd-scenario-adapter/angular.prefix create mode 100644 src/jstd-scenario-adapter/angular.suffix create mode 100755 test-scenario.sh create mode 100644 test/jstd-scenario-adapter/AdapterSpecs.js diff --git a/CHANGELOG.md b/CHANGELOG.md index daa7537d..1856124a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ # 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 diff --git a/Rakefile b/Rakefile index fff9ce1c..31307c4f 100644 --- a/Rakefile +++ b/Rakefile @@ -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} @@ -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/examples/settings.html b/docs/examples/settings.html new file mode 100644 index 00000000..2fa5dca8 --- /dev/null +++ b/docs/examples/settings.html @@ -0,0 +1,18 @@ + + + +
+ + + [ X ] +
+
+ [ add ] +
+ + + \ No newline at end of file diff --git a/docs/examples/welcome.html b/docs/examples/welcome.html new file mode 100644 index 00000000..b085123d --- /dev/null +++ b/docs/examples/welcome.html @@ -0,0 +1,5 @@ +Hello {{person.name}}, +
+ Your contact information: +
{{contact.type}}: {{contact.url|linky}}
+
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 @@ <angular/> Docs Scenario Runner - + 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/docs/static/settings.html b/docs/static/settings.html deleted file mode 100644 index 2fa5dca8..00000000 --- a/docs/static/settings.html +++ /dev/null @@ -1,18 +0,0 @@ - - - -
- - - [ X ] -
-
- [ add ] -
- - - \ No newline at end of file diff --git a/docs/static/welcome.html b/docs/static/welcome.html deleted file mode 100644 index b085123d..00000000 --- a/docs/static/welcome.html +++ /dev/null @@ -1,5 +0,0 @@ -Hello {{person.name}}, -
- Your contact information: -
{{contact.type}}: {{contact.url|linky}}
-
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 @@ Personal Log Scenario Runner - + 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 @@ - + 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 @@ - + 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: +
+    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/"}
+  
+ * + * 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 +
+    var jstdScenarioAdapter = {
+      relativeUrlPrefix: '/your-prefix/'
+    };
+  
+ * + * Whenever you use browser().navigateTo('relativeUrl') 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 browser().navigateTo('index.html') 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) { @@ -81,6 +115,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=} 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(' '); }; /** @@ -142,6 +208,19 @@ angular.scenario.ObjectModel.Spec.prototype.getLastStep = function() { return this.steps[this.steps.length-1]; }; +/** + * 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. * @@ -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('
').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('
').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( '' - ); + ' var $runner = new angular.scenario.Runner(window);' + + ''); })(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( '