aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--css/angular-scenario.css35
-rw-r--r--scenario/widgets-scenario.js1
-rw-r--r--src/scenario/Describe.js10
-rw-r--r--src/scenario/Future.js18
-rw-r--r--src/scenario/HtmlUI.js102
-rw-r--r--src/scenario/Runner.js7
-rw-r--r--src/scenario/Scenario.js97
-rw-r--r--src/scenario/SpecRunner.js61
-rw-r--r--src/scenario/dsl.js56
-rw-r--r--test/scenario/DescribeSpec.js8
-rw-r--r--test/scenario/FutureSpec.js3
-rw-r--r--test/scenario/HtmlUISpec.js41
-rw-r--r--test/scenario/RunnerSpec.js22
-rw-r--r--test/scenario/SpecRunnerSpec.js73
-rw-r--r--test/scenario/dslSpec.js33
15 files changed, 429 insertions, 138 deletions
diff --git a/css/angular-scenario.css b/css/angular-scenario.css
index 3462ecef..adadebb0 100644
--- a/css/angular-scenario.css
+++ b/css/angular-scenario.css
@@ -89,6 +89,20 @@ body {
border-radius: 8px 0 0 8px;
-webkit-border-radius: 8px 0 0 8px;
-moz-border-radius: 8px 0 0 8px;
+ cursor: pointer;
+}
+
+.test-info:hover .test-name {
+ text-decoration: underline;
+}
+
+.test-info .closed:before {
+ content: '\25b8\00A0';
+}
+
+.test-info .open:before {
+ content: '\25be\00A0';
+ font-weight: bold;
}
.test-it ol {
@@ -111,6 +125,21 @@ body {
padding: 4px;
}
+.test-actions .test-title,
+.test-actions .test-result {
+ display: table-cell;
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+.test-actions {
+ display: table;
+}
+
+.test-actions li {
+ display: table-row;
+}
+
.timer-result {
width: 4em;
padding: 0 10px;
@@ -121,6 +150,7 @@ body {
.test-it pre,
.test-actions pre {
clear: left;
+ color: black;
margin-left: 6em;
}
@@ -132,6 +162,11 @@ body {
content: '\00bb\00A0';
}
+.scrollpane {
+ max-height: 20em;
+ overflow: auto;
+}
+
/** Colors */
#header {
diff --git a/scenario/widgets-scenario.js b/scenario/widgets-scenario.js
index 0cb189f7..0d604fc9 100644
--- a/scenario/widgets-scenario.js
+++ b/scenario/widgets-scenario.js
@@ -23,7 +23,6 @@ describe('widgets', function() {
select('select').option('B');
expect(binding('select')).toEqual('B');
-
select('multiselect').options('A', 'C');
expect(binding('multiselect').fromJson()).toEqual(['A', 'C']);
diff --git a/src/scenario/Describe.js b/src/scenario/Describe.js
index 896b337f..f6a52f1e 100644
--- a/src/scenario/Describe.js
+++ b/src/scenario/Describe.js
@@ -78,12 +78,10 @@ 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);
- }
+ name: name,
+ before: self.setupBefore,
+ body: body,
+ after: self.setupAfter
});
};
diff --git a/src/scenario/Future.js b/src/scenario/Future.js
index 30c2d902..8853aa3f 100644
--- a/src/scenario/Future.js
+++ b/src/scenario/Future.js
@@ -1,12 +1,17 @@
/**
* A future action in a spec.
+ *
+ * @param {String} name of the future action
+ * @param {Function} future callback(error, result)
+ * @param {String} Optional. function that returns the file/line number.
*/
-angular.scenario.Future = function(name, behavior) {
+angular.scenario.Future = function(name, behavior, line) {
this.name = name;
this.behavior = behavior;
this.fulfilled = false;
this.value = undefined;
this.parser = angular.identity;
+ this.line = line || function() { return ''; };
};
/**
@@ -15,18 +20,19 @@ angular.scenario.Future = function(name, behavior) {
* @param {Function} Callback function(error, result)
*/
angular.scenario.Future.prototype.execute = function(doneFn) {
- this.behavior(angular.bind(this, function(error, result) {
- this.fulfilled = true;
+ var self = this;
+ this.behavior(function(error, result) {
+ self.fulfilled = true;
if (result) {
try {
- result = this.parser(result);
+ result = self.parser(result);
} catch(e) {
error = e;
}
}
- this.value = error || result;
+ self.value = error || result;
doneFn(error, result);
- }));
+ });
};
/**
diff --git a/src/scenario/HtmlUI.js b/src/scenario/HtmlUI.js
index a93ed1e3..78fe8c33 100644
--- a/src/scenario/HtmlUI.js
+++ b/src/scenario/HtmlUI.js
@@ -21,23 +21,29 @@ angular.scenario.ui.Html = function(context) {
};
/**
+ * The severity order of an error.
+ */
+angular.scenario.ui.Html.SEVERITY = ['pending', 'success', 'failure', 'error'];
+
+/**
* Adds a new spec to the UI.
*
* @param {Object} The spec object created by the Describe object.
*/
angular.scenario.ui.Html.prototype.addSpec = function(spec) {
+ var self = this;
var specContext = this.findContext(spec.definition);
specContext.find('> .tests').append(
'<li class="status-pending test-it"></li>'
);
specContext = specContext.find('> .tests li:last');
- return new angular.scenario.ui.Html.Spec(specContext, spec.name,
- angular.bind(this, function(status) {
- status = this.context.find('#status-legend .status-' + status);
+ return new angular.scenario.ui.Html.Spec(specContext, spec.name,
+ function(status) {
+ status = self.context.find('#status-legend .status-' + status);
var parts = status.text().split(' ');
var value = (parts[0] * 1) + 1;
status.text(value + ' ' + parts[1]);
- })
+ }
);
};
@@ -47,6 +53,7 @@ angular.scenario.ui.Html.prototype.addSpec = function(spec) {
* @param {Object} The definition created by the Describe object.
*/
angular.scenario.ui.Html.prototype.findContext = function(definition) {
+ var self = this;
var path = [];
var currentContext = this.context.find('#specs');
var currentDefinition = definition;
@@ -54,9 +61,9 @@ angular.scenario.ui.Html.prototype.findContext = function(definition) {
path.unshift(currentDefinition);
currentDefinition = currentDefinition.parent;
}
- angular.foreach(path, angular.bind(this, function(defn) {
+ angular.foreach(path, function(defn) {
var id = 'describe-' + defn.id;
- if (!this.context.find('#' + id).length) {
+ if (!self.context.find('#' + id).length) {
currentContext.find('> .test-children').append(
'<div class="test-describe" id="' + id + '">' +
' <h2></h2>' +
@@ -64,10 +71,10 @@ angular.scenario.ui.Html.prototype.findContext = function(definition) {
' <ul class="tests"></ul>' +
'</div>'
);
- this.context.find('#' + id).find('> h2').text('describe: ' + defn.name);
+ self.context.find('#' + id).find('> h2').text('describe: ' + defn.name);
}
- currentContext = this.context.find('#' + id);
- }));
+ currentContext = self.context.find('#' + id);
+ });
return this.context.find('#describe-' + definition.id);
};
@@ -90,9 +97,24 @@ angular.scenario.ui.Html.Spec = function(context, name, doneFn) {
' <span class="test-name"></span>' +
' </p>' +
'</div>' +
- '<ol class="test-actions">' +
- '</ol>'
+ '<div class="scrollpane">' +
+ ' <ol class="test-actions">' +
+ ' </ol>' +
+ '</div>'
);
+ context.find('> .test-info').click(function() {
+ var scrollpane = context.find('> .scrollpane');
+ var actions = scrollpane.find('> .test-actions');
+ var name = context.find('> .test-info .test-name');
+ if (actions.find(':visible').length) {
+ actions.hide();
+ name.removeClass('open').addClass('closed');
+ } else {
+ actions.show();
+ scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
+ name.removeClass('closed').addClass('open');
+ }
+ });
context.find('> .test-info .test-name').text('it ' + name);
};
@@ -100,13 +122,20 @@ angular.scenario.ui.Html.Spec = function(context, name, doneFn) {
* Adds a new Step to this spec and returns it.
*
* @param {String} The name of the step.
+ * @param {Function} function() that returns a string with the file/line number
+ * where the step was added from.
*/
-angular.scenario.ui.Html.Spec.prototype.addStep = function(name) {
- this.context.find('> .test-actions').append('<li class="status-pending"></li>');
- var stepContext = this.context.find('> .test-actions li:last');
+angular.scenario.ui.Html.Spec.prototype.addStep = function(name, location) {
+ this.context.find('> .scrollpane .test-actions').append('<li class="status-pending"></li>');
+ var stepContext = this.context.find('> .scrollpane .test-actions li:last');
var self = this;
- return new angular.scenario.ui.Html.Step(stepContext, name, function(status) {
- self.status = status;
+ return new angular.scenario.ui.Html.Step(stepContext, name, location, function(status) {
+ if (indexOf(angular.scenario.ui.Html.SEVERITY, status) >
+ indexOf(angular.scenario.ui.Html.SEVERITY, self.status)) {
+ self.status = status;
+ }
+ var scrollpane = self.context.find('> .scrollpane');
+ scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
});
};
@@ -118,6 +147,10 @@ angular.scenario.ui.Html.Spec.prototype.complete = function() {
var endTime = new Date().getTime();
this.context.find("> .test-info .timer-result").
text((endTime - this.startTime) + "ms");
+ if (this.status === 'success') {
+ this.context.find('> .test-info .test-name').addClass('closed');
+ this.context.find('> .scrollpane .test-actions').hide();
+ }
};
/**
@@ -125,15 +158,8 @@ angular.scenario.ui.Html.Spec.prototype.complete = function() {
*
* @param {Object} An optional error
*/
-angular.scenario.ui.Html.Spec.prototype.finish = function(error) {
+angular.scenario.ui.Html.Spec.prototype.finish = function() {
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);
};
@@ -144,7 +170,10 @@ angular.scenario.ui.Html.Spec.prototype.finish = function(error) {
* @param {Object} Required error
*/
angular.scenario.ui.Html.Spec.prototype.error = function(error) {
- this.finish(error);
+ this.status = 'error';
+ this.context.append('<pre></pre>');
+ this.context.find('> pre').text(formatException(error));
+ this.finish();
};
/**
@@ -152,28 +181,39 @@ angular.scenario.ui.Html.Spec.prototype.error = function(error) {
*
* @param {Object} The jQuery object for the context of the step.
* @param {String} The name of the step.
+ * @param {Function} function() that returns file/line number of step.
* @param {Function} Callback function(status) to call when complete.
*/
-angular.scenario.ui.Html.Step = function(context, name, doneFn) {
+angular.scenario.ui.Html.Step = function(context, name, location, doneFn) {
this.context = context;
this.name = name;
+ this.location = location;
this.startTime = new Date().getTime();
this.doneFn = doneFn;
context.append(
- '<span class="timer-result"></span>' +
- '<span class="test-title"></span>'
+ '<div class="timer-result"></div>' +
+ '<div class="test-title"></div>'
);
context.find('> .test-title').text(name);
+ var scrollpane = context.parents('.scrollpane');
+ scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight'));
};
/**
* Completes the step and sets the timer value.
*/
-angular.scenario.ui.Html.Step.prototype.complete = function() {
+angular.scenario.ui.Html.Step.prototype.complete = function(error) {
this.context.removeClass('status-pending');
var endTime = new Date().getTime();
this.context.find(".timer-result").
text((endTime - this.startTime) + "ms");
+ if (error) {
+ if (!this.context.find('.test-title pre').length) {
+ this.context.find('.test-title').append('<pre></pre>');
+ }
+ var message = _jQuery.trim(this.location() + '\n\n' + formatException(error));
+ this.context.find('.test-title pre').text(message);
+ }
};
/**
@@ -182,7 +222,7 @@ angular.scenario.ui.Html.Step.prototype.complete = function() {
* @param {Object} An optional error
*/
angular.scenario.ui.Html.Step.prototype.finish = function(error) {
- this.complete();
+ this.complete(error);
if (error) {
this.context.addClass('status-failure');
this.doneFn('failure');
@@ -198,7 +238,7 @@ angular.scenario.ui.Html.Step.prototype.finish = function(error) {
* @param {Object} Required error
*/
angular.scenario.ui.Html.Step.prototype.error = function(error) {
- this.complete();
+ this.complete(error);
this.context.addClass('status-error');
this.doneFn('error');
};
diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js
index 55360592..a8b23f83 100644
--- a/src/scenario/Runner.js
+++ b/src/scenario/Runner.js
@@ -91,6 +91,7 @@ angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClas
});
angular.foreach(angular.scenario.dsl, function(fn, key) {
self.$window[key] = function() {
+ var line = callerFile(3);
var scope = angular.scope(runner);
// Make the dsl accessible on the current chain
@@ -103,10 +104,12 @@ angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClas
// Make these methods work on the current chain
scope.addFuture = function() {
- return angular.scenario.SpecRunner.prototype.addFuture.apply(scope, arguments);
+ Array.prototype.push.call(arguments, line);
+ return specRunnerClass.prototype.addFuture.apply(scope, arguments);
};
scope.addFutureAction = function() {
- return angular.scenario.SpecRunner.prototype.addFutureAction.apply(scope, arguments);
+ Array.prototype.push.call(arguments, line);
+ return specRunnerClass.prototype.addFutureAction.apply(scope, arguments);
};
return scope.dsl[key].apply(scope, arguments);
diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js
index 979210e5..b1782cf2 100644
--- a/src/scenario/Scenario.js
+++ b/src/scenario/Scenario.js
@@ -32,9 +32,9 @@ angular.scenario.dsl = angular.scenario.dsl || function(name, fn) {
var chain = angular.extend({}, result);
angular.foreach(chain, function(value, name) {
if (angular.isFunction(value)) {
- chain[name] = angular.bind(self, function() {
+ chain[name] = function() {
return executeStatement.call(self, value, arguments);
- });
+ };
} else {
chain[name] = value;
}
@@ -63,17 +63,18 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
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);
+ var self = this;
+ this.addFuture(prefix + name + ' ' + angular.toJson(expected),
+ function(done) {
+ var error;
+ self.actual = self.future.value;
+ if ((self.inverse && fn.call(self, expected)) ||
+ (!self.inverse && !fn.call(self, expected))) {
+ error = 'expected ' + angular.toJson(expected) +
+ ' but was ' + angular.toJson(self.actual);
}
- done(this.error);
- })
- );
+ done(error);
+ });
};
};
@@ -88,7 +89,10 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
*/
function asyncForEach(list, iterator, done) {
var i = 0;
- function loop(error) {
+ function loop(error, index) {
+ if (index && index > i) {
+ i = index;
+ }
if (error || i >= list.length) {
done(error);
} else {
@@ -102,7 +106,63 @@ function asyncForEach(list, iterator, done) {
loop();
}
+/**
+ * Formats an exception into a string with the stack trace, but limits
+ * to a specific line length.
+ *
+ * @param {Object} the exception to format, can be anything throwable
+ * @param {Number} Optional. max lines of the stack trace to include
+ * default is 5.
+ */
+function formatException(error, maxStackLines) {
+ maxStackLines = maxStackLines || 5;
+ var message = error.toString();
+ if (error.stack) {
+ var stack = error.stack.split('\n');
+ if (stack[0].indexOf(message) === -1) {
+ maxStackLines++;
+ stack.unshift(error.message);
+ }
+ message = stack.slice(0, maxStackLines).join('\n');
+ }
+ return message;
+}
+/**
+ * Returns a function that gets the file name and line number from a
+ * location in the stack if available based on the call site.
+ *
+ * Note: this returns another function because accessing .stack is very
+ * expensive in Chrome.
+ */
+function callerFile(offset) {
+ var error = new Error();
+
+ return function() {
+ var line = (error.stack || '').split('\n')[offset];
+
+ // Clean up the stack trace line
+ if (line) {
+ if (line.indexOf('@') !== -1) {
+ // Firefox
+ line = line.substring(line.indexOf('@')+1);
+ } else {
+ // Chrome
+ line = line.substring(line.indexOf('(')+1).replace(')', '');
+ }
+ }
+
+ return line || '';
+ };
+}
+
+/**
+ * Triggers a browser event. Attempts to choose the right event if one is
+ * not specified.
+ *
+ * @param {Object} Either a wrapped jQuery/jqLite node or a DOMElement
+ * @param {String} Optional event type.
+ */
function browserTrigger(element, type) {
if (element && !element.nodeName) element = element[0];
if (!element) return;
@@ -136,10 +196,17 @@ function browserTrigger(element, type) {
}
}
+/**
+ * Don't use the jQuery trigger method since it works incorrectly.
+ *
+ * jQuery notifies listeners and then changes the state of a checkbox and
+ * does not create a real browser event. A real click changes the state of
+ * the checkbox and then notifies listeners.
+ *
+ * To work around this we instead use our own handler that fires a real event.
+ */
_jQuery.fn.trigger = function(type) {
return this.each(function(index, node) {
browserTrigger(node, type);
});
};
-
-
diff --git a/src/scenario/SpecRunner.js b/src/scenario/SpecRunner.js
index 26fa9b91..98ce4b53 100644
--- a/src/scenario/SpecRunner.js
+++ b/src/scenario/SpecRunner.js
@@ -8,6 +8,7 @@
*/
angular.scenario.SpecRunner = function() {
this.futures = [];
+ this.afterIndex = 0;
};
/**
@@ -20,32 +21,52 @@ angular.scenario.SpecRunner = function() {
* @param {Function} Callback function that is called when the spec finshes.
*/
angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
+ var self = this;
var specUI = ui.addSpec(spec);
try {
- spec.fn.call(this);
+ spec.before.call(this);
+ spec.body.call(this);
+ this.afterIndex = this.futures.length;
+ spec.after.call(this);
} catch (e) {
specUI.error(e);
specDone();
return;
}
+ var handleError = function(error, done) {
+ if (self.error) {
+ return done();
+ }
+ self.error = true;
+ done(null, self.afterIndex);
+ };
+
+ var spec = this;
asyncForEach(
this.futures,
function(future, futureDone) {
- var stepUI = specUI.addStep(future.name);
+ var stepUI = specUI.addStep(future.name, future.line);
try {
future.execute(function(error) {
stepUI.finish(error);
- futureDone(error);
+ if (error) {
+ return handleError(error, futureDone);
+ }
+ spec.$window.setTimeout( function() { futureDone(); }, 0);
});
} catch (e) {
stepUI.error(e);
- throw e;
+ handleError(e, futureDone);
}
},
function(e) {
- specUI.finish(e);
+ if (e) {
+ specUI.error(e);
+ } else {
+ specUI.finish();
+ }
specDone();
}
);
@@ -54,11 +75,14 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) {
/**
* Adds a new future action.
*
+ * Note: Do not pass line manually. It happens automatically.
+ *
* @param {String} Name of the future
* @param {Function} Behavior of the future
+ * @param {Function} fn() that returns file/line number
*/
-angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior) {
- var future = new angular.scenario.Future(name, angular.bind(this, behavior));
+angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line) {
+ var future = new angular.scenario.Future(name, angular.bind(this, behavior), line);
this.futures.push(future);
return future;
};
@@ -66,17 +90,20 @@ angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior) {
/**
* Adds a new future action to be executed on the application window.
*
+ * Note: Do not pass line manually. It happens automatically.
+ *
* @param {String} Name of the future
* @param {Function} Behavior of the future
+ * @param {Function} fn() that returns file/line number
*/
-angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior) {
+angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior, line) {
+ var self = this;
return this.addFuture(name, function(done) {
- this.application.executeAction(angular.bind(this, function($window, $document) {
-
- $document.elements = angular.bind(this, function(selector) {
+ this.application.executeAction(function($window, $document) {
+ $document.elements = function(selector) {
var args = Array.prototype.slice.call(arguments, 1);
- if (this.selector) {
- selector = this.selector + ' ' + (selector || '');
+ if (self.selector) {
+ selector = self.selector + ' ' + (selector || '');
}
angular.foreach(args, function(value, index) {
selector = selector.replace('$' + (index + 1), value);
@@ -90,10 +117,10 @@ angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior)
}
return result;
- });
+ };
try {
- behavior.call(this, $window, $document, done);
+ behavior.call(self, $window, $document, done);
} catch(e) {
if (e.type && e.type === 'selector') {
done(e.message);
@@ -101,6 +128,6 @@ angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior)
throw e;
}
}
- }));
- });
+ });
+ }, line);
};
diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js
index 69af39db..f4484df8 100644
--- a/src/scenario/dsl.js
+++ b/src/scenario/dsl.js
@@ -1,6 +1,18 @@
/**
* Shared DSL statements that are useful to all scenarios.
*/
+
+ /**
+ * Usage:
+ * wait() waits until you call resume() in the console
+ */
+ angular.scenario.dsl('wait', function() {
+ return function() {
+ return this.addFuture('waiting for you to call resume() in the console', function(done) {
+ this.$window.resume = function() { done(); };
+ });
+ };
+ });
/**
* Usage:
@@ -41,23 +53,22 @@ angular.scenario.dsl('expect', function() {
* of a URL to navigate to
*/
angular.scenario.dsl('navigateTo', function() {
- return function(url) {
+ return function(url, delegate) {
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() {
+ return this.addFuture('navigate to ' + url, function(done) {
+ if (delegate) {
+ url = delegate.call(this, url);
+ }
+ application.navigateTo(url, function() {
application.executeAction(function($window) {
if ($window.angular) {
var $browser = $window.angular.service.$browser();
$browser.poll();
$browser.notifyWhenNoOutstandingRequests(function() {
- done(null, url.value || url);
+ done(null, url);
});
} else {
- done(null, url.value || url);
+ done(null, url);
}
});
});
@@ -142,9 +153,9 @@ angular.scenario.dsl('input', function() {
/**
* 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
+ * 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 = {};
@@ -194,8 +205,8 @@ angular.scenario.dsl('repeater', function() {
/**
* Usage:
- * select(selector).option('value') // select one option
- * select(selector).options('value1', 'value2', ...) // select options from a multi select
+ * 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 = {};
@@ -227,11 +238,12 @@ angular.scenario.dsl('select', function() {
/**
* 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)
+ * 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)
+ * element(selector).query(fn) executes fn(selectedElements, done)
*/
angular.scenario.dsl('element', function() {
var chain = {};
@@ -263,6 +275,12 @@ angular.scenario.dsl('element', function() {
});
};
+ chain.query = function(fn) {
+ return this.addFutureAction('element ' + this.selector + ' custom query', function($window, $document, done) {
+ fn.call(this, $document.elements(), done);
+ });
+ };
+
return function(selector) {
this.dsl.using(selector);
return chain;
diff --git a/test/scenario/DescribeSpec.js b/test/scenario/DescribeSpec.js
index 05129cfe..417a0d2e 100644
--- a/test/scenario/DescribeSpec.js
+++ b/test/scenario/DescribeSpec.js
@@ -38,12 +38,16 @@ describe('angular.scenario.Describe', function() {
expect(specs.length).toEqual(2);
expect(specs[0].name).toEqual('2');
- specs[0].fn();
+ specs[0].before();
+ specs[0].body();
+ specs[0].after();
expect(log.text).toEqual('{(2)}');
log.reset();
expect(specs[1].name).toEqual('1');
- specs[1].fn();
+ specs[1].before();
+ specs[1].body();
+ specs[1].after();
expect(log.text).toEqual('{1}');
});
diff --git a/test/scenario/FutureSpec.js b/test/scenario/FutureSpec.js
index 1e6af7a1..52bd9c66 100644
--- a/test/scenario/FutureSpec.js
+++ b/test/scenario/FutureSpec.js
@@ -3,9 +3,10 @@ describe('angular.scenario.Future', function() {
it('should set the sane defaults', function() {
var behavior = function() {};
- var future = new angular.scenario.Future('test name', behavior);
+ var future = new angular.scenario.Future('test name', behavior, 'foo');
expect(future.name).toEqual('test name');
expect(future.behavior).toEqual(behavior);
+ expect(future.line).toEqual('foo');
expect(future.value).toBeUndefined();
expect(future.fulfilled).toBeFalsy();
expect(future.parser).toEqual(angular.identity);
diff --git a/test/scenario/HtmlUISpec.js b/test/scenario/HtmlUISpec.js
index 5b800fca..9357e00b 100644
--- a/test/scenario/HtmlUISpec.js
+++ b/test/scenario/HtmlUISpec.js
@@ -2,6 +2,8 @@ describe('angular.scenario.HtmlUI', function() {
var ui;
var context;
var spec;
+
+ function line() { return 'unknown:-1'; }
beforeEach(function() {
spec = {
@@ -35,44 +37,44 @@ describe('angular.scenario.HtmlUI', function() {
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');
+ specUI.addStep('some step', line).finish('failure');
+ specUI.finish();
// Failure
specUI = ui.addSpec(spec);
- specUI.addStep('some step').finish('failure');
- specUI.finish('failure');
+ specUI.addStep('some step', line).finish('failure');
+ specUI.finish();
// Failure
specUI = ui.addSpec(spec);
- specUI.addStep('some step').finish('failure');
- specUI.finish('failure');
+ specUI.addStep('some step', line).finish('failure');
+ specUI.finish();
// Success
specUI = ui.addSpec(spec);
- specUI.addStep('some step').finish();
+ specUI.addStep('some step', line).finish();
+ specUI.finish();
+ // Success
+ specUI = ui.addSpec(spec);
+ specUI.addStep('another step', line).finish();
specUI.finish();
expect(parseInt(context.find('#status-legend .status-failure').text(), 10)).
toEqual(3);
- expect(parseInt(context.find('#status-legend .status-error').text(), 10)).
- toEqual(2);
expect(parseInt(context.find('#status-legend .status-success').text(), 10)).
+ toEqual(2);
+ expect(parseInt(context.find('#status-legend .status-error').text(), 10)).
toEqual(1);
});
it('should update timer when test completes', function() {
// Success
specUI = ui.addSpec(spec);
- specUI.addStep('some step').finish();
+ specUI.addStep('some step', line).finish();
specUI.finish();
// Failure
specUI = ui.addSpec(spec);
- specUI.addStep('some step').finish('failure');
+ specUI.addStep('some step', line).finish('failure');
specUI.finish('failure');
// Error
@@ -83,5 +85,14 @@ describe('angular.scenario.HtmlUI', function() {
expect(timer.innerHTML).toMatch(/ms$/);
});
});
+
+ it('should include line if provided', function() {
+ specUI = ui.addSpec(spec);
+ specUI.addStep('some step', line).finish('error!');
+ specUI.finish();
+
+ var errorHtml = context.find('#describe-10 .tests li pre').html();
+ expect(errorHtml.indexOf('unknown:-1')).toEqual(0);
+ });
});
diff --git a/test/scenario/RunnerSpec.js b/test/scenario/RunnerSpec.js
index 43d97257..d34a228c 100644
--- a/test/scenario/RunnerSpec.js
+++ b/test/scenario/RunnerSpec.js
@@ -3,16 +3,28 @@
*/
function MockSpecRunner() {}
MockSpecRunner.prototype.run = function(ui, spec, specDone) {
- spec.fn.call(this);
+ spec.before.call(this);
+ spec.body.call(this);
+ spec.after.call(this);
specDone();
};
+MockSpecRunner.prototype.addFuture = function(name, fn, line) {
+ return {name: name, fn: fn, line: line};
+};
+
describe('angular.scenario.Runner', function() {
var $window;
var runner;
beforeEach(function() {
// Trick to get the scope out of a DSL statement
+ angular.scenario.dsl('dslAddFuture', function() {
+ return function() {
+ return this.addFuture('future name', angular.noop);
+ };
+ });
+ // Trick to get the scope out of a DSL statement
angular.scenario.dsl('dslScope', function() {
var scope = this;
return function() { return scope; };
@@ -25,7 +37,9 @@ describe('angular.scenario.Runner', function() {
return this;
};
});
- $window = {};
+ $window = {
+ location: {}
+ };
runner = new angular.scenario.Runner($window);
});
@@ -63,7 +77,9 @@ describe('angular.scenario.Runner', function() {
});
});
var specs = runner.rootDescribe.getSpecs();
- specs[0].fn();
+ specs[0].before();
+ specs[0].body();
+ specs[0].after();
expect(before).toEqual(['A', 'B', 'C']);
expect(after).toEqual(['C', 'B', 'A']);
expect(specs[2].definition.parent).toEqual(runner.rootDescribe);
diff --git a/test/scenario/SpecRunnerSpec.js b/test/scenario/SpecRunnerSpec.js
index e62bb392..dd7a1b72 100644
--- a/test/scenario/SpecRunnerSpec.js
+++ b/test/scenario/SpecRunnerSpec.js
@@ -49,11 +49,24 @@ ApplicationMock.prototype = {
describe('angular.scenario.SpecRunner', function() {
var $window;
var runner;
+
+ function createSpec(name, body) {
+ return {
+ name: name,
+ before: angular.noop,
+ body: body || angular.noop,
+ after: angular.noop
+ };
+ }
beforeEach(function() {
$window = {};
+ $window.setTimeout = function(fn, timeout) {
+ fn();
+ };
runner = angular.scope();
runner.application = new ApplicationMock($window);
+ runner.$window = $window;
runner.$become(angular.scenario.SpecRunner);
});
@@ -78,11 +91,11 @@ describe('angular.scenario.SpecRunner', function() {
});
it('should execute spec function and notify UI', function() {
- var finished = false;
+ var finished;
var ui = new UIMock();
- var spec = {name: 'test spec', fn: function() {
- this.test = 'some value';
- }};
+ var spec = createSpec('test spec', function() {
+ this.test = 'some value';
+ });
runner.addFuture('test future', function(done) {
done();
});
@@ -100,11 +113,11 @@ describe('angular.scenario.SpecRunner', function() {
});
it('should execute notify UI on spec setup error', function() {
- var finished = false;
+ var finished;
var ui = new UIMock();
- var spec = {name: 'test spec', fn: function() {
+ var spec = createSpec('test spec', function() {
throw 'message';
- }};
+ });
runner.run(ui, spec, function() {
finished = true;
});
@@ -116,9 +129,9 @@ describe('angular.scenario.SpecRunner', function() {
});
it('should execute notify UI on step failure', function() {
- var finished = false;
+ var finished;
var ui = new UIMock();
- var spec = {name: 'test spec', fn: angular.noop};
+ var spec = createSpec('test spec');
runner.addFuture('test future', function(done) {
done('failure message');
});
@@ -130,16 +143,17 @@ describe('angular.scenario.SpecRunner', function() {
'addSpec:test spec',
'addStep:test future',
'step finish:failure message',
- 'spec finish:failure message'
+ 'spec finish:'
]);
});
it('should execute notify UI on step error', function() {
- var finished = false;
+ var finished;
var ui = new UIMock();
- var spec = {name: 'test spec', fn: angular.noop};
- runner.addFuture('test future', function(done) {
- throw 'error message';
+ var spec = createSpec('test spec', function() {
+ this.addFuture('test future', function(done) {
+ throw 'error message';
+ });
});
runner.run(ui, spec, function() {
finished = true;
@@ -149,7 +163,36 @@ describe('angular.scenario.SpecRunner', function() {
'addSpec:test spec',
'addStep:test future',
'step error:error message',
- 'spec finish:error message'
+ 'spec finish:'
+ ]);
+ });
+
+ it('should run after handlers even if error in body of spec', function() {
+ var finished, after;
+ var ui = new UIMock();
+ var spec = createSpec('test spec', function() {
+ this.addFuture('body', function(done) {
+ throw 'error message';
+ });
+ });
+ spec.after = function() {
+ this.addFuture('after', function(done) {
+ after = true;
+ done();
+ });
+ };
+ runner.run(ui, spec, function() {
+ finished = true;
+ });
+ expect(finished).toBeTruthy();
+ expect(after).toBeTruthy();
+ expect(ui.log).toEqual([
+ 'addSpec:test spec',
+ 'addStep:body',
+ 'step error:error message',
+ 'addStep:after',
+ 'step finish:',
+ 'spec finish:'
]);
});
diff --git a/test/scenario/dslSpec.js b/test/scenario/dslSpec.js
index dd489d86..0d523ad3 100644
--- a/test/scenario/dslSpec.js
+++ b/test/scenario/dslSpec.js
@@ -35,13 +35,16 @@ describe("angular.scenario.dsl", function() {
document: _jQuery("<div></div>"),
angular: new AngularMock()
};
- $root = angular.scope({}, angular.service);
+ $root = angular.scope();
$root.futures = [];
+ $root.futureLog = [];
+ $root.$window = $window;
$root.addFuture = function(name, fn) {
this.futures.push(name);
fn.call(this, function(error, result) {
$root.futureError = error;
$root.futureResult = result;
+ $root.futureLog.push(name);
});
};
$root.dsl = {};
@@ -63,6 +66,18 @@ describe("angular.scenario.dsl", function() {
SpecRunner.prototype.addFutureAction;
});
+ describe('Wait', function() {
+ it('should wait until resume to complete', function() {
+ expect($window.resume).toBeUndefined();
+ $root.dsl.wait();
+ expect(angular.isFunction($window.resume)).toBeTruthy();
+ expect($root.futureLog).toEqual([]);
+ $window.resume();
+ expect($root.futureLog).
+ toEqual(['waiting for you to call resume() in the console']);
+ });
+ });
+
describe('Pause', function() {
beforeEach(function() {
$root.setTimeout = function(fn, value) {
@@ -99,10 +114,11 @@ describe("angular.scenario.dsl", function() {
});
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');
+ $root.dsl.navigateTo('http://myurl', function() {
+ return 'http://futureUrl/';
+ });
+ expect($window.location).toEqual('http://futureUrl/');
+ expect($root.futureResult).toEqual('http://futureUrl/');
});
it('should complete if angular is missing from app frame', function() {
@@ -205,6 +221,13 @@ describe("angular.scenario.dsl", function() {
expect(doc.find('input').val()).toEqual('baz');
});
+ it('should execute custom query', function() {
+ doc.append('<a id="test" href="myUrl"></a>');
+ $root.dsl.element('#test').query(function(elements, done) {
+ done(null, elements.attr('href'));
+ });
+ expect($root.futureResult).toEqual('myUrl');
+ });
});
describe('Repeater', function() {