diff options
| -rw-r--r-- | Rakefile | 2 | ||||
| -rw-r--r-- | jsTestDriver-coverage.conf | 3 | ||||
| -rw-r--r-- | jsTestDriver-jquery.conf | 3 | ||||
| -rw-r--r-- | jsTestDriver.conf | 3 | ||||
| -rw-r--r-- | src/angular-mocks.js (renamed from test/angular-mocks.js) | 64 | ||||
| -rw-r--r-- | src/services.js | 10 | ||||
| -rw-r--r-- | test/BinderSpec.js | 16 | ||||
| -rw-r--r-- | test/ScopeSpec.js | 24 | ||||
| -rw-r--r-- | test/servicesSpec.js | 41 | ||||
| -rw-r--r-- | test/testabilityPatch.js | 50 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 17 |
11 files changed, 193 insertions, 40 deletions
@@ -174,7 +174,7 @@ task :package => [:clean, :compile, :docs] do FileUtils.rm_r(path_to('pkg'), :force => true) FileUtils.mkdir_p(pkg_dir) - ['test/angular-mocks.js', + ['src/angular-mocks.js', path_to('angular.js'), path_to('angular.min.js'), path_to('angular-ie-compat.js'), diff --git a/jsTestDriver-coverage.conf b/jsTestDriver-coverage.conf index 616a9cad..21ee8a2e 100644 --- a/jsTestDriver-coverage.conf +++ b/jsTestDriver-coverage.conf @@ -13,7 +13,8 @@ load: - src/scenario/Scenario.js - src/scenario/output/*.js - src/scenario/*.js - - test/angular-mocks.js + - src/angular-mocks.js + - test/mocks.js - test/scenario/*.js - test/scenario/output/*.js - test/*.js diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf index 96d38ecb..217a354a 100644 --- a/jsTestDriver-jquery.conf +++ b/jsTestDriver-jquery.conf @@ -13,7 +13,8 @@ load: - src/scenario/Scenario.js - src/scenario/output/*.js - src/scenario/*.js - - test/angular-mocks.js + - src/angular-mocks.js + - test/mocks.js - test/scenario/*.js - test/scenario/output/*.js - test/*.js diff --git a/jsTestDriver.conf b/jsTestDriver.conf index c8ced595..8bb59ef1 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -13,7 +13,8 @@ load: - src/scenario/Scenario.js - src/scenario/output/*.js - src/scenario/*.js - - test/angular-mocks.js + - src/angular-mocks.js + - test/mocks.js - test/scenario/*.js - test/scenario/output/*.js - test/*.js diff --git a/test/angular-mocks.js b/src/angular-mocks.js index e601d9ab..b9e1dd5c 100644 --- a/test/angular-mocks.js +++ b/src/angular-mocks.js @@ -56,6 +56,24 @@ */ +/** + * @ngdoc overview + * @name angular.mock + * @namespace Namespace for all built-in angular mocks. + * + * @description + * `angular.mock` is a namespace for all built-in mocks that ship with angular and automatically + * replace real services if `angular-mocks.js` file is loaded after `angular.js` and before any + * tests. + */ +angular.mock = {}; + + +/** + * @workInProgress + * @ngdoc service + * @name angular.mock.service.$browser + */ function MockBrowser() { var self = this, expectations = {}, @@ -190,6 +208,52 @@ angular.service('$browser', function(){ /** + * @workInProgress + * @ngdoc service + * @name angular.mock.service.$exceptionHandler + * + * @description + * Mock implementation of {@link angular.service.$exceptionHandler} that rethrows any error passed + * into `$exceptionHandler`. If any errors are are passed into the handler in tests, it typically + * means that there is a bug in the application or test, so this mock will make these tests fail. + * + * See {@link angular.mock} for more info on angular mocks. + */ +angular.service('$exceptionHandler', function(e) { + return function(e) {throw e;}; +}); + + +/** + * @workInProgress + * @ngdoc service + * @name angular.mock.service.$log + * + * @description + * Mock implementation of {@link angular.service.$log} that gathers all logged messages in arrays + * (one array per logging level). These arrays are exposed as `logs` property of each of the + * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. + * + * See {@link angular.mock} for more info on angular mocks. + */ +angular.service('$log', function() { + var $log = { + log: function log(){ log.logs.push(arguments) }, + warn: function warn(){ warn.logs.push(arguments) }, + info: function info(){ info.logs.push(arguments) }, + error: function error(){ error.logs.push(arguments) } + }; + + $log.log.logs = []; + $log.warn.logs = []; + $log.info.logs = []; + $log.error.logs = []; + + return $log; +}); + + +/** * Mock of the Date type which has its timezone specified via constroctor arg. * * The main purpose is to create Date-like instances with timezone fixed to the specified timezone diff --git a/src/services.js b/src/services.js index aa4a2090..7c4e7dfd 100644 --- a/src/services.js +++ b/src/services.js @@ -290,10 +290,10 @@ angularServiceInject("$location", function($browser) { * @requires $window * * @description - * Is simple service for logging. Default implementation writes the message + * Simple service for logging. Default implementation writes the message * into the browser's console (if present). * - * This is useful for debugging. + * The main purpose of this service is to simplify debugging and troubleshooting. * * @example <p>Reload this page with open console, enter text and hit the log button...</p> @@ -304,7 +304,8 @@ angularServiceInject("$location", function($browser) { <button ng:click="$log.info(message)">info</button> <button ng:click="$log.error(message)">error</button> */ -angularServiceInject("$log", function($window){ +var $logFactory; //reference to be used only in tests +angularServiceInject("$log", $logFactory = function($window){ return { /** * @workInProgress @@ -387,7 +388,8 @@ angularServiceInject("$log", function($window){ * * @example */ -angularServiceInject('$exceptionHandler', function($log){ +var $exceptionHandlerFactory; //reference to be used only in tests +angularServiceInject('$exceptionHandler', $exceptionHandlerFactory = function($log){ return function(e) { $log.error(e); }; diff --git a/test/BinderSpec.js b/test/BinderSpec.js index f12c1a74..73650bd6 100644 --- a/test/BinderSpec.js +++ b/test/BinderSpec.js @@ -273,6 +273,7 @@ describe('Binder', function(){ it('IfTextBindingThrowsErrorDecorateTheSpan', function(){ var a = this.compile('<div>{{error.throw()}}</div>'); var doc = a.node; + var errorLogs = a.scope.$service('$log').error.logs; a.scope.$set('error.throw', function(){throw "ErrorMsg1";}); a.scope.$eval(); @@ -280,6 +281,7 @@ describe('Binder', function(){ assertTrue(span.hasClass('ng-exception')); assertTrue(!!span.text().match(/ErrorMsg1/)); assertTrue(!!span.attr('ng-exception').match(/ErrorMsg1/)); + assertEquals(['ErrorMsg1'], errorLogs.shift()); a.scope.$set('error.throw', function(){throw "MyError";}); a.scope.$eval(); @@ -287,30 +289,34 @@ describe('Binder', function(){ assertTrue(span.hasClass('ng-exception')); assertTrue(span.text(), span.text().match('MyError') !== null); assertEquals('MyError', span.attr('ng-exception')); + assertEquals(['MyError'], errorLogs.shift()); a.scope.$set('error.throw', function(){return "ok";}); a.scope.$eval(); assertFalse(span.hasClass('ng-exception')); assertEquals('ok', span.text()); assertEquals(null, span.attr('ng-exception')); + assertEquals(0, errorLogs.length); }); it('IfAttrBindingThrowsErrorDecorateTheAttribute', function(){ var a = this.compile('<div attr="before {{error.throw()}} after"></div>'); var doc = a.node; + var errorLogs = a.scope.$service('$log').error.logs; a.scope.$set('error.throw', function(){throw "ErrorMsg";}); a.scope.$eval(); assertTrue('ng-exception', doc.hasClass('ng-exception')); assertEquals('"ErrorMsg"', doc.attr('ng-exception')); assertEquals('before "ErrorMsg" after', doc.attr('attr')); + assertEquals(['ErrorMsg'], errorLogs.shift()); a.scope.$set('error.throw', function(){ return 'X';}); a.scope.$eval(); assertFalse('!ng-exception', doc.hasClass('ng-exception')); assertEquals('before X after', doc.attr('attr')); assertEquals(null, doc.attr('ng-exception')); - + assertEquals(0, errorLogs.length); }); it('NestedRepeater', function(){ @@ -447,6 +453,7 @@ describe('Binder', function(){ var error = input.attr('ng-exception'); assertTrue(!!error.match(/MyError/)); assertTrue("should have an error class", input.hasClass('ng-exception')); + assertTrue(!!c.scope.$service('$log').error.logs.shift()[0].message.match(/MyError/)); // TODO: I think that exception should never get cleared so this portion of test makes no sense //c.scope.action = noop; @@ -491,7 +498,7 @@ describe('Binder', function(){ it('FillInOptionValueWhenMissing', function(){ var c = this.compile( - '<select><option selected="true">{{a}}</option><option value="">{{b}}</option><option>C</option></select>'); + '<select name="foo"><option selected="true">{{a}}</option><option value="">{{b}}</option><option>C</option></select>'); c.scope.$set('a', 'A'); c.scope.$set('b', 'B'); c.scope.$eval(); @@ -569,18 +576,21 @@ describe('Binder', function(){ assertChild(5, false); }); - it('ItShouldDisplayErrorWhenActionIsSyntacticlyIncorect', function(){ + it('ItShouldDisplayErrorWhenActionIsSyntacticlyIncorrect', function(){ var c = this.compile('<div>' + '<input type="button" ng:click="greeting=\'ABC\'"/>' + '<input type="button" ng:click=":garbage:"/></div>'); var first = jqLite(c.node[0].childNodes[0]); var second = jqLite(c.node[0].childNodes[1]); + var errorLogs = c.scope.$service('$log').error.logs; browserTrigger(first, 'click'); assertEquals("ABC", c.scope.greeting); + expect(errorLogs).toEqual([]); browserTrigger(second, 'click'); assertTrue(second.hasClass("ng-exception")); + expect(errorLogs.shift()[0]).toMatchError(/Parse Error: Token ':' not a primary expression/); }); it('ItShouldSelectTheCorrectRadioBox', function(){ diff --git a/test/ScopeSpec.js b/test/ScopeSpec.js index 8984f605..ab1ba124 100644 --- a/test/ScopeSpec.js +++ b/test/ScopeSpec.js @@ -146,30 +146,34 @@ describe('scope/model', function(){ }); describe('$tryEval', function(){ - it('should report error on element', function(){ - var scope = createScope(); + it('should report error using the provided error handler and $log.error', function(){ + var scope = createScope(), + errorLogs = scope.$service('$log').error.logs; + scope.$tryEval(function(){throw "myError";}, function(error){ scope.error = error; }); expect(scope.error).toEqual('myError'); + expect(errorLogs.shift()[0]).toBe("myError"); }); it('should report error on visible element', function(){ - var element = jqLite('<div></div>'); - var scope = createScope(); + var element = jqLite('<div></div>'), + scope = createScope(), + errorLogs = scope.$service('$log').error.logs; + scope.$tryEval(function(){throw "myError";}, element); expect(element.attr('ng-exception')).toEqual('myError'); expect(element.hasClass('ng-exception')).toBeTruthy(); + expect(errorLogs.shift()[0]).toBe("myError"); }); it('should report error on $excetionHandler', function(){ - var errors = [], - errorLogs = [], - scope = createScope(null, {}, {$exceptionHandler: function(e) {errors.push(e)}, - $log: {error: function(e) {errorLogs.push(e)}}}); + var scope = createScope(null, {$exceptionHandler: $exceptionHandlerMockFactory}, + {$log: $logMock}); scope.$tryEval(function(){throw "myError";}); - expect(errors).toEqual(["myError"]); - expect(errorLogs).toEqual(["myError"]); + expect(scope.$service('$exceptionHandler').errors.shift()).toEqual("myError"); + expect(scope.$service('$log').error.logs.shift()).toEqual(["myError"]); }); }); diff --git a/test/servicesSpec.js b/test/servicesSpec.js index e9b16621..c3c10552 100644 --- a/test/servicesSpec.js +++ b/test/servicesSpec.js @@ -31,8 +31,14 @@ describe("service", function(){ function warn(){ logger+= 'warn;'; } function info(){ logger+= 'info;'; } function error(){ logger+= 'error;'; } - var scope = createScope({}, angularService, {$window: {console:{log:log, warn:warn, info:info, error:error}}, $document:[{cookie:''}]}), + var scope = createScope({}, {$log: $logFactory}, + {$exceptionHandler: rethrow, + $window: {console: {log: log, + warn: warn, + info: info, + error: error}}}), $log = scope.$service('$log'); + $log.log(); $log.warn(); $log.info(); @@ -40,10 +46,12 @@ describe("service", function(){ expect(logger).toEqual('log;warn;info;error;'); }); - it('should use console.log if other not present', function(){ + it('should use console.log() if other not present', function(){ var logger = ""; function log(){ logger+= 'log;'; } - var scope = createScope({}, angularService, {$window: {console:{log:log}}, $document:[{cookie:''}]}); + var scope = createScope({}, {$log: $logFactory}, + {$window: {console:{log:log}}, + $exceptionHandler: rethrow}); var $log = scope.$service('$log'); $log.log(); $log.warn(); @@ -53,7 +61,9 @@ describe("service", function(){ }); it('should use noop if no console', function(){ - var scope = createScope({}, angularService, {$window: {}, $document:[{cookie:''}]}), + var scope = createScope({}, {$log: $logFactory}, + {$window: {}, + $exceptionHandler: rethrow}), $log = scope.$service('$log'); $log.log(); $log.warn(); @@ -61,8 +71,8 @@ describe("service", function(){ $log.error(); }); - describe('Error', function(){ - var e, $log, $console, errorArgs; + describe('error', function(){ + var e, $log, errorArgs; beforeEach(function(){ e = new Error(''); e.message = undefined; @@ -70,19 +80,19 @@ describe("service", function(){ e.line = undefined; e.stack = undefined; - $console = angular.service('$log')({console:{error:function(){ + $log = $logFactory({console:{error:function(){ errorArgs = arguments; }}}); }); it('should pass error if does not have trace', function(){ - $console.error('abc', e); + $log.error('abc', e); expect(errorArgs).toEqual(['abc', e]); }); it('should print stack', function(){ e.stack = 'stack'; - $console.error('abc', e); + $log.error('abc', e); expect(errorArgs).toEqual(['abc', 'stack']); }); @@ -90,7 +100,7 @@ describe("service", function(){ e.message = 'message'; e.sourceURL = 'sourceURL'; e.line = '123'; - $console.error('abc', e); + $log.error('abc', e); expect(errorArgs).toEqual(['abc', 'message\nsourceURL:123']); }); @@ -100,10 +110,13 @@ describe("service", function(){ describe("$exceptionHandler", function(){ it('should log errors', function(){ - var error = ''; - $log.error = function(m) { error += m; }; - scope.$service('$exceptionHandler')('myError'); - expect(error).toEqual('myError'); + var scope = createScope({}, {$exceptionHandler: $exceptionHandlerFactory}, + {$log: $logMock}), + $log = scope.$service('$log'), + $exceptionHandler = scope.$service('$exceptionHandler'); + + $exceptionHandler('myError'); + expect($log.error.logs.shift()).toEqual(['myError']); }); }); diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index 7029b213..78cb767f 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -59,11 +59,59 @@ beforeEach(function(){ return this.actual.hasClass ? this.actual.hasClass(clazz) : jqLite(this.actual).hasClass(clazz); + }, + + toEqualError: function(message) { + this.message = function() { + var expected; + if (this.actual.message && this.actual.name == 'Error') { + expected = toJson(this.actual.message); + } else { + expected = toJson(this.actual); + } + return "Expected " + expected + " to be an Error with message " + toJson(message); + } + return this.actual.name == 'Error' && this.actual.message == message; + }, + + toMatchError: function(messageRegexp) { + this.message = function() { + var expected; + if (this.actual.message && this.actual.name == 'Error') { + expected = toJson(this.actual.message); + } else { + expected = toJson(this.actual); + } + return "Expected " + expected + " to match an Error with message " + toJson(messageRegexp); + } + return this.actual.name == 'Error' && messageRegexp.test(this.actual.message); } }); + + $logMock.log.logs = []; + $logMock.warn.logs = []; + $logMock.info.logs = []; + $logMock.error.logs = []; }); -afterEach(clearJqCache); +afterEach(function() { + // check $log mock + forEach(['error', 'warn', 'info', 'log'], function(logLevel) { + if ($logMock[logLevel].logs.length) { + forEach($logMock[logLevel].logs, function(log) { + forEach(log, function deleteStack(logItem) { + if (logItem instanceof Error) delete logItem.stack; + }); + }); + + throw new Error("Exprected $log." + logLevel + ".logs array to be empty. " + + "Either a message was logged unexpectedly, or an expected log message was not checked " + + "and removed. Array contents: " + toJson($logMock[logLevel].logs)); + } + }); + + clearJqCache(); +}); function clearJqCache(){ var count = 0; diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index 2812614f..ee339e89 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -546,12 +546,16 @@ describe("widget", function(){ it('should report error on assignment error', function(){ compile('<input type="text" name="throw \'\'" value="x"/>'); expect(element.hasClass('ng-exception')).toBeTruthy(); + expect(scope.$service('$log').error.logs.shift()[0]). + toMatchError(/Parse Error: Token '''' is extra token not part of expression/); }); it('should report error on ng:change exception', function(){ compile('<button ng:change="a-2=x">click</button>'); browserTrigger(element); expect(element.hasClass('ng-exception')).toBeTruthy(); + expect(scope.$service('$log').error.logs.shift()[0]). + toMatchError(/Parse Error: Token '=' implies assignment but \[a-2\] can not be assigned to/); }); }); @@ -750,10 +754,15 @@ describe("widget", function(){ it('should error on wrong parsing of ng:repeat', function(){ var scope = compile('<ul><li ng:repeat="i dont parse"></li></ul>'); - var log = ""; - log += element.attr('ng-exception') + ';'; - log += element.hasClass('ng-exception') + ';'; - expect(log).toMatch(/Expected ng:repeat in form of 'item in collection' but got 'i dont parse'./); + + expect(scope.$service('$log').error.logs.shift()[0]). + toEqualError("Expected ng:repeat in form of 'item in collection' but got 'i dont parse'."); + + expect(scope.$element.attr('ng-exception')). + toMatch(/Expected ng:repeat in form of 'item in collection' but got 'i dont parse'/); + expect(scope.$element).toHaveClass('ng-exception'); + + dealoc(scope); }); it('should expose iterator offset as $index when iterating over arrays', function() { |
