aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorElliott Sprehn2010-10-24 14:14:45 -0700
committerIgor Minar2010-10-26 15:17:57 -0700
commit40d7e66f408eaaa66efd8d7934ab2eb3324236a1 (patch)
treedaf88d70df9037416598307784eb83df93df4fed /src
parent1d52349440d40de527b5d7f3849070f525c1b79b (diff)
downloadangular.js-40d7e66f408eaaa66efd8d7934ab2eb3324236a1.tar.bz2
Lots of bug fixes in the scenario runner and a bunch of new features.
- By default the runner now creates multiple output formats as it runs. Nodes are created in the DOM with ids: json, xml, and html. ex. $('#json').html() => json output of the runner ex. $('#xml').html() => json output of the runner $result is also an object tree result. The permitted formats are html,json,xml,object. If you don't want certain formats you can select specific ones with the new ng:scenario-output attribute on the script tag. <script src="angular-scenario.js" ng:scenario-output="xml,json"> - Added element(...).count() that returns the number of matching elements for the selector. - repeater(...).count() now returns 0 if no elements matched which can be used to check if a repeater is empty. - Added toBe() matcher that does strict equality with === - Implement iit and ddescribe. If iit() is used instead of it() then only that test will run. If ddescribe() is used instead of describe() them only it() statements inside of it will run. Several iit/ddescribe() blocks can be used to run isolated tests. - Implement new event based model for SpecRunner. You can now listen for events in the runner. This is useful for writing your own UI or connecting a remote process (ex. WebDriver). Event callbacks execute on the Runner instance. Events, if fired, will always be in the below order. All events always happen except for Failure and Error events which only happen in error conditions. Events: RunnerBegin SpecBegin(spec) StepBegin(spec, step) StepError(spec, step, error) StepFailure(spec, step, error) StepEnd(spec, step) SpecError(spec, step, error) SpecEnd(spec) RunnerEnd - Only allow the browser to repaint every 10 steps. Cuts 700ms off Firefox in benchmark, 200ms off Chrome. - Bug Fix: Manually navigate anchors on click since trigger wont work in Firefox.
Diffstat (limited to 'src')
-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));
+ }
+ });
+ });
+ }
+});