mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
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:
parent
9c8b1800b9
commit
2115db6903
15 changed files with 429 additions and 138 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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:'
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue