'use strict'; describe('ngRepeat', function() { var element, $compile, scope, $exceptionHandler, $compileProvider; beforeEach(module(function(_$compileProvider_) { $compileProvider = _$compileProvider_; })); beforeEach(module(function($exceptionHandlerProvider) { $exceptionHandlerProvider.mode('log'); })); beforeEach(inject(function(_$compile_, $rootScope, _$exceptionHandler_) { $compile = _$compile_; $exceptionHandler = _$exceptionHandler_; scope = $rootScope.$new(); })); afterEach(function() { if ($exceptionHandler.errors.length) { dump(jasmine.getEnv().currentSpec.getFullName()); dump('$exceptionHandler has errors'); dump($exceptionHandler.errors); expect($exceptionHandler.errors).toBe([]); } dealoc(element); }); it('should iterate over an array of objects', function() { element = $compile( '')(scope); Array.prototype.extraProperty = "should be ignored"; // INIT scope.items = [{name: 'misko'}, {name:'shyam'}]; scope.$digest(); expect(element.find('li').length).toEqual(2); expect(element.text()).toEqual('misko;shyam;'); delete Array.prototype.extraProperty; // GROW scope.items.push({name: 'adam'}); scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('misko;shyam;adam;'); // SHRINK scope.items.pop(); scope.items.shift(); scope.$digest(); expect(element.find('li').length).toEqual(1); expect(element.text()).toEqual('shyam;'); }); it('should iterate over an array-like object', function() { element = $compile( '')(scope); document.body.innerHTML = "

" + "a" + "b" + "c" + "

"; var htmlCollection = document.getElementsByTagName('a'); scope.items = htmlCollection; scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('x;y;x;'); }); it('should iterate over on object/map', function() { element = $compile( '')(scope); scope.items = {misko:'swe', shyam:'set'}; scope.$digest(); expect(element.text()).toEqual('misko:swe|shyam:set|'); }); it('should iterate over an object/map with identical values', function() { element = $compile( '')(scope); scope.items = {age:20, wealth:20, prodname: "Bingo", dogname: "Bingo", codename: "20"}; scope.$digest(); expect(element.text()).toEqual('age:20|codename:20|dogname:Bingo|prodname:Bingo|wealth:20|'); }); describe('track by', function() { it('should track using expression function', function() { element = $compile( '')(scope); scope.items = [{id: 'misko'}, {id: 'igor'}]; scope.$digest(); var li0 = element.find('li')[0]; var li1 = element.find('li')[1]; scope.items.push(scope.items.shift()); scope.$digest(); expect(element.find('li')[0]).toBe(li1); expect(element.find('li')[1]).toBe(li0); }); it('should track using build in $id function', function() { element = $compile( '')(scope); scope.items = [{name: 'misko'}, {name: 'igor'}]; scope.$digest(); var li0 = element.find('li')[0]; var li1 = element.find('li')[1]; scope.items.push(scope.items.shift()); scope.$digest(); expect(element.find('li')[0]).toBe(li1); expect(element.find('li')[1]).toBe(li0); }); it('should still filter when track is present', function() { scope.isIgor = function (item) { return item.name === 'igor'; }; element = $compile( '')(scope); scope.items = [{name: 'igor'}, {name: 'misko'}]; scope.$digest(); expect(element.find('li').text()).toBe('igor;'); }); it('should track using provided function when a filter is present', function() { scope.newArray = function (items) { var newArray = []; angular.forEach(items, function (item) { newArray.push({ id: item.id, name: item.name }); }); return newArray; }; element = $compile( '')(scope); scope.items = [ {id: 1, name: 'igor'}, {id: 2, name: 'misko'} ]; scope.$digest(); expect(element.text()).toBe('igor;misko;'); var li0 = element.find('li')[0]; var li1 = element.find('li')[1]; scope.items.push(scope.items.shift()); scope.$digest(); expect(element.find('li')[0]).toBe(li1); expect(element.find('li')[1]).toBe(li0); }); it('should iterate over an array of primitives', function() { element = $compile( '')(scope); Array.prototype.extraProperty = "should be ignored"; // INIT scope.items = [true, true, true]; scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('true;true;true;'); delete Array.prototype.extraProperty; scope.items = [false, true, true]; scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('false;true;true;'); scope.items = [false, true, false]; scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('false;true;false;'); scope.items = [true]; scope.$digest(); expect(element.find('li').length).toEqual(1); expect(element.text()).toEqual('true;'); scope.items = [true, true, false]; scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('true;true;false;'); scope.items = [true, false, false]; scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('true;false;false;'); // string scope.items = ['a', 'a', 'a']; scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('a;a;a;'); scope.items = ['ab', 'a', 'a']; scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('ab;a;a;'); scope.items = ['test']; scope.$digest(); expect(element.find('li').length).toEqual(1); expect(element.text()).toEqual('test;'); scope.items = ['same', 'value']; scope.$digest(); expect(element.find('li').length).toEqual(2); expect(element.text()).toEqual('same;value;'); // number scope.items = [12, 12, 12]; scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('12;12;12;'); scope.items = [53, 12, 27]; scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('53;12;27;'); scope.items = [89]; scope.$digest(); expect(element.find('li').length).toEqual(1); expect(element.text()).toEqual('89;'); scope.items = [89, 23]; scope.$digest(); expect(element.find('li').length).toEqual(2); expect(element.text()).toEqual('89;23;'); }); it('should iterate over object with changing primitive property values', function() { // test for issue #933 element = $compile( '')(scope); scope.items = {misko: true, shyam: true, zhenbo:true}; scope.$digest(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;'); browserTrigger(element.find('input').eq(0), 'click'); expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;'); expect(element.find('input')[0].checked).toBe(false); expect(element.find('input')[1].checked).toBe(true); expect(element.find('input')[2].checked).toBe(true); browserTrigger(element.find('input').eq(0), 'click'); expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;'); expect(element.find('input')[0].checked).toBe(true); expect(element.find('input')[1].checked).toBe(true); expect(element.find('input')[2].checked).toBe(true); browserTrigger(element.find('input').eq(1), 'click'); expect(element.text()).toEqual('misko:true;shyam:false;zhenbo:true;'); expect(element.find('input')[0].checked).toBe(true); expect(element.find('input')[1].checked).toBe(false); expect(element.find('input')[2].checked).toBe(true); scope.items = {misko: false, shyam: true, zhenbo: true}; scope.$digest(); expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;'); expect(element.find('input')[0].checked).toBe(false); expect(element.find('input')[1].checked).toBe(true); expect(element.find('input')[2].checked).toBe(true); }); }); it('should not ngRepeat over parent properties', function() { var Class = function() {}; Class.prototype.abc = function() {}; Class.prototype.value = 'abc'; element = $compile( '')(scope); scope.items = new Class(); scope.items.name = 'value'; scope.$digest(); expect(element.text()).toEqual('name:value;'); }); it('should error on wrong parsing of ngRepeat', function() { element = jqLite(''); $compile(element)(scope); expect($exceptionHandler.errors.shift()[0].message). toBe("[ngRepeat:iexp] Expected expression in form of '_item_ in _collection_[ track by _id_]' but got 'i dont parse'."); }); it("should throw error when left-hand-side of ngRepeat can't be parsed", function() { element = jqLite(''); $compile(element)(scope); expect($exceptionHandler.errors.shift()[0].message). toBe("[ngRepeat:iidexp] '_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got 'i dont parse'."); }); it('should expose iterator offset as $index when iterating over arrays', function() { element = $compile( '')(scope); scope.items = ['misko', 'shyam', 'frodo']; scope.$digest(); expect(element.text()).toEqual('misko:0|shyam:1|frodo:2|'); }); it('should expose iterator offset as $index when iterating over objects', function() { element = $compile( '')(scope); scope.items = {'misko':'m', 'shyam':'s', 'frodo':'f'}; scope.$digest(); expect(element.text()).toEqual('frodo:f:0|misko:m:1|shyam:s:2|'); }); it('should expose iterator position as $first, $middle and $last when iterating over arrays', function() { element = $compile( '')(scope); scope.items = ['misko', 'shyam', 'doug']; scope.$digest(); expect(element.text()). toEqual('misko:true-false-false|shyam:false-true-false|doug:false-false-true|'); scope.items.push('frodo'); scope.$digest(); expect(element.text()). toEqual('misko:true-false-false|' + 'shyam:false-true-false|' + 'doug:false-true-false|' + 'frodo:false-false-true|'); scope.items.pop(); scope.items.pop(); scope.$digest(); expect(element.text()).toEqual('misko:true-false-false|shyam:false-false-true|'); scope.items.pop(); scope.$digest(); expect(element.text()).toEqual('misko:true-false-true|'); }); it('should expose iterator position as $even and $odd when iterating over arrays', function() { element = $compile( '')(scope); scope.items = ['misko', 'shyam', 'doug']; scope.$digest(); expect(element.text()). toEqual('misko:true-false|shyam:false-true|doug:true-false|'); scope.items.push('frodo'); scope.$digest(); expect(element.text()). toBe('misko:true-false|' + 'shyam:false-true|' + 'doug:true-false|' + 'frodo:false-true|'); scope.items.shift(); scope.items.pop(); scope.$digest(); expect(element.text()).toBe('shyam:true-false|doug:false-true|'); }); it('should expose iterator position as $first, $middle and $last when iterating over objects', function() { element = $compile( '')(scope); scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f'}; scope.$digest(); expect(element.text()). toEqual('doug:d:true-false-false|' + 'frodo:f:false-true-false|' + 'misko:m:false-true-false|' + 'shyam:s:false-false-true|'); delete scope.items.doug; delete scope.items.frodo; scope.$digest(); expect(element.text()).toEqual('misko:m:true-false-false|shyam:s:false-false-true|'); delete scope.items.shyam; scope.$digest(); expect(element.text()).toEqual('misko:m:true-false-true|'); }); it('should expose iterator position as $even and $odd when iterating over objects', function() { element = $compile( '')(scope); scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f'}; scope.$digest(); expect(element.text()). toBe('doug:d:true-false|' + 'frodo:f:false-true|' + 'misko:m:true-false|' + 'shyam:s:false-true|'); delete scope.items.frodo; delete scope.items.shyam; scope.$digest(); expect(element.text()).toBe('doug:d:true-false|misko:m:false-true|'); }); it('should calculate $first, $middle and $last when we filter out properties from an obj', function() { element = $compile( '')(scope); scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f', '$toBeFilteredOut': 'xxxx'}; scope.$digest(); expect(element.text()). toEqual('doug:d:true-false-false|' + 'frodo:f:false-true-false|' + 'misko:m:false-true-false|' + 'shyam:s:false-false-true|'); }); it('should calculate $even and $odd when we filter out properties from an obj', function() { element = $compile( '')(scope); scope.items = {'misko':'m', 'shyam':'s', 'doug':'d', 'frodo':'f', '$toBeFilteredOut': 'xxxx'}; scope.$digest(); expect(element.text()). toEqual('doug:d:true-false|' + 'frodo:f:false-true|' + 'misko:m:true-false|' + 'shyam:s:false-true|'); }); it('should ignore $ and $$ properties', function() { element = $compile('')(scope); scope.items = ['a', 'b', 'c']; scope.items.$$hashKey = 'xxx'; scope.items.$root = 'yyy'; scope.$digest(); expect(element.text()).toEqual('a|b|c|'); }); it('should repeat over nested arrays', function() { element = $compile( '')(scope); scope.groups = [['a', 'b'], ['c','d']]; scope.$digest(); expect(element.text()).toEqual('a|b|Xc|d|X'); }); it('should ignore non-array element properties when iterating over an array', function() { element = $compile('')(scope); scope.array = ['a', 'b', 'c']; scope.array.foo = '23'; scope.array.bar = function() {}; scope.$digest(); expect(element.text()).toBe('a|b|c|'); }); it('should iterate over non-existent elements of a sparse array', function() { element = $compile('')(scope); scope.array = ['a', 'b']; scope.array[4] = 'c'; scope.array[6] = 'd'; scope.$digest(); expect(element.text()).toBe('a|b|||c||d|'); }); it('should iterate over all kinds of types', function() { element = $compile('')(scope); scope.array = ['a', 1, null, undefined, {}]; scope.$digest(); expect(element.text()).toMatch(/a\|1\|\|\|\{\s*\}\|/); }); it('should preserve data on move of elements', function() { element = $compile('')(scope); scope.array = ['a', 'b']; scope.$digest(); var lis = element.find('li'); lis.eq(0).data('mark', 'a'); lis.eq(1).data('mark', 'b'); scope.array = ['b', 'a']; scope.$digest(); lis = element.find('li'); expect(lis.eq(0).data('mark')).toEqual('b'); expect(lis.eq(1).data('mark')).toEqual('a'); }); describe('nesting in replaced directive templates', function() { it('should work when placed on a non-root element of attr directive with SYNC replaced template', inject(function($templateCache, $compile, $rootScope) { $compileProvider.directive('rr', function() { return { restrict: 'A', replace: true, template: '
{{i}}|
' }; }); element = jqLite('
{{i}}|
'); $compile(element)($rootScope); $rootScope.$apply(); expect(element.text()).toBe(''); $rootScope.items = [1, 2]; $rootScope.$apply(); expect(element.text()).toBe('1|2|'); expect(sortedHtml(element)).toBe( '
' + '' + '
1|
' + '
2|
' + '
' ); })); it('should work when placed on a non-root element of attr directive with ASYNC replaced template', inject(function($templateCache, $compile, $rootScope) { $compileProvider.directive('rr', function() { return { restrict: 'A', replace: true, templateUrl: 'rr.html' }; }); $templateCache.put('rr.html', '
{{i}}|
'); element = jqLite('
{{i}}|
'); $compile(element)($rootScope); $rootScope.$apply(); expect(element.text()).toBe(''); $rootScope.items = [1, 2]; $rootScope.$apply(); expect(element.text()).toBe('1|2|'); expect(sortedHtml(element)).toBe( '
' + '' + '
1|
' + '
2|
' + '
' ); })); it('should work when placed on a root element of attr directive with SYNC replaced template', inject(function($templateCache, $compile, $rootScope) { $compileProvider.directive('replaceMeWithRepeater', function() { return { replace: true, template: '{{log(i)}}' }; }); element = jqLite(''); $compile(element)($rootScope); expect(element.text()).toBe(''); var logs = []; $rootScope.log = function(t) { logs.push(t); }; // This creates one item, but it has no parent so we can't get to it $rootScope.items = [1, 2]; $rootScope.$apply(); // This cleans up to prevent memory leak $rootScope.items = []; $rootScope.$apply(); expect(angular.mock.dump(element)).toBe(''); expect(logs).toEqual([1, 2, 1, 2]); })); it('should work when placed on a root element of attr directive with ASYNC replaced template', inject(function($templateCache, $compile, $rootScope) { $compileProvider.directive('replaceMeWithRepeater', function() { return { replace: true, templateUrl: 'replace-me-with-repeater.html' }; }); $templateCache.put('replace-me-with-repeater.html', '
{{log(i)}}
'); element = jqLite('--'); $compile(element)($rootScope); expect(element.text()).toBe('--'); var logs = []; $rootScope.log = function(t) { logs.push(t); }; // This creates one item, but it has no parent so we can't get to it $rootScope.items = [1, 2]; $rootScope.$apply(); // This cleans up to prevent memory leak $rootScope.items = []; $rootScope.$apply(); expect(sortedHtml(element)).toBe('--'); expect(logs).toEqual([1, 2, 1, 2]); })); it('should work when placed on a root element of element directive with SYNC replaced template', inject(function($templateCache, $compile, $rootScope) { $compileProvider.directive('replaceMeWithRepeater', function() { return { restrict: 'E', replace: true, template: '
{{i}}
' }; }); element = $compile('
')($rootScope); expect(element.text()).toBe(''); $rootScope.$apply(); expect(element.text()).toBe('123'); })); if (!msie || msie > 8) { // only IE>8 supports element directives it('should work when placed on a root element of element directive with ASYNC replaced template', inject(function($templateCache, $compile, $rootScope) { $compileProvider.directive('replaceMeWithRepeater', function() { return { restrict: 'E', replace: true, templateUrl: 'replace-me-with-repeater.html' }; }); $templateCache.put('replace-me-with-repeater.html', '
{{i}}
'); element = $compile('
')($rootScope); expect(element.text()).toBe(''); $rootScope.$apply(); expect(element.text()).toBe('123'); })); } }); describe('stability', function() { var a, b, c, d, lis; beforeEach(function() { element = $compile( '')(scope); 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 throw error on adding existing duplicates and recover', function() { scope.items = [a, a, a]; scope.$digest(); expect($exceptionHandler.errors.shift().message). toEqual("[ngRepeat:dupes] Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: item in items, Duplicate key: object:003"); // recover scope.items = [a]; scope.$digest(); var newElements = element.find('li'); expect(newElements.length).toEqual(1); expect(newElements[0]).toEqual(lis[0]); scope.items = []; scope.$digest(); newElements = element.find('li'); expect(newElements.length).toEqual(0); }); it('should throw error on new duplicates and recover', function() { scope.items = [d, d, d]; scope.$digest(); expect($exceptionHandler.errors.shift().message). toEqual("[ngRepeat:dupes] Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: item in items, Duplicate key: object:009"); // recover scope.items = [a]; scope.$digest(); var newElements = element.find('li'); expect(newElements.length).toEqual(1); expect(newElements[0]).toEqual(lis[0]); scope.items = []; scope.$digest(); newElements = element.find('li'); expect(newElements.length).toEqual(0); }); 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]); }); it('should reuse elements even when model is composed of primitives', function() { // rebuilding repeater from scratch can be expensive, we should try to avoid it even for // model that is composed of primitives. scope.items = ['hello', 'cau', 'ahoj']; scope.$digest(); lis = element.find('li'); lis[2].id = 'yes'; scope.items = ['ahoj', 'hello', 'cau']; scope.$digest(); var newLis = element.find('li'); expect(newLis.length).toEqual(3); expect(newLis[0]).toEqual(lis[2]); expect(newLis[1]).toEqual(lis[0]); expect(newLis[2]).toEqual(lis[1]); }); }); }); describe('ngRepeat ngAnimate', function() { var vendorPrefix, window; var body, element, $rootElement; function html(html) { $rootElement.html(html); element = $rootElement.children().eq(0); return element; } function applyCSS(element, cssProp, cssValue) { element.css(cssProp, cssValue); element.css(vendorPrefix + cssProp, cssValue); } beforeEach(module(function() { // we need to run animation on attached elements; return function(_$rootElement_) { $rootElement = _$rootElement_; body = jqLite(document.body); body.append($rootElement); }; })); afterEach(function(){ dealoc(body); dealoc(element); }); beforeEach(module(function($animationProvider, $provide) { $provide.value('$window', window = angular.mock.createMockWindow()); return function($sniffer, $animator) { vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; $animator.enabled(true); }; })); it('should fire off the enter animation + add and remove the css classes', inject(function($compile, $rootScope, $sniffer) { element = $compile(html( '
' + '{{ item }}' + '
' ))($rootScope); $rootScope.$digest(); // re-enable the animations; $rootScope.items = ['1','2','3']; $rootScope.$digest(); //if we add the custom css stuff here then it will get picked up before the animation takes place var kids = element.children(); for(var i=0;i
' + '{{ item }}' + '
' ))($rootScope); $rootScope.items = ['1','2','3']; $rootScope.$digest(); //if we add the custom css stuff here then it will get picked up before the animation takes place var kids = element.children(); for(var i=0;i' + '
' + '{{ item }}' + '
' + '' ))($rootScope); $rootScope.items = ['1','2','3']; $rootScope.$digest(); //if we add the custom css stuff here then it will get picked up before the animation takes place var kids = element.children(); for(var i=0;i
' + '{{ item }}' + '
' ))($rootScope); $rootScope.$digest(); // re-enable the animations; $rootScope.items = ['a','b']; $rootScope.$digest(); //if we add the custom css stuff here then it will get picked up before the animation takes place var kids = element.children(); var first = jqLite(kids[0]); var second = jqLite(kids[1]); var cssProp = 'transition'; var cssValue = '0.5s linear all'; applyCSS(first, cssProp, cssValue); applyCSS(second, cssProp, cssValue); if ($sniffer.transitions) { window.setTimeout.expect(1).process(); window.setTimeout.expect(1).process(); window.setTimeout.expect(500).process(); window.setTimeout.expect(500).process(); } else { expect(window.setTimeout.queue).toEqual([]); } })); it('should grow multi-node repeater', inject(function($compile, $rootScope) { $rootScope.show = false; $rootScope.books = [ {title:'T1', description: 'D1'}, {title:'T2', description: 'D2'} ]; element = $compile( '
' + '
{{book.title}}:
' + '
{{book.description}};
' + '
')($rootScope); $rootScope.$digest(); expect(element.text()).toEqual('T1:D1;T2:D2;'); $rootScope.books.push({title:'T3', description: 'D3'}); $rootScope.$digest(); expect(element.text()).toEqual('T1:D1;T2:D2;T3:D3;'); })); });