diff --git a/CHANGELOG.md b/CHANGELOG.md index daa7537d78a6..1856124a6bfc 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 fff9ce1c53f5..31307c4fcb1f 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 5270eb16af47..7d69ee846a1f 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/static/settings.html b/docs/examples/settings.html similarity index 100% rename from docs/static/settings.html rename to docs/examples/settings.html diff --git a/docs/static/welcome.html b/docs/examples/welcome.html similarity index 100% rename from docs/static/welcome.html rename to docs/examples/welcome.html diff --git a/docs/src/gen-docs.js b/docs/src/gen-docs.js index 83e339425b3b..464916b1a718 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 bc244d5d9259..fcc70431f73b 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 3251b9cdbb55..cf54e1a3aa94 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 7129c2281590..2dd776db4517 100644 --- a/example/personalLog/scenario/runner.html +++ b/example/personalLog/scenario/runner.html @@ -2,7 +2,7 @@ Personal Log Scenario Runner - + diff --git a/example/personalLog/test/personalLogSpec.js b/example/personalLog/test/personalLogSpec.js index 7502ff2147be..3e6935a3d9be 100644 --- a/example/personalLog/test/personalLogSpec.js +++ b/example/personalLog/test/personalLogSpec.js @@ -11,7 +11,7 @@ describe('example.personalLog.LogCtrl', function() { beforeEach(function() { logCtrl = createNotesCtrl(); }); - + it('should initialize notes with an empty array', function() { expect(logCtrl.logs).toEqual([]); @@ -28,7 +28,7 @@ describe('example.personalLog.LogCtrl', function() { it('should add newMsg to logs as a log entry', function() { logCtrl.newMsg = 'first log message'; logCtrl.addLog(); - + expect(logCtrl.logs.length).toBe(1); expect(logCtrl.logs[0].msg).toBe('first log message'); diff --git a/jsTestDriver-scenario.conf b/jsTestDriver-scenario.conf new file mode 100644 index 000000000000..1ad7d32f2250 --- /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 204594d4bf01..901803b73046 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 f5f76fde477a..78cd7e57d7dd 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 f715b8e5f4ac..fa3ccf23b937 100644 --- a/scenario/Runner.html +++ b/scenario/Runner.html @@ -1,7 +1,7 @@ - + diff --git a/scenario/datastore-scenarios.js b/scenario/datastore-scenarios.js index 6038070be69b..a844ac5377c7 100644 --- a/scenario/datastore-scenarios.js +++ b/scenario/datastore-scenarios.js @@ -1,8 +1,8 @@ angular.scenarioDef.datastore = { $before:[ - {Given:"dataset", + {Given:"dataset", dataset:{ - Book:[{$id:'moby', name:"Moby Dick"}, + Book:[{$id:'moby', name:"Moby Dick"}, {$id:'gadsby', name:'Great Gadsby'}] } }, @@ -10,10 +10,10 @@ angular.scenarioDef.datastore = { ], checkLoadBook:[ {Then:"drainRequestQueue"}, - + {Then:"text", at:"{{book.$id}}", should_be:"moby"}, {Then:"text", at:"li[$index=0] {{book.name}}", should_be:"Great Gahdsby"}, {Then:"text", at:"li[$index=0] {{book.name}}", should_be:"Moby Dick"}, - + ] }; diff --git a/server-scenario.sh b/server-scenario.sh new file mode 100755 index 000000000000..3f7c42d65b87 --- /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/Browser.js b/src/Browser.js index 554397626031..b10c43cf34ca 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -7,10 +7,14 @@ var XHR = window.XMLHttpRequest || function () { try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} throw new Error("This browser does not support XMLHttpRequest."); }; + +// default xhr headers var XHR_HEADERS = { - "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json, text/plain, */*", - "X-Requested-With": "XMLHttpRequest" + DEFAULT: { + "Accept": "application/json, text/plain, */*", + "X-Requested-With": "XMLHttpRequest" + }, + POST: {'Content-Type': 'application/x-www-form-urlencoded'} }; /** @@ -103,8 +107,9 @@ function Browser(window, document, body, XHR, $log) { } else { var xhr = new XHR(); xhr.open(method, url, true); - forEach(extend(XHR_HEADERS, headers || {}), function(value, key){ - if (value) xhr.setRequestHeader(key, value); + forEach(extend({}, XHR_HEADERS.DEFAULT, XHR_HEADERS[uppercase(method)] || {}, headers || {}), + function(value, key) { + if (value) xhr.setRequestHeader(key, value); }); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { diff --git a/src/jstd-scenario-adapter/Adapter.js b/src/jstd-scenario-adapter/Adapter.js new file mode 100644 index 000000000000..fd9674e1a5fb --- /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 000000000000..ab8a71525852 --- /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 000000000000..6134fb010679 --- /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/Application.js b/src/scenario/Application.js index 988dae90d76a..b2c372de73d5 100644 --- a/src/scenario/Application.js +++ b/src/scenario/Application.js @@ -37,35 +37,6 @@ angular.scenario.Application.prototype.getWindow_ = function() { return contentWindow; }; -/** - * Checks that a URL would return a 2xx success status code. Callback is called - * with no arguments on success, or with an error on failure. - * - * Warning: This requires the server to be able to respond to HEAD requests - * and not modify the state of your application. - * - * @param {string} url Url to check - * @param {Function} callback function(error) that is called with result. - */ -angular.scenario.Application.prototype.checkUrlStatus_ = function(url, callback) { - var self = this; - _jQuery.ajax({ - url: url.replace(/#.*/, ''), //IE encodes and sends the url fragment, so we must strip it - type: 'HEAD', - complete: function(request) { - if (request.status < 200 || request.status >= 300) { - if (!request.status) { - callback.call(self, 'Sandbox Error: Cannot access ' + url); - } else { - callback.call(self, request.status + ' ' + request.statusText); - } - } else { - callback.call(self); - } - } - }); -}; - /** * Changes the location of the frame. * @@ -87,21 +58,16 @@ angular.scenario.Application.prototype.navigateTo = function(url, loadFn, errorF this.executeAction(loadFn); } else { frame.css('display', 'none').attr('src', 'about:blank'); - this.checkUrlStatus_(url, function(error) { - if (error) { - return errorFn(error); + this.context.find('#test-frames').append('