From 61f2767ce65562257599649d9eaf9da08f321655 Mon Sep 17 00:00:00 2001
From: Misko Hevery
Date: Tue, 19 Mar 2013 22:27:27 -0700
Subject: feat(ngRepeat): add support for custom tracking of items
BREAKING CHANGE:
It is considered an error to have two items produce
the same track by key. (This was tolerated before.)
---
test/ApiSpecs.js | 37 -----
test/ng/directive/ngClassSpec.js | 2 +-
test/ng/directive/ngRepeatSpec.js | 322 ++++++++++++++++++++------------------
3 files changed, 175 insertions(+), 186 deletions(-)
(limited to 'test')
diff --git a/test/ApiSpecs.js b/test/ApiSpecs.js
index 1e52cf44..12de39d0 100644
--- a/test/ApiSpecs.js
+++ b/test/ApiSpecs.js
@@ -23,42 +23,5 @@ describe('api', function() {
expect(map.get('c')).toBe(undefined);
});
});
-
-
- 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.peek('key')).toEqual('a');
- expect(map[hashKey('key')]).toEqual(['a', 'b']);
- expect(map.shift('key')).toEqual('a');
- expect(map.peek('key')).toEqual('b');
- expect(map[hashKey('key')]).toEqual(['b']);
- expect(map.shift('key')).toEqual('b');
- expect(map.shift('key')).toEqual(undefined);
- expect(map[hashKey('key')]).toEqual(undefined);
- });
-
- it('should support primitive and object keys', function() {
- var obj1 = {},
- obj2 = {};
-
- var map = new HashQueueMap();
- map.push(obj1, 'a1');
- map.push(obj1, 'a2');
- map.push(obj2, 'b');
- map.push(1, 'c');
- map.push(undefined, 'd');
- map.push(null, 'e');
-
- expect(map[hashKey(obj1)]).toEqual(['a1', 'a2']);
- expect(map[hashKey(obj2)]).toEqual(['b']);
- expect(map[hashKey(1)]).toEqual(['c']);
- expect(map[hashKey(undefined)]).toEqual(['d']);
- expect(map[hashKey(null)]).toEqual(['e']);
- });
- });
});
diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js
index 69afef7a..d4bd76fe 100644
--- a/test/ng/directive/ngClassSpec.js
+++ b/test/ng/directive/ngClassSpec.js
@@ -249,7 +249,7 @@ describe('ngClass', function() {
it('should update ngClassOdd/Even when model is changed by filtering', inject(function($rootScope, $compile) {
element = $compile('
' +
- '' +
'')($rootScope);
$rootScope.items = ['a','b','a'];
diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js
index 33e4dcfd..44406d6d 100644
--- a/test/ng/directive/ngRepeatSpec.js
+++ b/test/ng/directive/ngRepeatSpec.js
@@ -1,16 +1,27 @@
'use strict';
describe('ngRepeat', function() {
- var element, $compile, scope;
+ var element, $compile, scope, $exceptionHandler;
- beforeEach(inject(function(_$compile_, $rootScope) {
+ beforeEach(module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ }));
+
+ beforeEach(inject(function(_$compile_, $rootScope, _$exceptionHandler_) {
$compile = _$compile_;
+ $exceptionHandler = _$exceptionHandler_;
scope = $rootScope.$new();
}));
- afterEach(function(){
+ afterEach(function() {
+ if ($exceptionHandler.errors.length) {
+ dump(jasmine.getEnv().currentSpec.getFullName());
+ dump('$exceptionHandler has errors');
+ dump($exceptionHandler.errors);
+ expect($exceptionHandler.errors).toBe([]);
+ }
dealoc(element);
});
@@ -44,141 +55,177 @@ describe('ngRepeat', function() {
});
- it('should iterate over an array of primitives', function() {
+ it('should iterate over on object/map', function() {
element = $compile(
'' +
- '- {{item}};
' +
+ '- {{key}}:{{value}}|
' +
'
')(scope);
-
- Array.prototype.extraProperty = "should be ignored";
- // INIT
- scope.items = [true, true, true];
+ scope.items = {misko:'swe', shyam:'set'};
scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('true;true;true;');
- delete Array.prototype.extraProperty;
+ expect(element.text()).toEqual('misko:swe|shyam:set|');
+ });
- 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;');
+ describe('track by', function() {
+ it('should track using expression function', function() {
+ element = $compile(
+ '' +
+ '- {{item.name}};
' +
+ '
')(scope);
+ scope.items = [{id: 'misko'}, {id: 'igor'}];
+ scope.$digest();
+ var li0 = element.find('li')[0];
+ var li1 = element.find('li')[1];
- scope.items = [true];
- scope.$digest();
- expect(element.find('li').length).toEqual(1);
- expect(element.text()).toEqual('true;');
+ scope.items.push(scope.items.shift());
+ scope.$digest();
+ expect(element.find('li')[0]).toBe(li1);
+ expect(element.find('li')[1]).toBe(li0);
+ });
- 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;');
+ it('should track using build in $id function', function() {
+ element = $compile(
+ '' +
+ '- {{item.name}};
' +
+ '
')(scope);
+ scope.items = [{name: 'misko'}, {name: 'igor'}];
+ scope.$digest();
+ var li0 = element.find('li')[0];
+ var li1 = element.find('li')[1];
- // string
- scope.items = ['a', 'a', 'a'];
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('a;a;a;');
+ scope.items.push(scope.items.shift());
+ scope.$digest();
+ expect(element.find('li')[0]).toBe(li1);
+ expect(element.find('li')[1]).toBe(li0);
+ });
- 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;');
+ it('should iterate over an array of primitives', function() {
+ element = $compile(
+ '')(scope);
- scope.items = ['same', 'value'];
- scope.$digest();
- expect(element.find('li').length).toEqual(2);
- expect(element.text()).toEqual('same;value;');
+ 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;
- // number
- scope.items = [12, 12, 12];
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('12;12;12;');
+ scope.items = [false, true, true];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('false;true;true;');
- scope.items = [53, 12, 27];
- scope.$digest();
- expect(element.find('li').length).toEqual(3);
- expect(element.text()).toEqual('53;12;27;');
+ scope.items = [false, true, false];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('false;true;false;');
- scope.items = [89];
- scope.$digest();
- expect(element.find('li').length).toEqual(1);
- expect(element.text()).toEqual('89;');
+ scope.items = [true];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(1);
+ expect(element.text()).toEqual('true;');
- scope.items = [89, 23];
- scope.$digest();
- expect(element.find('li').length).toEqual(2);
- expect(element.text()).toEqual('89;23;');
- });
+ 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;');
- it('should iterate over on object/map', function() {
- element = $compile(
- '' +
- '- {{key}}:{{value}}|
' +
- '
')(scope);
- scope.items = {misko:'swe', shyam:'set'};
- scope.$digest();
- expect(element.text()).toEqual('misko:swe|shyam:set|');
- });
+ // 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;');
- it('should iterate over object with changing primitive property values', function() {
- // test for issue #933
+ scope.items = ['test'];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(1);
+ expect(element.text()).toEqual('test;');
- element = $compile(
- '')(scope);
+ scope.items = ['same', 'value'];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(2);
+ expect(element.text()).toEqual('same;value;');
- 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;');
+ // number
+ scope.items = [12, 12, 12];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('12;12;12;');
- browserTrigger(element.find('input').eq(0), 'click');
+ scope.items = [53, 12, 27];
+ scope.$digest();
+ expect(element.find('li').length).toEqual(3);
+ expect(element.text()).toEqual('53;12;27;');
- 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);
+ 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;');
+ });
- 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);
+ it('should iterate over object with changing primitive property values', function() {
+ // test for issue #933
- 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);
+ 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);
+ });
});
@@ -199,19 +246,18 @@ describe('ngRepeat', function() {
it('should error on wrong parsing of ngRepeat', function() {
- expect(function() {
- element = jqLite('');
- $compile(element)(scope);
- }).toThrow("Expected ngRepeat in form of '_item_ in _collection_' but got 'i dont parse'.");
+ element = jqLite('');
+ $compile(element)(scope);
+ expect($exceptionHandler.errors.shift()[0].message).
+ toBe("Expected ngRepeat 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() {
- expect(function() {
element = jqLite('');
$compile(element)(scope);
- }).toThrow("'item' in 'item in collection' should be identifier or (key, value) but got " +
- "'i dont parse'.");
+ expect($exceptionHandler.errors.shift()[0].message).
+ toBe("'item' in 'item in collection' should be identifier or (key, value) but got 'i dont parse'.");
});
@@ -311,7 +357,7 @@ describe('ngRepeat', function() {
it('should ignore $ and $$ properties', function() {
element = $compile('')(scope);
scope.items = ['a', 'b', 'c'];
- scope.items.$$hashkey = 'xxx';
+ scope.items.$$hashKey = 'xxx';
scope.items.$root = 'yyy';
scope.$digest();
@@ -393,43 +439,23 @@ describe('ngRepeat', function() {
});
- 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;
+ it('should throw error on duplicates and recover', function() {
+ scope.items = [a, a, a];
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]);
+ expect($exceptionHandler.errors.shift().message).
+ toEqual('Duplicates in a repeater are not allowed. Repeater: item in items');
+ // recover
+ scope.items = [a];
scope.$digest();
- newElements = element.find('li');
+ var newElements = element.find('li');
+ expect(newElements.length).toEqual(1);
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.items = [];
scope.$digest();
var newElements = element.find('li');
- expect(newElements.length).toEqual(2);
- expect(newElements[0]).toEqual(lis[0]);
- expect(newElements[1]).toEqual(lis[1]);
+ expect(newElements.length).toEqual(0);
});
--
cgit v1.2.3