diff --git a/angularFiles.js b/angularFiles.js index d8be657aed0a..fb332a8a8c16 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -45,6 +45,7 @@ angularFiles = { 'src/ng/directive/ngClass.js', 'src/ng/directive/ngCloak.js', 'src/ng/directive/ngController.js', + 'src/ng/directive/ngCsp.js', 'src/ng/directive/ngEventDirs.js', 'src/ng/directive/ngInclude.js', 'src/ng/directive/ngInit.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 834fd04a3a08..a912448257ea 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -76,6 +76,7 @@ function publishExternalAPI(angular){ ngClass: ngClassDirective, ngClassEven: ngClassEvenDirective, ngClassOdd: ngClassOddDirective, + ngCsp: ngCspDirective, ngCloak: ngCloakDirective, ngController: ngControllerDirective, ngForm: ngFormDirective, diff --git a/src/ng/directive/ngCsp.js b/src/ng/directive/ngCsp.js new file mode 100644 index 000000000000..d4a3a45d0a06 --- /dev/null +++ b/src/ng/directive/ngCsp.js @@ -0,0 +1,26 @@ +'use strict'; + +/** + * TODO(i): this directive is not publicly documented until we know for sure that CSP can't be + * safely feature-detected. + * + * @name angular.module.ng.$compileProvider.directive.ngCsp + * @priority 1000 + * + * @description + * Enables CSP (Content Security Protection) support. This directive should be used on the `` + * element before any kind of interpolation or expression is processed. + * + * If enabled the performance of $parse will suffer. + * + * @element html + */ + +var ngCspDirective = ['$sniffer', function($sniffer) { + return { + priority: 1000, + compile: function() { + $sniffer.csp = true; + } + }; +}]; diff --git a/src/ng/parse.js b/src/ng/parse.js index e5cb55d7f270..a367c2918b66 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -27,7 +27,7 @@ var OPERATORS = { }; var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; -function lex(text){ +function lex(text, csp){ var tokens = [], token, index = 0, @@ -187,7 +187,7 @@ function lex(text){ if (OPERATORS.hasOwnProperty(ident)) { token.fn = token.json = OPERATORS[ident]; } else { - var getter = getterFn(ident); + var getter = getterFn(ident, csp); token.fn = extend(function(self, locals) { return (getter(self, locals)); }, { @@ -261,10 +261,10 @@ function lex(text){ ///////////////////////////////////////// -function parser(text, json, $filter){ +function parser(text, json, $filter, csp){ var ZERO = valueFn(0), value, - tokens = lex(text), + tokens = lex(text, csp), assignment = _assignment, functionCall = _functionCall, fieldAccess = _fieldAccess, @@ -532,7 +532,7 @@ function parser(text, json, $filter){ function _fieldAccess(object) { var field = expect().text; - var getter = getterFn(field); + var getter = getterFn(field, csp); return extend( function(self, locals) { return getter(object(self, locals), locals); @@ -685,32 +685,119 @@ function getter(obj, path, bindFnToScope) { var getterFnCache = {}; -function getterFn(path) { +/** + * Implementation of the "Black Hole" variant from: + * - http://jsperf.com/angularjs-parse-getter/4 + * - http://jsperf.com/path-evaluation-simplified/7 + */ +function cspSafeGetterFn(key0, key1, key2, key3, key4) { + return function(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, + promise; + + if (!pathVal) 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) 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) 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) 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) 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; + }; +}; + +function getterFn(path, csp) { if (getterFnCache.hasOwnProperty(path)) { return getterFnCache[path]; } - var fn, code = 'var l, fn, p;\n'; - forEach(path.split('.'), function(key, index) { - code += 'if(!s) return s;\n' + - 'l=s;\n' + - 's='+ (index - // we simply direference 's' on any .dot notation - ? 's' - // but if we are first then we check locals firs, 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'; - }); - code += 'return s;'; - fn = Function('s', 'k', code); - fn.toString = function() { return code; }; + var pathKeys = path.split('.'), + pathKeysLength = pathKeys.length, + fn; + + if (csp) { + fn = (pathKeysLength < 6) + ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4]) + : function(scope, locals) { + var i = 0, val; + do { + val = cspSafeGetterFn( + pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++] + )(scope, locals); + locals = undefined; // clear after first iteration + } while (i < pathKeysLength); + }; + } else { + var code = 'var l, fn, p;\n'; + forEach(pathKeys, function(key, index) { + code += 'if(!s) return s;\n' + + 'l=s;\n' + + 's='+ (index + // 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' + + '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'; + }); + code += 'return s;'; + fn = Function('s', 'k', code); // s=scope, k=locals + fn.toString = function() { return code; }; + } return getterFnCache[path] = fn; } @@ -719,13 +806,13 @@ function getterFn(path) { function $ParseProvider() { var cache = {}; - this.$get = ['$filter', function($filter) { + this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { return function(exp) { switch(typeof exp) { case 'string': return cache.hasOwnProperty(exp) ? cache[exp] - : cache[exp] = parser(exp, false, $filter); + : cache[exp] = parser(exp, false, $filter, $sniffer.csp); case 'function': return exp; default: diff --git a/src/ng/sniffer.js b/src/ng/sniffer.js index 3249b816e7c7..5389dc863a50 100644 --- a/src/ng/sniffer.js +++ b/src/ng/sniffer.js @@ -28,7 +28,9 @@ function $SnifferProvider() { } return eventSupport[event]; - } + }, + // TODO(i): currently there is no way to feature detect CSP without triggering alerts + csp: false }; }]; } diff --git a/test/ng/directive/ngCspSpec.js b/test/ng/directive/ngCspSpec.js new file mode 100644 index 000000000000..7a21b587c6c3 --- /dev/null +++ b/test/ng/directive/ngCspSpec.js @@ -0,0 +1,10 @@ +'use strict'; + +describe('ngCsp', function() { + + it('it should turn on CSP mode in $sniffer', inject(function($sniffer, $compile) { + expect($sniffer.csp).toBe(false); + $compile('
'); + expect($sniffer.csp).toBe(true); + })); +}); diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index c98b180c91db..947dd322eb91 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -165,467 +165,487 @@ describe('parser', function() { }); }); - var scope, $filterProvider; + var $filterProvider, scope; + beforeEach(module(['$filterProvider', function (filterProvider) { $filterProvider = filterProvider; }])); - beforeEach(inject(function ($rootScope) { - scope = $rootScope; - })); - - it('should parse expressions', function() { - 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() { - expect(scope.$eval("false")).toBeFalsy(); - expect(scope.$eval("!true")).toBeFalsy(); - 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); - }); - 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); - }); + forEach([true, false], function(cspEnabled) { - 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); + beforeEach(inject(function ($rootScope, $sniffer) { + scope = $rootScope; + $sniffer.csp = cspEnabled; })); - expect(function() { - scope.$eval("1|nonexistent"); - }).toThrow(new Error("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 parse expressions', function() { + 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 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(); - }); + it('should parse comparison', function() { + expect(scope.$eval("false")).toBeFalsy(); + expect(scope.$eval("!true")).toBeFalsy(); + 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); + }); - it('should support property names that colide with native object properties', function() { - // regression - scope.watch = 1; - scope.constructor = 2; - scope.toString = 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); + }); - expect(scope.$eval('watch', scope)).toBe(1); - expect(scope.$eval('constructor', scope)).toBe(2); - expect(scope.$eval('toString', scope)).toBe(3); - }); + it('should parse string', function() { + expect(scope.$eval("'a' + 'b c'")).toEqual("ab c"); + }); - it('should evaluate grouped expressions', function() { - expect(scope.$eval("(1+2)*3")).toEqual((1+2)*3); - }); + it('should parse filters', function() { + $filterProvider.register('substring', valueFn(function(input, start, end) { + return input.substring(start, end); + })); - it('should evaluate assignments', function() { - expect(scope.$eval("a=12")).toEqual(12); - expect(scope.a).toEqual(12); + expect(function() { + scope.$eval("1|nonexistent"); + }).toThrow(new Error("Unknown provider: nonexistentFilterProvider <- nonexistentFilter")); - expect(scope.$eval("x.y.z=123;")).toEqual(123); - expect(scope.x.y.z).toEqual(123); + scope.offset = 3; + expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc"); + expect(scope.$eval("'abcd'|substring:1:3|uppercase")).toEqual("BC"); + }); - expect(scope.$eval("a=123; b=234")).toEqual(234); - expect(scope.a).toEqual(123); - expect(scope.b).toEqual(234); - }); + 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(); + }); - it('should evaluate function call without arguments', function() { - scope['const'] = function(a,b){return 123;}; - expect(scope.$eval("const()")).toEqual(123); - }); + 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 evaluate function call with arguments', function() { - scope.add = function(a,b) { - return a+b; - }; - expect(scope.$eval("add(1,2)")).toEqual(3); - }); + 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 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 support property names that collide with native object properties', function() { + // regression + scope.watch = 1; + scope.constructor = 2; + scope.toString = 3; - 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); - }); + expect(scope.$eval('watch', scope)).toBe(1); + expect(scope.$eval('constructor', scope)).toBe(2); + expect(scope.$eval('toString', scope)).toBe(3); + }); - 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); - }); + it('should evaluate grouped expressions', function() { + expect(scope.$eval("(1+2)*3")).toEqual((1+2)*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 assignments', function() { + expect(scope.$eval("a=12")).toEqual(12); + expect(scope.a).toEqual(12); - 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(scope.$eval("x.y.z=123;")).toEqual(123); + expect(scope.x.y.z).toEqual(123); - it('should evaluate object access', function() { - expect(scope.$eval("{false:'WC', true:'CC'}[false]")).toEqual("WC"); - }); + expect(scope.$eval("a=123; b=234")).toEqual(234); + expect(scope.a).toEqual(123); + expect(scope.b).toEqual(234); + }); - it('should evaluate JSON', function() { - expect(toJson(scope.$eval("[{}]"))).toEqual("[{}]"); - expect(toJson(scope.$eval("[{a:[]}, {b:1}]"))).toEqual('[{"a":[]},{"b":1}]'); - }); + it('should evaluate function call without arguments', function() { + scope['const'] = function(a,b){return 123;}; + expect(scope.$eval("const()")).toEqual(123); + }); - 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 function call with arguments', function() { + scope.add = function(a,b) { + return a+b; + }; + expect(scope.$eval("add(1,2)")).toEqual(3); + }); - 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 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 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 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 objects on scope context', function() { - scope.a = "abc"; - expect(scope.$eval("{a:a}").a).toEqual("abc"); - }); + 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); + }); - 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 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 field access after array access', function () { - scope.items = [{}, {name:'misko'}]; - expect(scope.$eval('items[1].name')).toEqual("misko"); - }); + 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"}'); + }); - it('should evaluate array assignment', function() { - scope.items = []; + it('should evaluate object access', function() { + expect(scope.$eval("{false:'WC', true:'CC'}[false]")).toEqual("WC"); + }); - 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 JSON', function() { + expect(toJson(scope.$eval("[{}]"))).toEqual("[{}]"); + expect(toJson(scope.$eval("[{a:[]}, {b:1}]"))).toEqual('[{"a":[]},{"b":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 multiple statements', function() { + expect(scope.$eval("a=1;b=3;a+b")).toEqual(4); + expect(scope.$eval(";;1;;")).toEqual(1); + }); - it('should evaluate remainder', function() { - expect(scope.$eval('1%2')).toEqual(1); - }); + 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 sum with undefined', function() { - expect(scope.$eval('1+undefined')).toEqual(1); - expect(scope.$eval('undefined+1')).toEqual(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; + }; + + scope.obj = new C(); + expect(scope.$eval("obj.sum(obj.getA())")).toEqual(246); + expect(scope.$eval("obj['sum'](obj.getA())")).toEqual(246); + }); - it('should throw exception on non-closed bracket', function() { - expect(function() { - scope.$eval('[].count('); - }).toThrow('Unexpected end of expression: [].count('); - }); + it('should evaluate objects on scope context', function() { + scope.a = "abc"; + expect(scope.$eval("{a:a}").a).toEqual("abc"); + }); - 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 field access on function call result', function() { + scope.a = function() { + return {name:'misko'}; + }; + expect(scope.$eval("a().name")).toEqual("misko"); + }); - it('should evaluate negation', function() { - 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 field access after array access', function () { + scope.items = [{}, {name:'misko'}]; + expect(scope.$eval('items[1].name')).toEqual("misko"); + }); - it('should evaluate exclamation mark', function() { - expect(scope.$eval('suffix = "!"')).toEqual('!'); - }); + it('should evaluate array assignment', function() { + scope.items = []; - it('should evaluate minus', function() { - expect(scope.$eval("{a:'-'}")).toEqual({a: "-"}); - }); + 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 undefined', function() { - expect(scope.$eval("undefined")).not.toBeDefined(); - expect(scope.$eval("a=undefined")).not.toBeDefined(); - expect(scope.a).not.toBeDefined(); - }); + 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 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 remainder', function() { + expect(scope.$eval('1%2')).toEqual(1); + }); - 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 evaluate sum with undefined', function() { + expect(scope.$eval('1+undefined')).toEqual(1); + expect(scope.$eval('undefined+1')).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 throw exception on non-closed bracket', function() { + expect(function() { + scope.$eval('[].count('); + }).toThrow('Unexpected end of expression: [].count('); + }); + 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'); + }); - describe('promises', function() { - var deferred, promise, q; + it('should evaluate negation', function() { + 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); + }); - beforeEach(inject(function($q) { - q = $q; - deferred = q.defer(); - promise = deferred.promise; - })); + it('should evaluate exclamation mark', function() { + expect(scope.$eval('suffix = "!"')).toEqual('!'); + }); - 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 evaluate minus', function() { + expect(scope.$eval("{a:'-'}")).toEqual({a: "-"}); + }); + 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 evaluated rejected promise and ignore the rejection reason', function() { - deferred.reject('sorry'); - scope.greeting = promise; - expect(scope.$eval('gretting')).toBe(undefined); - scope.$digest(); - expect(scope.$eval('greeting')).toBe(undefined); - }); + 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 AND operator', function() { + scope.run = function() { + throw "IT SHOULD NOT HAVE RUN"; + }; + expect(scope.$eval('false && run()')).toBe(false); + }); - it('should evaluate a promise and eventualy get its value', function() { - scope.greeting = promise; - expect(scope.$eval('greeting')).toBe(undefined); + it('should short-circuit OR operator', function() { + scope.run = function() { + throw "IT SHOULD NOT HAVE RUN"; + }; + expect(scope.$eval('true || run()')).toBe(true); + }); - 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!'); - }); + describe('promises', function() { + var deferred, promise, q; + beforeEach(inject(function($q) { + q = $q; + deferred = q.defer(); + promise = deferred.promise; + })); - it('should evaluate a promise and eventualy ignore its rejection', function() { - scope.greeting = promise; - expect(scope.$eval('greeting')).toBe(undefined); + 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!'); + }); - 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); - }); - }); + it('should evaluated rejected promise and ignore the rejection reason', function() { + deferred.reject('sorry'); + scope.greeting = promise; + expect(scope.$eval('gretting')).toBe(undefined); + scope.$digest(); + expect(scope.$eval('greeting')).toBe(undefined); + }); - 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); + it('should evaluate a promise and eventualy get its value', function() { + scope.greeting = promise; + expect(scope.$eval('greeting')).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!'); - }); + scope.$digest(); + expect(scope.$eval('greeting')).toBe(undefined); - 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); + deferred.resolve('hello!'); + expect(scope.$eval('greeting')).toBe(undefined); + scope.$digest(); + expect(scope.$eval('greeting')).toBe('hello!'); + }); - 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 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('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); - 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('obj.greeting')).toBe(undefined); + expect(scope.$eval('obj.greeting.polite')).toBe(undefined); - scope.$digest(); - expect(scope.$eval('greetings[0]')).toBe(undefined); - expect(scope.$eval('greetings[0][0]')).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!'); + }); - 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 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); - 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!'); + 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!'); + }); - scope.$digest(); - expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); - deferred.resolve('Veronica'); - expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); + 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('greet(name)')).toBe('Hi Veronica!'); - }); + 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 evaluate and dereference promises used as array indexes', function() { - scope.childIndex = promise; - scope.kids = ['Adam', 'Veronica', 'Elisa']; - expect(scope.$eval('kids[childIndex]')).toBe(undefined); - scope.$digest(); - expect(scope.$eval('kids[childIndex]')).toBe(undefined); + 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!'); - deferred.resolve(1); - expect(scope.$eval('kids[childIndex]')).toBe(undefined); + scope.$digest(); + expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); - scope.$digest(); - expect(scope.$eval('kids[childIndex]')).toBe('Veronica'); - }); + deferred.resolve('Veronica'); + expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); + scope.$digest(); + expect(scope.$eval('greet(name)')).toBe('Hi Veronica!'); + }); - it('should evaluate and dereference promises used as keys in bracket notation', function() { - scope.childKey = promise; - scope.kids = {'a': 'Adam', 'v': 'Veronica', 'e': 'Elisa'}; - expect(scope.$eval('kids[childKey]')).toBe(undefined); + 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); - scope.$digest(); - expect(scope.$eval('kids[childKey]')).toBe(undefined); + scope.$digest(); + expect(scope.$eval('kids[childIndex]')).toBe(undefined); - deferred.resolve('v'); - expect(scope.$eval('kids[childKey]')).toBe(undefined); + deferred.resolve(1); + expect(scope.$eval('kids[childIndex]')).toBe(undefined); - scope.$digest(); - expect(scope.$eval('kids[childKey]')).toBe('Veronica'); - }); + scope.$digest(); + expect(scope.$eval('kids[childIndex]')).toBe('Veronica'); + }); - 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 evaluate and dereference promises used as keys in bracket notation', function() { + scope.childKey = promise; + scope.kids = {'a': 'Adam', 'v': 'Veronica', 'e': 'Elisa'}; + + expect(scope.$eval('kids[childKey]')).toBe(undefined); + + scope.$digest(); + expect(scope.$eval('kids[childKey]')).toBe(undefined); + + deferred.resolve('v'); + expect(scope.$eval('kids[childKey]')).toBe(undefined); + + scope.$digest(); + expect(scope.$eval('kids[childKey]')).toBe('Veronica'); + }); + + + 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'); + }); }); }); - }); - 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('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); - })); + 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); - })); + 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); + })); + }); }); });