From e0418ebc60302904b7eb198151c3f7bedf36b995 Mon Sep 17 00:00:00 2001 From: rodyhaddad Date: Thu, 22 May 2014 08:43:24 -0700 Subject: [PATCH] fix($parse): remove deprecated promise unwrapping The feature has been deprecated in #4317 BREAKING CHANGE: promise unwrapping has been removed. It has been deprecated since 1.2.0-rc.3. It can no longer be turned on. Two methods have been removed: * $parseProvider.unwrapPromises * $parseProvider.logPromiseWarnings --- src/ng/parse.js | 244 +----- test/ng/parseSpec.js | 1910 +++++++++++++++++------------------------- 2 files changed, 781 insertions(+), 1373 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 51eed0376fa7..0aa57a3351c6 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1,8 +1,6 @@ 'use strict'; var $parseMinErr = minErr('$parse'); -var promiseWarningCache = {}; -var promiseWarning; // Sandboxing Angular Expressions // ------------------------------ @@ -700,14 +698,6 @@ Parser.prototype = { if (!o) return undefined; v = ensureSafeObject(o[i], parser.text); - if (v && v.then && parser.options.unwrapPromises) { - p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; - } return v; }, { assign: function(self, value, locals) { @@ -835,18 +825,6 @@ function setter(obj, path, setValue, fullExp, options) { obj[key] = propertyObj; } obj = propertyObj; - if (obj.then && options.unwrapPromises) { - promiseWarning(fullExp); - if (!("$$v" in obj)) { - (function(promise) { - promise.then(function(val) { promise.$$v = val; }); } - )(obj); - } - if (obj.$$v === undefined) { - obj.$$v = {}; - } - obj = obj.$$v; - } } key = ensureSafeMemberName(element.shift(), fullExp); obj[key] = setValue; @@ -867,101 +845,30 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) { ensureSafeMemberName(key3, fullExp); ensureSafeMemberName(key4, fullExp); - return !options.unwrapPromises - ? function cspSafeGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - - if (pathVal == null) return pathVal; - pathVal = pathVal[key0]; + return function cspSafeGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; + if (pathVal == null) return pathVal; + pathVal = pathVal[key0]; - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; + if (!key1) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key1]; - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; + if (!key2) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key2]; - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; + if (!key3) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key3]; - return pathVal; - } - : function cspSafePromiseEnabledGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, - promise; - - if (pathVal == null) return pathVal; - - pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } + if (!key4) return pathVal; + if (pathVal == null) return undefined; + pathVal = pathVal[key4]; - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - return pathVal; - }; + return pathVal; + }; } function simpleGetterFn1(key0, fullExp) { @@ -998,9 +905,9 @@ function getterFn(path, options, fullExp) { // When we have only 1 or 2 tokens, use optimized special case closures. // http://jsperf.com/angularjs-parse-getter/6 - if (!options.unwrapPromises && pathKeysLength === 1) { + if (pathKeysLength === 1) { fn = simpleGetterFn1(pathKeys[0], fullExp); - } else if (!options.unwrapPromises && pathKeysLength === 2) { + } else if (pathKeysLength === 2) { fn = simpleGetterFn2(pathKeys[0], pathKeys[1], fullExp); } else if (options.csp) { if (pathKeysLength < 6) { @@ -1028,28 +935,15 @@ function getterFn(path, options, fullExp) { // we simply dereference 's' on any .dot notation ? 's' // but if we are first then we check locals first, and if so read it first - : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + - (options.unwrapPromises - ? 'if (s && s.then) {\n' + - ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' + - ' if (!("$$v" in s)) {\n' + - ' p=s;\n' + - ' p.$$v = undefined;\n' + - ' p.then(function(v) {p.$$v=v;});\n' + - '}\n' + - ' s=s.$$v\n' + - '}\n' - : ''); + : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n'; }); code += 'return s;'; /* jshint -W054 */ - var evaledFnGetter = new Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning + var evaledFnGetter = new Function('s', 'k', code); // s=scope, k=locals /* jshint +W054 */ evaledFnGetter.toString = valueFn(code); - fn = options.unwrapPromises ? function(scope, locals) { - return evaledFnGetter(scope, locals, promiseWarning); - } : evaledFnGetter; + fn = evaledFnGetter; } // Only cache the value if it's not going to mess up the cache object @@ -1116,103 +1010,13 @@ function $ParseProvider() { var cache = {}; var $parseOptions = { - csp: false, - unwrapPromises: false, - logPromiseWarnings: true - }; - - - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name $parseProvider#unwrapPromises - * @description - * - * **This feature is deprecated, see deprecation notes below for more info** - * - * If set to true (default is false), $parse will unwrap promises automatically when a promise is - * found at any part of the expression. In other words, if set to true, the expression will always - * result in a non-promise value. - * - * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, - * the fulfillment value is used in place of the promise while evaluating the expression. - * - * **Deprecation notice** - * - * This is a feature that didn't prove to be wildly useful or popular, primarily because of the - * dichotomy between data access in templates (accessed as raw values) and controller code - * (accessed as promises). - * - * In most code we ended up resolving promises manually in controllers anyway and thus unifying - * the model access there. - * - * Other downsides of automatic promise unwrapping: - * - * - when building components it's often desirable to receive the raw promises - * - adds complexity and slows down expression evaluation - * - makes expression code pre-generation unattractive due to the amount of code that needs to be - * generated - * - makes IDE auto-completion and tool support hard - * - * **Warning Logs** - * - * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a - * promise (to reduce the noise, each expression is logged only once). To disable this logging use - * `$parseProvider.logPromiseWarnings(false)` api. - * - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.unwrapPromises = function(value) { - if (isDefined(value)) { - $parseOptions.unwrapPromises = !!value; - return this; - } else { - return $parseOptions.unwrapPromises; - } - }; - - - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name $parseProvider#logPromiseWarnings - * @description - * - * Controls whether Angular should log a warning on any encounter of a promise in an expression. - * - * The default is set to `true`. - * - * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well. - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.logPromiseWarnings = function(value) { - if (isDefined(value)) { - $parseOptions.logPromiseWarnings = value; - return this; - } else { - return $parseOptions.logPromiseWarnings; - } + csp: false }; this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { $parseOptions.csp = $sniffer.csp; - promiseWarning = function promiseWarningFn(fullExp) { - if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return; - promiseWarningCache[fullExp] = true; - $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' + - 'Automatic unwrapping of promises in Angular expressions is deprecated.'); - }; - return function(exp) { var parsedExpression; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 24766c687203..154d2bc23ca5 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -3,10 +3,9 @@ describe('parser', function() { beforeEach(function() { - /* global getterFnCache: true, promiseWarningCache: true */ - // clear caches + /* global getterFnCache: true */ + // clear cache getterFnCache = {}; - promiseWarningCache = {}; }); @@ -16,7 +15,7 @@ describe('parser', function() { beforeEach(function () { /* global Lexer: false */ lex = function () { - var lexer = new Lexer({csp: false, unwrapPromises: false}); + var lexer = new Lexer({csp: false}); return lexer.lex.apply(lexer, arguments); }; }); @@ -202,1303 +201,908 @@ describe('parser', function() { forEach([true, false], function(cspEnabled) { - forEach([true, false], function(unwrapPromisesEnabled) { + describe('csp: ' + cspEnabled, function() { - describe('csp: ' + cspEnabled + ", unwrapPromises: " + unwrapPromisesEnabled, function() { + var originalSecurityPolicy; - var originalSecurityPolicy; + beforeEach(function() { + originalSecurityPolicy = window.document.securityPolicy; + window.document.securityPolicy = {isActive : cspEnabled}; + }); - beforeEach(function() { - originalSecurityPolicy = window.document.securityPolicy; - window.document.securityPolicy = {isActive : cspEnabled}; - }); - - afterEach(function() { - window.document.securityPolicy = originalSecurityPolicy; - }); - - beforeEach(module(function ($parseProvider, $provide) { - $parseProvider.unwrapPromises(unwrapPromisesEnabled); - })); + afterEach(function() { + window.document.securityPolicy = originalSecurityPolicy; + }); - beforeEach(inject(function ($rootScope) { - scope = $rootScope; - })); + beforeEach(inject(function ($rootScope) { + scope = $rootScope; + })); - it('should parse expressions', function() { - /*jshint -W006, -W007 */ - expect(scope.$eval("-1")).toEqual(-1); - expect(scope.$eval("1 + 2.5")).toEqual(3.5); - expect(scope.$eval("1 + -2.5")).toEqual(-1.5); - expect(scope.$eval("1+2*3/4")).toEqual(1+2*3/4); - expect(scope.$eval("0--1+1.5")).toEqual(0- -1 + 1.5); - expect(scope.$eval("-0--1++2*-3/-4")).toEqual(-0- -1+ +2*-3/-4); - expect(scope.$eval("1/2*3")).toEqual(1/2*3); - }); + it('should parse expressions', function() { + /*jshint -W006, -W007 */ + expect(scope.$eval("-1")).toEqual(-1); + expect(scope.$eval("1 + 2.5")).toEqual(3.5); + expect(scope.$eval("1 + -2.5")).toEqual(-1.5); + expect(scope.$eval("1+2*3/4")).toEqual(1+2*3/4); + expect(scope.$eval("0--1+1.5")).toEqual(0- -1 + 1.5); + expect(scope.$eval("-0--1++2*-3/-4")).toEqual(-0- -1+ +2*-3/-4); + expect(scope.$eval("1/2*3")).toEqual(1/2*3); + }); - it('should parse comparison', function() { - /* jshint -W041 */ - expect(scope.$eval("false")).toBeFalsy(); - expect(scope.$eval("!true")).toBeFalsy(); - expect(scope.$eval("1==1")).toBeTruthy(); - expect(scope.$eval("1==true")).toBeTruthy(); - expect(scope.$eval("1===1")).toBeTruthy(); - expect(scope.$eval("1==='1'")).toBeFalsy(); - expect(scope.$eval("1===true")).toBeFalsy(); - expect(scope.$eval("'true'===true")).toBeFalsy(); - expect(scope.$eval("1!==2")).toBeTruthy(); - expect(scope.$eval("1!=='1'")).toBeTruthy(); - expect(scope.$eval("1!=2")).toBeTruthy(); - expect(scope.$eval("1<2")).toBeTruthy(); - expect(scope.$eval("1<=1")).toBeTruthy(); - expect(scope.$eval("1>2")).toEqual(1>2); - expect(scope.$eval("2>=1")).toEqual(2>=1); - expect(scope.$eval("true==2<3")).toEqual(true == 2<3); - expect(scope.$eval("true===2<3")).toEqual(true === 2<3); - }); + it('should parse comparison', function() { + /* jshint -W041 */ + expect(scope.$eval("false")).toBeFalsy(); + expect(scope.$eval("!true")).toBeFalsy(); + expect(scope.$eval("1==1")).toBeTruthy(); + expect(scope.$eval("1==true")).toBeTruthy(); + expect(scope.$eval("1===1")).toBeTruthy(); + expect(scope.$eval("1==='1'")).toBeFalsy(); + expect(scope.$eval("1===true")).toBeFalsy(); + expect(scope.$eval("'true'===true")).toBeFalsy(); + expect(scope.$eval("1!==2")).toBeTruthy(); + expect(scope.$eval("1!=='1'")).toBeTruthy(); + expect(scope.$eval("1!=2")).toBeTruthy(); + expect(scope.$eval("1<2")).toBeTruthy(); + expect(scope.$eval("1<=1")).toBeTruthy(); + expect(scope.$eval("1>2")).toEqual(1>2); + expect(scope.$eval("2>=1")).toEqual(2>=1); + expect(scope.$eval("true==2<3")).toEqual(true == 2<3); + expect(scope.$eval("true===2<3")).toEqual(true === 2<3); + }); - it('should parse logical', function() { - expect(scope.$eval("0&&2")).toEqual(0&&2); - expect(scope.$eval("0||2")).toEqual(0||2); - expect(scope.$eval("0||1&&2")).toEqual(0||1&&2); - }); + it('should parse logical', function() { + expect(scope.$eval("0&&2")).toEqual(0&&2); + expect(scope.$eval("0||2")).toEqual(0||2); + expect(scope.$eval("0||1&&2")).toEqual(0||1&&2); + }); - it('should parse ternary', function(){ - var returnTrue = scope.returnTrue = function(){ return true; }; - var returnFalse = scope.returnFalse = function(){ return false; }; - var returnString = scope.returnString = function(){ return 'asd'; }; - var returnInt = scope.returnInt = function(){ return 123; }; - var identity = scope.identity = function(x){ return x; }; - - // Simple. - expect(scope.$eval('0?0:2')).toEqual(0?0:2); - expect(scope.$eval('1?0:2')).toEqual(1?0:2); - - // Nested on the left. - expect(scope.$eval('0?0?0:0:2')).toEqual(0?0?0:0:2); - expect(scope.$eval('1?0?0:0:2')).toEqual(1?0?0:0:2); - expect(scope.$eval('0?1?0:0:2')).toEqual(0?1?0:0:2); - expect(scope.$eval('0?0?1:0:2')).toEqual(0?0?1:0:2); - expect(scope.$eval('0?0?0:2:3')).toEqual(0?0?0:2:3); - expect(scope.$eval('1?1?0:0:2')).toEqual(1?1?0:0:2); - expect(scope.$eval('1?1?1:0:2')).toEqual(1?1?1:0:2); - expect(scope.$eval('1?1?1:2:3')).toEqual(1?1?1:2:3); - expect(scope.$eval('1?1?1:2:3')).toEqual(1?1?1:2:3); - - // Nested on the right. - expect(scope.$eval('0?0:0?0:2')).toEqual(0?0:0?0:2); - expect(scope.$eval('1?0:0?0:2')).toEqual(1?0:0?0:2); - expect(scope.$eval('0?1:0?0:2')).toEqual(0?1:0?0:2); - expect(scope.$eval('0?0:1?0:2')).toEqual(0?0:1?0:2); - expect(scope.$eval('0?0:0?2:3')).toEqual(0?0:0?2:3); - expect(scope.$eval('1?1:0?0:2')).toEqual(1?1:0?0:2); - expect(scope.$eval('1?1:1?0:2')).toEqual(1?1:1?0:2); - expect(scope.$eval('1?1:1?2:3')).toEqual(1?1:1?2:3); - expect(scope.$eval('1?1:1?2:3')).toEqual(1?1:1?2:3); - - // Precedence with respect to logical operators. - expect(scope.$eval('0&&1?0:1')).toEqual(0&&1?0:1); - expect(scope.$eval('1||0?0:0')).toEqual(1||0?0:0); - - expect(scope.$eval('0?0&&1:2')).toEqual(0?0&&1:2); - expect(scope.$eval('0?1&&1:2')).toEqual(0?1&&1:2); - expect(scope.$eval('0?0||0:1')).toEqual(0?0||0:1); - expect(scope.$eval('0?0||1:2')).toEqual(0?0||1:2); - - expect(scope.$eval('1?0&&1:2')).toEqual(1?0&&1:2); - expect(scope.$eval('1?1&&1:2')).toEqual(1?1&&1:2); - expect(scope.$eval('1?0||0:1')).toEqual(1?0||0:1); - expect(scope.$eval('1?0||1:2')).toEqual(1?0||1:2); - - expect(scope.$eval('0?1:0&&1')).toEqual(0?1:0&&1); - expect(scope.$eval('0?2:1&&1')).toEqual(0?2:1&&1); - expect(scope.$eval('0?1:0||0')).toEqual(0?1:0||0); - expect(scope.$eval('0?2:0||1')).toEqual(0?2:0||1); - - expect(scope.$eval('1?1:0&&1')).toEqual(1?1:0&&1); - expect(scope.$eval('1?2:1&&1')).toEqual(1?2:1&&1); - expect(scope.$eval('1?1:0||0')).toEqual(1?1:0||0); - expect(scope.$eval('1?2:0||1')).toEqual(1?2:0||1); - - // Function calls. - expect(scope.$eval('returnTrue() ? returnString() : returnInt()')).toEqual(returnTrue() ? returnString() : returnInt()); - expect(scope.$eval('returnFalse() ? returnString() : returnInt()')).toEqual(returnFalse() ? returnString() : returnInt()); - expect(scope.$eval('returnTrue() ? returnString() : returnInt()')).toEqual(returnTrue() ? returnString() : returnInt()); - expect(scope.$eval('identity(returnFalse() ? returnString() : returnInt())')).toEqual(identity(returnFalse() ? returnString() : returnInt())); - }); + it('should parse ternary', function(){ + var returnTrue = scope.returnTrue = function(){ return true; }; + var returnFalse = scope.returnFalse = function(){ return false; }; + var returnString = scope.returnString = function(){ return 'asd'; }; + var returnInt = scope.returnInt = function(){ return 123; }; + var identity = scope.identity = function(x){ return x; }; + + // Simple. + expect(scope.$eval('0?0:2')).toEqual(0?0:2); + expect(scope.$eval('1?0:2')).toEqual(1?0:2); + + // Nested on the left. + expect(scope.$eval('0?0?0:0:2')).toEqual(0?0?0:0:2); + expect(scope.$eval('1?0?0:0:2')).toEqual(1?0?0:0:2); + expect(scope.$eval('0?1?0:0:2')).toEqual(0?1?0:0:2); + expect(scope.$eval('0?0?1:0:2')).toEqual(0?0?1:0:2); + expect(scope.$eval('0?0?0:2:3')).toEqual(0?0?0:2:3); + expect(scope.$eval('1?1?0:0:2')).toEqual(1?1?0:0:2); + expect(scope.$eval('1?1?1:0:2')).toEqual(1?1?1:0:2); + expect(scope.$eval('1?1?1:2:3')).toEqual(1?1?1:2:3); + expect(scope.$eval('1?1?1:2:3')).toEqual(1?1?1:2:3); + + // Nested on the right. + expect(scope.$eval('0?0:0?0:2')).toEqual(0?0:0?0:2); + expect(scope.$eval('1?0:0?0:2')).toEqual(1?0:0?0:2); + expect(scope.$eval('0?1:0?0:2')).toEqual(0?1:0?0:2); + expect(scope.$eval('0?0:1?0:2')).toEqual(0?0:1?0:2); + expect(scope.$eval('0?0:0?2:3')).toEqual(0?0:0?2:3); + expect(scope.$eval('1?1:0?0:2')).toEqual(1?1:0?0:2); + expect(scope.$eval('1?1:1?0:2')).toEqual(1?1:1?0:2); + expect(scope.$eval('1?1:1?2:3')).toEqual(1?1:1?2:3); + expect(scope.$eval('1?1:1?2:3')).toEqual(1?1:1?2:3); + + // Precedence with respect to logical operators. + expect(scope.$eval('0&&1?0:1')).toEqual(0&&1?0:1); + expect(scope.$eval('1||0?0:0')).toEqual(1||0?0:0); + + expect(scope.$eval('0?0&&1:2')).toEqual(0?0&&1:2); + expect(scope.$eval('0?1&&1:2')).toEqual(0?1&&1:2); + expect(scope.$eval('0?0||0:1')).toEqual(0?0||0:1); + expect(scope.$eval('0?0||1:2')).toEqual(0?0||1:2); + + expect(scope.$eval('1?0&&1:2')).toEqual(1?0&&1:2); + expect(scope.$eval('1?1&&1:2')).toEqual(1?1&&1:2); + expect(scope.$eval('1?0||0:1')).toEqual(1?0||0:1); + expect(scope.$eval('1?0||1:2')).toEqual(1?0||1:2); + + expect(scope.$eval('0?1:0&&1')).toEqual(0?1:0&&1); + expect(scope.$eval('0?2:1&&1')).toEqual(0?2:1&&1); + expect(scope.$eval('0?1:0||0')).toEqual(0?1:0||0); + expect(scope.$eval('0?2:0||1')).toEqual(0?2:0||1); + + expect(scope.$eval('1?1:0&&1')).toEqual(1?1:0&&1); + expect(scope.$eval('1?2:1&&1')).toEqual(1?2:1&&1); + expect(scope.$eval('1?1:0||0')).toEqual(1?1:0||0); + expect(scope.$eval('1?2:0||1')).toEqual(1?2:0||1); + + // Function calls. + expect(scope.$eval('returnTrue() ? returnString() : returnInt()')).toEqual(returnTrue() ? returnString() : returnInt()); + expect(scope.$eval('returnFalse() ? returnString() : returnInt()')).toEqual(returnFalse() ? returnString() : returnInt()); + expect(scope.$eval('returnTrue() ? returnString() : returnInt()')).toEqual(returnTrue() ? returnString() : returnInt()); + expect(scope.$eval('identity(returnFalse() ? returnString() : returnInt())')).toEqual(identity(returnFalse() ? returnString() : returnInt())); + }); - it('should parse string', function() { - expect(scope.$eval("'a' + 'b c'")).toEqual("ab c"); - }); + it('should parse string', function() { + expect(scope.$eval("'a' + 'b c'")).toEqual("ab c"); + }); - it('should parse filters', function() { - $filterProvider.register('substring', valueFn(function(input, start, end) { - return input.substring(start, end); - })); + it('should parse filters', function() { + $filterProvider.register('substring', valueFn(function(input, start, end) { + return input.substring(start, end); + })); - expect(function() { - scope.$eval("1|nonexistent"); - }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: nonexistentFilterProvider <- nonexistentFilter'); + expect(function() { + scope.$eval("1|nonexistent"); + }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: nonexistentFilterProvider <- nonexistentFilter'); - scope.offset = 3; - expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc"); - expect(scope.$eval("'abcd'|substring:1:3|uppercase")).toEqual("BC"); - }); - - it('should access scope', function() { - scope.a = 123; - scope.b = {c: 456}; - expect(scope.$eval("a", scope)).toEqual(123); - expect(scope.$eval("b.c", scope)).toEqual(456); - expect(scope.$eval("x.y.z", scope)).not.toBeDefined(); - }); + scope.offset = 3; + expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc"); + expect(scope.$eval("'abcd'|substring:1:3|uppercase")).toEqual("BC"); + }); - it('should resolve deeply nested paths (important for CSP mode)', function() { - scope.a = {b: {c: {d: {e: {f: {g: {h: {i: {j: {k: {l: {m: {n: 'nooo!'}}}}}}}}}}}}}; - expect(scope.$eval("a.b.c.d.e.f.g.h.i.j.k.l.m.n", scope)).toBe('nooo!'); - }); + it('should access scope', function() { + scope.a = 123; + scope.b = {c: 456}; + expect(scope.$eval("a", scope)).toEqual(123); + expect(scope.$eval("b.c", scope)).toEqual(456); + expect(scope.$eval("x.y.z", scope)).not.toBeDefined(); + }); - forEach([2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 42, 99], function(pathLength) { - it('should resolve nested paths of length ' + pathLength, function() { - // Create a nested object {x2: {x3: {x4: ... {x[n]: 42} ... }}}. - var obj = 42, locals = {}; - for (var i = pathLength; i >= 2; i--) { - var newObj = {}; - newObj['x' + i] = obj; - obj = newObj; - } - // Assign to x1 and build path 'x1.x2.x3. ... .x[n]' to access the final value. - scope.x1 = obj; - var path = 'x1'; - for (i = 2; i <= pathLength; i++) { - path += '.x' + i; - } - expect(scope.$eval(path)).toBe(42); - locals['x' + pathLength] = 'not 42'; - expect(scope.$eval(path, locals)).toBe(42); - }); - }); + it('should resolve deeply nested paths (important for CSP mode)', function() { + scope.a = {b: {c: {d: {e: {f: {g: {h: {i: {j: {k: {l: {m: {n: 'nooo!'}}}}}}}}}}}}}; + expect(scope.$eval("a.b.c.d.e.f.g.h.i.j.k.l.m.n", scope)).toBe('nooo!'); + }); - it('should be forgiving', function() { - scope.a = {b: 23}; - expect(scope.$eval('b')).toBeUndefined(); - expect(scope.$eval('a.x')).toBeUndefined(); - expect(scope.$eval('a.b.c.d')).toBeUndefined(); + forEach([2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 42, 99], function(pathLength) { + it('should resolve nested paths of length ' + pathLength, function() { + // Create a nested object {x2: {x3: {x4: ... {x[n]: 42} ... }}}. + var obj = 42, locals = {}; + for (var i = pathLength; i >= 2; i--) { + var newObj = {}; + newObj['x' + i] = obj; + obj = newObj; + } + // Assign to x1 and build path 'x1.x2.x3. ... .x[n]' to access the final value. + scope.x1 = obj; + var path = 'x1'; + for (i = 2; i <= pathLength; i++) { + path += '.x' + i; + } + expect(scope.$eval(path)).toBe(42); + locals['x' + pathLength] = 'not 42'; + expect(scope.$eval(path, locals)).toBe(42); }); + }); - it('should support property names that collide with native object properties', function() { - // regression - scope.watch = 1; - scope.toString = function toString() { - return "custom toString"; - }; - - expect(scope.$eval('watch', scope)).toBe(1); - expect(scope.$eval('toString()', scope)).toBe('custom toString'); - }); + it('should be forgiving', function() { + scope.a = {b: 23}; + expect(scope.$eval('b')).toBeUndefined(); + expect(scope.$eval('a.x')).toBeUndefined(); + expect(scope.$eval('a.b.c.d')).toBeUndefined(); + }); - it('should not break if hasOwnProperty is referenced in an expression', function() { - scope.obj = { value: 1}; - // By evaluating an expression that calls hasOwnProperty, the getterFnCache - // will store a property called hasOwnProperty. This is effectively: - // getterFnCache['hasOwnProperty'] = null - scope.$eval('obj.hasOwnProperty("value")'); - // If we rely on this property then evaluating any expression will fail - // because it is not able to find out if obj.value is there in the cache - expect(scope.$eval('obj.value')).toBe(1); - }); + it('should support property names that collide with native object properties', function() { + // regression + scope.watch = 1; + scope.toString = function toString() { + return "custom toString"; + }; - it('should not break if the expression is "hasOwnProperty"', function() { - scope.fooExp = 'barVal'; - // By evaluating hasOwnProperty, the $parse cache will store a getter for - // the scope's own hasOwnProperty function, which will mess up future cache look ups. - // i.e. cache['hasOwnProperty'] = function(scope) { return scope.hasOwnProperty; } - scope.$eval('hasOwnProperty'); - expect(scope.$eval('fooExp')).toBe('barVal'); - }); + expect(scope.$eval('watch', scope)).toBe(1); + expect(scope.$eval('toString()', scope)).toBe('custom toString'); + }); - it('should evaluate grouped expressions', function() { - expect(scope.$eval("(1+2)*3")).toEqual((1+2)*3); - }); + it('should not break if hasOwnProperty is referenced in an expression', function() { + scope.obj = { value: 1}; + // By evaluating an expression that calls hasOwnProperty, the getterFnCache + // will store a property called hasOwnProperty. This is effectively: + // getterFnCache['hasOwnProperty'] = null + scope.$eval('obj.hasOwnProperty("value")'); + // If we rely on this property then evaluating any expression will fail + // because it is not able to find out if obj.value is there in the cache + expect(scope.$eval('obj.value')).toBe(1); + }); - it('should evaluate assignments', function() { - expect(scope.$eval("a=12")).toEqual(12); - expect(scope.a).toEqual(12); + it('should not break if the expression is "hasOwnProperty"', function() { + scope.fooExp = 'barVal'; + // By evaluating hasOwnProperty, the $parse cache will store a getter for + // the scope's own hasOwnProperty function, which will mess up future cache look ups. + // i.e. cache['hasOwnProperty'] = function(scope) { return scope.hasOwnProperty; } + scope.$eval('hasOwnProperty'); + expect(scope.$eval('fooExp')).toBe('barVal'); + }); - expect(scope.$eval("x.y.z=123;")).toEqual(123); - expect(scope.x.y.z).toEqual(123); + it('should evaluate grouped expressions', function() { + expect(scope.$eval("(1+2)*3")).toEqual((1+2)*3); + }); - expect(scope.$eval("a=123; b=234")).toEqual(234); - expect(scope.a).toEqual(123); - expect(scope.b).toEqual(234); - }); + it('should evaluate assignments', function() { + expect(scope.$eval("a=12")).toEqual(12); + expect(scope.a).toEqual(12); - it('should evaluate function call without arguments', function() { - scope['const'] = function(a,b){return 123;}; - expect(scope.$eval("const()")).toEqual(123); - }); + expect(scope.$eval("x.y.z=123;")).toEqual(123); + expect(scope.x.y.z).toEqual(123); - it('should evaluate function call with arguments', function() { - scope.add = function(a,b) { - return a+b; - }; - expect(scope.$eval("add(1,2)")).toEqual(3); - }); - - it('should evaluate function call from a return value', function() { - scope.val = 33; - scope.getter = function() { return function() { return this.val; }; }; - expect(scope.$eval("getter()()")).toBe(33); - }); + expect(scope.$eval("a=123; b=234")).toEqual(234); + expect(scope.a).toEqual(123); + expect(scope.b).toEqual(234); + }); - it('should evaluate multiplication and division', function() { - scope.taxRate = 8; - scope.subTotal = 100; - expect(scope.$eval("taxRate / 100 * subTotal")).toEqual(8); - expect(scope.$eval("subTotal * taxRate / 100")).toEqual(8); - }); + it('should evaluate function call without arguments', function() { + scope['const'] = function(a,b){return 123;}; + expect(scope.$eval("const()")).toEqual(123); + }); - it('should evaluate array', function() { - expect(scope.$eval("[]").length).toEqual(0); - expect(scope.$eval("[1, 2]").length).toEqual(2); - expect(scope.$eval("[1, 2]")[0]).toEqual(1); - expect(scope.$eval("[1, 2]")[1]).toEqual(2); - expect(scope.$eval("[1, 2,]")[1]).toEqual(2); - expect(scope.$eval("[1, 2,]").length).toEqual(2); - }); + it('should evaluate function call with arguments', function() { + scope.add = function(a,b) { + return a+b; + }; + expect(scope.$eval("add(1,2)")).toEqual(3); + }); - it('should evaluate array access', function() { - expect(scope.$eval("[1][0]")).toEqual(1); - expect(scope.$eval("[[1]][0][0]")).toEqual(1); - expect(scope.$eval("[].length")).toEqual(0); - expect(scope.$eval("[1, 2].length")).toEqual(2); - }); + it('should evaluate function call from a return value', function() { + scope.val = 33; + scope.getter = function() { return function() { return this.val; }; }; + expect(scope.$eval("getter()()")).toBe(33); + }); - it('should evaluate object', function() { - expect(toJson(scope.$eval("{}"))).toEqual("{}"); - expect(toJson(scope.$eval("{a:'b'}"))).toEqual('{"a":"b"}'); - expect(toJson(scope.$eval("{'a':'b'}"))).toEqual('{"a":"b"}'); - expect(toJson(scope.$eval("{\"a\":'b'}"))).toEqual('{"a":"b"}'); - expect(toJson(scope.$eval("{a:'b',}"))).toEqual('{"a":"b"}'); - expect(toJson(scope.$eval("{'a':'b',}"))).toEqual('{"a":"b"}'); - expect(toJson(scope.$eval("{\"a\":'b',}"))).toEqual('{"a":"b"}'); - }); + it('should evaluate multiplication and division', function() { + scope.taxRate = 8; + scope.subTotal = 100; + expect(scope.$eval("taxRate / 100 * subTotal")).toEqual(8); + expect(scope.$eval("subTotal * taxRate / 100")).toEqual(8); + }); - it('should evaluate object access', function() { - expect(scope.$eval("{false:'WC', true:'CC'}[false]")).toEqual("WC"); - }); + it('should evaluate array', function() { + expect(scope.$eval("[]").length).toEqual(0); + expect(scope.$eval("[1, 2]").length).toEqual(2); + expect(scope.$eval("[1, 2]")[0]).toEqual(1); + expect(scope.$eval("[1, 2]")[1]).toEqual(2); + expect(scope.$eval("[1, 2,]")[1]).toEqual(2); + expect(scope.$eval("[1, 2,]").length).toEqual(2); + }); - it('should evaluate JSON', function() { - expect(toJson(scope.$eval("[{}]"))).toEqual("[{}]"); - expect(toJson(scope.$eval("[{a:[]}, {b:1}]"))).toEqual('[{"a":[]},{"b":1}]'); - }); + it('should evaluate array access', function() { + expect(scope.$eval("[1][0]")).toEqual(1); + expect(scope.$eval("[[1]][0][0]")).toEqual(1); + expect(scope.$eval("[].length")).toEqual(0); + expect(scope.$eval("[1, 2].length")).toEqual(2); + }); - it('should evaluate multiple statements', function() { - expect(scope.$eval("a=1;b=3;a+b")).toEqual(4); - expect(scope.$eval(";;1;;")).toEqual(1); - }); + it('should evaluate object', function() { + expect(toJson(scope.$eval("{}"))).toEqual("{}"); + expect(toJson(scope.$eval("{a:'b'}"))).toEqual('{"a":"b"}'); + expect(toJson(scope.$eval("{'a':'b'}"))).toEqual('{"a":"b"}'); + expect(toJson(scope.$eval("{\"a\":'b'}"))).toEqual('{"a":"b"}'); + expect(toJson(scope.$eval("{a:'b',}"))).toEqual('{"a":"b"}'); + expect(toJson(scope.$eval("{'a':'b',}"))).toEqual('{"a":"b"}'); + expect(toJson(scope.$eval("{\"a\":'b',}"))).toEqual('{"a":"b"}'); + }); - it('should evaluate object methods in correct context (this)', function() { - var C = function () { - this.a = 123; - }; - C.prototype.getA = function() { - return this.a; - }; + it('should evaluate object access', function() { + expect(scope.$eval("{false:'WC', true:'CC'}[false]")).toEqual("WC"); + }); - scope.obj = new C(); - expect(scope.$eval("obj.getA()")).toEqual(123); - expect(scope.$eval("obj['getA']()")).toEqual(123); - }); + it('should evaluate JSON', function() { + expect(toJson(scope.$eval("[{}]"))).toEqual("[{}]"); + expect(toJson(scope.$eval("[{a:[]}, {b:1}]"))).toEqual('[{"a":[]},{"b":1}]'); + }); - it('should evaluate methods in correct context (this) in argument', function() { - var C = function () { - this.a = 123; - }; - C.prototype.sum = function(value) { - return this.a + value; - }; - C.prototype.getA = function() { - return this.a; - }; + it('should evaluate multiple statements', function() { + expect(scope.$eval("a=1;b=3;a+b")).toEqual(4); + expect(scope.$eval(";;1;;")).toEqual(1); + }); - scope.obj = new C(); - expect(scope.$eval("obj.sum(obj.getA())")).toEqual(246); - expect(scope.$eval("obj['sum'](obj.getA())")).toEqual(246); - }); + it('should evaluate object methods in correct context (this)', function() { + var C = function () { + this.a = 123; + }; + C.prototype.getA = function() { + return this.a; + }; + + scope.obj = new C(); + expect(scope.$eval("obj.getA()")).toEqual(123); + expect(scope.$eval("obj['getA']()")).toEqual(123); + }); - it('should evaluate objects on scope context', function() { - scope.a = "abc"; - expect(scope.$eval("{a:a}").a).toEqual("abc"); - }); + it('should evaluate methods in correct context (this) in argument', function() { + var C = function () { + this.a = 123; + }; + C.prototype.sum = function(value) { + return this.a + value; + }; + C.prototype.getA = function() { + return this.a; + }; + + scope.obj = new C(); + expect(scope.$eval("obj.sum(obj.getA())")).toEqual(246); + expect(scope.$eval("obj['sum'](obj.getA())")).toEqual(246); + }); - it('should evaluate field access on function call result', function() { - scope.a = function() { - return {name:'misko'}; - }; - expect(scope.$eval("a().name")).toEqual("misko"); - }); + it('should evaluate objects on scope context', function() { + scope.a = "abc"; + expect(scope.$eval("{a:a}").a).toEqual("abc"); + }); - it('should evaluate field access after array access', function () { - scope.items = [{}, {name:'misko'}]; - expect(scope.$eval('items[1].name')).toEqual("misko"); - }); + it('should evaluate field access on function call result', function() { + scope.a = function() { + return {name:'misko'}; + }; + expect(scope.$eval("a().name")).toEqual("misko"); + }); - it('should evaluate array assignment', function() { - scope.items = []; + it('should evaluate field access after array access', function () { + scope.items = [{}, {name:'misko'}]; + expect(scope.$eval('items[1].name')).toEqual("misko"); + }); - expect(scope.$eval('items[1] = "abc"')).toEqual("abc"); - expect(scope.$eval('items[1]')).toEqual("abc"); - // Dont know how to make this work.... - // expect(scope.$eval('books[1] = "moby"')).toEqual("moby"); - // expect(scope.$eval('books[1]')).toEqual("moby"); - }); + it('should evaluate array assignment', function() { + scope.items = []; - it('should evaluate grouped filters', function() { - scope.name = 'MISKO'; - expect(scope.$eval('n = (name|lowercase)')).toEqual('misko'); - expect(scope.$eval('n')).toEqual('misko'); - }); + expect(scope.$eval('items[1] = "abc"')).toEqual("abc"); + expect(scope.$eval('items[1]')).toEqual("abc"); + // Dont know how to make this work.... + // expect(scope.$eval('books[1] = "moby"')).toEqual("moby"); + // expect(scope.$eval('books[1]')).toEqual("moby"); + }); - it('should evaluate remainder', function() { - expect(scope.$eval('1%2')).toEqual(1); - }); + it('should evaluate grouped filters', function() { + scope.name = 'MISKO'; + expect(scope.$eval('n = (name|lowercase)')).toEqual('misko'); + expect(scope.$eval('n')).toEqual('misko'); + }); - it('should evaluate sum with undefined', function() { - expect(scope.$eval('1+undefined')).toEqual(1); - expect(scope.$eval('undefined+1')).toEqual(1); - }); + it('should evaluate remainder', function() { + expect(scope.$eval('1%2')).toEqual(1); + }); - it('should throw exception on non-closed bracket', function() { - expect(function() { - scope.$eval('[].count('); - }).toThrowMinErr('$parse', 'ueoe', 'Unexpected end of expression: [].count('); - }); + it('should evaluate sum with undefined', function() { + expect(scope.$eval('1+undefined')).toEqual(1); + expect(scope.$eval('undefined+1')).toEqual(1); + }); - it('should evaluate double negation', function() { - expect(scope.$eval('true')).toBeTruthy(); - expect(scope.$eval('!true')).toBeFalsy(); - expect(scope.$eval('!!true')).toBeTruthy(); - expect(scope.$eval('{true:"a", false:"b"}[!!true]')).toEqual('a'); - }); + it('should throw exception on non-closed bracket', function() { + expect(function() { + scope.$eval('[].count('); + }).toThrowMinErr('$parse', 'ueoe', 'Unexpected end of expression: [].count('); + }); - it('should evaluate negation', function() { - /* jshint -W018 */ - expect(scope.$eval("!false || true")).toEqual(!false || true); - expect(scope.$eval("!11 == 10")).toEqual(!11 == 10); - expect(scope.$eval("12/6/2")).toEqual(12/6/2); - }); + it('should evaluate double negation', function() { + expect(scope.$eval('true')).toBeTruthy(); + expect(scope.$eval('!true')).toBeFalsy(); + expect(scope.$eval('!!true')).toBeTruthy(); + expect(scope.$eval('{true:"a", false:"b"}[!!true]')).toEqual('a'); + }); - it('should evaluate exclamation mark', function() { - expect(scope.$eval('suffix = "!"')).toEqual('!'); - }); + it('should evaluate negation', function() { + /* jshint -W018 */ + expect(scope.$eval("!false || true")).toEqual(!false || true); + expect(scope.$eval("!11 == 10")).toEqual(!11 == 10); + expect(scope.$eval("12/6/2")).toEqual(12/6/2); + }); - it('should evaluate minus', function() { - expect(scope.$eval("{a:'-'}")).toEqual({a: "-"}); - }); + it('should evaluate exclamation mark', function() { + expect(scope.$eval('suffix = "!"')).toEqual('!'); + }); - it('should evaluate undefined', function() { - expect(scope.$eval("undefined")).not.toBeDefined(); - expect(scope.$eval("a=undefined")).not.toBeDefined(); - expect(scope.a).not.toBeDefined(); - }); + it('should evaluate minus', function() { + expect(scope.$eval("{a:'-'}")).toEqual({a: "-"}); + }); - it('should allow assignment after array dereference', function() { - scope.obj = [{}]; - scope.$eval('obj[0].name=1'); - expect(scope.obj.name).toBeUndefined(); - expect(scope.obj[0].name).toEqual(1); - }); + it('should evaluate undefined', function() { + expect(scope.$eval("undefined")).not.toBeDefined(); + expect(scope.$eval("a=undefined")).not.toBeDefined(); + expect(scope.a).not.toBeDefined(); + }); - it('should short-circuit AND operator', function() { - scope.run = function() { - throw "IT SHOULD NOT HAVE RUN"; - }; - expect(scope.$eval('false && run()')).toBe(false); - }); + it('should allow assignment after array dereference', function() { + scope.obj = [{}]; + scope.$eval('obj[0].name=1'); + expect(scope.obj.name).toBeUndefined(); + expect(scope.obj[0].name).toEqual(1); + }); - it('should short-circuit OR operator', function() { - scope.run = function() { - throw "IT SHOULD NOT HAVE RUN"; - }; - expect(scope.$eval('true || run()')).toBe(true); - }); + it('should short-circuit AND operator', function() { + scope.run = function() { + throw "IT SHOULD NOT HAVE RUN"; + }; + expect(scope.$eval('false && run()')).toBe(false); + }); + it('should short-circuit OR operator', function() { + scope.run = function() { + throw "IT SHOULD NOT HAVE RUN"; + }; + expect(scope.$eval('true || run()')).toBe(true); + }); - it('should support method calls on primitive types', function() { - scope.empty = ''; - scope.zero = 0; - scope.bool = false; - expect(scope.$eval('empty.substr(0)')).toBe(''); - expect(scope.$eval('zero.toString()')).toBe('0'); - expect(scope.$eval('bool.toString()')).toBe('false'); - }); + it('should support method calls on primitive types', function() { + scope.empty = ''; + scope.zero = 0; + scope.bool = false; - it('should evaluate expressions with line terminators', function() { - scope.a = "a"; - scope.b = {c: "bc"}; - expect(scope.$eval('a + \n b.c + \r "\td" + \t \r\n\r "\r\n\n"')).toEqual("abc\td\r\n\n"); - }); + expect(scope.$eval('empty.substr(0)')).toBe(''); + expect(scope.$eval('zero.toString()')).toBe('0'); + expect(scope.$eval('bool.toString()')).toBe('false'); + }); - describe('sandboxing', function() { - describe('Function constructor', function() { - it('should NOT allow access to Function constructor in getter', function() { - expect(function() { - scope.$eval('{}.toString.constructor'); - }).toThrowMinErr( - '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + - 'Expression: {}.toString.constructor'); - - expect(function() { - scope.$eval('{}.toString.constructor("alert(1)")'); - }).toThrowMinErr( - '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + - 'Expression: {}.toString.constructor("alert(1)")'); - - expect(function() { - scope.$eval('[].toString.constructor.foo'); - }).toThrowMinErr( - '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + - 'Expression: [].toString.constructor.foo'); - - expect(function() { - scope.$eval('{}.toString["constructor"]'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString["constructor"]'); - expect(function() { - scope.$eval('{}["toString"]["constructor"]'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}["toString"]["constructor"]'); - - scope.a = []; - expect(function() { - scope.$eval('a.toString.constructor', scope); - }).toThrowMinErr( - '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + - 'Expression: a.toString.constructor'); - expect(function() { - scope.$eval('a.toString["constructor"]', scope); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: a.toString["constructor"]'); - }); - - it('should NOT allow access to Function constructor in setter', function() { - expect(function() { - scope.$eval('{}.toString.constructor = 1'); - }).toThrowMinErr( - '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + - 'Expression: {}.toString.constructor = 1'); - - expect(function() { - scope.$eval('{}.toString.constructor.a = 1'); - }).toThrowMinErr( - '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + - 'Expression: {}.toString.constructor.a = 1'); - - expect(function() { - scope.$eval('{}.toString["constructor"]["constructor"] = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString["constructor"]["constructor"] = 1'); - - - scope.key1 = "const"; - scope.key2 = "ructor"; - expect(function() { - scope.$eval('{}.toString[key1 + key2].foo = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString[key1 + key2].foo = 1'); - - expect(function() { - scope.$eval('{}.toString["constructor"]["a"] = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString["constructor"]["a"] = 1'); - - scope.a = []; - expect(function() { - scope.$eval('a.toString.constructor = 1', scope); - }).toThrowMinErr( - '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + - 'Expression: a.toString.constructor = 1'); - }); - - - it('should NOT allow access to Function constructor that has been aliased', function() { - scope.foo = { "bar": Function }; - expect(function() { - scope.$eval('foo["bar"]'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: foo["bar"]'); - - }); - - - it('should NOT allow access to Function constructor in getter', function() { - expect(function() { - scope.$eval('{}.toString.constructor'); - }).toThrowMinErr( - '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + - 'Expression: {}.toString.constructor'); - }); - }); + it('should evaluate expressions with line terminators', function() { + scope.a = "a"; + scope.b = {c: "bc"}; + expect(scope.$eval('a + \n b.c + \r "\td" + \t \r\n\r "\r\n\n"')).toEqual("abc\td\r\n\n"); + }); + describe('sandboxing', function() { + describe('Function constructor', function() { + it('should NOT allow access to Function constructor in getter', function() { + expect(function() { + scope.$eval('{}.toString.constructor'); + }).toThrowMinErr( + '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + + 'Expression: {}.toString.constructor'); - describe('Window and $element/node', function() { - it('should NOT allow access to the Window or DOM when indexing', inject(function($window, $document) { - scope.wrap = {w: $window, d: $document}; - - expect(function() { - scope.$eval('wrap["w"]', scope); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + - 'disallowed! Expression: wrap["w"]'); - expect(function() { - scope.$eval('wrap["d"]', scope); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + - 'disallowed! Expression: wrap["d"]'); - })); - - it('should NOT allow access to the Window or DOM returned from a function', inject(function($window, $document) { - scope.getWin = valueFn($window); - scope.getDoc = valueFn($document); - - expect(function() { - scope.$eval('getWin()', scope); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + - 'disallowed! Expression: getWin()'); - expect(function() { - scope.$eval('getDoc()', scope); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + - 'disallowed! Expression: getDoc()'); - })); - - it('should NOT allow calling functions on Window or DOM', inject(function($window, $document) { - scope.a = {b: { win: $window, doc: $document }}; - expect(function() { - scope.$eval('a.b.win.alert(1)', scope); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + - 'disallowed! Expression: a.b.win.alert(1)'); - expect(function() { - scope.$eval('a.b.doc.on("click")', scope); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + - 'disallowed! Expression: a.b.doc.on("click")'); - })); - - // Issue #4805 - it('should NOT throw isecdom when referencing a Backbone Collection', function() { - // Backbone stuff is sort of hard to mock, if you have a better way of doing this, - // please fix this. - var fakeBackboneCollection = { - children: [{}, {}, {}], - find: function() {}, - on: function() {}, - off: function() {}, - bind: function() {} - }; - scope.backbone = fakeBackboneCollection; - expect(function() { scope.$eval('backbone'); }).not.toThrow(); - }); - - it('should NOT throw isecdom when referencing an array with node properties', function() { - var array = [1,2,3]; - array.on = array.attr = array.prop = array.bind = true; - scope.array = array; - expect(function() { scope.$eval('array'); }).not.toThrow(); - }); - }); - }); + expect(function() { + scope.$eval('{}.toString.constructor("alert(1)")'); + }).toThrowMinErr( + '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + + 'Expression: {}.toString.constructor("alert(1)")'); - describe('overriding constructor', function() { - it('should evaluate grouped expressions', function() { - scope.foo = function foo() { - return "foo"; - }; - // When not overridden, access should be restricted both by the dot operator and by the - // index operator. expect(function() { - scope.$eval('foo.constructor()', scope); + scope.$eval('[].toString.constructor.foo'); }).toThrowMinErr( '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + - 'Expression: foo.constructor()'); + 'Expression: [].toString.constructor.foo'); + + expect(function() { + scope.$eval('{}.toString["constructor"]'); + }).toThrowMinErr( + '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + + 'Expression: {}.toString["constructor"]'); expect(function() { - scope.$eval('foo["constructor"]()', scope); + scope.$eval('{}["toString"]["constructor"]'); }).toThrowMinErr( '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: foo["constructor"]()'); + 'Expression: {}["toString"]["constructor"]'); - // User defined value assigned to constructor. - scope.foo.constructor = function constructor() { - return "custom constructor"; - }; - // Dot operator should still block it. + scope.a = []; expect(function() { - scope.$eval('foo.constructor()', scope); + scope.$eval('a.toString.constructor', scope); }).toThrowMinErr( '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + - 'Expression: foo.constructor()'); - // However, the index operator should allow it. - expect(scope.$eval('foo["constructor"]()', scope)).toBe('custom constructor'); + 'Expression: a.toString.constructor'); + expect(function() { + scope.$eval('a.toString["constructor"]', scope); + }).toThrowMinErr( + '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + + 'Expression: a.toString["constructor"]'); }); - }); - - it('should call the function from the received instance and not from a new one', function() { - var n = 0; - scope.fn = function() { - var c = n++; - return { c: c, anotherFn: function() { return this.c == c; } }; - }; - expect(scope.$eval('fn().anotherFn()')).toBe(true); - }); - - - it('should call the function once when it is part of the context', function() { - var count = 0; - scope.fn = function() { - count++; - return { anotherFn: function() { return "lucas"; } }; - }; - expect(scope.$eval('fn().anotherFn()')).toBe('lucas'); - expect(count).toBe(1); - }); - - - it('should call the function once when it is not part of the context', function() { - var count = 0; - scope.fn = function() { - count++; - return function() { return 'lucas'; }; - }; - expect(scope.$eval('fn()()')).toBe('lucas'); - expect(count).toBe(1); - }); - - - it('should call the function once when it is part of the context on assignments', function() { - var count = 0; - var element = {}; - scope.fn = function() { - count++; - return element; - }; - expect(scope.$eval('fn().name = "lucas"')).toBe('lucas'); - expect(element.name).toBe('lucas'); - expect(count).toBe(1); - }); - - - it('should call the function once when it is part of the context on array lookups', function() { - var count = 0; - var element = []; - scope.fn = function() { - count++; - return element; - }; - expect(scope.$eval('fn()[0] = "lucas"')).toBe('lucas'); - expect(element[0]).toBe('lucas'); - expect(count).toBe(1); - }); + it('should NOT allow access to Function constructor in setter', function() { + expect(function() { + scope.$eval('{}.toString.constructor = 1'); + }).toThrowMinErr( + '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + + 'Expression: {}.toString.constructor = 1'); - it('should call the function once when it is part of the context on array lookup function', function() { - var count = 0; - var element = [{anotherFn: function() { return 'lucas';} }]; - scope.fn = function() { - count++; - return element; - }; - expect(scope.$eval('fn()[0].anotherFn()')).toBe('lucas'); - expect(count).toBe(1); - }); + expect(function() { + scope.$eval('{}.toString.constructor.a = 1'); + }).toThrowMinErr( + '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + + 'Expression: {}.toString.constructor.a = 1'); + expect(function() { + scope.$eval('{}.toString["constructor"]["constructor"] = 1'); + }).toThrowMinErr( + '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + + 'Expression: {}.toString["constructor"]["constructor"] = 1'); - it('should call the function once when it is part of the context on property lookup function', function() { - var count = 0; - var element = {name: {anotherFn: function() { return 'lucas';} } }; - scope.fn = function() { - count++; - return element; - }; - expect(scope.$eval('fn().name.anotherFn()')).toBe('lucas'); - expect(count).toBe(1); - }); + scope.key1 = "const"; + scope.key2 = "ructor"; + expect(function() { + scope.$eval('{}.toString[key1 + key2].foo = 1'); + }).toThrowMinErr( + '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + + 'Expression: {}.toString[key1 + key2].foo = 1'); - it('should call the function once when it is part of a sub-expression', function() { - var count = 0; - scope.element = [{}]; - scope.fn = function() { - count++; - return 0; - }; - expect(scope.$eval('element[fn()].name = "lucas"')).toBe('lucas'); - expect(scope.element[0].name).toBe('lucas'); - expect(count).toBe(1); - }); + expect(function() { + scope.$eval('{}.toString["constructor"]["a"] = 1'); + }).toThrowMinErr( + '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + + 'Expression: {}.toString["constructor"]["a"] = 1'); + scope.a = []; + expect(function() { + scope.$eval('a.toString.constructor = 1', scope); + }).toThrowMinErr( + '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + + 'Expression: a.toString.constructor = 1'); + }); - describe('assignable', function() { - it('should expose assignment function', inject(function($parse) { - var fn = $parse('a'); - expect(fn.assign).toBeTruthy(); - var scope = {}; - fn.assign(scope, 123); - expect(scope).toEqual({a:123}); - })); - }); + it('should NOT allow access to Function constructor that has been aliased', function() { + scope.foo = { "bar": Function }; + expect(function() { + scope.$eval('foo["bar"]'); + }).toThrowMinErr( + '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + + 'Expression: foo["bar"]'); - describe('locals', function() { - it('should expose local variables', inject(function($parse) { - expect($parse('a')({a: 0}, {a: 1})).toEqual(1); - expect($parse('add(a,b)')({b: 1, add: function(a, b) { return a + b; }}, {a: 2})).toEqual(3); - })); + }); - it('should expose traverse locals', inject(function($parse) { - expect($parse('a.b')({a: {b: 0}}, {a: {b:1}})).toEqual(1); - expect($parse('a.b')({a: null}, {a: {b:1}})).toEqual(1); - expect($parse('a.b')({a: {b: 0}}, {a: null})).toEqual(undefined); - expect($parse('a.b.c')({a: null}, {a: {b: {c: 1}}})).toEqual(1); - })); - it('should not use locals to resolve object properties', inject(function($parse) { - expect($parse('a[0].b')({a: [ {b : 'scope'} ]}, {b : 'locals'})).toBe('scope'); - expect($parse('a[0]["b"]')({a: [ {b : 'scope'} ]}, {b : 'locals'})).toBe('scope'); - expect($parse('a[0][0].b')({a: [[{b : 'scope'}]]}, {b : 'locals'})).toBe('scope'); - expect($parse('a[0].b.c')({a: [ {b: {c: 'scope'}}] }, {b : {c: 'locals'} })).toBe('scope'); - })); + it('should NOT allow access to Function constructor in getter', function() { + expect(function() { + scope.$eval('{}.toString.constructor'); + }).toThrowMinErr( + '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + + 'Expression: {}.toString.constructor'); + }); }); - describe('literal', function() { - it('should mark scalar value expressions as literal', inject(function($parse) { - expect($parse('0').literal).toBe(true); - expect($parse('"hello"').literal).toBe(true); - expect($parse('true').literal).toBe(true); - expect($parse('false').literal).toBe(true); - expect($parse('null').literal).toBe(true); - expect($parse('undefined').literal).toBe(true); - })); - - it('should mark array expressions as literal', inject(function($parse) { - expect($parse('[]').literal).toBe(true); - expect($parse('[1, 2, 3]').literal).toBe(true); - expect($parse('[1, identifier]').literal).toBe(true); - })); - it('should mark object expressions as literal', inject(function($parse) { - expect($parse('{}').literal).toBe(true); - expect($parse('{x: 1}').literal).toBe(true); - expect($parse('{foo: bar}').literal).toBe(true); - })); + describe('Window and $element/node', function() { + it('should NOT allow access to the Window or DOM when indexing', inject(function($window, $document) { + scope.wrap = {w: $window, d: $document}; - it('should not mark function calls or operator expressions as literal', inject(function($parse) { - expect($parse('1 + 1').literal).toBe(false); - expect($parse('call()').literal).toBe(false); - expect($parse('[].length').literal).toBe(false); + expect(function() { + scope.$eval('wrap["w"]', scope); + }).toThrowMinErr( + '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + + 'disallowed! Expression: wrap["w"]'); + expect(function() { + scope.$eval('wrap["d"]', scope); + }).toThrowMinErr( + '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + + 'disallowed! Expression: wrap["d"]'); })); - }); - describe('constant', function() { - it('should mark scalar value expressions as constant', inject(function($parse) { - expect($parse('12.3').constant).toBe(true); - expect($parse('"string"').constant).toBe(true); - expect($parse('true').constant).toBe(true); - expect($parse('false').constant).toBe(true); - expect($parse('null').constant).toBe(true); - expect($parse('undefined').constant).toBe(true); - })); + it('should NOT allow access to the Window or DOM returned from a function', inject(function($window, $document) { + scope.getWin = valueFn($window); + scope.getDoc = valueFn($document); - it('should mark arrays as constant if they only contain constant elements', inject(function($parse) { - expect($parse('[]').constant).toBe(true); - expect($parse('[1, 2, 3]').constant).toBe(true); - expect($parse('["string", null]').constant).toBe(true); - expect($parse('[[]]').constant).toBe(true); - expect($parse('[1, [2, 3], {4: 5}]').constant).toBe(true); + expect(function() { + scope.$eval('getWin()', scope); + }).toThrowMinErr( + '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + + 'disallowed! Expression: getWin()'); + expect(function() { + scope.$eval('getDoc()', scope); + }).toThrowMinErr( + '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + + 'disallowed! Expression: getDoc()'); })); - it('should not mark arrays as constant if they contain any non-constant elements', inject(function($parse) { - expect($parse('[foo]').constant).toBe(false); - expect($parse('[x + 1]').constant).toBe(false); - expect($parse('[bar[0]]').constant).toBe(false); + it('should NOT allow calling functions on Window or DOM', inject(function($window, $document) { + scope.a = {b: { win: $window, doc: $document }}; + expect(function() { + scope.$eval('a.b.win.alert(1)', scope); + }).toThrowMinErr( + '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + + 'disallowed! Expression: a.b.win.alert(1)'); + expect(function() { + scope.$eval('a.b.doc.on("click")', scope); + }).toThrowMinErr( + '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + + 'disallowed! Expression: a.b.doc.on("click")'); })); - it('should mark complex expressions involving constant values as constant', inject(function($parse) { - expect($parse('!true').constant).toBe(true); - expect($parse('-42').constant).toBe(true); - expect($parse('1 - 1').constant).toBe(true); - expect($parse('"foo" + "bar"').constant).toBe(true); - expect($parse('5 != null').constant).toBe(true); - expect($parse('{standard: 4/3, wide: 16/9}').constant).toBe(true); - })); + // Issue #4805 + it('should NOT throw isecdom when referencing a Backbone Collection', function() { + // Backbone stuff is sort of hard to mock, if you have a better way of doing this, + // please fix this. + var fakeBackboneCollection = { + children: [{}, {}, {}], + find: function() {}, + on: function() {}, + off: function() {}, + bind: function() {} + }; + scope.backbone = fakeBackboneCollection; + expect(function() { scope.$eval('backbone'); }).not.toThrow(); + }); - it('should not mark any expression involving variables or function calls as constant', inject(function($parse) { - expect($parse('true.toString()').constant).toBe(false); - expect($parse('foo(1, 2, 3)').constant).toBe(false); - expect($parse('"name" + id').constant).toBe(false); - })); + it('should NOT throw isecdom when referencing an array with node properties', function() { + var array = [1,2,3]; + array.on = array.attr = array.prop = array.bind = true; + scope.array = array; + expect(function() { scope.$eval('array'); }).not.toThrow(); + }); }); + }); - describe('null/undefined in expressions', function() { - // simpleGetterFn1 - it('should return null for `a` where `a` is null', inject(function($rootScope) { - $rootScope.a = null; - expect($rootScope.$eval('a')).toBe(null); - })); - - it('should return undefined for `a` where `a` is undefined', inject(function($rootScope) { - expect($rootScope.$eval('a')).toBeUndefined(); - })); - - // simpleGetterFn2 - it('should return undefined for properties of `null` constant', inject(function($rootScope) { - expect($rootScope.$eval('null.a')).toBeUndefined(); - })); - - it('should return undefined for properties of `null` values', inject(function($rootScope) { - $rootScope.a = null; - expect($rootScope.$eval('a.b')).toBeUndefined(); - })); - - it('should return null for `a.b` where `b` is null', inject(function($rootScope) { - $rootScope.a = { b: null }; - expect($rootScope.$eval('a.b')).toBe(null); - })); - - // cspSafeGetter && pathKeys.length < 6 || pathKeys.length > 2 - it('should return null for `a.b.c.d.e` where `e` is null', inject(function($rootScope) { - $rootScope.a = { b: { c: { d: { e: null } } } }; - expect($rootScope.$eval('a.b.c.d.e')).toBe(null); - })); - - it('should return undefined for `a.b.c.d.e` where `d` is null', inject(function($rootScope) { - $rootScope.a = { b: { c: { d: null } } }; - expect($rootScope.$eval('a.b.c.d.e')).toBeUndefined(); - })); - - // cspSafeGetter || pathKeys.length > 6 - it('should return null for `a.b.c.d.e.f.g` where `g` is null', inject(function($rootScope) { - $rootScope.a = { b: { c: { d: { e: { f: { g: null } } } } } }; - expect($rootScope.$eval('a.b.c.d.e.f.g')).toBe(null); - })); - - it('should return undefined for `a.b.c.d.e.f.g` where `f` is null', inject(function($rootScope) { - $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'); - })); + describe('overriding constructor', function() { + it('should evaluate grouped expressions', function() { + scope.foo = function foo() { + return "foo"; + }; + // When not overridden, access should be restricted both by the dot operator and by the + // index operator. + expect(function() { + scope.$eval('foo.constructor()', scope); + }).toThrowMinErr( + '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + + 'Expression: foo.constructor()'); + expect(function() { + scope.$eval('foo["constructor"]()', scope); + }).toThrowMinErr( + '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + + 'Expression: foo["constructor"]()'); + + // User defined value assigned to constructor. + scope.foo.constructor = function constructor() { + return "custom constructor"; + }; + // Dot operator should still block it. + expect(function() { + scope.$eval('foo.constructor()', scope); + }).toThrowMinErr( + '$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' + + 'Expression: foo.constructor()'); + // However, the index operator should allow it. + expect(scope.$eval('foo["constructor"]()', scope)).toBe('custom constructor'); }); }); - }); - }); + it('should call the function from the received instance and not from a new one', function() { + var n = 0; + scope.fn = function() { + var c = n++; + return { c: c, anotherFn: function() { return this.c == c; } }; + }; + expect(scope.$eval('fn().anotherFn()')).toBe(true); + }); - describe('promises', function() { - var deferred, promise, q; + it('should call the function once when it is part of the context', function() { + var count = 0; + scope.fn = function() { + count++; + return { anotherFn: function() { return "lucas"; } }; + }; + expect(scope.$eval('fn().anotherFn()')).toBe('lucas'); + expect(count).toBe(1); + }); - describe('unwrapPromises setting', function () { - beforeEach(inject(function($rootScope, $q) { - scope = $rootScope; + it('should call the function once when it is not part of the context', function() { + var count = 0; + scope.fn = function() { + count++; + return function() { return 'lucas'; }; + }; + expect(scope.$eval('fn()()')).toBe('lucas'); + expect(count).toBe(1); + }); - $rootScope.$apply(function() { - deferred = $q.defer(); - deferred.resolve('Bobo'); - promise = deferred.promise; - }); - })); - it('should not unwrap promises by default', inject(function ($parse) { - scope.person = promise; - scope.things = {person: promise}; - scope.getPerson = function () { return promise; }; + it('should call the function once when it is part of the context on assignments', function() { + var count = 0; + var element = {}; + scope.fn = function() { + count++; + return element; + }; + expect(scope.$eval('fn().name = "lucas"')).toBe('lucas'); + expect(element.name).toBe('lucas'); + expect(count).toBe(1); + }); - var getter = $parse('person'); - var propGetter = $parse('things.person'); - var fnGetter = $parse('getPerson()'); - expect(getter(scope)).toBe(promise); - expect(propGetter(scope)).toBe(promise); - expect(fnGetter(scope)).toBe(promise); - })); - }); + it('should call the function once when it is part of the context on array lookups', function() { + var count = 0; + var element = []; + scope.fn = function() { + count++; + return element; + }; + expect(scope.$eval('fn()[0] = "lucas"')).toBe('lucas'); + expect(element[0]).toBe('lucas'); + expect(count).toBe(1); + }); - forEach([true, false], function(cspEnabled) { + it('should call the function once when it is part of the context on array lookup function', function() { + var count = 0; + var element = [{anotherFn: function() { return 'lucas';} }]; + scope.fn = function() { + count++; + return element; + }; + expect(scope.$eval('fn()[0].anotherFn()')).toBe('lucas'); + expect(count).toBe(1); + }); - describe('promise logging (csp:' + cspEnabled + ')', function() { - var $log; - var PROMISE_WARNING_REGEXP = /\[\$parse\] Promise found in the expression `[^`]+`. Automatic unwrapping of promises in Angular expressions is deprecated\./; - var originalSecurityPolicy; + it('should call the function once when it is part of the context on property lookup function', function() { + var count = 0; + var element = {name: {anotherFn: function() { return 'lucas';} } }; + scope.fn = function() { + count++; + return element; + }; + expect(scope.$eval('fn().name.anotherFn()')).toBe('lucas'); + expect(count).toBe(1); + }); - beforeEach(function() { - originalSecurityPolicy = window.document.securityPolicy; - window.document.securityPolicy = {isActive : cspEnabled}; - }); + it('should call the function once when it is part of a sub-expression', function() { + var count = 0; + scope.element = [{}]; + scope.fn = function() { + count++; + return 0; + }; + expect(scope.$eval('element[fn()].name = "lucas"')).toBe('lucas'); + expect(scope.element[0].name).toBe('lucas'); + expect(count).toBe(1); + }); - afterEach(function() { - window.document.securityPolicy = originalSecurityPolicy; - }); - beforeEach(module(function($parseProvider) { - $parseProvider.unwrapPromises(true); + describe('assignable', function() { + it('should expose assignment function', inject(function($parse) { + var fn = $parse('a'); + expect(fn.assign).toBeTruthy(); + var scope = {}; + fn.assign(scope, 123); + expect(scope).toEqual({a:123}); })); + }); - beforeEach(inject(function($rootScope, $q, _$log_) { - scope = $rootScope; - - $rootScope.$apply(function() { - deferred = $q.defer(); - deferred.resolve('Bobo'); - promise = deferred.promise; - }); - $log = _$log_; + describe('locals', function() { + it('should expose local variables', inject(function($parse) { + expect($parse('a')({a: 0}, {a: 1})).toEqual(1); + expect($parse('add(a,b)')({b: 1, add: function(a, b) { return a + b; }}, {a: 2})).toEqual(3); })); - it('should log warnings by default', function() { - scope.person = promise; - scope.$eval('person'); - expect($log.warn.logs.pop()).toEqual(['[$parse] Promise found in the expression `person`. ' + - 'Automatic unwrapping of promises in Angular expressions is deprecated.']); - }); - - - it('should log warnings for deep promises', function() { - scope.car = {wheel: {disc: promise}}; - scope.$eval('car.wheel.disc.pad'); - expect($log.warn.logs.pop()).toMatch(PROMISE_WARNING_REGEXP); - }); - - - it('should log warnings for setters', function() { - scope.person = promise; - scope.$eval('person.name = "Bubu"'); - expect($log.warn.logs.pop()).toMatch(PROMISE_WARNING_REGEXP); - }); - - - it('should log only a single warning for each expression', function() { - scope.person1 = promise; - scope.person2 = promise; - - scope.$eval('person1'); - scope.$eval('person1'); - expect($log.warn.logs.pop()).toMatch(/`person1`/); - expect($log.warn.logs).toEqual([]); - - scope.$eval('person1'); - scope.$eval('person2'); - scope.$eval('person1'); - scope.$eval('person2'); - expect($log.warn.logs.pop()).toMatch(/`person2`/); - expect($log.warn.logs).toEqual([]); - }); - - - it('should log warning for complex expressions', function() { - scope.person1 = promise; - scope.person2 = promise; + it('should expose traverse locals', inject(function($parse) { + expect($parse('a.b')({a: {b: 0}}, {a: {b:1}})).toEqual(1); + expect($parse('a.b')({a: null}, {a: {b:1}})).toEqual(1); + expect($parse('a.b')({a: {b: 0}}, {a: null})).toEqual(undefined); + expect($parse('a.b.c')({a: null}, {a: {b: {c: 1}}})).toEqual(1); + })); - scope.$eval('person1 + person2'); - expect($log.warn.logs.pop()).toMatch(/`person1 \+ person2`/); - expect($log.warn.logs).toEqual([]); - }); + it('should not use locals to resolve object properties', inject(function($parse) { + expect($parse('a[0].b')({a: [ {b : 'scope'} ]}, {b : 'locals'})).toBe('scope'); + expect($parse('a[0]["b"]')({a: [ {b : 'scope'} ]}, {b : 'locals'})).toBe('scope'); + expect($parse('a[0][0].b')({a: [[{b : 'scope'}]]}, {b : 'locals'})).toBe('scope'); + expect($parse('a[0].b.c')({a: [ {b: {c: 'scope'}}] }, {b : {c: 'locals'} })).toBe('scope'); + })); }); - }); - - forEach([true, false], function(cspEnabled) { - - describe('csp ' + cspEnabled, function() { - - var originalSecurityPolicy; - - - beforeEach(function() { - originalSecurityPolicy = window.document.securityPolicy; - window.document.securityPolicy = {isActive : cspEnabled}; - }); - - afterEach(function() { - window.document.securityPolicy = originalSecurityPolicy; - }); - - - beforeEach(module(function($parseProvider) { - $parseProvider.unwrapPromises(true); - $parseProvider.logPromiseWarnings(false); + describe('literal', function() { + it('should mark scalar value expressions as literal', inject(function($parse) { + expect($parse('0').literal).toBe(true); + expect($parse('"hello"').literal).toBe(true); + expect($parse('true').literal).toBe(true); + expect($parse('false').literal).toBe(true); + expect($parse('null').literal).toBe(true); + expect($parse('undefined').literal).toBe(true); })); - - beforeEach(inject(function($rootScope, $q) { - scope = $rootScope; - - q = $q; - deferred = q.defer(); - promise = deferred.promise; + it('should mark array expressions as literal', inject(function($parse) { + expect($parse('[]').literal).toBe(true); + expect($parse('[1, 2, 3]').literal).toBe(true); + expect($parse('[1, identifier]').literal).toBe(true); })); + it('should mark object expressions as literal', inject(function($parse) { + expect($parse('{}').literal).toBe(true); + expect($parse('{x: 1}').literal).toBe(true); + expect($parse('{foo: bar}').literal).toBe(true); + })); - describe('{{promise}}', function() { - it('should evaluated resolved promise and get its value', function() { - deferred.resolve('hello!'); - scope.greeting = promise; - expect(scope.$eval('greeting')).toBe(undefined); - scope.$digest(); - expect(scope.$eval('greeting')).toBe('hello!'); - }); - - - it('should evaluated rejected promise and ignore the rejection reason', function() { - deferred.reject('sorry'); - scope.greeting = promise; - expect(scope.$eval('greeting')).toBe(undefined); - scope.$digest(); - expect(scope.$eval('greeting')).toBe(undefined); - }); - - - it('should evaluate a promise and eventualy get its value', function() { - scope.greeting = promise; - expect(scope.$eval('greeting')).toBe(undefined); - - scope.$digest(); - expect(scope.$eval('greeting')).toBe(undefined); - - deferred.resolve('hello!'); - expect(scope.$eval('greeting')).toBe(undefined); - scope.$digest(); - expect(scope.$eval('greeting')).toBe('hello!'); - }); - - - it('should evaluate a promise and eventualy ignore its rejection', function() { - scope.greeting = promise; - expect(scope.$eval('greeting')).toBe(undefined); - - scope.$digest(); - expect(scope.$eval('greeting')).toBe(undefined); - - deferred.reject('sorry'); - expect(scope.$eval('greeting')).toBe(undefined); - scope.$digest(); - expect(scope.$eval('greeting')).toBe(undefined); - }); - - - describe('assignment into promises', function() { - // This behavior is analogous to assignments to non-promise values - // that are lazily set on the scope. - it('should evaluate a resolved object promise and set its value', inject(function($parse) { - scope.person = promise; - deferred.resolve({'name': 'Bill Gates'}); - - var getter = $parse('person.name', { unwrapPromises: true }); - expect(getter(scope)).toBe(undefined); - - scope.$digest(); - expect(getter(scope)).toBe('Bill Gates'); - getter.assign(scope, 'Warren Buffet'); - expect(getter(scope)).toBe('Warren Buffet'); - })); - - - it('should evaluate a resolved primitive type promise and set its value', inject(function($parse) { - scope.greeting = promise; - deferred.resolve('Salut!'); - - var getter = $parse('greeting', { unwrapPromises: true }); - expect(getter(scope)).toBe(undefined); - - scope.$digest(); - expect(getter(scope)).toBe('Salut!'); - - getter.assign(scope, 'Bonjour'); - expect(getter(scope)).toBe('Bonjour'); - })); - - - it('should evaluate an unresolved promise and set and remember its value', inject(function($parse) { - scope.person = promise; - - var getter = $parse('person.name', { unwrapPromises: true }); - expect(getter(scope)).toBe(undefined); - - scope.$digest(); - expect(getter(scope)).toBe(undefined); - - getter.assign(scope, 'Bonjour'); - scope.$digest(); - - expect(getter(scope)).toBe('Bonjour'); - - var c1Getter = $parse('person.A.B.C1', { unwrapPromises: true }); - scope.$digest(); - expect(c1Getter(scope)).toBe(undefined); - c1Getter.assign(scope, 'c1_value'); - scope.$digest(); - expect(c1Getter(scope)).toBe('c1_value'); - - // Set another property on the person.A.B - var c2Getter = $parse('person.A.B.C2', { unwrapPromises: true }); - scope.$digest(); - expect(c2Getter(scope)).toBe(undefined); - c2Getter.assign(scope, 'c2_value'); - scope.$digest(); - expect(c2Getter(scope)).toBe('c2_value'); - - // c1 should be unchanged. - expect($parse('person.A', { unwrapPromises: true })(scope)).toEqual( - {B: {C1: 'c1_value', C2: 'c2_value'}}); - })); - - - it('should evaluate a resolved promise and overwrite the previous set value in the absence of the getter', - inject(function($parse) { - scope.person = promise; - var c1Getter = $parse('person.A.B.C1', { unwrapPromises: true }); - c1Getter.assign(scope, 'c1_value'); - // resolving the promise should update the tree. - deferred.resolve({A: {B: {C1: 'resolved_c1'}}}); - scope.$digest(); - expect(c1Getter(scope)).toEqual('resolved_c1'); - })); - }); - }); - - describe('dereferencing', function() { - it('should evaluate and dereference properties leading to and from a promise', function() { - scope.obj = {greeting: promise}; - expect(scope.$eval('obj.greeting')).toBe(undefined); - expect(scope.$eval('obj.greeting.polite')).toBe(undefined); - - scope.$digest(); - expect(scope.$eval('obj.greeting')).toBe(undefined); - expect(scope.$eval('obj.greeting.polite')).toBe(undefined); - - deferred.resolve({polite: 'Good morning!'}); - scope.$digest(); - expect(scope.$eval('obj.greeting')).toEqual({polite: 'Good morning!'}); - expect(scope.$eval('obj.greeting.polite')).toBe('Good morning!'); - }); - - it('should evaluate and dereference properties leading to and from a promise via bracket ' + - 'notation', function() { - scope.obj = {greeting: promise}; - expect(scope.$eval('obj["greeting"]')).toBe(undefined); - expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined); - - scope.$digest(); - expect(scope.$eval('obj["greeting"]')).toBe(undefined); - expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined); - - deferred.resolve({polite: 'Good morning!'}); - scope.$digest(); - expect(scope.$eval('obj["greeting"]')).toEqual({polite: 'Good morning!'}); - expect(scope.$eval('obj["greeting"]["polite"]')).toBe('Good morning!'); - }); - - - it('should evaluate and dereference array references leading to and from a promise', - function() { - scope.greetings = [promise]; - expect(scope.$eval('greetings[0]')).toBe(undefined); - expect(scope.$eval('greetings[0][0]')).toBe(undefined); - - scope.$digest(); - expect(scope.$eval('greetings[0]')).toBe(undefined); - expect(scope.$eval('greetings[0][0]')).toBe(undefined); - - deferred.resolve(['Hi!', 'Cau!']); - scope.$digest(); - expect(scope.$eval('greetings[0]')).toEqual(['Hi!', 'Cau!']); - expect(scope.$eval('greetings[0][0]')).toBe('Hi!'); - }); - + it('should not mark function calls or operator expressions as literal', inject(function($parse) { + expect($parse('1 + 1').literal).toBe(false); + expect($parse('call()').literal).toBe(false); + expect($parse('[].length').literal).toBe(false); + })); + }); - it('should evaluate and dereference promises used as function arguments', function() { - scope.greet = function(name) { return 'Hi ' + name + '!'; }; - scope.name = promise; - expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); + describe('constant', function() { + it('should mark scalar value expressions as constant', inject(function($parse) { + expect($parse('12.3').constant).toBe(true); + expect($parse('"string"').constant).toBe(true); + expect($parse('true').constant).toBe(true); + expect($parse('false').constant).toBe(true); + expect($parse('null').constant).toBe(true); + expect($parse('undefined').constant).toBe(true); + })); - scope.$digest(); - expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); + it('should mark arrays as constant if they only contain constant elements', inject(function($parse) { + expect($parse('[]').constant).toBe(true); + expect($parse('[1, 2, 3]').constant).toBe(true); + expect($parse('["string", null]').constant).toBe(true); + expect($parse('[[]]').constant).toBe(true); + expect($parse('[1, [2, 3], {4: 5}]').constant).toBe(true); + })); - deferred.resolve('Veronica'); - expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); + it('should not mark arrays as constant if they contain any non-constant elements', inject(function($parse) { + expect($parse('[foo]').constant).toBe(false); + expect($parse('[x + 1]').constant).toBe(false); + expect($parse('[bar[0]]').constant).toBe(false); + })); - scope.$digest(); - expect(scope.$eval('greet(name)')).toBe('Hi Veronica!'); - }); + it('should mark complex expressions involving constant values as constant', inject(function($parse) { + expect($parse('!true').constant).toBe(true); + expect($parse('-42').constant).toBe(true); + expect($parse('1 - 1').constant).toBe(true); + expect($parse('"foo" + "bar"').constant).toBe(true); + expect($parse('5 != null').constant).toBe(true); + expect($parse('{standard: 4/3, wide: 16/9}').constant).toBe(true); + })); + it('should not mark any expression involving variables or function calls as constant', inject(function($parse) { + expect($parse('true.toString()').constant).toBe(false); + expect($parse('foo(1, 2, 3)').constant).toBe(false); + expect($parse('"name" + id').constant).toBe(false); + })); + }); - it('should evaluate and dereference promises used as array indexes', function() { - scope.childIndex = promise; - scope.kids = ['Adam', 'Veronica', 'Elisa']; - expect(scope.$eval('kids[childIndex]')).toBe(undefined); + describe('null/undefined in expressions', function() { + // simpleGetterFn1 + it('should return null for `a` where `a` is null', inject(function($rootScope) { + $rootScope.a = null; + expect($rootScope.$eval('a')).toBe(null); + })); - scope.$digest(); - expect(scope.$eval('kids[childIndex]')).toBe(undefined); + it('should return undefined for `a` where `a` is undefined', inject(function($rootScope) { + expect($rootScope.$eval('a')).toBeUndefined(); + })); - deferred.resolve(1); - expect(scope.$eval('kids[childIndex]')).toBe(undefined); + // simpleGetterFn2 + it('should return undefined for properties of `null` constant', inject(function($rootScope) { + expect($rootScope.$eval('null.a')).toBeUndefined(); + })); - scope.$digest(); - expect(scope.$eval('kids[childIndex]')).toBe('Veronica'); - }); + it('should return undefined for properties of `null` values', inject(function($rootScope) { + $rootScope.a = null; + expect($rootScope.$eval('a.b')).toBeUndefined(); + })); + it('should return null for `a.b` where `b` is null', inject(function($rootScope) { + $rootScope.a = { b: null }; + expect($rootScope.$eval('a.b')).toBe(null); + })); - it('should evaluate and dereference promises used as keys in bracket notation', function() { - scope.childKey = promise; - scope.kids = {'a': 'Adam', 'v': 'Veronica', 'e': 'Elisa'}; + // cspSafeGetter && pathKeys.length < 6 || pathKeys.length > 2 + it('should return null for `a.b.c.d.e` where `e` is null', inject(function($rootScope) { + $rootScope.a = { b: { c: { d: { e: null } } } }; + expect($rootScope.$eval('a.b.c.d.e')).toBe(null); + })); - expect(scope.$eval('kids[childKey]')).toBe(undefined); + it('should return undefined for `a.b.c.d.e` where `d` is null', inject(function($rootScope) { + $rootScope.a = { b: { c: { d: null } } }; + expect($rootScope.$eval('a.b.c.d.e')).toBeUndefined(); + })); - scope.$digest(); - expect(scope.$eval('kids[childKey]')).toBe(undefined); + // cspSafeGetter || pathKeys.length > 6 + it('should return null for `a.b.c.d.e.f.g` where `g` is null', inject(function($rootScope) { + $rootScope.a = { b: { c: { d: { e: { f: { g: null } } } } } }; + expect($rootScope.$eval('a.b.c.d.e.f.g')).toBe(null); + })); - deferred.resolve('v'); - expect(scope.$eval('kids[childKey]')).toBe(undefined); + it('should return undefined for `a.b.c.d.e.f.g` where `f` is null', inject(function($rootScope) { + $rootScope.a = { b: { c: { d: { e: { f: null } } } } }; + expect($rootScope.$eval('a.b.c.d.e.f.g')).toBeUndefined(); + })); - scope.$digest(); - expect(scope.$eval('kids[childKey]')).toBe('Veronica'); - }); + 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 not mess with the promise if it was not directly evaluated', function() { - scope.obj = {greeting: promise, username: 'hi'}; - var obj = scope.$eval('obj'); - expect(obj.username).toEqual('hi'); - expect(typeof obj.greeting.then).toBe('function'); - }); - }); + it('should ignore undefined values when doing addition/concatenation', + inject(function($rootScope) { + $rootScope.fn = function() {}; + expect($rootScope.$eval('foo + "bar" + fn()')).toBe('bar'); + })); }); }); });