diff --git a/src/ng/parse.js b/src/ng/parse.js index 701647c50fea..6187762688e3 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -1,6 +1,8 @@ 'use strict'; var $parseMinErr = minErr('$parse'); +var promiseWarningCache = {}; +var promiseWarning; // Sandboxing Angular Expressions // ------------------------------ @@ -99,8 +101,8 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"' /** * @constructor */ -var Lexer = function (csp) { - this.csp = csp; +var Lexer = function (options) { + this.options = options; }; Lexer.prototype = { @@ -108,6 +110,7 @@ Lexer.prototype = { lex: function (text) { this.text = text; + this.index = 0; this.ch = undefined; this.lastCh = ':'; // can start regexp @@ -295,12 +298,12 @@ Lexer.prototype = { token.fn = OPERATORS[ident]; token.json = OPERATORS[ident]; } else { - var getter = getterFn(ident, this.csp, this.text); + var getter = getterFn(ident, this.options, this.text); token.fn = extend(function(self, locals) { return (getter(self, locals)); }, { assign: function(self, value) { - return setter(self, ident, value, parser.text); + return setter(self, ident, value, parser.text, parser.options); } }); } @@ -371,10 +374,10 @@ Lexer.prototype = { /** * @constructor */ -var Parser = function (lexer, $filter, csp) { +var Parser = function (lexer, $filter, options) { this.lexer = lexer; this.$filter = $filter; - this.csp = csp; + this.options = options; }; Parser.ZERO = function () { return 0; }; @@ -388,7 +391,7 @@ Parser.prototype = { //TODO(i): strip all the obsolte json stuff from this file this.json = json; - this.tokens = this.lexer.lex(text, this.csp); + this.tokens = this.lexer.lex(text); if (json) { // The extra level of aliasing is here, just in case the lexer misses something, so that @@ -688,13 +691,13 @@ Parser.prototype = { fieldAccess: function(object) { var parser = this; var field = this.expect().text; - var getter = getterFn(field, this.csp, this.text); + var getter = getterFn(field, this.options, this.text); return extend(function(scope, locals, self) { return getter(self || object(scope, locals), locals); }, { assign: function(scope, value, locals) { - return setter(object(scope, locals), field, value, parser.text); + return setter(object(scope, locals), field, value, parser.text, parser.options); } }); }, @@ -712,7 +715,7 @@ Parser.prototype = { if (!o) return undefined; v = ensureSafeObject(o[i], parser.text); - if (v && v.then) { + if (v && v.then && parser.options.unwrapPromises) { p = v; if (!('$$v' in v)) { p.$$v = undefined; @@ -758,16 +761,6 @@ Parser.prototype = { ? fnPtr.apply(context, args) : fnPtr(args[0], args[1], args[2], args[3], args[4]); - // Check for promise - if (v && v.then) { - var p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; - } - return ensureSafeObject(v, parser.text); }; }, @@ -827,7 +820,7 @@ Parser.prototype = { literal: true, constant: allConstant }); - }, + } }; @@ -835,7 +828,10 @@ Parser.prototype = { // Parser helper functions ////////////////////////////////////////////////// -function setter(obj, path, setValue, fullExp) { +function setter(obj, path, setValue, fullExp, options) { + //needed? + options = options || {}; + var element = path.split('.'), key; for (var i = 0; element.length > 1; i++) { key = ensureSafeMemberName(element.shift(), fullExp); @@ -845,7 +841,8 @@ function setter(obj, path, setValue, fullExp) { obj[key] = propertyObj; } obj = propertyObj; - if (obj.then) { + if (obj.then && options.unwrapPromises) { + promiseWarning(fullExp); if (!("$$v" in obj)) { (function(promise) { promise.then(function(val) { promise.$$v = val; }); } @@ -869,76 +866,103 @@ var getterFnCache = {}; * - http://jsperf.com/angularjs-parse-getter/4 * - http://jsperf.com/path-evaluation-simplified/7 */ -function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { +function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) { ensureSafeMemberName(key0, fullExp); ensureSafeMemberName(key1, fullExp); ensureSafeMemberName(key2, fullExp); ensureSafeMemberName(key3, fullExp); ensureSafeMemberName(key4, fullExp); - return function(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, - promise; - - if (pathVal === null || pathVal === undefined) return pathVal; - - pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key1 || pathVal === null || pathVal === undefined) return pathVal; - - pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key2 || pathVal === null || pathVal === undefined) return pathVal; - - pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key3 || pathVal === null || pathVal === undefined) return pathVal; - - pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - if (!key4 || pathVal === null || pathVal === undefined) return pathVal; - - pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - return pathVal; - }; + + return !options.unwrapPromises + ? function cspSafeGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; + + if (pathVal === null || pathVal === undefined) return pathVal; + pathVal = pathVal[key0]; + + if (!key1 || pathVal === null || pathVal === undefined) return pathVal; + pathVal = pathVal[key1]; + + if (!key2 || pathVal === null || pathVal === undefined) return pathVal; + pathVal = pathVal[key2]; + + if (!key3 || pathVal === null || pathVal === undefined) return pathVal; + pathVal = pathVal[key3]; + + if (!key4 || pathVal === null || pathVal === undefined) return pathVal; + pathVal = pathVal[key4]; + + return pathVal; + } + : function cspSafePromiseEnabledGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, + promise; + + if (pathVal === null || pathVal === undefined) 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 (!key1 || pathVal === null || pathVal === undefined) return pathVal; + + 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 || pathVal === null || pathVal === undefined) return pathVal; + + 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 || pathVal === null || pathVal === undefined) return pathVal; + + 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 || pathVal === null || pathVal === undefined) return pathVal; + + 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; + } } -function getterFn(path, csp, fullExp) { +function getterFn(path, options, fullExp) { // Check whether the cache has this getter already. // We can use hasOwnProperty directly on the cache because we ensure, // see below, that the cache never stores a path called 'hasOwnProperty' @@ -950,14 +974,14 @@ function getterFn(path, csp, fullExp) { pathKeysLength = pathKeys.length, fn; - if (csp) { + if (options.csp) { fn = (pathKeysLength < 6) - ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp) + ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, options) : function(scope, locals) { var i = 0, val; do { val = cspSafeGetterFn( - pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp + pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp, options )(scope, locals); locals = undefined; // clear after first iteration @@ -976,18 +1000,25 @@ function getterFn(path, csp, fullExp) { ? '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' + - 'if (s && s.then) {\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'; + (options.unwrapPromises + ? 'if (s && s.then) {\n' + + ' pw("' + fullExp.replace(/\"/g, '\\"') + '");\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' + : ''); }); code += 'return s;'; - fn = Function('s', 'k', code); // s=scope, k=locals - fn.toString = function() { return code; }; + + var evaledFnGetter = Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning + evaledFnGetter.toString = function() { return code; }; + fn = function(scope, locals) { + return evaledFnGetter(scope, locals, promiseWarning); + }; } // Only cache the value if it's not going to mess up the cache object @@ -1039,20 +1070,125 @@ function getterFn(path, csp, fullExp) { * set to a function to change its value on the given context. * */ + + +/** + * @ngdoc object + * @name ng.$parseProvider + * @function + * + * @description + * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} service. + */ function $ParseProvider() { var cache = {}; - this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { + + 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 ng.$parseProvider#unwrapPromises + * @methodOf ng.$parseProvider + * @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 ng.$parseProvider#logPromiseWarnings + * @methodOf ng.$parseProvider + * @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; + } + }; + + + this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { + $parseOptions.csp = $sniffer.csp; + + promiseWarning = function promiseWarning(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; switch (typeof exp) { case 'string': + if (cache.hasOwnProperty(exp)) { return cache[exp]; } - var lexer = new Lexer($sniffer.csp); - var parser = new Parser(lexer, $filter, $sniffer.csp); + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); parsedExpression = parser.parse(exp, false); if (exp !== 'hasOwnProperty') { diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 191823326b47..940bd6d6f6e6 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -1,12 +1,20 @@ 'use strict'; describe('parser', function() { + + beforeEach(function() { + // clear caches + getterFnCache = {}; + promiseWarningCache = {}; + }); + + describe('lexer', function() { var lex; beforeEach(function () { lex = function () { - var lexer = new Lexer(); + var lexer = new Lexer({csp: false, unwrapPromises: false}); return lexer.lex.apply(lexer, arguments); }; }); @@ -198,7 +206,6 @@ describe('parser', function() { beforeEach(inject(function ($rootScope, $sniffer) { scope = $rootScope; $sniffer.csp = cspEnabled; - getterFnCache = {}; // clear cache })); @@ -854,15 +861,228 @@ describe('parser', function() { }); - describe('promises', function() { - var deferred, promise, q; + 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}); + })); + }); + + + 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); + })); + }); + + 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); + })); + + 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); + })); + }); + + 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 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); + })); + + 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 mark complex expressions involving constant values as constant', inject(function($parse) { + expect($parse('!true').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); + })); + }); + }); + }); + + + describe('promises', function() { + + var deferred, promise, q; + + describe('unwrapPromises setting', function () { + + beforeEach(inject(function($rootScope, $q) { + scope = $rootScope; + + $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; }; + + 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); + })); + }); + + + forEach([true, false], function(cspEnabled) { + + 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\./; + + beforeEach(module(function($parseProvider) { + $parseProvider.unwrapPromises(true); + })); + + beforeEach(inject(function($rootScope, $q, _$log_) { + scope = $rootScope; + + $rootScope.$apply(function() { + deferred = $q.defer(); + deferred.resolve('Bobo'); + promise = deferred.promise; + }); + + $log = _$log_; + })); + + 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; + + scope.$eval('person1 + person2'); + expect($log.warn.logs.pop()).toMatch(/`person1 \+ person2`/); + expect($log.warn.logs).toEqual([]); + }); + }); + }); + + + forEach([true, false], function(cspEnabled) { + + describe('csp ' + cspEnabled, function() { + + beforeEach(module(function($parseProvider) { + $parseProvider.unwrapPromises(true); + $parseProvider.logPromiseWarnings(false); + })); + + + beforeEach(inject(function($rootScope, $sniffer, $q) { + scope = $rootScope; + $sniffer.csp = cspEnabled; - beforeEach(inject(function($q) { q = $q; deferred = q.defer(); promise = deferred.promise; })); + describe('{{promise}}', function() { it('should evaluated resolved promise and get its value', function() { deferred.resolve('hello!'); @@ -876,7 +1096,7 @@ describe('parser', function() { it('should evaluated rejected promise and ignore the rejection reason', function() { deferred.reject('sorry'); scope.greeting = promise; - expect(scope.$eval('gretting')).toBe(undefined); + expect(scope.$eval('greeting')).toBe(undefined); scope.$digest(); expect(scope.$eval('greeting')).toBe(undefined); }); @@ -909,18 +1129,6 @@ describe('parser', function() { expect(scope.$eval('greeting')).toBe(undefined); }); - it('should evaluate a function call returning a promise and eventually get its return value', function() { - scope.greetingFn = function() { return promise; }; - expect(scope.$eval('greetingFn()')).toBe(undefined); - - scope.$digest(); - expect(scope.$eval('greetingFn()')).toBe(undefined); - - deferred.resolve('hello!'); - expect(scope.$eval('greetingFn()')).toBe(undefined); - scope.$digest(); - expect(scope.$eval('greetingFn()')).toBe('hello!'); - }); describe('assignment into promises', function() { // This behavior is analogous to assignments to non-promise values @@ -929,7 +1137,7 @@ describe('parser', function() { scope.person = promise; deferred.resolve({'name': 'Bill Gates'}); - var getter = $parse('person.name'); + var getter = $parse('person.name', { unwrapPromises: true }); expect(getter(scope)).toBe(undefined); scope.$digest(); @@ -943,7 +1151,7 @@ describe('parser', function() { scope.greeting = promise; deferred.resolve('Salut!'); - var getter = $parse('greeting'); + var getter = $parse('greeting', { unwrapPromises: true }); expect(getter(scope)).toBe(undefined); scope.$digest(); @@ -957,7 +1165,7 @@ describe('parser', function() { it('should evaluate an unresolved promise and set and remember its value', inject(function($parse) { scope.person = promise; - var getter = $parse('person.name'); + var getter = $parse('person.name', { unwrapPromises: true }); expect(getter(scope)).toBe(undefined); scope.$digest(); @@ -968,7 +1176,7 @@ describe('parser', function() { expect(getter(scope)).toBe('Bonjour'); - var c1Getter = $parse('person.A.B.C1'); + var c1Getter = $parse('person.A.B.C1', { unwrapPromises: true }); scope.$digest(); expect(c1Getter(scope)).toBe(undefined); c1Getter.assign(scope, 'c1_value'); @@ -976,7 +1184,7 @@ describe('parser', function() { expect(c1Getter(scope)).toBe('c1_value'); // Set another property on the person.A.B - var c2Getter = $parse('person.A.B.C2'); + var c2Getter = $parse('person.A.B.C2', { unwrapPromises: true }); scope.$digest(); expect(c2Getter(scope)).toBe(undefined); c2Getter.assign(scope, 'c2_value'); @@ -984,15 +1192,15 @@ describe('parser', function() { expect(c2Getter(scope)).toBe('c2_value'); // c1 should be unchanged. - expect($parse('person.A')(scope)).toEqual( + 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 absense of the getter', - inject(function($parse) { + inject(function($parse) { scope.person = promise; - var c1Getter = $parse('person.A.B.C1'); + 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'}}}); @@ -1037,19 +1245,19 @@ describe('parser', function() { 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.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); + 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!'); - }); + deferred.resolve(['Hi!', 'Cau!']); + scope.$digest(); + expect(scope.$eval('greetings[0]')).toEqual(['Hi!', 'Cau!']); + expect(scope.$eval('greetings[0][0]')).toBe('Hi!'); + }); it('should evaluate and dereference promises used as function arguments', function() { @@ -1109,99 +1317,6 @@ describe('parser', function() { }); }); }); - - - 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}); - })); - }); - - - 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); - })); - }); - - 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); - })); - - 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); - })); - }); - - 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 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); - })); - - 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 mark complex expressions involving constant values as constant', inject(function($parse) { - expect($parse('!true').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); - })); - }); }); }); });