aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Rakefile2
-rw-r--r--jsTestDriver-coverage.conf3
-rw-r--r--jsTestDriver-jquery.conf3
-rw-r--r--jsTestDriver.conf3
-rw-r--r--src/angular-mocks.js (renamed from test/angular-mocks.js)64
-rw-r--r--src/services.js10
-rw-r--r--test/BinderSpec.js16
-rw-r--r--test/ScopeSpec.js24
-rw-r--r--test/servicesSpec.js41
-rw-r--r--test/testabilityPatch.js50
-rw-r--r--test/widgetsSpec.js17
11 files changed, 193 insertions, 40 deletions
diff --git a/Rakefile b/Rakefile
index 7b46a85b..15901f46 100644
--- a/Rakefile
+++ b/Rakefile
@@ -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() {