aboutsummaryrefslogtreecommitdiffstats
path: root/src/scenario
diff options
context:
space:
mode:
Diffstat (limited to 'src/scenario')
-rw-r--r--src/scenario/Application.js61
-rw-r--r--src/scenario/Describe.js65
-rw-r--r--src/scenario/Future.js8
-rw-r--r--src/scenario/HtmlUI.js244
-rw-r--r--src/scenario/ObjectModel.js153
-rw-r--r--src/scenario/Runner.js128
-rw-r--r--src/scenario/Scenario.js77
-rw-r--r--src/scenario/SpecRunner.js57
-rw-r--r--src/scenario/angular-bootstrap.js (renamed from src/scenario/bootstrap.js)38
-rw-r--r--src/scenario/angular.prefix2
-rw-r--r--src/scenario/angular.suffix20
-rw-r--r--src/scenario/dsl.js56
-rw-r--r--src/scenario/matchers.js4
-rw-r--r--src/scenario/output/Html.js165
-rw-r--r--src/scenario/output/Json.js10
-rw-r--r--src/scenario/output/Object.js6
-rw-r--r--src/scenario/output/Xml.js48
17 files changed, 747 insertions, 395 deletions
diff --git a/src/scenario/Application.js b/src/scenario/Application.js
index 4ee0dd03..e2d34551 100644
--- a/src/scenario/Application.js
+++ b/src/scenario/Application.js
@@ -1,51 +1,84 @@
/**
* Represents the application currently being tested and abstracts usage
* of iframes or separate windows.
+ *
+ * @param {Object} context jQuery wrapper around HTML context.
*/
angular.scenario.Application = function(context) {
this.context = context;
- context.append('<h2>Current URL: <a href="about:blank">None</a></h2>');
+ context.append(
+ '<h2>Current URL: <a href="about:blank">None</a></h2>' +
+ '<div id="test-frames"></div>'
+ );
};
/**
* Gets the jQuery collection of frames. Don't use this directly because
* frames may go stale.
*
+ * @private
* @return {Object} jQuery collection
*/
-angular.scenario.Application.prototype.getFrame = function() {
- return this.context.find('> iframe');
+angular.scenario.Application.prototype.getFrame_ = function() {
+ return this.context.find('#test-frames iframe:last');
};
/**
- * Gets the window of the test runner frame. Always favor executeAction()
+ * Gets the window of the test runner frame. Always favor executeAction()
* instead of this method since it prevents you from getting a stale window.
*
+ * @private
* @return {Object} the window of the frame
*/
-angular.scenario.Application.prototype.getWindow = function() {
- var contentWindow = this.getFrame().attr('contentWindow');
+angular.scenario.Application.prototype.getWindow_ = function() {
+ var contentWindow = this.getFrame_().attr('contentWindow');
if (!contentWindow)
- throw 'No window available because frame not loaded.';
+ throw 'Frame window is not accessible.';
return contentWindow;
};
/**
* Changes the location of the frame.
+ *
+ * @param {string} url The URL. If it begins with a # then only the
+ * hash of the page is changed.
+ * @param {Function} onloadFn function($window, $document)
*/
angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) {
- this.getFrame().remove();
- this.context.append('<iframe src=""></iframe>');
+ var self = this;
+ var frame = this.getFrame_();
+ if (url.charAt(0) === '#') {
+ url = frame.attr('src').split('#')[0] + url;
+ frame.attr('src', url);
+ this.executeAction(onloadFn);
+ } else {
+ frame.css('display', 'none').attr('src', 'about:blank');
+ this.context.find('#test-frames').append('<iframe>');
+ frame = this.getFrame_();
+ frame.load(function() {
+ self.executeAction(onloadFn);
+ frame.unbind();
+ }).attr('src', url);
+ }
this.context.find('> h2 a').attr('href', url).text(url);
- this.getFrame().attr('src', url).load(onloadFn);
};
/**
- * Executes a function in the context of the tested application.
+ * Executes a function in the context of the tested application. Will wait
+ * for all pending angular xhr requests before executing.
*
- * @param {Function} The callback to execute. function($window, $document)
+ * @param {Function} action The callback to execute. function($window, $document)
+ * $document is a jQuery wrapped document.
*/
angular.scenario.Application.prototype.executeAction = function(action) {
- var $window = this.getWindow();
- return action.call(this, $window, _jQuery($window.document));
+ var self = this;
+ var $window = this.getWindow_();
+ if (!$window.angular) {
+ return action.call(this, $window, _jQuery($window.document));
+ }
+ var $browser = $window.angular.service.$browser();
+ $browser.poll();
+ $browser.notifyWhenNoOutstandingRequests(function() {
+ action.call(self, $window, _jQuery($window.document));
+ });
};
diff --git a/src/scenario/Describe.js b/src/scenario/Describe.js
index f6a52f1e..69ed8238 100644
--- a/src/scenario/Describe.js
+++ b/src/scenario/Describe.js
@@ -1,8 +1,12 @@
/**
* The representation of define blocks. Don't used directly, instead use
* define() in your tests.
+ *
+ * @param {string} descName Name of the block
+ * @param {Object} parent describe or undefined if the root.
*/
angular.scenario.Describe = function(descName, parent) {
+ this.only = parent && parent.only;
this.beforeEachFns = [];
this.afterEachFns = [];
this.its = [];
@@ -10,7 +14,7 @@ angular.scenario.Describe = function(descName, parent) {
this.name = descName;
this.parent = parent;
this.id = angular.scenario.Describe.id++;
-
+
/**
* Calls all before functions.
*/
@@ -36,7 +40,7 @@ angular.scenario.Describe.id = 0;
/**
* Defines a block to execute before each it or nested describe.
*
- * @param {Function} Body of the block.
+ * @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.beforeEach = function(body) {
this.beforeEachFns.push(body);
@@ -45,7 +49,7 @@ angular.scenario.Describe.prototype.beforeEach = function(body) {
/**
* Defines a block to execute after each it or nested describe.
*
- * @param {Function} Body of the block.
+ * @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.afterEach = function(body) {
this.afterEachFns.push(body);
@@ -54,8 +58,8 @@ angular.scenario.Describe.prototype.afterEach = function(body) {
/**
* Creates a new describe block that's a child of this one.
*
- * @param {String} Name of the block. Appended to the parent block's name.
- * @param {Function} Body of the block.
+ * @param {string} name Name of the block. Appended to the parent block's name.
+ * @param {Function} body Body of the block.
*/
angular.scenario.Describe.prototype.describe = function(name, body) {
var child = new angular.scenario.Describe(name, this);
@@ -64,6 +68,19 @@ angular.scenario.Describe.prototype.describe = function(name, body) {
};
/**
+ * Same as describe() but makes ddescribe blocks the only to run.
+ *
+ * @param {string} name Name of the test.
+ * @param {Function} body Body of the block.
+ */
+angular.scenario.Describe.prototype.ddescribe = function(name, body) {
+ var child = new angular.scenario.Describe(name, this);
+ child.only = true;
+ this.children.push(child);
+ body.call(child);
+};
+
+/**
* Use to disable a describe block.
*/
angular.scenario.Describe.prototype.xdescribe = angular.noop;
@@ -71,21 +88,32 @@ angular.scenario.Describe.prototype.xdescribe = angular.noop;
/**
* Defines a test.
*
- * @param {String} Name of the test.
- * @param {Function} Body of the block.
+ * @param {string} name Name of the test.
+ * @param {Function} vody Body of the block.
*/
angular.scenario.Describe.prototype.it = function(name, body) {
- var self = this;
this.its.push({
definition: this,
+ only: this.only,
name: name,
- before: self.setupBefore,
+ before: this.setupBefore,
body: body,
- after: self.setupAfter
+ after: this.setupAfter
});
};
/**
+ * Same as it() but makes iit tests the only test to run.
+ *
+ * @param {string} name Name of the test.
+ * @param {Function} body Body of the block.
+ */
+angular.scenario.Describe.prototype.iit = function(name, body) {
+ this.it.apply(this, arguments);
+ this.its[this.its.length-1].only = true;
+};
+
+/**
* Use to disable a test block.
*/
angular.scenario.Describe.prototype.xit = angular.noop;
@@ -93,6 +121,15 @@ angular.scenario.Describe.prototype.xit = angular.noop;
/**
* Gets an array of functions representing all the tests (recursively).
* that can be executed with SpecRunner's.
+ *
+ * @return {Array<Object>} Array of it blocks {
+ * definition : Object // parent Describe
+ * only: boolean
+ * name: string
+ * before: Function
+ * body: Function
+ * after: Function
+ * }
*/
angular.scenario.Describe.prototype.getSpecs = function() {
var specs = arguments[0] || [];
@@ -102,5 +139,11 @@ angular.scenario.Describe.prototype.getSpecs = function() {
angular.foreach(this.its, function(it) {
specs.push(it);
});
- return specs;
+ var only = [];
+ angular.foreach(specs, function(it) {
+ if (it.only) {
+ only.push(it);
+ }
+ });
+ return (only.length && only) || specs;
};
diff --git a/src/scenario/Future.js b/src/scenario/Future.js
index 8853aa3f..f545c721 100644
--- a/src/scenario/Future.js
+++ b/src/scenario/Future.js
@@ -1,9 +1,9 @@
/**
* A future action in a spec.
*
- * @param {String} name of the future action
+ * @param {string} name of the future action
* @param {Function} future callback(error, result)
- * @param {String} Optional. function that returns the file/line number.
+ * @param {Function} Optional. function that returns the file/line number.
*/
angular.scenario.Future = function(name, behavior, line) {
this.name = name;
@@ -17,7 +17,7 @@ angular.scenario.Future = function(name, behavior, line) {
/**
* Executes the behavior of the closure.
*
- * @param {Function} Callback function(error, result)
+ * @param {Function} doneFn Callback function(error, result)
*/
angular.scenario.Future.prototype.execute = function(doneFn) {
var self = this;
@@ -37,6 +37,8 @@ angular.scenario.Future.prototype.execute = function(doneFn) {
/**
* Configures the future to convert it's final with a function fn(value)
+ *
+ * @param {Function} fn function(value) that returns the parsed value
*/
angular.scenario.Future.prototype.parsedWith = function(fn) {
this.parser = fn;
diff --git a/src/scenario/HtmlUI.js b/src/scenario/HtmlUI.js
deleted file mode 100644
index 78fe8c33..00000000
--- a/src/scenario/HtmlUI.js
+++ /dev/null
@@ -1,244 +0,0 @@
-/**
- * User Interface for the Scenario Runner.
- *
- * @param {Object} The jQuery UI object for the UI.
- */
-angular.scenario.ui.Html = function(context) {
- this.context = context;
- context.append(
- '<div id="header">' +
- ' <h1><span class="angular">&lt;angular/&gt;</span>: Scenario Test Runner</h1>' +
- ' <ul id="status-legend" class="status-display">' +
- ' <li class="status-error">0 Errors</li>' +
- ' <li class="status-failure">0 Failures</li>' +
- ' <li class="status-success">0 Passed</li>' +
- ' </ul>' +
- '</div>' +
- '<div id="specs">' +
- ' <div class="test-children"></div>' +
- '</div>'
- );
-};
-
-/**
- * The severity order of an error.
- */
-angular.scenario.ui.Html.SEVERITY = ['pending', 'success', 'failure', 'error'];
-
-/**
- * Adds a new spec to the UI.
- *
- * @param {Object} The spec object created by the Describe object.
- */
-angular.scenario.ui.Html.prototype.addSpec = function(spec) {
- var self = this;
- var specContext = this.findContext(spec.definition);
- specContext.find('> .tests').append(
- '<li class="status-pending test-it"></li>'
- );
- specContext = specContext.find('> .tests li:last');
- return new angular.scenario.ui.Html.Spec(specContext, spec.name,
- function(status) {
- status = self.context.find('#status-legend .status-' + status);
- var parts = status.text().split(' ');
- var value = (parts[0] * 1) + 1;
- status.text(value + ' ' + parts[1]);
- }
- );
-};
-
-/**
- * Finds the context of a spec block defined by the passed definition.
- *
- * @param {Object} The definition created by the Describe object.
- */
-angular.scenario.ui.Html.prototype.findContext = function(definition) {
- var self = this;
- var path = [];
- var currentContext = this.context.find('#specs');
- var currentDefinition = definition;
- while (currentDefinition && currentDefinition.name) {
- path.unshift(currentDefinition);
- currentDefinition = currentDefinition.parent;
- }
- angular.foreach(path, function(defn) {
- var id = 'describe-' + defn.id;
- if (!self.context.find('#' + id).length) {
- currentContext.find('> .test-children').append(
- '<div class="test-describe" id="' + id + '">' +
- ' <h2></h2>' +
- ' <div class="test-children"></div>' +
- ' <ul class="tests"></ul>' +
- '</div>'
- );
- self.context.find('#' + id).find('> h2').text('describe: ' + defn.name);
- }
- currentContext = self.context.find('#' + id);
- });
- return this.context.find('#describe-' + definition.id);
-};
-
-/**
- * A spec block in the UI.
- *
- * @param {Object} The jQuery object for the context of the spec.
- * @param {String} The name of the spec.
- * @param {Function} Callback function(status) to call when complete.
- */
-angular.scenario.ui.Html.Spec = function(context, name, doneFn) {
- this.status = 'pending';
- this.context = context;
- this.startTime = new Date().getTime();
- this.doneFn = doneFn;
- context.append(
- '<div class="test-info">' +
- ' <p class="test-title">' +
- ' <span class="timer-result"></span>' +
- ' <span class="test-name"></span>' +
- ' </p>' +
- '</div>' +
- '<div class="scrollpane">' +
- ' <ol class="test-actions">' +
- ' </ol>' +
- '</div>'
- );
- context.find('> .test-info').click(function() {
- var scrollpane = context.find('> .scrollpane');
- var actions = scrollpane.find('> .test-actions');
- var name = context.find('> .test-info .test-name');
- if (actions.find(':visible').length) {
- actions.hide();
- name.removeClass('open').addClass('closed');
- } else {
- actions.show();
- scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
- name.removeClass('closed').addClass('open');
- }
- });
- context.find('> .test-info .test-name').text('it ' + name);
-};
-
-/**
- * Adds a new Step to this spec and returns it.
- *
- * @param {String} The name of the step.
- * @param {Function} function() that returns a string with the file/line number
- * where the step was added from.
- */
-angular.scenario.ui.Html.Spec.prototype.addStep = function(name, location) {
- this.context.find('> .scrollpane .test-actions').append('<li class="status-pending"></li>');
- var stepContext = this.context.find('> .scrollpane .test-actions li:last');
- var self = this;
- return new angular.scenario.ui.Html.Step(stepContext, name, location, function(status) {
- if (indexOf(angular.scenario.ui.Html.SEVERITY, status) >
- indexOf(angular.scenario.ui.Html.SEVERITY, self.status)) {
- self.status = status;
- }
- var scrollpane = self.context.find('> .scrollpane');
- scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
- });
-};
-
-/**
- * Completes the spec and sets the timer value.
- */
-angular.scenario.ui.Html.Spec.prototype.complete = function() {
- this.context.removeClass('status-pending');
- var endTime = new Date().getTime();
- this.context.find("> .test-info .timer-result").
- text((endTime - this.startTime) + "ms");
- if (this.status === 'success') {
- this.context.find('> .test-info .test-name').addClass('closed');
- this.context.find('> .scrollpane .test-actions').hide();
- }
-};
-
-/**
- * Finishes the spec, possibly with an error.
- *
- * @param {Object} An optional error
- */
-angular.scenario.ui.Html.Spec.prototype.finish = function() {
- this.complete();
- this.context.addClass('status-' + this.status);
- this.doneFn(this.status);
-};
-
-/**
- * Finishes the spec, but with a Fatal Error.
- *
- * @param {Object} Required error
- */
-angular.scenario.ui.Html.Spec.prototype.error = function(error) {
- this.status = 'error';
- this.context.append('<pre></pre>');
- this.context.find('> pre').text(formatException(error));
- this.finish();
-};
-
-/**
- * A single step inside an it block (or a before/after function).
- *
- * @param {Object} The jQuery object for the context of the step.
- * @param {String} The name of the step.
- * @param {Function} function() that returns file/line number of step.
- * @param {Function} Callback function(status) to call when complete.
- */
-angular.scenario.ui.Html.Step = function(context, name, location, doneFn) {
- this.context = context;
- this.name = name;
- this.location = location;
- this.startTime = new Date().getTime();
- this.doneFn = doneFn;
- context.append(
- '<div class="timer-result"></div>' +
- '<div class="test-title"></div>'
- );
- context.find('> .test-title').text(name);
- var scrollpane = context.parents('.scrollpane');
- scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
-};
-
-/**
- * Completes the step and sets the timer value.
- */
-angular.scenario.ui.Html.Step.prototype.complete = function(error) {
- this.context.removeClass('status-pending');
- var endTime = new Date().getTime();
- this.context.find(".timer-result").
- text((endTime - this.startTime) + "ms");
- if (error) {
- if (!this.context.find('.test-title pre').length) {
- this.context.find('.test-title').append('<pre></pre>');
- }
- var message = _jQuery.trim(this.location() + '\n\n' + formatException(error));
- this.context.find('.test-title pre').text(message);
- }
-};
-
-/**
- * Finishes the step, possibly with an error.
- *
- * @param {Object} An optional error
- */
-angular.scenario.ui.Html.Step.prototype.finish = function(error) {
- this.complete(error);
- if (error) {
- this.context.addClass('status-failure');
- this.doneFn('failure');
- } else {
- this.context.addClass('status-success');
- this.doneFn('success');
- }
-};
-
-/**
- * Finishes the step, but with a Fatal Error.
- *
- * @param {Object} Required error
- */
-angular.scenario.ui.Html.Step.prototype.error = function(error) {
- this.complete(error);
- this.context.addClass('status-error');
- this.doneFn('error');
-};
diff --git a/src/scenario/ObjectModel.js b/src/scenario/ObjectModel.js
new file mode 100644
index 00000000..e9125e03
--- /dev/null
+++ b/src/scenario/ObjectModel.js
@@ -0,0 +1,153 @@
+/**
+ * Maintains an object tree from the runner events.
+ *
+ * @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
+ * so the HTML output doesn't need to do spec model.getSpec(spec.id)
+ * silliness.
+ */
+angular.scenario.ObjectModel = function(runner) {
+ var self = this;
+
+ this.specMap = {};
+ this.value = {
+ name: '',
+ children: {}
+ };
+
+ runner.on('SpecBegin', function(spec) {
+ var block = self.value;
+ angular.foreach(self.getDefinitionPath(spec), function(def) {
+ if (!block.children[def.name]) {
+ block.children[def.name] = {
+ id: def.id,
+ name: def.name,
+ children: {},
+ specs: {}
+ };
+ }
+ block = block.children[def.name];
+ });
+ self.specMap[spec.id] = block.specs[spec.name] =
+ new angular.scenario.ObjectModel.Spec(spec.id, spec.name);
+ });
+
+ runner.on('SpecError', function(spec, error) {
+ var it = self.getSpec(spec.id);
+ it.status = 'error';
+ it.error = error;
+ });
+
+ runner.on('SpecEnd', function(spec) {
+ var it = self.getSpec(spec.id);
+ complete(it);
+ });
+
+ runner.on('StepBegin', function(spec, step) {
+ var it = self.getSpec(spec.id);
+ it.steps.push(new angular.scenario.ObjectModel.Step(step.name));
+ });
+
+ 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());
+ });
+
+ 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';
+ }
+ });
+
+ 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;
+ });
+
+ function complete(item) {
+ item.endTime = new Date().getTime();
+ item.duration = item.endTime - item.startTime;
+ item.status = item.status || 'success';
+ }
+};
+
+/**
+ * Computes the path of definition describe blocks that wrap around
+ * this spec.
+ *
+ * @param spec Spec to compute the path for.
+ * @return {Array<Describe>} The describe block path
+ */
+angular.scenario.ObjectModel.prototype.getDefinitionPath = function(spec) {
+ var path = [];
+ var currentDefinition = spec.definition;
+ while (currentDefinition && currentDefinition.name) {
+ path.unshift(currentDefinition);
+ currentDefinition = currentDefinition.parent;
+ }
+ return path;
+};
+
+/**
+ * Gets a spec by id.
+ *
+ * @param {string} The id of the spec to get the object for.
+ * @return {Object} the Spec instance
+ */
+angular.scenario.ObjectModel.prototype.getSpec = function(id) {
+ return this.specMap[id];
+};
+
+/**
+ * A single it block.
+ *
+ * @param {string} id Id of the spec
+ * @param {string} name Name of the spec
+ */
+angular.scenario.ObjectModel.Spec = function(id, name) {
+ this.id = id;
+ this.name = name;
+ this.startTime = new Date().getTime();
+ this.steps = [];
+};
+
+/**
+ * Adds a new step to the Spec.
+ *
+ * @param {string} step Name of the step (really name of the future)
+ * @return {Object} the added step
+ */
+angular.scenario.ObjectModel.Spec.prototype.addStep = function(name) {
+ var step = new angular.scenario.ObjectModel.Step(name);
+ this.steps.push(step);
+ return step;
+};
+
+/**
+ * Gets the most recent step.
+ *
+ * @return {Object} the step
+ */
+angular.scenario.ObjectModel.Spec.prototype.getLastStep = function() {
+ return this.steps[this.steps.length-1];
+};
+
+/**
+ * A single step inside a Spec.
+ *
+ * @param {string} step Name of the step
+ */
+angular.scenario.ObjectModel.Step = function(name) {
+ this.name = name;
+ this.startTime = new Date().getTime();
+};
diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js
index a8b23f83..f628eb04 100644
--- a/src/scenario/Runner.js
+++ b/src/scenario/Runner.js
@@ -2,13 +2,16 @@
* Runner for scenarios.
*/
angular.scenario.Runner = function($window) {
+ this.listeners = [];
this.$window = $window;
this.rootDescribe = new angular.scenario.Describe();
this.currentDescribe = this.rootDescribe;
this.api = {
it: this.it,
+ iit: this.iit,
xit: angular.noop,
describe: this.describe,
+ ddescribe: this.ddescribe,
xdescribe: angular.noop,
beforeEach: this.beforeEach,
afterEach: this.afterEach
@@ -19,10 +22,41 @@ angular.scenario.Runner = function($window) {
};
/**
+ * Emits an event which notifies listeners and passes extra
+ * arguments.
+ *
+ * @param {string} eventName Name of the event to fire.
+ */
+angular.scenario.Runner.prototype.emit = function(eventName) {
+ var self = this;
+ var args = Array.prototype.slice.call(arguments, 1);
+ eventName = eventName.toLowerCase();
+ if (!this.listeners[eventName])
+ return;
+ angular.foreach(this.listeners[eventName], function(listener) {
+ listener.apply(self, args);
+ });
+};
+
+/**
+ * Adds a listener for an event.
+ *
+ * @param {string} eventName The name of the event to add a handler for
+ * @param {string} listener The fn(...) that takes the extra arguments from emit()
+ */
+angular.scenario.Runner.prototype.on = function(eventName, listener) {
+ eventName = eventName.toLowerCase();
+ this.listeners[eventName] = this.listeners[eventName] || [];
+ this.listeners[eventName].push(listener);
+};
+
+/**
* Defines a describe block of a spec.
*
- * @param {String} Name of the block
- * @param {Function} Body of the block
+ * @see Describe.js
+ *
+ * @param {string} name Name of the block
+ * @param {Function} body Body of the block
*/
angular.scenario.Runner.prototype.describe = function(name, body) {
var self = this;
@@ -38,19 +72,56 @@ angular.scenario.Runner.prototype.describe = function(name, body) {
};
/**
+ * Same as describe, but makes ddescribe the only blocks to run.
+ *
+ * @see Describe.js
+ *
+ * @param {string} name Name of the block
+ * @param {Function} body Body of the block
+ */
+angular.scenario.Runner.prototype.ddescribe = function(name, body) {
+ var self = this;
+ this.currentDescribe.ddescribe(name, function() {
+ var parentDescribe = self.currentDescribe;
+ self.currentDescribe = this;
+ try {
+ body.call(this);
+ } finally {
+ self.currentDescribe = parentDescribe;
+ }
+ });
+};
+
+/**
* Defines a test in a describe block of a spec.
*
- * @param {String} Name of the block
- * @param {Function} Body of the block
+ * @see Describe.js
+ *
+ * @param {string} name Name of the block
+ * @param {Function} body Body of the block
*/
angular.scenario.Runner.prototype.it = function(name, body) {
this.currentDescribe.it(name, body);
};
/**
+ * Same as it, but makes iit tests the only tests to run.
+ *
+ * @see Describe.js
+ *
+ * @param {string} name Name of the block
+ * @param {Function} body Body of the block
+ */
+angular.scenario.Runner.prototype.iit = function(name, body) {
+ this.currentDescribe.iit(name, body);
+};
+
+/**
* Defines a function to be called before each it block in the describe
* (and before all nested describes).
*
+ * @see Describe.js
+ *
* @param {Function} Callback to execute
*/
angular.scenario.Runner.prototype.beforeEach = function(body) {
@@ -61,6 +132,8 @@ angular.scenario.Runner.prototype.beforeEach = function(body) {
* Defines a function to be called after each it block in the describe
* (and before all nested describes).
*
+ * @see Describe.js
+ *
* @param {Function} Callback to execute
*/
angular.scenario.Runner.prototype.afterEach = function(body) {
@@ -68,24 +141,29 @@ angular.scenario.Runner.prototype.afterEach = function(body) {
};
/**
- * Defines a function to be called before each it block in the describe
- * (and before all nested describes).
+ * Creates a new spec runner.
*
- * @param {Function} Callback to execute
+ * @private
+ * @param {Object} scope parent scope
+ */
+angular.scenario.Runner.prototype.createSpecRunner_ = function(scope) {
+ return scope.$new(angular.scenario.SpecRunner);
+};
+
+/**
+ * Runs all the loaded tests with the specified runner class on the
+ * provided application.
+ *
+ * @param {angular.scenario.Application} application App to remote control.
*/
-angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClass, specsDone) {
- var $root = angular.scope({}, angular.service);
+angular.scenario.Runner.prototype.run = function(application) {
var self = this;
- var specs = this.rootDescribe.getSpecs();
+ var $root = angular.scope(this);
$root.application = application;
- $root.ui = ui;
- $root.setTimeout = function() {
- return self.$window.setTimeout.apply(self.$window, arguments);
- };
- asyncForEach(specs, function(spec, specDone) {
+ this.emit('RunnerBegin');
+ asyncForEach(this.rootDescribe.getSpecs(), function(spec, specDone) {
var dslCache = {};
- var runner = angular.scope($root);
- runner.$become(specRunnerClass);
+ var runner = self.createSpecRunner_($root);
angular.foreach(angular.scenario.dsl, function(fn, key) {
dslCache[key] = fn.call($root);
});
@@ -105,16 +183,24 @@ angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClas
// Make these methods work on the current chain
scope.addFuture = function() {
Array.prototype.push.call(arguments, line);
- return specRunnerClass.prototype.addFuture.apply(scope, arguments);
+ return angular.scenario.SpecRunner.
+ prototype.addFuture.apply(scope, arguments);
};
scope.addFutureAction = function() {
Array.prototype.push.call(arguments, line);
- return specRunnerClass.prototype.addFutureAction.apply(scope, arguments);
+ return angular.scenario.SpecRunner.
+ prototype.addFutureAction.apply(scope, arguments);
};
return scope.dsl[key].apply(scope, arguments);
};
});
- runner.run(ui, spec, specDone);
- }, specsDone || angular.noop);
+ runner.run(spec, specDone);
+ },
+ function(error) {
+ if (error) {
+ self.emit('RunnerError', error);
+ }
+ self.emit('RunnerEnd');
+ });
};
diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js
index c00ed3dd..f2ebc640 100644
--- a/src/scenario/Scenario.js
+++ b/src/scenario/Scenario.js
@@ -6,8 +6,15 @@
// Public namespace
angular.scenario = angular.scenario || {};
-// Namespace for the UI
-angular.scenario.ui = angular.scenario.ui || {};
+/**
+ * Defines a new output format.
+ *
+ * @param {string} name the name of the new output format
+ * @param {Function} fn function(context, runner) that generates the output
+ */
+angular.scenario.output = angular.scenario.output || function(name, fn) {
+ angular.scenario.output[name] = fn;
+};
/**
* Defines a new DSL statement. If your factory function returns a Future
@@ -18,8 +25,8 @@ angular.scenario.ui = angular.scenario.ui || {};
* set on "this" in your statement function are available in the chained
* functions.
*
- * @param {String} The name of the statement
- * @param {Function} Factory function(application), return a function for
+ * @param {string} name The name of the statement
+ * @param {Function} fn Factory function(), return a function for
* the statement.
*/
angular.scenario.dsl = angular.scenario.dsl || function(name, fn) {
@@ -54,8 +61,8 @@ angular.scenario.dsl = angular.scenario.dsl || function(name, fn) {
* against. Your function should return a boolean. The future is automatically
* created for you.
*
- * @param {String} The name of the matcher
- * @param {Function} The matching function(expected).
+ * @param {string} name The name of the matcher
+ * @param {Function} fn The matching function(expected).
*/
angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
angular.scenario.matcher[name] = function(expected) {
@@ -79,13 +86,55 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
};
/**
+ * Initialization function for the scenario runner.
+ *
+ * @param {angular.scenario.Runner} $scenario The runner to setup
+ * @param {Object} config Config options
+ */
+function angularScenarioInit($scenario, config) {
+ var body = _jQuery(document.body);
+ var output = [];
+
+ if (config.scenario_output) {
+ output = config.scenario_output.split(',');
+ }
+
+ angular.foreach(angular.scenario.output, function(fn, name) {
+ if (!output.length || indexOf(output,name) != -1) {
+ var context = body.append('<div></div>').find('div:last');
+ context.attr('id', name);
+ fn.call({}, context, $scenario);
+ }
+ });
+
+ var appFrame = body.append('<div id="application"></div>').find('#application');
+ var application = new angular.scenario.Application(appFrame);
+
+ $scenario.on('RunnerEnd', function() {
+ appFrame.css('display', 'none');
+ appFrame.find('iframe').attr('src', 'about:blank');
+ });
+
+ $scenario.on('RunnerError', function(error) {
+ if (window.console) {
+ console.log(formatException(error));
+ } else {
+ // Do something for IE
+ alert(error);
+ }
+ });
+
+ $scenario.run(application);
+}
+
+/**
* Iterates through list with iterator function that must call the
* continueFunction to continute iterating.
*
- * @param {Array} list to iterate over
- * @param {Function} Callback function(value, continueFunction)
- * @param {Function} Callback function(error, result) called when iteration
- * finishes or an error occurs.
+ * @param {Array} list list to iterate over
+ * @param {Function} iterator Callback function(value, continueFunction)
+ * @param {Function} done Callback function(error, result) called when
+ * iteration finishes or an error occurs.
*/
function asyncForEach(list, iterator, done) {
var i = 0;
@@ -110,8 +159,8 @@ function asyncForEach(list, iterator, done) {
* Formats an exception into a string with the stack trace, but limits
* to a specific line length.
*
- * @param {Object} the exception to format, can be anything throwable
- * @param {Number} Optional. max lines of the stack trace to include
+ * @param {Object} error The exception to format, can be anything throwable
+ * @param {Number} maxStackLines Optional. max lines of the stack trace to include
* default is 5.
*/
function formatException(error, maxStackLines) {
@@ -134,6 +183,8 @@ function formatException(error, maxStackLines) {
*
* Note: this returns another function because accessing .stack is very
* expensive in Chrome.
+ *
+ * @param {Number} offset Number of stack lines to skip
*/
function callerFile(offset) {
var error = new Error();
@@ -161,7 +212,7 @@ function callerFile(offset) {
* not specified.
*
* @param {Object} Either a wrapped jQuery/jqLite node or a DOMElement
- * @param {String} Optional event type.
+ * @param {string} Optional event type.
*/
function browserTrigger(element, type) {
if (element && !element.nodeName) element = element[0];
diff --git a/src/scenario/SpecRunner.js b/src/scenario/SpecRunner.js
index 98ce4b53..fb7d5c02 100644
--- a/src/scenario/SpecRunner.js
+++ b/src/scenario/SpecRunner.js
@@ -15,14 +15,16 @@ angular.scenario.SpecRunner = function() {
* Executes a spec which is an it block with associated before/after functions
* based on the describe nesting.
*
- * @param {Object} An angular.scenario.UI implementation
- * @param {Object} A spec object
- * @param {Object} An angular.scenario.Application instance
+ * @param {Object} spec A spec object
+ * @param {Object} specDone An angular.scenario.Application instance
* @param {Function} Callback function that is called when the spec finshes.
*/
-angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
+angular.scenario.SpecRunner.prototype.run = function(spec, specDone) {
var self = this;
- var specUI = ui.addSpec(spec);
+ var count = 0;
+ this.spec = spec;
+
+ this.emit('SpecBegin', spec);
try {
spec.before.call(this);
@@ -30,7 +32,8 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
this.afterIndex = this.futures.length;
spec.after.call(this);
} catch (e) {
- specUI.error(e);
+ this.emit('SpecError', spec, e);
+ this.emit('SpecEnd', spec);
specDone();
return;
}
@@ -42,32 +45,40 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
self.error = true;
done(null, self.afterIndex);
};
-
- var spec = this;
+
asyncForEach(
this.futures,
function(future, futureDone) {
- var stepUI = specUI.addStep(future.name, future.line);
+ self.step = future;
+ self.emit('StepBegin', spec, future);
try {
future.execute(function(error) {
- stepUI.finish(error);
if (error) {
+ self.emit('StepFailure', spec, future, error);
+ self.emit('StepEnd', spec, future);
return handleError(error, futureDone);
}
- spec.$window.setTimeout( function() { futureDone(); }, 0);
+ self.emit('StepEnd', spec, future);
+ if ((count++) % 10 === 0) {
+ self.$window.setTimeout(function() { futureDone(); }, 0);
+ } else {
+ futureDone();
+ }
});
} catch (e) {
- stepUI.error(e);
+ self.emit('StepError', spec, future, e);
+ self.emit('StepEnd', spec, future);
handleError(e, futureDone);
}
},
function(e) {
if (e) {
- specUI.error(e);
- } else {
- specUI.finish();
+ self.emit('SpecError', spec, e);
}
- specDone();
+ self.emit('SpecEnd', spec);
+ // Call done in a timeout so exceptions don't recursively
+ // call this function
+ self.$window.setTimeout(function() { specDone(); }, 0);
}
);
};
@@ -77,9 +88,9 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
*
* Note: Do not pass line manually. It happens automatically.
*
- * @param {String} Name of the future
- * @param {Function} Behavior of the future
- * @param {Function} fn() that returns file/line number
+ * @param {string} name Name of the future
+ * @param {Function} behavior Behavior of the future
+ * @param {Function} line fn() that returns file/line number
*/
angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line) {
var future = new angular.scenario.Future(name, angular.bind(this, behavior), line);
@@ -92,14 +103,16 @@ angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line)
*
* Note: Do not pass line manually. It happens automatically.
*
- * @param {String} Name of the future
- * @param {Function} Behavior of the future
- * @param {Function} fn() that returns file/line number
+ * @param {string} name Name of the future
+ * @param {Function} behavior Behavior of the future
+ * @param {Function} line fn() that returns file/line number
*/
angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior, line) {
var self = this;
return this.addFuture(name, function(done) {
this.application.executeAction(function($window, $document) {
+
+ //TODO(esprehn): Refactor this so it doesn't need to be in here.
$document.elements = function(selector) {
var args = Array.prototype.slice.call(arguments, 1);
if (self.selector) {
diff --git a/src/scenario/bootstrap.js b/src/scenario/angular-bootstrap.js
index 4661bfb2..68dc393e 100644
--- a/src/scenario/bootstrap.js
+++ b/src/scenario/angular-bootstrap.js
@@ -1,6 +1,6 @@
(function(previousOnLoad){
var prefix = (function(){
- var filename = /(.*\/)bootstrap.js(#(.*))?/;
+ var filename = /(.*\/)angular-bootstrap.js(#(.*))?/;
var scripts = document.getElementsByTagName("script");
for(var j = 0; j < scripts.length; j++) {
var src = scripts[j].src;
@@ -23,50 +23,36 @@
try {
if (previousOnLoad) previousOnLoad();
} catch(e) {}
- _jQuery(document.body).append(
- '<div id="runner"></div>' +
- '<div id="frame"></div>'
- );
- var frame = _jQuery('#frame');
- var runner = _jQuery('#runner');
- var application = new angular.scenario.Application(frame);
- var ui = new angular.scenario.ui.Html(runner);
- $scenario.run(ui, application, angular.scenario.SpecRunner, function(error) {
- frame.remove();
- if (error) {
- if (window.console) {
- console.log(error.stack || error);
- } else {
- // Do something for IE
- alert(error);
- }
- }
- });
+ angularScenarioInit($scenario, angularJsConfig(document));
};
addCSS("../../css/angular-scenario.css");
addScript("../../lib/jquery/jquery-1.4.2.js");
document.write(
- '<script type="text/javascript">' +
- 'var _jQuery = jQuery.noConflict(true);' +
- '</script>'
- );
+ '<script type="text/javascript">' +
+ 'var _jQuery = jQuery.noConflict(true);' +
+ '</script>'
+ );
addScript("../angular-bootstrap.js");
addScript("Scenario.js");
addScript("Application.js");
addScript("Describe.js");
addScript("Future.js");
- addScript("HtmlUI.js");
addScript("Runner.js");
addScript("SpecRunner.js");
addScript("dsl.js");
addScript("matchers.js");
+ addScript("ObjectModel.js");
+ addScript("output/Html.js");
+ addScript("output/Json.js");
+ addScript("output/Object.js");
+ addScript("output/Xml.js");
// Create the runner (which also sets up the global API)
document.write(
'<script type="text/javascript">' +
- 'var $scenario = new angular.scenario.Runner(window);' +
+ 'var $scenario = new angular.scenario.Runner(window, angular.scenario.SpecRunner);' +
'</script>'
);
diff --git a/src/scenario/angular.prefix b/src/scenario/angular.prefix
index d6660d61..fb9ae147 100644
--- a/src/scenario/angular.prefix
+++ b/src/scenario/angular.prefix
@@ -22,4 +22,4 @@
* THE SOFTWARE.
*/
(function(window, document, previousOnLoad){
- var _jQuery = window.jQuery.noConflict(true); \ No newline at end of file
+ var _jQuery = window.jQuery.noConflict(true);
diff --git a/src/scenario/angular.suffix b/src/scenario/angular.suffix
index c38f0ab5..66843013 100644
--- a/src/scenario/angular.suffix
+++ b/src/scenario/angular.suffix
@@ -4,25 +4,7 @@
try {
if (previousOnLoad) previousOnLoad();
} catch(e) {}
- _jQuery(document.body).append(
- '<div id="runner"></div>' +
- '<div id="frame"></div>'
- );
- var frame = _jQuery('#frame');
- var runner = _jQuery('#runner');
- var application = new angular.scenario.Application(frame);
- var ui = new angular.scenario.ui.Html(runner);
- $scenario.run(ui, application, angular.scenario.SpecRunner, function(error) {
- frame.remove();
- if (error) {
- if (window.console) {
- console.log(error.stack || error);
- } else {
- // Do something for IE
- alert(error);
- }
- }
- });
+ angularScenarioInit($scenario, angularJsConfig(document));
};
})(window, document, window.onload);
diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js
index f4484df8..1ae26db8 100644
--- a/src/scenario/dsl.js
+++ b/src/scenario/dsl.js
@@ -1,18 +1,19 @@
/**
* Shared DSL statements that are useful to all scenarios.
*/
-
+
/**
* Usage:
* wait() waits until you call resume() in the console
*/
- angular.scenario.dsl('wait', function() {
+angular.scenario.dsl('wait', function() {
return function() {
- return this.addFuture('waiting for you to call resume() in the console', function(done) {
+ return this.addFuture('waiting for you to resume', function(done) {
+ this.emit('InteractiveWait', this.spec, this.step);
this.$window.resume = function() { done(); };
});
};
- });
+});
/**
* Usage:
@@ -21,7 +22,7 @@
angular.scenario.dsl('pause', function() {
return function(time) {
return this.addFuture('pause for ' + time + ' seconds', function(done) {
- this.setTimeout(function() { done(null, time * 1000); }, time * 1000);
+ this.$window.setTimeout(function() { done(null, time * 1000); }, time * 1000);
});
};
});
@@ -49,8 +50,8 @@ angular.scenario.dsl('expect', function() {
/**
* Usage:
- * navigateTo(future|string) where url a string or future with a value
- * of a URL to navigate to
+ * navigateTo(url) Loads the url into the frame
+ * navigateTo(url, fn) where fn(url) is called and returns the URL to navigate to
*/
angular.scenario.dsl('navigateTo', function() {
return function(url, delegate) {
@@ -60,17 +61,7 @@ angular.scenario.dsl('navigateTo', function() {
url = delegate.call(this, url);
}
application.navigateTo(url, function() {
- application.executeAction(function($window) {
- if ($window.angular) {
- var $browser = $window.angular.service.$browser();
- $browser.poll();
- $browser.notifyWhenNoOutstandingRequests(function() {
- done(null, url);
- });
- } else {
- done(null, url);
- }
- });
+ done(null, url);
});
});
};
@@ -162,7 +153,11 @@ angular.scenario.dsl('repeater', function() {
chain.count = function() {
return this.addFutureAction('repeater ' + this.selector + ' count', function($window, $document, done) {
- done(null, $document.elements().size());
+ try {
+ done(null, $document.elements().length);
+ } catch (e) {
+ done(null, 0);
+ }
});
};
@@ -238,6 +233,7 @@ angular.scenario.dsl('select', function() {
/**
* Usage:
+ * element(selector).count() get the number of elements that match selector
* element(selector).click() clicks an element
* element(selector).attr(name) gets the value of an attribute
* element(selector).attr(name, value) sets the value of an attribute
@@ -248,10 +244,28 @@ angular.scenario.dsl('select', function() {
angular.scenario.dsl('element', function() {
var chain = {};
+ chain.count = function() {
+ return this.addFutureAction('element ' + this.selector + ' count', function($window, $document, done) {
+ try {
+ done(null, $document.elements().length);
+ } catch (e) {
+ done(null, 0);
+ }
+ });
+ };
+
chain.click = function() {
return this.addFutureAction('element ' + this.selector + ' click', function($window, $document, done) {
- $document.elements().trigger('click');
- done();
+ var elements = $document.elements();
+ var href = elements.attr('href');
+ elements.trigger('click');
+ if (href && elements[0].nodeName.toUpperCase() === 'A') {
+ this.application.navigateTo(href, function() {
+ done();
+ });
+ } else {
+ done();
+ }
});
};
diff --git a/src/scenario/matchers.js b/src/scenario/matchers.js
index 0dfbc455..8ef154e9 100644
--- a/src/scenario/matchers.js
+++ b/src/scenario/matchers.js
@@ -6,6 +6,10 @@ angular.scenario.matcher('toEqual', function(expected) {
return angular.equals(this.actual, expected);
});
+angular.scenario.matcher('toBe', function(expected) {
+ return this.actual === expected;
+});
+
angular.scenario.matcher('toBeDefined', function() {
return angular.isDefined(this.actual);
});
diff --git a/src/scenario/output/Html.js b/src/scenario/output/Html.js
new file mode 100644
index 00000000..4a682b9a
--- /dev/null
+++ b/src/scenario/output/Html.js
@@ -0,0 +1,165 @@
+/**
+ * User Interface for the Scenario Runner.
+ *
+ * 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);
+
+ context.append(
+ '<div id="header">' +
+ ' <h1><span class="angular">&lt;angular/&gt;</span>: Scenario Test Runner</h1>' +
+ ' <ul id="status-legend" class="status-display">' +
+ ' <li class="status-error">0 Errors</li>' +
+ ' <li class="status-failure">0 Failures</li>' +
+ ' <li class="status-success">0 Passed</li>' +
+ ' </ul>' +
+ '</div>' +
+ '<div id="specs">' +
+ ' <div class="test-children"></div>' +
+ '</div>'
+ );
+
+ runner.on('InteractiveWait', function(spec, step) {
+ var ui = model.getSpec(spec.id).getLastStep().ui;
+ ui.find('.test-title').
+ html('waiting for you to <a href="javascript:resume()">resume</a>.');
+ });
+
+ runner.on('SpecBegin', function(spec) {
+ var ui = findContext(spec);
+ ui.find('> .tests').append(
+ '<li class="status-pending test-it"></li>'
+ );
+ ui = ui.find('> .tests li:last');
+ ui.append(
+ '<div class="test-info">' +
+ ' <p class="test-title">' +
+ ' <span class="timer-result"></span>' +
+ ' <span class="test-name"></span>' +
+ ' </p>' +
+ '</div>' +
+ '<div class="scrollpane">' +
+ ' <ol class="test-actions"></ol>' +
+ '</div>'
+ );
+ ui.find('> .test-info .test-name').text(spec.name);
+ ui.find('> .test-info').click(function() {
+ var scrollpane = ui.find('> .scrollpane');
+ var actions = scrollpane.find('> .test-actions');
+ var name = context.find('> .test-info .test-name');
+ if (actions.find(':visible').length) {
+ actions.hide();
+ name.removeClass('open').addClass('closed');
+ } else {
+ actions.show();
+ scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
+ name.removeClass('closed').addClass('open');
+ }
+ });
+ model.getSpec(spec.id).ui = ui;
+ });
+
+ runner.on('SpecError', function(spec, error) {
+ var ui = model.getSpec(spec.id).ui;
+ ui.append('<pre></pre>');
+ ui.find('> pre').text(formatException(error));
+ });
+
+ runner.on('SpecEnd', function(spec) {
+ 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");
+ if (spec.status === 'success') {
+ spec.ui.find('> .test-info .test-name').addClass('closed');
+ spec.ui.find('> .scrollpane .test-actions').hide();
+ }
+ updateTotals(spec.status);
+ });
+
+ runner.on('StepBegin', function(spec, step) {
+ 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(
+ '<div class="timer-result"></div>' +
+ '<div class="test-title"></div>'
+ );
+ step.ui.find('> .test-title').text(step.name);
+ var scrollpane = step.ui.parents('.scrollpane');
+ scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
+ });
+
+ runner.on('StepFailure', function(spec, step, error) {
+ var ui = model.getSpec(spec.id).getLastStep().ui;
+ addError(ui, step.line, error);
+ });
+
+ runner.on('StepError', function(spec, step, error) {
+ var ui = model.getSpec(spec.id).getLastStep().ui;
+ addError(ui, step.line, error);
+ });
+
+ runner.on('StepEnd', function(spec, step) {
+ 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');
+ scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
+ });
+
+ /**
+ * Finds the context of a spec block defined by the passed definition.
+ *
+ * @param {Object} The definition created by the Describe object.
+ */
+ function findContext(spec) {
+ var currentContext = context.find('#specs');
+ angular.foreach(model.getDefinitionPath(spec), function(defn) {
+ var id = 'describe-' + defn.id;
+ if (!context.find('#' + id).length) {
+ currentContext.find('> .test-children').append(
+ '<div class="test-describe" id="' + id + '">' +
+ ' <h2></h2>' +
+ ' <div class="test-children"></div>' +
+ ' <ul class="tests"></ul>' +
+ '</div>'
+ );
+ context.find('#' + id).find('> h2').text('describe: ' + defn.name);
+ }
+ currentContext = context.find('#' + id);
+ });
+ return context.find('#describe-' + spec.definition.id);
+ };
+
+ /**
+ * Updates the test counter for the status.
+ *
+ * @param {string} the status.
+ */
+ function updateTotals(status) {
+ var legend = context.find('#status-legend .status-' + status);
+ var parts = legend.text().split(' ');
+ var value = (parts[0] * 1) + 1;
+ legend.text(value + ' ' + parts[1]);
+ }
+
+ /**
+ * Add an error to a step.
+ *
+ * @param {Object} The JQuery wrapped context
+ * @param {Function} fn() that should return the file/line number of the error
+ * @param {Object} the error.
+ */
+ function addError(context, line, error) {
+ context.find('.test-title').append('<pre></pre>');
+ var message = _jQuery.trim(line() + '\n\n' + formatException(error));
+ context.find('.test-title pre:last').text(message);
+ };
+});
diff --git a/src/scenario/output/Json.js b/src/scenario/output/Json.js
new file mode 100644
index 00000000..94212301
--- /dev/null
+++ b/src/scenario/output/Json.js
@@ -0,0 +1,10 @@
+/**
+ * Generates JSON output into a context.
+ */
+angular.scenario.output('json', function(context, runner) {
+ var model = new angular.scenario.ObjectModel(runner);
+
+ runner.on('RunnerEnd', function() {
+ context.text(angular.toJson(model.value));
+ });
+});
diff --git a/src/scenario/output/Object.js b/src/scenario/output/Object.js
new file mode 100644
index 00000000..3257cfd7
--- /dev/null
+++ b/src/scenario/output/Object.js
@@ -0,0 +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;
+});
diff --git a/src/scenario/output/Xml.js b/src/scenario/output/Xml.js
new file mode 100644
index 00000000..47d98c78
--- /dev/null
+++ b/src/scenario/output/Xml.js
@@ -0,0 +1,48 @@
+/**
+ * Generates XML output into a context.
+ */
+angular.scenario.output('xml', function(context, runner) {
+ var model = new angular.scenario.ObjectModel(runner);
+
+ runner.on('RunnerEnd', function() {
+ context.append('<scenario></scenario>');
+ serializeXml(context.find('> scenario'), model.value);
+ });
+
+ /**
+ * Convert the tree into XML.
+ *
+ * @param {Object} context jQuery context to add the XML to.
+ * @param {Object} tree node to serialize
+ */
+ function serializeXml(context, tree) {
+ angular.foreach(tree.children, function(child) {
+ context.append('<describe></describe>');
+ var describeContext = context.find('> describe:last');
+ describeContext.attr('id', child.id);
+ describeContext.attr('name', child.name);
+ serializeXml(describeContext, child);
+ });
+ context.append('<its></its>');
+ context = context.find('> its');
+ angular.foreach(tree.specs, function(spec) {
+ context.append('<it></it>')
+ var specContext = context.find('> it:last');
+ specContext.attr('id', spec.id);
+ specContext.attr('name', spec.name);
+ specContext.attr('duration', spec.duration);
+ specContext.attr('status', spec.status);
+ angular.foreach(spec.steps, function(step) {
+ specContext.append('<step></step>');
+ var stepContext = specContext.find('> step:last');
+ stepContext.attr('name', step.name);
+ stepContext.attr('duration', step.duration);
+ stepContext.attr('status', step.status);
+ if (step.error) {
+ stepContext.append('<error></error');
+ stepContext.find('error').text(formatException(step.error));
+ }
+ });
+ });
+ }
+});