diff options
| -rw-r--r-- | CHANGELOG.md | 7 | ||||
| -rw-r--r-- | src/apis.js | 58 | ||||
| -rw-r--r-- | src/widgets.js | 112 | ||||
| -rw-r--r-- | test/ApiSpecs.js | 105 | ||||
| -rw-r--r-- | test/BinderSpec.js | 40 | ||||
| -rw-r--r-- | test/scenario/dslSpec.js | 4 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 285 | 
7 files changed, 384 insertions, 227 deletions
| diff --git a/CHANGELOG.md b/CHANGELOG.md index 856731b2..cd5fb2b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,10 @@  - If Angular is being used with jQuery older than 1.6, some features might not work properly. Please    upgrade to jQuery version 1.6.4. - +## Breaking Changes +- ng:repeat no longer has ng:repeat-index property. This is because the elements now have +  affinity to the underlying collection, and moving items around in the collection would move +  ng:repeat-index property rendering it meaningless.  <a name="0.10.1"><a/> @@ -88,7 +91,7 @@      - $location.hashPath -> $location.path()      - $location.hashSearch -> $location.search()      - $location.search -> no equivalent, use $window.location.search (this is so that we can work in -      hashBang and html5 mode at the same time, check out the docs)  +      hashBang and html5 mode at the same time, check out the docs)      - $location.update() / $location.updateHash() -> use $location.url()      - n/a -> $location.replace() - new api for replacing history record instead of creating a new one diff --git a/src/apis.js b/src/apis.js index 7aa9f0c2..bec54b8e 100644 --- a/src/apis.js +++ b/src/apis.js @@ -840,20 +840,22 @@ var angularFunction = {   * Hash of a:   *  string is string   *  number is number as string - *  object is either call $hashKey function on object or assign unique hashKey id. + *  object is either result of calling $$hashKey function on the object or uniquely generated id,  + *         that is also assigned to the $$hashKey property of the object.   *   * @param obj - * @returns {String} hash string such that the same input will have the same hash string + * @returns {String} hash string such that the same input will have the same hash string. + *         The resulting string key is in 'type:hashKey' format.   */  function hashKey(obj) {    var objType = typeof obj;    var key = obj;    if (objType == 'object') { -    if (typeof (key = obj.$hashKey) == 'function') { +    if (typeof (key = obj.$$hashKey) == 'function') {        // must invoke on object to keep the right this -      key = obj.$hashKey(); +      key = obj.$$hashKey();      } else if (key === undefined) { -      key = obj.$hashKey = nextUid(); +      key = obj.$$hashKey = nextUid();      }    }    return objType + ':' + key; @@ -868,13 +870,9 @@ HashMap.prototype = {     * Store key value pair     * @param key key to store can be any type     * @param value value to store can be any type -   * @returns old value if any     */    put: function(key, value) { -    var _key = hashKey(key); -    var oldValue = this[_key]; -    this[_key] = value; -    return oldValue; +    this[hashKey(key)] = value;    },    /** @@ -888,16 +886,48 @@ HashMap.prototype = {    /**     * Remove the key/value pair     * @param key -   * @returns value associated with key before it was removed     */    remove: function(key) { -    var _key = hashKey(key); -    var value = this[_key]; -    delete this[_key]; +    var value = this[key = hashKey(key)]; +    delete this[key];      return value;    }  }; +/** + * A map where multiple values can be added to the same key such that the form a queue. + * @returns {HashQueueMap} + */ +function HashQueueMap(){} +HashQueueMap.prototype = { +  /** +   * Same as array push, but using an array as the value for the hash +   */ +  push: function(key, value) { +    var array = this[key = hashKey(key)]; +    if (!array) { +      this[key] = [value]; +    } else { +      array.push(value); +    } +  }, + +  /** +   * Same as array shift, but using an array as the value for the hash +   */ +  shift: function(key) { +    var array = this[key = hashKey(key)]; +    if (array) { +      if (array.length == 1) { +        delete this[key]; +        return array[0]; +      } else { +        return array.shift(); +      } +    } +  } +}; +  function defineApi(dst, chain){    angular[dst] = angular[dst] || {};    forEach(chain, function(parent){ diff --git a/src/widgets.js b/src/widgets.js index bd7c3d7f..1047c3ce 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -1182,10 +1182,9 @@ angularWidget('a', function() {   * @name angular.widget.@ng:repeat   *   * @description - * The `ng:repeat` widget instantiates a template once per item from a collection. The collection is - * enumerated with the `ng:repeat-index` attribute, starting from 0. Each template instance gets - * its own scope, where the given loop variable is set to the current collection item, and `$index` - * is set to the item index or key. + * The `ng:repeat` widget instantiates a template once per item from a collection. Each template + * instance gets its own scope, where the given loop variable is set to the current collection item, + * and `$index` is set to the item index or key.   *   * Special properties are exposed on the local scope of each template instance, including:   * @@ -1256,68 +1255,89 @@ angularWidget('@ng:repeat', function(expression, element){      valueIdent = match[3] || match[1];      keyIdent = match[2]; -    var childScopes = []; -    var childElements = [iterStartElement];      var parentScope = this; +    // Store a list of elements from previous run. This is a hash where key is the item from the +    // iterator, and the value is an array of objects with following properties. +    //   - scope: bound scope +    //   - element: previous element. +    //   - index: position +    // We need an array of these objects since the same object can be returned from the iterator. +    // We expect this to be a rare case. +    var lastOrder = new HashQueueMap();      this.$watch(function(scope){        var index = 0, -          childCount = childScopes.length,            collection = scope.$eval(rhs),            collectionLength = size(collection, true), -          fragment = document.createDocumentFragment(), -          addFragmentTo = (childCount < collectionLength) ? childElements[childCount] : null,            childScope, -          key; +          // Same as lastOrder but it has the current state. It will become the +          // lastOrder on the next iteration. +          nextOrder = new HashQueueMap(), +          key, value, // key/value of iteration +          array, last,       // last object information {scope, element, index} +          cursor = iterStartElement;     // current position of the node        for (key in collection) {          if (collection.hasOwnProperty(key)) { -          if (index < childCount) { -            // reuse existing child -            childScope = childScopes[index]; -            childScope[valueIdent] = collection[key]; -            if (keyIdent) childScope[keyIdent] = key; -            childScope.$position = index == 0 -                ? 'first' -                : (index == collectionLength - 1 ? 'last' : 'middle'); -            childScope.$eval(); +          last = lastOrder.shift(value = collection[key]); +          if (last) { +            // if we have already seen this object, then we need to reuse the +            // associated scope/element +            childScope = last.scope; +            nextOrder.push(value, last); + +            if (index === last.index) { +              // do nothing +              cursor = last.element; +            } else { +              // existing item which got moved +              last.index = index; +              // This may be a noop, if the element is next, but I don't know of a good way to +              // figure this out,  since it would require extra DOM access, so let's just hope that +              // the browsers realizes that it is noop, and treats it as such. +              cursor.after(last.element); +              cursor = last.element; +            }            } else { -            // grow children +            // new item which we don't know about              childScope = parentScope.$new(); -            childScope[valueIdent] = collection[key]; -            if (keyIdent) childScope[keyIdent] = key; -            childScope.$index = index; -            childScope.$position = index == 0 -                ? 'first' -                : (index == collectionLength - 1 ? 'last' : 'middle'); -            childScopes.push(childScope); +          } + +          childScope[valueIdent] = collection[key]; +          if (keyIdent) childScope[keyIdent] = key; +          childScope.$index = index; +          childScope.$position = index == 0 +              ? 'first' +              : (index == collectionLength - 1 ? 'last' : 'middle'); + +          if (!last) {              linker(childScope, function(clone){ -              clone.attr('ng:repeat-index', index); -              fragment.appendChild(clone[0]); -              // TODO(misko): Temporary hack - maybe think about it - removed after we add fragment after $digest() -              // This causes double $digest for children -              // The first flush will couse a lot of DOM access (initial) -              // Second flush shuld be noop since nothing has change hence no DOM access. -              childScope.$digest(); -              childElements[index + 1] = clone; +              cursor.after(clone); +              last = { +                  scope: childScope, +                  element: (cursor = clone), +                  index: index +                }; +              nextOrder.push(value, last);              });            } +            index ++;          }        } -      //attach new nodes buffered in doc fragment -      if (addFragmentTo) { -        // TODO(misko): For performance reasons, we should do the addition after all other widgets -        // have run. For this should happend after $digest() is done! -        addFragmentTo.after(jqLite(fragment)); +      //shrink children +      for (key in lastOrder) { +        if (lastOrder.hasOwnProperty(key)) { +          array = lastOrder[key]; +          while(array.length) { +            value = array.pop(); +            value.element.remove(); +            value.scope.$destroy(); +          } +        }        } -      // shrink children -      while(childScopes.length > index) { -        // can not use $destroy(true) since  there may be multiple iterators on same parent. -        childScopes.pop().$destroy(); -        childElements.pop().remove(); -      } +      lastOrder = nextOrder;      });    };  }); diff --git a/test/ApiSpecs.js b/test/ApiSpecs.js index ef25bb41..9683a7b7 100644 --- a/test/ApiSpecs.js +++ b/test/ApiSpecs.js @@ -1,26 +1,39 @@  'use strict'; -describe('api', function(){ +describe('api', function() { -  describe('HashMap', function(){ -    it('should do basic crud', function(){ +  describe('HashMap', function() { +    it('should do basic crud', function() {        var map = new HashMap();        var key = {};        var value1 = {};        var value2 = {}; -      expect(map.put(key, value1)).toEqual(undefined); -      expect(map.put(key, value2)).toEqual(value1); -      expect(map.get(key)).toEqual(value2); -      expect(map.get({})).toEqual(undefined); -      expect(map.remove(key)).toEqual(value2); -      expect(map.get(key)).toEqual(undefined); +      map.put(key, value1); +      map.put(key, value2); +      expect(map.get(key)).toBe(value2); +      expect(map.get({})).toBe(undefined); +      expect(map.remove(key)).toBe(value2); +      expect(map.get(key)).toBe(undefined);      });    }); -  describe('Object', function(){ +  describe('HashQueueMap', function() { +    it('should do basic crud with collections', function() { +      var map = new HashQueueMap(); +      map.push('key', 'a'); +      map.push('key', 'b'); +      expect(map[hashKey('key')]).toEqual(['a', 'b']); +      expect(map.shift('key')).toEqual('a'); +      expect(map.shift('key')).toEqual('b'); +      expect(map.shift('key')).toEqual(undefined); +      expect(map[hashKey('key')]).toEqual(undefined); +    }); +  }); + -    it('should return type of', function(){ +  describe('Object', function() { +    it('should return type of', function() {        assertEquals("undefined", angular.Object.typeOf(undefined));        assertEquals("null", angular.Object.typeOf(null));        assertEquals("object", angular.Collection.typeOf({})); @@ -28,46 +41,45 @@ describe('api', function(){        assertEquals("string", angular.Object.typeOf(""));        assertEquals("date", angular.Object.typeOf(new Date()));        assertEquals("element", angular.Object.typeOf(document.body)); -      assertEquals('function', angular.Object.typeOf(function(){})); +      assertEquals('function', angular.Object.typeOf(function() {}));      }); -    it('should extend object', function(){ +    it('should extend object', function() {        assertEquals({a:1, b:2}, angular.Object.extend({a:1}, {b:2}));      }); -    }); -  it('should return size', function(){ +  it('should return size', function() {      assertEquals(0, angular.Collection.size({}));      assertEquals(1, angular.Collection.size({a:"b"}));      assertEquals(0, angular.Object.size({}));      assertEquals(1, angular.Array.size([0]));    }); -  describe('Array', function(){ -    describe('sum', function(){ +  describe('Array', function() { -      it('should sum', function(){ +    describe('sum', function() { +      it('should sum', function() {          assertEquals(3, angular.Array.sum([{a:"1"}, {a:"2"}], 'a'));        }); -      it('should sum containing NaN', function(){ +      it('should sum containing NaN', function() {          assertEquals(1, angular.Array.sum([{a:1}, {a:Number.NaN}], 'a')); -        assertEquals(1, angular.Array.sum([{a:1}, {a:Number.NaN}], function($){return $.a;})); +        assertEquals(1, angular.Array.sum([{a:1}, {a:Number.NaN}], function($) {return $.a;}));        }); -      }); -    it('should find indexOf', function(){ + +    it('should find indexOf', function() {        assertEquals(angular.Array.indexOf(['a'], 'a'), 0);        assertEquals(angular.Array.indexOf(['a', 'b'], 'a'), 0);        assertEquals(angular.Array.indexOf(['b', 'a'], 'a'), 1);        assertEquals(angular.Array.indexOf(['b', 'b'],'x'), -1);      }); -    it('should remove item from array', function(){ +    it('should remove item from array', function() {        var items = ['a', 'b', 'c'];        assertEquals(angular.Array.remove(items, 'q'), 'q');        assertEquals(items.length, 3); @@ -85,8 +97,8 @@ describe('api', function(){        assertEquals(items.length, 0);      }); -    describe('filter', function(){ +    describe('filter', function() {        it('should filter by string', function() {          var items = ["MIsKO", {name:"shyam"}, ["adam"], 1234];          assertEquals(4, angular.Array.filter(items, "").length); @@ -113,7 +125,7 @@ describe('api', function(){          assertEquals(0, angular.Array.filter(items, "misko").length);        }); -      it('should filter on specific property', function(){ +      it('should filter on specific property', function() {          var items = [{ignore:"a", name:"a"}, {ignore:"a", name:"abc"}];          assertEquals(2, angular.Array.filter(items, {}).length); @@ -123,12 +135,12 @@ describe('api', function(){          assertEquals("abc", angular.Array.filter(items, {name:'b'})[0].name);        }); -      it('should take function as predicate', function(){ +      it('should take function as predicate', function() {          var items = [{name:"a"}, {name:"abc", done:true}]; -        assertEquals(1, angular.Array.filter(items, function(i){return i.done;}).length); +        assertEquals(1, angular.Array.filter(items, function(i) {return i.done;}).length);        }); -      it('should take object as perdicate', function(){ +      it('should take object as perdicate', function() {          var items = [{first:"misko", last:"hevery"},                       {first:"adam", last:"abrons"}]; @@ -139,7 +151,7 @@ describe('api', function(){          assertEquals(items[0], angular.Array.filter(items, {first:'misko', last:'hevery'})[0]);        }); -      it('should support negation operator', function(){ +      it('should support negation operator', function() {          var items = ["misko", "adam"];          assertEquals(1, angular.Array.filter(items, '!isk').length); @@ -198,12 +210,12 @@ describe('api', function(){      }); -    it('add', function(){ +    it('add', function() {        var add = angular.Array.add;        assertJsonEquals([{}, "a"], add(add([]),"a"));      }); -    it('count', function(){ +    it('count', function() {        var array = [{name:'a'},{name:'b'},{name:''}];        var obj = {}; @@ -212,24 +224,25 @@ describe('api', function(){        assertEquals(1, angular.Array.count(array, 'name=="a"'));      }); -    describe('orderBy', function(){ + +    describe('orderBy', function() {        var orderBy; -      beforeEach(function(){ +      beforeEach(function() {          orderBy = angular.Array.orderBy;        }); -      it('should return same array if predicate is falsy', function(){ +      it('should return same array if predicate is falsy', function() {          var array = [1, 2, 3];          expect(orderBy(array)).toBe(array);        }); -      it('shouldSortArrayInReverse', function(){ +      it('shouldSortArrayInReverse', function() {          assertJsonEquals([{a:15},{a:2}], angular.Array.orderBy([{a:15},{a:2}], 'a', true));          assertJsonEquals([{a:15},{a:2}], angular.Array.orderBy([{a:15},{a:2}], 'a', "T"));          assertJsonEquals([{a:15},{a:2}], angular.Array.orderBy([{a:15},{a:2}], 'a', "reverse"));        }); -      it('should sort array by predicate', function(){ +      it('should sort array by predicate', function() {          assertJsonEquals([{a:2, b:1},{a:15, b:1}],              angular.Array.orderBy([{a:15, b:1},{a:2, b:1}], ['a', 'b']));          assertJsonEquals([{a:2, b:1},{a:15, b:1}], @@ -238,11 +251,11 @@ describe('api', function(){              angular.Array.orderBy([{a:15, b:1},{a:2, b:1}], ['+b', '-a']));        }); -      it('should use function', function(){ +      it('should use function', function() {          expect(            orderBy(              [{a:15, b:1},{a:2, b:1}], -            function(value){ return value.a; })). +            function(value) { return value.a; })).          toEqual([{a:2, b:1},{a:15, b:1}]);        }); @@ -250,9 +263,9 @@ describe('api', function(){    }); -  describe('string', function(){ -    it('should quote', function(){ +  describe('string', function() { +    it('should quote', function() {        assertEquals(angular.String.quote('a'), '"a"');        assertEquals(angular.String.quote('\\'), '"\\\\"');        assertEquals(angular.String.quote("'a'"), '"\'a\'"'); @@ -260,22 +273,22 @@ describe('api', function(){        assertEquals(angular.String.quote('\n\f\r\t'), '"\\n\\f\\r\\t"');      }); -    it('should quote slashes', function(){ +    it('should quote slashes', function() {        assertEquals('"7\\\\\\\"7"', angular.String.quote("7\\\"7"));      }); -    it('should quote unicode', function(){ +    it('should quote unicode', function() {        assertEquals('"abc\\u00a0def"', angular.String.quoteUnicode('abc\u00A0def'));      }); -    it('should read/write to date', function(){ +    it('should read/write to date', function() {        var date = new Date("Sep 10 2003 13:02:03 GMT");        assertEquals("date", angular.Object.typeOf(date));        assertEquals("2003-09-10T13:02:03.000Z", angular.Date.toString(date));        assertEquals(date.getTime(), angular.String.toDate(angular.Date.toString(date)).getTime());      }); -    it('should convert to date', function(){ +    it('should convert to date', function() {        //full ISO8061        expect(angular.String.toDate("2003-09-10T13:02:03.000Z")).          toEqual(new Date("Sep 10 2003 13:02:03 GMT")); @@ -297,14 +310,12 @@ describe('api', function(){          toEqual(new Date("Sep 10 2003 00:00:00 GMT"));      }); -    it('should parse date', function(){ +    it('should parse date', function() {        var date = angular.String.toDate("2003-09-10T13:02:03.000Z");        assertEquals("date", angular.Object.typeOf(date));        assertEquals("2003-09-10T13:02:03.000Z", angular.Date.toString(date));        assertEquals("str", angular.String.toDate("str"));      }); -    }); -  }); diff --git a/test/BinderSpec.js b/test/BinderSpec.js index 68513f62..93f23eef 100644 --- a/test/BinderSpec.js +++ b/test/BinderSpec.js @@ -194,25 +194,25 @@ describe('Binder', function(){      scope.$apply();      assertEquals('<ul>' +            '<#comment></#comment>' + -          '<li ng:bind="item.a" ng:repeat-index="0">A</li>' + -          '<li ng:bind="item.a" ng:repeat-index="1">B</li>' + +          '<li ng:bind="item.a">A</li>' + +          '<li ng:bind="item.a">B</li>' +            '</ul>', sortedHtml(form));      items.unshift({a:'C'});      scope.$apply();      assertEquals('<ul>' +            '<#comment></#comment>' + -          '<li ng:bind="item.a" ng:repeat-index="0">C</li>' + -          '<li ng:bind="item.a" ng:repeat-index="1">A</li>' + -          '<li ng:bind="item.a" ng:repeat-index="2">B</li>' + +          '<li ng:bind="item.a">C</li>' + +          '<li ng:bind="item.a">A</li>' + +          '<li ng:bind="item.a">B</li>' +            '</ul>', sortedHtml(form));      items.shift();      scope.$apply();      assertEquals('<ul>' +            '<#comment></#comment>' + -          '<li ng:bind="item.a" ng:repeat-index="0">A</li>' + -          '<li ng:bind="item.a" ng:repeat-index="1">B</li>' + +          '<li ng:bind="item.a">A</li>' + +          '<li ng:bind="item.a">B</li>' +            '</ul>', sortedHtml(form));      items.shift(); @@ -226,7 +226,7 @@ describe('Binder', function(){      scope.$apply();      assertEquals('<ul>' +            '<#comment></#comment>' + -          '<li ng:repeat-index="0"><span ng:bind="item.a">A</span></li>' + +          '<li><span ng:bind="item.a">A</span></li>' +            '</ul>', sortedHtml(scope.$element));    }); @@ -329,15 +329,15 @@ describe('Binder', function(){      assertEquals('<div>'+          '<#comment></#comment>'+ -        '<div name="a" ng:bind-attr="{"name":"{{m.name}}"}" ng:repeat-index="0">'+ +        '<div name="a" ng:bind-attr="{"name":"{{m.name}}"}">'+            '<#comment></#comment>'+ -          '<ul name="a1" ng:bind-attr="{"name":"{{i}}"}" ng:repeat-index="0"></ul>'+ -          '<ul name="a2" ng:bind-attr="{"name":"{{i}}"}" ng:repeat-index="1"></ul>'+ +          '<ul name="a1" ng:bind-attr="{"name":"{{i}}"}"></ul>'+ +          '<ul name="a2" ng:bind-attr="{"name":"{{i}}"}"></ul>'+          '</div>'+ -        '<div name="b" ng:bind-attr="{"name":"{{m.name}}"}" ng:repeat-index="1">'+ +        '<div name="b" ng:bind-attr="{"name":"{{m.name}}"}">'+            '<#comment></#comment>'+ -          '<ul name="b1" ng:bind-attr="{"name":"{{i}}"}" ng:repeat-index="0"></ul>'+ -          '<ul name="b2" ng:bind-attr="{"name":"{{i}}"}" ng:repeat-index="1"></ul>'+ +          '<ul name="b1" ng:bind-attr="{"name":"{{i}}"}"></ul>'+ +          '<ul name="b2" ng:bind-attr="{"name":"{{i}}"}"></ul>'+          '</div></div>', sortedHtml(scope.$element));    }); @@ -417,8 +417,8 @@ describe('Binder', function(){      expect(d2.hasClass('e')).toBeTruthy();      assertEquals(          '<div><#comment></#comment>' + -        '<div class="o" ng:class-even="\'e\'" ng:class-odd="\'o\'" ng:repeat-index="0"></div>' + -        '<div class="e" ng:class-even="\'e\'" ng:class-odd="\'o\'" ng:repeat-index="1"></div></div>', +        '<div class="o" ng:class-even="\'e\'" ng:class-odd="\'o\'"></div>' + +        '<div class="e" ng:class-even="\'e\'" ng:class-odd="\'o\'"></div></div>',          sortedHtml(scope.$element));    }); @@ -459,8 +459,8 @@ describe('Binder', function(){      scope.items = [{}, {name:'misko'}];      scope.$apply(); -    assertEquals("123", scope.$eval('items[0].name')); -    assertEquals("misko", scope.$eval('items[1].name')); +    expect(scope.$eval('items[0].name')).toEqual("123"); +    expect(scope.$eval('items[1].name')).toEqual("misko");    });    it('ShouldTemplateBindPreElements', function () { @@ -593,8 +593,8 @@ describe('Binder', function(){      scope.$apply();      assertEquals('<ul>' +          '<#comment></#comment>' + -        '<li ng:bind=\"k + v\" ng:repeat-index="0">a0</li>' + -        '<li ng:bind=\"k + v\" ng:repeat-index="1">b1</li>' + +        '<li ng:bind=\"k + v\">a0</li>' + +        '<li ng:bind=\"k + v\">b1</li>' +          '</ul>',          sortedHtml(scope.$element));    }); diff --git a/test/scenario/dslSpec.js b/test/scenario/dslSpec.js index 3160da8d..c5d0a29d 100644 --- a/test/scenario/dslSpec.js +++ b/test/scenario/dslSpec.js @@ -378,9 +378,9 @@ describe("angular.scenario.dsl", function() {        beforeEach(function() {          doc.append(            '<ul>' + -          '  <li ng:repeat-index="0"><span ng:bind="name" class="ng-binding">misko</span>' + +          '  <li><span ng:bind="name" class="ng-binding">misko</span>' +            '    <span ng:bind="test && gender" class="ng-binding">male</span></li>' + -          '  <li ng:repeat-index="1"><span ng:bind="name" class="ng-binding">felisa</span>' + +          '  <li><span ng:bind="name" class="ng-binding">felisa</span>' +            '    <span ng:bind="gender | uppercase" class="ng-binding">female</span></li>' +            '</ul>'          ); diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index 6fccaa48..02d0ef71 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -1,6 +1,6 @@  'use strict'; -describe("widget", function(){ +describe("widget", function() {    var compile, element, scope;    beforeEach(function() { @@ -19,14 +19,15 @@ describe("widget", function(){      };    }); -  afterEach(function(){ +  afterEach(function() {      dealoc(element);    }); -  describe("input", function(){ -    describe("text", function(){ -      it('should input-text auto init and handle keydown/change events', function(){ +  describe("input", function() { + +    describe("text", function() { +      it('should input-text auto init and handle keydown/change events', function() {          compile('<input type="Text" name="name" value="Misko" ng:change="count = count + 1" ng:init="count=0"/>');          expect(scope.name).toEqual("Misko");          expect(scope.count).toEqual(0); @@ -49,7 +50,7 @@ describe("widget", function(){          expect(scope.count).toEqual(2);        }); -      it('should not trigger eval if value does not change', function(){ +      it('should not trigger eval if value does not change', function() {          compile('<input type="Text" name="name" value="Misko" ng:change="count = count + 1" ng:init="count=0"/>');          expect(scope.name).toEqual("Misko");          expect(scope.count).toEqual(0); @@ -58,16 +59,16 @@ describe("widget", function(){          expect(scope.count).toEqual(0);        }); -      it('should allow complex refernce binding', function(){ +      it('should allow complex refernce binding', function() {          compile('<div ng:init="obj={abc:{}}">'+                    '<input type="Text" name="obj[\'abc\'].name" value="Misko""/>'+                  '</div>');          expect(scope.obj['abc'].name).toEqual('Misko');        }); -      describe("ng:format", function(){ -        it("should format text", function(){ +      describe("ng:format", function() { +        it("should format text", function() {            compile('<input type="Text" name="list" value="a,b,c" ng:format="list"/>');            expect(scope.list).toEqual(['a', 'b', 'c']); @@ -80,13 +81,13 @@ describe("widget", function(){            expect(scope.list).toEqual(['1', '2', '3']);          }); -        it("should come up blank if null", function(){ +        it("should come up blank if null", function() {            compile('<input type="text" name="age" ng:format="number" ng:init="age=null"/>');            expect(scope.age).toBeNull();            expect(scope.$element[0].value).toEqual('');          }); -        it("should show incorect text while number does not parse", function(){ +        it("should show incorect text while number does not parse", function() {            compile('<input type="text" name="age" ng:format="number"/>');            scope.age = 123;            scope.$digest(); @@ -97,14 +98,14 @@ describe("widget", function(){            expect(scope.$element).toBeInvalid();          }); -        it("should clober incorect text if model changes", function(){ +        it("should clober incorect text if model changes", function() {            compile('<input type="text" name="age" ng:format="number" value="123X"/>');            scope.age = 456;            scope.$digest();            expect(scope.$element.val()).toEqual('456');          }); -        it("should not clober text if model changes due to itself", function(){ +        it("should not clober text if model changes due to itself", function() {            compile('<input type="text" name="list" ng:format="list" value="a"/>');            scope.$element.val('a '); @@ -128,23 +129,23 @@ describe("widget", function(){            expect(scope.list).toEqual(['a', 'b']);          }); -        it("should come up blank when no value specifiend", function(){ +        it("should come up blank when no value specifiend", function() {            compile('<input type="text" name="age" ng:format="number"/>');            scope.$digest();            expect(scope.$element.val()).toEqual('');            expect(scope.age).toEqual(null);          }); -        }); -      describe("checkbox", function(){ -        it("should format booleans", function(){ + +      describe("checkbox", function() { +        it("should format booleans", function() {            compile('<input type="checkbox" name="name" ng:init="name=false"/>');            expect(scope.name).toEqual(false);            expect(scope.$element[0].checked).toEqual(false);          }); -        it('should support type="checkbox"', function(){ +        it('should support type="checkbox"', function() {            compile('<input type="checkBox" name="checkbox" checked ng:change="action = true"/>');            expect(scope.checkbox).toEqual(true);            browserTrigger(element); @@ -154,9 +155,9 @@ describe("widget", function(){            expect(scope.checkbox).toEqual(true);          }); -        it("should use ng:format", function(){ +        it("should use ng:format", function() {            angularFormatter('testFormat', { -            parse: function(value){ +            parse: function(value) {                return value ? "Worked" : "Failed";              }, @@ -181,8 +182,9 @@ describe("widget", function(){          });        }); -      describe("ng:validate", function(){ -        it("should process ng:validate", function(){ + +      describe("ng:validate", function() { +        it("should process ng:validate", function() {            compile('<input type="text" name="price" value="abc" ng:validate="number"/>',                    jqLite(document.body));            expect(element.hasClass('ng-validation-error')).toBeTruthy(); @@ -210,9 +212,9 @@ describe("widget", function(){            expect(element.attr('ng-validation-error')).toBeFalsy();          }); -        it("should not call validator if undefined/empty", function(){ +        it("should not call validator if undefined/empty", function() {            var lastValue = "NOT_CALLED"; -          angularValidator.myValidator = function(value){lastValue = value;}; +          angularValidator.myValidator = function(value) {lastValue = value;};            compile('<input type="text" name="url" ng:validate="myValidator"/>');            expect(lastValue).toEqual("NOT_CALLED"); @@ -225,19 +227,20 @@ describe("widget", function(){        });      }); -    it("should ignore disabled widgets", function(){ + +    it("should ignore disabled widgets", function() {        compile('<input type="text" name="price" ng:required disabled/>');        expect(element.hasClass('ng-validation-error')).toBeFalsy();        expect(element.attr('ng-validation-error')).toBeFalsy();      }); -    it("should ignore readonly widgets", function(){ +    it("should ignore readonly widgets", function() {        compile('<input type="text" name="price" ng:required readonly/>');        expect(element.hasClass('ng-validation-error')).toBeFalsy();        expect(element.attr('ng-validation-error')).toBeFalsy();      }); -    it("should process ng:required", function(){ +    it("should process ng:required", function() {        compile('<input type="text" name="price" ng:required/>', jqLite(document.body));        expect(element.hasClass('ng-validation-error')).toBeTruthy();        expect(element.attr('ng-validation-error')).toEqual('Required'); @@ -296,9 +299,8 @@ describe("widget", function(){      }); -    describe('radio', function(){ - -      it('should support type="radio"', function(){ +    describe('radio', function() { +      it('should support type="radio"', function() {          compile('<div>' +              '<input type="radio" name="chose" value="A" ng:change="clicked = 1"/>' +              '<input type="radio" name="chose" value="B" checked ng:change="clicked = 2"/>' + @@ -323,7 +325,7 @@ describe("widget", function(){          expect(scope.clicked).toEqual(1);        }); -      it('should honor model over html checked keyword after', function(){ +      it('should honor model over html checked keyword after', function() {          compile('<div ng:init="choose=\'C\'">' +              '<input type="radio" name="choose" value="A""/>' +              '<input type="radio" name="choose" value="B" checked/>' + @@ -333,7 +335,7 @@ describe("widget", function(){          expect(scope.choose).toEqual('C');        }); -      it('should honor model over html checked keyword before', function(){ +      it('should honor model over html checked keyword before', function() {          compile('<div ng:init="choose=\'A\'">' +              '<input type="radio" name="choose" value="A""/>' +              '<input type="radio" name="choose" value="B" checked/>' + @@ -345,8 +347,9 @@ describe("widget", function(){      }); -    describe('select-one', function(){ -      it('should initialize to selected', function(){ + +    describe('select-one', function() { +      it('should initialize to selected', function() {          compile(              '<select name="selection">' +                  '<option>A</option>' + @@ -372,11 +375,11 @@ describe("widget", function(){          expect(scope.$element.text()).toBe('foobarC');        }); -      }); -    describe('select-multiple', function(){ -      it('should support type="select-multiple"', function(){ + +    describe('select-multiple', function() { +      it('should support type="select-multiple"', function() {          compile('<select name="selection" multiple>' +                    '<option>A</option>' +                    '<option selected>B</option>' + @@ -386,32 +389,32 @@ describe("widget", function(){          scope.$digest();          expect(element[0].childNodes[0].selected).toEqual(true);        }); -      }); -    it('should ignore text widget which have no name', function(){ + +    it('should ignore text widget which have no name', function() {        compile('<input type="text"/>');        expect(scope.$element.attr('ng-exception')).toBeFalsy();        expect(scope.$element.hasClass('ng-exception')).toBeFalsy();      }); -    it('should ignore checkbox widget which have no name', function(){ +    it('should ignore checkbox widget which have no name', function() {        compile('<input type="checkbox"/>');        expect(scope.$element.attr('ng-exception')).toBeFalsy();        expect(scope.$element.hasClass('ng-exception')).toBeFalsy();      }); -    it('should report error on assignment error', function(){ -      expect(function(){ +    it('should report error on assignment error', function() { +      expect(function() {          compile('<input type="text" name="throw \'\'" value="x"/>');        }).toThrow("Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at [''].");        $logMock.error.logs.shift();      }); -    }); -  describe('ng:switch', function(){ -    it('should switch on value change', function(){ + +  describe('ng:switch', function() { +    it('should switch on value change', function() {        compile('<ng:switch on="select">' +            '<div ng:switch-when="1">first:{{name}}</div>' +            '<div ng:switch-when="2">second:{{name}}</div>' + @@ -435,7 +438,7 @@ describe("widget", function(){        expect(element.text()).toEqual('true:misko');      }); -    it('should switch on switch-when-default', function(){ +    it('should switch on switch-when-default', function() {        compile('<ng:switch on="select">' +                  '<div ng:switch-when="1">one</div>' +                  '<div ng:switch-default>other</div>' + @@ -447,7 +450,7 @@ describe("widget", function(){        expect(element.text()).toEqual('one');      }); -    it('should call change on switch', function(){ +    it('should call change on switch', function() {        var scope = angular.compile('<ng:switch on="url" change="name=\'works\'"><div ng:switch-when="a">{{name}}</div></ng:switch>')();        scope.url = 'a';        scope.$apply(); @@ -457,7 +460,8 @@ describe("widget", function(){      });    }); -  describe('ng:include', function(){ + +  describe('ng:include', function() {      it('should include on external file', function() {        var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');        var scope = angular.compile(element)(); @@ -488,7 +492,7 @@ describe("widget", function(){        dealoc(scope);      }); -    it('should allow this for scope', function(){ +    it('should allow this for scope', function() {        var element = jqLite('<ng:include src="url" scope="this"></ng:include>');        var scope = angular.compile(element)();        scope.url = 'myUrl'; @@ -518,7 +522,7 @@ describe("widget", function(){        dealoc(element);      }); -    it('should destroy old scope', function(){ +    it('should destroy old scope', function() {        var element = jqLite('<ng:include src="url"></ng:include>');        var scope = angular.compile(element)(); @@ -536,6 +540,7 @@ describe("widget", function(){      });    }); +    describe('a', function() {      it('should prevent default action to be executed when href is empty', function() {        var orgLocation = document.location.href, @@ -571,12 +576,13 @@ describe("widget", function(){      });    }); -  describe('ng:options', function(){ + +  describe('ng:options', function() {      var select, scope; -    function createSelect(attrs, blank, unknown){ +    function createSelect(attrs, blank, unknown) {        var html = '<select'; -      forEach(attrs, function(value, key){ +      forEach(attrs, function(value, key) {          if (isBoolean(value)) {            if (value) html += ' ' + key;          } else { @@ -591,14 +597,14 @@ describe("widget", function(){        scope = compile(select);      } -    function createSingleSelect(blank, unknown){ +    function createSingleSelect(blank, unknown) {        createSelect({          'name':'selected',          'ng:options':'value.name for value in values'        }, blank, unknown);      } -    function createMultiSelect(blank, unknown){ +    function createMultiSelect(blank, unknown) {        createSelect({          'name':'selected',          'multiple':true, @@ -606,19 +612,19 @@ describe("widget", function(){        }, blank, unknown);      } -    afterEach(function(){ +    afterEach(function() {        dealoc(select);        dealoc(scope);      }); -    it('should throw when not formated "? for ? in ?"', function(){ -      expect(function(){ +    it('should throw when not formated "? for ? in ?"', function() { +      expect(function() {          compile('<select name="selected" ng:options="i dont parse"></select>');        }).toThrow("Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in" +                   " _collection_' but got 'i dont parse'.");      }); -    it('should render a list', function(){ +    it('should render a list', function() {        createSingleSelect();        scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];        scope.selected = scope.values[0]; @@ -630,7 +636,7 @@ describe("widget", function(){        expect(sortedHtml(options[2])).toEqual('<option value="2">C</option>');      }); -    it('should render an object', function(){ +    it('should render an object', function() {        createSelect({          'name':'selected',          'ng:options': 'value as key for (key, value) in object' @@ -651,7 +657,7 @@ describe("widget", function(){        expect(options[3].selected).toEqual(true);      }); -    it('should grow list', function(){ +    it('should grow list', function() {        createSingleSelect();        scope.values = [];        scope.$digest(); @@ -671,7 +677,7 @@ describe("widget", function(){        expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>');      }); -    it('should shrink list', function(){ +    it('should shrink list', function() {        createSingleSelect();        scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];        scope.selected = scope.values[0]; @@ -695,7 +701,7 @@ describe("widget", function(){        expect(select.find('option').length).toEqual(1); // we add back the special empty option      }); -    it('should shrink and then grow list', function(){ +    it('should shrink and then grow list', function() {        createSingleSelect();        scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];        scope.selected = scope.values[0]; @@ -713,7 +719,7 @@ describe("widget", function(){        expect(select.find('option').length).toEqual(3);      }); -    it('should update list', function(){ +    it('should update list', function() {        createSingleSelect();        scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];        scope.selected = scope.values[0]; @@ -729,7 +735,7 @@ describe("widget", function(){        expect(sortedHtml(options[2])).toEqual('<option value="2">D</option>');      }); -    it('should preserve existing options', function(){ +    it('should preserve existing options', function() {        createSingleSelect(true);        scope.$digest(); @@ -749,8 +755,9 @@ describe("widget", function(){        expect(jqLite(select.find('option')[0]).text()).toEqual('blank');      }); -    describe('binding', function(){ -      it('should bind to scope value', function(){ + +    describe('binding', function() { +      it('should bind to scope value', function() {          createSingleSelect();          scope.values = [{name:'A'}, {name:'B'}];          scope.selected = scope.values[0]; @@ -762,7 +769,8 @@ describe("widget", function(){          expect(select.val()).toEqual('1');        }); -      it('should bind to scope value and group', function(){ + +      it('should bind to scope value and group', function() {          createSelect({            'name':'selected',            'ng:options':'item.name group by item.group for item in values' @@ -795,7 +803,7 @@ describe("widget", function(){          expect(select.val()).toEqual('0');        }); -      it('should bind to scope value through experession', function(){ +      it('should bind to scope value through experession', function() {          createSelect({'name':'selected', 'ng:options':'item.id as item.name for item in values'});          scope.values = [{id:10, name:'A'}, {id:20, name:'B'}];          scope.selected = scope.values[0].id; @@ -807,7 +815,7 @@ describe("widget", function(){          expect(select.val()).toEqual('1');        }); -      it('should bind to object key', function(){ +      it('should bind to object key', function() {          createSelect({            'name':'selected',            'ng:options':'key as value for (key, value) in object' @@ -822,7 +830,7 @@ describe("widget", function(){          expect(select.val()).toEqual('blue');        }); -      it('should bind to object value', function(){ +      it('should bind to object value', function() {          createSelect({            name:'selected',            'ng:options':'value as key for (key, value) in object' @@ -837,7 +845,7 @@ describe("widget", function(){          expect(select.val()).toEqual('blue');        }); -      it('should insert a blank option if bound to null', function(){ +      it('should insert a blank option if bound to null', function() {          createSingleSelect();          scope.values = [{name:'A'}];          scope.selected = null; @@ -852,7 +860,7 @@ describe("widget", function(){          expect(select.find('option').length).toEqual(1);        }); -      it('should reuse blank option if bound to null', function(){ +      it('should reuse blank option if bound to null', function() {          createSingleSelect(true);          scope.values = [{name:'A'}];          scope.selected = null; @@ -867,7 +875,7 @@ describe("widget", function(){          expect(select.find('option').length).toEqual(2);        }); -      it('should insert a unknown option if bound to something not in the list', function(){ +      it('should insert a unknown option if bound to something not in the list', function() {          createSingleSelect();          scope.values = [{name:'A'}];          scope.selected = {}; @@ -883,8 +891,9 @@ describe("widget", function(){        });      }); -    describe('on change', function(){ -      it('should update model on change', function(){ + +    describe('on change', function() { +      it('should update model on change', function() {          createSingleSelect();          scope.values = [{name:'A'}, {name:'B'}];          scope.selected = scope.values[0]; @@ -896,7 +905,7 @@ describe("widget", function(){          expect(scope.selected).toEqual(scope.values[1]);        }); -      it('should fire ng:change if present', function(){ +      it('should fire ng:change if present', function() {          createSelect({            name:'selected',            'ng:options':'value for value in values', @@ -924,7 +933,7 @@ describe("widget", function(){          expect(scope.selected).toEqual(scope.values[0]);        }); -      it('should update model on change through expression', function(){ +      it('should update model on change through expression', function() {          createSelect({name:'selected', 'ng:options':'item.id as item.name for item in values'});          scope.values = [{id:10, name:'A'}, {id:20, name:'B'}];          scope.selected = scope.values[0].id; @@ -936,7 +945,7 @@ describe("widget", function(){          expect(scope.selected).toEqual(scope.values[1].id);        }); -      it('should update model to null on change', function(){ +      it('should update model to null on change', function() {          createSingleSelect(true);          scope.values = [{name:'A'}, {name:'B'}];          scope.selected = scope.values[0]; @@ -949,8 +958,9 @@ describe("widget", function(){        });      }); -    describe('select-many', function(){ -      it('should read multiple selection', function(){ + +    describe('select-many', function() { +      it('should read multiple selection', function() {          createMultiSelect();          scope.values = [{name:'A'}, {name:'B'}]; @@ -973,7 +983,7 @@ describe("widget", function(){          expect(select.find('option')[1].selected).toEqual(true);        }); -      it('should update model on change', function(){ +      it('should update model on change', function() {          createMultiSelect();          scope.values = [{name:'A'}, {name:'B'}]; @@ -990,8 +1000,7 @@ describe("widget", function(){    describe('@ng:repeat', function() { - -    it('should ng:repeat over array', function(){ +    it('should ng:repeat over array', function() {        var scope = compile('<ul><li ng:repeat="item in items" ng:init="suffix = \';\'" ng:bind="item + suffix"></li></ul>');        Array.prototype.extraProperty = "should be ignored"; @@ -1015,16 +1024,16 @@ describe("widget", function(){        expect(element.text()).toEqual('brad;');      }); -    it('should ng:repeat over object', function(){ +    it('should ng:repeat over object', function() {        var scope = compile('<ul><li ng:repeat="(key, value) in items" ng:bind="key + \':\' + value + \';\' "></li></ul>');        scope.items = {misko:'swe', shyam:'set'};        scope.$digest();        expect(element.text()).toEqual('misko:swe;shyam:set;');      }); -    it('should not ng:repeat over parent properties', function(){ -      var Class = function(){}; -      Class.prototype.abc = function(){}; +    it('should not ng:repeat over parent properties', function() { +      var Class = function() {}; +      Class.prototype.abc = function() {};        Class.prototype.value = 'abc';        var scope = compile('<ul><li ng:repeat="(key, value) in items" ng:bind="key + \':\' + value + \';\' "></li></ul>'); @@ -1034,8 +1043,8 @@ describe("widget", function(){        expect(element.text()).toEqual('name:value;');      }); -    it('should error on wrong parsing of ng:repeat', function(){ -      expect(function(){ +    it('should error on wrong parsing of ng:repeat', function() { +      expect(function() {          compile('<ul><li ng:repeat="i dont parse"></li></ul>');        }).toThrow("Expected ng:repeat in form of '_item_ in _collection_' but got 'i dont parse'."); @@ -1076,8 +1085,11 @@ describe("widget", function(){      });      it('should expose iterator position as $position when iterating over objects', function() { -      var scope = compile('<ul><li ng:repeat="(key, val) in items" ' + -                                  'ng:bind="key + \':\' + val + \':\' + $position + \'|\'"></li></ul>'); +      var scope = compile( +        '<ul>' + +          '<li ng:repeat="(key, val) in items" ng:bind="key + \':\' + val + \':\' + $position + \'|\'">' + +          '</li>' + +        '</ul>');        scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f'};        scope.$digest();        expect(element.text()).toEqual('misko:m:first|shyam:s:middle|doug:d:middle|frodo:f:last|'); @@ -1087,12 +1099,93 @@ describe("widget", function(){        scope.$digest();        expect(element.text()).toEqual('misko:m:first|shyam:s:last|');      }); + + +    describe('stability', function() { +      var a, b, c, d, scope, lis; + +      beforeEach(function() { +        scope = compile( +          '<ul>' + +            '<li ng:repeat="item in items" ng:bind="key + \':\' + val + \':\' + $position + \'|\'">' + +            '</li>' + +          '</ul>'); +        a = {}; +        b = {}; +        c = {}; +        d = {}; + +        scope.items = [a, b, c]; +        scope.$digest(); +        lis = element.find('li'); +      }); + +      it('should preserve the order of elements', function() { +        scope.items = [a, c, d]; +        scope.$digest(); +        var newElements = element.find('li'); +        expect(newElements[0]).toEqual(lis[0]); +        expect(newElements[1]).toEqual(lis[2]); +        expect(newElements[2]).not.toEqual(lis[1]); +      }); + +      it('should support duplicates', function() { +        scope.items = [a, a, b, c]; +        scope.$digest(); +        var newElements = element.find('li'); +        expect(newElements[0]).toEqual(lis[0]); +        expect(newElements[1]).not.toEqual(lis[0]); +        expect(newElements[2]).toEqual(lis[1]); +        expect(newElements[3]).toEqual(lis[2]); + +        lis = newElements; +        scope.$digest(); +        newElements = element.find('li'); +        expect(newElements[0]).toEqual(lis[0]); +        expect(newElements[1]).toEqual(lis[1]); +        expect(newElements[2]).toEqual(lis[2]); +        expect(newElements[3]).toEqual(lis[3]); + +        scope.$digest(); +        newElements = element.find('li'); +        expect(newElements[0]).toEqual(lis[0]); +        expect(newElements[1]).toEqual(lis[1]); +        expect(newElements[2]).toEqual(lis[2]); +        expect(newElements[3]).toEqual(lis[3]); +      }); + +      it('should remove last item when one duplicate instance is removed', function() { +        scope.items = [a, a, a]; +        scope.$digest(); +        lis = element.find('li'); + +        scope.items = [a, a]; +        scope.$digest(); +        var newElements = element.find('li'); +        expect(newElements.length).toEqual(2); +        expect(newElements[0]).toEqual(lis[0]); +        expect(newElements[1]).toEqual(lis[1]); +      }); + +      it('should reverse items when the collection is reversed', function() { +        scope.items = [a, b, c]; +        scope.$digest(); +        lis = element.find('li'); + +        scope.items = [c, b, a]; +        scope.$digest(); +        var newElements = element.find('li'); +        expect(newElements.length).toEqual(3); +        expect(newElements[0]).toEqual(lis[2]); +        expect(newElements[1]).toEqual(lis[1]); +        expect(newElements[2]).toEqual(lis[0]); +      }); +    });    });    describe('@ng:non-bindable', function() { - -    it('should prevent compilation of the owning element and its children', function(){ +    it('should prevent compilation of the owning element and its children', function() {        var scope = compile('<div ng:non-bindable><span ng:bind="name"></span></div>');        scope.name =  'misko';        scope.$digest(); @@ -1203,7 +1296,6 @@ describe("widget", function(){        dealoc($route.current.scope);      }); -      it('should initialize view template after the view controller was initialized even when ' +         'templates were cached', function() {        //this is a test for a regression that was introduced by making the ng:view cache sync @@ -1245,6 +1337,8 @@ describe("widget", function(){    describe('ng:pluralize', function() { + +      describe('deal with pluralized strings without offset', function() {         beforeEach(function() {            compile('<ng:pluralize count="email"' + @@ -1366,7 +1460,6 @@ describe("widget", function(){          expect(element.text()).toBe('Igor, Misko and 2 other people are viewing.');        });      }); -    });  }); | 
