diff options
| author | Elliott Sprehn | 2010-10-19 13:17:49 -0700 |
|---|---|---|
| committer | Elliott Sprehn | 2010-10-20 14:38:00 -0700 |
| commit | 2115db69035c5993533fe7a3825e64cf6e9068ad (patch) | |
| tree | 796a502b28cd2bda8108a672eac0bf28c8bc21d4 | |
| parent | 9c8b1800b90e14b643bab6ada8e96f8f850e84a6 (diff) | |
| download | angular.js-2115db69035c5993533fe7a3825e64cf6e9068ad.tar.bz2 | |
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.
| -rw-r--r-- | css/angular-scenario.css | 35 | ||||
| -rw-r--r-- | scenario/widgets-scenario.js | 1 | ||||
| -rw-r--r-- | src/scenario/Describe.js | 10 | ||||
| -rw-r--r-- | src/scenario/Future.js | 18 | ||||
| -rw-r--r-- | src/scenario/HtmlUI.js | 102 | ||||
| -rw-r--r-- | src/scenario/Runner.js | 7 | ||||
| -rw-r--r-- | src/scenario/Scenario.js | 97 | ||||
| -rw-r--r-- | src/scenario/SpecRunner.js | 61 | ||||
| -rw-r--r-- | src/scenario/dsl.js | 56 | ||||
| -rw-r--r-- | test/scenario/DescribeSpec.js | 8 | ||||
| -rw-r--r-- | test/scenario/FutureSpec.js | 3 | ||||
| -rw-r--r-- | test/scenario/HtmlUISpec.js | 41 | ||||
| -rw-r--r-- | test/scenario/RunnerSpec.js | 22 | ||||
| -rw-r--r-- | test/scenario/SpecRunnerSpec.js | 73 | ||||
| -rw-r--r-- | test/scenario/dslSpec.js | 33 |
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() { |
