
package.src.core.parse.parse.spec.js Maven / Gradle / Ivy
import { AST } from "./ast/ast.js";
import { Lexer } from "./lexer/lexer.js";
import {
isFunction,
sliceArgs,
csp,
valueFn,
extend,
} from "../../shared/utils.js";
import { createInjector } from "../di/injector.js";
import { ASTType } from "./ast-type.js";
import { Angular } from "../../loader.js";
import { wait } from "../../shared/test-utils.js";
describe("parser", () => {
let $rootScope;
let $parse;
let scope;
let logs = [];
beforeEach(() => {
window.angular = new Angular();
window.angular
.module("myModule", ["ng"])
.decorator("$exceptionHandler", function () {
return (exception, cause) => {
logs.push(exception);
console.error(exception, cause);
};
});
let injector = createInjector(["myModule"]);
$parse = injector.get("$parse");
$rootScope = injector.get("$rootScope");
});
let filterProvider;
beforeEach(() => {
createInjector([
"ng",
function ($filterProvider) {
filterProvider = $filterProvider;
},
]).invoke((_$rootScope_) => {
$rootScope = _$rootScope_;
});
});
[true, false].forEach((cspEnabled) => {
describe(`csp: ${cspEnabled}`, () => {
beforeEach(() => {
createInjector([
"ng",
function ($filterProvider) {
filterProvider = $filterProvider;
},
]).invoke((_$rootScope_) => {
scope = _$rootScope_;
});
});
it("should parse expressions", () => {
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", () => {
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", () => {
/* 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(2 < 3 == true);
expect(scope.$eval("true===2<3")).toEqual(2 < 3 === true);
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", () => {
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", () => {
const returnTrue = (scope.returnTrue = function () {
return true;
});
const returnFalse = (scope.returnFalse = function () {
return false;
});
const returnString = (scope.returnString = function () {
return "asd";
});
const returnInt = (scope.returnInt = function () {
return 123;
});
const 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", () => {
expect(scope.$eval("'a' + 'b c'")).toEqual("ab c");
});
it("should parse filters", () => {
filterProvider.register(
"substring",
valueFn((input, start, end) => input.substring(start, end)),
);
expect(() => {
scope.$eval("1|nonexistent");
}).toThrowError();
scope.offset = 3;
expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc");
});
it("should access scope", () => {
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", () => {
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", () => {
scope.a = {
b() {
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", () => {
scope.a = { b: 4 };
expect(() => {
scope.$eval("a.", scope);
}).toThrowError(/ueoe/);
expect(() => {
scope.$eval("a .", scope);
}).toThrowError(/ueoe/);
});
it("should resolve deeply nested paths (important for CSP mode)", () => {
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!");
});
[2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 42, 99].forEach((pathLength) => {
it(`should resolve nested paths of length ${pathLength}`, () => {
// Create a nested object {x2: {x3: {x4: ... {x[n]: 42} ... }}}.
let obj = 42;
const locals = {};
for (var i = pathLength; i >= 2; i--) {
const 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;
let 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", () => {
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", () => {
// 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", () => {
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"', () => {
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", () => {
expect(scope.$eval("(1+2)*3")).toEqual((1 + 2) * 3);
});
it("should evaluate assignments", () => {
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", () => {
expect(() => {
scope.$eval("1 = 1");
}).toThrowError(/lval/);
expect(() => {
scope.$eval("{} = 1");
}).toThrowError(/lval/);
expect(() => {
scope.$eval("[] = 1");
}).toThrowError(/lval/);
expect(() => {
scope.$eval("true = 1");
}).toThrowError(/lval/);
expect(() => {
scope.$eval("(a=b) = 1");
}).toThrowError(/lval/);
expect(() => {
scope.$eval("(1<2) = 1");
}).toThrowError(/lval/);
expect(() => {
scope.$eval("(1+2) = 1");
}).toThrowError(/lval/);
expect(() => {
scope.$eval("!v = 1");
}).toThrowError(/lval/);
expect(() => {
scope.$eval("this = 1");
}).toThrowError(/lval/);
expect(() => {
scope.$eval("+v = 1");
}).toThrowError(/lval/);
expect(() => {
scope.$eval("(1?v1:v2) = 1");
}).toThrowError(/lval/);
});
it("should evaluate assignments in ternary operator", () => {
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", () => {
scope.const = function (a, b) {
return 123;
};
expect(scope.$eval("const()")).toEqual(123);
});
it("should evaluate function call with arguments", () => {
scope.add = function (a, b) {
return a + b;
};
expect(scope.$eval("add(1,2)")).toEqual(3);
});
it("should allow filter chains as arguments", () => {
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)"),
).toEqual("bcbc");
});
it("should evaluate function call from a return value", () => {
scope.getter = function () {
return function () {
return 33;
};
};
expect(scope.$eval("getter()()")).toBe(33);
});
it("should evaluate multiplication and division", () => {
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", () => {
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", () => {
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", () => {
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", () => {
expect(() => {
scope.$eval("{[:0}");
}).toThrowError(/syntax/);
expect(() => {
scope.$eval("{{:0}");
}).toThrowError(/syntax/);
expect(() => {
scope.$eval("{?:0}");
}).toThrowError(/syntax/);
expect(() => {
scope.$eval("{):0}");
}).toThrowError(/syntax/);
});
it("should evaluate object access", () => {
expect(scope.$eval("{false:'WC', true:'CC'}[false]")).toEqual("WC");
});
it("should evaluate JSON", () => {
expect(scope.$eval("[{}]")).toEqual([{}]);
expect(scope.$eval("[{a:[]}, {b:1}]")).toEqual([{ a: [] }, { b: 1 }]);
});
it("should evaluate multiple statements", () => {
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 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 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", () => {
scope.a = "abc";
expect(scope.$eval("{a:a}").a).toEqual("abc");
});
it("should evaluate field access on function call result", () => {
scope.a = function () {
return { name: "misko" };
};
expect(scope.$eval("a().name")).toEqual("misko");
});
it("should evaluate field access after array access", () => {
scope.items = [{}, { name: "misko" }];
expect(scope.$eval("items[1].name")).toEqual("misko");
});
it("should evaluate array assignment", () => {
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", () => {
scope.name = "MISKO";
expect(scope.$eval("n = (name|limitTo:2|limitTo:1)")).toEqual("M");
expect(scope.$eval("n")).toEqual("M");
});
it("should evaluate remainder", () => {
expect(scope.$eval("1%2")).toEqual(1);
});
it("should evaluate sum with undefined", () => {
expect(scope.$eval("1+undefined")).toEqual(1);
expect(scope.$eval("undefined+1")).toEqual(1);
});
it("should throw exception on non-closed bracket", () => {
expect(() => {
scope.$eval("[].count(");
}).toThrowError(/ueoe/);
});
it("should evaluate double negation", () => {
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", () => {
expect(scope.$eval("!false || true")).toEqual(!false || true);
expect(scope.$eval("!11 == 10")).toEqual(!11 == 10);
expect(scope.$eval("12/6/2")).toEqual(12 / 6 / 2);
});
it("should evaluate exclamation mark", () => {
expect(scope.$eval('suffix = "!"')).toEqual("!");
});
it("should evaluate minus", () => {
expect(scope.$eval("{a:'-'}")).toEqual({ a: "-" });
});
it("should evaluate undefined", () => {
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", () => {
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", () => {
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", () => {
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", () => {
scope.object = {};
[
{ toString: 2 },
{ toString: null },
{
toString() {
return {};
},
},
].forEach((brokenObject) => {
scope.brokenObject = brokenObject;
expect(() => {
scope.$eval("object[brokenObject]");
}).toThrow();
});
});
it("should support method calls on primitive types", () => {
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", () => {
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", () => {
const s = {
c() {
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", () => {
const s = {
c() {
return { b: 1 };
},
i(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", () => {
const s = {
c() {
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", () => {
let n = 0;
scope.fn = function () {
const c = n++;
return {
c,
anotherFn() {
return this.c === c;
},
};
};
expect(scope.$eval("fn().anotherFn()")).toBe(true);
});
it("should call the function once when it is part of the context", () => {
let count = 0;
scope.fn = function () {
count++;
return {
anotherFn() {
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", () => {
let 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", () => {
let count = 0;
const 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", () => {
let count = 0;
const 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", () => {
let count = 0;
const element = [
{
anotherFn() {
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", () => {
let count = 0;
const element = {
name: {
anotherFn() {
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", () => {
let 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", () => {
beforeEach(() => {
createInjector([
"ng",
function ($filterProvider) {
filterProvider = $filterProvider;
},
]);
});
it("should expose assignment function", () => {
const fn = $parse("a");
expect(fn.assign).toBeTruthy();
const scope = {};
fn.assign(scope, 123);
expect(scope).toEqual({ a: 123 });
});
it("should return the assigned value", () => {
const fn = $parse("a");
const scope = {};
expect(fn.assign(scope, 123)).toBe(123);
const someObject = {};
expect(fn.assign(scope, someObject)).toBe(someObject);
});
it("should expose working assignment function for expressions ending with brackets", () => {
const fn = $parse('a.b["c"]');
expect(fn.assign).toBeTruthy();
const 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", () => {
const fn = $parse('a["b"].c');
expect(fn.assign).toBeTruthy();
const scope = {};
fn.assign(scope, 123);
expect(scope.a.b.c).toEqual(123);
});
it("should create objects when finding a null", () => {
const fn = $parse("foo.bar");
const scope = { foo: null };
fn.assign(scope, 123);
expect(scope.foo.bar).toEqual(123);
});
it("should create objects when finding a null", () => {
const fn = $parse('foo["bar"]');
const scope = { foo: null };
fn.assign(scope, 123);
expect(scope.foo.bar).toEqual(123);
});
it("should create objects when finding a null", () => {
const fn = $parse("foo.bar.baz");
const scope = { foo: null };
fn.assign(scope, 123);
expect(scope.foo.bar.baz).toEqual(123);
});
});
describe("one-time binding", () => {
beforeEach(() => {
createInjector([
"ng",
function ($filterProvider) {
filterProvider = $filterProvider;
},
]).invoke((_$rootScope_) => {
$rootScope = _$rootScope_;
});
logs = [];
});
it("should always use the cache", () => {
expect($parse("foo")).toBe($parse("foo"));
expect($parse("::foo")).toBe($parse("::foo"));
});
it("should not affect calling the parseFn directly", async () => {
$rootScope.$watch("::foo");
$rootScope.foo = "bar";
expect($rootScope.$$watchersCount).toBe(1);
expect($rootScope.foo).toEqual("bar");
await wait();
expect($rootScope.$$watchersCount).toBe(0);
expect($rootScope.foo).toEqual("bar");
$rootScope.foo = "man";
await wait();
expect($rootScope.$$watchersCount).toBe(0);
expect($rootScope.foo).toEqual("man");
$rootScope.foo = "shell";
await wait();
expect($rootScope.$$watchersCount).toBe(0);
expect($rootScope.foo).toEqual("shell");
});
it("should stay stable once the value defined", async () => {
$rootScope.$watch("::foo", (value, old) => {
if (value !== old) logs.push(value);
});
expect(logs.length).toEqual(0);
expect($rootScope.$$watchersCount).toBe(1);
$rootScope.foo = "bar";
await wait();
expect($rootScope.$$watchersCount).toBe(0);
expect(logs[0]).toEqual("bar");
$rootScope.foo = "man";
await wait();
expect($rootScope.$$watchersCount).toBe(0);
expect(logs.length).toEqual(1);
});
it("should have a stable value if at the end of a $digest it has a defined value", async () => {
$rootScope.$watch("::foo", (value, old) => {
if (value !== old) logs.push(value);
});
$rootScope.$watch("foo", () => {
if ($rootScope.foo === "bar") {
$rootScope.foo = undefined;
}
});
$rootScope.foo = "bar";
await wait();
expect($rootScope.$$watchersCount).toBe(1);
expect(logs[0]).toEqual("bar");
$rootScope.foo = "man";
await wait();
expect($rootScope.$$watchersCount).toBe(1);
expect(logs[0]).toEqual("bar");
$rootScope.foo = "shell";
await wait();
expect($rootScope.$$watchersCount).toBe(1);
expect(logs.length).toEqual(1);
});
it("should not throw if the stable value is `null`", async () => {
$rootScope.$watch("::foo");
$rootScope.foo = null;
await wait();
$rootScope.foo = "foo";
await wait();
expect($rootScope.foo).toEqual("foo");
});
xit("should invoke a stateless filter", async () => {
const countFilter = jasmine.createSpy();
countFilter.and.callThrough();
createInjector([
"ng",
function ($filterProvider) {
$filterProvider.register("count", valueFn(countFilter));
},
]).invoke((_$rootScope_, _$parse_) => {
scope = _$rootScope_;
$parse = _$parse_;
});
scope.count = 0;
scope.foo = function () {
scope.count++;
return scope.count;
};
// todo investiage our function stategy
scope.$watch("::foo() | count");
await wait();
expect(countFilter.calls.count()).toBe(1);
});
});
describe("literal expressions", () => {
it("should mark an empty expressions as literal", () => {
expect($parse("").literal).toBe(true);
expect($parse(" ").literal).toBe(true);
expect($parse("::").literal).toBe(true);
expect($parse(":: ").literal).toBe(true);
});
[true, false].forEach((isDeep) => {
describe(isDeep ? "deepWatch" : "watch", () => {
beforeEach(() => {
logs = [];
});
fit("should only become stable when all the properties of an object have defined values", async () => {
$rootScope.$watch("::{foo: foo, bar: bar}", (value) => {
logs.push(value);
});
expect(logs).toEqual([]);
expect($rootScope.$$watchersCount).toBe(1);
// $rootScope.$digest();
// expect($rootScope.$$watchersCount).toBe(1);
// expect(logs[0]).toEqual({ foo: undefined, bar: undefined });
// $rootScope.foo = "foo";
// await wait();
// $rootScope.$digest();
// expect($rootScope.$$watchersCount).toBe(1);
//expect(logs[0]).toEqual({ foo: undefined, bar: undefined });
// $rootScope.foo = "foobar";
// $rootScope.bar = "bar";
// $rootScope.$digest();
// expect($rootScope.$$watchersCount).toBe(0);
// expect(logs[2]).toEqual({ foo: "foobar", bar: "bar" });
// $rootScope.foo = "baz";
// $rootScope.$digest();
// expect($rootScope.$$watchersCount).toBe(0);
// expect(logs[3]).toBeUndefined();
});
it("should only become stable when all the elements of an array have defined values", () => {
const fn = $parse("::[foo,bar]");
$rootScope.$watch(
fn,
(value) => {
logs.push(value);
},
isDeep,
);
expect(logs.length).toEqual(0);
expect($rootScope.$$watchersCount).toBe(1);
$rootScope.$digest();
expect($rootScope.$$watchersCount).toBe(1);
expect(logs[0]).toEqual([undefined, undefined]);
$rootScope.foo = "foo";
$rootScope.$digest();
expect($rootScope.$$watchersCount).toBe(1);
expect(logs[1]).toEqual(["foo", undefined]);
$rootScope.foo = "foobar";
$rootScope.bar = "bar";
$rootScope.$digest();
expect($rootScope.$$watchersCount).toBe(0);
expect(logs[2]).toEqual(["foobar", "bar"]);
$rootScope.foo = "baz";
$rootScope.$digest();
expect($rootScope.$$watchersCount).toBe(0);
expect(logs[3]).toBeUndefined();
});
it("should only become stable when all the elements of an array have defined values at the end of a $digest", () => {
const fn = $parse("::[foo]");
$rootScope.$watch(
fn,
(value) => {
logs.push(value);
},
isDeep,
);
$rootScope.$watch("foo", () => {
if ($rootScope.foo === "bar") {
$rootScope.foo = undefined;
}
});
$rootScope.foo = "bar";
$rootScope.$digest();
expect($rootScope.$$watchersCount).toBe(2);
expect(logs[0]).toEqual(["bar"]);
expect(logs[1]).toEqual([undefined]);
$rootScope.foo = "baz";
$rootScope.$digest();
expect($rootScope.$$watchersCount).toBe(1);
expect(logs[2]).toEqual(["baz"]);
$rootScope.bar = "qux";
$rootScope.$digest();
expect($rootScope.$$watchersCount).toBe(1);
expect(logs[3]).toBeUndefined();
});
});
});
});
describe("watched $parse expressions", () => {
beforeEach(() => {
createInjector(["ng"]).invoke((_$rootScope_) => {
scope = _$rootScope_;
});
});
it("should respect short-circuiting AND if it could have side effects", () => {
let 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", () => {
let 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", () => {
let 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", () => {
beforeEach(() => {
createInjector([
"ng",
function ($filterProvider) {
filterProvider = $filterProvider;
},
]).invoke((_$rootScope_, _$parse_) => {
scope = _$rootScope_;
$parse = _$parse_;
});
logs = [];
});
it("should not be invoked unless the input/arguments change", () => {
let filterCalled = false;
filterProvider.register(
"foo",
valueFn((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", () => {
const filterCalls = [];
filterProvider.register(
"foo",
valueFn((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)", () => {
const filterCalls = [];
filterProvider.register(
"foo",
valueFn((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", () => {
let filterCalled = false;
filterProvider.register(
"foo",
valueFn(
extend(
(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", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
return input;
}),
);
const parsed = $parse("{x: 1} | foo:1");
expect(parsed.constant).toBe(true);
let watcherCalls = 0;
scope.$watch(parsed, (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", () => {
const watchCalls = [];
scope.$watch("[a]", (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)", () => {
const watchCalls = [];
scope.$watch("::[a, undefined]", (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", () => {
beforeEach(() => {
createInjector([
"ng",
function ($filterProvider) {
filterProvider = $filterProvider;
},
]).invoke((_$rootScope_, _$parse_) => {
scope = _$rootScope_;
$parse = _$parse_;
});
logs = [];
});
describe("that does NOT support valueOf()", () => {
it("should always be reevaluated", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
return input;
}),
);
const parsed = $parse("obj | foo");
const obj = (scope.obj = {});
let watcherCalls = 0;
scope.$watch(parsed, (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", () => {
filterProvider.register(
"foo",
valueFn((input) => input.b > 0),
);
scope.$watch("[(a | foo)]", () => {});
// Would be great if filter-output was checked for changes and this didn't throw...
expect(() => {
scope.$apply("a = {b: 1}");
}).toThrowError(/infdig/);
});
it("should always be reevaluated when passed literals", () => {
scope.$watch("[a] | filter", () => {});
scope.$apply("a = 1");
// Would be great if filter-output was checked for changes and this didn't throw...
expect(() => {
scope.$apply("a = {}");
}).toThrowError(/infdig/);
});
});
describe("that does support valueOf()", () => {
it("should not be reevaluated", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
expect(input instanceof Date).toBe(true);
return input;
}),
);
const parsed = $parse("date | foo:a");
const date = (scope.date = new Date());
let watcherCalls = 0;
scope.$watch(parsed, (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", async () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
return input;
}),
);
let watcherCalls = 0;
scope.$watch("[(date | foo)]", (input) => {
watcherCalls++;
});
scope.date = new Date(1234567890123);
await wait();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
scope.date = new Date(1234567890124);
await wait();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
});
it("should be reevaluated when valueOf() changes", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
expect(input instanceof Date).toBe(true);
return input;
}),
);
const parsed = $parse("date | foo:a");
const date = (scope.date = new Date());
let watcherCalls = 0;
scope.$watch(parsed, (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", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
return input;
}),
);
scope.date = new Date(1234567890123);
let watcherCalls = 0;
scope.$watch("[(date | foo)]", (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", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
return input;
}),
);
scope.date = new Date(1234567890123);
let watcherCalls = 0;
scope.$watch($parse("[(date | foo)]"), (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", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
return input;
}),
);
scope.obj = {};
let watcherCalls = 0;
scope.$watch("!obj | foo:!obj", (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", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
return input;
}),
);
scope.obj = {};
let watcherCalls = 0;
scope.$watch("1 - obj | foo:(1 * obj)", (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", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
return input;
}),
);
scope.obj = {};
let watcherCalls = 0;
scope.$watch("1 + obj | foo", (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", () => {
let toStringCalls = 0;
scope.obj = {};
scope.key = {
toString() {
toStringCalls++;
return "foo";
},
};
let watcherCalls = 0;
scope.$watch("obj[key]", (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", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
return input;
}),
);
const parsed = $parse("obj | foo");
const obj = (scope.obj = Object.create(null));
let watcherCalls = 0;
scope.$watch(parsed, (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", () => {
beforeEach(() => {
createInjector([
"ng",
function ($filterProvider) {
filterProvider = $filterProvider;
},
]).invoke((_$rootScope_, _$parse_) => {
scope = _$rootScope_;
$parse = _$parse_;
});
logs = [];
});
it("should not be reevaluated when passed literals", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
return input;
}),
);
let watcherCalls = 0;
scope.$watch("[a] | foo", (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", () => {
let filterCalls = 0;
filterProvider.register(
"foo",
valueFn((input) => {
filterCalls++;
return input;
}),
);
scope.prim = 1234567890123;
let watcherCalls = 0;
scope.$watch("[(prim | foo)]", (input) => {
watcherCalls++;
});
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
});
});
describe("interceptorFns", () => {
beforeEach(() => {
createInjector([
"ng",
function ($filterProvider) {
filterProvider = $filterProvider;
},
]).invoke((_$rootScope_, _$parse_) => {
scope = _$rootScope_;
$parse = _$parse_;
});
logs = [];
});
it("should only be passed the intercepted value", () => {
let 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", () => {
let 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", () => {
let args1;
function int1(v) {
args1 = sliceArgs(arguments);
return v + 2;
}
let 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", () => {
let args;
function interceptor(v) {
args = sliceArgs(arguments);
return v + 4;
}
const 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", () => {
let args1;
function int1(v) {
args1 = sliceArgs(arguments);
return v + 4;
}
let args2;
function int2(v) {
args2 = sliceArgs(arguments);
return v + 8;
}
const 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", () => {
let 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", () => {
let 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", () => {
filterProvider.register(
"identity",
valueFn((x) => x),
);
let 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", () => {
let 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", () => {
let 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", () => {
let 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", () => {
let 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", () => {
let 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`", () => {
let 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", () => {
scope.$watch($parse("::x"));
scope.$watch($parse("::x", (x) => x));
scope.$watch($parse("::x", () => 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", () => {
scope.$watch($parse("::[x]"));
scope.$watch($parse("::[x]", (x) => x));
scope.$watch($parse("::[x]", () => 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", () => {
scope.$watch(
$parse("::{x:x, y:y}", (lit) => lit.x),
(val) => logs.push(val),
);
scope.$apply();
expect(logs[0]).toBeUndefined();
scope.$apply("x = 1");
expect(logs[1]).toEqual(1);
scope.$apply("x = 2; y=1");
expect(logs[2]).toEqual(2);
scope.$apply("x = 1; y=2");
expect(logs[3]).toBeUndefined();
});
it("should watch the intercepted value of one-time bindings in nested interceptors", () => {
scope.$watch(
$parse(
$parse("::{x:x, y:y}", (lit) => lit.x),
(x) => x,
),
(val) => logs.push(val),
);
scope.$apply();
expect(logs[0]).toBeUndefined();
scope.$apply("x = 1");
expect(logs[1]).toEqual(1);
scope.$apply("x = 2; y=1");
expect(logs[2]).toEqual(2);
expect(logs[3]).toBeUndefined();
});
it("should nest interceptors around eachother, not around the intercepted", () => {
function origin() {
return 0;
}
let 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", () => {
function getter(s) {
return s.x;
}
getter.$$watchDelegate = getter;
function doubler(v) {
return 2 * v;
}
let 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", () => {
beforeEach(() => {
createInjector([
"ng",
function ($filterProvider) {
filterProvider = $filterProvider;
},
]).invoke((_$rootScope_, _$parse_) => {
scope = _$rootScope_;
$parse = _$parse_;
});
logs = [];
});
it("should support watching", () => {
let lastVal = NaN;
let callCount = 0;
const 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", () => {
let lastVal = NaN;
let callCount = 0;
const 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", () => {
let lastVal = NaN;
let callCount = 0;
const 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", () => {
beforeEach(() => {
createInjector([
"ng",
function ($filterProvider) {
filterProvider = $filterProvider;
},
]).invoke((_$rootScope_, _$parse_) => {
scope = _$rootScope_;
$parse = _$parse_;
});
logs = [];
});
describe("that does NOT support valueOf()", () => {
it("should not be reevaluated", () => {
const obj = (scope.obj = {});
const parsed = $parse("[obj]");
let watcherCalls = 0;
scope.$watch(parsed, (input) => {
expect(input[0]).toBe(obj);
watcherCalls++;
});
scope.$digest();
expect(watcherCalls).toBe(1);
scope.$digest();
expect(watcherCalls).toBe(1);
});
});
describe("that does support valueOf()", () => {
it("should not be reevaluated", () => {
const date = (scope.date = new Date());
const parsed = $parse("[date]");
let watcherCalls = 0;
scope.$watch(parsed, (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", () => {
const date = (scope.date = new Date());
const parsed = $parse("[date]");
let watcherCalls = 0;
scope.$watch(parsed, (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", () => {
scope.date = new Date(1234567890123);
const parsed = $parse("[date]");
let watcherCalls = 0;
scope.$watch(parsed, (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", () => {
scope.date = new Date(1234567890123);
const parsed = $parse("[date]");
let watcherCalls = 0;
scope.$watch(parsed, (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", () => {
let value = "foo";
const spy = jasmine.createSpy();
spy.and.callFake(() => value);
scope.foo = spy;
scope.$watch("foo()");
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", () => {
let lastVal = NaN;
const 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", () => {
let lastVal = NaN;
const listener = function (val) {
lastVal = val;
};
const objA = {};
const objB = {};
scope.$watch("curObj.value = input", () => {});
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", () => {
let count = 0;
let lastValue;
scope.$watch("{[a]: true}", (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", () => {
let count = 0;
let lastValue;
scope.$watch("{[a]: true}", (val) => {
count++;
lastValue = val;
});
scope.a = {
toString() {
return this.b;
},
};
scope.a.b = 1;
// TODO: would be great if it didn't throw!
expect(() => {
scope.$apply();
}).toThrowError(/infdig/);
expect(lastValue).toEqual({ 1: true });
expect(() => {
scope.$apply("a.b = 2");
}).toThrowError(/infdig/);
expect(lastValue).toEqual({ 2: true });
});
describe("locals", () => {
it("should expose local variables", () => {
expect($parse("a")({ a: 0 }, { a: 1 })).toEqual(1);
expect(
$parse("add(a,b)")(
{
b: 1,
add(a, b) {
return a + b;
},
},
{ a: 2 },
),
).toEqual(3);
});
it("should expose traverse locals", () => {
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", () => {
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", () => {
const s = {};
const 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", () => {
let 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 } } });
});
[0, false, "", NaN].forEach((falsyValue) => {
"should not overwrite $prop scope properties when assigning",
() => {
let 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(() => {$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", () => {
it("should mark scalar value expressions as literal", () => {
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", () => {
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", () => {
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", () => {
expect($parse("1 + 1").literal).toBe(false);
expect($parse("call()").literal).toBe(false);
expect($parse("[].length").literal).toBe(false);
});
});
describe("constant", () => {
it("should mark an empty expressions as constant", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
// simpleGetterFn1
it("should return null for `a` where `a` is null", () => {
$rootScope.a = null;
expect($rootScope.$eval("a")).toBe(null);
});
it("should return undefined for `a` where `a` is undefined", () => {
expect($rootScope.$eval("a")).toBeUndefined();
});
// simpleGetterFn2
it("should return undefined for properties of `null` constant", () => {
expect($rootScope.$eval("null.a")).toBeUndefined();
});
it("should return undefined for properties of `null` values", () => {
$rootScope.a = null;
expect($rootScope.$eval("a.b")).toBeUndefined();
});
it("should return null for `a.b` where `b` is null", () => {
$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", () => {
$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", () => {
$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", () => {
$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", () => {
$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", () => {
$rootScope.fn = function () {};
expect($rootScope.$eval("fn()")).toBeUndefined();
});
it("should ignore undefined values when doing addition/concatenation", () => {
$rootScope.fn = function () {};
expect($rootScope.$eval('foo + "bar" + fn()')).toBe("bar");
});
it("should treat properties named null/undefined as normal properties", () => {
expect(
$rootScope.$eval("a.null.undefined.b", {
a: { null: { undefined: { b: 1 } } },
}),
).toBe(1);
});
it("should not allow overriding null/undefined keywords", () => {
expect($rootScope.$eval("null.a", { null: { a: 42 } })).toBeUndefined();
});
it("should allow accessing null/undefined properties on `this`", () => {
$rootScope.null = { a: 42 };
expect($rootScope.$eval("this.null.a")).toBe(42);
});
it("should allow accessing $locals", () => {
$rootScope.foo = "foo";
$rootScope.bar = "bar";
$rootScope.$locals = "foo";
const locals = { foo: 42 };
expect($rootScope.$eval("$locals")).toBeUndefined();
expect($rootScope.$eval("$locals.foo")).toBeUndefined();
expect($rootScope.$eval("this.$locals")).toBe("foo");
expect(() => {
$rootScope.$eval("$locals = {}");
}).toThrow();
expect(() => {
$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(() => {
$rootScope.$eval("$locals = {}", locals);
}).toThrow();
expect($rootScope.$eval("$locals.bar = 23", locals)).toEqual(23);
expect(locals.bar).toBe(23);
});
});
[true, false].forEach((cspEnabled) => {
describe(`custom identifiers (csp: ${cspEnabled})`, () => {
const isIdentifierStartRe = /[#a-z]/;
const isIdentifierContinueRe = /[-a-z]/;
let isIdentifierStartFn;
let isIdentifierContinueFn;
let scope;
beforeEach(() => {
createInjector([
"ng",
function ($parseProvider) {
isIdentifierStartFn = jasmine
.createSpy("isIdentifierStart")
.and.callFake((ch, cp) => isIdentifierStartRe.test(ch));
isIdentifierContinueFn = jasmine
.createSpy("isIdentifierContinue")
.and.callFake((ch, cp) => isIdentifierContinueRe.test(ch));
$parseProvider.setIdentifierFns(
isIdentifierStartFn,
isIdentifierContinueFn,
);
csp().noUnsafeEval = cspEnabled;
},
]).invoke((_$rootScope_) => {
scope = _$rootScope_;
});
});
it("should allow specifying a custom `isIdentifierStart/Continue` functions", () => {
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(() => {
scope.$eval("##");
}).toThrow();
expect(() => {
scope.$eval("x.##");
}).toThrow();
expect(() => {
scope.$eval("--");
}).toThrow();
expect(() => {
scope.$eval("x.--");
}).toThrow();
});
it("should pass the character and codepoint to the custom functions", () => {
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)],
]);
});
});
});
});
});
© 2015 - 2025 Weber Informatics LLC | Privacy Policy