diff --git a/src/ng/compile.js b/src/ng/compile.js index 41c45f5b0661..4256adcaabc7 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1862,9 +1862,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // register any observers if (!interpolateFn) return; - // TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the - // actual attr value + // initialize attr object so that it's ready in case we need the value for isolate + // scope initialization, otherwise the value would not be available from isolate + // directive's linking fn during linking phase attr[name] = interpolateFn(scope); + ($$observers[name] || ($$observers[name] = [])).$$inter = true; (attr.$$observers && attr.$$observers[name].$$scope || scope). $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { diff --git a/src/ng/directive/ngPluralize.js b/src/ng/directive/ngPluralize.js index 1962d2042def..0585009a5128 100644 --- a/src/ng/directive/ngPluralize.js +++ b/src/ng/directive/ngPluralize.js @@ -204,7 +204,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise, //check it against pluralization rules in $locale service if (!(value in whens)) value = $locale.pluralCat(value - offset); - return whensExpFns[value](scope, element, true); + return whensExpFns[value](scope); } else { return ''; } diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index 628d21776b85..95fe5ad7d088 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -621,7 +621,9 @@ var optionDirective = ['$interpolate', function($interpolate) { if (interpolateFn) { scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) { attr.$set('value', newVal); - if (newVal !== oldVal) selectCtrl.removeOption(oldVal); + if (oldVal !== newVal) { + selectCtrl.removeOption(oldVal); + } selectCtrl.addOption(newVal); }); } else { diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index 6b61e56f2a70..9ac68e8b7727 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -117,40 +117,44 @@ function $InterpolateProvider() { * @returns {function(context)} an interpolation function which is used to compute the * interpolated string. The function has these parameters: * - * * `context`: an object against which any expressions embedded in the strings are evaluated - * against. - * + * - `context`: evaluation context for all expressions embedded in the interpolated text */ function $interpolate(text, mustHaveExpression, trustedContext) { var startIndex, endIndex, index = 0, - parts = [], - length = text.length, + separators = [], + expressions = [], + parseFns = [], + textLength = text.length, hasInterpolation = false, - fn, + hasText = false, exp, - concat = []; + concat = [], + lastValuesCache = { values: {}, results: {}}; - while(index < length) { + while(index < textLength) { if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { - (index != startIndex) && parts.push(text.substring(index, startIndex)); - parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); - fn.exp = exp; + if (index !== startIndex) hasText = true; + separators.push(text.substring(index, startIndex)); + exp = text.substring(startIndex + startSymbolLength, endIndex); + expressions.push(exp); + parseFns.push($parse(exp)); index = endIndex + endSymbolLength; hasInterpolation = true; } else { - // we did not find anything, so we have to add the remainder to the parts array - (index != length) && parts.push(text.substring(index)); - index = length; + // we did not find an interpolation, so we have to add the remainder to the separators array + if (index !== textLength) { + hasText = true; + separators.push(text.substring(index)); + } + break; } } - if (!(length = parts.length)) { - // we added, nothing, must have been an empty string. - parts.push(''); - length = 1; + if (separators.length === expressions.length) { + separators.push(''); } // Concatenating expressions makes it hard to reason about whether some combination of @@ -159,44 +163,92 @@ function $InterpolateProvider() { // that's used is assigned or constructed by some JS code somewhere that is more testable or // make it obvious that you bound the value to some user controlled value. This helps reduce // the load when auditing for XSS issues. - if (trustedContext && parts.length > 1) { + if (trustedContext && hasInterpolation && (hasText || expressions.length > 1)) { throw $interpolateMinErr('noconcat', "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + "interpolations that concatenate multiple expressions when a trusted value is " + "required. See http://docs.angularjs.org/api/ng.$sce", text); } - if (!mustHaveExpression || hasInterpolation) { - concat.length = length; - fn = function(context) { - try { - for(var i = 0, ii = length, part; i} watchExpressions Array of expressions that will be individually + * watched using {@link ng.$rootScope.Scope#$watch $watch()} + * + * @param {function(newValues, oldValues, scope)} listener Callback called whenever the return value of any + * expression in `watchExpressions` changes + * The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching + * those of `watchExpression` + * and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching + * those of `watchExpression` + * The `scope` refers to the current scope. + * + * @returns {function()} Returns a de-registration function for all listeners. + */ + $watchGroup: function(watchExpressions, listener) { + var oldValues = new Array(watchExpressions.length); + var newValues = new Array(watchExpressions.length); + var deregisterFns = []; + var changeCount = 0; + var self = this; + + forEach(watchExpressions, function (expr, i) { + deregisterFns.push(self.$watch(expr, function (value, oldValue) { + newValues[i] = value; + oldValues[i] = oldValue; + changeCount++; + })); + }, this); + + deregisterFns.push(self.$watch(function () {return changeCount;}, function () { + listener(newValues, oldValues, self); + })); + + return function deregisterWatchGroup() { + forEach(deregisterFns, function (fn) { + fn(); + }); + }; + }, + /** * @ngdoc method @@ -756,7 +808,7 @@ function $RootScopeProvider(){ // prevent NPEs since these methods have references to properties we nulled out this.$destroy = this.$digest = this.$apply = noop; - this.$on = this.$watch = function() { return noop; }; + this.$on = this.$watch = this.$watchGroup = function() { return noop; }; }, /** diff --git a/src/ngScenario/Scenario.js b/src/ngScenario/Scenario.js index f323e01d0f5a..bafcba059455 100644 --- a/src/ngScenario/Scenario.js +++ b/src/ngScenario/Scenario.js @@ -302,27 +302,24 @@ _jQuery.fn.bindings = function(windowJquery, bindExp) { selection.each(function() { var element = windowJquery(this), - binding; - if (binding = element.data('$binding')) { - if (typeof binding == 'string') { - if (match(binding)) { - push(element.scope().$eval(binding)); - } - } else { - if (!angular.isArray(binding)) { - binding = [binding]; + bindings; + if (bindings = element.data('$binding')) { + if (!angular.isArray(bindings)) { + bindings = [bindings]; + } + for(var expressions = [], binding, j=0, jj=bindings.length; j{{error.throw()}}', null, true)($rootScope); - var errorLogs = $exceptionHandler.errors; - - $rootScope.error = { - 'throw': function() {throw 'ErrorMsg1';} - }; - $rootScope.$apply(); - - $rootScope.error['throw'] = function() {throw 'MyError';}; - errorLogs.length = 0; - $rootScope.$apply(); - expect(errorLogs.shift().message).toMatch(/^\[\$interpolate:interr\] Can't interpolate: \{\{error.throw\(\)\}\}\nMyError/); - - $rootScope.error['throw'] = function() {return 'ok';}; - $rootScope.$apply(); - expect(errorLogs.length).toBe(0); - }); - }); - it('IfAttrBindingThrowsErrorDecorateTheAttribute', function() { module(function($exceptionHandlerProvider){ $exceptionHandlerProvider.mode('log'); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 2b6a100fdb44..4e6d85c7dc1d 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -2080,6 +2080,8 @@ describe('$compile', function() { it('should set interpolated attrs to initial interpolation value', inject(function($rootScope, $compile) { + // we need the interpolated attributes to be initialized so that linking fn in a component + // can access the value during link $rootScope.whatever = 'test value'; $compile('
')($rootScope); expect(directiveAttrs.someAttr).toBe($rootScope.whatever); diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js index 606123296ddc..864b4b87d06c 100644 --- a/test/ng/interpolateSpec.js +++ b/test/ng/interpolateSpec.js @@ -2,9 +2,15 @@ describe('$interpolate', function() { - it('should return a function when there are no bindings and textOnly is undefined', + it('should return the interpolation object when there are no bindings and textOnly is undefined', inject(function($interpolate) { - expect(typeof $interpolate('some text')).toBe('function'); + var interpolateFn = $interpolate('some text'); + + expect(interpolateFn.exp).toBe('some text'); + expect(interpolateFn.separators).toEqual(['some text']); + expect(interpolateFn.expressions).toEqual([]); + + expect(interpolateFn({})).toBe('some text'); })); @@ -14,63 +20,41 @@ describe('$interpolate', function() { })); it('should suppress falsy objects', inject(function($interpolate) { - expect($interpolate('{{undefined}}')()).toEqual(''); - expect($interpolate('{{undefined+undefined}}')()).toEqual(''); - expect($interpolate('{{null}}')()).toEqual(''); - expect($interpolate('{{a.b}}')()).toEqual(''); + expect($interpolate('{{undefined}}')({})).toEqual(''); + expect($interpolate('{{null}}')({})).toEqual(''); + expect($interpolate('{{a.b}}')({})).toEqual(''); })); it('should jsonify objects', inject(function($interpolate) { - expect($interpolate('{{ {} }}')()).toEqual('{}'); - expect($interpolate('{{ true }}')()).toEqual('true'); - expect($interpolate('{{ false }}')()).toEqual('false'); - })); - - it('should rethrow exceptions', inject(function($interpolate, $rootScope) { - $rootScope.err = function () { - throw new Error('oops'); - }; - expect(function () { - $interpolate('{{err()}}')($rootScope); - }).toThrowMinErr("$interpolate", "interr", "Can't interpolate: {{err()}}\nError: oops"); - })); - - it('should stop interpolation when encountering an exception', inject(function($interpolate, $compile, $rootScope) { - $rootScope.err = function () { - throw new Error('oops'); - }; - var dom = jqLite('
{{1 + 1}}
{{err()}}
{{1 + 2}}
'); - $compile(dom)($rootScope); - expect(function () { - $rootScope.$apply(); - }).toThrowMinErr("$interpolate", "interr", "Can't interpolate: {{err()}}\nError: oops"); - expect(dom[0].innerHTML).toEqual('2'); - expect(dom[1].innerHTML).toEqual('{{err()}}'); - expect(dom[2].innerHTML).toEqual('{{1 + 2}}'); + expect($interpolate('{{ {} }}')({})).toEqual('{}'); + expect($interpolate('{{ true }}')({})).toEqual('true'); + expect($interpolate('{{ false }}')({})).toEqual('false'); })); it('should return interpolation function', inject(function($interpolate, $rootScope) { - $rootScope.name = 'Misko'; - expect($interpolate('Hello {{name}}!')($rootScope)).toEqual('Hello Misko!'); - })); + var interpolateFn = $interpolate('Hello {{name}}!'); + expect(interpolateFn.exp).toBe('Hello {{name}}!'); + expect(interpolateFn.separators).toEqual(['Hello ', '!']); + expect(interpolateFn.expressions).toEqual(['name']); - it('should ignore undefined model', inject(function($interpolate) { - expect($interpolate("Hello {{'World' + foo}}")()).toEqual('Hello World'); + var scope = $rootScope.$new(); + scope.name = 'Bubu'; + + expect(interpolateFn(scope)).toBe('Hello Bubu!'); })); - it('should ignore undefined return value', inject(function($interpolate, $rootScope) { - $rootScope.foo = function() {return undefined}; - expect($interpolate("Hello {{'World' + foo()}}")($rootScope)).toEqual('Hello World'); + it('should ignore undefined model', inject(function($interpolate) { + expect($interpolate("Hello {{'World'}}{{foo}}")({})).toBe('Hello World'); })); describe('interpolating in a trusted context', function() { var sce; beforeEach(function() { - function log() {}; + function log() {} var fakeLog = {log: log, warn: log, info: log, error: log}; module(function($provide, $sceProvider) { $provide.value('$log', fakeLog); @@ -79,24 +63,32 @@ describe('$interpolate', function() { inject(['$sce', function($sce) { sce = $sce; }]); }); - it('should NOT interpolate non-trusted expressions', inject(function($interpolate) { - var foo = "foo"; - expect($interpolate('{{foo}}', true, sce.CSS)({}, {foo: foo})).toEqual(''); + it('should NOT interpolate non-trusted expressions', inject(function($interpolate, $rootScope) { + var scope = $rootScope.$new(); + scope.foo = "foo"; + + expect(function() { + $interpolate('{{foo}}', true, sce.CSS)(scope); + }).toThrowMinErr('$interpolate', 'interr'); })); - it('should NOT interpolate mistyped expressions', inject(function($interpolate) { - var foo = sce.trustAsCss("foo"); - expect($interpolate('{{foo}}', true, sce.HTML)({}, {foo: foo})).toEqual(''); + it('should NOT interpolate mistyped expressions', inject(function($interpolate, $rootScope) { + var scope = $rootScope.$new(); + scope.foo = sce.trustAsCss("foo"); + + expect(function() { + $interpolate('{{foo}}', true, sce.HTML)(scope); + }).toThrowMinErr('$interpolate', 'interr'); })); it('should interpolate trusted expressions in a regular context', inject(function($interpolate) { var foo = sce.trustAsCss("foo"); - expect($interpolate('{{foo}}', true)({foo: foo})).toEqual('foo'); + expect($interpolate('{{foo}}', true)({foo: foo})).toBe('foo'); })); it('should interpolate trusted expressions in a specific trustedContext', inject(function($interpolate) { var foo = sce.trustAsCss("foo"); - expect($interpolate('{{foo}}', true, sce.CSS)({foo: foo})).toEqual('foo'); + expect($interpolate('{{foo}}', true, sce.CSS)({foo: foo})).toBe('foo'); })); // The concatenation of trusted values does not necessarily result in a trusted value. (For @@ -106,8 +98,8 @@ describe('$interpolate', function() { var foo = sce.trustAsCss("foo"); var bar = sce.trustAsCss("bar"); expect(function() { - return $interpolate('{{foo}}{{bar}}', true, sce.CSS)( - {foo: foo, bar: bar}); }).toThrowMinErr( + return $interpolate('{{foo}}{{bar}}', true, sce.CSS)({foo: foo, bar: bar}); + }).toThrowMinErr( "$interpolate", "noconcat", "Error while interpolating: {{foo}}{{bar}}\n" + "Strict Contextual Escaping disallows interpolations that concatenate multiple " + "expressions when a trusted value is required. See " + @@ -123,77 +115,78 @@ describe('$interpolate', function() { })); it('should not get confused with same markers', inject(function($interpolate) { - expect($interpolate('---').parts).toEqual(['---']); - expect($interpolate('----')()).toEqual(''); - expect($interpolate('--1--')()).toEqual('1'); + expect($interpolate('---').separators).toEqual(['---']); + expect($interpolate('---').expressions).toEqual([]); + expect($interpolate('----')({})).toEqual(''); + expect($interpolate('--1--')({})).toEqual('1'); })); }); - describe('parseBindings', function() { it('should Parse Text With No Bindings', inject(function($interpolate) { - var parts = $interpolate("a").parts; - expect(parts.length).toEqual(1); - expect(parts[0]).toEqual("a"); + expect($interpolate("a").separators).toEqual(['a']); + expect($interpolate("a").expressions).toEqual([]); })); it('should Parse Empty Text', inject(function($interpolate) { - var parts = $interpolate("").parts; - expect(parts.length).toEqual(1); - expect(parts[0]).toEqual(""); + expect($interpolate("").separators).toEqual(['']); + expect($interpolate("").expressions).toEqual([]); })); it('should Parse Inner Binding', inject(function($interpolate) { - var parts = $interpolate("a{{b}}C").parts; - expect(parts.length).toEqual(3); - expect(parts[0]).toEqual("a"); - expect(parts[1].exp).toEqual("b"); - expect(parts[1]({b:123})).toEqual(123); - expect(parts[2]).toEqual("C"); + var interpolateFn = $interpolate("a{{b}}C"), + separators = interpolateFn.separators, expressions = interpolateFn.expressions; + expect(separators).toEqual(['a', 'C']); + expect(expressions).toEqual(['b']); + expect(interpolateFn({b: 123})).toEqual('a123C'); })); it('should Parse Ending Binding', inject(function($interpolate) { - var parts = $interpolate("a{{b}}").parts; - expect(parts.length).toEqual(2); - expect(parts[0]).toEqual("a"); - expect(parts[1].exp).toEqual("b"); - expect(parts[1]({b:123})).toEqual(123); + var interpolateFn = $interpolate("a{{b}}"), + separators = interpolateFn.separators, expressions = interpolateFn.expressions; + expect(separators).toEqual(['a', '']); + expect(expressions).toEqual(['b']); + expect(interpolateFn({b: 123})).toEqual('a123'); })); it('should Parse Begging Binding', inject(function($interpolate) { - var parts = $interpolate("{{b}}c").parts; - expect(parts.length).toEqual(2); - expect(parts[0].exp).toEqual("b"); - expect(parts[1]).toEqual("c"); + var interpolateFn = $interpolate("{{b}}c"), + separators = interpolateFn.separators, expressions = interpolateFn.expressions; + expect(separators).toEqual(['', 'c']); + expect(expressions).toEqual(['b']); + expect(interpolateFn({b: 123})).toEqual('123c'); })); it('should Parse Loan Binding', inject(function($interpolate) { - var parts = $interpolate("{{b}}").parts; - expect(parts.length).toEqual(1); - expect(parts[0].exp).toEqual("b"); + var interpolateFn = $interpolate("{{b}}"), + separators = interpolateFn.separators, expressions = interpolateFn.expressions; + expect(separators).toEqual(['', '']); + expect(expressions).toEqual(['b']); + expect(interpolateFn({b: 123})).toEqual('123'); })); it('should Parse Two Bindings', inject(function($interpolate) { - var parts = $interpolate("{{b}}{{c}}").parts; - expect(parts.length).toEqual(2); - expect(parts[0].exp).toEqual("b"); - expect(parts[1].exp).toEqual("c"); + var interpolateFn = $interpolate("{{b}}{{c}}"), + separators = interpolateFn.separators, expressions = interpolateFn.expressions; + expect(separators).toEqual(['', '', '']); + expect(expressions).toEqual(['b', 'c']); + expect(interpolateFn({b: 111, c: 222})).toEqual('111222'); })); it('should Parse Two Bindings With Text In Middle', inject(function($interpolate) { - var parts = $interpolate("{{b}}x{{c}}").parts; - expect(parts.length).toEqual(3); - expect(parts[0].exp).toEqual("b"); - expect(parts[1]).toEqual("x"); - expect(parts[2].exp).toEqual("c"); + var interpolateFn = $interpolate("{{b}}x{{c}}"), + separators = interpolateFn.separators, expressions = interpolateFn.expressions; + expect(separators).toEqual(['', 'x', '']); + expect(expressions).toEqual(['b', 'c']); + expect(interpolateFn({b: 111, c: 222})).toEqual('111x222'); })); it('should Parse Multiline', inject(function($interpolate) { - var parts = $interpolate('"X\nY{{A\n+B}}C\nD"').parts; - expect(parts.length).toEqual(3); - expect(parts[0]).toEqual('"X\nY'); - expect(parts[1].exp).toEqual('A\n+B'); - expect(parts[2]).toEqual('C\nD"'); + var interpolateFn = $interpolate('"X\nY{{A\n+B}}C\nD"'), + separators = interpolateFn.separators, expressions = interpolateFn.expressions; + expect(separators).toEqual(['"X\nY', 'C\nD"']); + expect(expressions).toEqual(['A\n+B']); + expect(interpolateFn({'A': 'aa', 'B': 'bb'})).toEqual('"X\nYaabbC\nD"'); })); }); @@ -207,6 +200,12 @@ describe('$interpolate', function() { "$interpolate", "noconcat", "Error while interpolating: constant/{{var}}\nStrict " + "Contextual Escaping disallows interpolations that concatenate multiple expressions " + "when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce"); + expect(function() { + $interpolate('{{var}}/constant', true, isTrustedContext); + }).toThrowMinErr( + "$interpolate", "noconcat", "Error while interpolating: {{var}}/constant\nStrict " + + "Contextual Escaping disallows interpolations that concatenate multiple expressions " + + "when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce"); expect(function() { $interpolate('{{foo}}{{bar}}', true, isTrustedContext); }).toThrowMinErr( @@ -216,7 +215,7 @@ describe('$interpolate', function() { })); it('should interpolate a multi-part expression when isTrustedContext is false', inject(function($interpolate) { - expect($interpolate('some/{{id}}')()).toEqual('some/'); + expect($interpolate('some/{{id}}')({})).toEqual('some/'); expect($interpolate('some/{{id}}')({id: 1})).toEqual('some/1'); expect($interpolate('{{foo}}{{bar}}')({foo: 1, bar: 2})).toEqual('12'); })); @@ -248,9 +247,10 @@ describe('$interpolate', function() { }); inject(function($interpolate) { - expect($interpolate('---').parts).toEqual(['---']); - expect($interpolate('----')()).toEqual(''); - expect($interpolate('--1--')()).toEqual('1'); + expect($interpolate('---').separators).toEqual(['---']); + expect($interpolate('---').expressions).toEqual([]); + expect($interpolate('----')({})).toEqual(''); + expect($interpolate('--1--')({})).toEqual('1'); }); }); }); diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index a76fa86b6de4..cf6d639bb480 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -1045,7 +1045,7 @@ describe('parser', function() { })); }); - describe('nulls in expressions', function() { + describe('null/undefined in expressions', function() { // simpleGetterFn1 it('should return null for `a` where `a` is null', inject(function($rootScope) { $rootScope.a = null; @@ -1092,6 +1092,19 @@ describe('parser', function() { $rootScope.a = { b: { c: { d: { e: { f: null } } } } }; expect($rootScope.$eval('a.b.c.d.e.f.g')).toBeUndefined(); })); + + + it('should return undefined if the return value of a function invocation is undefined', + inject(function($rootScope) { + $rootScope.fn = function() {}; + expect($rootScope.$eval('fn()')).toBeUndefined(); + })); + + it('should ignore undefined values when doing addition/concatenation', + inject(function($rootScope) { + $rootScope.fn = function() {}; + expect($rootScope.$eval('foo + "bar" + fn()')).toBe('bar'); + })); }); }); }); diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 01b5d1bdfdd4..13efd5d9110e 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -777,6 +777,75 @@ describe('Scope', function() { }); }); + describe('$watchGroup', function() { + var scope; + var log; + + beforeEach(inject(function($rootScope, _log_) { + scope = $rootScope.$new(); + log = _log_; + })); + + + it('should work for a group with just a single expression', function() { + scope.$watchGroup(['a'], function(values, oldValues, s) { + expect(s).toBe(scope); + log(oldValues + ' >>> ' + values); + }); + + scope.a = 'foo'; + scope.$digest(); + expect(log).toEqual('foo >>> foo'); + + log.reset(); + scope.$digest(); + expect(log).toEqual(''); + + scope.a = 'bar'; + scope.$digest(); + expect(log).toEqual('foo >>> bar'); + }); + + + it('should detect a change to any one expression in the group', function() { + scope.$watchGroup(['a', 'b'], function(values, oldValues, s) { + expect(s).toBe(scope); + log(oldValues + ' >>> ' + values); + }); + + scope.a = 'foo'; + scope.b = 'bar'; + scope.$digest(); + expect(log).toEqual('foo,bar >>> foo,bar'); + + log.reset(); + scope.$digest(); + expect(log).toEqual(''); + + scope.a = 'a'; + scope.$digest(); + expect(log).toEqual('foo,bar >>> a,bar'); + + log.reset(); + scope.a = 'A'; + scope.b = 'B'; + scope.$digest(); + expect(log).toEqual('a,bar >>> A,B'); + }); + + + it('should not call watch action fn when watchGroup was deregistered', function() { + var deregister = scope.$watchGroup(['a', 'b'], function(values, oldValues) { + log(oldValues + ' >>> ' + values); + }); + + deregister(); + scope.a = 'xxx'; + scope.b = 'yyy'; + scope.$digest(); + expect(log).toEqual(''); + }); + }); describe('$destroy', function() { var first = null, middle = null, last = null, log = null;