split mocks and create $log and $exceptionHandler mocks

- split mocks between angular-mocks.js and mocks.js
- src/angular-mocks.js now contains only mocks that we want to ship
- test/mocks.js contains mocks that we use internally for testing
  angular
- created angular.mock namespace
- created public $exceptionHandler mock rethrows errors
- created public $log mock stores all logs messages in an array that can
  be accessed to make assertions
- internally we now have factory to create $exceptionHandler
  that we can assert on
- internally we also keep track of all messages logged and
  fail tests if messages were not expected and cleaned up (checked
  via global beforeEach and afterEach)
- updated RakeFile and docs reader.js to point to the new
  angular-mocks.js location
- made real $exceptionHandler and $log factories accessible from tests
  and simplified their specs
- fixed typos in several spec descriptions
- added log assertions throughout the test suite
This commit is contained in:
Igor Minar 2011-01-25 20:44:44 -08:00
parent 7a48ee6aa9
commit f5d08963b0
11 changed files with 193 additions and 40 deletions

View file

@ -174,7 +174,7 @@ task :package => [:clean, :compile, :docs] do
FileUtils.rm_r(path_to('pkg'), :force => true) FileUtils.rm_r(path_to('pkg'), :force => true)
FileUtils.mkdir_p(pkg_dir) FileUtils.mkdir_p(pkg_dir)
['test/angular-mocks.js', ['src/angular-mocks.js',
path_to('angular.js'), path_to('angular.js'),
path_to('angular.min.js'), path_to('angular.min.js'),
path_to('angular-ie-compat.js'), path_to('angular-ie-compat.js'),

View file

@ -13,7 +13,8 @@ load:
- src/scenario/Scenario.js - src/scenario/Scenario.js
- src/scenario/output/*.js - src/scenario/output/*.js
- src/scenario/*.js - src/scenario/*.js
- test/angular-mocks.js - src/angular-mocks.js
- test/mocks.js
- test/scenario/*.js - test/scenario/*.js
- test/scenario/output/*.js - test/scenario/output/*.js
- test/*.js - test/*.js

View file

@ -13,7 +13,8 @@ load:
- src/scenario/Scenario.js - src/scenario/Scenario.js
- src/scenario/output/*.js - src/scenario/output/*.js
- src/scenario/*.js - src/scenario/*.js
- test/angular-mocks.js - src/angular-mocks.js
- test/mocks.js
- test/scenario/*.js - test/scenario/*.js
- test/scenario/output/*.js - test/scenario/output/*.js
- test/*.js - test/*.js

View file

@ -13,7 +13,8 @@ load:
- src/scenario/Scenario.js - src/scenario/Scenario.js
- src/scenario/output/*.js - src/scenario/output/*.js
- src/scenario/*.js - src/scenario/*.js
- test/angular-mocks.js - src/angular-mocks.js
- test/mocks.js
- test/scenario/*.js - test/scenario/*.js
- test/scenario/output/*.js - test/scenario/output/*.js
- test/*.js - test/*.js

View file

@ -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() { function MockBrowser() {
var self = this, var self = this,
expectations = {}, expectations = {},
@ -189,6 +207,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. * Mock of the Date type which has its timezone specified via constroctor arg.
* *

View file

@ -290,10 +290,10 @@ angularServiceInject("$location", function($browser) {
* @requires $window * @requires $window
* *
* @description * @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). * 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 * @example
<p>Reload this page with open console, enter text and hit the log button...</p> <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.info(message)">info</button>
<button ng:click="$log.error(message)">error</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 { return {
/** /**
* @workInProgress * @workInProgress
@ -387,7 +388,8 @@ angularServiceInject("$log", function($window){
* *
* @example * @example
*/ */
angularServiceInject('$exceptionHandler', function($log){ var $exceptionHandlerFactory; //reference to be used only in tests
angularServiceInject('$exceptionHandler', $exceptionHandlerFactory = function($log){
return function(e) { return function(e) {
$log.error(e); $log.error(e);
}; };

View file

@ -273,6 +273,7 @@ describe('Binder', function(){
it('IfTextBindingThrowsErrorDecorateTheSpan', function(){ it('IfTextBindingThrowsErrorDecorateTheSpan', function(){
var a = this.compile('<div>{{error.throw()}}</div>'); var a = this.compile('<div>{{error.throw()}}</div>');
var doc = a.node; var doc = a.node;
var errorLogs = a.scope.$service('$log').error.logs;
a.scope.$set('error.throw', function(){throw "ErrorMsg1";}); a.scope.$set('error.throw', function(){throw "ErrorMsg1";});
a.scope.$eval(); a.scope.$eval();
@ -280,6 +281,7 @@ describe('Binder', function(){
assertTrue(span.hasClass('ng-exception')); assertTrue(span.hasClass('ng-exception'));
assertTrue(!!span.text().match(/ErrorMsg1/)); assertTrue(!!span.text().match(/ErrorMsg1/));
assertTrue(!!span.attr('ng-exception').match(/ErrorMsg1/)); assertTrue(!!span.attr('ng-exception').match(/ErrorMsg1/));
assertEquals(['ErrorMsg1'], errorLogs.shift());
a.scope.$set('error.throw', function(){throw "MyError";}); a.scope.$set('error.throw', function(){throw "MyError";});
a.scope.$eval(); a.scope.$eval();
@ -287,30 +289,34 @@ describe('Binder', function(){
assertTrue(span.hasClass('ng-exception')); assertTrue(span.hasClass('ng-exception'));
assertTrue(span.text(), span.text().match('MyError') !== null); assertTrue(span.text(), span.text().match('MyError') !== null);
assertEquals('MyError', span.attr('ng-exception')); assertEquals('MyError', span.attr('ng-exception'));
assertEquals(['MyError'], errorLogs.shift());
a.scope.$set('error.throw', function(){return "ok";}); a.scope.$set('error.throw', function(){return "ok";});
a.scope.$eval(); a.scope.$eval();
assertFalse(span.hasClass('ng-exception')); assertFalse(span.hasClass('ng-exception'));
assertEquals('ok', span.text()); assertEquals('ok', span.text());
assertEquals(null, span.attr('ng-exception')); assertEquals(null, span.attr('ng-exception'));
assertEquals(0, errorLogs.length);
}); });
it('IfAttrBindingThrowsErrorDecorateTheAttribute', function(){ it('IfAttrBindingThrowsErrorDecorateTheAttribute', function(){
var a = this.compile('<div attr="before {{error.throw()}} after"></div>'); var a = this.compile('<div attr="before {{error.throw()}} after"></div>');
var doc = a.node; var doc = a.node;
var errorLogs = a.scope.$service('$log').error.logs;
a.scope.$set('error.throw', function(){throw "ErrorMsg";}); a.scope.$set('error.throw', function(){throw "ErrorMsg";});
a.scope.$eval(); a.scope.$eval();
assertTrue('ng-exception', doc.hasClass('ng-exception')); assertTrue('ng-exception', doc.hasClass('ng-exception'));
assertEquals('"ErrorMsg"', doc.attr('ng-exception')); assertEquals('"ErrorMsg"', doc.attr('ng-exception'));
assertEquals('before "ErrorMsg" after', doc.attr('attr')); assertEquals('before "ErrorMsg" after', doc.attr('attr'));
assertEquals(['ErrorMsg'], errorLogs.shift());
a.scope.$set('error.throw', function(){ return 'X';}); a.scope.$set('error.throw', function(){ return 'X';});
a.scope.$eval(); a.scope.$eval();
assertFalse('!ng-exception', doc.hasClass('ng-exception')); assertFalse('!ng-exception', doc.hasClass('ng-exception'));
assertEquals('before X after', doc.attr('attr')); assertEquals('before X after', doc.attr('attr'));
assertEquals(null, doc.attr('ng-exception')); assertEquals(null, doc.attr('ng-exception'));
assertEquals(0, errorLogs.length);
}); });
it('NestedRepeater', function(){ it('NestedRepeater', function(){
@ -447,6 +453,7 @@ describe('Binder', function(){
var error = input.attr('ng-exception'); var error = input.attr('ng-exception');
assertTrue(!!error.match(/MyError/)); assertTrue(!!error.match(/MyError/));
assertTrue("should have an error class", input.hasClass('ng-exception')); 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 // TODO: I think that exception should never get cleared so this portion of test makes no sense
//c.scope.action = noop; //c.scope.action = noop;
@ -491,7 +498,7 @@ describe('Binder', function(){
it('FillInOptionValueWhenMissing', function(){ it('FillInOptionValueWhenMissing', function(){
var c = this.compile( 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('a', 'A');
c.scope.$set('b', 'B'); c.scope.$set('b', 'B');
c.scope.$eval(); c.scope.$eval();
@ -569,18 +576,21 @@ describe('Binder', function(){
assertChild(5, false); assertChild(5, false);
}); });
it('ItShouldDisplayErrorWhenActionIsSyntacticlyIncorect', function(){ it('ItShouldDisplayErrorWhenActionIsSyntacticlyIncorrect', function(){
var c = this.compile('<div>' + var c = this.compile('<div>' +
'<input type="button" ng:click="greeting=\'ABC\'"/>' + '<input type="button" ng:click="greeting=\'ABC\'"/>' +
'<input type="button" ng:click=":garbage:"/></div>'); '<input type="button" ng:click=":garbage:"/></div>');
var first = jqLite(c.node[0].childNodes[0]); var first = jqLite(c.node[0].childNodes[0]);
var second = jqLite(c.node[0].childNodes[1]); var second = jqLite(c.node[0].childNodes[1]);
var errorLogs = c.scope.$service('$log').error.logs;
browserTrigger(first, 'click'); browserTrigger(first, 'click');
assertEquals("ABC", c.scope.greeting); assertEquals("ABC", c.scope.greeting);
expect(errorLogs).toEqual([]);
browserTrigger(second, 'click'); browserTrigger(second, 'click');
assertTrue(second.hasClass("ng-exception")); assertTrue(second.hasClass("ng-exception"));
expect(errorLogs.shift()[0]).toMatchError(/Parse Error: Token ':' not a primary expression/);
}); });
it('ItShouldSelectTheCorrectRadioBox', function(){ it('ItShouldSelectTheCorrectRadioBox', function(){

View file

@ -146,30 +146,34 @@ describe('scope/model', function(){
}); });
describe('$tryEval', function(){ describe('$tryEval', function(){
it('should report error on element', function(){ it('should report error using the provided error handler and $log.error', function(){
var scope = createScope(); var scope = createScope(),
errorLogs = scope.$service('$log').error.logs;
scope.$tryEval(function(){throw "myError";}, function(error){ scope.$tryEval(function(){throw "myError";}, function(error){
scope.error = error; scope.error = error;
}); });
expect(scope.error).toEqual('myError'); expect(scope.error).toEqual('myError');
expect(errorLogs.shift()[0]).toBe("myError");
}); });
it('should report error on visible element', function(){ it('should report error on visible element', function(){
var element = jqLite('<div></div>'); var element = jqLite('<div></div>'),
var scope = createScope(); scope = createScope(),
errorLogs = scope.$service('$log').error.logs;
scope.$tryEval(function(){throw "myError";}, element); scope.$tryEval(function(){throw "myError";}, element);
expect(element.attr('ng-exception')).toEqual('myError'); expect(element.attr('ng-exception')).toEqual('myError');
expect(element.hasClass('ng-exception')).toBeTruthy(); expect(element.hasClass('ng-exception')).toBeTruthy();
expect(errorLogs.shift()[0]).toBe("myError");
}); });
it('should report error on $excetionHandler', function(){ it('should report error on $excetionHandler', function(){
var errors = [], var scope = createScope(null, {$exceptionHandler: $exceptionHandlerMockFactory},
errorLogs = [], {$log: $logMock});
scope = createScope(null, {}, {$exceptionHandler: function(e) {errors.push(e)},
$log: {error: function(e) {errorLogs.push(e)}}});
scope.$tryEval(function(){throw "myError";}); scope.$tryEval(function(){throw "myError";});
expect(errors).toEqual(["myError"]); expect(scope.$service('$exceptionHandler').errors.shift()).toEqual("myError");
expect(errorLogs).toEqual(["myError"]); expect(scope.$service('$log').error.logs.shift()).toEqual(["myError"]);
}); });
}); });

View file

@ -31,8 +31,14 @@ describe("service", function(){
function warn(){ logger+= 'warn;'; } function warn(){ logger+= 'warn;'; }
function info(){ logger+= 'info;'; } function info(){ logger+= 'info;'; }
function error(){ logger+= 'error;'; } 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 = scope.$service('$log');
$log.log(); $log.log();
$log.warn(); $log.warn();
$log.info(); $log.info();
@ -40,10 +46,12 @@ describe("service", function(){
expect(logger).toEqual('log;warn;info;error;'); 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 = ""; var logger = "";
function log(){ logger+= 'log;'; } 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'); var $log = scope.$service('$log');
$log.log(); $log.log();
$log.warn(); $log.warn();
@ -53,7 +61,9 @@ describe("service", function(){
}); });
it('should use noop if no console', 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 = scope.$service('$log');
$log.log(); $log.log();
$log.warn(); $log.warn();
@ -61,8 +71,8 @@ describe("service", function(){
$log.error(); $log.error();
}); });
describe('Error', function(){ describe('error', function(){
var e, $log, $console, errorArgs; var e, $log, errorArgs;
beforeEach(function(){ beforeEach(function(){
e = new Error(''); e = new Error('');
e.message = undefined; e.message = undefined;
@ -70,19 +80,19 @@ describe("service", function(){
e.line = undefined; e.line = undefined;
e.stack = undefined; e.stack = undefined;
$console = angular.service('$log')({console:{error:function(){ $log = $logFactory({console:{error:function(){
errorArgs = arguments; errorArgs = arguments;
}}}); }}});
}); });
it('should pass error if does not have trace', function(){ it('should pass error if does not have trace', function(){
$console.error('abc', e); $log.error('abc', e);
expect(errorArgs).toEqual(['abc', e]); expect(errorArgs).toEqual(['abc', e]);
}); });
it('should print stack', function(){ it('should print stack', function(){
e.stack = 'stack'; e.stack = 'stack';
$console.error('abc', e); $log.error('abc', e);
expect(errorArgs).toEqual(['abc', 'stack']); expect(errorArgs).toEqual(['abc', 'stack']);
}); });
@ -90,7 +100,7 @@ describe("service", function(){
e.message = 'message'; e.message = 'message';
e.sourceURL = 'sourceURL'; e.sourceURL = 'sourceURL';
e.line = '123'; e.line = '123';
$console.error('abc', e); $log.error('abc', e);
expect(errorArgs).toEqual(['abc', 'message\nsourceURL:123']); expect(errorArgs).toEqual(['abc', 'message\nsourceURL:123']);
}); });
@ -100,10 +110,13 @@ describe("service", function(){
describe("$exceptionHandler", function(){ describe("$exceptionHandler", function(){
it('should log errors', function(){ it('should log errors', function(){
var error = ''; var scope = createScope({}, {$exceptionHandler: $exceptionHandlerFactory},
$log.error = function(m) { error += m; }; {$log: $logMock}),
scope.$service('$exceptionHandler')('myError'); $log = scope.$service('$log'),
expect(error).toEqual('myError'); $exceptionHandler = scope.$service('$exceptionHandler');
$exceptionHandler('myError');
expect($log.error.logs.shift()).toEqual(['myError']);
}); });
}); });

View file

@ -59,11 +59,59 @@ beforeEach(function(){
return this.actual.hasClass ? return this.actual.hasClass ?
this.actual.hasClass(clazz) : this.actual.hasClass(clazz) :
jqLite(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(){ function clearJqCache(){
var count = 0; var count = 0;

View file

@ -546,12 +546,16 @@ describe("widget", function(){
it('should report error on assignment error', function(){ it('should report error on assignment error', function(){
compile('<input type="text" name="throw \'\'" value="x"/>'); compile('<input type="text" name="throw \'\'" value="x"/>');
expect(element.hasClass('ng-exception')).toBeTruthy(); 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(){ it('should report error on ng:change exception', function(){
compile('<button ng:change="a-2=x">click</button>'); compile('<button ng:change="a-2=x">click</button>');
browserTrigger(element); browserTrigger(element);
expect(element.hasClass('ng-exception')).toBeTruthy(); 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(){ it('should error on wrong parsing of ng:repeat', function(){
var scope = compile('<ul><li ng:repeat="i dont parse"></li></ul>'); var scope = compile('<ul><li ng:repeat="i dont parse"></li></ul>');
var log = "";
log += element.attr('ng-exception') + ';'; expect(scope.$service('$log').error.logs.shift()[0]).
log += element.hasClass('ng-exception') + ';'; toEqualError("Expected ng:repeat in form of 'item in collection' but got 'i dont parse'.");
expect(log).toMatch(/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() { it('should expose iterator offset as $index when iterating over arrays', function() {