aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/ng/parse.js332
-rw-r--r--test/ng/parseSpec.js363
2 files changed, 484 insertions, 211 deletions
diff --git a/src/ng/parse.js b/src/ng/parse.js
index 701647c5..aad740e2 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;
@@ -759,7 +762,7 @@ Parser.prototype = {
: fnPtr(args[0], args[1], args[2], args[3], args[4]);
// Check for promise
- if (v && v.then) {
+ if (v && v.then && parser.options.unwrapPromises) {
var p = v;
if (!('$$v' in v)) {
p.$$v = undefined;
@@ -827,7 +830,7 @@ Parser.prototype = {
literal: true,
constant: allConstant
});
- },
+ }
};
@@ -835,7 +838,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 +851,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 +876,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 +984,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 +1010,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 +1080,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 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;
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 19182332..277178a1 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);
});
@@ -929,7 +1149,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 +1163,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 +1177,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 +1188,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 +1196,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 +1204,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 +1257,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 +1329,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);
- }));
- });
});
});
});