From e7e894a2e36e042be6d62af56b0f3126f4e4fc77 Mon Sep 17 00:00:00 2001
From: Elliott Sprehn
Date: Mon, 18 Oct 2010 14:02:18 -0700
Subject: Significantly clean up the way the scenario DSL works and implement
many more DSL statements.
- "this" always means the current chain scope inside a DSL
- addFutureAction callbacks now take ($window, $document, done)
- $document has a special method elements() that uses the currently selected nodes in the document as defined by using() statements.
- $document.elements() allows placeholder insertion into selectors to make them more readable.
ex. $document.elements('input[name="$1"]', myVar) will substitute the value of myVar for $1 in the selector. Subsequent arguments are $2 and so on.
- $document.elements() results have a special method trigger(event) which should be used to events. This method implements some hacks to make sure browser UI controls update and the correct angular events fire.
- futures now allow custom formatting. By default any chain that results in a future can use toJson() or fromJson() to convert the future value to and from json. A custom parser can be provided with parsedWith(fn) where fn is a callback(value) that must return the parsed result.
Note: The entire widgets.html UI is now able to be controlled and asserted through DSL statements!!! Victory! :)
---
scenario/widgets-scenario.js | 87 +++++-----
scenario/widgets.html | 9 +-
src/jqLite.js | 16 +-
src/scenario/Application.js | 2 +-
src/scenario/DSL.js | 134 ---------------
src/scenario/Future.js | 32 ++++
src/scenario/Runner.js | 34 +++-
src/scenario/SpecRunner.js | 56 +++++-
src/scenario/dsl.js | 270 +++++++++++++++++++++++++++++
test/scenario/ApplicationSpec.js | 13 +-
test/scenario/DSLSpec.js | 233 -------------------------
test/scenario/FutureSpec.js | 38 ++++-
test/scenario/SpecRunnerSpec.js | 17 +-
test/scenario/dslSpec.js | 361 +++++++++++++++++++++++++++++++++++++++
14 files changed, 847 insertions(+), 455 deletions(-)
delete mode 100644 src/scenario/DSL.js
create mode 100644 src/scenario/dsl.js
delete mode 100644 test/scenario/DSLSpec.js
create mode 100644 test/scenario/dslSpec.js
diff --git a/scenario/widgets-scenario.js b/scenario/widgets-scenario.js
index 69fdc10e..befc481c 100644
--- a/scenario/widgets-scenario.js
+++ b/scenario/widgets-scenario.js
@@ -1,58 +1,55 @@
describe('widgets', function() {
it('should verify that basic widgets work', function(){
navigateTo('widgets.html');
- input('text.basic').enter('Carlos');
+
+ using('#text-basic-box').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(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);
- });
- 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);
+ expect(using('#gender-box').binding('gender')).toEqual('female');
+
+ expect(repeater('#repeater-row ul li').count()).toEqual(2);
+ expect(repeater('#repeater-row ul li').row(1)).toEqual(['adam']);
+ expect(repeater('#repeater-row ul li').column('name')).toEqual(['misko', 'adam']);
+
+ select('select').option('B');
+ expect(binding('select')).toEqual('B');
+
+ select('multiselect').options('A', 'C');
+ expect(binding('multiselect').fromJson()).toEqual(['A', 'C']);
+
+ expect(binding('button').fromJson()).toEqual({'count': 0});
+ element('form a').click();
+ expect(binding('button').fromJson()).toEqual({'count': 1});
+ element('input[value="submit"]').click();
+ expect(binding('button').fromJson()).toEqual({'count': 2});
+ element('input[value="button"]').click();
+ expect(binding('button').fromJson()).toEqual({'count': 3});
+ element('input[type="image"]').click();
+ expect(binding('button').fromJson()).toEqual({'count': 4});
+
+ /**
+ * Custom value parser for futures.
+ */
+ function checkboxParser(value) {
+ return angular.fromJson(value.substring(value.indexOf('=')+1));
+ }
+
+ input('checkbox.tea').check();
+ expect(binding('checkbox').parsedWith(checkboxParser)).toEqual({coffee: false, tea: false});
+ input('checkbox.coffee').check();
+ expect(binding('checkbox').parsedWith(checkboxParser)).toEqual({coffee: true, tea: false});
+ input('checkbox.tea').check();
+ input('checkbox.tea').check();
+ input('checkbox.tea').check();
+ expect(binding('checkbox').parsedWith(checkboxParser)).toEqual({coffee: true, tea: true});
});
});
diff --git a/scenario/widgets.html b/scenario/widgets.html
index 08443d2a..80a0a22f 100644
--- a/scenario/widgets.html
+++ b/scenario/widgets.html
@@ -2,6 +2,7 @@
| ng:repeat |
diff --git a/src/jqLite.js b/src/jqLite.js
index 2f32b121..a2ea286b 100644
--- a/src/jqLite.js
+++ b/src/jqLite.js
@@ -119,10 +119,14 @@ JQLite.prototype = {
},
trigger: function(type) {
- var evnt = document.createEvent('MouseEvents'),
- element = this[0];
- evnt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element);
- element.dispatchEvent(evnt);
+ if (msie) {
+ this[0].fireEvent('on' + type);
+ } else {
+ var evnt = document.createEvent('MouseEvents'),
+ element = this[0];
+ evnt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element);
+ element.dispatchEvent(evnt);
+ }
},
replaceWith: function(replaceNode) {
@@ -249,10 +253,6 @@ if (msie) {
if (isDefined(value)) e.innerText = value;
return e.innerText;
}
- },
-
- trigger: function(type) {
- this[0].fireEvent('on' + type);
}
});
}
diff --git a/src/scenario/Application.js b/src/scenario/Application.js
index 24ae99e9..4ee0dd03 100644
--- a/src/scenario/Application.js
+++ b/src/scenario/Application.js
@@ -47,5 +47,5 @@ angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) {
*/
angular.scenario.Application.prototype.executeAction = function(action) {
var $window = this.getWindow();
- return action.call($window, _jQuery($window.document), $window);
+ return action.call(this, $window, _jQuery($window.document));
};
diff --git a/src/scenario/DSL.js b/src/scenario/DSL.js
deleted file mode 100644
index a7571afe..00000000
--- a/src/scenario/DSL.js
+++ /dev/null
@@ -1,134 +0,0 @@
-/**
- * Shared DSL statements that are useful to all scenarios.
- */
-
-/**
-* 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);
- });
- };
-});
-
-/**
- * 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;
- };
-});
-
-/**
- * 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);
- });
- } else {
- done(null, url.value || url);
- }
- });
- });
- });
- };
-});
-
-/**
- * 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/Future.js b/src/scenario/Future.js
index 60fad9c5..30c2d902 100644
--- a/src/scenario/Future.js
+++ b/src/scenario/Future.js
@@ -6,6 +6,7 @@ angular.scenario.Future = function(name, behavior) {
this.behavior = behavior;
this.fulfilled = false;
this.value = undefined;
+ this.parser = angular.identity;
};
/**
@@ -16,7 +17,38 @@ angular.scenario.Future = function(name, behavior) {
angular.scenario.Future.prototype.execute = function(doneFn) {
this.behavior(angular.bind(this, function(error, result) {
this.fulfilled = true;
+ if (result) {
+ try {
+ result = this.parser(result);
+ } catch(e) {
+ error = e;
+ }
+ }
this.value = error || result;
doneFn(error, result);
}));
};
+
+/**
+ * Configures the future to convert it's final with a function fn(value)
+ */
+angular.scenario.Future.prototype.parsedWith = function(fn) {
+ this.parser = fn;
+ return this;
+};
+
+/**
+ * Configures the future to parse it's final value from JSON
+ * into objects.
+ */
+angular.scenario.Future.prototype.fromJson = function() {
+ return this.parsedWith(angular.fromJson);
+};
+
+/**
+ * Configures the future to convert it's final value from objects
+ * into JSON.
+ */
+angular.scenario.Future.prototype.toJson = function() {
+ return this.parsedWith(angular.toJson);
+};
diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js
index ff20d1d1..55360592 100644
--- a/src/scenario/Runner.js
+++ b/src/scenario/Runner.js
@@ -82,14 +82,36 @@ angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClas
$root.setTimeout = function() {
return self.$window.setTimeout.apply(self.$window, arguments);
};
- asyncForEach(specs, angular.bind(this, function(spec, specDone) {
+ asyncForEach(specs, function(spec, specDone) {
+ var dslCache = {};
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);
+ angular.foreach(angular.scenario.dsl, function(fn, key) {
+ dslCache[key] = fn.call($root);
+ });
+ angular.foreach(angular.scenario.dsl, function(fn, key) {
+ self.$window[key] = function() {
+ var scope = angular.scope(runner);
+
+ // Make the dsl accessible on the current chain
+ scope.dsl = {};
+ angular.foreach(dslCache, function(fn, key) {
+ scope.dsl[key] = function() {
+ return dslCache[key].apply(scope, arguments);
+ };
+ });
+
+ // Make these methods work on the current chain
+ scope.addFuture = function() {
+ return angular.scenario.SpecRunner.prototype.addFuture.apply(scope, arguments);
+ };
+ scope.addFutureAction = function() {
+ return angular.scenario.SpecRunner.prototype.addFutureAction.apply(scope, arguments);
+ };
+
+ return scope.dsl[key].apply(scope, arguments);
};
- }));
+ });
runner.run(ui, spec, specDone);
- }), specsDone || angular.noop);
+ }, specsDone || angular.noop);
};
diff --git a/src/scenario/SpecRunner.js b/src/scenario/SpecRunner.js
index 8b6d4ef1..d6cbdcdc 100644
--- a/src/scenario/SpecRunner.js
+++ b/src/scenario/SpecRunner.js
@@ -41,7 +41,7 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
});
} catch (e) {
stepUI.error(e);
- rethrow(e);
+ throw e;
}
},
function(e) {
@@ -71,8 +71,56 @@ angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior) {
*/
angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior) {
return this.addFuture(name, function(done) {
- this.application.executeAction(function() {
- behavior.call(this, done);
- });
+ this.application.executeAction(angular.bind(this, function($window, $document) {
+
+ $document.elements = angular.bind(this, function(selector) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ if (this.selector) {
+ selector = this.selector + ' ' + (selector || '');
+ }
+ angular.foreach(args, function(value, index) {
+ selector = selector.replace('$' + (index + 1), value);
+ });
+ var result = $document.find(selector);
+ if (!result.length) {
+ throw {
+ type: 'selector',
+ message: 'Selector ' + selector + ' did not match any elements.'
+ };
+ }
+
+ result.trigger = function(type) {
+ result.each(function(index, node) {
+ var element = $window.angular.element(node);
+ //TODO(esprehn): HACK!!! Something is broken in angular event dispatching
+ // and if the real jQuery is used we need to set the attribtue after too
+ if (angular.isDefined(element.selector)) {
+ if (type === 'click' && node.nodeName.toLowerCase() === 'input') {
+ element.attr('checked', !element.attr('checked'));
+ }
+ }
+ //TODO(esprehn): HACK!! See above comment.
+ element.trigger(type);
+ if (angular.isDefined(element.selector)) {
+ if (type === 'click' && node.nodeName.toLowerCase() === 'input') {
+ element.attr('checked', !element.attr('checked'));
+ }
+ }
+ });
+ };
+
+ return result;
+ });
+
+ try {
+ behavior.call(this, $window, $document, done);
+ } catch(e) {
+ if (e.type && e.type === 'selector') {
+ done(e.message);
+ } else {
+ throw e;
+ }
+ }
+ }));
});
};
diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js
new file mode 100644
index 00000000..69af39db
--- /dev/null
+++ b/src/scenario/dsl.js
@@ -0,0 +1,270 @@
+/**
+ * Shared DSL statements that are useful to all scenarios.
+ */
+
+/**
+* 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);
+ });
+ };
+});
+
+/**
+ * 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;
+ };
+});
+
+/**
+ * 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($window) {
+ if ($window.angular) {
+ var $browser = $window.angular.service.$browser();
+ $browser.poll();
+ $browser.notifyWhenNoOutstandingRequests(function() {
+ done(null, url.value || url);
+ });
+ } else {
+ done(null, url.value || url);
+ }
+ });
+ });
+ });
+ };
+});
+
+/**
+ * Usage:
+ * using(selector) scopes the next DSL element selection
+ *
+ * ex.
+ * using('#foo').input('bar')
+ */
+angular.scenario.dsl('using', function() {
+ return function(selector) {
+ this.selector = (this.selector||'') + ' ' + selector;
+ return this.dsl;
+ };
+});
+
+/**
+ * Usage:
+ * binding(name) returns the value of a binding
+ */
+angular.scenario.dsl('binding', function() {
+ return function(name) {
+ return this.addFutureAction("select binding '" + name + "'", function($window, $document, done) {
+ var element;
+ try {
+ element = $document.elements('[ng\\:bind-template*="{{$1}}"]', name);
+ } catch(e) {
+ if (e.type !== 'selector')
+ throw e;
+ element = $document.elements('[ng\\:bind="$1"]', name);
+ }
+ done(null, element.text());
+ });
+ };
+});
+
+/**
+ * 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) {
+ return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function($window, $document, done) {
+ var input = $document.elements('input[name="$1"]', this.name);
+ input.val(value);
+ input.trigger('change');
+ done();
+ });
+ };
+
+ chain.check = function() {
+ return this.addFutureAction("checkbox '" + this.name + "' toggle", function($window, $document, done) {
+ var input = $document.elements('input:checkbox[name="$1"]', this.name);
+ input.trigger('click');
+ done();
+ });
+ };
+
+ chain.select = function(value) {
+ return this.addFutureAction("radio button '" + this.name + "' toggle '" + value + "'", function($window, $document, done) {
+ var input = $document.
+ elements('input:radio[name$="@$1"][value="$2"]', this.name, value);
+ input.trigger('click');
+ done();
+ });
+ };
+
+ return function(name) {
+ this.name = name;
+ return chain;
+ };
+});
+
+/**
+ * Usage:
+ * repeater('#products table').count() // number of rows
+ * repeater('#products table').row(1) // all bindings in row as an array
+ * repeater('#products table').column('product.name') // all values across all rows in an array
+ */
+angular.scenario.dsl('repeater', function() {
+ var chain = {};
+
+ chain.count = function() {
+ return this.addFutureAction('repeater ' + this.selector + ' count', function($window, $document, done) {
+ done(null, $document.elements().size());
+ });
+ };
+
+ chain.column = function(binding) {
+ return this.addFutureAction('repeater ' + this.selector + ' column ' + binding, function($window, $document, done) {
+ var values = [];
+ $document.elements().each(function() {
+ _jQuery(this).find(':visible').each(function() {
+ var element = _jQuery(this);
+ if (element.attr('ng:bind') === binding) {
+ values.push(element.text());
+ }
+ });
+ });
+ done(null, values);
+ });
+ };
+
+ chain.row = function(index) {
+ return this.addFutureAction('repeater ' + this.selector + ' row ' + index, function($window, $document, done) {
+ var values = [];
+ var matches = $document.elements().slice(index, index + 1);
+ if (!matches.length)
+ return done('row ' + index + ' out of bounds');
+ _jQuery(matches[0]).find(':visible').each(function() {
+ var element = _jQuery(this);
+ if (element.attr('ng:bind')) {
+ values.push(element.text());
+ }
+ });
+ done(null, values);
+ });
+ };
+
+ return function(selector) {
+ this.dsl.using(selector);
+ return chain;
+ };
+});
+
+/**
+ * Usage:
+ * select(selector).option('value') // select one option
+ * select(selector).options('value1', 'value2', ...) // select options from a multi select
+ */
+angular.scenario.dsl('select', function() {
+ var chain = {};
+
+ chain.option = function(value) {
+ return this.addFutureAction('select ' + this.name + ' option ' + value, function($window, $document, done) {
+ var select = $document.elements('select[name="$1"]', this.name);
+ select.val(value);
+ select.trigger('change');
+ done();
+ });
+ };
+
+ chain.options = function() {
+ var values = arguments;
+ return this.addFutureAction('select ' + this.name + ' options ' + values, function($window, $document, done) {
+ var select = $document.elements('select[multiple][name="$1"]', this.name);
+ select.val(values);
+ select.trigger('change');
+ done();
+ });
+ };
+
+ return function(name) {
+ this.name = name;
+ return chain;
+ };
+});
+
+/**
+ * Usage:
+ * 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
+ * element(selector).val() // gets the value (as defined by jQuery)
+ * element(selector).val(value) // sets the value (as defined by jQuery)
+ */
+angular.scenario.dsl('element', function() {
+ var chain = {};
+
+ chain.click = function() {
+ return this.addFutureAction('element ' + this.selector + ' click', function($window, $document, done) {
+ $document.elements().trigger('click');
+ done();
+ });
+ };
+
+ chain.attr = function(name, value) {
+ var futureName = 'element ' + this.selector + ' get attribute ' + name;
+ if (value) {
+ futureName = 'element ' + this.selector + ' set attribute ' + name + ' to ' + value;
+ }
+ return this.addFutureAction(futureName, function($window, $document, done) {
+ done(null, $document.elements().attr(name, value));
+ });
+ };
+
+ chain.val = function(value) {
+ var futureName = 'element ' + this.selector + ' value';
+ if (value) {
+ futureName = 'element ' + this.selector + ' set value to ' + value;
+ }
+ return this.addFutureAction(futureName, function($window, $document, done) {
+ done(null, $document.elements().val(value));
+ });
+ };
+
+ return function(selector) {
+ this.dsl.using(selector);
+ return chain;
+ };
+});
diff --git a/test/scenario/ApplicationSpec.js b/test/scenario/ApplicationSpec.js
index 706fbc36..2fb9881f 100644
--- a/test/scenario/ApplicationSpec.js
+++ b/test/scenario/ApplicationSpec.js
@@ -18,20 +18,21 @@ describe('angular.scenario.Application', function() {
testDocument = $document;
});
app.navigateTo('http://www.google.com/');
- app.executeAction(function($document, $window) {
+ app.executeAction(function($window, $document) {
expect($window).not.toEqual(testWindow);
expect($document).not.toEqual(testDocument);
});
});
- it('should execute callback on $window of frame', function() {
+ it('should execute callback with correct arguments', function() {
var testWindow = {document: {}};
app.getWindow = function() {
return testWindow;
};
- app.executeAction(function($document, $window) {
- expect(this).toEqual($window);
- expect(this).toEqual(testWindow);
+ app.executeAction(function($window, $document) {
+ expect(this).toEqual(app);
+ expect($document).toEqual(_jQuery($window.document));
+ expect($window).toEqual(testWindow);
});
});
@@ -52,7 +53,7 @@ describe('angular.scenario.Application', function() {
it('should call onload handler when frame loads', function() {
var called;
- app.getFrame = function() {
+ app.getFrame = function() {
// Mock a little jQuery
var result = {
remove: function() {
diff --git a/test/scenario/DSLSpec.js b/test/scenario/DSLSpec.js
deleted file mode 100644
index b144a3ce..00000000
--- a/test/scenario/DSLSpec.js
+++ /dev/null
@@ -1,233 +0,0 @@
-/**
- * Very basic Mock of angular.
- */
-function AngularMock() {
- this.reset();
- this.service = this;
-}
-
-AngularMock.prototype.reset = function() {
- this.log = [];
-};
-
-AngularMock.prototype.element = function(node) {
- this.log.push('element(' + node.nodeName.toLowerCase() + ')');
- return this;
-};
-
-AngularMock.prototype.trigger = function(value) {
- this.log.push('element().trigger(' + value + ')');
-};
-
-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;
-
- beforeEach(function() {
- $window = {
- document: _jQuery(""),
- 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('Pause', function() {
- beforeEach(function() {
- $root.setTimeout = function(fn, value) {
- $root.timerValue = value;
- fn();
- };
- });
-
- 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();
- result = angular.scenario.dsl.expect.call($root).call($root, future);
- result.toEqual(20);
- expect($root.futureError).toBeDefined();
- });
- });
-
- 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 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 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 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() {
- doc = _jQuery('');
- _jQuery(document.body).html('').append(doc);
- $window.document = window.document;
- });
-
- afterEach(function() {
- _jQuery(document.body).
- find('#angular-scenario-binding').
- remove();
- });
-
- describe('Binding', function() {
- it('should select binding by name', function() {
- doc.append('some value');
- 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/);
- });
- });
-
- describe('Input', function() {
- it('should change value in text input', function() {
- doc.append('');
- 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('');
- 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(
- '' +
- '');
- // HACK! We don't know why this is sometimes false on chrome
- _jQuery('input[name="0@test.input"][value="bar"]').attr('checked', true);
- 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/FutureSpec.js b/test/scenario/FutureSpec.js
index ae475779..1e6af7a1 100644
--- a/test/scenario/FutureSpec.js
+++ b/test/scenario/FutureSpec.js
@@ -1,13 +1,14 @@
describe('angular.scenario.Future', function() {
var future;
- it('should set the name and behavior', function() {
+ it('should set the sane defaults', 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();
+ expect(future.parser).toEqual(angular.identity);
});
it('should be fulfilled after execution and done callback', function() {
@@ -35,4 +36,39 @@ describe('angular.scenario.Future', function() {
future.execute(angular.noop);
expect(future.value).toEqual(10);
});
+
+ it('should parse json with fromJson', function() {
+ var future = new angular.scenario.Future('test name', function(done) {
+ done(null, "{test: 'foo'}");
+ });
+ future.fromJson().execute(angular.noop);
+ expect(future.value).toEqual({test: 'foo'});
+ });
+
+ it('should convert to json with toJson', function() {
+ var future = new angular.scenario.Future('test name', function(done) {
+ done(null, {test: 'foo'});
+ });
+ future.toJson().execute(angular.noop);
+ expect(future.value).toEqual('{"test":"foo"}');
+ });
+
+ it('should convert with custom parser', function() {
+ var future = new angular.scenario.Future('test name', function(done) {
+ done(null, 'foo');
+ });
+ future.parsedWith(function(value) {
+ return value.toUpperCase();
+ }).execute(angular.noop);
+ expect(future.value).toEqual('FOO');
+ });
+
+ it('should pass error if parser fails', function() {
+ var future = new angular.scenario.Future('test name', function(done) {
+ done(null, '{');
+ });
+ future.fromJson().execute(function(error, result) {
+ expect(error).toBeDefined();
+ });
+ });
});
diff --git a/test/scenario/SpecRunnerSpec.js b/test/scenario/SpecRunnerSpec.js
index 0926c3f8..e62bb392 100644
--- a/test/scenario/SpecRunnerSpec.js
+++ b/test/scenario/SpecRunnerSpec.js
@@ -42,7 +42,7 @@ function ApplicationMock($window) {
}
ApplicationMock.prototype = {
executeAction: function(callback) {
- callback.call(this.$window);
+ callback.call(this.$window, _jQuery(this.$window.document), this.$window);
}
};
@@ -59,15 +59,15 @@ describe('angular.scenario.SpecRunner', function() {
it('should bind futures to the spec', function() {
runner.addFuture('test future', function(done) {
- this.application.value = 10;
+ this.value = 10;
done();
});
runner.futures[0].execute(angular.noop);
- expect(runner.application.value).toEqual(10);
+ expect(runner.value).toEqual(10);
});
it('should pass done to future action behavior', function() {
- runner.addFutureAction('test future', function(done) {
+ runner.addFutureAction('test future', function($window, $document, done) {
expect(angular.isFunction(done)).toBeTruthy();
done(10, 20);
});
@@ -77,15 +77,6 @@ describe('angular.scenario.SpecRunner', function() {
});
});
- 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();
diff --git a/test/scenario/dslSpec.js b/test/scenario/dslSpec.js
new file mode 100644
index 00000000..a30fe165
--- /dev/null
+++ b/test/scenario/dslSpec.js
@@ -0,0 +1,361 @@
+/**
+ * Very basic Mock of angular.
+ */
+function AngularMock() {
+ this.reset();
+ this.service = this;
+}
+
+AngularMock.prototype.reset = function() {
+ this.log = [];
+};
+
+AngularMock.prototype.element = function(node) {
+ this.log.push('element(' + node.nodeName.toLowerCase() + ')');
+ var mock = this;
+ return {
+ selector: '',
+ attr: function(name, value) {
+ mock.log.push('attr(' + name + (angular.isDefined(value) ? ',' + value : '') + ')');
+ return _jQuery.fn.attr.apply(_jQuery(node), arguments);
+ },
+ trigger: function(type) {
+ mock.log.push('element().trigger(' + type + ')');
+ //TODO(esprehn): See the HACK!! in the SpecRunner. This avoids
+ // triggering the second part of the hack in tests
+ delete this.selector;
+ }
+ };
+};
+
+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;
+
+ beforeEach(function() {
+ $window = {
+ document: _jQuery(""),
+ 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.dsl = {};
+ angular.foreach(angular.scenario.dsl, function(fn, name) {
+ $root.dsl[name] = function() {
+ return fn.call($root).apply($root, arguments);
+ };
+ });
+ $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('Pause', function() {
+ beforeEach(function() {
+ $root.setTimeout = function(fn, value) {
+ $root.timerValue = value;
+ fn();
+ };
+ });
+
+ it('should pause for specified seconds', function() {
+ $root.dsl.pause(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 = $root.dsl.expect(future);
+ result.toEqual(10);
+ expect($root.futureError).toBeUndefined();
+ expect($root.futureResult).toBeUndefined();
+ result = $root.dsl.expect(future);
+ result.toEqual(20);
+ expect($root.futureError).toBeDefined();
+ });
+ });
+
+ describe('NavigateTo', function() {
+ it('should allow a string url', function() {
+ $root.dsl.navigateTo('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'};
+ $root.dsl.navigateTo(future);
+ expect($window.location).toEqual('http://myurl');
+ expect($root.futureResult).toEqual('http://myurl');
+ });
+
+ it('should complete if angular is missing from app frame', function() {
+ delete $window.angular;
+ $root.dsl.navigateTo('http://myurl');
+ 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() {
+ 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() {
+ doc = _jQuery('');
+ _jQuery(document.body).html('').append(doc);
+ $window.document = window.document;
+ });
+
+ afterEach(function() {
+ _jQuery(document.body).
+ find('#angular-scenario-binding').
+ remove();
+ });
+
+ describe('Select', function() {
+ it('should select single option', function() {
+ doc.append(
+ ''
+ );
+ $root.dsl.select('test').option('A');
+ expect(_jQuery('[name="test"]').val()).toEqual('A');
+ });
+
+ it('should select multiple options', function() {
+ doc.append(
+ ''
+ );
+ $root.dsl.select('test').options('A', 'B');
+ expect(_jQuery('[name="test"]').val()).toEqual(['A','B']);
+ });
+
+ it('should fail to select multiple options on non-multiple select', function() {
+ doc.append('');
+ $root.dsl.select('test').options('A', 'B');
+ expect($root.futureError).toMatch(/did not match/);
+ });
+ });
+
+ describe('Element', function() {
+ it('should execute click', function() {
+ var clicked;
+ doc.append('');
+ doc.find('a').click(function() {
+ clicked = true;
+ });
+ $root.dsl.element('a').click();
+ });
+
+ it('should get attribute', function() {
+ doc.append('');
+ $root.dsl.element('#test').attr('class');
+ expect($root.futureResult).toEqual('foo');
+ });
+
+ it('should set attribute', function() {
+ doc.append('');
+ $root.dsl.element('#test').attr('class', 'bam');
+ expect(doc.find('div').attr('class')).toEqual('bam');
+ });
+
+ it('should get val', function() {
+ doc.append('');
+ $root.dsl.element('input').val();
+ expect($root.futureResult).toEqual('bar');
+ });
+
+ it('should set val', function() {
+ doc.append('');
+ $root.dsl.element('input').val('baz');
+ expect(doc.find('input').val()).toEqual('baz');
+ });
+
+ });
+
+ describe('Repeater', function() {
+ var chain;
+ beforeEach(function() {
+ doc.append(
+ '' +
+ ' - miskomale
' +
+ ' - felisafemale
' +
+ ' '
+ );
+ chain = $root.dsl.repeater('ul li');
+ });
+
+ it('should get the row count', function() {
+ chain.count();
+ expect($root.futureResult).toEqual(2);
+ });
+
+ it('should get a row of bindings', function() {
+ chain.row(1);
+ expect($root.futureResult).toEqual(['felisa', 'female']);
+ });
+
+ it('should get a column of bindings', function() {
+ chain.column('gender');
+ expect($root.futureResult).toEqual(['male', 'female']);
+ });
+ });
+
+ describe('Binding', function() {
+ it('should select binding by name', function() {
+ doc.append('some value');
+ $root.dsl.binding('foo.bar');
+ expect($root.futureResult).toEqual('some value');
+ });
+
+ it('should select binding in template by name', function() {
+ doc.append(' foo some baz ');
+ $root.dsl.binding('bar');
+ expect($root.futureResult).toEqual('foo some baz');
+ });
+
+ it('should return error if no binding exists', function() {
+ $root.dsl.binding('foo.bar');
+ expect($root.futureError).toMatch(/did not match/);
+ });
+ });
+
+ describe('Using', function() {
+ it('should prefix selector in $document.elements()', function() {
+ var chain;
+ doc.append(
+ '' +
+ ''
+ );
+ chain = $root.dsl.using('div#test2');
+ chain.input('test.input').enter('foo');
+ expect($window.angular.log).toContain('element(input)');
+ expect($window.angular.log).toContain('element().trigger(change)');
+ var inputs = _jQuery('input[name="test.input"]');
+ expect(inputs.first().val()).toEqual('something');
+ expect(inputs.last().val()).toEqual('foo');
+ });
+ });
+
+ describe('Input', function() {
+ it('should change value in text input', function() {
+ doc.append('');
+ var chain = $root.dsl.input('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 = $root.dsl.input('test.input');
+ chain.enter('foo');
+ expect($root.futureError).toMatch(/did not match/);
+ });
+
+ it('should toggle checkbox state', function() {
+ doc.append('');
+ expect(_jQuery('input[name="test.input"]').
+ attr('checked')).toBeTruthy();
+ var chain = $root.dsl.input('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 did not match', function() {
+ var chain = $root.dsl.input('test.input');
+ chain.check();
+ expect($root.futureError).toMatch(/did not match/);
+ });
+
+ it('should select option from radio group', function() {
+ doc.append(
+ '' +
+ ''
+ );
+ // HACK! We don't know why this is sometimes false on chrome
+ _jQuery('input[name="0@test.input"][value="bar"]').attr('checked', true);
+ 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 = $root.dsl.input('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 did not match', function() {
+ var chain = $root.dsl.input('test.input');
+ chain.select('foo');
+ expect($root.futureError).toMatch(/did not match/);
+ });
+ });
+
+ });
+});
--
cgit v1.2.3
|