diff options
| author | Igor Minar | 2011-01-25 20:44:44 -0800 | 
|---|---|---|
| committer | Igor Minar | 2011-01-26 15:46:05 -0800 | 
| commit | f5d08963b0c836b10133a94d03a81254242661eb (patch) | |
| tree | 14c525d0aa22193e5b1a1f8aa791b96608dc83b2 /test | |
| parent | 7a48ee6aa949e8d4338033de8a0c6a079ceb17a3 (diff) | |
| download | angular.js-f5d08963b0c836b10133a94d03a81254242661eb.tar.bz2 | |
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
Diffstat (limited to 'test')
| -rw-r--r-- | test/BinderSpec.js | 16 | ||||
| -rw-r--r-- | test/ScopeSpec.js | 24 | ||||
| -rw-r--r-- | test/angular-mocks.js | 320 | ||||
| -rw-r--r-- | test/servicesSpec.js | 41 | ||||
| -rw-r--r-- | test/testabilityPatch.js | 50 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 17 | 
6 files changed, 116 insertions, 352 deletions
| 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/angular-mocks.js b/test/angular-mocks.js deleted file mode 100644 index e601d9ab..00000000 --- a/test/angular-mocks.js +++ /dev/null @@ -1,320 +0,0 @@ -/** - * The MIT License - * - * Copyright (c) 2010 Adam Abrons and Misko Hevery http://getangular.com - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - - -/* - - NUGGGGGH MUST TONGUE WANGS -                           \ -                                ..... -                               C C  / -                              /<   / -               ___ __________/_#__=o -              /(- /(\_\________   \ -              \ ) \ )_      \o     \ -              /|\ /|\       |'     | -                            |     _| -                            /o   __\ -                           / '     | -                          / /      | -                         /_/\______| -                        (   _(    < -                         \    \    \ -                          \    \    | -                           \____\____\ -                           ____\_\__\_\ -                         /`   /`     o\ -                         |___ |_______|.. . b'ger - - - IN THE FINAL BUILD THIS FILE DOESN'T HAVE DIRECT ACCESS TO GLOBAL FUNCTIONS - DEFINED IN Angular.js YOU *MUST* REFER TO THEM VIA angular OBJECT - (e.g. angular.forEach(...)) AND MAKE SURE THAT THE GIVEN FUNCTION IS EXPORTED - TO THE angular NAMESPACE in AngularPublic.js - - */ - - -function MockBrowser() { -  var self = this, -      expectations = {}, -      requests = []; - -  this.isMock = true; -  self.url = "http://server"; -  self.lastUrl = self.url; // used by url polling fn -  self.pollFns = []; - - -  // register url polling fn - -  self.onHashChange = function(listener) { -    self.pollFns.push( -      function() { -        if (self.lastUrl != self.url) { -          listener(); -        } -      } -    ); - -    return listener; -  }; - - -  self.xhr = function(method, url, data, callback) { -    if (angular.isFunction(data)) { -      callback = data; -      data = null; -    } -    if (data && angular.isObject(data)) data = angular.toJson(data); -    if (data && angular.isString(data)) url += "|" + data; -    var expect = expectations[method] || {}; -    var response = expect[url]; -    if (!response) { -      throw { -        message: "Unexpected request for method '" + method + "' and url '" + url + "'.", -        name: "Unexpected Request" -      }; -    } -    requests.push(function(){ -      callback(response.code, response.response); -    }); -  }; -  self.xhr.expectations = expectations; -  self.xhr.requests = requests; -  self.xhr.expect = function(method, url, data) { -    if (data && angular.isObject(data)) data = angular.toJson(data); -    if (data && angular.isString(data)) url += "|" + data; -    var expect = expectations[method] || (expectations[method] = {}); -    return { -      respond: function(code, response) { -        if (!angular.isNumber(code)) { -          response = code; -          code = 200; -        } -        expect[url] = {code:code, response:response}; -      } -    }; -  }; -  self.xhr.expectGET    = angular.bind(self, self.xhr.expect, 'GET'); -  self.xhr.expectPOST   = angular.bind(self, self.xhr.expect, 'POST'); -  self.xhr.expectDELETE = angular.bind(self, self.xhr.expect, 'DELETE'); -  self.xhr.expectPUT    = angular.bind(self, self.xhr.expect, 'PUT'); -  self.xhr.expectJSON   = angular.bind(self, self.xhr.expect, 'JSON'); -  self.xhr.flush = function() { -    while(requests.length) { -      requests.pop()(); -    } -  }; - -  self.cookieHash = {}; -  self.lastCookieHash = {}; -  self.deferredFns = []; - -  self.defer = function(fn) { -    self.deferredFns.push(fn); -  }; - -  self.defer.flush = function() { -    while (self.deferredFns.length) self.deferredFns.shift()(); -  }; -} -MockBrowser.prototype = { - -  poll: function poll(){ -    angular.forEach(this.pollFns, function(pollFn){ -      pollFn(); -    }); -  }, - -  addPollFn: function(pollFn) { -    this.pollFns.push(pollFn); -    return pollFn; -  }, - -  hover: function(onHover) { -  }, - -  getUrl: function(){ -    return this.url; -  }, - -  setUrl: function(url){ -    this.url = url; -  }, - -  cookies:  function(name, value) { -    if (name) { -      if (value == undefined) { -        delete this.cookieHash[name]; -      } else { -        if (angular.isString(value) &&       //strings only -            value.length <= 4096) {          //strict cookie storage limits -          this.cookieHash[name] = value; -        } -      } -    } else { -      if (!angular.equals(this.cookieHash, this.lastCookieHash)) { -        this.lastCookieHash = angular.copy(this.cookieHash); -        this.cookieHash = angular.copy(this.cookieHash); -      } -      return this.cookieHash; -    } -  } -}; - -angular.service('$browser', function(){ -  return new MockBrowser(); -}); - - -/** - * 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 - * offset, so that we can test code that depends on local timezone settings without dependency on - * the time zone settings of the machine where the code is running. - * - * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) - * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* - * - * @example - * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z'); - * newYearInBratislava.getTimezoneOffset() => -60; - * newYearInBratislava.getFullYear() => 2010; - * newYearInBratislava.getMonth() => 0; - * newYearInBratislava.getDate() => 1; - * newYearInBratislava.getHours() => 0; - * newYearInBratislava.getMinutes() => 0; - * - * - * !!!! WARNING !!!!! - * This is not a complete Date object so only methods that were implemented can be called safely. - * To make matters worse, TzDate instances inherit stuff from Date via a prototype. - * - * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is - * incomplete we might be missing some non-standard methods. This can result in errors like: - * "Date.prototype.foo called on incompatible Object". - */ -function TzDate(offset, timestamp) { -  if (angular.isString(timestamp)) { -    var tsStr = timestamp; - -    this.origDate = angular.String.toDate(timestamp); - -    timestamp = this.origDate.getTime(); -    if (isNaN(timestamp)) -      throw { -        name: "Illegal Argument", -        message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" -      }; -  } else { -    this.origDate = new Date(timestamp); -  } - -  var localOffset = new Date(timestamp).getTimezoneOffset(); -  this.offsetDiff = localOffset*60*1000 - offset*1000*60*60; -  this.date = new Date(timestamp + this.offsetDiff); - -  this.getTime = function() { -    return this.date.getTime() - this.offsetDiff; -  }; - -  this.toLocaleDateString = function() { -    return this.date.toLocaleDateString(); -  }; - -  this.getFullYear = function() { -    return this.date.getFullYear(); -  }; - -  this.getMonth = function() { -    return this.date.getMonth(); -  }; - -  this.getDate = function() { -    return this.date.getDate(); -  }; - -  this.getHours = function() { -    return this.date.getHours(); -  }; - -  this.getMinutes = function() { -    return this.date.getMinutes(); -  }; - -  this.getSeconds = function() { -    return this.date.getSeconds(); -  }; - -  this.getTimezoneOffset = function() { -    return offset * 60; -  }; - -  this.getUTCFullYear = function() { -    return this.origDate.getUTCFullYear(); -  }; - -  this.getUTCMonth = function() { -    return this.origDate.getUTCMonth(); -  }; - -  this.getUTCDate = function() { -    return this.origDate.getUTCDate(); -  }; - -  this.getUTCHours = function() { -    return this.origDate.getUTCHours(); -  }; - -  this.getUTCMinutes = function() { -    return this.origDate.getUTCMinutes(); -  }; - -  this.getUTCSeconds = function() { -    return this.origDate.getUTCSeconds(); -  }; - - -  //hide all methods not implemented in this mock that the Date prototype exposes -  var unimplementedMethods = ['getDay', 'getMilliseconds', 'getTime', 'getUTCDay', -      'getUTCMilliseconds', 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', -      'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', -      'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', -      'setYear', 'toDateString', 'toJSON', 'toGMTString', 'toLocaleFormat', 'toLocaleString', -      'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; - -  angular.forEach(unimplementedMethods, function(methodName) { -    this[methodName] = function() { -      throw { -        name: "MethodNotImplemented", -        message: "Method '" + methodName + "' is not implemented in the TzDate mock" -      }; -    }; -  }); -} - -//make "tzDateInstance instanceof Date" return true -TzDate.prototype = Date.prototype; 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() { | 
