Lots of stability and performance updates and UI polish too.

Polish the Scenario Runner UI to include:
- a scroll pane that steps appear in since the list can be very long
- Collapse successful tests
- Show the line where the DSL statements were when there's an error (Chrome, Firefox)

Also:
- Remove lots angular.bind calls to reduce the amount of stack space used.
- Use setTimeout(...,0) to schedule the next future to let the browser breathe and have it repaint the steps. Also prevents overflowing the stack when an it() creates many futures.
- Run afterEach() handlers even if the it() block fails.
- Make navigateTo() take a function as the second argument so you can compute a URL in the future.
- Add wait() DSL statement to allow interactive debugging of tests.
- Allow custom jQuery selectors with element(...).query(fn) DSL statement.

Known Issues:
- All afterEach() handlers run even if a beforeEach() handler fails. Only after handlers for the same level as the failure and above should run.
This commit is contained in:
Elliott Sprehn 2010-10-19 13:17:49 -07:00
parent 9c8b1800b9
commit 2115db6903
15 changed files with 429 additions and 138 deletions

View file

@ -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 {

View file

@ -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']);

View file

@ -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
});
};

View file

@ -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);
}));
});
};
/**

View file

@ -20,24 +20,30 @@ 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');
};

View file

@ -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);

View file

@ -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);
});
};

View file

@ -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);
};

View file

@ -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;

View file

@ -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}');
});

View file

@ -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);

View file

@ -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);
});
});

View file

@ -3,15 +3,27 @@
*/
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;
@ -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);

View file

@ -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:'
]);
});

View file

@ -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() {