diff options
37 files changed, 1362 insertions, 648 deletions
@@ -27,12 +27,16 @@ ANGULAR_SCENARIO = [ 'src/scenario/Application.js', 'src/scenario/Describe.js', 'src/scenario/Future.js', - 'src/scenario/HtmlUI.js', + 'src/scenario/ObjectModel.js', 'src/scenario/Describe.js', 'src/scenario/Runner.js', 'src/scenario/SpecRunner.js', 'src/scenario/dsl.js', 'src/scenario/matchers.js', + 'src/scenario/output/Html.js', + 'src/scenario/output/Json.js', + 'src/scenario/output/Xml.js', + 'src/scenario/output/Object.js', ] BUILD_DIR = 'build' diff --git a/css/angular-scenario.css b/css/angular-scenario.css index adadebb0..f5cded7f 100644 --- a/css/angular-scenario.css +++ b/css/angular-scenario.css @@ -8,6 +8,10 @@ body { font-size: 14px; } +#json, #xml { + display: none; +} + #header { position: fixed; width: 100%; @@ -32,7 +36,7 @@ body { height: 30px; } -#frame h2, +#application h2, #specs h2 { margin: 0; padding: 0.5em; @@ -45,26 +49,26 @@ body { } #header, -#frame, +#application, .test-info, .test-actions li { overflow: hidden; } -#frame { +#application { margin: 10px; } -#frame iframe { +#application iframe { width: 100%; height: 758px; } -#frame .popout { +#application .popout { float: right; } -#frame iframe { +#application iframe { border: none; } @@ -154,6 +158,10 @@ body { margin-left: 6em; } +.test-describe { + padding-bottom: 0.5em; +} + .test-describe .test-describe { margin: 5px 5px 10px 2em; } @@ -178,11 +186,11 @@ body { } #specs h2, -#frame h2 { +#application h2 { background-color: #efefef; } -#frame { +#application { border: 1px solid #BABAD1; } diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf index 7f0d6912..a2388662 100644 --- a/jsTestDriver-jquery.conf +++ b/jsTestDriver-jquery.conf @@ -10,9 +10,11 @@ load: - src/*.js - test/testabilityPatch.js - src/scenario/Scenario.js + - src/scenario/output/*.js - src/scenario/*.js - test/angular-mocks.js - test/scenario/*.js + - test/scenario/output/*.js - test/*.js exclude: @@ -20,6 +22,6 @@ exclude: - src/angular.suffix - src/angular-bootstrap.js - src/AngularPublic.js - - src/scenario/bootstrap.js + - src/scenario/angular-bootstrap.js - test/jquery_remove.js diff --git a/jsTestDriver.conf b/jsTestDriver.conf index 7d202d72..c8ced595 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -11,9 +11,11 @@ load: - example/personalLog/*.js - test/testabilityPatch.js - src/scenario/Scenario.js + - src/scenario/output/*.js - src/scenario/*.js - test/angular-mocks.js - test/scenario/*.js + - test/scenario/output/*.js - test/*.js - example/personalLog/test/*.js @@ -22,5 +24,5 @@ exclude: - src/angular.prefix - src/angular.suffix - src/angular-bootstrap.js - - src/scenario/bootstrap.js + - src/scenario/angular-bootstrap.js - src/AngularPublic.js diff --git a/scenario/Runner.html b/scenario/Runner.html index ffa08af9..f715b8e5 100644 --- a/scenario/Runner.html +++ b/scenario/Runner.html @@ -1,7 +1,7 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> - <script type="text/javascript" src="../src/scenario/bootstrap.js"></script> + <script type="text/javascript" src="../src/scenario/angular-bootstrap.js"></script> <script type="text/javascript" src="widgets-scenario.js"></script> </head> <body> diff --git a/scenario/widgets-scenario.js b/scenario/widgets-scenario.js index 0d604fc9..ba3ef3cf 100644 --- a/scenario/widgets-scenario.js +++ b/scenario/widgets-scenario.js @@ -36,6 +36,9 @@ describe('widgets', function() { element('input[type="image"]').click(); expect(binding('button').fromJson()).toEqual({'count': 4}); + element('#navigate a').click(); + expect(binding('$location.hash')).toEqual('route'); + /** * Custom value parser for futures. */ diff --git a/scenario/widgets.html b/scenario/widgets.html index 8960f5f4..a520a326 100644 --- a/scenario/widgets.html +++ b/scenario/widgets.html @@ -2,7 +2,6 @@ <html xmlns:ng="http://angularjs.org"> <head> <link rel="stylesheet" type="text/css" href="style.css"/> - <script type="text/javascript" src="../libs/jquery/jquery-1.4.2.js"></script> <script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script> </head> <body ng:init="$window.$scope = this"> @@ -94,6 +93,11 @@ </td> <td></td> </tr> + <tr id="navigate"> + <td>navigate</td> + <td><a href="#route">Go to #route</td> + <td>{{$location.hash}}</td> + </tr> </table> </body> </html> diff --git a/src/scenario/Application.js b/src/scenario/Application.js index 4ee0dd03..e2d34551 100644 --- a/src/scenario/Application.js +++ b/src/scenario/Application.js @@ -1,51 +1,84 @@ /** * Represents the application currently being tested and abstracts usage * of iframes or separate windows. + * + * @param {Object} context jQuery wrapper around HTML context. */ angular.scenario.Application = function(context) { this.context = context; - context.append('<h2>Current URL: <a href="about:blank">None</a></h2>'); + context.append( + '<h2>Current URL: <a href="about:blank">None</a></h2>' + + '<div id="test-frames"></div>' + ); }; /** * Gets the jQuery collection of frames. Don't use this directly because * frames may go stale. * + * @private * @return {Object} jQuery collection */ -angular.scenario.Application.prototype.getFrame = function() { - return this.context.find('> iframe'); +angular.scenario.Application.prototype.getFrame_ = function() { + return this.context.find('#test-frames iframe:last'); }; /** - * Gets the window of the test runner frame. Always favor executeAction() + * Gets the window of the test runner frame. Always favor executeAction() * instead of this method since it prevents you from getting a stale window. * + * @private * @return {Object} the window of the frame */ -angular.scenario.Application.prototype.getWindow = function() { - var contentWindow = this.getFrame().attr('contentWindow'); +angular.scenario.Application.prototype.getWindow_ = function() { + var contentWindow = this.getFrame_().attr('contentWindow'); if (!contentWindow) - throw 'No window available because frame not loaded.'; + throw 'Frame window is not accessible.'; return contentWindow; }; /** * Changes the location of the frame. + * + * @param {string} url The URL. If it begins with a # then only the + * hash of the page is changed. + * @param {Function} onloadFn function($window, $document) */ angular.scenario.Application.prototype.navigateTo = function(url, onloadFn) { - this.getFrame().remove(); - this.context.append('<iframe src=""></iframe>'); + var self = this; + var frame = this.getFrame_(); + if (url.charAt(0) === '#') { + url = frame.attr('src').split('#')[0] + url; + frame.attr('src', url); + this.executeAction(onloadFn); + } else { + frame.css('display', 'none').attr('src', 'about:blank'); + this.context.find('#test-frames').append('<iframe>'); + frame = this.getFrame_(); + frame.load(function() { + self.executeAction(onloadFn); + frame.unbind(); + }).attr('src', url); + } this.context.find('> h2 a').attr('href', url).text(url); - this.getFrame().attr('src', url).load(onloadFn); }; /** - * Executes a function in the context of the tested application. + * Executes a function in the context of the tested application. Will wait + * for all pending angular xhr requests before executing. * - * @param {Function} The callback to execute. function($window, $document) + * @param {Function} action The callback to execute. function($window, $document) + * $document is a jQuery wrapped document. */ angular.scenario.Application.prototype.executeAction = function(action) { - var $window = this.getWindow(); - return action.call(this, $window, _jQuery($window.document)); + var self = this; + var $window = this.getWindow_(); + if (!$window.angular) { + return action.call(this, $window, _jQuery($window.document)); + } + var $browser = $window.angular.service.$browser(); + $browser.poll(); + $browser.notifyWhenNoOutstandingRequests(function() { + action.call(self, $window, _jQuery($window.document)); + }); }; diff --git a/src/scenario/Describe.js b/src/scenario/Describe.js index f6a52f1e..69ed8238 100644 --- a/src/scenario/Describe.js +++ b/src/scenario/Describe.js @@ -1,8 +1,12 @@ /** * The representation of define blocks. Don't used directly, instead use * define() in your tests. + * + * @param {string} descName Name of the block + * @param {Object} parent describe or undefined if the root. */ angular.scenario.Describe = function(descName, parent) { + this.only = parent && parent.only; this.beforeEachFns = []; this.afterEachFns = []; this.its = []; @@ -10,7 +14,7 @@ angular.scenario.Describe = function(descName, parent) { this.name = descName; this.parent = parent; this.id = angular.scenario.Describe.id++; - + /** * Calls all before functions. */ @@ -36,7 +40,7 @@ angular.scenario.Describe.id = 0; /** * Defines a block to execute before each it or nested describe. * - * @param {Function} Body of the block. + * @param {Function} body Body of the block. */ angular.scenario.Describe.prototype.beforeEach = function(body) { this.beforeEachFns.push(body); @@ -45,7 +49,7 @@ angular.scenario.Describe.prototype.beforeEach = function(body) { /** * Defines a block to execute after each it or nested describe. * - * @param {Function} Body of the block. + * @param {Function} body Body of the block. */ angular.scenario.Describe.prototype.afterEach = function(body) { this.afterEachFns.push(body); @@ -54,8 +58,8 @@ angular.scenario.Describe.prototype.afterEach = function(body) { /** * Creates a new describe block that's a child of this one. * - * @param {String} Name of the block. Appended to the parent block's name. - * @param {Function} Body of the block. + * @param {string} name Name of the block. Appended to the parent block's name. + * @param {Function} body Body of the block. */ angular.scenario.Describe.prototype.describe = function(name, body) { var child = new angular.scenario.Describe(name, this); @@ -64,6 +68,19 @@ angular.scenario.Describe.prototype.describe = function(name, body) { }; /** + * Same as describe() but makes ddescribe blocks the only to run. + * + * @param {string} name Name of the test. + * @param {Function} body Body of the block. + */ +angular.scenario.Describe.prototype.ddescribe = function(name, body) { + var child = new angular.scenario.Describe(name, this); + child.only = true; + this.children.push(child); + body.call(child); +}; + +/** * Use to disable a describe block. */ angular.scenario.Describe.prototype.xdescribe = angular.noop; @@ -71,21 +88,32 @@ angular.scenario.Describe.prototype.xdescribe = angular.noop; /** * Defines a test. * - * @param {String} Name of the test. - * @param {Function} Body of the block. + * @param {string} name Name of the test. + * @param {Function} vody Body of the block. */ angular.scenario.Describe.prototype.it = function(name, body) { - var self = this; this.its.push({ definition: this, + only: this.only, name: name, - before: self.setupBefore, + before: this.setupBefore, body: body, - after: self.setupAfter + after: this.setupAfter }); }; /** + * Same as it() but makes iit tests the only test to run. + * + * @param {string} name Name of the test. + * @param {Function} body Body of the block. + */ +angular.scenario.Describe.prototype.iit = function(name, body) { + this.it.apply(this, arguments); + this.its[this.its.length-1].only = true; +}; + +/** * Use to disable a test block. */ angular.scenario.Describe.prototype.xit = angular.noop; @@ -93,6 +121,15 @@ angular.scenario.Describe.prototype.xit = angular.noop; /** * Gets an array of functions representing all the tests (recursively). * that can be executed with SpecRunner's. + * + * @return {Array<Object>} Array of it blocks { + * definition : Object // parent Describe + * only: boolean + * name: string + * before: Function + * body: Function + * after: Function + * } */ angular.scenario.Describe.prototype.getSpecs = function() { var specs = arguments[0] || []; @@ -102,5 +139,11 @@ angular.scenario.Describe.prototype.getSpecs = function() { angular.foreach(this.its, function(it) { specs.push(it); }); - return specs; + var only = []; + angular.foreach(specs, function(it) { + if (it.only) { + only.push(it); + } + }); + return (only.length && only) || specs; }; diff --git a/src/scenario/Future.js b/src/scenario/Future.js index 8853aa3f..f545c721 100644 --- a/src/scenario/Future.js +++ b/src/scenario/Future.js @@ -1,9 +1,9 @@ /** * A future action in a spec. * - * @param {String} name of the future action + * @param {string} name of the future action * @param {Function} future callback(error, result) - * @param {String} Optional. function that returns the file/line number. + * @param {Function} Optional. function that returns the file/line number. */ angular.scenario.Future = function(name, behavior, line) { this.name = name; @@ -17,7 +17,7 @@ angular.scenario.Future = function(name, behavior, line) { /** * Executes the behavior of the closure. * - * @param {Function} Callback function(error, result) + * @param {Function} doneFn Callback function(error, result) */ angular.scenario.Future.prototype.execute = function(doneFn) { var self = this; @@ -37,6 +37,8 @@ angular.scenario.Future.prototype.execute = function(doneFn) { /** * Configures the future to convert it's final with a function fn(value) + * + * @param {Function} fn function(value) that returns the parsed value */ angular.scenario.Future.prototype.parsedWith = function(fn) { this.parser = fn; diff --git a/src/scenario/HtmlUI.js b/src/scenario/HtmlUI.js deleted file mode 100644 index 78fe8c33..00000000 --- a/src/scenario/HtmlUI.js +++ /dev/null @@ -1,244 +0,0 @@ -/** - * User Interface for the Scenario Runner. - * - * @param {Object} The jQuery UI object for the UI. - */ -angular.scenario.ui.Html = function(context) { - this.context = context; - context.append( - '<div id="header">' + - ' <h1><span class="angular"><angular/></span>: Scenario Test Runner</h1>' + - ' <ul id="status-legend" class="status-display">' + - ' <li class="status-error">0 Errors</li>' + - ' <li class="status-failure">0 Failures</li>' + - ' <li class="status-success">0 Passed</li>' + - ' </ul>' + - '</div>' + - '<div id="specs">' + - ' <div class="test-children"></div>' + - '</div>' - ); -}; - -/** - * 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, - 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]); - } - ); -}; - -/** - * Finds the context of a spec block defined by the passed definition. - * - * @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; - while (currentDefinition && currentDefinition.name) { - path.unshift(currentDefinition); - currentDefinition = currentDefinition.parent; - } - angular.foreach(path, function(defn) { - var id = 'describe-' + defn.id; - if (!self.context.find('#' + id).length) { - currentContext.find('> .test-children').append( - '<div class="test-describe" id="' + id + '">' + - ' <h2></h2>' + - ' <div class="test-children"></div>' + - ' <ul class="tests"></ul>' + - '</div>' - ); - self.context.find('#' + id).find('> h2').text('describe: ' + defn.name); - } - currentContext = self.context.find('#' + id); - }); - return this.context.find('#describe-' + definition.id); -}; - -/** - * A spec block in the UI. - * - * @param {Object} The jQuery object for the context of the spec. - * @param {String} The name of the spec. - * @param {Function} Callback function(status) to call when complete. - */ -angular.scenario.ui.Html.Spec = function(context, name, doneFn) { - this.status = 'pending'; - this.context = context; - this.startTime = new Date().getTime(); - this.doneFn = doneFn; - context.append( - '<div class="test-info">' + - ' <p class="test-title">' + - ' <span class="timer-result"></span>' + - ' <span class="test-name"></span>' + - ' </p>' + - '</div>' + - '<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); -}; - -/** - * 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, 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, 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')); - }); -}; - -/** - * Completes the spec and sets the timer value. - */ -angular.scenario.ui.Html.Spec.prototype.complete = function() { - this.context.removeClass('status-pending'); - 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(); - } -}; - -/** - * Finishes the spec, possibly with an error. - * - * @param {Object} An optional error - */ -angular.scenario.ui.Html.Spec.prototype.finish = function() { - this.complete(); - this.context.addClass('status-' + this.status); - this.doneFn(this.status); -}; - -/** - * Finishes the spec, but with a Fatal Error. - * - * @param {Object} Required error - */ -angular.scenario.ui.Html.Spec.prototype.error = function(error) { - this.status = 'error'; - this.context.append('<pre></pre>'); - this.context.find('> pre').text(formatException(error)); - this.finish(); -}; - -/** - * A single step inside an it block (or a before/after function). - * - * @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, location, doneFn) { - this.context = context; - this.name = name; - this.location = location; - this.startTime = new Date().getTime(); - this.doneFn = doneFn; - context.append( - '<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(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); - } -}; - -/** - * Finishes the step, possibly with an error. - * - * @param {Object} An optional error - */ -angular.scenario.ui.Html.Step.prototype.finish = function(error) { - this.complete(error); - if (error) { - this.context.addClass('status-failure'); - this.doneFn('failure'); - } else { - this.context.addClass('status-success'); - this.doneFn('success'); - } -}; - -/** - * Finishes the step, but with a Fatal Error. - * - * @param {Object} Required error - */ -angular.scenario.ui.Html.Step.prototype.error = function(error) { - this.complete(error); - this.context.addClass('status-error'); - this.doneFn('error'); -}; diff --git a/src/scenario/ObjectModel.js b/src/scenario/ObjectModel.js new file mode 100644 index 00000000..e9125e03 --- /dev/null +++ b/src/scenario/ObjectModel.js @@ -0,0 +1,153 @@ +/** + * Maintains an object tree from the runner events. + * + * @param {Object} runner The scenario Runner instance to connect to. + * + * TODO(esprehn): Every output type creates one of these, but we probably + * want one glonal shared instance. Need to handle events better too + * so the HTML output doesn't need to do spec model.getSpec(spec.id) + * silliness. + */ +angular.scenario.ObjectModel = function(runner) { + var self = this; + + this.specMap = {}; + this.value = { + name: '', + children: {} + }; + + runner.on('SpecBegin', function(spec) { + var block = self.value; + angular.foreach(self.getDefinitionPath(spec), function(def) { + if (!block.children[def.name]) { + block.children[def.name] = { + id: def.id, + name: def.name, + children: {}, + specs: {} + }; + } + block = block.children[def.name]; + }); + self.specMap[spec.id] = block.specs[spec.name] = + new angular.scenario.ObjectModel.Spec(spec.id, spec.name); + }); + + runner.on('SpecError', function(spec, error) { + var it = self.getSpec(spec.id); + it.status = 'error'; + it.error = error; + }); + + runner.on('SpecEnd', function(spec) { + var it = self.getSpec(spec.id); + complete(it); + }); + + runner.on('StepBegin', function(spec, step) { + var it = self.getSpec(spec.id); + it.steps.push(new angular.scenario.ObjectModel.Step(step.name)); + }); + + runner.on('StepEnd', function(spec, step) { + var it = self.getSpec(spec.id); + if (it.getLastStep().name !== step.name) + throw 'Events fired in the wrong order. Step names don\' match.'; + complete(it.getLastStep()); + }); + + runner.on('StepFailure', function(spec, step, error) { + var it = self.getSpec(spec.id); + var item = it.getLastStep(); + item.error = error; + if (!it.status) { + it.status = item.status = 'failure'; + } + }); + + runner.on('StepError', function(spec, step, error) { + var it = self.getSpec(spec.id); + var item = it.getLastStep(); + it.status = 'error'; + item.status = 'error'; + item.error = error; + }); + + function complete(item) { + item.endTime = new Date().getTime(); + item.duration = item.endTime - item.startTime; + item.status = item.status || 'success'; + } +}; + +/** + * Computes the path of definition describe blocks that wrap around + * this spec. + * + * @param spec Spec to compute the path for. + * @return {Array<Describe>} The describe block path + */ +angular.scenario.ObjectModel.prototype.getDefinitionPath = function(spec) { + var path = []; + var currentDefinition = spec.definition; + while (currentDefinition && currentDefinition.name) { + path.unshift(currentDefinition); + currentDefinition = currentDefinition.parent; + } + return path; +}; + +/** + * Gets a spec by id. + * + * @param {string} The id of the spec to get the object for. + * @return {Object} the Spec instance + */ +angular.scenario.ObjectModel.prototype.getSpec = function(id) { + return this.specMap[id]; +}; + +/** + * A single it block. + * + * @param {string} id Id of the spec + * @param {string} name Name of the spec + */ +angular.scenario.ObjectModel.Spec = function(id, name) { + this.id = id; + this.name = name; + this.startTime = new Date().getTime(); + this.steps = []; +}; + +/** + * Adds a new step to the Spec. + * + * @param {string} step Name of the step (really name of the future) + * @return {Object} the added step + */ +angular.scenario.ObjectModel.Spec.prototype.addStep = function(name) { + var step = new angular.scenario.ObjectModel.Step(name); + this.steps.push(step); + return step; +}; + +/** + * Gets the most recent step. + * + * @return {Object} the step + */ +angular.scenario.ObjectModel.Spec.prototype.getLastStep = function() { + return this.steps[this.steps.length-1]; +}; + +/** + * A single step inside a Spec. + * + * @param {string} step Name of the step + */ +angular.scenario.ObjectModel.Step = function(name) { + this.name = name; + this.startTime = new Date().getTime(); +}; diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js index a8b23f83..f628eb04 100644 --- a/src/scenario/Runner.js +++ b/src/scenario/Runner.js @@ -2,13 +2,16 @@ * Runner for scenarios. */ angular.scenario.Runner = function($window) { + this.listeners = []; this.$window = $window; this.rootDescribe = new angular.scenario.Describe(); this.currentDescribe = this.rootDescribe; this.api = { it: this.it, + iit: this.iit, xit: angular.noop, describe: this.describe, + ddescribe: this.ddescribe, xdescribe: angular.noop, beforeEach: this.beforeEach, afterEach: this.afterEach @@ -19,10 +22,41 @@ angular.scenario.Runner = function($window) { }; /** + * Emits an event which notifies listeners and passes extra + * arguments. + * + * @param {string} eventName Name of the event to fire. + */ +angular.scenario.Runner.prototype.emit = function(eventName) { + var self = this; + var args = Array.prototype.slice.call(arguments, 1); + eventName = eventName.toLowerCase(); + if (!this.listeners[eventName]) + return; + angular.foreach(this.listeners[eventName], function(listener) { + listener.apply(self, args); + }); +}; + +/** + * Adds a listener for an event. + * + * @param {string} eventName The name of the event to add a handler for + * @param {string} listener The fn(...) that takes the extra arguments from emit() + */ +angular.scenario.Runner.prototype.on = function(eventName, listener) { + eventName = eventName.toLowerCase(); + this.listeners[eventName] = this.listeners[eventName] || []; + this.listeners[eventName].push(listener); +}; + +/** * Defines a describe block of a spec. * - * @param {String} Name of the block - * @param {Function} Body of the block + * @see Describe.js + * + * @param {string} name Name of the block + * @param {Function} body Body of the block */ angular.scenario.Runner.prototype.describe = function(name, body) { var self = this; @@ -38,19 +72,56 @@ angular.scenario.Runner.prototype.describe = function(name, body) { }; /** + * Same as describe, but makes ddescribe the only blocks to run. + * + * @see Describe.js + * + * @param {string} name Name of the block + * @param {Function} body Body of the block + */ +angular.scenario.Runner.prototype.ddescribe = function(name, body) { + var self = this; + this.currentDescribe.ddescribe(name, function() { + var parentDescribe = self.currentDescribe; + self.currentDescribe = this; + try { + body.call(this); + } finally { + self.currentDescribe = parentDescribe; + } + }); +}; + +/** * Defines a test in a describe block of a spec. * - * @param {String} Name of the block - * @param {Function} Body of the block + * @see Describe.js + * + * @param {string} name Name of the block + * @param {Function} body Body of the block */ angular.scenario.Runner.prototype.it = function(name, body) { this.currentDescribe.it(name, body); }; /** + * Same as it, but makes iit tests the only tests to run. + * + * @see Describe.js + * + * @param {string} name Name of the block + * @param {Function} body Body of the block + */ +angular.scenario.Runner.prototype.iit = function(name, body) { + this.currentDescribe.iit(name, body); +}; + +/** * Defines a function to be called before each it block in the describe * (and before all nested describes). * + * @see Describe.js + * * @param {Function} Callback to execute */ angular.scenario.Runner.prototype.beforeEach = function(body) { @@ -61,6 +132,8 @@ angular.scenario.Runner.prototype.beforeEach = function(body) { * Defines a function to be called after each it block in the describe * (and before all nested describes). * + * @see Describe.js + * * @param {Function} Callback to execute */ angular.scenario.Runner.prototype.afterEach = function(body) { @@ -68,24 +141,29 @@ angular.scenario.Runner.prototype.afterEach = function(body) { }; /** - * Defines a function to be called before each it block in the describe - * (and before all nested describes). + * Creates a new spec runner. * - * @param {Function} Callback to execute + * @private + * @param {Object} scope parent scope + */ +angular.scenario.Runner.prototype.createSpecRunner_ = function(scope) { + return scope.$new(angular.scenario.SpecRunner); +}; + +/** + * Runs all the loaded tests with the specified runner class on the + * provided application. + * + * @param {angular.scenario.Application} application App to remote control. */ -angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClass, specsDone) { - var $root = angular.scope({}, angular.service); +angular.scenario.Runner.prototype.run = function(application) { var self = this; - var specs = this.rootDescribe.getSpecs(); + var $root = angular.scope(this); $root.application = application; - $root.ui = ui; - $root.setTimeout = function() { - return self.$window.setTimeout.apply(self.$window, arguments); - }; - asyncForEach(specs, function(spec, specDone) { + this.emit('RunnerBegin'); + asyncForEach(this.rootDescribe.getSpecs(), function(spec, specDone) { var dslCache = {}; - var runner = angular.scope($root); - runner.$become(specRunnerClass); + var runner = self.createSpecRunner_($root); angular.foreach(angular.scenario.dsl, function(fn, key) { dslCache[key] = fn.call($root); }); @@ -105,16 +183,24 @@ angular.scenario.Runner.prototype.run = function(ui, application, specRunnerClas // Make these methods work on the current chain scope.addFuture = function() { Array.prototype.push.call(arguments, line); - return specRunnerClass.prototype.addFuture.apply(scope, arguments); + return angular.scenario.SpecRunner. + prototype.addFuture.apply(scope, arguments); }; scope.addFutureAction = function() { Array.prototype.push.call(arguments, line); - return specRunnerClass.prototype.addFutureAction.apply(scope, arguments); + return angular.scenario.SpecRunner. + prototype.addFutureAction.apply(scope, arguments); }; return scope.dsl[key].apply(scope, arguments); }; }); - runner.run(ui, spec, specDone); - }, specsDone || angular.noop); + runner.run(spec, specDone); + }, + function(error) { + if (error) { + self.emit('RunnerError', error); + } + self.emit('RunnerEnd'); + }); }; diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js index c00ed3dd..f2ebc640 100644 --- a/src/scenario/Scenario.js +++ b/src/scenario/Scenario.js @@ -6,8 +6,15 @@ // Public namespace angular.scenario = angular.scenario || {}; -// Namespace for the UI -angular.scenario.ui = angular.scenario.ui || {}; +/** + * Defines a new output format. + * + * @param {string} name the name of the new output format + * @param {Function} fn function(context, runner) that generates the output + */ +angular.scenario.output = angular.scenario.output || function(name, fn) { + angular.scenario.output[name] = fn; +}; /** * Defines a new DSL statement. If your factory function returns a Future @@ -18,8 +25,8 @@ angular.scenario.ui = angular.scenario.ui || {}; * set on "this" in your statement function are available in the chained * functions. * - * @param {String} The name of the statement - * @param {Function} Factory function(application), return a function for + * @param {string} name The name of the statement + * @param {Function} fn Factory function(), return a function for * the statement. */ angular.scenario.dsl = angular.scenario.dsl || function(name, fn) { @@ -54,8 +61,8 @@ angular.scenario.dsl = angular.scenario.dsl || function(name, fn) { * against. Your function should return a boolean. The future is automatically * created for you. * - * @param {String} The name of the matcher - * @param {Function} The matching function(expected). + * @param {string} name The name of the matcher + * @param {Function} fn The matching function(expected). */ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) { angular.scenario.matcher[name] = function(expected) { @@ -79,13 +86,55 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) { }; /** + * Initialization function for the scenario runner. + * + * @param {angular.scenario.Runner} $scenario The runner to setup + * @param {Object} config Config options + */ +function angularScenarioInit($scenario, config) { + var body = _jQuery(document.body); + var output = []; + + if (config.scenario_output) { + output = config.scenario_output.split(','); + } + + angular.foreach(angular.scenario.output, function(fn, name) { + if (!output.length || indexOf(output,name) != -1) { + var context = body.append('<div></div>').find('div:last'); + context.attr('id', name); + fn.call({}, context, $scenario); + } + }); + + var appFrame = body.append('<div id="application"></div>').find('#application'); + var application = new angular.scenario.Application(appFrame); + + $scenario.on('RunnerEnd', function() { + appFrame.css('display', 'none'); + appFrame.find('iframe').attr('src', 'about:blank'); + }); + + $scenario.on('RunnerError', function(error) { + if (window.console) { + console.log(formatException(error)); + } else { + // Do something for IE + alert(error); + } + }); + + $scenario.run(application); +} + +/** * Iterates through list with iterator function that must call the * continueFunction to continute iterating. * - * @param {Array} list to iterate over - * @param {Function} Callback function(value, continueFunction) - * @param {Function} Callback function(error, result) called when iteration - * finishes or an error occurs. + * @param {Array} list list to iterate over + * @param {Function} iterator Callback function(value, continueFunction) + * @param {Function} done Callback function(error, result) called when + * iteration finishes or an error occurs. */ function asyncForEach(list, iterator, done) { var i = 0; @@ -110,8 +159,8 @@ function asyncForEach(list, iterator, done) { * 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 + * @param {Object} error The exception to format, can be anything throwable + * @param {Number} maxStackLines Optional. max lines of the stack trace to include * default is 5. */ function formatException(error, maxStackLines) { @@ -134,6 +183,8 @@ function formatException(error, maxStackLines) { * * Note: this returns another function because accessing .stack is very * expensive in Chrome. + * + * @param {Number} offset Number of stack lines to skip */ function callerFile(offset) { var error = new Error(); @@ -161,7 +212,7 @@ function callerFile(offset) { * not specified. * * @param {Object} Either a wrapped jQuery/jqLite node or a DOMElement - * @param {String} Optional event type. + * @param {string} Optional event type. */ function browserTrigger(element, type) { if (element && !element.nodeName) element = element[0]; diff --git a/src/scenario/SpecRunner.js b/src/scenario/SpecRunner.js index 98ce4b53..fb7d5c02 100644 --- a/src/scenario/SpecRunner.js +++ b/src/scenario/SpecRunner.js @@ -15,14 +15,16 @@ angular.scenario.SpecRunner = function() { * Executes a spec which is an it block with associated before/after functions * based on the describe nesting. * - * @param {Object} An angular.scenario.UI implementation - * @param {Object} A spec object - * @param {Object} An angular.scenario.Application instance + * @param {Object} spec A spec object + * @param {Object} specDone An angular.scenario.Application instance * @param {Function} Callback function that is called when the spec finshes. */ -angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) { +angular.scenario.SpecRunner.prototype.run = function(spec, specDone) { var self = this; - var specUI = ui.addSpec(spec); + var count = 0; + this.spec = spec; + + this.emit('SpecBegin', spec); try { spec.before.call(this); @@ -30,7 +32,8 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) { this.afterIndex = this.futures.length; spec.after.call(this); } catch (e) { - specUI.error(e); + this.emit('SpecError', spec, e); + this.emit('SpecEnd', spec); specDone(); return; } @@ -42,32 +45,40 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) { self.error = true; done(null, self.afterIndex); }; - - var spec = this; + asyncForEach( this.futures, function(future, futureDone) { - var stepUI = specUI.addStep(future.name, future.line); + self.step = future; + self.emit('StepBegin', spec, future); try { future.execute(function(error) { - stepUI.finish(error); if (error) { + self.emit('StepFailure', spec, future, error); + self.emit('StepEnd', spec, future); return handleError(error, futureDone); } - spec.$window.setTimeout( function() { futureDone(); }, 0); + self.emit('StepEnd', spec, future); + if ((count++) % 10 === 0) { + self.$window.setTimeout(function() { futureDone(); }, 0); + } else { + futureDone(); + } }); } catch (e) { - stepUI.error(e); + self.emit('StepError', spec, future, e); + self.emit('StepEnd', spec, future); handleError(e, futureDone); } }, function(e) { if (e) { - specUI.error(e); - } else { - specUI.finish(); + self.emit('SpecError', spec, e); } - specDone(); + self.emit('SpecEnd', spec); + // Call done in a timeout so exceptions don't recursively + // call this function + self.$window.setTimeout(function() { specDone(); }, 0); } ); }; @@ -77,9 +88,9 @@ angular.scenario.SpecRunner.prototype.run = function(ui, spec, specDone) { * * 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 + * @param {string} name Name of the future + * @param {Function} behavior Behavior of the future + * @param {Function} line fn() that returns file/line number */ angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line) { var future = new angular.scenario.Future(name, angular.bind(this, behavior), line); @@ -92,14 +103,16 @@ angular.scenario.SpecRunner.prototype.addFuture = function(name, behavior, line) * * 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 + * @param {string} name Name of the future + * @param {Function} behavior Behavior of the future + * @param {Function} line fn() that returns file/line number */ angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior, line) { var self = this; return this.addFuture(name, function(done) { this.application.executeAction(function($window, $document) { + + //TODO(esprehn): Refactor this so it doesn't need to be in here. $document.elements = function(selector) { var args = Array.prototype.slice.call(arguments, 1); if (self.selector) { diff --git a/src/scenario/bootstrap.js b/src/scenario/angular-bootstrap.js index 4661bfb2..68dc393e 100644 --- a/src/scenario/bootstrap.js +++ b/src/scenario/angular-bootstrap.js @@ -1,6 +1,6 @@ (function(previousOnLoad){ var prefix = (function(){ - var filename = /(.*\/)bootstrap.js(#(.*))?/; + var filename = /(.*\/)angular-bootstrap.js(#(.*))?/; var scripts = document.getElementsByTagName("script"); for(var j = 0; j < scripts.length; j++) { var src = scripts[j].src; @@ -23,50 +23,36 @@ try { if (previousOnLoad) previousOnLoad(); } catch(e) {} - _jQuery(document.body).append( - '<div id="runner"></div>' + - '<div id="frame"></div>' - ); - var frame = _jQuery('#frame'); - var runner = _jQuery('#runner'); - var application = new angular.scenario.Application(frame); - var ui = new angular.scenario.ui.Html(runner); - $scenario.run(ui, application, angular.scenario.SpecRunner, function(error) { - frame.remove(); - if (error) { - if (window.console) { - console.log(error.stack || error); - } else { - // Do something for IE - alert(error); - } - } - }); + angularScenarioInit($scenario, angularJsConfig(document)); }; addCSS("../../css/angular-scenario.css"); addScript("../../lib/jquery/jquery-1.4.2.js"); document.write( - '<script type="text/javascript">' + - 'var _jQuery = jQuery.noConflict(true);' + - '</script>' - ); + '<script type="text/javascript">' + + 'var _jQuery = jQuery.noConflict(true);' + + '</script>' + ); addScript("../angular-bootstrap.js"); addScript("Scenario.js"); addScript("Application.js"); addScript("Describe.js"); addScript("Future.js"); - addScript("HtmlUI.js"); addScript("Runner.js"); addScript("SpecRunner.js"); addScript("dsl.js"); addScript("matchers.js"); + addScript("ObjectModel.js"); + addScript("output/Html.js"); + addScript("output/Json.js"); + addScript("output/Object.js"); + addScript("output/Xml.js"); // Create the runner (which also sets up the global API) document.write( '<script type="text/javascript">' + - 'var $scenario = new angular.scenario.Runner(window);' + + 'var $scenario = new angular.scenario.Runner(window, angular.scenario.SpecRunner);' + '</script>' ); diff --git a/src/scenario/angular.prefix b/src/scenario/angular.prefix index d6660d61..fb9ae147 100644 --- a/src/scenario/angular.prefix +++ b/src/scenario/angular.prefix @@ -22,4 +22,4 @@ * THE SOFTWARE. */ (function(window, document, previousOnLoad){ - var _jQuery = window.jQuery.noConflict(true);
\ No newline at end of file + var _jQuery = window.jQuery.noConflict(true); diff --git a/src/scenario/angular.suffix b/src/scenario/angular.suffix index c38f0ab5..66843013 100644 --- a/src/scenario/angular.suffix +++ b/src/scenario/angular.suffix @@ -4,25 +4,7 @@ try { if (previousOnLoad) previousOnLoad(); } catch(e) {} - _jQuery(document.body).append( - '<div id="runner"></div>' + - '<div id="frame"></div>' - ); - var frame = _jQuery('#frame'); - var runner = _jQuery('#runner'); - var application = new angular.scenario.Application(frame); - var ui = new angular.scenario.ui.Html(runner); - $scenario.run(ui, application, angular.scenario.SpecRunner, function(error) { - frame.remove(); - if (error) { - if (window.console) { - console.log(error.stack || error); - } else { - // Do something for IE - alert(error); - } - } - }); + angularScenarioInit($scenario, angularJsConfig(document)); }; })(window, document, window.onload); diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js index f4484df8..1ae26db8 100644 --- a/src/scenario/dsl.js +++ b/src/scenario/dsl.js @@ -1,18 +1,19 @@ /** * Shared DSL statements that are useful to all scenarios. */ - + /** * Usage: * wait() waits until you call resume() in the console */ - angular.scenario.dsl('wait', function() { +angular.scenario.dsl('wait', function() { return function() { - return this.addFuture('waiting for you to call resume() in the console', function(done) { + return this.addFuture('waiting for you to resume', function(done) { + this.emit('InteractiveWait', this.spec, this.step); this.$window.resume = function() { done(); }; }); }; - }); +}); /** * Usage: @@ -21,7 +22,7 @@ angular.scenario.dsl('pause', function() { return function(time) { return this.addFuture('pause for ' + time + ' seconds', function(done) { - this.setTimeout(function() { done(null, time * 1000); }, time * 1000); + this.$window.setTimeout(function() { done(null, time * 1000); }, time * 1000); }); }; }); @@ -49,8 +50,8 @@ angular.scenario.dsl('expect', function() { /** * Usage: - * navigateTo(future|string) where url a string or future with a value - * of a URL to navigate to + * navigateTo(url) Loads the url into the frame + * navigateTo(url, fn) where fn(url) is called and returns the URL to navigate to */ angular.scenario.dsl('navigateTo', function() { return function(url, delegate) { @@ -60,17 +61,7 @@ angular.scenario.dsl('navigateTo', function() { 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); - }); - } else { - done(null, url); - } - }); + done(null, url); }); }); }; @@ -162,7 +153,11 @@ angular.scenario.dsl('repeater', function() { chain.count = function() { return this.addFutureAction('repeater ' + this.selector + ' count', function($window, $document, done) { - done(null, $document.elements().size()); + try { + done(null, $document.elements().length); + } catch (e) { + done(null, 0); + } }); }; @@ -238,6 +233,7 @@ angular.scenario.dsl('select', function() { /** * Usage: + * element(selector).count() get the number of elements that match selector * 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 @@ -248,10 +244,28 @@ angular.scenario.dsl('select', function() { angular.scenario.dsl('element', function() { var chain = {}; + chain.count = function() { + return this.addFutureAction('element ' + this.selector + ' count', function($window, $document, done) { + try { + done(null, $document.elements().length); + } catch (e) { + done(null, 0); + } + }); + }; + chain.click = function() { return this.addFutureAction('element ' + this.selector + ' click', function($window, $document, done) { - $document.elements().trigger('click'); - done(); + var elements = $document.elements(); + var href = elements.attr('href'); + elements.trigger('click'); + if (href && elements[0].nodeName.toUpperCase() === 'A') { + this.application.navigateTo(href, function() { + done(); + }); + } else { + done(); + } }); }; diff --git a/src/scenario/matchers.js b/src/scenario/matchers.js index 0dfbc455..8ef154e9 100644 --- a/src/scenario/matchers.js +++ b/src/scenario/matchers.js @@ -6,6 +6,10 @@ angular.scenario.matcher('toEqual', function(expected) { return angular.equals(this.actual, expected); }); +angular.scenario.matcher('toBe', function(expected) { + return this.actual === expected; +}); + angular.scenario.matcher('toBeDefined', function() { return angular.isDefined(this.actual); }); diff --git a/src/scenario/output/Html.js b/src/scenario/output/Html.js new file mode 100644 index 00000000..4a682b9a --- /dev/null +++ b/src/scenario/output/Html.js @@ -0,0 +1,165 @@ +/** + * User Interface for the Scenario Runner. + * + * TODO(esprehn): This should be refactored now that ObjectModel exists + * to use angular bindings for the UI. + */ +angular.scenario.output('html', function(context, runner) { + var model = new angular.scenario.ObjectModel(runner); + + context.append( + '<div id="header">' + + ' <h1><span class="angular"><angular/></span>: Scenario Test Runner</h1>' + + ' <ul id="status-legend" class="status-display">' + + ' <li class="status-error">0 Errors</li>' + + ' <li class="status-failure">0 Failures</li>' + + ' <li class="status-success">0 Passed</li>' + + ' </ul>' + + '</div>' + + '<div id="specs">' + + ' <div class="test-children"></div>' + + '</div>' + ); + + runner.on('InteractiveWait', function(spec, step) { + var ui = model.getSpec(spec.id).getLastStep().ui; + ui.find('.test-title'). + html('waiting for you to <a href="javascript:resume()">resume</a>.'); + }); + + runner.on('SpecBegin', function(spec) { + var ui = findContext(spec); + ui.find('> .tests').append( + '<li class="status-pending test-it"></li>' + ); + ui = ui.find('> .tests li:last'); + ui.append( + '<div class="test-info">' + + ' <p class="test-title">' + + ' <span class="timer-result"></span>' + + ' <span class="test-name"></span>' + + ' </p>' + + '</div>' + + '<div class="scrollpane">' + + ' <ol class="test-actions"></ol>' + + '</div>' + ); + ui.find('> .test-info .test-name').text(spec.name); + ui.find('> .test-info').click(function() { + var scrollpane = ui.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'); + } + }); + model.getSpec(spec.id).ui = ui; + }); + + runner.on('SpecError', function(spec, error) { + var ui = model.getSpec(spec.id).ui; + ui.append('<pre></pre>'); + ui.find('> pre').text(formatException(error)); + }); + + runner.on('SpecEnd', function(spec) { + spec = model.getSpec(spec.id); + spec.ui.removeClass('status-pending'); + spec.ui.addClass('status-' + spec.status); + spec.ui.find("> .test-info .timer-result").text(spec.duration + "ms"); + if (spec.status === 'success') { + spec.ui.find('> .test-info .test-name').addClass('closed'); + spec.ui.find('> .scrollpane .test-actions').hide(); + } + updateTotals(spec.status); + }); + + runner.on('StepBegin', function(spec, step) { + spec = model.getSpec(spec.id); + step = spec.getLastStep(); + spec.ui.find('> .scrollpane .test-actions'). + append('<li class="status-pending"></li>'); + step.ui = spec.ui.find('> .scrollpane .test-actions li:last'); + step.ui.append( + '<div class="timer-result"></div>' + + '<div class="test-title"></div>' + ); + step.ui.find('> .test-title').text(step.name); + var scrollpane = step.ui.parents('.scrollpane'); + scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight')); + }); + + runner.on('StepFailure', function(spec, step, error) { + var ui = model.getSpec(spec.id).getLastStep().ui; + addError(ui, step.line, error); + }); + + runner.on('StepError', function(spec, step, error) { + var ui = model.getSpec(spec.id).getLastStep().ui; + addError(ui, step.line, error); + }); + + runner.on('StepEnd', function(spec, step) { + spec = model.getSpec(spec.id); + step = spec.getLastStep(); + step.ui.find('.timer-result').text(step.duration + 'ms'); + step.ui.removeClass('status-pending'); + step.ui.addClass('status-' + step.status); + var scrollpane = spec.ui.find('> .scrollpane'); + scrollpane.attr('scrollTop', scrollpane.attr('scrollHeight')); + }); + + /** + * Finds the context of a spec block defined by the passed definition. + * + * @param {Object} The definition created by the Describe object. + */ + function findContext(spec) { + var currentContext = context.find('#specs'); + angular.foreach(model.getDefinitionPath(spec), function(defn) { + var id = 'describe-' + defn.id; + if (!context.find('#' + id).length) { + currentContext.find('> .test-children').append( + '<div class="test-describe" id="' + id + '">' + + ' <h2></h2>' + + ' <div class="test-children"></div>' + + ' <ul class="tests"></ul>' + + '</div>' + ); + context.find('#' + id).find('> h2').text('describe: ' + defn.name); + } + currentContext = context.find('#' + id); + }); + return context.find('#describe-' + spec.definition.id); + }; + + /** + * Updates the test counter for the status. + * + * @param {string} the status. + */ + function updateTotals(status) { + var legend = context.find('#status-legend .status-' + status); + var parts = legend.text().split(' '); + var value = (parts[0] * 1) + 1; + legend.text(value + ' ' + parts[1]); + } + + /** + * Add an error to a step. + * + * @param {Object} The JQuery wrapped context + * @param {Function} fn() that should return the file/line number of the error + * @param {Object} the error. + */ + function addError(context, line, error) { + context.find('.test-title').append('<pre></pre>'); + var message = _jQuery.trim(line() + '\n\n' + formatException(error)); + context.find('.test-title pre:last').text(message); + }; +}); diff --git a/src/scenario/output/Json.js b/src/scenario/output/Json.js new file mode 100644 index 00000000..94212301 --- /dev/null +++ b/src/scenario/output/Json.js @@ -0,0 +1,10 @@ +/** + * Generates JSON output into a context. + */ +angular.scenario.output('json', function(context, runner) { + var model = new angular.scenario.ObjectModel(runner); + + runner.on('RunnerEnd', function() { + context.text(angular.toJson(model.value)); + }); +}); diff --git a/src/scenario/output/Object.js b/src/scenario/output/Object.js new file mode 100644 index 00000000..3257cfd7 --- /dev/null +++ b/src/scenario/output/Object.js @@ -0,0 +1,6 @@ +/** + * Creates a global value $result with the result of the runner. + */ +angular.scenario.output('object', function(context, runner) { + runner.$window.$result = new angular.scenario.ObjectModel(runner).value; +}); diff --git a/src/scenario/output/Xml.js b/src/scenario/output/Xml.js new file mode 100644 index 00000000..47d98c78 --- /dev/null +++ b/src/scenario/output/Xml.js @@ -0,0 +1,48 @@ +/** + * Generates XML output into a context. + */ +angular.scenario.output('xml', function(context, runner) { + var model = new angular.scenario.ObjectModel(runner); + + runner.on('RunnerEnd', function() { + context.append('<scenario></scenario>'); + serializeXml(context.find('> scenario'), model.value); + }); + + /** + * Convert the tree into XML. + * + * @param {Object} context jQuery context to add the XML to. + * @param {Object} tree node to serialize + */ + function serializeXml(context, tree) { + angular.foreach(tree.children, function(child) { + context.append('<describe></describe>'); + var describeContext = context.find('> describe:last'); + describeContext.attr('id', child.id); + describeContext.attr('name', child.name); + serializeXml(describeContext, child); + }); + context.append('<its></its>'); + context = context.find('> its'); + angular.foreach(tree.specs, function(spec) { + context.append('<it></it>') + var specContext = context.find('> it:last'); + specContext.attr('id', spec.id); + specContext.attr('name', spec.name); + specContext.attr('duration', spec.duration); + specContext.attr('status', spec.status); + angular.foreach(spec.steps, function(step) { + specContext.append('<step></step>'); + var stepContext = specContext.find('> step:last'); + stepContext.attr('name', step.name); + stepContext.attr('duration', step.duration); + stepContext.attr('status', step.status); + if (step.error) { + stepContext.append('<error></error'); + stepContext.find('error').text(formatException(step.error)); + } + }); + }); + } +}); diff --git a/test/AngularSpec.js b/test/AngularSpec.js index b60b7bd8..bab7df18 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -196,6 +196,27 @@ describe ('rngScript', function() { expect('my-angular-app-0.9.0-de0a8612.min.js'.match(rngScript)).toBeNull(); expect('foo/../my-angular-app-0.9.0-de0a8612.min.js'.match(rngScript)).toBeNull(); }); + + it('should match angular-scenario.js', function() { + expect('angular-scenario.js'.match(rngScript)).not.toBeNull(); + expect('angular-scenario.min.js'.match(rngScript)).not.toBeNull(); + expect('../angular-scenario.js'.match(rngScript)).not.toBeNull(); + expect('foo/angular-scenario.min.js'.match(rngScript)).not.toBeNull(); + }); + + it('should match angular-scenario-0.9.0(.min).js', function() { + expect('angular-scenario-0.9.0.js'.match(rngScript)).not.toBeNull(); + expect('angular-scenario-0.9.0.min.js'.match(rngScript)).not.toBeNull(); + expect('../angular-scenario-0.9.0.js'.match(rngScript)).not.toBeNull(); + expect('foo/angular-scenario-0.9.0.min.js'.match(rngScript)).not.toBeNull(); + }); + + it('should match angular-scenario-0.9.0-de0a8612(.min).js', function() { + expect('angular-scenario-0.9.0-de0a8612.js'.match(rngScript)).not.toBeNull(); + expect('angular-scenario-0.9.0-de0a8612.min.js'.match(rngScript)).not.toBeNull(); + expect('../angular-scenario-0.9.0-de0a8612.js'.match(rngScript)).not.toBeNull(); + expect('foo/angular-scenario-0.9.0-de0a8612.min.js'.match(rngScript)).not.toBeNull(); + }); }); diff --git a/test/scenario/ApplicationSpec.js b/test/scenario/ApplicationSpec.js index 883701ba..122292c6 100644 --- a/test/scenario/ApplicationSpec.js +++ b/test/scenario/ApplicationSpec.js @@ -7,9 +7,10 @@ describe('angular.scenario.Application', function() { }); it('should return new $window and $document after navigate', function() { + var called; var testWindow, testDocument, counter = 0; app.navigateTo = noop; - app.getWindow = function() { + app.getWindow_ = function() { return {x:counter++, document:{x:counter++}}; }; app.navigateTo('http://www.google.com/'); @@ -21,30 +22,45 @@ describe('angular.scenario.Application', function() { app.executeAction(function($window, $document) { expect($window).not.toEqual(testWindow); expect($document).not.toEqual(testDocument); + called = true; }); + expect(called).toBeTruthy(); }); it('should execute callback with correct arguments', function() { + var called; var testWindow = {document: {}}; - app.getWindow = function() { + app.getWindow_ = function() { return testWindow; }; app.executeAction(function($window, $document) { expect(this).toEqual(app); expect($document).toEqual(_jQuery($window.document)); expect($window).toEqual(testWindow); + called = true; }); + expect(called).toBeTruthy(); }); - it('should create a new iframe each time', function() { + it('should use a new iframe each time', function() { app.navigateTo('about:blank'); - var frame = app.getFrame(); + var frame = app.getFrame_(); frame.attr('test', true); app.navigateTo('about:blank'); - expect(app.getFrame().attr('test')).toBeFalsy(); + expect(app.getFrame_().attr('test')).toBeFalsy(); }); - it('should URL description bar', function() { + it('should hide old iframes and navigate to about:blank', function() { + app.navigateTo('about:blank#foo'); + app.navigateTo('about:blank#bar'); + var iframes = frames.find('iframe'); + expect(iframes.length).toEqual(2); + expect(iframes[0].src).toEqual('about:blank'); + expect(iframes[1].src).toEqual('about:blank#bar'); + expect(_jQuery(iframes[0]).css('display')).toEqual('none'); + }); + + it('should URL update description bar', function() { app.navigateTo('about:blank'); var anchor = frames.find('> h2 a'); expect(anchor.attr('href')).toEqual('about:blank'); @@ -53,24 +69,48 @@ describe('angular.scenario.Application', function() { it('should call onload handler when frame loads', function() { var called; - app.getFrame = function() { - // Mock a little jQuery - var result = { - remove: function() { - return result; - }, - attr: function(key, value) { - return (!value) ? 'attribute value' : result; - }, - load: function() { - called = true; - } - }; - return result; + app.getWindow_ = function() { + return {}; }; - app.navigateTo('about:blank', function() { + app.navigateTo('about:blank', function($window, $document) { called = true; }); + var handlers = app.getFrame_().data('events').load; + expect(handlers).toBeDefined(); + expect(handlers.length).toEqual(1); + handlers[0].handler(); expect(called).toBeTruthy(); }); + + it('should wait for pending requests in executeAction', function() { + var called, polled; + var handlers = []; + var testWindow = { + document: _jQuery('<div class="test-foo"></div>'), + angular: { + service: {}, + } + }; + testWindow.angular.service.$browser = function() { + return { + poll: function() { + polled = true; + }, + notifyWhenNoOutstandingRequests: function(fn) { + handlers.push(fn); + } + } + }; + app.getWindow_ = function() { + return testWindow; + }; + app.executeAction(function($window, $document) { + expect($window).toEqual(testWindow); + expect($document).toBeDefined(); + expect($document[0].className).toEqual('test-foo'); + }); + expect(polled).toBeTruthy(); + expect(handlers.length).toEqual(1); + handlers[0](); + }); }); diff --git a/test/scenario/DescribeSpec.js b/test/scenario/DescribeSpec.js index c2e7310e..6fcee731 100644 --- a/test/scenario/DescribeSpec.js +++ b/test/scenario/DescribeSpec.js @@ -80,6 +80,27 @@ describe('angular.scenario.Describe', function() { expect(specs.length).toEqual(0); }); + it('should only return iit and ddescribe if present', function() { + root.describe('A', function() { + this.it('1', angular.noop); + this.iit('2', angular.noop); + this.describe('B', function() { + this.it('3', angular.noop); + this.ddescribe('C', function() { + this.it('4', angular.noop); + this.describe('D', function() { + this.it('5', angular.noop); + }); + }); + }); + }); + var specs = root.getSpecs(); + expect(specs.length).toEqual(3); + expect(specs[0].name).toEqual('5'); + expect(specs[1].name).toEqual('4'); + expect(specs[2].name).toEqual('2'); + }); + it('should create uniqueIds in the tree', function() { angular.scenario.Describe.id = 0; var a = new angular.scenario.Describe(); diff --git a/test/scenario/HtmlUISpec.js b/test/scenario/HtmlUISpec.js deleted file mode 100644 index 2c9ff080..00000000 --- a/test/scenario/HtmlUISpec.js +++ /dev/null @@ -1,98 +0,0 @@ -describe('angular.scenario.HtmlUI', function() { - var ui; - var context; - var spec; - - function line() { return 'unknown:-1'; } - - beforeEach(function() { - spec = { - name: 'test spec', - definition: { - id: 10, - name: 'child', - children: [], - parent: { - id: 20, - name: 'parent', - children: [] - } - } - }; - context = _jQuery("<div></div>"); - ui = new angular.scenario.ui.Html(context); - }); - - it('should create nested describe context', function() { - ui.addSpec(spec); - expect(context.find('#describe-20 #describe-10 > h2').text()). - toEqual('describe: child'); - expect(context.find('#describe-20 > h2').text()).toEqual('describe: parent'); - expect(context.find('#describe-10 .tests > li .test-info .test-name').text()). - toEqual('it test spec'); - expect(context.find('#describe-10 .tests > li').hasClass('status-pending')). - toBeTruthy(); - }); - - it('should update totals when steps complete', function() { - // Error - ui.addSpec(spec).error('error'); - // Failure - specUI = ui.addSpec(spec); - specUI.addStep('some step', line).finish('failure'); - specUI.finish(); - // Failure - specUI = ui.addSpec(spec); - specUI.addStep('some step', line).finish('failure'); - specUI.finish(); - // Failure - specUI = ui.addSpec(spec); - specUI.addStep('some step', line).finish('failure'); - specUI.finish(); - // Success - specUI = ui.addSpec(spec); - 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-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', line).finish(); - specUI.finish(); - - // Failure - specUI = ui.addSpec(spec); - specUI.addStep('some step', line).finish('failure'); - specUI.finish('failure'); - - // Error - specUI = ui.addSpec(spec).error('error'); - - context.find('#describe-10 .tests > li .test-info .timer-result'). - each(function(index, timer) { - 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/ObjectModelSpec.js b/test/scenario/ObjectModelSpec.js new file mode 100644 index 00000000..8b83a52f --- /dev/null +++ b/test/scenario/ObjectModelSpec.js @@ -0,0 +1,112 @@ +describe('angular.scenario.ObjectModel', function() { + var model; + var runner; + var spec, step; + + beforeEach(function() { + spec = { + name: 'test spec', + definition: { + id: 10, + name: 'describe 1' + } + }; + step = { + name: 'test step', + line: function() { return ''; } + }; + runner = new angular.scenario.testing.MockRunner(); + model = new angular.scenario.ObjectModel(runner); + }); + + it('should value default empty value', function() { + expect(model.value).toEqual({ + name: '', + children: [] + }); + }); + + it('should add spec and create describe blocks on SpecBegin event', function() { + runner.emit('SpecBegin', { + name: 'test spec', + definition: { + id: 10, + name: 'describe 2', + parent: { + id: 12, + name: 'describe 1' + } + } + }); + + expect(model.value.children['describe 1']).toBeDefined(); + expect(model.value.children['describe 1'].children['describe 2']).toBeDefined(); + expect(model.value.children['describe 1'].children['describe 2'].specs['test spec']).toBeDefined(); + }); + + it('should add step to spec on StepBegin', function() { + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + + expect(model.value.children['describe 1'].specs['test spec'].steps.length).toEqual(1); + }); + + it('should update spec timer duration on SpecEnd event', function() { + runner.emit('SpecBegin', spec); + runner.emit('SpecEnd', spec); + + expect(model.value.children['describe 1'].specs['test spec'].duration).toBeDefined(); + }); + + it('should update step timer duration on StepEnd event', function() { + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + + expect(model.value.children['describe 1'].specs['test spec'].steps[0].duration).toBeDefined(); + }); + + it('should set spec status on SpecEnd to success if no status set', function() { + runner.emit('SpecBegin', spec); + runner.emit('SpecEnd', spec); + + expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('success'); + }); + + it('should set status to error after SpecError', function() { + runner.emit('SpecBegin', spec); + runner.emit('SpecError', spec, 'error'); + + expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('error'); + }); + + it('should set spec status to failure if step fails', function() { + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepEnd', spec, step); + runner.emit('StepBegin', spec, step); + runner.emit('StepFailure', spec, step, 'error'); + runner.emit('StepEnd', spec, step); + runner.emit('StepBegin', spec, step); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + + expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('failure'); + }); + + it('should set spec status to error if step errors', function() { + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepError', spec, step, 'error'); + runner.emit('StepEnd', spec, step); + runner.emit('StepBegin', spec, step); + runner.emit('StepFailure', spec, step, 'error'); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + + expect(model.value.children['describe 1'].specs['test spec'].status).toEqual('error'); + }); +}); diff --git a/test/scenario/RunnerSpec.js b/test/scenario/RunnerSpec.js index 1641a8f1..059dd874 100644 --- a/test/scenario/RunnerSpec.js +++ b/test/scenario/RunnerSpec.js @@ -2,7 +2,7 @@ * Mock spec runner. */ function MockSpecRunner() {} -MockSpecRunner.prototype.run = function(ui, spec, specDone) { +MockSpecRunner.prototype.run = function(spec, specDone) { spec.before.call(this); spec.body.call(this); spec.after.call(this); @@ -41,6 +41,11 @@ describe('angular.scenario.Runner', function() { location: {} }; runner = new angular.scenario.Runner($window); + runner.createSpecRunner_ = function(scope) { + return scope.$new(MockSpecRunner); + }; + runner.on('SpecError', rethrow); + runner.on('StepError', rethrow); }); afterEach(function() { @@ -92,7 +97,7 @@ describe('angular.scenario.Runner', function() { expect($window.dslScope).toBeDefined(); }); }); - runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow); + runner.run(null/*application*/); }); it('should create a new scope for each DSL chain', function() { @@ -107,6 +112,6 @@ describe('angular.scenario.Runner', function() { expect(scope.chained).toEqual(2); }); }); - runner.run(null/*ui*/, null/*application*/, MockSpecRunner, rethrow); + runner.run(null/*application*/); }); }); diff --git a/test/scenario/SpecRunnerSpec.js b/test/scenario/SpecRunnerSpec.js index 921d6853..7947a3ca 100644 --- a/test/scenario/SpecRunnerSpec.js +++ b/test/scenario/SpecRunnerSpec.js @@ -1,40 +1,4 @@ /** - * Mock of all required UI classes/methods. (UI, Spec, Step). - */ -function UIMock() { - this.log = []; -} -UIMock.prototype = { - addSpec: function(spec) { - var log = this.log; - log.push('addSpec:' + spec.name); - return { - addStep: function(name) { - log.push('addStep:' + name); - return { - finish: function(e) { - log.push('step finish:' + (e ? e : '')); - return this; - }, - error: function(e) { - log.push('step error:' + (e ? e : '')); - return this; - } - }; - }, - finish: function(e) { - log.push('spec finish:' + (e ? e : '')); - return this; - }, - error: function(e) { - log.push('spec error:' + (e ? e : '')); - return this; - } - }; - } -}; - -/** * Mock Application */ function ApplicationMock($window) { @@ -47,7 +11,7 @@ ApplicationMock.prototype = { }; describe('angular.scenario.SpecRunner', function() { - var $window; + var $window, $root, log; var runner; function createSpec(name, body) { @@ -60,14 +24,22 @@ describe('angular.scenario.SpecRunner', function() { } beforeEach(function() { + log = []; $window = {}; $window.setTimeout = function(fn, timeout) { fn(); }; - runner = angular.scope(); - runner.application = new ApplicationMock($window); - runner.$window = $window; - runner.$become(angular.scenario.SpecRunner); + $root = angular.scope({ + emit: function(eventName) { + log.push(eventName); + }, + on: function(eventName) { + log.push('Listener Added for ' + eventName); + } + }); + $root.application = new ApplicationMock($window); + $root.$window = $window; + runner = $root.$new(angular.scenario.SpecRunner); }); it('should bind futures to the spec', function() { @@ -92,84 +64,82 @@ describe('angular.scenario.SpecRunner', function() { it('should execute spec function and notify UI', function() { var finished; - var ui = new UIMock(); var spec = createSpec('test spec', function() { this.test = 'some value'; }); runner.addFuture('test future', function(done) { done(); }); - runner.run(ui, spec, function() { + runner.run(spec, function() { finished = true; }); expect(runner.test).toEqual('some value'); expect(finished).toBeTruthy(); - expect(ui.log).toEqual([ - 'addSpec:test spec', - 'addStep:test future', - 'step finish:', - 'spec finish:' + expect(log).toEqual([ + 'SpecBegin', + 'StepBegin', + 'StepEnd', + 'SpecEnd' ]); }); it('should execute notify UI on spec setup error', function() { var finished; - var ui = new UIMock(); var spec = createSpec('test spec', function() { throw 'message'; }); - runner.run(ui, spec, function() { + runner.run(spec, function() { finished = true; }); expect(finished).toBeTruthy(); - expect(ui.log).toEqual([ - 'addSpec:test spec', - 'spec error:message' + expect(log).toEqual([ + 'SpecBegin', + 'SpecError', + 'SpecEnd' ]); }); it('should execute notify UI on step failure', function() { var finished; - var ui = new UIMock(); var spec = createSpec('test spec'); runner.addFuture('test future', function(done) { done('failure message'); }); - runner.run(ui, spec, function() { + runner.run(spec, function() { finished = true; }); expect(finished).toBeTruthy(); - expect(ui.log).toEqual([ - 'addSpec:test spec', - 'addStep:test future', - 'step finish:failure message', - 'spec finish:' + expect(log).toEqual([ + 'SpecBegin', + 'StepBegin', + 'StepFailure', + 'StepEnd', + 'SpecEnd' ]); }); it('should execute notify UI on step error', function() { var finished; - var ui = new UIMock(); var spec = createSpec('test spec', function() { this.addFuture('test future', function(done) { throw 'error message'; }); }); - runner.run(ui, spec, function() { + runner.run(spec, function() { finished = true; }); expect(finished).toBeTruthy(); - expect(ui.log).toEqual([ - 'addSpec:test spec', - 'addStep:test future', - 'step error:error message', - 'spec finish:' + expect(log).toEqual([ + 'SpecBegin', + 'StepBegin', + 'StepError', + 'StepEnd', + 'SpecEnd' ]); }); 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'; @@ -181,18 +151,19 @@ describe('angular.scenario.SpecRunner', function() { done(); }); }; - runner.run(ui, spec, function() { + runner.run(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:' + expect(log).toEqual([ + 'SpecBegin', + 'StepBegin', + 'StepError', + 'StepEnd', + 'StepBegin', + 'StepEnd', + 'SpecEnd' ]); }); diff --git a/test/scenario/dslSpec.js b/test/scenario/dslSpec.js index 14ca8b2c..bbba0b7d 100644 --- a/test/scenario/dslSpec.js +++ b/test/scenario/dslSpec.js @@ -1,41 +1,21 @@ -/** - * Very basic Mock of angular. - */ -function AngularMock() { - this.reset(); - this.service = this; -} - -AngularMock.prototype.reset = function() { - this.log = []; -}; - -AngularMock.prototype.$browser = function() { - this.log.push('$brower()'); - return this; -}; - -AngularMock.prototype.poll = function() { - this.log.push('$brower.poll()'); - return this; -}; - -AngularMock.prototype.notifyWhenNoOutstandingRequests = function(fn) { - this.log.push('$brower.notifyWhenNoOutstandingRequests()'); - fn(); -}; - describe("angular.scenario.dsl", function() { - var $window; - var $root; - var application; + var $window, $root; + var application, eventLog; beforeEach(function() { + eventLog = []; $window = { document: _jQuery("<div></div>"), - angular: new AngularMock() + angular: new angular.scenario.testing.MockAngular() }; - $root = angular.scope(); + $root = angular.scope({ + emit: function(eventName) { + eventLog.push(eventName); + }, + on: function(eventName) { + eventLog.push('Listener Added for ' + eventName); + } + }); $root.futures = []; $root.futureLog = []; $root.$window = $window; @@ -54,7 +34,7 @@ describe("angular.scenario.dsl", function() { }; }); $root.application = new angular.scenario.Application($window.document); - $root.application.getWindow = function() { + $root.application.getWindow_ = function() { return $window; }; $root.application.navigateTo = function(url, callback) { @@ -74,13 +54,14 @@ describe("angular.scenario.dsl", function() { expect($root.futureLog).toEqual([]); $window.resume(); expect($root.futureLog). - toEqual(['waiting for you to call resume() in the console']); + toEqual(['waiting for you to resume']); + expect(eventLog).toContain('InteractiveWait'); }); }); describe('Pause', function() { beforeEach(function() { - $root.setTimeout = function(fn, value) { + $root.$window.setTimeout = function(fn, value) { $root.timerValue = value; fn(); }; @@ -127,13 +108,6 @@ describe("angular.scenario.dsl", function() { expect($window.location).toEqual('http://myurl'); expect($root.futureResult).toEqual('http://myurl'); }); - - it('should wait for angular notify when no requests pending', function() { - $root.dsl.navigateTo('url'); - expect($window.angular.log).toContain('$brower.poll()'); - expect($window.angular.log). - toContain('$brower.notifyWhenNoOutstandingRequests()'); - }); }); describe('Element Finding', function() { @@ -199,6 +173,24 @@ describe("angular.scenario.dsl", function() { $root.dsl.element('a').click(); }); + it('should navigate page if click on anchor', function() { + expect($window.location).not.toEqual('#foo'); + doc.append('<a href="#foo"></a>'); + $root.dsl.element('a').click(); + expect($window.location).toEqual('#foo'); + }); + + it('should count matching elements', function() { + doc.append('<span></span><span></span>'); + $root.dsl.element('span').count(); + expect($root.futureResult).toEqual(2); + }); + + it('should return count of 0 if no matching elements', function() { + $root.dsl.element('span').count(); + expect($root.futureResult).toEqual(0); + }); + it('should get attribute', function() { doc.append('<div id="test" class="foo"></div>'); $root.dsl.element('#test').attr('class'); @@ -249,6 +241,12 @@ describe("angular.scenario.dsl", function() { expect($root.futureResult).toEqual(2); }); + it('should return 0 if repeater doesnt match', function() { + doc.find('ul').html(''); + chain.count(); + expect($root.futureResult).toEqual(0); + }); + it('should get a row of bindings', function() { chain.row(1); expect($root.futureResult).toEqual(['felisa', 'female']); diff --git a/test/scenario/mocks.js b/test/scenario/mocks.js new file mode 100644 index 00000000..5cd2f30a --- /dev/null +++ b/test/scenario/mocks.js @@ -0,0 +1,41 @@ +angular.scenario.testing = angular.scenario.testing || {}; + +angular.scenario.testing.MockAngular = function() { + this.reset(); + this.service = this; +}; + +angular.scenario.testing.MockAngular.prototype.reset = function() { + this.log = []; +}; + +angular.scenario.testing.MockAngular.prototype.$browser = function() { + this.log.push('$brower()'); + return this; +}; + +angular.scenario.testing.MockAngular.prototype.poll = function() { + this.log.push('$brower.poll()'); + return this; +}; + +angular.scenario.testing.MockAngular.prototype.notifyWhenNoOutstandingRequests = function(fn) { + this.log.push('$brower.notifyWhenNoOutstandingRequests()'); + fn(); +}; + +angular.scenario.testing.MockRunner = function() { + this.listeners = []; +}; + +angular.scenario.testing.MockRunner.prototype.on = function(eventName, fn) { + this.listeners[eventName] = this.listeners[eventName] || []; + this.listeners[eventName].push(fn); +}; + +angular.scenario.testing.MockRunner.prototype.emit = function(eventName) { + var args = Array.prototype.slice.call(arguments, 1); + angular.foreach(this.listeners[eventName] || [], function(fn) { + fn.apply(this, args); + }); +}; diff --git a/test/scenario/output/HtmlSpec.js b/test/scenario/output/HtmlSpec.js new file mode 100644 index 00000000..f5bb90b0 --- /dev/null +++ b/test/scenario/output/HtmlSpec.js @@ -0,0 +1,124 @@ +describe('angular.scenario.output.html', function() { + var runner, spec, listeners; + var ui, context; + + beforeEach(function() { + listeners = []; + spec = { + name: 'test spec', + definition: { + id: 10, + name: 'child', + children: [], + parent: { + id: 20, + name: 'parent', + children: [] + } + } + }; + step = { + name: 'some step', + line: function() { return 'unknown:-1'; }, + }; + runner = new angular.scenario.testing.MockRunner(); + context = _jQuery("<div></div>"); + ui = angular.scenario.output.html(context, runner); + }); + + it('should create nested describe context', function() { + runner.emit('SpecBegin', spec); + expect(context.find('#describe-20 #describe-10 > h2').text()). + toEqual('describe: child'); + expect(context.find('#describe-20 > h2').text()).toEqual('describe: parent'); + expect(context.find('#describe-10 .tests > li .test-info .test-name').text()). + toEqual('test spec'); + expect(context.find('#describe-10 .tests > li').hasClass('status-pending')). + toBeTruthy(); + }); + + it('should add link on InteractiveWait', function() { + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepEnd', spec, step); + runner.emit('StepBegin', spec, step); + runner.emit('InteractiveWait', spec, step); + expect(context.find('.test-actions .test-title:first').text()).toEqual('some step'); + expect(context.find('.test-actions .test-title:last').html()).toEqual( + 'waiting for you to <a href="javascript:resume()">resume</a>.' + ); + }); + + it('should update totals when steps complete', function() { + // Failure + for (var i=0; i < 3; ++i) { + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepFailure', spec, step, 'error'); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + } + + // Error + runner.emit('SpecBegin', spec); + runner.emit('SpecError', spec, 'error'); + runner.emit('SpecEnd', spec); + + // Error + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepError', spec, step, 'error'); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + + // Success + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + + 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(1); + }); + + it('should update timer when test completes', function() { + // Success + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + + // Failure + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepFailure', spec, step, 'error'); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + + // Error + runner.emit('SpecBegin', spec); + runner.emit('SpecError', spec, 'error'); + runner.emit('SpecEnd', spec); + + context.find('#describe-10 .tests > li .test-info .timer-result'). + each(function(index, timer) { + expect(timer.innerHTML).toMatch(/ms$/); + }); + }); + + it('should include line if provided', function() { + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepFailure', spec, step, 'error'); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + + var errorHtml = context.find('#describe-10 .tests li pre').html(); + expect(errorHtml.indexOf('unknown:-1')).toEqual(0); + }); + +}); diff --git a/test/scenario/output/jsonSpec.js b/test/scenario/output/jsonSpec.js new file mode 100644 index 00000000..b3592608 --- /dev/null +++ b/test/scenario/output/jsonSpec.js @@ -0,0 +1,34 @@ +describe('angular.scenario.output.json', function() { + var output, context; + var runner, $window; + var spec, step; + + beforeEach(function() { + $window = {}; + context = _jQuery('<div></div>'); + runner = new angular.scenario.testing.MockRunner(); + output = angular.scenario.output.json(context, runner); + spec = { + name: 'test spec', + definition: { + id: 10, + name: 'describe', + } + }; + step = { + name: 'some step', + line: function() { return 'unknown:-1'; }, + }; + }); + + it('should put json in context on RunnerEnd', function() { + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + runner.emit('RunnerEnd'); + + expect(angular.fromJson(context.html()).children['describe'] + .specs['test spec'].status).toEqual('success'); + }); +}); diff --git a/test/scenario/output/objectSpec.js b/test/scenario/output/objectSpec.js new file mode 100644 index 00000000..c4e8d451 --- /dev/null +++ b/test/scenario/output/objectSpec.js @@ -0,0 +1,37 @@ +describe('angular.scenario.output.object', function() { + var output; + var runner, $window; + var spec, step; + + beforeEach(function() { + $window = {}; + runner = new angular.scenario.testing.MockRunner(); + runner.$window = $window; + output = angular.scenario.output.object(null, runner); + spec = { + name: 'test spec', + definition: { + id: 10, + name: 'describe', + children: [] + } + }; + step = { + name: 'some step', + line: function() { return 'unknown:-1'; }, + }; + }); + + it('should create a global variable $result', function() { + expect($window.$result).toBeDefined(); + }); + + it('should maintain live state in $result', function() { + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepEnd', spec, step); + + expect($window.$result.children['describe'] + .specs['test spec'].steps[0].duration).toBeDefined(); + }); +}); diff --git a/test/scenario/output/xmlSpec.js b/test/scenario/output/xmlSpec.js new file mode 100644 index 00000000..448c8d10 --- /dev/null +++ b/test/scenario/output/xmlSpec.js @@ -0,0 +1,33 @@ +describe('angular.scenario.output.json', function() { + var output, context; + var runner, $window; + var spec, step; + + beforeEach(function() { + $window = {}; + context = _jQuery('<div></div>'); + runner = new angular.scenario.testing.MockRunner(); + output = angular.scenario.output.xml(context, runner); + spec = { + name: 'test spec', + definition: { + id: 10, + name: 'describe', + } + }; + step = { + name: 'some step', + line: function() { return 'unknown:-1'; }, + }; + }); + + it('should create XML nodes for object model', function() { + runner.emit('SpecBegin', spec); + runner.emit('StepBegin', spec, step); + runner.emit('StepEnd', spec, step); + runner.emit('SpecEnd', spec); + runner.emit('RunnerEnd'); + expect(_jQuery(context).find('it').attr('status')).toEqual('success'); + expect(_jQuery(context).find('it step').attr('status')).toEqual('success'); + }); +}); |
