'use strict';

// This file has many tests which read nicely if constant conditions
// are used.
/* eslint-disable no-constant-condition */

describe('parser', function() {

  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"');
      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');

      i++;
      expect(tokens[i].index).toEqual(4);
      expect(tokens[i].text).toEqual('[');

      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);

      i++;
      expect(tokens[i].index).toEqual(7);
      expect(tokens[i].text).toEqual(']');

      i++;
      expect(tokens[i].index).toEqual(8);
      expect(tokens[i].text).toEqual('+');

      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);

      i++;
      expect(tokens[i].index).toEqual(12);
      expect(tokens[i].text).toEqual('|');

      i++;
      expect(tokens[i].index).toEqual(13);
      expect(tokens[i].text).toEqual('f');

      i++;
      expect(tokens[i].index).toEqual(14);
      expect(tokens[i].text).toEqual(':');

      i++;
      expect(tokens[i].index).toEqual(15);
      expect(tokens[i].value).toEqual('a\'c');

      i++;
      expect(tokens[i].index).toEqual(21);
      expect(tokens[i].text).toEqual(':');

      i++;
      expect(tokens[i].index).toEqual(22);
      expect(tokens[i].value).toEqual('d"e');
    });

    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');
      var i = 0;
      expect(tokens[i].index).toEqual(0);
      expect(tokens[i].text).toEqual('undefined');
    });

    it('should tokenize quoted string', function() {
      var str = '[\'\\\'\', "\\""]';
      var tokens = lex(str);

      expect(tokens[1].index).toEqual(1);
      expect(tokens[1].value).toEqual('\'');

      expect(tokens[3].index).toEqual(7);
      expect(tokens[3].value).toEqual('"');
    });

    it('should tokenize escaped quoted string', function() {
      var str = '"\\"\\n\\f\\r\\t\\v\\u00A0"';
      var tokens = lex(str);

      expect(tokens[0].value).toEqual('"\n\f\r\t\v\u00A0');
    });

    it('should tokenize unicode', function() {
      var tokens = lex('"\\u00A0"');
      expect(tokens.length).toEqual(1);
      expect(tokens[0].value).toEqual('\u00a0');
    });

    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');
    });

    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('!==');
    });

    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(':');
    });

    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(';');
    });

    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);
    });

    it('should tokenize negative number', inject(function($rootScope) {
      var value = $rootScope.$eval('-0.5');
      expect(value).toEqual(-0.5);

      value = $rootScope.$eval('{a:-0.5}');
      expect(value).toEqual({a:-0.5});
    }));

    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);

      tokens = lex('0.5E+10');
      expect(tokens[0].value).toEqual(0.5E+10);
    }));

    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);
    });

    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\'].');
    });
  });

  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}});
        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) {
        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.)');
    });


    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]');
    });


    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() {
      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() {
      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,
                    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,
                    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,
                    value: { type: 'Identifier', name: 'bar' }
                  },
                  {
                    type: 'Property',
                    kind: 'init',
                    key: { type: 'Literal', value: 'man' },
                    computed: false,
                    value: { type: 'Literal', value: 'shell' }
                  },
                  {
                    type: 'Property',
                    kind: 'init',
                    key: { type: 'Literal', value: 42 },
                    computed: false,
                    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,
                    value: { type: 'Identifier', name: 'bar' }
                  },
                  {
                    type: 'Property',
                    kind: 'init',
                    key: { type: 'Literal', value: 'man' },
                    computed: false,
                    value: { type: 'Literal', value: 'shell' }
                  },
                  {
                    type: 'Property',
                    kind: 'init',
                    key: { type: 'Literal', value: 42 },
                    computed: false,
                    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' }
                  }
                ]
              }
            }
          ]
        }
      );
    });


    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
              }
            }
          ]
        }
      );
    });


    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
              }
            }
          ]
        }
      );
    });


    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
              }
            }
          ]
        }
      );
    });


    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
              }
            }
          ]
        }
      );
    });


    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: '='
                  }
                ]
              }
            }
          ]
        }
      );
    });


    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: '='
                }
              }
            }
          ]
        }
      );
    });


    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: '='
                  }
                ]
              }
            }
          ]
        }
      );
    });


    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,
                    value: {
                      type: 'AssignmentExpression',
                      left: { type: 'Identifier', name: 'bar' },
                      right: { type: 'Literal', value: 1 },
                      operator: '='
                    }
                  }
                ]
              }
            }
          ]
        }
      );
    });


    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
              }
            }
          ]
        }
      );
    });


    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;
  }]));

  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() {

      beforeEach(module(function() {
        expect(csp().noUnsafeEval === true ||
               csp().noUnsafeEval === false).toEqual(true);
        csp().noUnsafeEval = cspEnabled;
      }, provideLog));

      beforeEach(inject(function($rootScope) {
        scope = $rootScope;
      }));

      it('should parse expressions', function() {
        expect(scope.$eval('-1')).toEqual(-1);
        expect(scope.$eval('1 + 2.5')).toEqual(3.5);
        expect(scope.$eval('1 + -2.5')).toEqual(-1.5);
        expect(scope.$eval('1+2*3/4')).toEqual(1 + 2 * 3 / 4);
        expect(scope.$eval('0--1+1.5')).toEqual(0 - -1 + 1.5);
        expect(scope.$eval('-0--1++2*-3/-4')).toEqual(-0 - -1 + +2 * -3 / -4);
        expect(scope.$eval('1/2*3')).toEqual(1 / 2 * 3);
      });

      it('should parse 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);
      });

      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()));
      });

      it('should parse string', function() {
        expect(scope.$eval('\'a\' + \'b c\'')).toEqual('ab c');
      });

      it('should parse filters', function() {
        $filterProvider.register('substring', valueFn(function(input, start, end) {
          return input.substring(start, end);
        }));

        expect(function() {
          scope.$eval('1|nonexistent');
        }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: nonexistentFilterProvider <- nonexistentFilter');

        scope.offset =  3;
        expect(scope.$eval('\'abcd\'|substring:1:offset')).toEqual('bc');
        expect(scope.$eval('\'abcd\'|substring:1:3|uppercase')).toEqual('BC');
      });

      it('should access scope', function() {
        scope.a =  123;
        scope.b = {c: 456};
        expect(scope.$eval('a', scope)).toEqual(123);
        expect(scope.$eval('b.c', scope)).toEqual(456);
        expect(scope.$eval('x.y.z', scope)).not.toBeDefined();
      });

      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);
        });
      });

      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();
        scope.a = undefined;
        expect(scope.$eval('a - b')).toBe(0);
        expect(scope.$eval('a + b')).toBeUndefined();
        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');
      });

      it('should not break if hasOwnProperty is referenced in an expression', function() {
        scope.obj = { value: 1};
        // By evaluating an expression that calls hasOwnProperty, the getterFnCache
        // will store a property called hasOwnProperty.  This is effectively:
        // getterFnCache['hasOwnProperty'] = null
        scope.$eval('obj.hasOwnProperty("value")');
        // If we rely on this property then evaluating any expression will fail
        // because it is not able to find out if obj.value is there in the cache
        expect(scope.$eval('obj.value')).toBe(1);
      });

      it('should 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');
      });

      it('should evaluate grouped expressions', function() {
        expect(scope.$eval('(1+2)*3')).toEqual((1 + 2) * 3);
      });

      it('should evaluate assignments', function() {
        expect(scope.$eval('a=12')).toEqual(12);
        expect(scope.a).toEqual(12);

        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);
      });

      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);
      });

      it('should evaluate function call with arguments', function() {
        scope.add =  function(a, b) {
          return a + b;
        };
        expect(scope.$eval('add(1,2)')).toEqual(3);
      });

      it('should 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);
      });

      // 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);
      });

      it('should evaluate array', function() {
        expect(scope.$eval('[]').length).toEqual(0);
        expect(scope.$eval('[1, 2]').length).toEqual(2);
        expect(scope.$eval('[1, 2]')[0]).toEqual(1);
        expect(scope.$eval('[1, 2]')[1]).toEqual(2);
        expect(scope.$eval('[1, 2,]')[1]).toEqual(2);
        expect(scope.$eval('[1, 2,]').length).toEqual(2);
      });

      it('should evaluate array access', function() {
        expect(scope.$eval('[1][0]')).toEqual(1);
        expect(scope.$eval('[[1]][0][0]')).toEqual(1);
        expect(scope.$eval('[].length')).toEqual(0);
        expect(scope.$eval('[1, 2].length')).toEqual(2);
      });

      it('should evaluate 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}]');
      });

      it('should evaluate object access', function() {
        expect(scope.$eval('{false:\'WC\', true:\'CC\'}[false]')).toEqual('WC');
      });

      it('should evaluate JSON', function() {
        expect(scope.$eval('[{}]')).toEqual([{}]);
        expect(scope.$eval('[{a:[]}, {b:1}]')).toEqual([{a:[]}, {b:1}]);
      });

      it('should evaluate multiple statements', function() {
        expect(scope.$eval('a=1;b=3;a+b')).toEqual(4);
        expect(scope.$eval(';;1;;')).toEqual(1);
      });

      it('should evaluate object 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);
      });

      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);
      });

      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);
      });


      it('should call the function once when it is part of the context', function() {
        var count = 0;
        scope.fn = function() {
          count++;
          return { anotherFn: function() { return 'lucas'; } };
        };
        expect(scope.$eval('fn().anotherFn()')).toBe('lucas');
        expect(count).toBe(1);
      });


      it('should call the function once when it is not part of the context', function() {
        var count = 0;
        scope.fn = function() {
          count++;
          return function() { return 'lucas'; };
        };
        expect(scope.$eval('fn()()')).toBe('lucas');
        expect(count).toBe(1);
      });


      it('should call the function once when it is part of the context on assignments', function() {
        var count = 0;
        var element = {};
        scope.fn = function() {
          count++;
          return element;
        };
        expect(scope.$eval('fn().name = "lucas"')).toBe('lucas');
        expect(element.name).toBe('lucas');
        expect(count).toBe(1);
      });


      it('should call the function once when it is part of the context on array lookups', function() {
        var count = 0;
        var element = [];
        scope.fn = function() {
          count++;
          return element;
        };
        expect(scope.$eval('fn()[0] = "lucas"')).toBe('lucas');
        expect(element[0]).toBe('lucas');
        expect(count).toBe(1);
      });


      it('should 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);
      });


      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);
      });


      describe('assignable', function() {
        it('should expose assignment function', inject(function($parse) {
          var fn = $parse('a');
          expect(fn.assign).toBeTruthy();
          var scope = {};
          fn.assign(scope, 123);
          expect(scope).toEqual({a:123});
        }));

        it('should 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);
        }));
      });

      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() {
          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);
        });

        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]);
          });

          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);
          });

          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;
            }));

            var parsed = $parse('{x: 1} | foo:1');

            expect(parsed.constant).toBe(true);

            var watcherCalls = 0;
            scope.$watch(parsed, function(input) {
              expect(input).toEqual({x:1});
              watcherCalls++;
            });

            scope.$digest();
            expect(filterCalls).toBe(1);
            expect(watcherCalls).toBe(1);

            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}]);
          });

          describe('with non-primitive input', function() {

            describe('that does NOT support valueOf()', function() {

              it('should always be reevaluated', inject(function($parse) {
                var filterCalls = 0;
                $filterProvider.register('foo', valueFn(function(input) {
                  filterCalls++;
                  return input;
                }));

                var parsed = $parse('obj | foo');
                var obj = scope.obj = {};

                var watcherCalls = 0;
                scope.$watch(parsed, function(input) {
                  expect(input).toBe(obj);
                  watcherCalls++;
                });

                scope.$digest();
                expect(filterCalls).toBe(2);
                expect(watcherCalls).toBe(1);

                scope.$digest();
                expect(filterCalls).toBe(3);
                expect(watcherCalls).toBe(1);
              }));

              it('should always be reevaluated in literals', inject(function($parse) {
                $filterProvider.register('foo', valueFn(function(input) {
                  return input.b > 0;
                }));

                scope.$watch('[(a | foo)]', function() {});

                // 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');
              }));

              it('should always be reevaluated when passed literals', inject(function($parse) {
                scope.$watch('[a] | filter', function() {});

                scope.$apply('a = 1');

                // Would be great if filter-output was checked for changes and this didn't throw...
                expect(function() { scope.$apply('a = {}'); }).toThrowMinErr('$rootScope', 'infdig');
              }));
            });

            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);
              }));

              it('should not be reevaluated in literals', inject(function($parse) {
                var filterCalls = 0;
                $filterProvider.register('foo', valueFn(function(input) {
                  filterCalls++;
                  return input;
                }));

                scope.date = new Date(1234567890123);

                var watcherCalls = 0;
                scope.$watch('[(date | foo)]', function(input) {
                  watcherCalls++;
                });

                scope.$digest();
                expect(filterCalls).toBe(1);
                expect(watcherCalls).toBe(1);

                scope.$digest();
                expect(filterCalls).toBe(1);
                expect(watcherCalls).toBe(1);
              }));

              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;
                }));

                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);

                date.setYear(1901);

                scope.$digest();
                expect(filterCalls).toBe(2);
                expect(watcherCalls).toBe(1);
              }));

              it('should be reevaluated in literals when valueOf() changes', inject(function($parse) {
                var filterCalls = 0;
                $filterProvider.register('foo', valueFn(function(input) {
                  filterCalls++;
                  return input;
                }));

                scope.date = new Date(1234567890123);

                var watcherCalls = 0;
                scope.$watch('[(date | foo)]', function(input) {
                  watcherCalls++;
                });

                scope.$digest();
                expect(filterCalls).toBe(1);
                expect(watcherCalls).toBe(1);

                scope.date.setTime(1234567890);

                scope.$digest();
                expect(filterCalls).toBe(2);
                expect(watcherCalls).toBe(2);
              }));

              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;
                }));

                scope.date = new Date(1234567890123);

                var watcherCalls = 0;
                scope.$watch($parse('[(date | foo)]'), function(input) {
                  watcherCalls++;
                });

                scope.$digest();
                expect(watcherCalls).toBe(1);
                expect(filterCalls).toBe(1);

                scope.date = new Date(1234567890123);
                scope.$digest();
                expect(watcherCalls).toBe(1);
                expect(filterCalls).toBe(1);
              }));
            });

            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;
              }));

              scope.obj = {};

              var watcherCalls = 0;
              scope.$watch('!obj | foo:!obj', function(input) {
                watcherCalls++;
              });

              scope.$digest();
              expect(filterCalls).toBe(1);
              expect(watcherCalls).toBe(1);

              scope.$digest();
              expect(filterCalls).toBe(1);
              expect(watcherCalls).toBe(1);
            }));

            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;
              }));

              scope.obj = {};

              var watcherCalls = 0;
              scope.$watch('1 - obj | foo:(1 * obj)', function(input) {
                watcherCalls++;
              });

              scope.$digest();
              expect(filterCalls).toBe(1);
              expect(watcherCalls).toBe(1);

              scope.$digest();
              expect(filterCalls).toBe(1);
              expect(watcherCalls).toBe(1);
            }));

            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;
              }));

              scope.obj = {};

              var watcherCalls = 0;
              scope.$watch('1 + obj | foo', function(input) {
                watcherCalls++;
              });

              scope.$digest();
              expect(filterCalls).toBe(2);
              expect(watcherCalls).toBe(1);

              scope.$digest();
              expect(filterCalls).toBe(3);
              expect(watcherCalls).toBe(1);
            }));

            it('should reevaluate computed member expressions', inject(function($parse) {
              var toStringCalls = 0;

              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;
              }));

              var parsed = $parse('obj | foo');
              var obj = scope.obj = Object.create(null);

              var watcherCalls = 0;
              scope.$watch(parsed, function(input) {
                expect(input).toBe(obj);
                watcherCalls++;
              });

              scope.$digest();
              expect(filterCalls).toBe(2);
              expect(watcherCalls).toBe(1);

              scope.$digest();
              expect(filterCalls).toBe(3);
              expect(watcherCalls).toBe(1);
            }));
          });

          describe('with primitive input', function() {

            it('should not be reevaluated when passed literals', inject(function($parse) {
              var filterCalls = 0;
              $filterProvider.register('foo', valueFn(function(input) {
                filterCalls++;
                return input;
              }));

              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;
              }));

              scope.prim = 1234567890123;

              var watcherCalls = 0;
              scope.$watch('[(prim | foo)]', function(input) {
                watcherCalls++;
              });

              scope.$digest();
              expect(filterCalls).toBe(1);
              expect(watcherCalls).toBe(1);

              scope.$digest();
              expect(filterCalls).toBe(1);
              expect(watcherCalls).toBe(1);
            }));
          });
        });

        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]);
          }));

          it('should always be invoked if they are flagged as having $stateful',
              inject(function($parse) {
            var called = false;
            function interceptor() {
              called = true;
            }
            interceptor.$stateful = true;

            scope.$watch($parse('a', interceptor));
            scope.a = 0;
            scope.$digest();
            expect(called).toBe(true);

            called = false;
            scope.$digest();
            expect(called).toBe(true);

            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);
          }));

          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);
          }));

          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);
          }));

          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);
          }));

          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);
          }));
        });

        describe('literals', function() {

          it('should support watching', inject(function($parse) {
            var lastVal = NaN;
            var callCount = 0;
            var listener = function(val) { callCount++; lastVal = val; };

            scope.$watch('{val: val}', listener);

            scope.$apply('val = 1');
            expect(callCount).toBe(1);
            expect(lastVal).toEqual({val: 1});

            scope.$apply('val = []');
            expect(callCount).toBe(2);
            expect(lastVal).toEqual({val: []});

            scope.$apply('val = []');
            expect(callCount).toBe(3);
            expect(lastVal).toEqual({val: []});

            scope.$apply('val = {}');
            expect(callCount).toBe(4);
            expect(lastVal).toEqual({val: {}});
          }));

          it('should only watch the direct inputs', inject(function($parse) {
            var lastVal = NaN;
            var callCount = 0;
            var listener = function(val) { callCount++; lastVal = val; };

            scope.$watch('{val: val}', listener);

            scope.$apply('val = 1');
            expect(callCount).toBe(1);
            expect(lastVal).toEqual({val: 1});

            scope.$apply('val = [2]');
            expect(callCount).toBe(2);
            expect(lastVal).toEqual({val: [2]});

            scope.$apply('val.push(3)');
            expect(callCount).toBe(2);

            scope.$apply('val.length = 0');
            expect(callCount).toBe(2);
          }));

          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; };

            scope.$watch('[{val: [val]}]', listener);

            scope.$apply('val = 1');
            expect(callCount).toBe(1);
            expect(lastVal).toEqual([{val: [1]}]);

            scope.$apply('val = [2]');
            expect(callCount).toBe(2);
            expect(lastVal).toEqual([{val: [[2]]}]);

            scope.$apply('val.push(3)');
            expect(callCount).toBe(2);

            scope.$apply('val.length = 0');
            expect(callCount).toBe(2);
          }));

          describe('with non-primative input', function() {

            describe('that does NOT support valueOf()', function() {
              it('should not be reevaluated', inject(function($parse) {
                var obj = scope.obj = {};

                var parsed = $parse('[obj]');
                var watcherCalls = 0;
                scope.$watch(parsed, function(input) {
                  expect(input[0]).toBe(obj);
                  watcherCalls++;
                });

                scope.$digest();
                expect(watcherCalls).toBe(1);

                scope.$digest();
                expect(watcherCalls).toBe(1);
              }));
            });

            describe('that does support valueOf()', function() {
              it('should not be reevaluated', inject(function($parse) {
                var date = scope.date = new Date();

                var parsed = $parse('[date]');
                var watcherCalls = 0;
                scope.$watch(parsed, function(input) {
                  expect(input[0]).toBe(date);
                  watcherCalls++;
                });

                scope.$digest();
                expect(watcherCalls).toBe(1);

                scope.$digest();
                expect(watcherCalls).toBe(1);
              }));

              it('should be reevaluated even when valueOf() changes', inject(function($parse) {
                var date = scope.date = new Date();

                var parsed = $parse('[date]');
                var watcherCalls = 0;
                scope.$watch(parsed, function(input) {
                  expect(input[0]).toBe(date);
                  watcherCalls++;
                });

                scope.$digest();
                expect(watcherCalls).toBe(1);

                date.setYear(1901);

                scope.$digest();
                expect(watcherCalls).toBe(2);
              }));

              it('should not be reevaluated when the instance changes but valueOf() does not', inject(function($parse) {
                scope.date = new Date(1234567890123);

                var parsed = $parse('[date]');
                var watcherCalls = 0;
                scope.$watch(parsed, function(input) {
                  watcherCalls++;
                });

                scope.$digest();
                expect(watcherCalls).toBe(1);

                scope.date = new Date(1234567890123);
                scope.$digest();
                expect(watcherCalls).toBe(1);
              }));

              it('should be reevaluated when the instance does not change but valueOf() does', inject(function($parse) {

                scope.date = new Date(1234567890123);

                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);
              }));
            });
          });
        });

        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; });
          scope.foo = spy;
          scope.$watch('foo() | uppercase');
          scope.$digest();
          expect(spy).toHaveBeenCalledTimes(2);
          scope.$digest();
          expect(spy).toHaveBeenCalledTimes(3);
          value = 'bar';
          scope.$digest();
          expect(spy).toHaveBeenCalledTimes(5);
        }));

        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);

          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);

          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);
        }));

        it('should expose traverse locals', inject(function($parse) {
          expect($parse('a.b')({a: {b: 0}}, {a: {b:1}})).toEqual(1);
          expect($parse('a.b')({a: null}, {a: {b:1}})).toEqual(1);
          expect($parse('a.b')({a: {b: 0}}, {a: null})).toEqual(undefined);
          expect($parse('a.b.c')({a: null}, {a: {b: {c: 1}}})).toEqual(1);
        }));

        it('should not use locals to resolve object properties', inject(function($parse) {
          expect($parse('a[0].b')({a: [{b: 'scope'}]}, {b: 'locals'})).toBe('scope');
          expect($parse('a[0]["b"]')({a: [{b: 'scope'}]}, {b: 'locals'})).toBe('scope');
          expect($parse('a[0][0].b')({a: [[{b: 'scope'}]]}, {b: 'locals'})).toBe('scope');
          expect($parse('a[0].b.c')({a: [{b: {c: 'scope'}}] }, {b: {c: 'locals'} })).toBe('scope');
        }));

        it('should assign directly to locals when the local property exists', inject(function($parse) {
          var s = {}, l = {};

          $parse('a = 1')(s, l);
          expect(s.a).toBe(1);
          expect(l.a).toBeUndefined();

          l.a = 2;
          $parse('a = 0')(s, l);
          expect(s.a).toBe(1);
          expect(l.a).toBe(0);

          $parse('toString = 1')(s, l);
          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 */}
              }
            });
          });
      });

      describe('literal', function() {
        it('should mark scalar value expressions as literal', inject(function($parse) {
          expect($parse('0').literal).toBe(true);
          expect($parse('"hello"').literal).toBe(true);
          expect($parse('true').literal).toBe(true);
          expect($parse('false').literal).toBe(true);
          expect($parse('null').literal).toBe(true);
          expect($parse('undefined').literal).toBe(true);
        }));

        it('should mark array expressions as literal', inject(function($parse) {
          expect($parse('[]').literal).toBe(true);
          expect($parse('[1, 2, 3]').literal).toBe(true);
          expect($parse('[1, identifier]').literal).toBe(true);
        }));

        it('should mark object expressions as literal', inject(function($parse) {
          expect($parse('{}').literal).toBe(true);
          expect($parse('{x: 1}').literal).toBe(true);
          expect($parse('{foo: bar}').literal).toBe(true);
        }));

        it('should not mark function calls or operator expressions as literal', inject(function($parse) {
          expect($parse('1 + 1').literal).toBe(false);
          expect($parse('call()').literal).toBe(false);
          expect($parse('[].length').literal).toBe(false);
        }));
      });

      describe('constant', function() {
        it('should mark 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
              }
            }
          ]
        });
      }));
    });
  });
});
