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 --- 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 +- 13 files changed, 376 insertions(+), 69 deletions(-) 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 (limited to 'src') 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