aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorElliott Sprehn2010-10-24 14:14:45 -0700
committerIgor Minar2010-10-26 15:17:57 -0700
commit40d7e66f408eaaa66efd8d7934ab2eb3324236a1 (patch)
treedaf88d70df9037416598307784eb83df93df4fed
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.
-rw-r--r--Rakefile6
-rw-r--r--css/angular-scenario.css24
-rw-r--r--jsTestDriver-jquery.conf4
-rw-r--r--jsTestDriver.conf4
-rw-r--r--scenario/Runner.html2
-rw-r--r--scenario/widgets-scenario.js3
-rw-r--r--scenario/widgets.html6
-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
-rw-r--r--test/AngularSpec.js21
-rw-r--r--test/scenario/ApplicationSpec.js82
-rw-r--r--test/scenario/DescribeSpec.js21
-rw-r--r--test/scenario/HtmlUISpec.js98
-rw-r--r--test/scenario/ObjectModelSpec.js112
-rw-r--r--test/scenario/RunnerSpec.js11
-rw-r--r--test/scenario/SpecRunnerSpec.js123
-rw-r--r--test/scenario/dslSpec.js82
-rw-r--r--test/scenario/mocks.js41
-rw-r--r--test/scenario/output/HtmlSpec.js124
-rw-r--r--test/scenario/output/jsonSpec.js34
-rw-r--r--test/scenario/output/objectSpec.js37
-rw-r--r--test/scenario/output/xmlSpec.js33
37 files changed, 1362 insertions, 648 deletions
diff --git a/Rakefile b/Rakefile
index 59be55fc..b66d467b 100644
--- a/Rakefile
+++ b/Rakefile
@@ -27,12 +27,16 @@ ANGULAR_SCENARIO = [
'src/scenario/Application.js',
'src/scenario/Describe.js',
'src/scenario/Future.js',
- 'src/scenario/HtmlUI.js',
+ 'src/scenario/ObjectModel.js',
'src/scenario/Describe.js',
'src/scenario/Runner.js',
'src/scenario/SpecRunner.js',
'src/scenario/dsl.js',
'src/scenario/matchers.js',
+ 'src/scenario/output/Html.js',
+ 'src/scenario/output/Json.js',
+ 'src/scenario/output/Xml.js',
+ 'src/scenario/output/Object.js',
]
BUILD_DIR = 'build'
diff --git a/css/angular-scenario.css b/css/angular-scenario.css
index adadebb0..f5cded7f 100644
--- a/css/angular-scenario.css
+++ b/css/angular-scenario.css
@@ -8,6 +8,10 @@ body {
font-size: 14px;
}
+#json, #xml {
+ display: none;
+}
+
#header {
position: fixed;
width: 100%;
@@ -32,7 +36,7 @@ body {
height: 30px;
}
-#frame h2,
+#application h2,
#specs h2 {
margin: 0;
padding: 0.5em;
@@ -45,26 +49,26 @@ body {
}
#header,
-#frame,
+#application,
.test-info,
.test-actions li {
overflow: hidden;
}
-#frame {
+#application {
margin: 10px;
}
-#frame iframe {
+#application iframe {
width: 100%;
height: 758px;
}
-#frame .popout {
+#application .popout {
float: right;
}
-#frame iframe {
+#application iframe {
border: none;
}
@@ -154,6 +158,10 @@ body {
margin-left: 6em;
}
+.test-describe {
+ padding-bottom: 0.5em;
+}
+
.test-describe .test-describe {
margin: 5px 5px 10px 2em;
}
@@ -178,11 +186,11 @@ body {
}
#specs h2,
-#frame h2 {
+#application h2 {
background-color: #efefef;
}
-#frame {
+#application {
border: 1px solid #BABAD1;
}
diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf
index 7f0d6912..a2388662 100644
--- a/jsTestDriver-jquery.conf
+++ b/jsTestDriver-jquery.conf
@@ -10,9 +10,11 @@ load:
- src/*.js
- test/testabilityPatch.js
- src/scenario/Scenario.js
+ - src/scenario/output/*.js
- src/scenario/*.js
- test/angular-mocks.js
- test/scenario/*.js
+ - test/scenario/output/*.js
- test/*.js
exclude:
@@ -20,6 +22,6 @@ exclude:
- src/angular.suffix
- src/angular-bootstrap.js
- src/AngularPublic.js
- - src/scenario/bootstrap.js
+ - src/scenario/angular-bootstrap.js
- test/jquery_remove.js
diff --git a/jsTestDriver.conf b/jsTestDriver.conf
index 7d202d72..c8ced595 100644
--- a/jsTestDriver.conf
+++ b/jsTestDriver.conf
@@ -11,9 +11,11 @@ load:
- example/personalLog/*.js
- test/testabilityPatch.js
- src/scenario/Scenario.js
+ - src/scenario/output/*.js
- src/scenario/*.js
- test/angular-mocks.js
- test/scenario/*.js
+ - test/scenario/output/*.js
- test/*.js
- example/personalLog/test/*.js
@@ -22,5 +24,5 @@ exclude:
- src/angular.prefix
- src/angular.suffix
- src/angular-bootstrap.js
- - src/scenario/bootstrap.js
+ - src/scenario/angular-bootstrap.js
- src/AngularPublic.js
diff --git a/scenario/Runner.html b/scenario/Runner.html
index ffa08af9..f715b8e5 100644
--- a/scenario/Runner.html
+++ b/scenario/Runner.html
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
- <script type="text/javascript" src="../src/scenario/bootstrap.js"></script>
+ <script type="text/javascript" src="../src/scenario/angular-bootstrap.js"></script>
<script type="text/javascript" src="widgets-scenario.js"></script>
</head>
<body>
diff --git a/scenario/widgets-scenario.js b/scenario/widgets-scenario.js
index 0d604fc9..ba3ef3cf 100644
--- a/scenario/widgets-scenario.js
+++ b/scenario/widgets-scenario.js
@@ -36,6 +36,9 @@ describe('widgets', function() {
element('input[type="image"]').click();
expect(binding('button').fromJson()).toEqual({'count': 4});
+ element('#navigate a').click();
+ expect(binding('$location.hash')).toEqual('route');
+
/**
* Custom value parser for futures.
*/
diff --git a/scenario/widgets.html b/scenario/widgets.html
index 8960f5f4..a520a326 100644
--- a/scenario/widgets.html
+++ b/scenario/widgets.html
@@ -2,7 +2,6 @@
<html xmlns:ng="http://angularjs.org">
<head>
<link rel="stylesheet" type="text/css" href="style.css"/>
- <script type="text/javascript" src="../libs/jquery/jquery-1.4.2.js"></script>
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
</head>
<body ng:init="$window.$scope = this">
@@ -94,6 +93,11 @@
</td>
<td></td>
</tr>
+ <tr id="navigate">
+ <td>navigate</td>
+ <td><a href="#route">Go to #route</td>
+ <td>{{$location.hash}}</td>
+ </tr>
</table>
</body>
</html>
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));
+ }
+ });
+ });
+ }
+});
diff --git a/test/AngularSpec.js b/test/AngularSpec.js
index b60b7bd8..bab7df18 100644
--- a/test/AngularSpec.js
+++ b/test/AngularSpec.js
@@ -196,6 +196,27 @@ describe ('rngScript', function() {
expect('my-angular-app-0.9.0-de0a8612.min.js'.match(rngScript)).toBeNull();
expect('foo/../my-angular-app-0.9.0-de0a8612.min.js'.match(rngScript)).toBeNull();
});
+
+ it('should match angular-scenario.js', function() {
+ expect('angular-scenario.js'.match(rngScript)).not.toBeNull();
+ expect('angular-scenario.min.js'.match(rngScript)).not.toBeNull();
+ expect('../angular-scenario.js'.match(rngScript)).not.toBeNull();
+ expect('foo/angular-scenario.min.js'.match(rngScript)).not.toBeNull();
+ });
+
+ it('should match angular-scenario-0.9.0(.min).js', function() {
+ expect('angular-scenario-0.9.0.js'.match(rngScript)).not.toBeNull();
+ expect('angular-scenario-0.9.0.min.js'.match(rngScript)).not.toBeNull();
+ expect('../angular-scenario-0.9.0.js'.match(rngScript)).not.toBeNull();
+ expect('foo/angular-scenario-0.9.0.min.js'.match(rngScript)).not.toBeNull();
+ });
+
+ it('should match angular-scenario-0.9.0-de0a8612(.min).js', function() {
+ expect('angular-scenario-0.9.0-de0a8612.js'.match(rngScript)).not.toBeNull();
+ expect('angular-scenario-0.9.0-de0a8612.min.js'.match(rngScript)).not.toBeNull();
+ expect('../angular-scenario-0.9.0-de0a8612.js'.match(rngScript)).not.toBeNull();
+ expect('foo/angular-scenario-0.9.0-de0a8612.min.js'.match(rngScript)).not.toBeNull();
+ });
});
diff --git a/test/scenario/ApplicationSpec.js b/test/scenario/ApplicationSpec.js
index 883701ba..122292c6 100644
--- a/test/scenario/ApplicationSpec.js
+++ b/test/scenario/ApplicationSpec.js
@@ -7,9 +7,10 @@ describe('angular.scenario.Application', function() {
});
it('should return new $window and $document after navigate', function() {
+ var called;
var testWindow, testDocument, counter = 0;
app.navigateTo = noop;
- app.getWindow = function() {
+ app.getWindow_ = function() {
return {x:counter++, document:{x:counter++}};
};
app.navigateTo('http://www.google.com/');
@@ -21,30 +22,45 @@ describe('angular.scenario.Application', function() {
app.executeAction(function($window, $document) {
expect($window).not.toEqual(testWindow);
expect($document).not.toEqual(testDocument);
+ called = true;
});
+ expect(called).toBeTruthy();
});
it('should execute callback with correct arguments', function() {
+ var called;
var testWindow = {document: {}};
- app.getWindow = function() {
+ app.getWindow_ = function() {
return testWindow;
};
app.executeAction(function($window, $document) {
expect(this).toEqual(app);
expect($document).toEqual(_jQuery($window.document));
expect($window).toEqual(testWindow);
+ called = true;
});
+ expect(called).toBeTruthy();
});
- it('should create a new iframe each time', function() {
+ it('should use a new iframe each time', function() {
app.navigateTo('about:blank');
- var frame = app.getFrame();
+ var frame = app.getFrame_();
frame.attr('test', true);
app.navigateTo('about:blank');
- expect(app.getFrame().attr('test')).toBeFalsy();
+ expect(app.getFrame_().attr('test')).toBeFalsy();
});
- it('should URL description bar', function() {
+ it('should hide old iframes and navigate to about:blank', function() {
+ app.navigateTo('about:blank#foo');
+ app.navigateTo('about:blank#bar');
+ var iframes = frames.find('iframe');
+ expect(iframes.length).toEqual(2);
+ expect(iframes[0].src).toEqual('about:blank');
+ expect(iframes[1].src).toEqual('about:blank#bar');
+ expect(_jQuery(iframes[0]).css('display')).toEqual('none');
+ });
+
+ it('should URL update description bar', function() {
app.navigateTo('about:blank');
var anchor = frames.find('> h2 a');
expect(anchor.attr('href')).toEqual('about:blank');
@@ -53,24 +69,48 @@ describe('angular.scenario.Application', function() {
it('should call onload handler when frame loads', function() {
var called;
- app.getFrame = function() {
- // Mock a little jQuery
- var result = {
- remove: function() {
- return result;
- },
- attr: function(key, value) {
- return (!value) ? 'attribute value' : result;
- },
- load: function() {
- called = true;
- }
- };
- return result;
+ app.getWindow_ = function() {
+ return {};
};
- app.navigateTo('about:blank', function() {
+ app.navigateTo('about:blank', function($window, $document) {
called = true;
});
+ var handlers = app.getFrame_().data('events').load;
+ expect(handlers).toBeDefined();
+ expect(handlers.length).toEqual(1);
+ handlers[0].handler();
expect(called).toBeTruthy();
});
+
+ it('should wait for pending requests in executeAction', function() {
+ var called, polled;
+ var handlers = [];
+ var testWindow = {
+ document: _jQuery('<div class="test-foo"></div>'),
+ angular: {
+ service: {},
+ }
+ };
+ testWindow.angular.service.$browser = function() {
+ return {
+ poll: function() {
+ polled = true;
+ },
+ notifyWhenNoOutstandingRequests: function(fn) {
+ handlers.push(fn);
+ }
+ }
+ };
+ app.getWindow_ = function() {
+ return testWindow;
+ };
+ app.executeAction(function($window, $document) {
+ expect($window).toEqual(testWindow);
+ expect($document).toBeDefined();
+ expect($document[0].className).toEqual('test-foo');
+ });
+ expect(polled).toBeTruthy();
+ expect(handlers.length).toEqual(1);
+ handlers[0]();
+ });
});
diff --git a/test/scenario/DescribeSpec.js b/test/scenario/DescribeSpec.js
index c2e7310e..6fcee731 100644
--- a/test/scenario/DescribeSpec.js
+++ b/test/scenario/DescribeSpec.js
@@ -80,6 +80,27 @@ describe('angular.scenario.Describe', function() {
expect(specs.length).toEqual(0);
});
+ it('should only return iit and ddescribe if present', function() {
+ root.describe('A', function() {
+ this.it('1', angular.noop);
+ this.iit('2', angular.noop);
+ this.describe('B', function() {
+ this.it('3', angular.noop);
+ this.ddescribe('C', function() {
+ this.it('4', angular.noop);
+ this.describe('D', function() {
+ this.it('5', angular.noop);
+ });
+ });
+ });
+ });
+ var specs = root.getSpecs();
+ expect(specs.length).toEqual(3);
+ expect(specs[0].name).toEqual('5');
+ expect(specs[1].name).toEqual('4');
+ expect(specs[2].name).toEqual('2');
+ });
+
it('should create uniqueIds in the tree', function() {
angular.scenario.Describe.id = 0;
var a = new angular.scenario.Describe();
diff --git a/test/scenario/HtmlUISpec.js b/test/scenario/HtmlUISpec.js
deleted file mode 100644
index 2c9ff080..00000000
--- a/test/scenario/HtmlUISpec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-describe('angular.scenario.HtmlUI', function() {
- var ui;
- var context;
- var spec;
-
- function line() { return 'unknown:-1'; }
-
- beforeEach(function() {
- spec = {
- name: 'test spec',
- definition: {
- id: 10,
- name: 'child',
- children: [],
- parent: {
- id: 20,
- name: 'parent',
- children: []
- }
- }
- };
- context = _jQuery("<div></div>");
- ui = new angular.scenario.ui.Html(context);
- });
-
- it('should create nested describe context', function() {
- ui.addSpec(spec);
- expect(context.find('#describe-20 #describe-10 > h2').text()).
- toEqual('describe: child');
- expect(context.find('#describe-20 > h2').text()).toEqual('describe: parent');
- expect(context.find('#describe-10 .tests > li .test-info .test-name').text()).
- toEqual('it test spec');
- expect(context.find('#describe-10 .tests > li').hasClass('status-pending')).
- toBeTruthy();
- });
-
- it('should update totals when steps complete', function() {
- // Error
- ui.addSpec(spec).error('error');
- // Failure
- specUI = ui.addSpec(spec);
- specUI.addStep('some step', line).finish('failure');
- specUI.finish();
- // Failure
- specUI = ui.addSpec(spec);
- specUI.addStep('some step', line).finish('failure');
- specUI.finish();
- // Failure
- specUI = ui.addSpec(spec);
- specUI.addStep('some step', line).finish('failure');
- specUI.finish();
- // Success
- specUI = ui.addSpec(spec);
- specUI.addStep('some step', line).finish();
- specUI.finish();
- // Success
- specUI = ui.addSpec(spec);
- specUI.addStep('another step', line).finish();
- specUI.finish();
-
- expect(parseInt(context.find('#status-legend .status-failure').text(), 10)).
- toEqual(3);
- expect(parseInt(context.find('#status-legend .status-success').text(), 10)).
- toEqual(2);
- expect(parseInt(context.find('#status-legend .status-error').text(), 10)).
- toEqual(1);
- });
-
- it('should update timer when test completes', function() {
- // Success
- specUI = ui.addSpec(spec);
- specUI.addStep('some step', line).finish();
- specUI.finish();
-
- // Failure
- specUI = ui.addSpec(spec);
- specUI.addStep('some step', line).finish('failure');
- specUI.finish('failure');
-
- // Error
- specUI = ui.addSpec(spec).error('error');
-
- context.find('#describe-10 .tests > li .test-info .timer-result').
- each(function(index, timer) {
- expect(timer.innerHTML).toMatch(/ms$/);
- });
- });
-
- it('should include line if provided', function() {
- specUI = ui.addSpec(spec);
- specUI.addStep('some step', line).finish('error!');
- specUI.finish();
-
- var errorHtml = context.find('#describe-10 .tests li pre').html();
- expect(errorHtml.indexOf('unknown:-1')).toEqual(0);
- });
-
-});
diff --git a/test/scenario/ObjectModelSpec.js b/test/scenario/ObjectModelSpec.js
new file mode 100644
index 00000000..8b83a52f
--- /dev/null
+++ b/test/scenario/ObjectModelSpec.js
@@ -0,0 +1,112 @@
+describe('angular.scenario.ObjectModel', function() {
+ var model;
+ var runner;
+ var spec, step;
+
+ beforeEach(function() {
+ spec = {
+ name: 'test spec',
+ definition: {
+ id: 10,
+ name: 'describe 1'
+ }
+ };
+ step = {
+ name: 'test step',
+ line: function() { return ''; }
+ };
+ runner = new angular.scenario.testing.MockRunner();
+ model = new angular.scenario.ObjectModel(runner);
+ });
+
+ it('should value default empty value', function() {
+ expect(model.value).toEqual({
+ name: '',
+ children: []
+ });
+ });
+
+ it('should add spec and create describe blocks on SpecBegin event', function() {
+ runner.emit('SpecBegin', {
+ name: 'test spec',
+ definition: {
+ id: 10,
+ name: 'describe 2',
+ parent: {
+ id: 12,
+ name: 'describe 1'
+ }
+ }
+ });
+
+ expect(model.value.children['describe 1']).toBeDefined();
+ expect(model.value.children['describe 1'].children['describe 2']).toBeDefined();
+ expect(model.value.children['describe 1'].children['describe 2'].specs['test spec']).toBeDefined();
+ });
+
+ it('should add step to spec on StepBegin', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+
+ expect(model.value.children['describe 1'].specs['test spec'].steps.length).toEqual(1);
+ });
+
+ it('should update spec timer duration on SpecEnd event', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('SpecEnd', spec);
+
+ expect(model.value.children['describe 1'].specs['test spec'].duration).toBeDefined();
+ });
+
+ it('should update step timer duration on StepEnd event', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+
+ expect(model.value.children['describe 1'].specs['test spec'].steps[0].duration).toBeDefined();
+ });
+
+ it('should set spec status on SpecEnd to success if no status set', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('SpecEnd', spec);
+
+ expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('success');
+ });
+
+ it('should set status to error after SpecError', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('SpecError', spec, 'error');
+
+ expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('error');
+ });
+
+ it('should set spec status to failure if step fails', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepEnd', spec, step);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepFailure', spec, step, 'error');
+ runner.emit('StepEnd', spec, step);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+
+ expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('failure');
+ });
+
+ it('should set spec status to error if step errors', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepError', spec, step, 'error');
+ runner.emit('StepEnd', spec, step);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepFailure', spec, step, 'error');
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+
+ expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('error');
+ });
+});
diff --git a/test/scenario/RunnerSpec.js b/test/scenario/RunnerSpec.js
index 1641a8f1..059dd874 100644
--- a/test/scenario/RunnerSpec.js
+++ b/test/scenario/RunnerSpec.js
@@ -2,7 +2,7 @@
* Mock spec runner.
*/
function MockSpecRunner() {}
-MockSpecRunner.prototype.run = function(ui, spec, specDone) {
+MockSpecRunner.prototype.run = function(spec, specDone) {
spec.before.call(this);
spec.body.call(this);
spec.after.call(this);
@@ -41,6 +41,11 @@ describe('angular.scenario.Runner', function() {
location: {}
};
runner = new angular.scenario.Runner($window);
+ runner.createSpecRunner_ = function(scope) {
+ return scope.$new(MockSpecRunner);
+ };
+ runner.on('SpecError', rethrow);
+ runner.on('StepError', rethrow);
});
afterEach(function() {
@@ -92,7 +97,7 @@ describe('angular.scenario.Runner', function() {
expect($window.dslScope).toBeDefined();
});
});
- runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow);
+ runner.run(null/*application*/);
});
it('should create a new scope for each DSL chain', function() {
@@ -107,6 +112,6 @@ describe('angular.scenario.Runner', function() {
expect(scope.chained).toEqual(2);
});
});
- runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow);
+ runner.run(null/*application*/);
});
});
diff --git a/test/scenario/SpecRunnerSpec.js b/test/scenario/SpecRunnerSpec.js
index 921d6853..7947a3ca 100644
--- a/test/scenario/SpecRunnerSpec.js
+++ b/test/scenario/SpecRunnerSpec.js
@@ -1,40 +1,4 @@
/**
- * Mock of all required UI classes/methods. (UI, Spec, Step).
- */
-function UIMock() {
- this.log = [];
-}
-UIMock.prototype = {
- addSpec: function(spec) {
- var log = this.log;
- log.push('addSpec:' + spec.name);
- return {
- addStep: function(name) {
- log.push('addStep:' + name);
- return {
- finish: function(e) {
- log.push('step finish:' + (e ? e : ''));
- return this;
- },
- error: function(e) {
- log.push('step error:' + (e ? e : ''));
- return this;
- }
- };
- },
- finish: function(e) {
- log.push('spec finish:' + (e ? e : ''));
- return this;
- },
- error: function(e) {
- log.push('spec error:' + (e ? e : ''));
- return this;
- }
- };
- }
-};
-
-/**
* Mock Application
*/
function ApplicationMock($window) {
@@ -47,7 +11,7 @@ ApplicationMock.prototype = {
};
describe('angular.scenario.SpecRunner', function() {
- var $window;
+ var $window, $root, log;
var runner;
function createSpec(name, body) {
@@ -60,14 +24,22 @@ describe('angular.scenario.SpecRunner', function() {
}
beforeEach(function() {
+ log = [];
$window = {};
$window.setTimeout = function(fn, timeout) {
fn();
};
- runner = angular.scope();
- runner.application = new ApplicationMock($window);
- runner.$window = $window;
- runner.$become(angular.scenario.SpecRunner);
+ $root = angular.scope({
+ emit: function(eventName) {
+ log.push(eventName);
+ },
+ on: function(eventName) {
+ log.push('Listener Added for ' + eventName);
+ }
+ });
+ $root.application = new ApplicationMock($window);
+ $root.$window = $window;
+ runner = $root.$new(angular.scenario.SpecRunner);
});
it('should bind futures to the spec', function() {
@@ -92,84 +64,82 @@ describe('angular.scenario.SpecRunner', function() {
it('should execute spec function and notify UI', function() {
var finished;
- var ui = new UIMock();
var spec = createSpec('test spec', function() {
this.test = 'some value';
});
runner.addFuture('test future', function(done) {
done();
});
- runner.run(ui, spec, function() {
+ runner.run(spec, function() {
finished = true;
});
expect(runner.test).toEqual('some value');
expect(finished).toBeTruthy();
- expect(ui.log).toEqual([
- 'addSpec:test spec',
- 'addStep:test future',
- 'step finish:',
- 'spec finish:'
+ expect(log).toEqual([
+ 'SpecBegin',
+ 'StepBegin',
+ 'StepEnd',
+ 'SpecEnd'
]);
});
it('should execute notify UI on spec setup error', function() {
var finished;
- var ui = new UIMock();
var spec = createSpec('test spec', function() {
throw 'message';
});
- runner.run(ui, spec, function() {
+ runner.run(spec, function() {
finished = true;
});
expect(finished).toBeTruthy();
- expect(ui.log).toEqual([
- 'addSpec:test spec',
- 'spec error:message'
+ expect(log).toEqual([
+ 'SpecBegin',
+ 'SpecError',
+ 'SpecEnd'
]);
});
it('should execute notify UI on step failure', function() {
var finished;
- var ui = new UIMock();
var spec = createSpec('test spec');
runner.addFuture('test future', function(done) {
done('failure message');
});
- runner.run(ui, spec, function() {
+ runner.run(spec, function() {
finished = true;
});
expect(finished).toBeTruthy();
- expect(ui.log).toEqual([
- 'addSpec:test spec',
- 'addStep:test future',
- 'step finish:failure message',
- 'spec finish:'
+ expect(log).toEqual([
+ 'SpecBegin',
+ 'StepBegin',
+ 'StepFailure',
+ 'StepEnd',
+ 'SpecEnd'
]);
});
it('should execute notify UI on step error', function() {
var finished;
- var ui = new UIMock();
var spec = createSpec('test spec', function() {
this.addFuture('test future', function(done) {
throw 'error message';
});
});
- runner.run(ui, spec, function() {
+ runner.run(spec, function() {
finished = true;
});
expect(finished).toBeTruthy();
- expect(ui.log).toEqual([
- 'addSpec:test spec',
- 'addStep:test future',
- 'step error:error message',
- 'spec finish:'
+ expect(log).toEqual([
+ 'SpecBegin',
+ 'StepBegin',
+ 'StepError',
+ 'StepEnd',
+ 'SpecEnd'
]);
});
it('should run after handlers even if error in body of spec', function() {
var finished, after;
- var ui = new UIMock();
var spec = createSpec('test spec', function() {
this.addFuture('body', function(done) {
throw 'error message';
@@ -181,18 +151,19 @@ describe('angular.scenario.SpecRunner', function() {
done();
});
};
- runner.run(ui, spec, function() {
+ runner.run(spec, function() {
finished = true;
});
expect(finished).toBeTruthy();
expect(after).toBeTruthy();
- expect(ui.log).toEqual([
- 'addSpec:test spec',
- 'addStep:body',
- 'step error:error message',
- 'addStep:after',
- 'step finish:',
- 'spec finish:'
+ expect(log).toEqual([
+ 'SpecBegin',
+ 'StepBegin',
+ 'StepError',
+ 'StepEnd',
+ 'StepBegin',
+ 'StepEnd',
+ 'SpecEnd'
]);
});
diff --git a/test/scenario/dslSpec.js b/test/scenario/dslSpec.js
index 14ca8b2c..bbba0b7d 100644
--- a/test/scenario/dslSpec.js
+++ b/test/scenario/dslSpec.js
@@ -1,41 +1,21 @@
-/**
- * Very basic Mock of angular.
- */
-function AngularMock() {
- this.reset();
- this.service = this;
-}
-
-AngularMock.prototype.reset = function() {
- this.log = [];
-};
-
-AngularMock.prototype.$browser = function() {
- this.log.push('$brower()');
- return this;
-};
-
-AngularMock.prototype.poll = function() {
- this.log.push('$brower.poll()');
- return this;
-};
-
-AngularMock.prototype.notifyWhenNoOutstandingRequests = function(fn) {
- this.log.push('$brower.notifyWhenNoOutstandingRequests()');
- fn();
-};
-
describe("angular.scenario.dsl", function() {
- var $window;
- var $root;
- var application;
+ var $window, $root;
+ var application, eventLog;
beforeEach(function() {
+ eventLog = [];
$window = {
document: _jQuery("<div></div>"),
- angular: new AngularMock()
+ angular: new angular.scenario.testing.MockAngular()
};
- $root = angular.scope();
+ $root = angular.scope({
+ emit: function(eventName) {
+ eventLog.push(eventName);
+ },
+ on: function(eventName) {
+ eventLog.push('Listener Added for ' + eventName);
+ }
+ });
$root.futures = [];
$root.futureLog = [];
$root.$window = $window;
@@ -54,7 +34,7 @@ describe("angular.scenario.dsl", function() {
};
});
$root.application = new angular.scenario.Application($window.document);
- $root.application.getWindow = function() {
+ $root.application.getWindow_ = function() {
return $window;
};
$root.application.navigateTo = function(url, callback) {
@@ -74,13 +54,14 @@ describe("angular.scenario.dsl", function() {
expect($root.futureLog).toEqual([]);
$window.resume();
expect($root.futureLog).
- toEqual(['waiting for you to call resume() in the console']);
+ toEqual(['waiting for you to resume']);
+ expect(eventLog).toContain('InteractiveWait');
});
});
describe('Pause', function() {
beforeEach(function() {
- $root.setTimeout = function(fn, value) {
+ $root.$window.setTimeout = function(fn, value) {
$root.timerValue = value;
fn();
};
@@ -127,13 +108,6 @@ describe("angular.scenario.dsl", function() {
expect($window.location).toEqual('http://myurl');
expect($root.futureResult).toEqual('http://myurl');
});
-
- it('should wait for angular notify when no requests pending', function() {
- $root.dsl.navigateTo('url');
- expect($window.angular.log).toContain('$brower.poll()');
- expect($window.angular.log).
- toContain('$brower.notifyWhenNoOutstandingRequests()');
- });
});
describe('Element Finding', function() {
@@ -199,6 +173,24 @@ describe("angular.scenario.dsl", function() {
$root.dsl.element('a').click();
});
+ it('should navigate page if click on anchor', function() {
+ expect($window.location).not.toEqual('#foo');
+ doc.append('<a href="#foo"></a>');
+ $root.dsl.element('a').click();
+ expect($window.location).toEqual('#foo');
+ });
+
+ it('should count matching elements', function() {
+ doc.append('<span></span><span></span>');
+ $root.dsl.element('span').count();
+ expect($root.futureResult).toEqual(2);
+ });
+
+ it('should return count of 0 if no matching elements', function() {
+ $root.dsl.element('span').count();
+ expect($root.futureResult).toEqual(0);
+ });
+
it('should get attribute', function() {
doc.append('<div id="test" class="foo"></div>');
$root.dsl.element('#test').attr('class');
@@ -249,6 +241,12 @@ describe("angular.scenario.dsl", function() {
expect($root.futureResult).toEqual(2);
});
+ it('should return 0 if repeater doesnt match', function() {
+ doc.find('ul').html('');
+ chain.count();
+ expect($root.futureResult).toEqual(0);
+ });
+
it('should get a row of bindings', function() {
chain.row(1);
expect($root.futureResult).toEqual(['felisa', 'female']);
diff --git a/test/scenario/mocks.js b/test/scenario/mocks.js
new file mode 100644
index 00000000..5cd2f30a
--- /dev/null
+++ b/test/scenario/mocks.js
@@ -0,0 +1,41 @@
+angular.scenario.testing = angular.scenario.testing || {};
+
+angular.scenario.testing.MockAngular = function() {
+ this.reset();
+ this.service = this;
+};
+
+angular.scenario.testing.MockAngular.prototype.reset = function() {
+ this.log = [];
+};
+
+angular.scenario.testing.MockAngular.prototype.$browser = function() {
+ this.log.push('$brower()');
+ return this;
+};
+
+angular.scenario.testing.MockAngular.prototype.poll = function() {
+ this.log.push('$brower.poll()');
+ return this;
+};
+
+angular.scenario.testing.MockAngular.prototype.notifyWhenNoOutstandingRequests = function(fn) {
+ this.log.push('$brower.notifyWhenNoOutstandingRequests()');
+ fn();
+};
+
+angular.scenario.testing.MockRunner = function() {
+ this.listeners = [];
+};
+
+angular.scenario.testing.MockRunner.prototype.on = function(eventName, fn) {
+ this.listeners[eventName] = this.listeners[eventName] || [];
+ this.listeners[eventName].push(fn);
+};
+
+angular.scenario.testing.MockRunner.prototype.emit = function(eventName) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ angular.foreach(this.listeners[eventName] || [], function(fn) {
+ fn.apply(this, args);
+ });
+};
diff --git a/test/scenario/output/HtmlSpec.js b/test/scenario/output/HtmlSpec.js
new file mode 100644
index 00000000..f5bb90b0
--- /dev/null
+++ b/test/scenario/output/HtmlSpec.js
@@ -0,0 +1,124 @@
+describe('angular.scenario.output.html', function() {
+ var runner, spec, listeners;
+ var ui, context;
+
+ beforeEach(function() {
+ listeners = [];
+ spec = {
+ name: 'test spec',
+ definition: {
+ id: 10,
+ name: 'child',
+ children: [],
+ parent: {
+ id: 20,
+ name: 'parent',
+ children: []
+ }
+ }
+ };
+ step = {
+ name: 'some step',
+ line: function() { return 'unknown:-1'; },
+ };
+ runner = new angular.scenario.testing.MockRunner();
+ context = _jQuery("<div></div>");
+ ui = angular.scenario.output.html(context, runner);
+ });
+
+ it('should create nested describe context', function() {
+ runner.emit('SpecBegin', spec);
+ expect(context.find('#describe-20 #describe-10 > h2').text()).
+ toEqual('describe: child');
+ expect(context.find('#describe-20 > h2').text()).toEqual('describe: parent');
+ expect(context.find('#describe-10 .tests > li .test-info .test-name').text()).
+ toEqual('test spec');
+ expect(context.find('#describe-10 .tests > li').hasClass('status-pending')).
+ toBeTruthy();
+ });
+
+ it('should add link on InteractiveWait', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepEnd', spec, step);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('InteractiveWait', spec, step);
+ expect(context.find('.test-actions .test-title:first').text()).toEqual('some step');
+ expect(context.find('.test-actions .test-title:last').html()).toEqual(
+ 'waiting for you to <a href="javascript:resume()">resume</a>.'
+ );
+ });
+
+ it('should update totals when steps complete', function() {
+ // Failure
+ for (var i=0; i < 3; ++i) {
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepFailure', spec, step, 'error');
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+ }
+
+ // Error
+ runner.emit('SpecBegin', spec);
+ runner.emit('SpecError', spec, 'error');
+ runner.emit('SpecEnd', spec);
+
+ // Error
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepError', spec, step, 'error');
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+
+ // Success
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+
+ expect(parseInt(context.find('#status-legend .status-failure').text(), 10)).
+ toEqual(3);
+ expect(parseInt(context.find('#status-legend .status-error').text(), 10)).
+ toEqual(2);
+ expect(parseInt(context.find('#status-legend .status-success').text(), 10)).
+ toEqual(1);
+ });
+
+ it('should update timer when test completes', function() {
+ // Success
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+
+ // Failure
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepFailure', spec, step, 'error');
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+
+ // Error
+ runner.emit('SpecBegin', spec);
+ runner.emit('SpecError', spec, 'error');
+ runner.emit('SpecEnd', spec);
+
+ context.find('#describe-10 .tests > li .test-info .timer-result').
+ each(function(index, timer) {
+ expect(timer.innerHTML).toMatch(/ms$/);
+ });
+ });
+
+ it('should include line if provided', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepFailure', spec, step, 'error');
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+
+ var errorHtml = context.find('#describe-10 .tests li pre').html();
+ expect(errorHtml.indexOf('unknown:-1')).toEqual(0);
+ });
+
+});
diff --git a/test/scenario/output/jsonSpec.js b/test/scenario/output/jsonSpec.js
new file mode 100644
index 00000000..b3592608
--- /dev/null
+++ b/test/scenario/output/jsonSpec.js
@@ -0,0 +1,34 @@
+describe('angular.scenario.output.json', function() {
+ var output, context;
+ var runner, $window;
+ var spec, step;
+
+ beforeEach(function() {
+ $window = {};
+ context = _jQuery('<div></div>');
+ runner = new angular.scenario.testing.MockRunner();
+ output = angular.scenario.output.json(context, runner);
+ spec = {
+ name: 'test spec',
+ definition: {
+ id: 10,
+ name: 'describe',
+ }
+ };
+ step = {
+ name: 'some step',
+ line: function() { return 'unknown:-1'; },
+ };
+ });
+
+ it('should put json in context on RunnerEnd', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+ runner.emit('RunnerEnd');
+
+ expect(angular.fromJson(context.html()).children['describe']
+ .specs['test spec'].status).toEqual('success');
+ });
+});
diff --git a/test/scenario/output/objectSpec.js b/test/scenario/output/objectSpec.js
new file mode 100644
index 00000000..c4e8d451
--- /dev/null
+++ b/test/scenario/output/objectSpec.js
@@ -0,0 +1,37 @@
+describe('angular.scenario.output.object', function() {
+ var output;
+ var runner, $window;
+ var spec, step;
+
+ beforeEach(function() {
+ $window = {};
+ runner = new angular.scenario.testing.MockRunner();
+ runner.$window = $window;
+ output = angular.scenario.output.object(null, runner);
+ spec = {
+ name: 'test spec',
+ definition: {
+ id: 10,
+ name: 'describe',
+ children: []
+ }
+ };
+ step = {
+ name: 'some step',
+ line: function() { return 'unknown:-1'; },
+ };
+ });
+
+ it('should create a global variable $result', function() {
+ expect($window.$result).toBeDefined();
+ });
+
+ it('should maintain live state in $result', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepEnd', spec, step);
+
+ expect($window.$result.children['describe']
+ .specs['test spec'].steps[0].duration).toBeDefined();
+ });
+});
diff --git a/test/scenario/output/xmlSpec.js b/test/scenario/output/xmlSpec.js
new file mode 100644
index 00000000..448c8d10
--- /dev/null
+++ b/test/scenario/output/xmlSpec.js
@@ -0,0 +1,33 @@
+describe('angular.scenario.output.json', function() {
+ var output, context;
+ var runner, $window;
+ var spec, step;
+
+ beforeEach(function() {
+ $window = {};
+ context = _jQuery('<div></div>');
+ runner = new angular.scenario.testing.MockRunner();
+ output = angular.scenario.output.xml(context, runner);
+ spec = {
+ name: 'test spec',
+ definition: {
+ id: 10,
+ name: 'describe',
+ }
+ };
+ step = {
+ name: 'some step',
+ line: function() { return 'unknown:-1'; },
+ };
+ });
+
+ it('should create XML nodes for object model', function() {
+ runner.emit('SpecBegin', spec);
+ runner.emit('StepBegin', spec, step);
+ runner.emit('StepEnd', spec, step);
+ runner.emit('SpecEnd', spec);
+ runner.emit('RunnerEnd');
+ expect(_jQuery(context).find('it').attr('status')).toEqual('success');
+ expect(_jQuery(context).find('it step').attr('status')).toEqual('success');
+ });
+});