aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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');
+ });
+});