SIGN IN SIGN UP
angular / angular.js UNCLAIMED

AngularJS - HTML enhanced for web apps!

58862 0 0 JavaScript
'use strict';
// This file has many tests which read nicely if constant conditions
// are used.
/* eslint-disable no-constant-condition */
describe('parser', function() {
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
describe('lexer', function() {
var lex;
beforeEach(function() {
/* global Lexer: false */
lex = function() {
var lexer = new Lexer({csp: false});
return lexer.lex.apply(lexer, arguments);
};
});
it('should only match number chars with isNumber', function() {
expect(Lexer.prototype.isNumber('0')).toBe(true);
expect(Lexer.prototype.isNumber('')).toBeFalsy();
expect(Lexer.prototype.isNumber(' ')).toBeFalsy();
expect(Lexer.prototype.isNumber(0)).toBeFalsy();
expect(Lexer.prototype.isNumber(false)).toBeFalsy();
expect(Lexer.prototype.isNumber(true)).toBeFalsy();
expect(Lexer.prototype.isNumber(undefined)).toBeFalsy();
expect(Lexer.prototype.isNumber(null)).toBeFalsy();
});
it('should tokenize a string', function() {
var tokens = lex('a.bc[22]+1.3|f:\'a\\\'c\':"d\\"e"');
2010-10-15 13:44:53 -07:00
var i = 0;
expect(tokens[i].index).toEqual(0);
expect(tokens[i].text).toEqual('a');
i++;
expect(tokens[i].index).toEqual(1);
expect(tokens[i].text).toEqual('.');
i++;
expect(tokens[i].index).toEqual(2);
expect(tokens[i].text).toEqual('bc');
2010-10-15 13:44:53 -07:00
i++;
expect(tokens[i].index).toEqual(4);
expect(tokens[i].text).toEqual('[');
2010-10-15 13:44:53 -07:00
i++;
expect(tokens[i].index).toEqual(5);
expect(tokens[i].text).toEqual('22');
expect(tokens[i].value).toEqual(22);
expect(tokens[i].constant).toEqual(true);
2010-10-15 13:44:53 -07:00
i++;
expect(tokens[i].index).toEqual(7);
expect(tokens[i].text).toEqual(']');
2010-10-15 13:44:53 -07:00
i++;
expect(tokens[i].index).toEqual(8);
expect(tokens[i].text).toEqual('+');
2010-10-15 13:44:53 -07:00
i++;
expect(tokens[i].index).toEqual(9);
expect(tokens[i].text).toEqual('1.3');
expect(tokens[i].value).toEqual(1.3);
expect(tokens[i].constant).toEqual(true);
2010-10-15 13:44:53 -07:00
i++;
expect(tokens[i].index).toEqual(12);
expect(tokens[i].text).toEqual('|');
2010-10-15 13:44:53 -07:00
i++;
expect(tokens[i].index).toEqual(13);
expect(tokens[i].text).toEqual('f');
2010-10-15 13:44:53 -07:00
i++;
expect(tokens[i].index).toEqual(14);
expect(tokens[i].text).toEqual(':');
2010-10-15 13:44:53 -07:00
i++;
expect(tokens[i].index).toEqual(15);
expect(tokens[i].value).toEqual('a\'c');
2010-10-15 13:44:53 -07:00
i++;
expect(tokens[i].index).toEqual(21);
expect(tokens[i].text).toEqual(':');
2010-10-15 13:44:53 -07:00
i++;
expect(tokens[i].index).toEqual(22);
expect(tokens[i].value).toEqual('d"e');
2010-10-15 13:44:53 -07:00
});
it('should tokenize identifiers with spaces around dots the same as without spaces', function() {
function getText(t) { return t.text; }
var spaces = lex('foo. bar . baz').map(getText);
var noSpaces = lex('foo.bar.baz').map(getText);
expect(spaces).toEqual(noSpaces);
});
it('should use callback functions to know when an identifier is valid', function() {
function getText(t) { return t.text; }
var isIdentifierStart = jasmine.createSpy('start');
var isIdentifierContinue = jasmine.createSpy('continue');
isIdentifierStart.and.returnValue(true);
var lex = new Lexer({csp: false, isIdentifierStart: isIdentifierStart, isIdentifierContinue: isIdentifierContinue});
isIdentifierContinue.and.returnValue(true);
var tokens = lex.lex('πΣε').map(getText);
expect(tokens).toEqual(['πΣε']);
isIdentifierContinue.and.returnValue(false);
tokens = lex.lex('πΣε').map(getText);
expect(tokens).toEqual(['π', 'Σ', 'ε']);
});
it('should send the unicode characters and code points', function() {
function getText(t) { return t.text; }
var isIdentifierStart = jasmine.createSpy('start');
var isIdentifierContinue = jasmine.createSpy('continue');
isIdentifierStart.and.returnValue(true);
isIdentifierContinue.and.returnValue(true);
var lex = new Lexer({csp: false, isIdentifierStart: isIdentifierStart, isIdentifierContinue: isIdentifierContinue});
var tokens = lex.lex('\uD801\uDC37\uD852\uDF62\uDBFF\uDFFF');
expect(isIdentifierStart).toHaveBeenCalledTimes(1);
expect(isIdentifierStart.calls.argsFor(0)).toEqual(['\uD801\uDC37', 0x10437]);
expect(isIdentifierContinue).toHaveBeenCalledTimes(2);
expect(isIdentifierContinue.calls.argsFor(0)).toEqual(['\uD852\uDF62', 0x24B62]);
expect(isIdentifierContinue.calls.argsFor(1)).toEqual(['\uDBFF\uDFFF', 0x10FFFF]);
});
it('should tokenize undefined', function() {
var tokens = lex('undefined');
2010-10-15 13:44:53 -07:00
var i = 0;
expect(tokens[i].index).toEqual(0);
expect(tokens[i].text).toEqual('undefined');
2010-10-15 13:44:53 -07:00
});
it('should tokenize quoted string', function() {
var str = '[\'\\\'\', "\\""]';
2010-10-15 13:44:53 -07:00
var tokens = lex(str);
expect(tokens[1].index).toEqual(1);
expect(tokens[1].value).toEqual('\'');
2010-10-15 13:44:53 -07:00
expect(tokens[3].index).toEqual(7);
expect(tokens[3].value).toEqual('"');
2010-10-15 13:44:53 -07:00
});
it('should tokenize escaped quoted string', function() {
2010-10-15 13:44:53 -07:00
var str = '"\\"\\n\\f\\r\\t\\v\\u00A0"';
var tokens = lex(str);
expect(tokens[0].value).toEqual('"\n\f\r\t\v\u00A0');
2010-10-15 13:44:53 -07:00
});
it('should tokenize unicode', function() {
2010-10-15 13:44:53 -07:00
var tokens = lex('"\\u00A0"');
expect(tokens.length).toEqual(1);
expect(tokens[0].value).toEqual('\u00a0');
2010-10-15 13:44:53 -07:00
});
it('should ignore whitespace', function() {
var tokens = lex('a \t \n \r b');
expect(tokens[0].text).toEqual('a');
expect(tokens[1].text).toEqual('b');
2010-10-15 13:44:53 -07:00
});
it('should tokenize relation and equality', function() {
var tokens = lex('! == != < > <= >= === !==');
expect(tokens[0].text).toEqual('!');
expect(tokens[1].text).toEqual('==');
expect(tokens[2].text).toEqual('!=');
expect(tokens[3].text).toEqual('<');
expect(tokens[4].text).toEqual('>');
expect(tokens[5].text).toEqual('<=');
expect(tokens[6].text).toEqual('>=');
expect(tokens[7].text).toEqual('===');
expect(tokens[8].text).toEqual('!==');
2010-10-15 13:44:53 -07:00
});
it('should tokenize logical and ternary', function() {
var tokens = lex('&& || ? :');
expect(tokens[0].text).toEqual('&&');
expect(tokens[1].text).toEqual('||');
expect(tokens[2].text).toEqual('?');
expect(tokens[3].text).toEqual(':');
});
2010-10-15 13:44:53 -07:00
it('should tokenize statements', function() {
var tokens = lex('a;b;');
expect(tokens[0].text).toEqual('a');
expect(tokens[1].text).toEqual(';');
expect(tokens[2].text).toEqual('b');
expect(tokens[3].text).toEqual(';');
2010-10-15 13:44:53 -07:00
});
it('should tokenize function invocation', function() {
var tokens = lex('a()');
expect(tokens.map(function(t) { return t.text;})).toEqual(['a', '(', ')']);
});
it('should tokenize method invocation', function() {
var tokens = lex('a.b.c (d) - e.f()');
expect(tokens.map(function(t) { return t.text;})).
toEqual(['a', '.', 'b', '.', 'c', '(', 'd', ')', '-', 'e', '.', 'f', '(', ')']);
});
it('should tokenize number', function() {
var tokens = lex('0.5');
expect(tokens[0].value).toEqual(0.5);
2010-10-15 13:44:53 -07:00
});
it('should tokenize negative number', inject(function($rootScope) {
var value = $rootScope.$eval('-0.5');
2010-10-15 13:44:53 -07:00
expect(value).toEqual(-0.5);
value = $rootScope.$eval('{a:-0.5}');
2010-10-15 13:44:53 -07:00
expect(value).toEqual({a:-0.5});
}));
2010-10-15 13:44:53 -07:00
it('should tokenize number with exponent', inject(function($rootScope) {
var tokens = lex('0.5E-10');
expect(tokens[0].value).toEqual(0.5E-10);
expect($rootScope.$eval('0.5E-10')).toEqual(0.5E-10);
2010-10-15 13:44:53 -07:00
tokens = lex('0.5E+10');
expect(tokens[0].value).toEqual(0.5E+10);
}));
2010-10-15 13:44:53 -07:00
it('should throws exception for invalid exponent', function() {
expect(function() {
lex('0.5E-');
}).toThrowMinErr('$parse', 'lexerr', 'Lexer Error: Invalid exponent at column 4 in expression [0.5E-].');
expect(function() {
lex('0.5E-A');
}).toThrowMinErr('$parse', 'lexerr', 'Lexer Error: Invalid exponent at column 4 in expression [0.5E-A].');
});
it('should tokenize number starting with a dot', function() {
var tokens = lex('.5');
expect(tokens[0].value).toEqual(0.5);
2010-10-15 13:44:53 -07:00
});
2010-10-15 14:06:30 -07:00
it('should throw error on invalid unicode', function() {
expect(function() {
lex('\'\\u1\'\'bla\'');
}).toThrowMinErr('$parse', 'lexerr', 'Lexer Error: Invalid unicode escape [\\u1\'\'b] at column 2 in expression [\'\\u1\'\'bla\'].');
2010-10-15 14:06:30 -07:00
});
2010-10-15 13:44:53 -07:00
});
2014-11-23 23:08:33 +01:00
describe('ast', function() {
var createAst;
beforeEach(function() {
/* global AST: false */
createAst = function() {
var lexer = new Lexer({csp: false});
var ast = new AST(lexer, {csp: false, literals: {'true': true, 'false': false, 'undefined': undefined, 'null': null}});
2014-11-23 23:08:33 +01:00
return ast.ast.apply(ast, arguments);
};
});
it('should handle an empty list of tokens', function() {
expect(createAst('')).toEqual({type: 'Program', body: []});
});
it('should understand identifiers', function() {
expect(createAst('foo')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: { type: 'Identifier', name: 'foo' }
}
]
}
);
});
it('should understand non-computed member expressions', function() {
expect(createAst('foo.bar')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo'},
property: {type: 'Identifier', name: 'bar'},
computed: false
}
}
]
}
);
});
it('should associate non-computed member expressions left-to-right', function() {
expect(createAst('foo.bar.baz')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'MemberExpression',
object: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo'},
property: { type: 'Identifier', name: 'bar' },
computed: false
},
property: {type: 'Identifier', name: 'baz'},
computed: false
}
}
]
}
);
});
it('should understand computed member expressions', function() {
expect(createAst('foo[bar]')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo'},
property: {type: 'Identifier', name: 'bar'},
computed: true
}
}
]
}
);
});
it('should associate computed member expressions left-to-right', function() {
expect(createAst('foo[bar][baz]')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'MemberExpression',
object: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo' },
property: { type: 'Identifier', name: 'bar' },
computed: true
},
property: { type: 'Identifier', name: 'baz' },
computed: true
}
}
]
}
);
});
it('should understand call expressions', function() {
expect(createAst('foo()')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'foo'},
arguments: []
}
}
]
}
);
});
it('should parse call expression arguments', function() {
expect(createAst('foo(bar, baz)')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'foo'},
arguments: [
{ type: 'Identifier', name: 'bar' },
{ type: 'Identifier', name: 'baz' }
]
}
}
]
}
);
});
it('should parse call expression left-to-right', function() {
expect(createAst('foo(bar, baz)(man, shell)')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'foo' },
arguments: [
{ type: 'Identifier', name: 'bar' },
{ type: 'Identifier', name: 'baz' }
]
},
arguments: [
{ type: 'Identifier', name: 'man' },
{ type: 'Identifier', name: 'shell' }
]
}
}
]
}
);
});
it('should keep the context when having superfluous parenthesis', function() {
expect(createAst('(foo)(bar, baz)')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'foo'},
arguments: [
{ type: 'Identifier', name: 'bar' },
{ type: 'Identifier', name: 'baz' }
]
}
}
]
}
);
});
it('should treat member expressions and call expression with the same precedence', function() {
expect(createAst('foo.bar[baz]()')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo' },
property: { type: 'Identifier', name: 'bar' },
computed: false
},
property: { type: 'Identifier', name: 'baz' },
computed: true
},
arguments: []
}
}
]
}
);
expect(createAst('foo[bar]().baz')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'MemberExpression',
object: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo' },
property: { type: 'Identifier', name: 'bar' },
computed: true
},
arguments: []
},
property: { type: 'Identifier', name: 'baz' },
computed: false
}
}
]
}
);
expect(createAst('foo().bar[baz]')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'MemberExpression',
object: {
type: 'MemberExpression',
object: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'foo' },
arguments: [] },
property: { type: 'Identifier', name: 'bar' },
computed: false
},
property: { type: 'Identifier', name: 'baz' },
computed: true
}
}
]
}
);
});
it('should understand literals', function() {
// In a strict sense, `undefined` is not a literal but an identifier
forEach({'123': 123, '"123"': '123', 'true': true, 'false': false, 'null': null, 'undefined': undefined}, function(value, expression) {
expect(createAst(expression)).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: { type: 'Literal', value: value }
}
]
}
);
});
});
it('should understand the `this` expression', function() {
expect(createAst('this')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: { type: 'ThisExpression' }
}
]
}
);
});
it('should understand the `$locals` expression', function() {
expect(createAst('$locals')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: { type: 'LocalsExpression' }
}
]
}
);
});
it('should not confuse `this`, `$locals`, `undefined`, `true`, `false`, `null` when used as identifiers', function() {
forEach(['this', '$locals', 'undefined', 'true', 'false', 'null'], function(identifier) {
2014-11-23 23:08:33 +01:00
expect(createAst('foo.' + identifier)).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo' },
property: { type: 'Identifier', name: identifier },
computed: false
}
}
]
}
);
});
});
it('should throw when trying to use non-identifiers as identifiers', function() {
expect(function() { createAst('foo.)'); }).toThrowMinErr('$parse', 'syntax',
'Syntax Error: Token \')\' is not a valid identifier at column 5 of the expression [foo.)');
2014-11-23 23:08:33 +01:00
});
it('should throw when all tokens are not consumed', function() {
expect(function() { createAst('foo bar'); }).toThrowMinErr('$parse', 'syntax',
'Syntax Error: Token \'bar\' is an unexpected token at column 5 of the expression [foo bar] starting at [bar]');
2014-11-23 23:08:33 +01:00
});
it('should understand the unary operators `-`, `+` and `!`', function() {
forEach(['-', '+', '!'], function(operator) {
expect(createAst(operator + 'foo')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'UnaryExpression',
operator: operator,
prefix: true,
argument: { type: 'Identifier', name: 'foo' }
}
}
]
}
);
});
});
it('should handle all unary operators with the same precedence', function() {
forEach([['+', '-', '!'], ['-', '!', '+'], ['!', '+', '-']], function(operators) {
expect(createAst(operators.join('') + 'foo')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'UnaryExpression',
operator: operators[0],
prefix: true,
argument: {
type: 'UnaryExpression',
operator: operators[1],
prefix: true,
argument: {
type: 'UnaryExpression',
operator: operators[2],
prefix: true,
argument: { type: 'Identifier', name: 'foo' }
}
}
}
}
]
}
);
});
});
it('should be able to understand binary operators', function() {
forEach(['*', '/', '%', '+', '-', '<', '>', '<=', '>=', '==','!=','===','!=='], function(operator) {
expect(createAst('foo' + operator + 'bar')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'BinaryExpression',
operator: operator,
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' }
}
}
]
}
);
});
});
it('should associate binary operators with the same precedence left-to-right', function() {
2014-11-23 23:08:33 +01:00
var operatorsByPrecedence = [['*', '/', '%'], ['+', '-'], ['<', '>', '<=', '>='], ['==','!=','===','!==']];
forEach(operatorsByPrecedence, function(operators) {
forEach(operators, function(op1) {
forEach(operators, function(op2) {
expect(createAst('foo' + op1 + 'bar' + op2 + 'baz')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'BinaryExpression',
operator: op2,
left: {
type: 'BinaryExpression',
operator: op1,
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' }
},
right: { type: 'Identifier', name: 'baz' }
}
}
]
}
);
});
});
});
});
it('should give higher precedence to member calls than to unary expressions', function() {
2014-11-23 23:08:33 +01:00
forEach(['!', '+', '-'], function(operator) {
expect(createAst(operator + 'foo()')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'UnaryExpression',
operator: operator,
prefix: true,
argument: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'foo' },
arguments: []
}
}
}
]
}
);
expect(createAst(operator + 'foo.bar')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'UnaryExpression',
operator: operator,
prefix: true,
argument: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo' },
property: { type: 'Identifier', name: 'bar' },
computed: false
}
}
}
]
}
);
expect(createAst(operator + 'foo[bar]')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'UnaryExpression',
operator: operator,
prefix: true,
argument: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo' },
property: { type: 'Identifier', name: 'bar' },
computed: true
}
}
}
]
}
);
});
});
it('should give higher precedence to unary operators over multiplicative operators', function() {
forEach(['!', '+', '-'], function(op1) {
forEach(['*', '/', '%'], function(op2) {
expect(createAst(op1 + 'foo' + op2 + op1 + 'bar')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'BinaryExpression',
operator: op2,
left: {
type: 'UnaryExpression',
operator: op1,
prefix: true,
argument: { type: 'Identifier', name: 'foo' }
},
right: {
type: 'UnaryExpression',
operator: op1,
prefix: true,
argument: { type: 'Identifier', name: 'bar' }
}
}
}
]
}
);
});
});
});
it('should give binary operators their right precedence', function() {
var operatorsByPrecedence = [['*', '/', '%'], ['+', '-'], ['<', '>', '<=', '>='], ['==','!=','===','!==']];
for (var i = 0; i < operatorsByPrecedence.length - 1; ++i) {
forEach(operatorsByPrecedence[i], function(op1) {
forEach(operatorsByPrecedence[i + 1], function(op2) {
expect(createAst('foo' + op1 + 'bar' + op2 + 'baz' + op1 + 'man')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'BinaryExpression',
operator: op2,
left: {
type: 'BinaryExpression',
operator: op1,
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' }
},
right: {
type: 'BinaryExpression',
operator: op1,
left: { type: 'Identifier', name: 'baz' },
right: { type: 'Identifier', name: 'man' }
}
}
}
]
}
);
});
});
}
});
it('should understand logical operators', function() {
forEach(['||', '&&'], function(operator) {
expect(createAst('foo' + operator + 'bar')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'LogicalExpression',
operator: operator,
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' }
}
}
]
}
);
});
});
it('should associate logical operators left-to-right', function() {
forEach(['||', '&&'], function(op) {
expect(createAst('foo' + op + 'bar' + op + 'baz')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'LogicalExpression',
operator: op,
left: {
type: 'LogicalExpression',
operator: op,
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' }
},
right: { type: 'Identifier', name: 'baz' }
}
}
]
}
);
});
});
it('should understand ternary operators', function() {
expect(createAst('foo?bar:baz')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ConditionalExpression',
test: { type: 'Identifier', name: 'foo' },
alternate: { type: 'Identifier', name: 'bar' },
consequent: { type: 'Identifier', name: 'baz' }
}
}
]
}
);
});
it('should associate the conditional operator right-to-left', function() {
expect(createAst('foo0?foo1:foo2?bar0?bar1:bar2:man0?man1:man2')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ConditionalExpression',
test: { type: 'Identifier', name: 'foo0' },
alternate: { type: 'Identifier', name: 'foo1' },
consequent: {
type: 'ConditionalExpression',
test: { type: 'Identifier', name: 'foo2' },
alternate: {
type: 'ConditionalExpression',
test: { type: 'Identifier', name: 'bar0' },
alternate: { type: 'Identifier', name: 'bar1' },
consequent: { type: 'Identifier', name: 'bar2' }
},
consequent: {
type: 'ConditionalExpression',
test: { type: 'Identifier', name: 'man0' },
alternate: { type: 'Identifier', name: 'man1' },
consequent: { type: 'Identifier', name: 'man2' }
}
}
}
}
]
}
);
});
it('should understand assignment operator', function() {
// Currently, only `=` is supported
expect(createAst('foo=bar')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' },
operator: '='
}
}
]
}
);
});
it('should associate assignments right-to-left', function() {
// Currently, only `=` is supported
expect(createAst('foo=bar=man')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'foo' },
right: {
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'bar' },
right: { type: 'Identifier', name: 'man' },
operator: '='
},
operator: '='
}
}
]
}
);
});
it('should give higher precedence to equality than to the logical `and` operator', function() {
forEach(['==','!=','===','!=='], function(operator) {
expect(createAst('foo' + operator + 'bar && man' + operator + 'shell')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'LogicalExpression',
operator: '&&',
left: {
type: 'BinaryExpression',
operator: operator,
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' }
},
right: {
type: 'BinaryExpression',
operator: operator,
left: { type: 'Identifier', name: 'man' },
right: { type: 'Identifier', name: 'shell' }
}
}
}
]
}
);
});
});
it('should give higher precedence to logical `and` than to logical `or`', function() {
expect(createAst('foo&&bar||man&&shell')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'LogicalExpression',
operator: '||',
left: {
type: 'LogicalExpression',
operator: '&&',
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' }
},
right: {
type: 'LogicalExpression',
operator: '&&',
left: { type: 'Identifier', name: 'man' },
right: { type: 'Identifier', name: 'shell' }
}
}
}
]
}
);
});
it('should give higher precedence to the logical `or` than to the conditional operator', function() {
expect(createAst('foo||bar?man:shell')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ConditionalExpression',
test: {
type: 'LogicalExpression',
operator: '||',
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' }
},
alternate: { type: 'Identifier', name: 'man' },
consequent: { type: 'Identifier', name: 'shell' }
}
}
]
}
);
});
it('should give higher precedence to the conditional operator than to assignment operators', function() {
expect(createAst('foo=bar?man:shell')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'foo' },
right: {
type: 'ConditionalExpression',
test: { type: 'Identifier', name: 'bar' },
alternate: { type: 'Identifier', name: 'man' },
consequent: { type: 'Identifier', name: 'shell' }
},
operator: '='
}
}
]
}
);
});
it('should understand array literals', function() {
expect(createAst('[]')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ArrayExpression',
elements: []
}
}
]
}
);
expect(createAst('[foo]')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ArrayExpression',
elements: [
{ type: 'Identifier', name: 'foo' }
]
}
}
]
}
);
expect(createAst('[foo,]')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ArrayExpression',
elements: [
{ type: 'Identifier', name: 'foo' }
]
}
}
]
}
);
expect(createAst('[foo,bar,man,shell]')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ArrayExpression',
elements: [
{ type: 'Identifier', name: 'foo' },
{ type: 'Identifier', name: 'bar' },
{ type: 'Identifier', name: 'man' },
{ type: 'Identifier', name: 'shell' }
]
}
}
]
}
);
expect(createAst('[foo,bar,man,shell,]')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ArrayExpression',
elements: [
{ type: 'Identifier', name: 'foo' },
{ type: 'Identifier', name: 'bar' },
{ type: 'Identifier', name: 'man' },
{ type: 'Identifier', name: 'shell' }
]
}
}
]
}
);
});
it('should understand objects', function() {
expect(createAst('{}')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ObjectExpression',
properties: []
}
}
]
}
);
expect(createAst('{foo: bar}')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
kind: 'init',
key: { type: 'Identifier', name: 'foo' },
computed: false,
2014-11-23 23:08:33 +01:00
value: { type: 'Identifier', name: 'bar' }
}
]
}
}
]
}
);
expect(createAst('{foo: bar,}')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
kind: 'init',
key: { type: 'Identifier', name: 'foo' },
computed: false,
2014-11-23 23:08:33 +01:00
value: { type: 'Identifier', name: 'bar' }
}
]
}
}
]
}
);
expect(createAst('{foo: bar, "man": "shell", 42: 23}')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
kind: 'init',
key: { type: 'Identifier', name: 'foo' },
computed: false,
2014-11-23 23:08:33 +01:00
value: { type: 'Identifier', name: 'bar' }
},
{
type: 'Property',
kind: 'init',
key: { type: 'Literal', value: 'man' },
computed: false,
2014-11-23 23:08:33 +01:00
value: { type: 'Literal', value: 'shell' }
},
{
type: 'Property',
kind: 'init',
key: { type: 'Literal', value: 42 },
computed: false,
2014-11-23 23:08:33 +01:00
value: { type: 'Literal', value: 23 }
}
]
}
}
]
}
);
expect(createAst('{foo: bar, "man": "shell", 42: 23,}')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
kind: 'init',
key: { type: 'Identifier', name: 'foo' },
computed: false,
2014-11-23 23:08:33 +01:00
value: { type: 'Identifier', name: 'bar' }
},
{
type: 'Property',
kind: 'init',
key: { type: 'Literal', value: 'man' },
computed: false,
2014-11-23 23:08:33 +01:00
value: { type: 'Literal', value: 'shell' }
},
{
type: 'Property',
kind: 'init',
key: { type: 'Literal', value: 42 },
computed: false,
2014-11-23 23:08:33 +01:00
value: { type: 'Literal', value: 23 }
}
]
}
}
]
}
);
});
it('should understand ES6 object initializer', function() {
// Shorthand properties definitions.
expect(createAst('{x, y, z}')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
kind: 'init',
key: { type: 'Identifier', name: 'x' },
computed: false,
value: { type: 'Identifier', name: 'x' }
},
{
type: 'Property',
kind: 'init',
key: { type: 'Identifier', name: 'y' },
computed: false,
value: { type: 'Identifier', name: 'y' }
},
{
type: 'Property',
kind: 'init',
key: { type: 'Identifier', name: 'z' },
computed: false,
value: { type: 'Identifier', name: 'z' }
}
]
}
}
]
}
);
expect(function() { createAst('{"foo"}'); }).toThrow();
// Computed properties
expect(createAst('{[x]: x}')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
kind: 'init',
key: { type: 'Identifier', name: 'x' },
computed: true,
value: { type: 'Identifier', name: 'x' }
}
]
}
}
]
}
);
expect(createAst('{[x + 1]: x}')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
kind: 'init',
key: {
type: 'BinaryExpression',
operator: '+',
left: { type: 'Identifier', name: 'x' },
right: { type: 'Literal', value: 1 }
},
computed: true,
value: { type: 'Identifier', name: 'x' }
}
]
}
}
]
}
);
});
2014-11-23 23:08:33 +01:00
2014-11-23 23:08:33 +01:00
it('should understand multiple expressions', function() {
expect(createAst('foo = bar; man = shell')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' },
operator: '='
}
},
{
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'man' },
right: { type: 'Identifier', name: 'shell' },
operator: '='
}
}
]
}
);
});
// This is non-standard syntax
it('should understand filters', function() {
expect(createAst('foo | bar')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'bar'},
arguments: [
{ type: 'Identifier', name: 'foo' }
],
filter: true
}
}
]
}
);
});
it('should understand filters with extra parameters', function() {
expect(createAst('foo | bar:baz')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'bar'},
arguments: [
{ type: 'Identifier', name: 'foo' },
{ type: 'Identifier', name: 'baz' }
],
filter: true
}
}
]
}
);
});
it('should associate filters right-to-left', function() {
expect(createAst('foo | bar:man | shell')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'shell' },
arguments: [
{
type: 'CallExpression',
callee: { type: 'Identifier', name: 'bar' },
arguments: [
{ type: 'Identifier', name: 'foo' },
{ type: 'Identifier', name: 'man' }
],
filter: true
}
],
filter: true
}
}
]
}
);
});
2014-11-23 23:08:33 +01:00
it('should give higher precedence to assignments over filters', function() {
expect(createAst('foo=bar | man')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'man' },
arguments: [
{
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' },
operator: '='
}
],
filter: true
}
}
]
}
);
});
2014-11-23 23:08:33 +01:00
it('should accept expression as filters parameters', function() {
expect(createAst('foo | bar:baz=man')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'bar' },
arguments: [
{ type: 'Identifier', name: 'foo' },
{
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'baz' },
right: { type: 'Identifier', name: 'man' },
operator: '='
}
],
filter: true
}
}
]
}
);
});
2014-11-23 23:08:33 +01:00
it('should accept expression as computer members', function() {
expect(createAst('foo[a = 1]')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo' },
property: {
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'a' },
right: { type: 'Literal', value: 1 },
operator: '='
},
computed: true
}
}
]
}
);
});
2014-11-23 23:08:33 +01:00
it('should accept expression in function arguments', function() {
expect(createAst('foo(a = 1)')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'foo' },
arguments: [
{
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'a' },
right: { type: 'Literal', value: 1 },
operator: '='
}
]
}
}
]
}
);
});
2014-11-23 23:08:33 +01:00
it('should accept expression as part of ternary operators', function() {
expect(createAst('foo || bar ? man = 1 : shell = 1')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ConditionalExpression',
test: {
type: 'LogicalExpression',
operator: '||',
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' }
},
alternate: {
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'man' },
right: { type: 'Literal', value: 1 },
operator: '='
},
consequent: {
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'shell' },
right: { type: 'Literal', value: 1 },
operator: '='
}
}
}
]
}
);
});
2014-11-23 23:08:33 +01:00
it('should accept expression as part of array literals', function() {
expect(createAst('[foo = 1]')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ArrayExpression',
elements: [
{
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Literal', value: 1 },
operator: '='
}
]
}
}
]
}
);
});
2014-11-23 23:08:33 +01:00
it('should accept expression as part of object literals', function() {
expect(createAst('{foo: bar = 1}')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
kind: 'init',
key: { type: 'Identifier', name: 'foo' },
computed: false,
2014-11-23 23:08:33 +01:00
value: {
type: 'AssignmentExpression',
left: { type: 'Identifier', name: 'bar' },
right: { type: 'Literal', value: 1 },
operator: '='
}
}
]
}
}
]
}
);
});
2014-11-23 23:08:33 +01:00
it('should be possible to use parenthesis to indicate precedence', function() {
expect(createAst('(foo + bar).man')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'MemberExpression',
object: {
type: 'BinaryExpression',
operator: '+',
left: { type: 'Identifier', name: 'foo' },
right: { type: 'Identifier', name: 'bar' }
},
property: { type: 'Identifier', name: 'man' },
computed: false
}
}
]
}
);
});
2014-11-23 23:08:33 +01:00
it('should skip empty expressions', function() {
expect(createAst('foo;;;;bar')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: { type: 'Identifier', name: 'foo' }
},
{
type: 'ExpressionStatement',
expression: { type: 'Identifier', name: 'bar' }
}
]
}
);
expect(createAst(';foo')).toEqual(
{
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: { type: 'Identifier', name: 'foo' }
}
]
}
);
expect(createAst('foo;')).toEqual({
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: { type: 'Identifier', name: 'foo' }
}
]
});
expect(createAst(';;;;')).toEqual({type: 'Program', body: []});
expect(createAst('')).toEqual({type: 'Program', body: []});
});
});
var $filterProvider, scope;
beforeEach(module(['$filterProvider', function(filterProvider) {
$filterProvider = filterProvider;
}]));
2010-10-15 13:44:53 -07:00
forEach([true, false], function(cspEnabled) {
beforeEach(module(function($parseProvider) {
$parseProvider.addLiteral('Infinity', Infinity);
csp().noUnsafeEval = cspEnabled;
}));
it('should allow extending literals with csp ' + cspEnabled, inject(function($rootScope) {
expect($rootScope.$eval('Infinity')).toEqual(Infinity);
expect($rootScope.$eval('-Infinity')).toEqual(-Infinity);
expect(function() {$rootScope.$eval('Infinity = 1');}).toThrow();
expect($rootScope.$eval('Infinity')).toEqual(Infinity);
}));
});
forEach([true, false], function(cspEnabled) {
describe('csp: ' + cspEnabled, function() {
2010-10-15 13:44:53 -07:00
beforeEach(module(function() {
expect(csp().noUnsafeEval === true ||
csp().noUnsafeEval === false).toEqual(true);
csp().noUnsafeEval = cspEnabled;
}, provideLog));
beforeEach(inject(function($rootScope) {
scope = $rootScope;
}));
2010-10-15 13:44:53 -07:00
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);
});
2014-11-23 23:08:33 +01:00
it('should parse unary', function() {
expect(scope.$eval('+1')).toEqual(+1);
expect(scope.$eval('-1')).toEqual(-1);
expect(scope.$eval('+\'1\'')).toEqual(+'1');
expect(scope.$eval('-\'1\'')).toEqual(-'1');
expect(scope.$eval('+undefined')).toEqual(0);
// Note: don't change toEqual to toBe as toBe collapses 0 & -0.
expect(scope.$eval('-undefined')).toEqual(-0);
expect(scope.$eval('+null')).toEqual(+null);
expect(scope.$eval('-null')).toEqual(-null);
expect(scope.$eval('+false')).toEqual(+false);
expect(scope.$eval('-false')).toEqual(-false);
expect(scope.$eval('+true')).toEqual(+true);
expect(scope.$eval('-true')).toEqual(-true);
2014-11-23 23:08:33 +01:00
});
it('should parse comparison', function() {
/* eslint-disable eqeqeq, no-self-compare */
expect(scope.$eval('false')).toBeFalsy();
expect(scope.$eval('!true')).toBeFalsy();
expect(scope.$eval('1==1')).toBeTruthy();
expect(scope.$eval('1==true')).toBeTruthy();
expect(scope.$eval('1!=true')).toBeFalsy();
expect(scope.$eval('1===1')).toBeTruthy();
expect(scope.$eval('1===\'1\'')).toBeFalsy();
expect(scope.$eval('1===true')).toBeFalsy();
expect(scope.$eval('\'true\'===true')).toBeFalsy();
expect(scope.$eval('1!==2')).toBeTruthy();
expect(scope.$eval('1!==\'1\'')).toBeTruthy();
expect(scope.$eval('1!=2')).toBeTruthy();
expect(scope.$eval('1<2')).toBeTruthy();
expect(scope.$eval('1<=1')).toBeTruthy();
expect(scope.$eval('1>2')).toEqual(1 > 2);
expect(scope.$eval('2>=1')).toEqual(2 >= 1);
expect(scope.$eval('true==2<3')).toEqual(true == 2 < 3);
expect(scope.$eval('true===2<3')).toEqual(true === 2 < 3);
expect(scope.$eval('true===3===3')).toEqual(true === 3 === 3);
expect(scope.$eval('3===3===true')).toEqual(3 === 3 === true);
expect(scope.$eval('3 >= 3 > 2')).toEqual(3 >= 3 > 2);
/* eslint-enable */
});
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('true&&a')).toEqual(true && undefined);
expect(scope.$eval('true&&a()')).toEqual(true && undefined);
expect(scope.$eval('true&&a()()')).toEqual(true && undefined);
expect(scope.$eval('true&&a.b')).toEqual(true && undefined);
expect(scope.$eval('true&&a.b.c')).toEqual(true && undefined);
expect(scope.$eval('false||a')).toEqual(false || undefined);
expect(scope.$eval('false||a()')).toEqual(false || undefined);
expect(scope.$eval('false||a()()')).toEqual(false || undefined);
expect(scope.$eval('false||a.b')).toEqual(false || undefined);
expect(scope.$eval('false||a.b.c')).toEqual(false || undefined);
});
it('should parse ternary', function() {
var returnTrue = scope.returnTrue = function() { return true; };
var returnFalse = scope.returnFalse = function() { return false; };
var returnString = scope.returnString = function() { return 'asd'; };
var returnInt = scope.returnInt = function() { return 123; };
var identity = scope.identity = function(x) { return x; };
// Simple.
expect(scope.$eval('0?0:2')).toEqual(0 ? 0 : 2);
expect(scope.$eval('1?0:2')).toEqual(1 ? 0 : 2);
// Nested on the left.
expect(scope.$eval('0?0?0:0:2')).toEqual(0 ? 0 ? 0 : 0 : 2);
expect(scope.$eval('1?0?0:0:2')).toEqual(1 ? 0 ? 0 : 0 : 2);
expect(scope.$eval('0?1?0:0:2')).toEqual(0 ? 1 ? 0 : 0 : 2);
expect(scope.$eval('0?0?1:0:2')).toEqual(0 ? 0 ? 1 : 0 : 2);
expect(scope.$eval('0?0?0:2:3')).toEqual(0 ? 0 ? 0 : 2 : 3);
expect(scope.$eval('1?1?0:0:2')).toEqual(1 ? 1 ? 0 : 0 : 2);
expect(scope.$eval('1?1?1:0:2')).toEqual(1 ? 1 ? 1 : 0 : 2);
expect(scope.$eval('1?1?1:2:3')).toEqual(1 ? 1 ? 1 : 2 : 3);
expect(scope.$eval('1?1?1:2:3')).toEqual(1 ? 1 ? 1 : 2 : 3);
// Nested on the right.
expect(scope.$eval('0?0:0?0:2')).toEqual(0 ? 0 : 0 ? 0 : 2);
expect(scope.$eval('1?0:0?0:2')).toEqual(1 ? 0 : 0 ? 0 : 2);
expect(scope.$eval('0?1:0?0:2')).toEqual(0 ? 1 : 0 ? 0 : 2);
expect(scope.$eval('0?0:1?0:2')).toEqual(0 ? 0 : 1 ? 0 : 2);
expect(scope.$eval('0?0:0?2:3')).toEqual(0 ? 0 : 0 ? 2 : 3);
expect(scope.$eval('1?1:0?0:2')).toEqual(1 ? 1 : 0 ? 0 : 2);
expect(scope.$eval('1?1:1?0:2')).toEqual(1 ? 1 : 1 ? 0 : 2);
expect(scope.$eval('1?1:1?2:3')).toEqual(1 ? 1 : 1 ? 2 : 3);
expect(scope.$eval('1?1:1?2:3')).toEqual(1 ? 1 : 1 ? 2 : 3);
// Precedence with respect to logical operators.
expect(scope.$eval('0&&1?0:1')).toEqual(0 && 1 ? 0 : 1);
expect(scope.$eval('1||0?0:0')).toEqual(1 || 0 ? 0 : 0);
expect(scope.$eval('0?0&&1:2')).toEqual(0 ? 0 && 1 : 2);
expect(scope.$eval('0?1&&1:2')).toEqual(0 ? 1 && 1 : 2);
expect(scope.$eval('0?0||0:1')).toEqual(0 ? 0 || 0 : 1);
expect(scope.$eval('0?0||1:2')).toEqual(0 ? 0 || 1 : 2);
expect(scope.$eval('1?0&&1:2')).toEqual(1 ? 0 && 1 : 2);
expect(scope.$eval('1?1&&1:2')).toEqual(1 ? 1 && 1 : 2);
expect(scope.$eval('1?0||0:1')).toEqual(1 ? 0 || 0 : 1);
expect(scope.$eval('1?0||1:2')).toEqual(1 ? 0 || 1 : 2);
expect(scope.$eval('0?1:0&&1')).toEqual(0 ? 1 : 0 && 1);
expect(scope.$eval('0?2:1&&1')).toEqual(0 ? 2 : 1 && 1);
expect(scope.$eval('0?1:0||0')).toEqual(0 ? 1 : 0 || 0);
expect(scope.$eval('0?2:0||1')).toEqual(0 ? 2 : 0 || 1);
expect(scope.$eval('1?1:0&&1')).toEqual(1 ? 1 : 0 && 1);
expect(scope.$eval('1?2:1&&1')).toEqual(1 ? 2 : 1 && 1);
expect(scope.$eval('1?1:0||0')).toEqual(1 ? 1 : 0 || 0);
expect(scope.$eval('1?2:0||1')).toEqual(1 ? 2 : 0 || 1);
// Function calls.
expect(scope.$eval('returnTrue() ? returnString() : returnInt()')).toEqual(returnTrue() ? returnString() : returnInt());
expect(scope.$eval('returnFalse() ? returnString() : returnInt()')).toEqual(returnFalse() ? returnString() : returnInt());
expect(scope.$eval('returnTrue() ? returnString() : returnInt()')).toEqual(returnTrue() ? returnString() : returnInt());
expect(scope.$eval('identity(returnFalse() ? returnString() : returnInt())')).toEqual(identity(returnFalse() ? returnString() : returnInt()));
});
2010-10-15 13:44:53 -07:00
it('should parse string', function() {
expect(scope.$eval('\'a\' + \'b c\'')).toEqual('ab c');
});
2010-10-15 13:44:53 -07:00
it('should parse filters', function() {
$filterProvider.register('substring', valueFn(function(input, start, end) {
return input.substring(start, end);
}));
2010-10-15 13:44:53 -07:00
expect(function() {
scope.$eval('1|nonexistent');
}).toThrowMinErr('$injector', 'unpr', 'Unknown provider: nonexistentFilterProvider <- nonexistentFilter');
2010-10-15 13:44:53 -07:00
scope.offset = 3;
expect(scope.$eval('\'abcd\'|substring:1:offset')).toEqual('bc');
expect(scope.$eval('\'abcd\'|substring:1:3|uppercase')).toEqual('BC');
});
2010-10-15 13:44:53 -07:00
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 handle white-spaces around dots in paths', function() {
scope.a = {b: 4};
expect(scope.$eval('a . b', scope)).toEqual(4);
expect(scope.$eval('a. b', scope)).toEqual(4);
expect(scope.$eval('a .b', scope)).toEqual(4);
expect(scope.$eval('a . \nb', scope)).toEqual(4);
});
it('should handle white-spaces around dots in method invocations', function() {
scope.a = {b: function() { return this.c; }, c: 4};
expect(scope.$eval('a . b ()', scope)).toEqual(4);
expect(scope.$eval('a. b ()', scope)).toEqual(4);
expect(scope.$eval('a .b ()', scope)).toEqual(4);
expect(scope.$eval('a \n . \nb \n ()', scope)).toEqual(4);
});
it('should throw syntax error exception for identifiers ending with a dot', function() {
scope.a = {b: 4};
expect(function() {
scope.$eval('a.', scope);
}).toThrowMinErr('$parse', 'ueoe',
'Unexpected end of expression: a.');
expect(function() {
scope.$eval('a .', scope);
}).toThrowMinErr('$parse', 'ueoe',
'Unexpected end of expression: a .');
});
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!');
});
forEach([2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 42, 99], function(pathLength) {
it('should resolve nested paths of length ' + pathLength, function() {
// Create a nested object {x2: {x3: {x4: ... {x[n]: 42} ... }}}.
var obj = 42, locals = {};
for (var i = pathLength; i >= 2; i--) {
var newObj = {};
newObj['x' + i] = obj;
obj = newObj;
}
// Assign to x1 and build path 'x1.x2.x3. ... .x[n]' to access the final value.
scope.x1 = obj;
var path = 'x1';
for (i = 2; i <= pathLength; i++) {
path += '.x' + i;
}
expect(scope.$eval(path)).toBe(42);
locals['x' + pathLength] = 'not 42';
expect(scope.$eval(path, locals)).toBe(42);
});
});
2010-10-15 13:44:53 -07:00
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();
2014-11-23 23:08:33 +01:00
scope.a = undefined;
expect(scope.$eval('a - b')).toBe(0);
expect(scope.$eval('a + b')).toBeUndefined();
2014-11-23 23:08:33 +01:00
scope.a = 0;
expect(scope.$eval('a - b')).toBe(0);
expect(scope.$eval('a + b')).toBe(0);
scope.a = undefined;
scope.b = 0;
expect(scope.$eval('a - b')).toBe(0);
expect(scope.$eval('a + b')).toBe(0);
});
it('should support property names that collide with native object properties', function() {
// regression
scope.watch = 1;
scope.toString = function toString() {
return 'custom toString';
};
expect(scope.$eval('watch', scope)).toBe(1);
expect(scope.$eval('toString()', scope)).toBe('custom toString');
});
2010-10-15 13:44:53 -07:00
it('should not break if hasOwnProperty is referenced in an expression', function() {
scope.obj = { value: 1};
// By evaluating an expression that calls hasOwnProperty, the getterFnCache
// will store a property called hasOwnProperty. This is effectively:
// getterFnCache['hasOwnProperty'] = null
scope.$eval('obj.hasOwnProperty("value")');
// If we rely on this property then evaluating any expression will fail
// because it is not able to find out if obj.value is there in the cache
expect(scope.$eval('obj.value')).toBe(1);
});
2010-10-15 13:44:53 -07:00
it('should not break if the expression is "hasOwnProperty"', function() {
scope.fooExp = 'barVal';
// By evaluating hasOwnProperty, the $parse cache will store a getter for
// the scope's own hasOwnProperty function, which will mess up future cache look ups.
// i.e. cache['hasOwnProperty'] = function(scope) { return scope.hasOwnProperty; }
scope.$eval('hasOwnProperty');
expect(scope.$eval('fooExp')).toBe('barVal');
});
2010-10-15 13:44:53 -07:00
it('should evaluate grouped expressions', function() {
expect(scope.$eval('(1+2)*3')).toEqual((1 + 2) * 3);
});
2010-10-15 13:44:53 -07:00
it('should evaluate assignments', function() {
expect(scope.$eval('a=12')).toEqual(12);
expect(scope.a).toEqual(12);
2010-10-15 13:44:53 -07:00
expect(scope.$eval('x.y.z=123;')).toEqual(123);
expect(scope.x.y.z).toEqual(123);
expect(scope.$eval('a=123; b=234')).toEqual(234);
expect(scope.a).toEqual(123);
expect(scope.b).toEqual(234);
});
2010-10-15 13:44:53 -07:00
it('should throw with invalid left-val in assignments', function() {
expect(function() { scope.$eval('1 = 1'); }).toThrowMinErr('$parse', 'lval');
expect(function() { scope.$eval('{} = 1'); }).toThrowMinErr('$parse', 'lval');
expect(function() { scope.$eval('[] = 1'); }).toThrowMinErr('$parse', 'lval');
expect(function() { scope.$eval('true = 1'); }).toThrowMinErr('$parse', 'lval');
expect(function() { scope.$eval('(a=b) = 1'); }).toThrowMinErr('$parse', 'lval');
expect(function() { scope.$eval('(1<2) = 1'); }).toThrowMinErr('$parse', 'lval');
expect(function() { scope.$eval('(1+2) = 1'); }).toThrowMinErr('$parse', 'lval');
expect(function() { scope.$eval('!v = 1'); }).toThrowMinErr('$parse', 'lval');
expect(function() { scope.$eval('this = 1'); }).toThrowMinErr('$parse', 'lval');
expect(function() { scope.$eval('+v = 1'); }).toThrowMinErr('$parse', 'lval');
expect(function() { scope.$eval('(1?v1:v2) = 1'); }).toThrowMinErr('$parse', 'lval');
});
it('should evaluate assignments in ternary operator', function() {
scope.$eval('a = 1 ? 2 : 3');
expect(scope.a).toBe(2);
scope.$eval('0 ? a = 2 : a = 3');
expect(scope.a).toBe(3);
scope.$eval('1 ? a = 2 : a = 3');
expect(scope.a).toBe(2);
});
it('should evaluate function call without arguments', function() {
scope['const'] = function(a, b) {return 123;};
expect(scope.$eval('const()')).toEqual(123);
});
2010-10-15 13:44:53 -07:00
it('should evaluate function call with arguments', function() {
scope.add = function(a, b) {
return a + b;
};
expect(scope.$eval('add(1,2)')).toEqual(3);
});
2010-10-15 13:44:53 -07:00
it('should allow filter chains as arguments', function() {
scope.concat = function(a, b) {
return a + b;
};
scope.begin = 1;
scope.limit = 2;
expect(scope.$eval('concat(\'abcd\'|limitTo:limit:begin,\'abcd\'|limitTo:2:1|uppercase)')).toEqual('bcBC');
});
it('should evaluate function call from a return value', function() {
scope.getter = function() { return function() { return 33; }; };
expect(scope.$eval('getter()()')).toBe(33);
});
2010-10-15 13:44:53 -07:00
// Support: IE 9 only
// There is no "strict mode" in IE9
if (msie !== 9) {
it('should set no context to functions returned by other functions', function() {
scope.getter = function() { return function() { expect(this).toBeUndefined(); }; };
scope.$eval('getter()()');
});
}
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);
});
2010-10-15 13:44:53 -07:00
it('should evaluate array', function() {
expect(scope.$eval('[]').length).toEqual(0);
expect(scope.$eval('[1, 2]').length).toEqual(2);
expect(scope.$eval('[1, 2]')[0]).toEqual(1);
expect(scope.$eval('[1, 2]')[1]).toEqual(2);
expect(scope.$eval('[1, 2,]')[1]).toEqual(2);
expect(scope.$eval('[1, 2,]').length).toEqual(2);
});
2010-10-15 13:44:53 -07:00
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);
});
2010-10-15 13:44:53 -07:00
it('should evaluate object', function() {
expect(scope.$eval('{}')).toEqual({});
expect(scope.$eval('{a:\'b\'}')).toEqual({a:'b'});
expect(scope.$eval('{\'a\':\'b\'}')).toEqual({a:'b'});
expect(scope.$eval('{"a":\'b\'}')).toEqual({a:'b'});
expect(scope.$eval('{a:\'b\',}')).toEqual({a:'b'});
expect(scope.$eval('{\'a\':\'b\',}')).toEqual({a:'b'});
expect(scope.$eval('{"a":\'b\',}')).toEqual({a:'b'});
expect(scope.$eval('{\'0\':1}')).toEqual({0:1});
expect(scope.$eval('{0:1}')).toEqual({0:1});
expect(scope.$eval('{1:1}')).toEqual({1:1});
expect(scope.$eval('{null:1}')).toEqual({null:1});
expect(scope.$eval('{\'null\':1}')).toEqual({null:1});
expect(scope.$eval('{false:1}')).toEqual({false:1});
expect(scope.$eval('{\'false\':1}')).toEqual({false:1});
expect(scope.$eval('{\'\':1,}')).toEqual({'':1});
// ES6 object initializers.
expect(scope.$eval('{x, y}', {x: 'foo', y: 'bar'})).toEqual({x: 'foo', y: 'bar'});
expect(scope.$eval('{[x]: x}', {x: 'foo'})).toEqual({foo: 'foo'});
expect(scope.$eval('{[x + "z"]: x}', {x: 'foo'})).toEqual({fooz: 'foo'});
expect(scope.$eval('{x, 1: x, [x = x + 1]: x, 3: x + 1, [x = x + 2]: x, 5: x + 1}', {x: 1}))
.toEqual({x: 1, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5});
});
it('should throw syntax error exception for non constant/identifier JSON keys', function() {
expect(function() { scope.$eval('{[:0}'); }).toThrowMinErr('$parse', 'syntax',
'Syntax Error: Token \':\' not a primary expression at column 3 of the expression [{[:0}] starting at [:0}]');
expect(function() { scope.$eval('{{:0}'); }).toThrowMinErr('$parse', 'syntax',
'Syntax Error: Token \'{\' invalid key at column 2 of the expression [{{:0}] starting at [{:0}]');
expect(function() { scope.$eval('{?:0}'); }).toThrowMinErr('$parse', 'syntax',
'Syntax Error: Token \'?\' invalid key at column 2 of the expression [{?:0}] starting at [?:0}]');
expect(function() { scope.$eval('{):0}'); }).toThrowMinErr('$parse', 'syntax',
'Syntax Error: Token \')\' invalid key at column 2 of the expression [{):0}] starting at [):0}]');
});
2010-10-15 13:44:53 -07:00
it('should evaluate object access', function() {
expect(scope.$eval('{false:\'WC\', true:\'CC\'}[false]')).toEqual('WC');
});
2010-10-15 13:44:53 -07:00
it('should evaluate JSON', function() {
expect(scope.$eval('[{}]')).toEqual([{}]);
expect(scope.$eval('[{a:[]}, {b:1}]')).toEqual([{a:[]}, {b:1}]);
});
2010-10-15 13:44:53 -07:00
it('should evaluate multiple statements', function() {
expect(scope.$eval('a=1;b=3;a+b')).toEqual(4);
expect(scope.$eval(';;1;;')).toEqual(1);
});
2010-10-15 13:44:53 -07:00
it('should evaluate object methods in correct context (this)', function() {
function C() {
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);
});
2010-10-15 13:44:53 -07:00
it('should evaluate methods in correct context (this) in argument', function() {
function C() {
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);
});
2010-10-15 13:44:53 -07:00
it('should evaluate objects on scope context', function() {
scope.a = 'abc';
expect(scope.$eval('{a:a}').a).toEqual('abc');
});
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 field access after array access', function() {
scope.items = [{}, {name:'misko'}];
expect(scope.$eval('items[1].name')).toEqual('misko');
});
it('should evaluate array assignment', function() {
scope.items = [];
expect(scope.$eval('items[1] = "abc"')).toEqual('abc');
expect(scope.$eval('items[1]')).toEqual('abc');
expect(scope.$eval('books[1] = "moby"')).toEqual('moby');
expect(scope.$eval('books[1]')).toEqual('moby');
});
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 remainder', function() {
expect(scope.$eval('1%2')).toEqual(1);
});
it('should evaluate sum with undefined', function() {
expect(scope.$eval('1+undefined')).toEqual(1);
expect(scope.$eval('undefined+1')).toEqual(1);
});
it('should throw exception on non-closed bracket', function() {
expect(function() {
scope.$eval('[].count(');
}).toThrowMinErr('$parse', 'ueoe', 'Unexpected end of expression: [].count(');
});
it('should evaluate 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 negation', function() {
expect(scope.$eval('!false || true')).toEqual(!false || true);
// eslint-disable-next-line eqeqeq
expect(scope.$eval('!11 == 10')).toEqual(!11 == 10);
expect(scope.$eval('12/6/2')).toEqual(12 / 6 / 2);
});
it('should evaluate exclamation mark', function() {
expect(scope.$eval('suffix = "!"')).toEqual('!');
});
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 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 new Error('IT SHOULD NOT HAVE RUN');
};
expect(scope.$eval('false && run()')).toBe(false);
expect(scope.$eval('false && true && run()')).toBe(false);
});
it('should short-circuit OR operator', function() {
scope.run = function() {
throw new Error('IT SHOULD NOT HAVE RUN');
};
expect(scope.$eval('true || run()')).toBe(true);
expect(scope.$eval('true || false || run()')).toBe(true);
});
it('should throw TypeError on using a \'broken\' object as a key to access a property', function() {
scope.object = {};
forEach([
{ toString: 2 },
{ toString: null },
{ toString: function() { return {}; } }
], function(brokenObject) {
scope.brokenObject = brokenObject;
expect(function() {
scope.$eval('object[brokenObject]');
}).toThrow();
});
});
it('should support method calls on primitive types', function() {
scope.empty = '';
scope.zero = 0;
scope.bool = false;
expect(scope.$eval('empty.substr(0)')).toBe('');
expect(scope.$eval('zero.toString()')).toBe('0');
expect(scope.$eval('bool.toString()')).toBe('false');
});
it('should evaluate expressions with line terminators', function() {
scope.a = 'a';
scope.b = {c: 'bc'};
expect(scope.$eval('a + \n b.c + \r "\td" + \t \r\n\r "\r\n\n"')).toEqual('abc\td\r\n\n');
});
// https://github.com/angular/angular.js/issues/10968
it('should evaluate arrays literals initializers left-to-right', inject(function($parse) {
var s = {c:function() {return {b: 1}; }};
expect($parse('e=1;[a=c(),d=a.b+1]')(s)).toEqual([{b: 1}, 2]);
}));
it('should evaluate function arguments left-to-right', inject(function($parse) {
var s = {c:function() {return {b: 1}; }, i: function(x, y) { return [x, y];}};
expect($parse('e=1;i(a=c(),d=a.b+1)')(s)).toEqual([{b: 1}, 2]);
}));
it('should evaluate object properties expressions left-to-right', inject(function($parse) {
var s = {c:function() {return {b: 1}; }};
expect($parse('e=1;{x: a=c(), y: d=a.b+1}')(s)).toEqual({x: {b: 1}, y: 2});
}));
it('should call the function from the received instance and not from a new one', function() {
var n = 0;
scope.fn = function() {
var c = n++;
return { c: c, anotherFn: function() { return this.c === c; } };
};
expect(scope.$eval('fn().anotherFn()')).toBe(true);
});
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
it('should call the function once when it is part of the context', function() {
var count = 0;
scope.fn = function() {
count++;
return { anotherFn: function() { return 'lucas'; } };
};
expect(scope.$eval('fn().anotherFn()')).toBe('lucas');
expect(count).toBe(1);
});
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
it('should call the function once when it is not part of the context', function() {
var count = 0;
scope.fn = function() {
count++;
return function() { return 'lucas'; };
};
expect(scope.$eval('fn()()')).toBe('lucas');
expect(count).toBe(1);
});
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
it('should call the function once when it is part of the context on assignments', function() {
var count = 0;
var element = {};
scope.fn = function() {
count++;
return element;
};
expect(scope.$eval('fn().name = "lucas"')).toBe('lucas');
expect(element.name).toBe('lucas');
expect(count).toBe(1);
});
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
it('should call the function once when it is part of the context on array lookups', function() {
var count = 0;
var element = [];
scope.fn = function() {
count++;
return element;
};
expect(scope.$eval('fn()[0] = "lucas"')).toBe('lucas');
expect(element[0]).toBe('lucas');
expect(count).toBe(1);
});
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
it('should call the function once when it is part of the context on array lookup function', function() {
var count = 0;
var element = [{anotherFn: function() { return 'lucas';} }];
scope.fn = function() {
count++;
return element;
};
expect(scope.$eval('fn()[0].anotherFn()')).toBe('lucas');
expect(count).toBe(1);
});
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
it('should call the function once when it is part of the context on property lookup function', function() {
var count = 0;
var element = {name: {anotherFn: function() { return 'lucas';} } };
scope.fn = function() {
count++;
return element;
};
expect(scope.$eval('fn().name.anotherFn()')).toBe('lucas');
expect(count).toBe(1);
});
it('should call the function once when it is part of a sub-expression', function() {
var count = 0;
scope.element = [{}];
scope.fn = function() {
count++;
return 0;
};
expect(scope.$eval('element[fn()].name = "lucas"')).toBe('lucas');
expect(scope.element[0].name).toBe('lucas');
expect(count).toBe(1);
});
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
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});
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
}));
it('should return the assigned value', inject(function($parse) {
var fn = $parse('a');
var scope = {};
expect(fn.assign(scope, 123)).toBe(123);
var someObject = {};
expect(fn.assign(scope, someObject)).toBe(someObject);
}));
it('should expose working assignment function for expressions ending with brackets', inject(function($parse) {
var fn = $parse('a.b["c"]');
expect(fn.assign).toBeTruthy();
var scope = {};
fn.assign(scope, 123);
expect(scope.a.b.c).toEqual(123);
}));
it('should expose working assignment function for expressions with brackets in the middle', inject(function($parse) {
var fn = $parse('a["b"].c');
expect(fn.assign).toBeTruthy();
var scope = {};
fn.assign(scope, 123);
expect(scope.a.b.c).toEqual(123);
}));
it('should create objects when finding a null', inject(function($parse) {
var fn = $parse('foo.bar');
var scope = {foo: null};
fn.assign(scope, 123);
expect(scope.foo.bar).toEqual(123);
}));
it('should create objects when finding a null', inject(function($parse) {
var fn = $parse('foo["bar"]');
var scope = {foo: null};
fn.assign(scope, 123);
expect(scope.foo.bar).toEqual(123);
}));
it('should create objects when finding a null', inject(function($parse) {
var fn = $parse('foo.bar.baz');
var scope = {foo: null};
fn.assign(scope, 123);
expect(scope.foo.bar.baz).toEqual(123);
}));
});
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
describe('one-time binding', function() {
it('should always use the cache', inject(function($parse) {
expect($parse('foo')).toBe($parse('foo'));
expect($parse('::foo')).toBe($parse('::foo'));
}));
it('should not affect calling the parseFn directly', inject(function($parse, $rootScope) {
var fn = $parse('::foo');
$rootScope.$watch(fn);
$rootScope.foo = 'bar';
expect($rootScope.$$watchers.length).toBe(1);
expect(fn($rootScope)).toEqual('bar');
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(fn($rootScope)).toEqual('bar');
$rootScope.foo = 'man';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(fn($rootScope)).toEqual('man');
$rootScope.foo = 'shell';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(fn($rootScope)).toEqual('shell');
}));
it('should stay stable once the value defined', inject(function($parse, $rootScope, log) {
var fn = $parse('::foo');
$rootScope.$watch(fn, function(value, old) { if (value !== old) log(value); });
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
$rootScope.foo = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log).toEqual('bar');
log.reset();
$rootScope.foo = 'man';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log).toEqual('');
}));
it('should have a stable value if at the end of a $digest it has a defined value', inject(function($parse, $rootScope, log) {
var fn = $parse('::foo');
$rootScope.$watch(fn, function(value, old) { if (value !== old) log(value); });
$rootScope.$watch('foo', function() { if ($rootScope.foo === 'bar') {$rootScope.foo = undefined; } });
$rootScope.foo = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(2);
expect(log).toEqual('');
$rootScope.foo = 'man';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log).toEqual('; man');
$rootScope.foo = 'shell';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log).toEqual('; man');
}));
it('should not throw if the stable value is `null`', inject(function($parse, $rootScope) {
var fn = $parse('::foo');
$rootScope.$watch(fn);
$rootScope.foo = null;
$rootScope.$digest();
$rootScope.foo = 'foo';
$rootScope.$digest();
expect(fn()).toEqual(undefined);
}));
it('should invoke a stateless filter once when the parsed expression has an interceptor',
inject(function($parse, $rootScope) {
var countFilter = jasmine.createSpy();
var interceptor = jasmine.createSpy();
countFilter.and.returnValue(1);
$filterProvider.register('count', valueFn(countFilter));
$rootScope.foo = function() { return 1; };
$rootScope.$watch($parse(':: foo() | count', interceptor));
$rootScope.$digest();
expect(countFilter.calls.count()).toBe(1);
}));
describe('literal expressions', function() {
2014-11-23 23:08:33 +01:00
it('should mark an empty expressions as literal', inject(function($parse) {
expect($parse('').literal).toBe(true);
expect($parse(' ').literal).toBe(true);
expect($parse('::').literal).toBe(true);
expect($parse(':: ').literal).toBe(true);
}));
[true, false].forEach(function(isDeep) {
describe(isDeep ? 'deepWatch' : 'watch', function() {
it('should only become stable when all the properties of an object have defined values', inject(function($parse, $rootScope, log) {
var fn = $parse('::{foo: foo, bar: bar}');
$rootScope.$watch(fn, function(value) { log(value); }, isDeep);
expect(log.empty()).toEqual([]);
expect($rootScope.$$watchers.length).toBe(1);
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([{foo: undefined, bar: undefined}]);
$rootScope.foo = 'foo';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([{foo: 'foo', bar: undefined}]);
$rootScope.foo = 'foobar';
$rootScope.bar = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([{foo: 'foobar', bar: 'bar'}]);
$rootScope.foo = 'baz';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([]);
}));
it('should only become stable when all the elements of an array have defined values', inject(function($parse, $rootScope, log) {
var fn = $parse('::[foo,bar]');
$rootScope.$watch(fn, function(value) { log(value); }, isDeep);
expect(log.empty()).toEqual([]);
expect($rootScope.$$watchers.length).toBe(1);
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([[undefined, undefined]]);
$rootScope.foo = 'foo';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([['foo', undefined]]);
$rootScope.foo = 'foobar';
$rootScope.bar = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([['foobar', 'bar']]);
$rootScope.foo = 'baz';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([]);
}));
it('should only become stable when all the elements of an array have defined values at the end of a $digest', inject(function($parse, $rootScope, log) {
var fn = $parse('::[foo]');
$rootScope.$watch(fn, function(value) { log(value); }, isDeep);
$rootScope.$watch('foo', function() { if ($rootScope.foo === 'bar') {$rootScope.foo = undefined; } });
$rootScope.foo = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(2);
expect(log.empty()).toEqual([['bar'], [undefined]]);
$rootScope.foo = 'baz';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([['baz']]);
$rootScope.bar = 'qux';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log).toEqual([]);
}));
});
});
});
});
describe('watched $parse expressions', function() {
it('should respect short-circuiting AND if it could have side effects', function() {
var bCalled = 0;
scope.b = function() { bCalled++; };
scope.$watch('a && b()');
scope.$digest();
scope.$digest();
expect(bCalled).toBe(0);
scope.a = true;
scope.$digest();
expect(bCalled).toBe(1);
scope.$digest();
expect(bCalled).toBe(2);
});
it('should respect short-circuiting OR if it could have side effects', function() {
var bCalled = false;
scope.b = function() { bCalled = true; };
scope.$watch('a || b()');
scope.$digest();
expect(bCalled).toBe(true);
bCalled = false;
scope.a = true;
scope.$digest();
expect(bCalled).toBe(false);
});
it('should respect the branching ternary operator if it could have side effects', function() {
var bCalled = false;
scope.b = function() { bCalled = true; };
scope.$watch('a ? b() : 1');
scope.$digest();
expect(bCalled).toBe(false);
scope.a = true;
scope.$digest();
expect(bCalled).toBe(true);
});
2017-05-24 00:20:31 -07:00
describe('filters', function() {
it('should not be invoked unless the input/arguments change', function() {
var filterCalled = false;
$filterProvider.register('foo', valueFn(function(input) {
filterCalled = true;
return input;
}));
scope.$watch('a | foo:b:1');
scope.a = 0;
scope.$digest();
expect(filterCalled).toBe(true);
filterCalled = false;
scope.$digest();
expect(filterCalled).toBe(false);
scope.a++;
scope.$digest();
expect(filterCalled).toBe(true);
});
it('should not be invoked unless the input/arguments change within literals', function() {
var filterCalls = [];
$filterProvider.register('foo', valueFn(function(input) {
filterCalls.push(input);
return input;
}));
scope.$watch('[(a | foo:b:1), undefined]');
scope.a = 0;
scope.$digest();
expect(filterCalls).toEqual([0]);
scope.$digest();
expect(filterCalls).toEqual([0]);
scope.a++;
scope.$digest();
expect(filterCalls).toEqual([0, 1]);
});
it('should not be invoked unless the input/arguments change within literals (one-time)', function() {
var filterCalls = [];
$filterProvider.register('foo', valueFn(function(input) {
filterCalls.push(input);
return input;
}));
scope.$watch('::[(a | foo:b:1), undefined]');
scope.a = 0;
scope.$digest();
expect(filterCalls).toEqual([0]);
scope.$digest();
expect(filterCalls).toEqual([0]);
scope.a++;
scope.$digest();
expect(filterCalls).toEqual([0, 1]);
});
2017-05-24 00:20:31 -07:00
it('should always be invoked if they are marked as having $stateful', function() {
var filterCalled = false;
$filterProvider.register('foo', valueFn(extend(function(input) {
filterCalled = true;
return input;
}, {$stateful: true})));
scope.$watch('a | foo:b:1');
scope.a = 0;
scope.$digest();
expect(filterCalled).toBe(true);
filterCalled = false;
scope.$digest();
expect(filterCalled).toBe(true);
});
2017-05-24 00:20:31 -07:00
it('should be treated as constant when input are constant', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
return input;
}));
2017-05-24 00:20:31 -07:00
var parsed = $parse('{x: 1} | foo:1');
2017-05-24 00:20:31 -07:00
expect(parsed.constant).toBe(true);
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch(parsed, function(input) {
expect(input).toEqual({x:1});
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
}));
it('should ignore changes within nested objects', function() {
var watchCalls = [];
scope.$watch('[a]', function(a) { watchCalls.push(a[0]); });
scope.a = 0;
scope.$digest();
expect(watchCalls).toEqual([0]);
scope.$digest();
expect(watchCalls).toEqual([0]);
scope.a++;
scope.$digest();
expect(watchCalls).toEqual([0, 1]);
scope.a = {};
scope.$digest();
expect(watchCalls).toEqual([0, 1, {}]);
scope.a.foo = 42;
scope.$digest();
expect(watchCalls).toEqual([0, 1, {foo: 42}]);
});
it('should ignore changes within nested objects (one-time)', function() {
var watchCalls = [];
scope.$watch('::[a, undefined]', function(a) { watchCalls.push(a[0]); });
scope.a = 0;
scope.$digest();
expect(watchCalls).toEqual([0]);
scope.$digest();
expect(watchCalls).toEqual([0]);
scope.a++;
scope.$digest();
expect(watchCalls).toEqual([0, 1]);
scope.a = {};
scope.$digest();
expect(watchCalls).toEqual([0, 1, {}]);
scope.a.foo = 42;
scope.$digest();
expect(watchCalls).toEqual([0, 1, {foo: 42}]);
});
2017-05-24 00:20:31 -07:00
describe('with non-primitive input', function() {
2017-05-24 00:20:31 -07:00
describe('that does NOT support valueOf()', function() {
2017-05-24 00:20:31 -07:00
it('should always be reevaluated', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
return input;
}));
2017-05-24 00:20:31 -07:00
var parsed = $parse('obj | foo');
var obj = scope.obj = {};
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch(parsed, function(input) {
expect(input).toBe(obj);
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(2);
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(3);
expect(watcherCalls).toBe(1);
}));
2017-05-24 00:20:31 -07:00
it('should always be reevaluated in literals', inject(function($parse) {
$filterProvider.register('foo', valueFn(function(input) {
return input.b > 0;
}));
2017-05-24 00:20:31 -07:00
scope.$watch('[(a | foo)]', function() {});
2017-05-24 00:20:31 -07:00
// Would be great if filter-output was checked for changes and this didn't throw...
expect(function() { scope.$apply('a = {b: 1}'); }).toThrowMinErr('$rootScope', 'infdig');
}));
2017-05-24 00:20:31 -07:00
it('should always be reevaluated when passed literals', inject(function($parse) {
scope.$watch('[a] | filter', function() {});
2017-05-24 00:20:31 -07:00
scope.$apply('a = 1');
2017-05-24 00:20:31 -07:00
// Would be great if filter-output was checked for changes and this didn't throw...
expect(function() { scope.$apply('a = {}'); }).toThrowMinErr('$rootScope', 'infdig');
}));
});
2017-05-24 00:20:31 -07:00
describe('that does support valueOf()', function() {
it('should not be reevaluated',
inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
expect(input instanceof Date).toBe(true);
return input;
}));
var parsed = $parse('date | foo:a');
var date = scope.date = new Date();
var watcherCalls = 0;
scope.$watch(parsed, function(input) {
expect(input).toBe(date);
watcherCalls++;
});
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
}));
2017-05-24 00:20:31 -07:00
it('should not be reevaluated in literals', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
return input;
}));
2017-05-24 00:20:31 -07:00
scope.date = new Date(1234567890123);
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch('[(date | foo)]', function(input) {
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
}));
2017-05-24 00:20:31 -07:00
it('should be reevaluated when valueOf() changes', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
expect(input instanceof Date).toBe(true);
return input;
}));
2017-05-24 00:20:31 -07:00
var parsed = $parse('date | foo:a');
var date = scope.date = new Date();
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch(parsed, function(input) {
expect(input).toBe(date);
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
date.setYear(1901);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(2);
expect(watcherCalls).toBe(1);
}));
2017-05-24 00:20:31 -07:00
it('should be reevaluated in literals when valueOf() changes', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
return input;
}));
2017-05-24 00:20:31 -07:00
scope.date = new Date(1234567890123);
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch('[(date | foo)]', function(input) {
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.date.setTime(1234567890);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(2);
expect(watcherCalls).toBe(2);
}));
2017-05-24 00:20:31 -07:00
it('should not be reevaluated when the instance changes but valueOf() does not', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
return input;
}));
2017-05-24 00:20:31 -07:00
scope.date = new Date(1234567890123);
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch($parse('[(date | foo)]'), function(input) {
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(watcherCalls).toBe(1);
expect(filterCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.date = new Date(1234567890123);
scope.$digest();
expect(watcherCalls).toBe(1);
expect(filterCalls).toBe(1);
}));
});
2017-05-24 00:20:31 -07:00
it('should not be reevaluated when input is simplified via unary operators', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
return input;
}));
2017-05-24 00:20:31 -07:00
scope.obj = {};
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch('!obj | foo:!obj', function(input) {
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
}));
2017-05-24 00:20:31 -07:00
it('should not be reevaluated when input is simplified via non-plus/concat binary operators', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
return input;
}));
2017-05-24 00:20:31 -07:00
scope.obj = {};
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch('1 - obj | foo:(1 * obj)', function(input) {
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
}));
2017-05-24 00:20:31 -07:00
it('should be reevaluated when input is simplified via plus/concat', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
return input;
}));
2017-05-24 00:20:31 -07:00
scope.obj = {};
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch('1 + obj | foo', function(input) {
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(2);
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(3);
expect(watcherCalls).toBe(1);
}));
2017-05-24 00:20:31 -07:00
it('should reevaluate computed member expressions', inject(function($parse) {
var toStringCalls = 0;
2017-05-24 00:20:31 -07:00
scope.obj = {};
scope.key = {
toString: function() {
toStringCalls++;
return 'foo';
}
};
var watcherCalls = 0;
scope.$watch('obj[key]', function(input) {
watcherCalls++;
});
scope.$digest();
expect(toStringCalls).toBe(2);
expect(watcherCalls).toBe(1);
scope.$digest();
expect(toStringCalls).toBe(3);
expect(watcherCalls).toBe(1);
}));
it('should be reevaluated with input created with null prototype', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
return input;
}));
2017-05-24 00:20:31 -07:00
var parsed = $parse('obj | foo');
var obj = scope.obj = Object.create(null);
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch(parsed, function(input) {
expect(input).toBe(obj);
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(2);
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(3);
expect(watcherCalls).toBe(1);
}));
});
2017-05-24 00:20:31 -07:00
describe('with primitive input', function() {
2017-05-24 00:20:31 -07:00
it('should not be reevaluated when passed literals', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
return input;
}));
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch('[a] | foo', function(input) {
watcherCalls++;
});
scope.$apply('a = 1');
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
scope.$apply('a = 2');
expect(filterCalls).toBe(2);
expect(watcherCalls).toBe(2);
}));
it('should not be reevaluated in literals', inject(function($parse) {
var filterCalls = 0;
$filterProvider.register('foo', valueFn(function(input) {
filterCalls++;
return input;
}));
2017-05-24 00:20:31 -07:00
scope.prim = 1234567890123;
2017-05-24 00:20:31 -07:00
var watcherCalls = 0;
scope.$watch('[(prim | foo)]', function(input) {
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
}));
});
});
2017-05-24 00:20:31 -07:00
describe('interceptorFns', function() {
it('should only be passed the intercepted value', inject(function($parse) {
var args;
function interceptor(v) {
args = sliceArgs(arguments);
return v;
}
scope.$watch($parse('a', interceptor));
scope.a = 1;
scope.$digest();
expect(args).toEqual([1]);
}));
it('should only be passed the intercepted value when wrapping one-time', inject(function($parse) {
var args;
function interceptor(v) {
args = sliceArgs(arguments);
return v;
}
scope.$watch($parse('::a', interceptor));
scope.a = 1;
scope.$digest();
expect(args).toEqual([1]);
}));
it('should only be passed the intercepted value when double-intercepted',
inject(function($parse) {
var args1;
function int1(v) {
args1 = sliceArgs(arguments);
return v + 2;
}
var args2;
function int2(v) {
args2 = sliceArgs(arguments);
return v + 4;
}
scope.$watch($parse($parse('a', int1), int2));
scope.a = 1;
scope.$digest();
expect(args1).toEqual([1]);
expect(args2).toEqual([3]);
}));
it('should support locals', inject(function($parse) {
var args;
function interceptor(v) {
args = sliceArgs(arguments);
return v + 4;
}
var exp = $parse('a + b', interceptor);
scope.a = 1;
expect(exp(scope, {b: 2})).toBe(7);
expect(args).toEqual([3]);
}));
it('should support locals when double-intercepted', inject(function($parse) {
var args1;
function int1(v) {
args1 = sliceArgs(arguments);
return v + 4;
}
var args2;
function int2(v) {
args2 = sliceArgs(arguments);
return v + 8;
}
var exp = $parse($parse('a + b', int1), int2);
scope.a = 1;
expect(exp(scope, {b: 2})).toBe(15);
expect(args1).toEqual([3]);
expect(args2).toEqual([7]);
}));
2017-05-24 00:20:31 -07:00
it('should always be invoked if they are flagged as having $stateful',
inject(function($parse) {
var called = false;
function interceptor() {
called = true;
}
2017-05-24 00:20:31 -07:00
interceptor.$stateful = true;
2017-05-24 00:20:31 -07:00
scope.$watch($parse('a', interceptor));
scope.a = 0;
scope.$digest();
expect(called).toBe(true);
2017-05-24 00:20:31 -07:00
called = false;
scope.$digest();
expect(called).toBe(true);
2017-05-24 00:20:31 -07:00
scope.a++;
called = false;
scope.$digest();
expect(called).toBe(true);
}));
it('should always be invoked if flagged as $stateful when wrapping one-time',
inject(function($parse) {
var interceptorCalls = 0;
function interceptor() {
interceptorCalls++;
return 123;
}
interceptor.$stateful = true;
scope.$watch($parse('::a', interceptor));
interceptorCalls = 0;
scope.$digest();
expect(interceptorCalls).not.toBe(0);
interceptorCalls = 0;
scope.$digest();
expect(interceptorCalls).not.toBe(0);
}));
it('should always be invoked if flagged as $stateful when wrapping one-time with inputs',
inject(function($parse) {
$filterProvider.register('identity', valueFn(identity));
var interceptorCalls = 0;
function interceptor() {
interceptorCalls++;
return 123;
}
interceptor.$stateful = true;
scope.$watch($parse('::a | identity', interceptor));
interceptorCalls = 0;
scope.$digest();
expect(interceptorCalls).not.toBe(0);
interceptorCalls = 0;
scope.$digest();
expect(interceptorCalls).not.toBe(0);
}));
it('should always be invoked if flagged as $stateful when wrapping one-time literal',
inject(function($parse) {
var interceptorCalls = 0;
function interceptor() {
interceptorCalls++;
return 123;
}
interceptor.$stateful = true;
scope.$watch($parse('::[a]', interceptor));
interceptorCalls = 0;
scope.$digest();
expect(interceptorCalls).not.toBe(0);
interceptorCalls = 0;
scope.$digest();
expect(interceptorCalls).not.toBe(0);
}));
2017-05-24 00:20:31 -07:00
it('should not be invoked unless the input changes', inject(function($parse) {
var called = false;
function interceptor(v) {
called = true;
return v;
}
scope.$watch($parse('a', interceptor));
scope.$watch($parse('a + b', interceptor));
scope.a = scope.b = 0;
scope.$digest();
expect(called).toBe(true);
called = false;
scope.$digest();
expect(called).toBe(false);
scope.a++;
scope.$digest();
expect(called).toBe(true);
}));
it('should always be invoked if inputs are non-primitive', inject(function($parse) {
var called = false;
function interceptor(v) {
called = true;
return v.sub;
}
scope.$watch($parse('[o]', interceptor));
scope.o = {sub: 1};
called = false;
scope.$digest();
expect(called).toBe(true);
called = false;
scope.$digest();
expect(called).toBe(true);
}));
2017-05-24 00:20:31 -07:00
it('should not be invoked unless the input.valueOf() changes even if the instance changes', inject(function($parse) {
var called = false;
function interceptor(v) {
called = true;
return v;
}
scope.$watch($parse('a', interceptor));
scope.a = new Date();
scope.$digest();
expect(called).toBe(true);
called = false;
scope.a = new Date(scope.a.valueOf());
scope.$digest();
expect(called).toBe(false);
}));
2017-05-24 00:20:31 -07:00
it('should be invoked if input.valueOf() changes even if the instance does not', inject(function($parse) {
var called = false;
function interceptor(v) {
called = true;
return v;
}
scope.$watch($parse('a', interceptor));
scope.a = new Date();
scope.$digest();
expect(called).toBe(true);
called = false;
scope.a.setTime(scope.a.getTime() + 1);
scope.$digest();
expect(called).toBe(true);
}));
2017-05-24 00:20:31 -07:00
it('should be invoked when the expression is `undefined`', inject(function($parse) {
var called = false;
function interceptor(v) {
called = true;
return v;
}
scope.$watch($parse(undefined, interceptor));
scope.$digest();
expect(called).toBe(true);
}));
it('should not affect when a one-time binding becomes stable', inject(function($parse) {
scope.$watch($parse('::x'));
scope.$watch($parse('::x', identity));
scope.$watch($parse('::x', function() { return 1; })); //interceptor that returns non-undefined
scope.$digest();
expect(scope.$$watchersCount).toBe(3);
scope.x = 1;
scope.$digest();
expect(scope.$$watchersCount).toBe(0);
}));
it('should not affect when a one-time literal binding becomes stable', inject(function($parse) {
scope.$watch($parse('::[x]'));
scope.$watch($parse('::[x]', identity));
scope.$watch($parse('::[x]', function() { return 1; })); //interceptor that returns non-literal
scope.$digest();
expect(scope.$$watchersCount).toBe(3);
scope.x = 1;
scope.$digest();
expect(scope.$$watchersCount).toBe(0);
}));
it('should watch the intercepted value of one-time bindings', inject(function($parse, log) {
scope.$watch($parse('::{x:x, y:y}', function(lit) { return lit.x; }), log);
scope.$apply();
expect(log.empty()).toEqual([undefined]);
scope.$apply('x = 1');
expect(log.empty()).toEqual([1]);
scope.$apply('x = 2; y=1');
expect(log.empty()).toEqual([2]);
scope.$apply('x = 1; y=2');
expect(log.empty()).toEqual([]);
}));
it('should watch the intercepted value of one-time bindings in nested interceptors', inject(function($parse, log) {
scope.$watch($parse($parse('::{x:x, y:y}', function(lit) { return lit.x; }), identity), log);
scope.$apply();
expect(log.empty()).toEqual([undefined]);
scope.$apply('x = 1');
expect(log.empty()).toEqual([1]);
scope.$apply('x = 2; y=1');
expect(log.empty()).toEqual([2]);
scope.$apply('x = 1; y=2');
expect(log.empty()).toEqual([]);
}));
it('should nest interceptors around eachother, not around the intercepted', inject(function($parse) {
function origin() { return 0; }
var fn = origin;
function addOne(n) { return n + 1; }
fn = $parse(fn, addOne);
expect(fn.$$intercepted).toBe(origin);
expect(fn()).toBe(1);
fn = $parse(fn, addOne);
expect(fn.$$intercepted).toBe(origin);
expect(fn()).toBe(2);
fn = $parse(fn, addOne);
expect(fn.$$intercepted).toBe(origin);
expect(fn()).toBe(3);
}));
it('should not propogate $$watchDelegate to the interceptor wrapped expression', inject(function($parse) {
function getter(s) {
return s.x;
}
getter.$$watchDelegate = getter;
function doubler(v) {
return 2 * v;
}
var lastValue;
function watcher(val) {
lastValue = val;
}
scope.$watch($parse(getter, doubler), watcher);
scope.$apply('x = 1');
expect(lastValue).toBe(2 * 1);
scope.$apply('x = 123');
expect(lastValue).toBe(2 * 123);
}));
2017-05-24 00:20:31 -07:00
});
2017-05-24 00:20:31 -07:00
describe('literals', function() {
2017-05-24 00:20:31 -07:00
it('should support watching', inject(function($parse) {
var lastVal = NaN;
var callCount = 0;
var listener = function(val) { callCount++; lastVal = val; };
2017-05-24 00:20:31 -07:00
scope.$watch('{val: val}', listener);
2017-05-24 00:20:31 -07:00
scope.$apply('val = 1');
expect(callCount).toBe(1);
expect(lastVal).toEqual({val: 1});
2017-05-24 00:20:31 -07:00
scope.$apply('val = []');
expect(callCount).toBe(2);
expect(lastVal).toEqual({val: []});
2017-05-24 00:20:31 -07:00
scope.$apply('val = []');
expect(callCount).toBe(3);
expect(lastVal).toEqual({val: []});
2017-05-24 00:20:31 -07:00
scope.$apply('val = {}');
expect(callCount).toBe(4);
expect(lastVal).toEqual({val: {}});
}));
2017-05-24 00:20:31 -07:00
it('should only watch the direct inputs', inject(function($parse) {
var lastVal = NaN;
var callCount = 0;
var listener = function(val) { callCount++; lastVal = val; };
2017-05-24 00:20:31 -07:00
scope.$watch('{val: val}', listener);
2017-05-24 00:20:31 -07:00
scope.$apply('val = 1');
expect(callCount).toBe(1);
expect(lastVal).toEqual({val: 1});
2017-05-24 00:20:31 -07:00
scope.$apply('val = [2]');
expect(callCount).toBe(2);
expect(lastVal).toEqual({val: [2]});
2017-05-24 00:20:31 -07:00
scope.$apply('val.push(3)');
expect(callCount).toBe(2);
2017-05-24 00:20:31 -07:00
scope.$apply('val.length = 0');
expect(callCount).toBe(2);
}));
2017-05-24 00:20:31 -07:00
it('should only watch the direct inputs when nested', inject(function($parse) {
var lastVal = NaN;
var callCount = 0;
var listener = function(val) { callCount++; lastVal = val; };
2017-05-24 00:20:31 -07:00
scope.$watch('[{val: [val]}]', listener);
2017-05-24 00:20:31 -07:00
scope.$apply('val = 1');
expect(callCount).toBe(1);
expect(lastVal).toEqual([{val: [1]}]);
2014-11-23 23:08:33 +01:00
2017-05-24 00:20:31 -07:00
scope.$apply('val = [2]');
expect(callCount).toBe(2);
expect(lastVal).toEqual([{val: [[2]]}]);
2017-05-24 00:20:31 -07:00
scope.$apply('val.push(3)');
expect(callCount).toBe(2);
2017-05-24 00:20:31 -07:00
scope.$apply('val.length = 0');
expect(callCount).toBe(2);
}));
2017-05-24 00:20:31 -07:00
describe('with non-primative input', function() {
2017-05-24 00:20:31 -07:00
describe('that does NOT support valueOf()', function() {
it('should not be reevaluated', inject(function($parse) {
var obj = scope.obj = {};
2017-05-24 00:20:31 -07:00
var parsed = $parse('[obj]');
var watcherCalls = 0;
scope.$watch(parsed, function(input) {
expect(input[0]).toBe(obj);
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(watcherCalls).toBe(1);
}));
});
2017-05-24 00:20:31 -07:00
describe('that does support valueOf()', function() {
it('should not be reevaluated', inject(function($parse) {
var date = scope.date = new Date();
2017-05-24 00:20:31 -07:00
var parsed = $parse('[date]');
var watcherCalls = 0;
scope.$watch(parsed, function(input) {
expect(input[0]).toBe(date);
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(watcherCalls).toBe(1);
}));
2017-05-24 00:20:31 -07:00
it('should be reevaluated even when valueOf() changes', inject(function($parse) {
var date = scope.date = new Date();
2017-05-24 00:20:31 -07:00
var parsed = $parse('[date]');
var watcherCalls = 0;
scope.$watch(parsed, function(input) {
expect(input[0]).toBe(date);
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
date.setYear(1901);
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(watcherCalls).toBe(2);
}));
2017-05-24 00:20:31 -07:00
it('should not be reevaluated when the instance changes but valueOf() does not', inject(function($parse) {
scope.date = new Date(1234567890123);
2017-05-24 00:20:31 -07:00
var parsed = $parse('[date]');
var watcherCalls = 0;
scope.$watch(parsed, function(input) {
watcherCalls++;
});
2017-05-24 00:20:31 -07:00
scope.$digest();
expect(watcherCalls).toBe(1);
2017-05-24 00:20:31 -07:00
scope.date = new Date(1234567890123);
scope.$digest();
expect(watcherCalls).toBe(1);
}));
2017-05-24 00:20:31 -07:00
it('should be reevaluated when the instance does not change but valueOf() does', inject(function($parse) {
2017-05-24 00:20:31 -07:00
scope.date = new Date(1234567890123);
2017-05-24 00:20:31 -07:00
var parsed = $parse('[date]');
var watcherCalls = 0;
scope.$watch(parsed, function(input) {
watcherCalls++;
});
scope.$digest();
expect(watcherCalls).toBe(1);
scope.date.setTime(scope.date.getTime() + 1);
scope.$digest();
expect(watcherCalls).toBe(2);
}));
});
});
});
2014-11-23 23:08:33 +01:00
it('should continue with the evaluation of the expression without invoking computed parts',
inject(function($parse) {
var value = 'foo';
var spy = jasmine.createSpy();
spy.and.callFake(function() { return value; });
2014-11-23 23:08:33 +01:00
scope.foo = spy;
scope.$watch('foo() | uppercase');
2014-11-23 23:08:33 +01:00
scope.$digest();
expect(spy).toHaveBeenCalledTimes(2);
2014-11-23 23:08:33 +01:00
scope.$digest();
expect(spy).toHaveBeenCalledTimes(3);
2014-11-23 23:08:33 +01:00
value = 'bar';
scope.$digest();
expect(spy).toHaveBeenCalledTimes(5);
2014-11-23 23:08:33 +01:00
}));
it('should invoke all statements in multi-statement expressions', inject(function($parse) {
var lastVal = NaN;
var listener = function(val) { lastVal = val; };
scope.setBarToOne = false;
scope.bar = 0;
scope.two = 2;
scope.foo = function() { if (scope.setBarToOne) scope.bar = 1; };
scope.$watch('foo(); bar + two', listener);
2014-11-23 23:08:33 +01:00
scope.$digest();
expect(lastVal).toBe(2);
scope.bar = 2;
scope.$digest();
expect(lastVal).toBe(4);
scope.setBarToOne = true;
scope.$digest();
expect(lastVal).toBe(3);
}));
it('should watch the left side of assignments', inject(function($parse) {
var lastVal = NaN;
var listener = function(val) { lastVal = val; };
var objA = {};
var objB = {};
scope.$watch('curObj.value = input', noop);
2014-11-23 23:08:33 +01:00
scope.curObj = objA;
scope.input = 1;
scope.$digest();
expect(objA.value).toBe(scope.input);
scope.curObj = objB;
scope.$digest();
expect(objB.value).toBe(scope.input);
scope.input = 2;
scope.$digest();
expect(objB.value).toBe(scope.input);
}));
it('should watch ES6 object computed property changes', function() {
var count = 0;
var lastValue;
scope.$watch('{[a]: true}', function(val) {
count++;
lastValue = val;
});
scope.$digest();
expect(count).toBe(1);
expect(lastValue).toEqual({'undefined': true});
scope.$digest();
expect(count).toBe(1);
expect(lastValue).toEqual({'undefined': true});
scope.a = true;
scope.$digest();
expect(count).toBe(2);
expect(lastValue).toEqual({'true': true});
scope.a = 'abc';
scope.$digest();
expect(count).toBe(3);
expect(lastValue).toEqual({'abc': true});
scope.a = undefined;
scope.$digest();
expect(count).toBe(4);
expect(lastValue).toEqual({'undefined': true});
});
it('should not shallow-watch ES6 object computed properties in case of stateful toString', function() {
var count = 0;
var lastValue;
scope.$watch('{[a]: true}', function(val) {
count++;
lastValue = val;
});
scope.a = {toString: function() { return this.b; }};
scope.a.b = 1;
//TODO: would be great if it didn't throw!
expect(function() { scope.$apply(); }).toThrowMinErr('$rootScope', 'infdig');
expect(lastValue).toEqual({1: true});
expect(function() { scope.$apply('a.b = 2'); }).toThrowMinErr('$rootScope', 'infdig');
expect(lastValue).toEqual({2: true});
});
});
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);
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
}));
it('should expose traverse locals', inject(function($parse) {
expect($parse('a.b')({a: {b: 0}}, {a: {b:1}})).toEqual(1);
expect($parse('a.b')({a: null}, {a: {b:1}})).toEqual(1);
expect($parse('a.b')({a: {b: 0}}, {a: null})).toEqual(undefined);
expect($parse('a.b.c')({a: null}, {a: {b: {c: 1}}})).toEqual(1);
}));
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
it('should not use locals to resolve object properties', inject(function($parse) {
expect($parse('a[0].b')({a: [{b: 'scope'}]}, {b: 'locals'})).toBe('scope');
expect($parse('a[0]["b"]')({a: [{b: 'scope'}]}, {b: 'locals'})).toBe('scope');
expect($parse('a[0][0].b')({a: [[{b: 'scope'}]]}, {b: 'locals'})).toBe('scope');
expect($parse('a[0].b.c')({a: [{b: {c: 'scope'}}] }, {b: {c: 'locals'} })).toBe('scope');
}));
2014-11-23 23:08:33 +01:00
it('should assign directly to locals when the local property exists', inject(function($parse) {
var s = {}, l = {};
$parse('a = 1')(s, l);
2014-11-23 23:08:33 +01:00
expect(s.a).toBe(1);
expect(l.a).toBeUndefined();
l.a = 2;
$parse('a = 0')(s, l);
2014-11-23 23:08:33 +01:00
expect(s.a).toBe(1);
expect(l.a).toBe(0);
$parse('toString = 1')(s, l);
2014-11-23 23:08:33 +01:00
expect(isFunction(s.toString)).toBe(true);
expect(l.toString).toBe(1);
}));
it('should overwrite undefined / null scope properties when assigning', inject(function($parse) {
var scope;
scope = {};
$parse('a.b = 1')(scope);
$parse('c["d"] = 2')(scope);
expect(scope).toEqual({a: {b: 1}, c: {d: 2}});
scope = {a: {}};
$parse('a.b.c = 1')(scope);
$parse('a.c["d"] = 2')(scope);
expect(scope).toEqual({a: {b: {c: 1}, c: {d: 2}}});
scope = {a: undefined, c: undefined};
$parse('a.b = 1')(scope);
$parse('c["d"] = 2')(scope);
expect(scope).toEqual({a: {b: 1}, c: {d: 2}});
scope = {a: {b: undefined, c: undefined}};
$parse('a.b.c = 1')(scope);
$parse('a.c["d"] = 2')(scope);
expect(scope).toEqual({a: {b: {c: 1}, c: {d: 2}}});
scope = {a: null, c: null};
$parse('a.b = 1')(scope);
$parse('c["d"] = 2')(scope);
expect(scope).toEqual({a: {b: 1}, c: {d: 2}});
scope = {a: {b: null, c: null}};
$parse('a.b.c = 1')(scope);
$parse('a.c["d"] = 2')(scope);
expect(scope).toEqual({a: {b: {c: 1}, c: {d: 2}}});
}));
they('should not overwrite $prop scope properties when assigning', [0, false, '', NaN],
function(falsyValue) {
inject(function($parse) {
var scope;
scope = {a: falsyValue, c: falsyValue};
tryParseAndIgnoreException('a.b = 1');
tryParseAndIgnoreException('c["d"] = 2');
expect(scope).toEqual({a: falsyValue, c: falsyValue});
scope = {a: {b: falsyValue, c: falsyValue}};
tryParseAndIgnoreException('a.b.c = 1');
tryParseAndIgnoreException('a.c["d"] = 2');
expect(scope).toEqual({a: {b: falsyValue, c: falsyValue}});
// Helpers
//
// Normally assigning property on a primitive should throw exception in strict mode
// and silently fail in non-strict mode, IE seems to always have the non-strict-mode behavior,
// so if we try to use 'expect(function() {$parse('a.b=1')({a:false});).toThrow()' for testing
// the test will fail in case of IE because it will not throw exception, and if we just use
// '$parse('a.b=1')({a:false})' the test will fail because it will throw exception in case of Chrome
// so we use tryParseAndIgnoreException helper to catch the exception silently for all cases.
//
function tryParseAndIgnoreException(expression) {
try {
$parse(expression)(scope);
} catch (error) {/* ignore exception */}
}
});
});
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
});
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);
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
}));
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);
}));
fix($parse): deprecate promise unwrapping and make it an opt-in This commit disables promise unwrapping and adds $parseProvider.unwrapPromises() getter/setter api that allows developers to turn the feature back on if needed. Promise unwrapping support will be removed from Angular in the future and this setting only allows for enabling it during transitional period. 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 onces). To disable this logging use `$parseProvider.logPromiseWarnings(false)`. Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. 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 or automatically via routing and unifying the model access in this way. 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 - adds too much magic BREAKING CHANGE: $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via `$parseProvider.unwrapPromises(true)` api. Closes #4158 Closes #4270
2013-10-07 09:58:37 -07:00
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() {
2014-11-23 23:08:33 +01:00
it('should mark an empty expressions as constant', inject(function($parse) {
expect($parse('').constant).toBe(true);
expect($parse(' ').constant).toBe(true);
expect($parse('::').constant).toBe(true);
expect($parse(':: ').constant).toBe(true);
}));
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('-42').constant).toBe(true);
expect($parse('1 - 1').constant).toBe(true);
expect($parse('"foo" + "bar"').constant).toBe(true);
expect($parse('5 != null').constant).toBe(true);
expect($parse('{standard: 4/3, wide: 16/9}').constant).toBe(true);
expect($parse('{[standard]: 4/3, wide: 16/9}').constant).toBe(false);
expect($parse('{["key"]: 1}').constant).toBe(true);
expect($parse('[0].length').constant).toBe(true);
expect($parse('[0][0]').constant).toBe(true);
expect($parse('{x: 1}.x').constant).toBe(true);
expect($parse('{x: 1}["x"]').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('null/undefined in expressions', function() {
// simpleGetterFn1
it('should return null for `a` where `a` is null', inject(function($rootScope) {
$rootScope.a = null;
expect($rootScope.$eval('a')).toBe(null);
}));
it('should return undefined for `a` where `a` is undefined', inject(function($rootScope) {
expect($rootScope.$eval('a')).toBeUndefined();
}));
// simpleGetterFn2
it('should return undefined for properties of `null` constant', inject(function($rootScope) {
expect($rootScope.$eval('null.a')).toBeUndefined();
}));
it('should return undefined for properties of `null` values', inject(function($rootScope) {
$rootScope.a = null;
expect($rootScope.$eval('a.b')).toBeUndefined();
}));
it('should return null for `a.b` where `b` is null', inject(function($rootScope) {
$rootScope.a = { b: null };
expect($rootScope.$eval('a.b')).toBe(null);
}));
// cspSafeGetter && pathKeys.length < 6 || pathKeys.length > 2
it('should return null for `a.b.c.d.e` where `e` is null', inject(function($rootScope) {
$rootScope.a = { b: { c: { d: { e: null } } } };
expect($rootScope.$eval('a.b.c.d.e')).toBe(null);
}));
it('should return undefined for `a.b.c.d.e` where `d` is null', inject(function($rootScope) {
$rootScope.a = { b: { c: { d: null } } };
expect($rootScope.$eval('a.b.c.d.e')).toBeUndefined();
}));
// cspSafeGetter || pathKeys.length > 6
it('should return null for `a.b.c.d.e.f.g` where `g` is null', inject(function($rootScope) {
$rootScope.a = { b: { c: { d: { e: { f: { g: null } } } } } };
expect($rootScope.$eval('a.b.c.d.e.f.g')).toBe(null);
}));
it('should return undefined for `a.b.c.d.e.f.g` where `f` is null', inject(function($rootScope) {
$rootScope.a = { b: { c: { d: { e: { f: null } } } } };
expect($rootScope.$eval('a.b.c.d.e.f.g')).toBeUndefined();
}));
it('should return undefined if the return value of a function invocation is undefined',
inject(function($rootScope) {
$rootScope.fn = function() {};
expect($rootScope.$eval('fn()')).toBeUndefined();
}));
it('should ignore undefined values when doing addition/concatenation',
inject(function($rootScope) {
$rootScope.fn = function() {};
expect($rootScope.$eval('foo + "bar" + fn()')).toBe('bar');
}));
it('should treat properties named null/undefined as normal properties', inject(function($rootScope) {
expect($rootScope.$eval('a.null.undefined.b', {a:{null:{undefined:{b: 1}}}})).toBe(1);
}));
it('should not allow overriding null/undefined keywords', inject(function($rootScope) {
expect($rootScope.$eval('null.a', {null: {a: 42}})).toBeUndefined();
}));
it('should allow accessing null/undefined properties on `this`', inject(function($rootScope) {
$rootScope.null = {a: 42};
expect($rootScope.$eval('this.null.a')).toBe(42);
}));
it('should allow accessing $locals', inject(function($rootScope) {
$rootScope.foo = 'foo';
$rootScope.bar = 'bar';
$rootScope.$locals = 'foo';
var locals = {foo: 42};
expect($rootScope.$eval('$locals')).toBeUndefined();
expect($rootScope.$eval('$locals.foo')).toBeUndefined();
expect($rootScope.$eval('this.$locals')).toBe('foo');
expect(function() {
$rootScope.$eval('$locals = {}');
}).toThrow();
expect(function() {
$rootScope.$eval('$locals.bar = 23');
}).toThrow();
expect($rootScope.$eval('$locals', locals)).toBe(locals);
expect($rootScope.$eval('$locals.foo', locals)).toBe(42);
expect($rootScope.$eval('this.$locals', locals)).toBe('foo');
expect(function() {
$rootScope.$eval('$locals = {}', locals);
}).toThrow();
expect($rootScope.$eval('$locals.bar = 23', locals)).toEqual(23);
expect(locals.bar).toBe(23);
}));
});
});
});
forEach([true, false], function(cspEnabled) {
describe('custom identifiers (csp: ' + cspEnabled + ')', function() {
var isIdentifierStartRe = /[#a-z]/;
var isIdentifierContinueRe = /[-a-z]/;
var isIdentifierStartFn;
var isIdentifierContinueFn;
var scope;
beforeEach(module(function($parseProvider) {
isIdentifierStartFn = jasmine.
createSpy('isIdentifierStart').
and.callFake(function(ch, cp) { return isIdentifierStartRe.test(ch); });
isIdentifierContinueFn = jasmine.
createSpy('isIdentifierContinue').
and.callFake(function(ch, cp) { return isIdentifierContinueRe.test(ch); });
$parseProvider.setIdentifierFns(isIdentifierStartFn, isIdentifierContinueFn);
csp().noUnsafeEval = cspEnabled;
}));
beforeEach(inject(function($rootScope) {
scope = $rootScope;
}));
it('should allow specifying a custom `isIdentifierStart/Continue` functions', function() {
scope.x = {};
scope['#foo'] = 'foo';
scope.x['#foo'] = 'foo';
expect(scope.$eval('#foo')).toBe('foo');
expect(scope.$eval('x.#foo')).toBe('foo');
scope['bar--'] = 42;
scope.x['bar--'] = 42;
expect(scope.$eval('bar--')).toBe(42);
expect(scope.$eval('x.bar--')).toBe(42);
expect(scope['bar--']).toBe(42);
expect(scope.x['bar--']).toBe(42);
scope['#-'] = 'baz';
scope.x['#-'] = 'baz';
expect(scope.$eval('#-')).toBe('baz');
expect(scope.$eval('x.#-')).toBe('baz');
expect(function() { scope.$eval('##'); }).toThrow();
expect(function() { scope.$eval('x.##'); }).toThrow();
expect(function() { scope.$eval('--'); }).toThrow();
expect(function() { scope.$eval('x.--'); }).toThrow();
});
it('should pass the character and codepoint to the custom functions', function() {
scope.$eval('#-');
expect(isIdentifierStartFn).toHaveBeenCalledOnceWith('#', '#'.charCodeAt(0));
expect(isIdentifierContinueFn).toHaveBeenCalledOnceWith('-', '-'.charCodeAt(0));
isIdentifierStartFn.calls.reset();
isIdentifierContinueFn.calls.reset();
scope.$eval('#.foo.#-.bar-');
expect(isIdentifierStartFn).toHaveBeenCalledTimes(7);
expect(isIdentifierStartFn.calls.allArgs()).toEqual([
['#', '#'.charCodeAt(0)],
['.', '.'.charCodeAt(0)],
['f', 'f'.charCodeAt(0)],
['.', '.'.charCodeAt(0)],
['#', '#'.charCodeAt(0)],
['.', '.'.charCodeAt(0)],
['b', 'b'.charCodeAt(0)]
]);
expect(isIdentifierContinueFn).toHaveBeenCalledTimes(9);
expect(isIdentifierContinueFn.calls.allArgs()).toEqual([
['.', '.'.charCodeAt(0)],
['o', 'o'.charCodeAt(0)],
['o', 'o'.charCodeAt(0)],
['.', '.'.charCodeAt(0)],
['-', '-'.charCodeAt(0)],
['.', '.'.charCodeAt(0)],
['a', 'a'.charCodeAt(0)],
['r', 'r'.charCodeAt(0)],
['-', '-'.charCodeAt(0)]
]);
});
});
});
describe('hidden/unsupported features', function() {
describe('$$getAst()', function() {
it('should be a method exposed on the `$parse` service', inject(function($parse) {
expect(isFunction($parse.$$getAst)).toBeTruthy();
}));
it('should accept a string expression argument and return the corresponding AST', inject(function($parse) {
var ast = $parse.$$getAst('foo.bar');
expect(ast).toEqual({
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo' },
property: { type: 'Identifier', name: 'bar' },
computed: false
}
}
]
});
}));
it('should parse one time binding expressions', inject(function($parse) {
var ast = $parse.$$getAst('::foo.bar');
expect(ast).toEqual({
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'foo' },
property: { type: 'Identifier', name: 'bar' },
computed: false
}
}
]
});
}));
});
});
2010-10-15 13:44:53 -07:00
});