'use strict'; describe('ngRepeat', function() { var element, $compile, scope, $exceptionHandler; 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|'); }); 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 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("[NgErr7] ngRepeat error! 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("[NgErr8] ngRepeat error! '_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 $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 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 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(); var lis = element.find('li'); expect(lis.eq(0).data('mark')).toEqual('b'); expect(lis.eq(1).data('mark')).toEqual('a'); }); 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("[NgErr50] ngRepeat error! 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(); var 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("[NgErr50] ngRepeat error! 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(); var 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; function html(html) { body.html(html); element = body.children().eq(0); return element; } function applyCSS(element, cssProp, cssValue) { element.css(cssProp, cssValue); element.css(vendorPrefix + cssProp, cssValue); } beforeEach(function() { // we need to run animation on attached elements; body = jqLite(document.body); }); 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([]); } })); });