aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorElliott Sprehn2010-10-08 16:43:40 -0700
committerElliott Sprehn2010-10-14 09:47:39 -0700
commit03df6cbddbb80186caf571e29957370b2ef9881c (patch)
treed5a321c8b207b464a5c8a300c422186e20e8ae31
parent0f104317dff5628765e26cc68df7dd1175b2aa5e (diff)
downloadangular.js-03df6cbddbb80186caf571e29957370b2ef9881c.tar.bz2
New Angular Scenario runner and DSL system with redesigned HTML UI.
Uses the Jasmine syntax for tests, ex: describe('widgets', function() { it('should verify that basic widgets work', function(){ navigateTo('widgets.html'); input('text.basic').enter('Carlos'); expect(binding('text.basic')).toEqual('Carlos'); input('text.basic').enter('Carlos Santana'); expect(binding('text.basic')).not().toEqual('Carlos Boozer'); input('text.password').enter('secret'); expect(binding('text.password')).toEqual('secret'); expect(binding('text.hidden')).toEqual('hiddenValue'); expect(binding('gender')).toEqual('male'); input('gender').select('female'); expect(binding('gender')).toEqual('female'); }); }); Note: To create new UI's implement the interface shown in angular.scenario.ui.Html.
-rw-r--r--Rakefile109
-rw-r--r--css/angular-scenario.css211
-rw-r--r--jsTestDriver-jquery.conf2
-rw-r--r--jsTestDriver.conf2
-rw-r--r--scenario/style.css4
-rw-r--r--scenario/widgets-scenario.js73
-rw-r--r--src/Angular.js4
-rw-r--r--src/scenario/Application.js51
-rw-r--r--src/scenario/DSL.js249
-rw-r--r--src/scenario/Describe.js108
-rw-r--r--src/scenario/Future.js23
-rw-r--r--src/scenario/HtmlUI.js204
-rw-r--r--src/scenario/Matcher.js21
-rw-r--r--src/scenario/Runner.js262
-rw-r--r--src/scenario/Scenario.js103
-rw-r--r--src/scenario/SpecRunner.js78
-rw-r--r--src/scenario/angular.prefix6
-rw-r--r--src/scenario/angular.suffix28
-rw-r--r--src/scenario/bootstrap.js62
-rw-r--r--src/scenario/matchers.js39
-rw-r--r--test/AngularSpec.js4
-rw-r--r--test/scenario/ApplicationSpec.js75
-rw-r--r--test/scenario/DSLSpec.js369
-rw-r--r--test/scenario/DescribeSpec.js85
-rw-r--r--test/scenario/FutureSpec.js38
-rw-r--r--test/scenario/HtmlUISpec.js87
-rw-r--r--test/scenario/MatcherSpec.js38
-rw-r--r--test/scenario/RunnerSpec.js302
-rw-r--r--test/scenario/SpecRunnerSpec.js165
-rw-r--r--test/scenario/TestContext.js15
-rw-r--r--test/scenario/matchersSpec.js43
-rw-r--r--test/testabilityPatch.js19
32 files changed, 1979 insertions, 900 deletions
diff --git a/Rakefile b/Rakefile
index 66a1e77b..fc20dc01 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,5 +1,46 @@
include FileUtils
+ANGULAR = [
+ 'src/Angular.js',
+ 'src/JSON.js',
+ 'src/Compiler.js',
+ 'src/Scope.js',
+ 'src/Injector.js',
+ 'src/Parser.js',
+ 'src/Resource.js',
+ 'src/Browser.js',
+ 'src/jqLite.js',
+ 'src/apis.js',
+ 'src/filters.js',
+ 'src/formatters.js',
+ 'src/validators.js',
+ 'src/services.js',
+ 'src/directives.js',
+ 'src/markups.js',
+ 'src/widgets.js',
+ 'src/AngularPublic.js',
+]
+
+ANGULAR_SCENARIO = [
+ 'src/scenario/Scenario.js',
+ 'src/scenario/Application.js',
+ 'src/scenario/Describe.js',
+ 'src/scenario/Future.js',
+ 'src/scenario/HtmlUI.js',
+ 'src/scenario/Describe.js',
+ 'src/scenario/Runner.js',
+ 'src/scenario/SpecRunner.js',
+ 'src/scenario/dsl.js',
+ 'src/scenario/matchers.js',
+]
+
+GENERATED_FILES = [
+ 'angular-debug.js',
+ 'angular-minified.js',
+ 'angular-minified.map',
+ 'angular-scenario.js',
+]
+
task :default => [:compile, :test]
desc 'Generate Externs'
@@ -20,31 +61,27 @@ task :compile_externs do
out.close
end
+desc 'Clean Generated Files'
+task :clean do
+ GENERATED_FILES.each do |file|
+ `rm #{file}`
+ end
+end
+
desc 'Compile Scenario'
task :compile_scenario do
- concat = %x(cat \
- lib/jquery/jquery-1.4.2.js \
- src/scenario/angular.prefix \
- src/Angular.js \
- src/jqLite.js \
- src/JSON.js \
- src/Scope.js \
- src/Injector.js \
- src/Parser.js \
- src/Resource.js \
- src/Browser.js \
- src/apis.js \
- src/services.js \
- src/AngularPublic.js \
- src/scenario/DSL.js \
- src/scenario/Future.js \
- src/scenario/Matcher.js \
- src/scenario/Runner.js \
- src/scenario/angular.suffix \
- )
+
+ deps = [
+ 'lib/jquery/jquery-1.4.2.js',
+ 'src/scenario/angular.prefix',
+ ANGULAR,
+ ANGULAR_SCENARIO,
+ 'src/scenario/angular.suffix',
+ ]
css = %x(cat css/angular-scenario.css)
+ concat = 'cat ' + deps.flatten.join(' ')
f = File.new("angular-scenario.js", 'w')
- f.write(concat)
+ f.write(%x{#{concat}})
f.write('document.write(\'<style type="text/css">\n')
f.write(css.gsub(/'/, "\\'").gsub(/\n/, "\\n"));
f.write('\n</style>\');')
@@ -54,30 +91,14 @@ end
desc 'Compile JavaScript'
task :compile => [:compile_externs, :compile_scenario] do
- concat = %x(cat \
- src/angular.prefix \
- src/Angular.js \
- src/JSON.js \
- src/Compiler.js \
- src/Scope.js \
- src/Injector.js \
- src/Parser.js \
- src/Resource.js \
- src/Browser.js \
- src/jqLite.js \
- src/apis.js \
- src/filters.js \
- src/formatters.js \
- src/validators.js \
- src/services.js \
- src/directives.js \
- src/markups.js \
- src/widgets.js \
- src/AngularPublic.js \
- src/angular.suffix \
- )
+ deps = [
+ 'src/angular.prefix',
+ ANGULAR,
+ 'src/angular.suffix',
+ ]
f = File.new("angular-debug.js", 'w')
- f.write(concat)
+ concat = 'cat ' + deps.flatten.join(' ')
+ f.write(%x{#{concat}})
f.close
%x(java -jar lib/compiler-closure/compiler.jar \
diff --git a/css/angular-scenario.css b/css/angular-scenario.css
index 3960c357..2cf24b19 100644
--- a/css/angular-scenario.css
+++ b/css/angular-scenario.css
@@ -1,76 +1,199 @@
@charset "UTF-8";
/* CSS Document */
-#runner {
- position: absolute;
- top:5px;
- left:10px;
- right:10px;
- height: 200px;
+/** Structure */
+body {
+ font-family: Arial, sans-serif;
+ margin: 0;
+ font-size: 14px;
}
-.console {
- display: block;
- overflow: scroll;
- height: 200px;
- border: 1px solid black;
+#header {
+ position: fixed;
+ width: 100%;
+}
+
+#specs {
+ padding-top: 50px;
+}
+
+#header .angular {
+ font-family: Courier New, monospace;
+ font-weight: bold;
+}
+
+#header h1 {
+ font-weight: normal;
+ float: left;
+ font-size: 30px;
+ line-height: 30px;
+ margin: 0;
+ padding: 10px 10px;
+ height: 30px;
+}
+
+#frame h2,
+#specs h2 {
+ margin: 0;
+ padding: 0.5em;
+ font-size: 1.1em;
+}
+
+#status-legend {
+ margin-top: 10px;
+ margin-right: 10px;
+}
+
+#header,
+#frame,
+.test-info,
+.test-actions li {
+ overflow: hidden;
}
-#testView {
- position: absolute;
- bottom:10px;
- top:230px;
- left:10px;
- right:10px;
+#frame {
+ margin: 10px;
}
-#testView iframe {
+#frame iframe {
width: 100%;
- height: 100%;
+ height: 758px;
+}
+
+#frame .popout {
+ float: right;
}
-li.running > span {
- background-color: yellow;
+#frame iframe {
+ border: none;
+}
+
+.tests li,
+.test-actions li,
+.test-it li,
+.test-it ol,
+.status-display {
+ list-style-type: none;
}
-#runner span {
- background-color: green;
+.tests,
+.test-it ol,
+.status-display {
+ margin: 0;
+ padding: 0;
}
-#runner .fail > span {
- background-color: red;
+.test-info {
+ margin-left: 1em;
+ margin-top: 0.5em;
+ border-radius: 8px 0 0 8px;
+ -webkit-border-radius: 8px 0 0 8px;
+ -moz-border-radius: 8px 0 0 8px;
+}
+
+.test-it ol {
+ margin-left: 2.5em;
}
-.collapsed > ul {
- display: none;
+.status-display,
+.status-display li {
+ float: right;
}
-//////
+.status-display li {
+ padding: 5px 10px;
+}
-.run, .info, .error {
- display: block;
- padding: 0 1em;
+.timer-result,
+.test-title {
+ display: inline-block;
+ margin: 0;
+ padding: 4px;
+}
+
+.timer-result {
+ width: 4em;
+ padding: 0 10px;
+ text-align: right;
font-family: monospace;
- white-space: pre;
}
-.run {
- background-color: lightgrey;
- padding: 0 .2em;
+.test-it pre,
+.test-actions pre {
+ clear: left;
+ margin-left: 6em;
}
-.run.pass {
- background-color: lightgreen;
+.test-describe .test-describe {
+ margin: 5px 5px 10px 2em;
}
-.run.fail {
- background-color: lightred;
+.test-actions .status-pending .test-title:before {
+ content: 'ยป ';
+}
+
+/** Colors */
+
+#header {
+ background-color: #F2C200;
}
-.name, .time, .state {
- padding-right: 2em;
+#specs h2 {
+ border-top: 2px solid #BABAD1;
}
-error {
- color: red;
-} \ No newline at end of file
+#specs h2,
+#frame h2 {
+ background-color: #efefef;
+}
+
+#frame {
+ border: 1px solid #BABAD1;
+}
+
+.test-describe .test-describe {
+ border-left: 1px solid #BABAD1;
+ border-right: 1px solid #BABAD1;
+ border-bottom: 1px solid #BABAD1;
+}
+
+.status-display {
+ border: 1px solid #777;
+}
+
+.status-display .status-pending,
+.status-pending .test-info {
+ background-color: #F9EEBC;
+}
+
+.status-display .status-success,
+.status-success .test-info {
+ background-color: #B1D7A1;
+}
+
+.status-display .status-failure,
+.status-failure .test-info {
+ background-color: #FF8286;
+}
+
+.status-display .status-error,
+.status-error .test-info {
+ background-color: black;
+ color: white;
+}
+
+.test-actions .status-success .test-title {
+ color: #30B30A;
+}
+
+.test-actions .status-failure .test-title {
+ color: #DF0000;
+}
+
+.test-actions .status-error .test-title {
+ color: black;
+}
+
+.test-actions .timer-result {
+ color: #888;
+}
diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf
index ed58d269..26bdd614 100644
--- a/jsTestDriver-jquery.conf
+++ b/jsTestDriver-jquery.conf
@@ -9,7 +9,7 @@ load:
- src/JSON.js
- src/*.js
- test/testabilityPatch.js
- - src/scenario/Runner.js
+ - src/scenario/Scenario.js
- src/scenario/*.js
- test/angular-mocks.js
- test/scenario/*.js
diff --git a/jsTestDriver.conf b/jsTestDriver.conf
index c7d74b75..9d3d980d 100644
--- a/jsTestDriver.conf
+++ b/jsTestDriver.conf
@@ -9,7 +9,7 @@ load:
- src/JSON.js
- src/*.js
- test/testabilityPatch.js
- - src/scenario/Runner.js
+ - src/scenario/Scenario.js
- src/scenario/*.js
- test/angular-mocks.js
- test/scenario/*.js
diff --git a/scenario/style.css b/scenario/style.css
index 956bdc52..43690e2c 100644
--- a/scenario/style.css
+++ b/scenario/style.css
@@ -5,3 +5,7 @@ th {
tr {
border: 1px solid black;
}
+
+.redbox {
+ background-color: red;
+} \ No newline at end of file
diff --git a/scenario/widgets-scenario.js b/scenario/widgets-scenario.js
index f4488190..69fdc10e 100644
--- a/scenario/widgets-scenario.js
+++ b/scenario/widgets-scenario.js
@@ -1,25 +1,58 @@
-describe('widgets', function(){
+describe('widgets', function() {
it('should verify that basic widgets work', function(){
- browser.navigateTo('widgets.html');
-
- expect('{{text.basic}}').toEqual('');
- input('text.basic').enter('John');
- expect('{{text.basic}}').toEqual('John');
-
- expect('{{text.password}}').toEqual('');
+ navigateTo('widgets.html');
+ input('text.basic').enter('Carlos');
+ expect(binding('text.basic')).toEqual('Carlos');
+ pause(2);
+ input('text.basic').enter('Carlos Santana');
+ pause(2);
+ expect(binding('text.basic')).not().toEqual('Carlos Boozer');
+ pause(2);
input('text.password').enter('secret');
- expect('{{text.password}}').toEqual('secret');
-
- expect('{{text.hidden}}').toEqual('hiddenValue');
-
- expect('{{gender}}').toEqual('male');
+ expect(binding('text.password')).toEqual('secret');
+ expect(binding('text.hidden')).toEqual('hiddenValue');
+ expect(binding('gender')).toEqual('male');
+ pause(2);
input('gender').select('female');
- input('gender').isChecked('female');
- expect('{{gender}}').toEqual('female');
-
-// expect('{{tea}}').toBeChecked();
-// input('gender').select('female');
-// expect('{{gender}}').toEqual('female');
-
+ expect(binding('gender')).toEqual('female');
+ pause(2);
+ });
+ describe('do it again', function() {
+ it('should verify that basic widgets work', function(){
+ navigateTo('widgets.html');
+ input('text.basic').enter('Carlos');
+ expect(binding('text.basic')).toEqual('Carlos');
+ pause(2);
+ input('text.basic').enter('Carlos Santana');
+ pause(2);
+ expect(binding('text.basic')).toEqual('Carlos Santana');
+ pause(2);
+ input('text.password').enter('secret');
+ expect(binding('text.password')).toEqual('secret');
+ expect(binding('text.hidden')).toEqual('hiddenValue');
+ expect(binding('gender')).toEqual('male');
+ pause(2);
+ input('gender').select('female');
+ expect(binding('gender')).toEqual('female');
+ pause(2);
+ });
+ });
+ it('should verify that basic widgets work', function(){
+ navigateTo('widgets.html');
+ input('text.basic').enter('Carlos');
+ expect(binding('text.basic')).toEqual('Carlos');
+ pause(2);
+ input('text.basic').enter('Carlos Santana');
+ pause(2);
+ expect(binding('text.basic')).toEqual('Carlos Santana');
+ pause(2);
+ input('text.password').enter('secret');
+ expect(binding('text.password')).toEqual('secret');
+ expect(binding('text.hidden')).toEqual('hiddenValue');
+ expect(binding('gender')).toEqual('male');
+ pause(2);
+ input('gender').select('female');
+ expect(binding('gender')).toEqual('female');
+ pause(2);
});
});
diff --git a/src/Angular.js b/src/Angular.js
index 95970850..72f341f3 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -270,11 +270,11 @@ function equals(o1, o2) {
} else {
keySet = {};
for(key in o1) {
- if (key.charAt(0) !== '$' && !equals(o1[key], o2[key])) return false;
+ if (key.charAt(0) !== '$' && !isFunction(o1[key]) && !equals(o1[key], o2[key])) return false;
keySet[key] = true;
}
for(key in o2) {
- if (key.charAt(0) !== '$' && keySet[key] !== true) return false;
+ if (!keySet[key] && key.charAt(0) !== '$' && !isFunction(o2[key])) return false;
}
return true;
}
diff --git a/src/scenario/Application.js b/src/scenario/Application.js
new file mode 100644
index 00000000..24ae99e9
--- /dev/null
+++ b/src/scenario/Application.js
@@ -0,0 +1,51 @@
+/**
+ * Represents the application currently being tested and abstracts usage
+ * of iframes or separate windows.
+ */
+angular.scenario.Application = function(context) {
+ this.context = context;
+ context.append('<h2>Current URL: <a href="about:blank">None</a></h2>');
+};
+
+/**
+ * Gets the jQuery collection of frames. Don't use this directly because
+ * frames may go stale.
+ *
+ * @return {Object} jQuery collection
+ */
+angular.scenario.Application.prototype.getFrame = function() {
+ return this.context.find('> iframe');
+};
+
+/**
+ * Gets the window of the test runner frame. Always favor executeAction()
+ * instead of this method since it prevents you from getting a stale window.
+ *
+ * @return {Object} the window of the frame
+ */
+angular.scenario.Application.prototype.getWindow = function() {
+ var contentWindow = this.getFrame().attr('contentWindow');
+ if (!contentWindow)
+ throw 'No window available because frame not loaded.';
+ return contentWindow;
+};
+
+/**
+ * Changes the location of the frame.
+ */
+angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) {
+ this.getFrame().remove();
+ this.context.append('<iframe src=""></iframe>');
+ 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.
+ *
+ * @param {Function} The callback to execute. function($window, $document)
+ */
+angular.scenario.Application.prototype.executeAction = function(action) {
+ var $window = this.getWindow();
+ return action.call($window, _jQuery($window.document), $window);
+};
diff --git a/src/scenario/DSL.js b/src/scenario/DSL.js
index dc85ea45..a7571afe 100644
--- a/src/scenario/DSL.js
+++ b/src/scenario/DSL.js
@@ -1,131 +1,134 @@
-angular.scenario.dsl.browser = {
- navigateTo: function(url){
- var location = this.location;
- return $scenario.addFuture('Navigate to: ' + url, function(done){
- var self = this;
- this.testFrame.load(function(){
- self.testFrame.unbind();
- self.testWindow = self.testFrame[0].contentWindow;
- self.testDocument = self.jQuery(self.testWindow.document);
- self.$browser = self.testWindow.angular.service.$browser();
- self.notifyWhenNoOutstandingRequests =
- bind(self.$browser, self.$browser.notifyWhenNoOutstandingRequests);
- self.notifyWhenNoOutstandingRequests(done);
- });
- if (this.testFrame.attr('src') == url) {
- this.testFrame[0].contentWindow.location.reload();
- } else {
- this.testFrame.attr('src', url);
- location.setLocation(url);
- }
- });
- },
- location: {
- href: "",
- hash: "",
- toEqual: function(url) {
- return (this.hash === "" ? (url == this.href) :
- (url == (this.href + "/#/" + this.hash)));
- },
- setLocation: function(url) {
- var urlParts = url.split("/#/");
- this.href = urlParts[0] || "";
- this.hash = urlParts[1] || "";
- }
- }
-};
-
-angular.scenario.dsl.input = function(selector) {
- var namePrefix = "input '" + selector + "'";
- return {
- enter: function(value) {
- return $scenario.addFuture(namePrefix + " enter '" + value + "'", function(done) {
- var input = this.testDocument.find('input[name=' + selector + ']');
- input.val(value);
- this.testWindow.angular.element(input[0]).trigger('change');
- done();
- });
- },
- select: function(value) {
- return $scenario.addFuture(namePrefix + " select '" + value + "'", function(done) {
- var input = this.testDocument.
- find(':radio[name$=@' + selector + '][value=' + value + ']');
- jqLiteWrap(input[0]).trigger('click');
- input[0].checked = true;
- done();
- });
- }
- };
-};
+/**
+ * Shared DSL statements that are useful to all scenarios.
+ */
-angular.scenario.dsl.NG_BIND_PATTERN =/\{\{[^\}]+\}\}/;
+/**
+* Usage:
+* pause(seconds) pauses the test for specified number of seconds
+*/
+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);
+ });
+ };
+});
-angular.scenario.dsl.repeater = function(selector) {
- var namePrefix = "repeater '" + selector + "'";
- return {
- count: function() {
- return $scenario.addFuture(namePrefix + ' count', function(done) {
- done(this.testDocument.find(selector).size());
- });
- },
- collect: function(collectSelector) {
- return $scenario.addFuture(
- namePrefix + " collect '" + collectSelector + "'",
- function(done) {
- var self = this;
- var doCollect = bind(this, function() {
- var repeaterArray = [], ngBindPattern;
- var startIndex = collectSelector.search(
- angular.scenario.dsl.NG_BIND_PATTERN);
- if (startIndex >= 0) {
- ngBindPattern = collectSelector.substring(
- startIndex + 2, collectSelector.length - 2);
- collectSelector = '*';
-
- }
- this.testDocument.find(selector).each(function() {
- var element = self.jQuery(this);
- element.find(collectSelector).
- each(function() {
- var foundElem = self.jQuery(this);
- if (foundElem.attr('ng:bind') == ngBindPattern) {
- repeaterArray.push(foundElem.text());
- }
- });
- });
- return repeaterArray;
- });
- done(doCollect());
- });
- }
+/**
+ * Usage:
+ * expect(future).{matcher} where matcher is one of the matchers defined
+ * with angular.scenario.matcher
+ *
+ * ex. expect(binding("name")).toEqual("Elliott")
+ */
+angular.scenario.dsl('expect', function() {
+ var chain = angular.extend({}, angular.scenario.matcher);
+
+ chain.not = function() {
+ this.inverse = true;
+ return chain;
+ };
+
+ return function(future) {
+ this.future = future;
+ return chain;
};
-};
+});
-angular.scenario.dsl.element = function(selector) {
- var namePrefix = "Element '" + selector + "'";
- var futureJquery = {};
- for (key in (jQuery || _jQuery).fn) {
- (function(){
- var jqFnName = key;
- var jqFn = (jQuery || _jQuery).fn[key];
- futureJquery[key] = function() {
- var jqArgs = arguments;
- return $scenario.addFuture(namePrefix + "." + jqFnName + "()",
- function(done) {
- var self = this, repeaterArray = [], ngBindPattern;
- var startIndex = selector.search(angular.scenario.dsl.NG_BIND_PATTERN);
- if (startIndex >= 0) {
- ngBindPattern = selector.substring(startIndex + 2, selector.length - 2);
- var element = this.testDocument.find('*').filter(function() {
- return self.jQuery(this).attr('ng:bind') == ngBindPattern;
+/**
+ * Usage:
+ * navigateTo(future|string) where url a string or future with a value
+ * of a URL to navigate to
+ */
+angular.scenario.dsl('navigateTo', function() {
+ return function(url) {
+ var application = this.application;
+ var name = url;
+ if (url.name) {
+ name = ' value of ' + url.name;
+ }
+ return this.addFuture('navigate to ' + name, function(done) {
+ application.navigateTo(url.value || url, function() {
+ application.executeAction(function() {
+ if (this.angular) {
+ var $browser = this.angular.service.$browser();
+ $browser.poll();
+ $browser.notifyWhenNoOutstandingRequests(function() {
+ done(null, url.value || url);
});
- done(jqFn.apply(element, jqArgs));
} else {
- done(jqFn.apply(this.testDocument.find(selector), jqArgs));
+ done(null, url.value || url);
}
});
- };
- })();
- }
- return futureJquery;
-};
+ });
+ });
+ };
+});
+
+/**
+ * Usage:
+ * input(name).enter(value) enters value in input with specified name
+ * input(name).check() checks checkbox
+ * input(name).select(value) selects the readio button with specified name/value
+ */
+angular.scenario.dsl('input', function() {
+ var chain = {};
+
+ chain.enter = function(value) {
+ var spec = this;
+ return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function(done) {
+ var input = _jQuery(this.document).find('input[name=' + spec.name + ']');
+ if (!input.length)
+ return done("Input named '" + spec.name + "' does not exist.");
+ input.val(value);
+ this.angular.element(input[0]).trigger('change');
+ done();
+ });
+ };
+
+ chain.check = function() {
+ var spec = this;
+ return this.addFutureAction("checkbox '" + this.name + "' toggle", function(done) {
+ var input = _jQuery(this.document).
+ find('input:checkbox[name=' + spec.name + ']');
+ if (!input.length)
+ return done("Input named '" + spec.name + "' does not exist.");
+ this.angular.element(input[0]).trigger('click');
+ input.attr('checked', !input.attr('checked'));
+ done();
+ });
+ };
+
+ chain.select = function(value) {
+ var spec = this;
+ return this.addFutureAction("radio button '" + this.name + "' toggle '" + value + "'", function(done) {
+ var input = _jQuery(this.document).
+ find('input:radio[name$="@' + spec.name + '"][value="' + value + '"]');
+ if (!input.length)
+ return done("Input named '" + spec.name + "' does not exist.");
+ this.angular.element(input[0]).trigger('click');
+ input.attr('checked', !input.attr('checked'));
+ done();
+ });
+ };
+
+ return function(name) {
+ this.name = name;
+ return chain;
+ };
+});
+
+/**
+ * Usage:
+ * binding(name) returns the value of a binding
+ */
+angular.scenario.dsl('binding', function() {
+ return function(name) {
+ return this.addFutureAction("select binding '" + name + "'", function(done) {
+ var element = _jQuery(this.document).find('[ng\\:bind="' + name + '"]');
+ if (!element.length)
+ return done("Binding named '" + name + "' does not exist.");
+ done(null, element.text());
+ });
+ };
+});
diff --git a/src/scenario/Describe.js b/src/scenario/Describe.js
new file mode 100644
index 00000000..896b337f
--- /dev/null
+++ b/src/scenario/Describe.js
@@ -0,0 +1,108 @@
+/**
+ * The representation of define blocks. Don't used directly, instead use
+ * define() in your tests.
+ */
+angular.scenario.Describe = function(descName, parent) {
+ this.beforeEachFns = [];
+ this.afterEachFns = [];
+ this.its = [];
+ this.children = [];
+ this.name = descName;
+ this.parent = parent;
+ this.id = angular.scenario.Describe.id++;
+
+ /**
+ * Calls all before functions.
+ */
+ var beforeEachFns = this.beforeEachFns;
+ this.setupBefore = function() {
+ if (parent) parent.setupBefore.call(this);
+ angular.foreach(beforeEachFns, function(fn) { fn.call(this); }, this);
+ };
+
+ /**
+ * Calls all after functions.
+ */
+ var afterEachFns = this.afterEachFns;
+ this.setupAfter = function() {
+ angular.foreach(afterEachFns, function(fn) { fn.call(this); }, this);
+ if (parent) parent.setupAfter.call(this);
+ };
+};
+
+// Shared Unique ID generator for every describe block
+angular.scenario.Describe.id = 0;
+
+/**
+ * Defines a block to execute before each it or nested describe.
+ *
+ * @param {Function} Body of the block.
+ */
+angular.scenario.Describe.prototype.beforeEach = function(body) {
+ this.beforeEachFns.push(body);
+};
+
+/**
+ * Defines a block to execute after each it or nested describe.
+ *
+ * @param {Function} Body of the block.
+ */
+angular.scenario.Describe.prototype.afterEach = function(body) {
+ this.afterEachFns.push(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.
+ */
+angular.scenario.Describe.prototype.describe = function(name, body) {
+ var child = new angular.scenario.Describe(name, this);
+ this.children.push(child);
+ body.call(child);
+};
+
+/**
+ * Use to disable a describe block.
+ */
+angular.scenario.Describe.prototype.xdescribe = angular.noop;
+
+/**
+ * Defines a test.
+ *
+ * @param {String} Name of the test.
+ * @param {Function} Body of the block.
+ */
+angular.scenario.Describe.prototype.it = function(name, body) {
+ var self = this;
+ this.its.push({
+ definition: this,
+ name: name,
+ fn: function() {
+ self.setupBefore.call(this);
+ body.call(this);
+ self.setupAfter.call(this);
+ }
+ });
+};
+
+/**
+ * Use to disable a test block.
+ */
+angular.scenario.Describe.prototype.xit = angular.noop;
+
+/**
+ * Gets an array of functions representing all the tests (recursively).
+ * that can be executed with SpecRunner's.
+ */
+angular.scenario.Describe.prototype.getSpecs = function() {
+ var specs = arguments[0] || [];
+ angular.foreach(this.children, function(child) {
+ child.getSpecs(specs);
+ });
+ angular.foreach(this.its, function(it) {
+ specs.push(it);
+ });
+ return specs;
+};
diff --git a/src/scenario/Future.js b/src/scenario/Future.js
index cc40eff0..60fad9c5 100644
--- a/src/scenario/Future.js
+++ b/src/scenario/Future.js
@@ -1,13 +1,22 @@
-function Future(name, behavior) {
+/**
+ * A future action in a spec.
+ */
+angular.scenario.Future = function(name, behavior) {
this.name = name;
this.behavior = behavior;
this.fulfilled = false;
- this.value = _undefined;
-}
+ this.value = undefined;
+};
-Future.prototype = {
- fulfill: function(value) {
+/**
+ * Executes the behavior of the closure.
+ *
+ * @param {Function} Callback function(error, result)
+ */
+angular.scenario.Future.prototype.execute = function(doneFn) {
+ this.behavior(angular.bind(this, function(error, result) {
this.fulfilled = true;
- this.value = value;
- }
+ this.value = error || result;
+ doneFn(error, result);
+ }));
};
diff --git a/src/scenario/HtmlUI.js b/src/scenario/HtmlUI.js
new file mode 100644
index 00000000..46c88837
--- /dev/null
+++ b/src/scenario/HtmlUI.js
@@ -0,0 +1,204 @@
+/**
+ * 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>'
+ );
+};
+
+/**
+ * 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 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,
+ angular.bind(this, function(status) {
+ var status = this.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 path = [];
+ var currentContext = this.context.find('#specs');
+ var currentDefinition = definition;
+ while (currentDefinition && currentDefinition.name) {
+ path.unshift(currentDefinition);
+ currentDefinition = currentDefinition.parent;
+ }
+ angular.foreach(path, angular.bind(this, function(defn) {
+ var id = 'describe-' + defn.id;
+ if (!this.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>'
+ );
+ this.context.find('#' + id).find('> h2').text('describe: ' + defn.name);
+ }
+ currentContext = this.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>' +
+ '<ol class="test-actions">' +
+ '</ol>'
+ );
+ 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.
+ */
+angular.scenario.ui.Html.Spec.prototype.addStep = function(name) {
+ this.context.find('> .test-actions').append('<li class="status-pending"></li>');
+ var stepContext = this.context.find('> .test-actions li:last');
+ var self = this;
+ return new angular.scenario.ui.Html.Step(stepContext, name, function(status) {
+ self.status = status;
+ });
+};
+
+/**
+ * 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");
+};
+
+/**
+ * Finishes the spec, possibly with an error.
+ *
+ * @param {Object} An optional error
+ */
+angular.scenario.ui.Html.Spec.prototype.finish = function(error) {
+ this.complete();
+ if (error) {
+ if (this.status !== 'failure') {
+ this.status = 'error';
+ }
+ this.context.append('<pre></pre>');
+ this.context.find('pre:first').text(error.stack || error.toString());
+ }
+ 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.finish(error);
+};
+
+/**
+ * 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} Callback function(status) to call when complete.
+ */
+angular.scenario.ui.Html.Step = function(context, name, doneFn) {
+ this.context = context;
+ this.name = name;
+ this.startTime = new Date().getTime();
+ this.doneFn = doneFn;
+ context.append(
+ '<span class="timer-result"></span>' +
+ '<span class="test-title"></span>'
+ );
+ context.find('> .test-title').text(name);
+};
+
+/**
+ * Completes the step and sets the timer value.
+ */
+angular.scenario.ui.Html.Step.prototype.complete = function() {
+ this.context.removeClass('status-pending');
+ var endTime = new Date().getTime();
+ this.context.find(".timer-result")
+ .text((endTime - this.startTime) + "ms");
+};
+
+/**
+ * Finishes the step, possibly with an error.
+ *
+ * @param {Object} An optional error
+ */
+angular.scenario.ui.Html.Step.prototype.finish = function(error) {
+ this.complete();
+ 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();
+ this.context.addClass('status-error');
+ this.doneFn('error');
+};
diff --git a/src/scenario/Matcher.js b/src/scenario/Matcher.js
deleted file mode 100644
index a9c86571..00000000
--- a/src/scenario/Matcher.js
+++ /dev/null
@@ -1,21 +0,0 @@
-function Matcher(scope, future, logger) {
- var self = scope.$scenario = this;
- this.logger = logger;
- this.future = future;
-}
-
-Matcher.addMatcher = function(name, matcher) {
- Matcher.prototype[name] = function(expected) {
- var future = this.future;
- $scenario.addFuture(
- 'expect ' + future.name + ' ' + name + ' ' + expected,
- function(done){
- if (!matcher(future.value, expected))
- throw "Expected " + expected + ' but was ' + future.value;
- done();
- }
- );
- };
-};
-
-Matcher.addMatcher('toEqual', angular.equals);
diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js
index 77969618..0267bb2d 100644
--- a/src/scenario/Runner.js
+++ b/src/scenario/Runner.js
@@ -1,183 +1,95 @@
-angular['scenario'] = angular['scenario'] || (angular['scenario'] = {});
-angular.scenario['dsl'] = angular.scenario['dsl'] || (angular.scenario['dsl'] = {});
-
-angular.scenario.Runner = function(scope, jQuery){
- var self = scope.$scenario = this;
- this.scope = scope;
- this.jQuery = jQuery;
- this.scope.$testrun = {done: false, results: []};
-
- var specs = this.specs = {};
- this.currentSpec = {name: '', futures: []};
- var path = [];
- this.scope.describe = function(name, body){
- path.push(name);
- body();
- path.pop();
- };
- var beforeEach = noop;
- var afterEach = noop;
- this.scope.beforeEach = function(body) {
- beforeEach = body;
- };
- this.scope.afterEach = function(body) {
- afterEach = body;
- };
- this.scope.expect = function(future) {
- return new Matcher(self, future, self.logger);
+/**
+ * Runner for scenarios.
+ */
+angular.scenario.Runner = function($window) {
+ this.$window = $window;
+ this.rootDescribe = new angular.scenario.Describe();
+ this.currentDescribe = this.rootDescribe;
+ this.api = {
+ it: this.it,
+ xit: angular.noop,
+ describe: this.describe,
+ xdescribe: angular.noop,
+ beforeEach: this.beforeEach,
+ afterEach: this.afterEach
};
- this.scope.it = function(name, body) {
- var specName = path.join(' ') + ': it ' + name;
- self.currentSpec = specs[specName] = {
- name: specName,
- futures: []
- };
+ angular.foreach(this.api, angular.bind(this, function(fn, key) {
+ this.$window[key] = angular.bind(this, fn);
+ }));
+};
+
+/**
+ * Defines a describe block of a spec.
+ *
+ * @param {String} Name of the block
+ * @param {Function} Body of the block
+ */
+angular.scenario.Runner.prototype.describe = function(name, body) {
+ var self = this;
+ this.currentDescribe.describe(name, function() {
+ var parentDescribe = self.currentDescribe;
+ self.currentDescribe = this;
try {
- beforeEach();
- body();
- } catch(err) {
- self.addFuture(err.message || 'ERROR', function(){
- throw err;
- });
+ body.call(this);
} finally {
- afterEach();
+ self.currentDescribe = parentDescribe;
}
- self.currentSpec = _null;
- };
- this.logger = function returnNoop(){
- return extend(returnNoop, {close:noop, fail:noop});
- };
+ });
};
-angular.scenario.Runner.prototype = {
- run: function(body){
- var jQuery = this.jQuery;
- body.append(
- '<div id="runner">' +
- '<div class="console"></div>' +
- '</div>' +
- '<div id="testView">' +
- '<iframe></iframe>' +
- '</div>');
- var console = body.find('#runner .console');
- console.find('li').live('click', function(){
- jQuery(this).toggleClass('collapsed');
- });
- this.testFrame = body.find('#testView iframe');
- function logger(parent) {
- var container;
- return function(type, text) {
- if (!container) {
- container = jQuery('<ul></ul>');
- parent.append(container);
- }
- var element = jQuery('<li class="running '+type+'"><span></span></li>');
- element.find('span').text(text);
- container.append(element);
- return extend(logger(element), {
- close: function(){
- element.removeClass('running');
- if(!element.hasClass('fail'))
- element.addClass('collapsed');
- console.scrollTop(console[0].scrollHeight);
- },
- fail: function(){
- element.removeClass('running');
- var current = element;
- while (current[0] != console[0]) {
- if (current.is('li'))
- current.addClass('fail');
- current = current.parent();
- }
- }
- });
- };
- }
- this.logger = logger(console);
- var specNames = [];
- foreach(this.specs, function(spec, name){
- specNames.push(name);
- }, this);
- specNames.sort();
- var self = this;
- function callback(){
- var next = specNames.shift();
- if(next) {
- self.execute(next, callback);
- } else {
- self.scope.$testrun.done = true;
- }
- }
- callback();
- },
+/**
+ * Defines a test in a describe block of a spec.
+ *
+ * @param {String} Name of the block
+ * @param {Function} Body of the block
+ */
+angular.scenario.Runner.prototype.it = function(name, body) {
+ this.currentDescribe.it(name, body);
+};
- addFuture: function(name, behavior) {
- var future = new Future(name, behavior);
- this.currentSpec.futures.push(future);
- return future;
- },
+/**
+ * Defines a function to be called before each it block in the describe
+ * (and before all nested describes).
+ *
+ * @param {Function} Callback to execute
+ */
+angular.scenario.Runner.prototype.beforeEach = function(body) {
+ this.currentDescribe.beforeEach(body);
+};
- execute: function(name, callback) {
- var spec = this.specs[name],
- self = this,
- futuresFulfilled = [],
- result = {
- passed: false,
- failed: false,
- finished: false,
- fail: function(error) {
- result.passed = false;
- result.failed = true;
- result.error = error;
- result.log('fail', isString(error) ? error : toJson(error)).fail();
- }
- },
- specThis = createScope({
- result: result,
- jQuery: this.jQuery,
- testFrame: this.testFrame,
- testWindow: this.testWindow
- }, angularService, {});
- this.self = specThis;
- var futureLogger = this.logger('spec', name);
- spec.nextFutureIndex = 0;
- function done() {
- result.finished = true;
- futureLogger.close();
- self.self = _null;
- (callback||noop).call(specThis);
- }
- function next(value){
- if (spec.nextFutureIndex > 0) {
- spec.futures[spec.nextFutureIndex - 1].fulfill(value);
- }
- var future = spec.futures[spec.nextFutureIndex];
- (result.log || {close:noop}).close();
- result.log = _null;
- if (future) {
- spec.nextFutureIndex ++;
- result.log = futureLogger('future', future.name);
- futuresFulfilled.push(future.name);
- try {
- future.behavior.call(specThis, next);
- } catch (e) {
- console.error(e);
- result.fail(e);
- self.scope.$testrun.results.push(
- {name: name, passed: false, error: e, steps: futuresFulfilled});
- done();
- }
- } else {
- result.passed = !result.failed;
- self.scope.$testrun.results.push({
- name: name,
- passed: !result.failed,
- error: result.error,
- steps: futuresFulfilled});
- done();
- }
- }
- next();
- return specThis;
- }
-}; \ No newline at end of file
+/**
+ * Defines a function to be called after each it block in the describe
+ * (and before all nested describes).
+ *
+ * @param {Function} Callback to execute
+ */
+angular.scenario.Runner.prototype.afterEach = function(body) {
+ this.currentDescribe.afterEach(body);
+};
+
+/**
+ * Defines a function to be called before each it block in the describe
+ * (and before all nested describes).
+ *
+ * @param {Function} Callback to execute
+ */
+angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClass, specsDone) {
+ var $root = angular.scope({}, angular.service);
+ var self = this;
+ var specs = this.rootDescribe.getSpecs();
+ $root.application = application;
+ $root.ui = ui;
+ $root.setTimeout = function() {
+ return self.$window.setTimeout.apply(self.$window, arguments);
+ };
+ asyncForEach(specs, angular.bind(this, function(spec, specDone) {
+ var runner = angular.scope($root);
+ runner.$become(specRunnerClass);
+ angular.foreach(angular.scenario.dsl, angular.bind(this, function(fn, key) {
+ this.$window[key] = function() {
+ return fn.call($root).apply(angular.scope(runner), arguments);
+ }
+ }));
+ runner.run(ui, spec, specDone);
+ }), specsDone || angular.noop);
+};
diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js
new file mode 100644
index 00000000..e93f6b2e
--- /dev/null
+++ b/src/scenario/Scenario.js
@@ -0,0 +1,103 @@
+/**
+ * Setup file for the Scenario.
+ * Must be first in the compilation/bootstrap list.
+ */
+
+// Public namespace
+angular.scenario = {};
+
+// Namespace for the UI
+angular.scenario.ui = {};
+
+/**
+ * Defines a new DSL statement. If your factory function returns a Future
+ * it's returned, otherwise the result is assumed to be a map of functions
+ * for chaining. Chained functions are subject to the same rules.
+ *
+ * Note: All functions on the chain are bound to the chain scope so values
+ * 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
+ * the statement.
+ */
+angular.scenario.dsl = function(name, fn) {
+ angular.scenario.dsl[name] = function() {
+ function executeStatement(statement, args) {
+ var result = statement.apply(this, args);
+ if (angular.isFunction(result) || result instanceof angular.scenario.Future)
+ return result;
+ var self = this;
+ var chain = angular.extend({}, result);
+ angular.foreach(chain, function(value, name) {
+ if (angular.isFunction(value)) {
+ chain[name] = angular.bind(self, function() {
+ return executeStatement.call(self, value, arguments);
+ });
+ } else {
+ chain[name] = value;
+ }
+ });
+ return chain;
+ }
+ var statement = fn.apply(this, arguments);
+ return function() {
+ return executeStatement.call(this, statement, arguments);
+ };
+ };
+};
+
+/**
+ * Defines a new matcher for use with the expects() statement. The value
+ * this.actual (like in Jasmine) is available in your matcher to compare
+ * 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).
+ */
+angular.scenario.matcher = function(name, fn) {
+ angular.scenario.matcher[name] = function(expected) {
+ var prefix = 'expect ' + this.future.name + ' ';
+ if (this.inverse) {
+ prefix += 'not ';
+ }
+ this.addFuture(prefix + name + ' ' + angular.toJson(expected),
+ angular.bind(this, function(done) {
+ this.actual = this.future.value;
+ if ((this.inverse && fn.call(this, expected)) ||
+ (!this.inverse && !fn.call(this, expected))) {
+ this.error = 'expected ' + angular.toJson(expected) +
+ ' but was ' + angular.toJson(this.actual);
+ }
+ done(this.error);
+ })
+ );
+ };
+};
+
+/**
+ * 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.
+ */
+function asyncForEach(list, iterator, done) {
+ var i = 0;
+ function loop(error) {
+ if (error || i >= list.length) {
+ done(error);
+ } else {
+ try {
+ iterator(list[i++], loop);
+ } catch (e) {
+ done(e);
+ }
+ }
+ }
+ loop();
+}
diff --git a/src/scenario/SpecRunner.js b/src/scenario/SpecRunner.js
new file mode 100644
index 00000000..8b6d4ef1
--- /dev/null
+++ b/src/scenario/SpecRunner.js
@@ -0,0 +1,78 @@
+/**
+ * This class is the "this" of the it/beforeEach/afterEach method.
+ * Responsibilities:
+ * - "this" for it/beforeEach/afterEach
+ * - keep state for single it/beforeEach/afterEach execution
+ * - keep track of all of the futures to execute
+ * - run single spec (execute each future)
+ */
+angular.scenario.SpecRunner = function() {
+ this.futures = [];
+};
+
+/**
+ * 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 {Function} Callback function that is called when the spec finshes.
+ */
+angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
+ var specUI = ui.addSpec(spec);
+
+ try {
+ spec.fn.call(this);
+ } catch (e) {
+ specUI.error(e);
+ specDone();
+ return;
+ }
+
+ asyncForEach(
+ this.futures,
+ function(future, futureDone) {
+ var stepUI = specUI.addStep(future.name);
+ try {
+ future.execute(function(error) {
+ stepUI.finish(error);
+ futureDone(error);
+ });
+ } catch (e) {
+ stepUI.error(e);
+ rethrow(e);
+ }
+ },
+ function(e) {
+ specUI.finish(e);
+ specDone();
+ }
+ );
+};
+
+/**
+ * Adds a new future action.
+ *
+ * @param {String} Name of the future
+ * @param {Function} Behavior of the future
+ */
+angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior) {
+ var future = new angular.scenario.Future(name, angular.bind(this, behavior));
+ this.futures.push(future);
+ return future;
+};
+
+/**
+ * Adds a new future action to be executed on the application window.
+ *
+ * @param {String} Name of the future
+ * @param {Function} Behavior of the future
+ */
+angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior) {
+ return this.addFuture(name, function(done) {
+ this.application.executeAction(function() {
+ behavior.call(this, done);
+ });
+ });
+};
diff --git a/src/scenario/angular.prefix b/src/scenario/angular.prefix
index 5b44e17c..a1b4e151 100644
--- a/src/scenario/angular.prefix
+++ b/src/scenario/angular.prefix
@@ -22,9 +22,3 @@
* THE SOFTWARE.
*/
(function(window, document, previousOnLoad){
- window.angular = {
- scenario: {
- dsl: window
- }
- };
-
diff --git a/src/scenario/angular.suffix b/src/scenario/angular.suffix
index fc861cbf..53d99dd2 100644
--- a/src/scenario/angular.suffix
+++ b/src/scenario/angular.suffix
@@ -1,11 +1,31 @@
+ var $scenario = new angular.scenario.Runner(window);
- var $scenarioRunner = new angular.scenario.Runner(window, jQuery);
-
- window.onload = function(){
+ window.onload = function() {
try {
if (previousOnLoad) previousOnLoad();
} catch(e) {}
- $scenarioRunner.run(jQuery(window.document.body));
+ 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, function(error) {
+ frame.remove();
+ if (error) {
+ if (window.console) {
+ console.log(error);
+ if (error.stack) {
+ console.log(error.stack);
+ }
+ } else {
+ // Do something for IE
+ alert(error);
+ }
+ }
+ });
};
})(window, document, window.onload);
diff --git a/src/scenario/bootstrap.js b/src/scenario/bootstrap.js
index f74305c3..014c636d 100644
--- a/src/scenario/bootstrap.js
+++ b/src/scenario/bootstrap.js
@@ -1,4 +1,4 @@
-(function(onLoadDelegate){
+(function(previousOnLoad){
var prefix = (function(){
var filename = /(.*\/)bootstrap.js(#(.*))?/;
var scripts = document.getElementsByTagName("script");
@@ -10,6 +10,7 @@
}
}
})();
+
function addScript(path) {
document.write('<script type="text/javascript" src="' + prefix + path + '"></script>');
}
@@ -18,26 +19,51 @@
document.write('<link rel="stylesheet" type="text/css" href="' + prefix + path + '"/>');
}
- window.angular = {
- scenario: {
- dsl: window
- }
- };
-
window.onload = function(){
- setTimeout(function(){
- $scenarioRunner.run(jQuery(window.document.body));
- }, 0);
- (onLoadDelegate||function(){})();
+ 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);
+ }
+ }
+ });
};
+
addCSS("../../css/angular-scenario.css");
addScript("../../lib/jquery/jquery-1.4.2.js");
+ addScript("../angular-bootstrap.js");
+
+ addScript("Scenario.js");
+ addScript("Application.js");
+ addScript("Describe.js");
+ addScript("Future.js");
+ addScript("HtmlUI.js");
addScript("Runner.js");
- addScript("../Angular.js");
- addScript("../JSON.js");
- addScript("DSL.js");
- document.write('<script type="text/javascript">' +
- '$scenarioRunner = new angular.scenario.Runner(window, jQuery);' +
- '</script>');
-})(window.onload);
+ addScript("SpecRunner.js");
+ addScript("dsl.js");
+ addScript("matchers.js");
+ // Create the runner (which also sets up the global API)
+ document.write(
+ '<script type="text/javascript">' +
+ 'var _jQuery = jQuery.noConflict(true);' +
+ 'var $scenario = new angular.scenario.Runner(window);' +
+ '</script>'
+ );
+
+})(window.onload);
diff --git a/src/scenario/matchers.js b/src/scenario/matchers.js
new file mode 100644
index 00000000..0dfbc455
--- /dev/null
+++ b/src/scenario/matchers.js
@@ -0,0 +1,39 @@
+/**
+ * Matchers for implementing specs. Follows the Jasmine spec conventions.
+ */
+
+angular.scenario.matcher('toEqual', function(expected) {
+ return angular.equals(this.actual, expected);
+});
+
+angular.scenario.matcher('toBeDefined', function() {
+ return angular.isDefined(this.actual);
+});
+
+angular.scenario.matcher('toBeTruthy', function() {
+ return this.actual;
+});
+
+angular.scenario.matcher('toBeFalsy', function() {
+ return !this.actual;
+});
+
+angular.scenario.matcher('toMatch', function(expected) {
+ return new RegExp(expected).test(this.actual);
+});
+
+angular.scenario.matcher('toBeNull', function() {
+ return this.actual === null;
+});
+
+angular.scenario.matcher('toContain', function(expected) {
+ return includes(this.actual, expected);
+});
+
+angular.scenario.matcher('toBeLessThan', function(expected) {
+ return this.actual < expected;
+});
+
+angular.scenario.matcher('toBeGreaterThan', function(expected) {
+ return this.actual > expected;
+});
diff --git a/test/AngularSpec.js b/test/AngularSpec.js
index e0228e16..6faed707 100644
--- a/test/AngularSpec.js
+++ b/test/AngularSpec.js
@@ -86,6 +86,10 @@ describe('equals', function(){
expect(equals({name:'misko'}, {name:'misko', $id:2})).toEqual(true);
expect(equals({name:'misko', $id:1}, {name:'misko'})).toEqual(true);
});
+
+ it('should ignore functions', function(){
+ expect(equals({func: function() {}}, {bar: function() {}})).toEqual(true);
+ });
});
describe('parseKeyValue', function() {
diff --git a/test/scenario/ApplicationSpec.js b/test/scenario/ApplicationSpec.js
new file mode 100644
index 00000000..706fbc36
--- /dev/null
+++ b/test/scenario/ApplicationSpec.js
@@ -0,0 +1,75 @@
+describe('angular.scenario.Application', function() {
+ var app, frames;
+
+ beforeEach(function() {
+ frames = _jQuery("<div></div>");
+ app = new angular.scenario.Application(frames);
+ });
+
+ it('should return new $window and $document after navigate', function() {
+ var testWindow, testDocument, counter = 0;
+ app.navigateTo = noop;
+ app.getWindow = function() {
+ return {x:counter++, document:{x:counter++}};
+ };
+ app.navigateTo('http://www.google.com/');
+ app.executeAction(function($document, $window) {
+ testWindow = $window;
+ testDocument = $document;
+ });
+ app.navigateTo('http://www.google.com/');
+ app.executeAction(function($document, $window) {
+ expect($window).not.toEqual(testWindow);
+ expect($document).not.toEqual(testDocument);
+ });
+ });
+
+ it('should execute callback on $window of frame', function() {
+ var testWindow = {document: {}};
+ app.getWindow = function() {
+ return testWindow;
+ };
+ app.executeAction(function($document, $window) {
+ expect(this).toEqual($window);
+ expect(this).toEqual(testWindow);
+ });
+ });
+
+ it('should create a new iframe each time', function() {
+ app.navigateTo('about:blank');
+ var frame = app.getFrame();
+ frame.attr('test', true);
+ app.navigateTo('about:blank');
+ expect(app.getFrame().attr('test')).toBeFalsy();
+ });
+
+ it('should URL description bar', function() {
+ app.navigateTo('about:blank');
+ var anchor = frames.find('> h2 a');
+ expect(anchor.attr('href')).toEqual('about:blank');
+ expect(anchor.text()).toEqual('about:blank');
+ });
+
+ 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.navigateTo('about:blank', function() {
+ called = true;
+ });
+ expect(called).toBeTruthy();
+ });
+});
diff --git a/test/scenario/DSLSpec.js b/test/scenario/DSLSpec.js
index 7a8e2e3b..9b011847 100644
--- a/test/scenario/DSLSpec.js
+++ b/test/scenario/DSLSpec.js
@@ -1,181 +1,232 @@
-describe("DSL", function() {
+/**
+ * Very basic Mock of angular.
+ */
+function AngularMock() {
+ this.reset();
+ this.service = this;
+}
- var lastDocument, executeFuture, Expect;
+AngularMock.prototype.reset = function() {
+ this.log = [];
+};
- beforeEach(function() {
- setUpContext();
- executeFuture = function(future, html, callback) {
- lastDocument = _jQuery('<div>' + html + '</div>');
- lastFrame = _jQuery('<iframe>' + lastDocument + '</iframe>');
- _jQuery(document.body).append(lastDocument);
- var specThis = {
- testWindow: window,
- testDocument: lastDocument,
- testFrame: lastFrame,
- jQuery: _jQuery
- };
- future.behavior.call(specThis, callback || noop);
- };
- Expect = _window.expect;
- });
-
- describe("input", function() {
+AngularMock.prototype.element = function(node) {
+ this.log.push('element(' + node.nodeName.toLowerCase() + ')');
+ return this;
+};
- var input = angular.scenario.dsl.input;
+AngularMock.prototype.trigger = function(value) {
+ this.log.push('element().trigger(' + value + ')');
+};
- it('should enter', function() {
- var future = input('name').enter('John');
- expect(future.name).toEqual("input 'name' enter 'John'");
- executeFuture(future, '<input type="text" name="name" />');
- expect(lastDocument.find('input').val()).toEqual('John');
- });
+AngularMock.prototype.$browser = function() {
+ this.log.push('$brower()');
+ return this;
+};
- it('should select', function() {
- var future = input('gender').select('female');
- expect(future.name).toEqual("input 'gender' select 'female'");
- executeFuture(future,
- '<input type="radio" name="0@gender" value="male" checked/>' +
- '<input type="radio" name="0@gender" value="female"/>');
- expect(lastDocument.find(':radio:checked').length).toEqual(1);
- expect(lastDocument.find(':radio:checked').val()).toEqual('female');
- });
- });
+AngularMock.prototype.poll = function() {
+ this.log.push('$brower.poll()');
+ return this;
+};
- describe('browser', function() {
- var browser = angular.scenario.dsl.browser;
- it('shoud return true if location with empty hash provided is same ' +
- 'as location of the page', function() {
- browser.location.href = "http://server";
- expect(browser.location.toEqual("http://server")).toEqual(true);
- });
- it('shoud return true if location with hash provided is same ' +
- 'as location of the page', function() {
- browser.location.href = "http://server";
- browser.location.hash = "hashPath";
- expect(browser.location.toEqual("http://server/#/hashPath")).toEqual(true);
- });
- it('should return true if the location provided is the same as which ' +
- 'browser navigated to', function() {
- var future = browser.navigateTo("http://server/#/hashPath");
- expect(future.name).toEqual("Navigate to: http://server/#/hashPath");
- executeFuture(future, '<input type="text" name="name" />');
- expect(browser.location.toEqual("http://server/#/hashPath")).toEqual(true);
- expect(browser.location.toEqual("http://server/")).toEqual(false);
+AngularMock.prototype.notifyWhenNoOutstandingRequests = function(fn) {
+ this.log.push('$brower.notifyWhenNoOutstandingRequests()');
+ fn();
+};
- future = browser.navigateTo("http://server/");
- expect(future.name).toEqual("Navigate to: http://server/");
- executeFuture(future, '<input type="text" name="name" />');
- expect(browser.location.toEqual("http://server/")).toEqual(true);
- });
+describe("angular.scenario.dsl", function() {
+ var $window;
+ var $root;
+ var application;
+
+ beforeEach(function() {
+ $window = {
+ document: _jQuery("<div></div>"),
+ angular: new AngularMock()
+ };
+ $root = angular.scope({}, angular.service);
+ $root.futures = [];
+ $root.addFuture = function(name, fn) {
+ this.futures.push(name);
+ fn.call(this, function(error, result) {
+ $root.futureError = error;
+ $root.futureResult = result;
+ });
+ };
+ $root.application = new angular.scenario.Application($window.document);
+ $root.application.getWindow = function() {
+ return $window;
+ };
+ $root.application.navigateTo = function(url, callback) {
+ $window.location = url;
+ callback();
+ };
+ // Just use the real one since it delegates to this.addFuture
+ $root.addFutureAction = angular.scenario.
+ SpecRunner.prototype.addFutureAction;
});
-
- describe('repeater', function() {
-
- var repeater = angular.scenario.dsl.repeater;
- var html;
+
+ describe('Pause', function() {
beforeEach(function() {
- html = "<table>" +
- "<tr class='epic'>" +
- "<td class='hero-name'>" +
- "<span ng:bind='hero'>John Marston</span>" +
- "</td>" +
- "<td class='game-name'>" +
- "<span ng:bind='game'>Red Dead Redemption</span>" +
- "</td>" +
- "</tr>" +
- "<tr class='epic'>" +
- "<td class='hero-name'>" +
- "<span ng:bind='hero'>Nathan Drake</span>" +
- "</td>" +
- "<td class='game-name'>" +
- "<span ng:bind='game'>Uncharted</span>" +
- "</td>" +
- "</tr>" +
- "</table>";
+ $root.setTimeout = function(fn, value) {
+ $root.timerValue = value;
+ fn();
+ };
});
- it('should count', function() {
- var future = repeater('.repeater-row').count();
- expect(future.name).toEqual("repeater '.repeater-row' count");
- executeFuture(future,
- "<div class='repeater-row'>a</div>" +
- "<div class='repeater-row'>b</div>",
- function(value) {
- future.fulfill(value);
- });
- expect(future.fulfilled).toBeTruthy();
- expect(future.value).toEqual(2);
+
+ it('should pause for specified seconds', function() {
+ angular.scenario.dsl.pause.call($root).call($root, 10);
+ expect($root.timerValue).toEqual(10000);
+ expect($root.futureResult).toEqual(10000);
+ });
+ });
+
+ describe('Expect', function() {
+ it('should chain and execute matcher', function() {
+ var future = {value: 10};
+ var result = angular.scenario.dsl.expect.call($root).call($root, future);
+ result.toEqual(10);
+ expect($root.futureError).toBeUndefined();
+ expect($root.futureResult).toBeUndefined();
+ var result = angular.scenario.dsl.expect.call($root).call($root, future);
+ result.toEqual(20);
+ expect($root.futureError).toBeDefined();
});
-
- function assertFutureState(future, expectedName, expectedValue) {
- expect(future.name).toEqual(expectedName);
- executeFuture(future, html, function(value) {
- future.fulfill(value);
- });
- expect(future.fulfilled).toBeTruthy();
- expect(future.value).toEqual(expectedValue);
- }
- it('should collect bindings', function() {
- assertFutureState(repeater('.epic').collect('{{hero}}'),
- "repeater '.epic' collect '{{hero}}'",
- ['John Marston', 'Nathan Drake']);
- assertFutureState(repeater('.epic').collect('{{game}}'),
- "repeater '.epic' collect '{{game}}'",
- ['Red Dead Redemption', 'Uncharted']);
+ });
+
+ describe('NavigateTo', function() {
+ it('should allow a string url', function() {
+ angular.scenario.dsl.navigateTo.call($root).call($root, 'http://myurl');
+ expect($window.location).toEqual('http://myurl');
+ expect($root.futureResult).toEqual('http://myurl');
+ });
+
+ it('should allow a future url', function() {
+ var future = {name: 'future name', value: 'http://myurl'};
+ angular.scenario.dsl.navigateTo.call($root).call($root, future);
+ expect($window.location).toEqual('http://myurl');
+ expect($root.futureResult).toEqual('http://myurl');
});
- it('should collect normal selectors', function() {
- assertFutureState(repeater('.epic').collect('.hero-name'),
- "repeater '.epic' collect '.hero-name'",
- ['John Marston', 'Nathan Drake']);
- assertFutureState(repeater('.epic').collect('.game-name'),
- "repeater '.epic' collect '.game-name'",
- ['Red Dead Redemption', 'Uncharted']);
+
+ it('should complete if angular is missing from app frame', function() {
+ delete $window.angular;
+ angular.scenario.dsl.navigateTo.call($root).call($root, 'http://myurl');
+ expect($window.location).toEqual('http://myurl');
+ expect($root.futureResult).toEqual('http://myurl');
});
- it('should collect normal attributes', function() {
- //TODO(shyamseshadri) : Left as an exercise to the user
+
+ it('should wait for angular notify when no requests pending', function() {
+ angular.scenario.dsl.navigateTo.call($root).call($root, 'url');
+ expect($window.angular.log).toContain('$brower.poll()');
+ expect($window.angular.log)
+ .toContain('$brower.notifyWhenNoOutstandingRequests()');
});
});
-
- describe('element', function() {
- var element = angular.scenario.dsl.element;
- var html;
+
+ describe('Element Finding', function() {
+ var doc;
+ //TODO(esprehn): Work around a bug in jQuery where attribute selectors
+ // only work if they are executed on a real document, not an element.
+ //
+ // ex. jQuery('#foo').find('[name="bar"]') // fails
+ // ex. jQuery('#foo [name="bar"]') // works, wtf?
+ //
beforeEach(function() {
- html = '<div class="container">' +
- '<div class="reports-detail">' +
- '<span class="desc">Description : ' +
- '<span ng:bind="report.description">Details...</span>' +
- '</span>' +
- '<span>Date created: ' +
- '<span ng:bind="report.creationDate">01/01/01</span>' +
- '</span>' +
- '</div>' +
- '</div>';
+ doc = _jQuery('<div id="angular-scenario-binding"></div>');
+ _jQuery(document.body).append(doc);
+ $window.document = window.document;
});
- function timeTravel(future) {
- executeFuture(future, html, function(value) { future.fulfill(value); });
- expect(future.fulfilled).toBeTruthy();
- }
- it('should find elements on the page and provide jquery api', function() {
- var future = element('.reports-detail').text();
- expect(future.name).toEqual("Element '.reports-detail'.text()");
- timeTravel(future);
- expect(future.value).
- toEqual('Description : Details...Date created: 01/01/01');
-// expect(future.value.find('.desc').text()).
-// toEqual('Description : Details...');
+
+ afterEach(function() {
+ _jQuery(document.body)
+ .find('#angular-scenario-binding')
+ .remove();
});
- it('should find elements with angular syntax', function() {
- var future = element('{{report.description}}').text();
- expect(future.name).toEqual("Element '{{report.description}}'.text()");
- timeTravel(future);
- expect(future.value).toEqual('Details...');
-// expect(future.value.attr('ng:bind')).toEqual('report.description');
+
+ describe('Binding', function() {
+ it('should select binding by name', function() {
+ doc.append('<span ng:bind="foo.bar">some value</span>');
+ angular.scenario.dsl.binding.call($root).call($root, 'foo.bar');
+ expect($root.futureResult).toEqual('some value');
+ });
+
+ it('should return error if no binding exists', function() {
+ angular.scenario.dsl.binding.call($root).call($root, 'foo.bar');
+ expect($root.futureError).toMatch(/does not exist/);
+ });
});
- it('should be able to click elements', function(){
- var future = element('.link-class').click();
- expect(future.name).toEqual("Element '.link-class'.click()");
- executeFuture(future, html, function(value) { future.fulfill(value); });
- expect(future.fulfilled).toBeTruthy();
- // TODO(rajat): look for some side effect from click happening?
+
+ describe('Input', function() {
+ it('should change value in text input', function() {
+ doc.append('<input name="test.input" value="something">');
+ var chain = angular.scenario.dsl.input
+ .call($root).call($root, 'test.input');
+ chain.enter('foo');
+ expect($window.angular.log).toContain('element(input)');
+ expect($window.angular.log).toContain('element().trigger(change)');
+ expect(_jQuery('input[name="test.input"]').val()).toEqual('foo');
+ });
+
+ it('should return error if no input exists', function() {
+ var chain = angular.scenario.dsl.input
+ .call($root).call($root, 'test.input');
+ chain.enter('foo');
+ expect($root.futureError).toMatch(/does not exist/);
+ });
+
+ it('should toggle checkbox state', function() {
+ doc.append('<input type="checkbox" name="test.input" checked>');
+ expect(_jQuery('input[name="test.input"]')
+ .attr('checked')).toBeTruthy();
+ var chain = angular.scenario.dsl.input
+ .call($root).call($root, 'test.input');
+ chain.check();
+ expect($window.angular.log).toContain('element(input)');
+ expect($window.angular.log).toContain('element().trigger(click)');
+ expect(_jQuery('input[name="test.input"]')
+ .attr('checked')).toBeFalsy();
+ $window.angular.reset();
+ chain.check();
+ expect($window.angular.log).toContain('element(input)');
+ expect($window.angular.log).toContain('element().trigger(click)');
+ expect(_jQuery('input[name="test.input"]')
+ .attr('checked')).toBeTruthy();
+ });
+
+ it('should return error if checkbox does not exist', function() {
+ var chain = angular.scenario.dsl.input
+ .call($root).call($root, 'test.input');
+ chain.check();
+ expect($root.futureError).toMatch(/does not exist/);
+ });
+
+ it('should select option from radio group', function() {
+ doc.append(
+ '<input type="radio" name="0@test.input" value="foo">' +
+ '<input type="radio" name="0@test.input" value="bar" checked>'
+ );
+ expect(_jQuery('input[name="0@test.input"][value="bar"]')
+ .attr('checked')).toBeTruthy();
+ expect(_jQuery('input[name="0@test.input"][value="foo"]')
+ .attr('checked')).toBeFalsy();
+ var chain = angular.scenario.dsl.input
+ .call($root).call($root, 'test.input');
+ chain.select('foo');
+ expect($window.angular.log).toContain('element(input)');
+ expect($window.angular.log).toContain('element().trigger(click)');
+ expect(_jQuery('input[name="0@test.input"][value="bar"]')
+ .attr('checked')).toBeFalsy();
+ expect(_jQuery('input[name="0@test.input"][value="foo"]')
+ .attr('checked')).toBeTruthy();
+ });
+
+ it('should return error if radio button does not exist', function() {
+ var chain = angular.scenario.dsl.input
+ .call($root).call($root, 'test.input');
+ chain.select('foo');
+ expect($root.futureError).toMatch(/does not exist/);
+ });
});
});
+
});
diff --git a/test/scenario/DescribeSpec.js b/test/scenario/DescribeSpec.js
new file mode 100644
index 00000000..05129cfe
--- /dev/null
+++ b/test/scenario/DescribeSpec.js
@@ -0,0 +1,85 @@
+describe('angular.scenario.Describe', function() {
+ var log;
+ var root;
+
+ beforeEach(function() {
+ root = new angular.scenario.Describe();
+
+ /**
+ * Simple callback logging system. Use to assert proper order of calls.
+ */
+ log = function(text) {
+ log.text = log.text + text;
+ };
+ log.fn = function(text) {
+ return function(done){
+ log(text);
+ (done || angular.noop)();
+ };
+ };
+ log.reset = function() {
+ log.text = '';
+ };
+ log.reset();
+ });
+
+ it('should handle basic nested case', function() {
+ root.describe('A', function(){
+ this.beforeEach(log.fn('{'));
+ this.afterEach(log.fn('}'));
+ this.it('1', log.fn('1'));
+ this.describe('B', function(){
+ this.beforeEach(log.fn('('));
+ this.afterEach(log.fn(')'));
+ this.it('2', log.fn('2'));
+ });
+ });
+ var specs = root.getSpecs();
+ expect(specs.length).toEqual(2);
+
+ expect(specs[0].name).toEqual('2');
+ specs[0].fn();
+ expect(log.text).toEqual('{(2)}');
+
+ log.reset();
+ expect(specs[1].name).toEqual('1');
+ specs[1].fn();
+ expect(log.text).toEqual('{1}');
+ });
+
+ it('should link nested describe blocks with parent and children', function() {
+ root.describe('A', function() {
+ this.it('1', angular.noop);
+ this.describe('B', function() {
+ this.it('2', angular.noop);
+ this.describe('C', function() {
+ this.it('3', angular.noop);
+ });
+ });
+ });
+ var specs = root.getSpecs();
+ expect(specs[2].definition.parent).toEqual(root);
+ expect(specs[0].definition.parent).toEqual(specs[2].definition.children[0]);
+ });
+
+ it('should not process xit and xdescribe', function() {
+ root.describe('A', function() {
+ this.xit('1', angular.noop);
+ this.xdescribe('B', function() {
+ this.it('2', angular.noop);
+ this.describe('C', function() {
+ this.it('3', angular.noop);
+ });
+ });
+ });
+ var specs = root.getSpecs();
+ expect(specs.length).toEqual(0);
+ });
+
+ it('should create uniqueIds in the tree', function() {
+ angular.scenario.Describe.id = 0;
+ var a = new angular.scenario.Describe();
+ var b = new angular.scenario.Describe();
+ expect(a.id).toNotEqual(b.id);
+ });
+});
diff --git a/test/scenario/FutureSpec.js b/test/scenario/FutureSpec.js
new file mode 100644
index 00000000..ae475779
--- /dev/null
+++ b/test/scenario/FutureSpec.js
@@ -0,0 +1,38 @@
+describe('angular.scenario.Future', function() {
+ var future;
+
+ it('should set the name and behavior', function() {
+ var behavior = function() {};
+ var future = new angular.scenario.Future('test name', behavior);
+ expect(future.name).toEqual('test name');
+ expect(future.behavior).toEqual(behavior);
+ expect(future.value).toBeUndefined();
+ expect(future.fulfilled).toBeFalsy();
+ });
+
+ it('should be fulfilled after execution and done callback', function() {
+ var future = new angular.scenario.Future('test name', function(done) {
+ done();
+ });
+ future.execute(angular.noop);
+ expect(future.fulfilled).toBeTruthy();
+ });
+
+ it('should take callback with (error, result) and forward', function() {
+ var future = new angular.scenario.Future('test name', function(done) {
+ done(10, 20);
+ });
+ future.execute(function(error, result) {
+ expect(error).toEqual(10);
+ expect(result).toEqual(20);
+ });
+ });
+
+ it('should use error as value if provided', function() {
+ var future = new angular.scenario.Future('test name', function(done) {
+ done(10, 20);
+ });
+ future.execute(angular.noop);
+ expect(future.value).toEqual(10);
+ });
+});
diff --git a/test/scenario/HtmlUISpec.js b/test/scenario/HtmlUISpec.js
new file mode 100644
index 00000000..b2e2652f
--- /dev/null
+++ b/test/scenario/HtmlUISpec.js
@@ -0,0 +1,87 @@
+describe('angular.scenario.HtmlUI', function() {
+ var ui;
+ var context;
+ var spec;
+
+ 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');
+ // Error
+ specUI = ui.addSpec(spec);
+ specUI.addStep('some step').finish();
+ specUI.finish('error');
+ // Failure
+ specUI = ui.addSpec(spec);
+ specUI.addStep('some step').finish('failure');
+ specUI.finish('failure');
+ // Failure
+ specUI = ui.addSpec(spec);
+ specUI.addStep('some step').finish('failure');
+ specUI.finish('failure');
+ // Failure
+ specUI = ui.addSpec(spec);
+ specUI.addStep('some step').finish('failure');
+ specUI.finish('failure');
+ // Success
+ specUI = ui.addSpec(spec);
+ specUI.addStep('some step').finish();
+ specUI.finish();
+
+ expect(parseInt(context.find('#status-legend .status-failure').text()))
+ .toEqual(3);
+ expect(parseInt(context.find('#status-legend .status-error').text()))
+ .toEqual(2);
+ expect(parseInt(context.find('#status-legend .status-success').text()))
+ .toEqual(1);
+ });
+
+ it('should update timer when test completes', function() {
+ // Success
+ specUI = ui.addSpec(spec);
+ specUI.addStep('some step').finish();
+ specUI.finish();
+
+ // Failure
+ specUI = ui.addSpec(spec);
+ specUI.addStep('some step').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$/);
+ });
+ });
+
+});
diff --git a/test/scenario/MatcherSpec.js b/test/scenario/MatcherSpec.js
deleted file mode 100644
index 2eddd2bc..00000000
--- a/test/scenario/MatcherSpec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-describe('Matcher', function () {
- function executeFutures() {
- for(var i in $scenario.currentSpec.futures) {
- var future = $scenario.currentSpec.futures[i];
- future.behavior.call({}, function(value) { future.fulfill(value); });
- }
- }
- var matcher;
- beforeEach(function() {
- setUpContext();
- var future = $scenario.addFuture('Calculate first future', function(done) {
- done(123);
- });
- matcher = new Matcher(this, future);
-
- });
- it('should correctly match toEqual', function() {
- matcher.toEqual(123);
- executeFutures();
- });
- it('should throw an error when incorrect match toEqual', function() {
- matcher.toEqual(456);
- try {
- executeFutures();
- fail();
- } catch (e) {
- expect(e).toEqual('Expected 456 but was 123');
- }
- });
- it('should correctly match arrays', function() {
- var future = $scenario.addFuture('Calculate first future', function(done) {
- done(['a', 'b']);
- });
- matcher = new Matcher(this, future);
- matcher.toEqual(['a', 'b']);
- executeFutures();
- });
-}); \ No newline at end of file
diff --git a/test/scenario/RunnerSpec.js b/test/scenario/RunnerSpec.js
index 2986add6..43d97257 100644
--- a/test/scenario/RunnerSpec.js
+++ b/test/scenario/RunnerSpec.js
@@ -1,238 +1,96 @@
-describe('Runner', function() {
-
- var Describe, It, BeforeEach, AfterEach, body;
-
+/**
+ * Mock spec runner.
+ */
+function MockSpecRunner() {}
+MockSpecRunner.prototype.run = function(ui, spec, specDone) {
+ spec.fn.call(this);
+ specDone();
+};
+
+describe('angular.scenario.Runner', function() {
+ var $window;
+ var runner;
+
beforeEach(function() {
- setUpContext();
- Describe = _window.describe;
- It = _window.it;
- BeforeEach = _window.beforeEach;
- AfterEach = _window.afterEach;
- body = _jQuery('<div></div>');
- });
-
- describe('describe', function() {
- it('should consume the describe functions', function() {
- Describe('describe name', logger('body'));
- expect(log).toEqual('body');
+ // Trick to get the scope out of a DSL statement
+ angular.scenario.dsl('dslScope', function() {
+ var scope = this;
+ return function() { return scope; };
});
-
- describe('it', function() {
- it('should consume it', function() {
- Describe('describe name', function() {
- It('should text', logger('body'));
- });
- expect(log).toEqual('body');
- var spec = $scenario.specs['describe name: it should text'];
- expect(spec.futures).toEqual([]);
- expect(spec.name).toEqual('describe name: it should text');
- });
-
- it('should complain on duplicate it', function() {
- // WRITE ME!!!!
- });
-
- it('should create a failing future if there is a javascript error', function() {
- var spec;
- Describe('D1', function() {
- It('I1', function() {
- spec = $scenario.currentSpec;
- throw {message: 'blah'};
- });
- });
- var future = spec.futures[0];
- expect(future.name).toEqual('blah');
- try {
- future.behavior();
- fail();
- } catch (e) {
- expect(e.message).toEqual('blah');
- }
- });
- });
-
- describe('beforeEach', function() {
- it('should execute beforeEach before every it', function() {
- Describe('describe name', function() {
- BeforeEach(logger('before;'));
- It('should text', logger('body;'));
- It('should text2', logger('body2;'));
- });
- expect(log).toEqual('before;body;before;body2;');
- });
+ // Trick to get the scope out of a DSL statement
+ angular.scenario.dsl('dslChain', function() {
+ return function() {
+ this.chained = 0;
+ this.chain = function() { this.chained++; return this; };
+ return this;
+ };
});
- describe('afterEach', function() {
- it('should execute afterEach after every it', function() {
- Describe('describe name', function() {
- AfterEach(logger('after;'));
- It('should text1', logger('body1;'));
- It('should text2', logger('body2;'));
- });
- expect(log).toEqual('body1;after;body2;after;');
- });
-
- it('should always execute afterEach after every it', function() {
- Describe('describe name', function() {
- AfterEach(logger('after;'));
- It('should text', function() {
- logger('body1;')();
- throw "MyError";
- });
- It('should text2', logger('body2;'));
- });
- expect(log).toEqual('body1;after;body2;after;');
- });
-
- it('should report an error if afterEach fails', function() {
- var next;
- Describe('describe name', function() {
- AfterEach(function() {
- $scenario.addFuture('afterEachLog', logger('after;'));
- $scenario.addFuture('afterEachThrow', function() {
- throw "AfterError";
- });
- });
- It('should text1', function() {
- $scenario.addFuture('future1', logger('future1;'));
- });
- It('should text2', function() {
- $scenario.addFuture('future2', logger('future2;'));
- });
- });
- $scenario.run(body);
- expect(log).toEqual('future1;after;future2;after;');
- expect(_window.$testrun.results).toEqual([
- { name : 'describe name: it should text1',
- passed : false,
- error : 'AfterError',
- steps : [ 'future1', 'afterEachLog', 'afterEachThrow' ] },
- { name : 'describe name: it should text2',
- passed : false,
- error : 'AfterError',
- steps : [ 'future2', 'afterEachLog', 'afterEachThrow' ] }]);
- });
+ $window = {};
+ runner = new angular.scenario.Runner($window);
+ });
+
+ afterEach(function() {
+ delete angular.scenario.dsl.dslScope;
+ delete angular.scenario.dsl.dslChain;
+ });
+
+ it('should publish the functions in the public API', function() {
+ angular.foreach(runner.api, function(fn, name) {
+ var func;
+ if (name in $window) {
+ func = $window[name];
+ }
+ expect(angular.isFunction(func)).toBeTruthy();
});
});
-
- describe('future building', function() {
- it('should queue futures', function() {
- function behavior(){}
- Describe('name', function() {
- It('should', function() {
- $scenario.addFuture('futureName', behavior);
+
+ it('should construct valid describe trees with public API', function() {
+ var before = [];
+ var after = [];
+ $window.describe('A', function() {
+ $window.beforeEach(function() { before.push('A'); });
+ $window.afterEach(function() { after.push('A'); });
+ $window.it('1', angular.noop);
+ $window.describe('B', function() {
+ $window.beforeEach(function() { before.push('B'); });
+ $window.afterEach(function() { after.push('B'); });
+ $window.it('2', angular.noop);
+ $window.describe('C', function() {
+ $window.beforeEach(function() { before.push('C'); });
+ $window.afterEach(function() { after.push('C'); });
+ $window.it('3', angular.noop);
});
});
- expect($scenario.specs['name: it should'].futures[0].name).
- toEqual('futureName');
});
+ var specs = runner.rootDescribe.getSpecs();
+ specs[0].fn();
+ expect(before).toEqual(['A', 'B', 'C']);
+ expect(after).toEqual(['C', 'B', 'A']);
+ expect(specs[2].definition.parent).toEqual(runner.rootDescribe);
+ expect(specs[0].definition.parent).toEqual(specs[2].definition.children[0]);
});
-
- describe('execution', function() {
- it('should execute the queued futures', function() {
- var next, firstThis, secondThis, doneThis, spec;
- $scenario.specs['spec'] = {
- futures: [
- new Future('future1', function(done) {
- next = done;
- log += 'first;';
- firstThis = this;
- }),
- new Future('future2', function(done) {
- next = done;
- log += 'second;';
- secondThis = this;
- })
- ]
- };
-
- spec = $scenario.execute('spec', function(done){
- log += 'done;';
- doneThis = this;
+
+ it('should publish the DSL statements to the $window', function() {
+ $window.describe('describe', function() {
+ $window.it('1', function() {
+ expect($window.dslScope).toBeDefined();
});
- expect(log).toEqual('first;');
- next();
- expect(log).toEqual('first;second;');
- next();
- expect(log).toEqual('first;second;done;');
- expect(spec === window).toEqual(false);
- expect(spec).toEqual(firstThis);
- expect(spec).toEqual(secondThis);
- expect(spec).toEqual(doneThis);
-
- expect(spec.result.failed).toEqual(false);
- expect(spec.result.finished).toEqual(true);
- expect(spec.result.error).toBeUndefined();
- expect(spec.result.passed).toEqual(true);
- });
-
- it('should handle exceptions in a future', function() {
- $scenario.specs['spec'] = {
- futures: [
- new Future('first future', function(done) {
- done();
- }),
- new Future('error', function(done) {
- throw "MyError";
- }),
- new Future('should not execute', function(done) {
- done();
- })
- ]
- };
-
- var spec = $scenario.execute('spec');
-
- expect(spec.result.passed).toEqual(false);
- expect(spec.result.failed).toEqual(true);
- expect(spec.result.finished).toEqual(true);
- expect(spec.result.error).toEqual("MyError");
- expect(_window.$testrun.results).toEqual([{
- name: 'spec',
- passed: false,
- error: 'MyError',
- steps: ['first future', 'error']}]);
});
+ runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow);
});
-
- describe('run', function() {
- var next;
- beforeEach(function() {
- Describe('d1', function() {
- It('it1', function() { $scenario.addFuture('s1', logger('s1,')); });
- It('it2', function() {
- $scenario.addFuture('s2', logger('s2,'));
- $scenario.addFuture('s2.2', function(done){ next = done; });
- });
+
+ it('should create a new scope for each DSL chain', function() {
+ $window.describe('describe', function() {
+ $window.it('1', function() {
+ var scope = $window.dslScope();
+ scope.test = "foo";
+ expect($window.dslScope().test).toBeUndefined();
});
- Describe('d2', function() {
- It('it3', function() { $scenario.addFuture('s3', logger('s3,')); });
- It('it4', function() { $scenario.addFuture('s4', logger('s4,')); });
+ $window.it('2', function() {
+ var scope = $window.dslChain().chain().chain();
+ expect(scope.chained).toEqual(2);
});
});
- it('should execute all specs', function() {
- $scenario.run(body);
-
- expect(log).toEqual('s1,s2,');
- next();
- expect(log).toEqual('s1,s2,s3,s4,');
- });
- it('should publish done state and results as tests are run', function() {
- expect(_window.$testrun.done).toBeFalsy();
- expect(_window.$testrun.results).toEqual([]);
- $scenario.run(body);
- expect(_window.$testrun.done).toBeFalsy();
- expect(_window.$testrun.results).toEqual([
- {name: 'd1: it it1', passed: true, error: undefined, steps: ['s1']}
- ]);
- next();
- expect(_window.$testrun.done).toBeTruthy();
- expect(_window.$testrun.results).toEqual([
- {name: 'd1: it it1', passed: true, error: undefined, steps: ['s1']},
- {name: 'd1: it it2', passed: true, error: undefined, steps: ['s2', 's2.2']},
- {name: 'd2: it it3', passed: true, error: undefined, steps: ['s3']},
- {name: 'd2: it it4', passed: true, error: undefined, steps: ['s4']}
- ]);
- });
+ runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow);
});
-
-}); \ No newline at end of file
+});
diff --git a/test/scenario/SpecRunnerSpec.js b/test/scenario/SpecRunnerSpec.js
new file mode 100644
index 00000000..81b66956
--- /dev/null
+++ b/test/scenario/SpecRunnerSpec.js
@@ -0,0 +1,165 @@
+/**
+ * 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) {
+ this.$window = $window;
+}
+ApplicationMock.prototype = {
+ executeAction: function(callback) {
+ callback.call(this.$window);
+ }
+};
+
+describe('angular.scenario.SpecRunner', function() {
+ var $window;
+ var runner;
+
+ beforeEach(function() {
+ $window = {};
+ runner = angular.scope();
+ runner.application = new ApplicationMock($window);
+ runner.$become(angular.scenario.SpecRunner);
+ });
+
+ it('should bind futures to the spec', function() {
+ runner.addFuture('test future', function(done) {
+ this.application.value = 10;
+ done();
+ });
+ runner.futures[0].execute(angular.noop);
+ expect(runner.application.value).toEqual(10);
+ });
+
+ it('should pass done to future action behavior', function() {
+ runner.addFutureAction('test future', function(done) {
+ expect(angular.isFunction(done)).toBeTruthy();
+ done(10, 20);
+ });
+ runner.futures[0].execute(function(error, result) {
+ expect(error).toEqual(10);
+ expect(result).toEqual(20);
+ });
+ });
+
+ it('should pass execute future action on the $window', function() {
+ runner.addFutureAction('test future', function(done) {
+ this.test = 'test value';
+ done();
+ });
+ runner.futures[0].execute(angular.noop);
+ expect($window.test).toEqual('test value');
+ });
+
+ it('should execute spec function and notify UI', function() {
+ var finished = false;
+ var ui = new UIMock();
+ var spec = {name: 'test spec', fn: function() {
+ this.test = 'some value';
+ }};
+ runner.addFuture('test future', function(done) {
+ done();
+ });
+ runner.run(ui, 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:'
+ ]);
+ });
+
+ it('should execute notify UI on spec setup error', function() {
+ var finished = false;
+ var ui = new UIMock();
+ var spec = {name: 'test spec', fn: function() {
+ throw 'message';
+ }};
+ runner.run(ui, spec, function() {
+ finished = true;
+ });
+ expect(finished).toBeTruthy();
+ expect(ui.log).toEqual([
+ 'addSpec:test spec',
+ 'spec error:message'
+ ]);
+ });
+
+ it('should execute notify UI on step failure', function() {
+ var finished = false;
+ var ui = new UIMock();
+ var spec = {name: 'test spec', fn: angular.noop};
+ runner.addFuture('test future', function(done) {
+ done('failure message');
+ });
+ runner.run(ui, spec, function() {
+ finished = true;
+ });
+ expect(finished).toBeTruthy();
+ expect(ui.log).toEqual([
+ 'addSpec:test spec',
+ 'addStep:test future',
+ 'step finish:failure message',
+ 'spec finish:failure message'
+ ]);
+ });
+
+ it('should execute notify UI on step error', function() {
+ var finished = false;
+ var ui = new UIMock();
+ var spec = {name: 'test spec', fn: angular.noop};
+ runner.addFuture('test future', function(done) {
+ throw 'error message';
+ });
+ runner.run(ui, spec, function() {
+ finished = true;
+ });
+ expect(finished).toBeTruthy();
+ expect(ui.log).toEqual([
+ 'addSpec:test spec',
+ 'addStep:test future',
+ 'step error:error message',
+ 'spec finish:error message'
+ ]);
+ });
+
+});
diff --git a/test/scenario/TestContext.js b/test/scenario/TestContext.js
deleted file mode 100644
index 0c8e6143..00000000
--- a/test/scenario/TestContext.js
+++ /dev/null
@@ -1,15 +0,0 @@
-var _window, runner, log, $scenario;
-
-function logger(text) {
- return function(done){
- log += text;
- (done||noop)();
- };
-}
-
-function setUpContext() {
- _window = {};
- runner = new angular.scenario.Runner(_window, _jQuery);
- $scenario = _window.$scenario;
- log = '';
-}
diff --git a/test/scenario/matchersSpec.js b/test/scenario/matchersSpec.js
new file mode 100644
index 00000000..faabd1a2
--- /dev/null
+++ b/test/scenario/matchersSpec.js
@@ -0,0 +1,43 @@
+describe('angular.scenario.matchers', function () {
+ var matchers;
+
+ function expectMatcher(value, test) {
+ delete matchers.error;
+ delete matchers.future.value;
+ if (value !== undefined) {
+ matchers.future.value = value;
+ }
+ test();
+ expect(matchers.error).toBeUndefined();
+ }
+
+ beforeEach(function() {
+ /**
+ * Mock up the future system wrapped around matchers.
+ *
+ * @see Scenario.js#angular.scenario.matcher
+ */
+ matchers = {
+ future: { name: 'test' }
+ };
+ matchers.addFuture = function(name, callback) {
+ callback(function(error) {
+ matchers.error = error;
+ });
+ };
+ angular.extend(matchers, angular.scenario.matcher);
+ });
+
+ it('should handle basic matching', function() {
+ expectMatcher(10, function() { matchers.toEqual(10); });
+ expectMatcher('value', function() { matchers.toBeDefined(); });
+ expectMatcher([1], function() { matchers.toBeTruthy(); });
+ expectMatcher("", function() { matchers.toBeFalsy(); });
+ expectMatcher(0, function() { matchers.toBeFalsy(); });
+ expectMatcher('foo', function() { matchers.toMatch('.o.'); });
+ expectMatcher(null, function() { matchers.toBeNull(); });
+ expectMatcher([1, 2, 3], function() { matchers.toContain(2); });
+ expectMatcher(3, function() { matchers.toBeLessThan(10); });
+ expectMatcher(3, function() { matchers.toBeGreaterThan(-5); });
+ });
+});
diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js
index 955dccfa..47bc0d0d 100644
--- a/test/testabilityPatch.js
+++ b/test/testabilityPatch.js
@@ -22,6 +22,19 @@ beforeEach(function(){
return "Expected to not have class 'ng-validation-error' but found.";
};
return !hasClass;
+ },
+
+ toEqualData: function(expected) {
+ return equals(this.actual, expected);
+ },
+
+ toHaveClass: function(clazz) {
+ this.message = function(){
+ return "Expected '" + sortedHtml(this.actual) + "' to have class '" + clazz + "'.";
+ };
+ return this.actual.hasClass ?
+ this.actual.hasClass(clazz) :
+ jqLite(this.actual).hasClass(clazz);
}
});
});
@@ -194,3 +207,9 @@ function click(element) {
JQLite.prototype.trigger.call(element, 'click');
}
}
+
+function rethrow(e) {
+ if(e) {
+ throw e;
+ }
+}