@mariozechner/jailjs
Version:
Lightweight JavaScript interpreter for isolated execution. For plugins, user scripts, and browser extensions. Not for adversarial code - use SandboxJS or isolated-vm for that.
770 lines • 26.8 kB
JavaScript
import { describe, expect, it } from "vitest";
import { Interpreter } from "./interpreter";
import { parse } from "./parser";
// Helper to maintain existing test API
class TestInterpreter extends Interpreter {
constructor(globalEnv = {}, options = {}) {
super(globalEnv, { ...options, parse }); // Inject parser for eval() support
}
run(code) {
return this.evaluate(parse(code));
}
}
describe("Interpreter - ES5 Smoke Tests", () => {
describe("Literals", () => {
it("should handle number literals", () => {
const interp = new TestInterpreter();
expect(interp.run("42")).toBe(42);
expect(interp.run("3.14")).toBe(3.14);
expect(interp.run("0")).toBe(0);
});
it("should handle string literals", () => {
const interp = new TestInterpreter();
expect(interp.run('"hello"')).toBe("hello");
expect(interp.run("'world'")).toBe("world");
});
it("should handle boolean literals", () => {
const interp = new TestInterpreter();
expect(interp.run("true")).toBe(true);
expect(interp.run("false")).toBe(false);
});
it("should handle null", () => {
const interp = new TestInterpreter();
expect(interp.run("null")).toBe(null);
});
it("should handle undefined", () => {
const interp = new TestInterpreter();
expect(interp.run("undefined")).toBe(undefined);
});
it("should handle regex literals", () => {
const interp = new TestInterpreter();
const result = interp.run("/test/gi");
expect(result).toBeInstanceOf(RegExp);
expect(result.source).toBe("test");
expect(result.flags).toBe("gi");
});
});
describe("Variables", () => {
it("should declare and use var", () => {
const interp = new TestInterpreter();
expect(interp.run("var x = 5; x")).toBe(5);
});
it("should handle var hoisting", () => {
const interp = new TestInterpreter();
expect(interp.run("x = 10; var x; x")).toBe(10);
});
it("should handle var function scoping", () => {
const interp = new TestInterpreter();
const result = interp.run(`
function test() {
if (true) {
var x = 5;
}
return x;
}
test()
`);
expect(result).toBe(5);
});
});
describe("Arithmetic Operations", () => {
it("should handle basic arithmetic", () => {
const interp = new TestInterpreter();
expect(interp.run("2 + 3")).toBe(5);
expect(interp.run("10 - 4")).toBe(6);
expect(interp.run("3 * 7")).toBe(21);
expect(interp.run("15 / 3")).toBe(5);
expect(interp.run("10 % 3")).toBe(1);
});
it("should handle operator precedence", () => {
const interp = new TestInterpreter();
expect(interp.run("2 + 3 * 4")).toBe(14);
expect(interp.run("(2 + 3) * 4")).toBe(20);
});
it("should handle unary operators", () => {
const interp = new TestInterpreter();
expect(interp.run("-5")).toBe(-5);
expect(interp.run("+5")).toBe(5);
expect(interp.run("!true")).toBe(false);
expect(interp.run("!false")).toBe(true);
});
it("should handle increment/decrement", () => {
const interp = new TestInterpreter();
expect(interp.run("var x = 5; x++")).toBe(5);
expect(interp.run("var x = 5; ++x")).toBe(6);
expect(interp.run("var x = 5; x--")).toBe(5);
expect(interp.run("var x = 5; --x")).toBe(4);
});
});
describe("Comparison and Logical Operations", () => {
it("should handle comparison operators", () => {
const interp = new TestInterpreter();
expect(interp.run("5 > 3")).toBe(true);
expect(interp.run("5 < 3")).toBe(false);
expect(interp.run("5 >= 5")).toBe(true);
expect(interp.run("5 <= 4")).toBe(false);
expect(interp.run("5 == 5")).toBe(true);
expect(interp.run("5 === 5")).toBe(true);
expect(interp.run("5 != 4")).toBe(true);
expect(interp.run('5 !== "5"')).toBe(true);
});
it("should handle logical operators", () => {
const interp = new TestInterpreter();
expect(interp.run("true && true")).toBe(true);
expect(interp.run("true && false")).toBe(false);
expect(interp.run("true || false")).toBe(true);
expect(interp.run("false || false")).toBe(false);
});
it("should handle short-circuit evaluation", () => {
const interp = new TestInterpreter();
expect(interp.run("false && undefined.x")).toBe(false);
expect(interp.run("true || undefined.x")).toBe(true);
});
});
describe("Bitwise Operations", () => {
it("should handle bitwise operators", () => {
const interp = new TestInterpreter();
expect(interp.run("5 & 3")).toBe(1);
expect(interp.run("5 | 3")).toBe(7);
expect(interp.run("5 ^ 3")).toBe(6);
expect(interp.run("~5")).toBe(-6);
expect(interp.run("5 << 1")).toBe(10);
expect(interp.run("5 >> 1")).toBe(2);
expect(interp.run("-5 >>> 1")).toBe(2147483645);
});
});
describe("Objects", () => {
it("should create and access objects", () => {
const interp = new TestInterpreter();
expect(interp.run("var obj = {x: 5}; obj.x")).toBe(5);
expect(interp.run('var obj = {x: 5}; obj["x"]')).toBe(5);
});
it("should modify object properties", () => {
const interp = new TestInterpreter();
expect(interp.run("var obj = {x: 5}; obj.x = 10; obj.x")).toBe(10);
});
it("should handle nested objects", () => {
const interp = new TestInterpreter();
expect(interp.run("var obj = {a: {b: 5}}; obj.a.b")).toBe(5);
});
it("should handle computed properties", () => {
const interp = new TestInterpreter();
expect(interp.run('var obj = {}; var key = "x"; obj[key] = 5; obj.x')).toBe(5);
});
});
describe("Arrays", () => {
it("should create and access arrays", () => {
const interp = new TestInterpreter();
expect(interp.run("[1, 2, 3][0]")).toBe(1);
expect(interp.run("[1, 2, 3][2]")).toBe(3);
});
it("should modify array elements", () => {
const interp = new TestInterpreter();
expect(interp.run("var arr = [1, 2, 3]; arr[1] = 5; arr[1]")).toBe(5);
});
it("should handle array methods", () => {
const interp = new TestInterpreter();
expect(interp.run("[1, 2, 3].length")).toBe(3);
expect(interp.run('[1, 2, 3].join(",")')).toBe("1,2,3");
});
});
describe("Functions", () => {
it("should declare and call functions", () => {
const interp = new TestInterpreter();
expect(interp.run(`
function add(a, b) {
return a + b;
}
add(2, 3)
`)).toBe(5);
});
it("should handle function expressions", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var add = function(a, b) {
return a + b;
};
add(2, 3)
`)).toBe(5);
});
it("should handle closures", () => {
const interp = new TestInterpreter();
expect(interp.run(`
function makeCounter() {
var count = 0;
return function() {
return ++count;
};
}
var counter = makeCounter();
counter(); counter(); counter()
`)).toBe(3);
});
it("should handle recursion", () => {
const interp = new TestInterpreter();
expect(interp.run(`
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
factorial(5)
`)).toBe(120);
});
it("should handle arguments object", () => {
const interp = new TestInterpreter();
expect(interp.run(`
function sum() {
var total = 0;
for (var i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
sum(1, 2, 3, 4)
`)).toBe(10);
});
it("should handle this context", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var obj = {
value: 42,
getValue: function() {
return this.value;
}
};
obj.getValue()
`)).toBe(42);
});
});
describe("Control Flow", () => {
it("should handle if/else", () => {
const interp = new TestInterpreter();
expect(interp.run("if (true) 1; else 2;")).toBe(1);
expect(interp.run("if (false) 1; else 2;")).toBe(2);
});
it("should handle ternary operator", () => {
const interp = new TestInterpreter();
expect(interp.run("true ? 1 : 2")).toBe(1);
expect(interp.run("false ? 1 : 2")).toBe(2);
});
it("should handle while loops", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var sum = 0;
var i = 1;
while (i <= 5) {
sum += i;
i++;
}
sum
`)).toBe(15);
});
it("should handle do-while loops", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var sum = 0;
var i = 1;
do {
sum += i;
i++;
} while (i <= 5);
sum
`)).toBe(15);
});
it("should handle for loops", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var sum = 0;
for (var i = 1; i <= 5; i++) {
sum += i;
}
sum
`)).toBe(15);
});
it("should handle for-in loops", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var obj = {a: 1, b: 2, c: 3};
var keys = [];
for (var key in obj) {
keys.push(key);
}
keys.join(',')
`)).toBe("a,b,c");
});
it("should handle break", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var sum = 0;
for (var i = 1; i <= 10; i++) {
if (i > 5) break;
sum += i;
}
sum
`)).toBe(15);
});
it("should handle continue", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var sum = 0;
for (var i = 1; i <= 5; i++) {
if (i === 3) continue;
sum += i;
}
sum
`)).toBe(12);
});
it("should handle labeled statements", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var result = 0;
outer: for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
if (i === 1 && j === 1) break outer;
result++;
}
}
result
`)).toBe(4);
});
it("should handle switch statements", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var x = 2;
var result;
switch (x) {
case 1:
result = 'one';
break;
case 2:
result = 'two';
break;
default:
result = 'other';
}
result
`)).toBe("two");
});
it("should handle switch fall-through", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var x = 2;
var result = '';
switch (x) {
case 1:
result += 'a';
case 2:
result += 'b';
case 3:
result += 'c';
break;
default:
result += 'd';
}
result
`)).toBe("bc");
});
});
describe("Exception Handling", () => {
it("should handle try/catch", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var result;
try {
throw new Error('test');
} catch (e) {
result = e.message;
}
result
`)).toBe("test");
});
it("should handle try/finally", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var result = 0;
try {
result = 1;
} finally {
result = 2;
}
result
`)).toBe(2);
});
it("should handle try/catch/finally", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var result = '';
try {
result += 'a';
throw new Error('test');
result += 'b';
} catch (e) {
result += 'c';
} finally {
result += 'd';
}
result
`)).toBe("acd");
});
});
describe("Type Operations", () => {
it("should handle typeof operator", () => {
const interp = new TestInterpreter();
expect(interp.run("typeof 5")).toBe("number");
expect(interp.run('typeof "hello"')).toBe("string");
expect(interp.run("typeof true")).toBe("boolean");
expect(interp.run("typeof undefined")).toBe("undefined");
expect(interp.run("typeof {}")).toBe("object");
expect(interp.run("typeof []")).toBe("object");
expect(interp.run("typeof null")).toBe("object");
});
it("should handle instanceof operator", () => {
const interp = new TestInterpreter();
expect(interp.run("[] instanceof Array")).toBe(true);
expect(interp.run("({}) instanceof Object")).toBe(true); // Parentheses to avoid ambiguity
expect(interp.run("5 instanceof Number")).toBe(false);
});
it("should handle in operator", () => {
const interp = new TestInterpreter();
expect(interp.run('"x" in {x: 5}')).toBe(true);
expect(interp.run('"y" in {x: 5}')).toBe(false);
expect(interp.run("0 in [1, 2, 3]")).toBe(true);
});
it("should handle delete operator", () => {
const interp = new TestInterpreter();
expect(interp.run("var obj = {x: 5}; delete obj.x; obj.x")).toBe(undefined);
expect(interp.run('var obj = {x: 5}; delete obj.x; "x" in obj')).toBe(false);
});
it("should handle void operator", () => {
const interp = new TestInterpreter();
expect(interp.run("void 0")).toBe(undefined);
expect(interp.run("void 5")).toBe(undefined);
});
});
describe("Assignment Operators", () => {
it("should handle compound assignment", () => {
const interp = new TestInterpreter();
expect(interp.run("var x = 5; x += 3; x")).toBe(8);
expect(interp.run("var x = 5; x -= 3; x")).toBe(2);
expect(interp.run("var x = 5; x *= 3; x")).toBe(15);
expect(interp.run("var x = 6; x /= 3; x")).toBe(2);
expect(interp.run("var x = 5; x %= 3; x")).toBe(2);
});
it("should handle bitwise assignment", () => {
const interp = new TestInterpreter();
expect(interp.run("var x = 5; x &= 3; x")).toBe(1);
expect(interp.run("var x = 5; x |= 3; x")).toBe(7);
expect(interp.run("var x = 5; x ^= 3; x")).toBe(6);
expect(interp.run("var x = 5; x <<= 1; x")).toBe(10);
expect(interp.run("var x = 5; x >>= 1; x")).toBe(2);
});
});
describe("Comma Operator", () => {
it("should handle comma operator", () => {
const interp = new TestInterpreter();
expect(interp.run("var x = (1, 2, 3); x")).toBe(3);
expect(interp.run("var a, b; a = 1, b = 2; b")).toBe(2);
});
});
describe("new Operator", () => {
it("should handle constructor calls", () => {
const interp = new TestInterpreter();
expect(interp.run("new Date(2020, 0, 1).getFullYear()")).toBe(2020);
expect(interp.run("new Array(1, 2, 3).length")).toBe(3);
});
});
describe("Scope and Hoisting", () => {
it("should handle function declaration hoisting", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var result = test();
function test() {
return 42;
}
result
`)).toBe(42);
});
it("should handle nested scopes", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var x = 1;
function outer() {
var x = 2;
function inner() {
var x = 3;
return x;
}
return inner() + x;
}
outer() + x
`)).toBe(6);
});
});
describe("Callbacks to Native Functions", () => {
it("should pass interpreted functions to native array methods", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var arr = [1, 2, 3];
var result = arr.map(function(x) {
return x * 2;
});
result.join(',')
`)).toBe("2,4,6");
});
it("should handle filter", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var arr = [1, 2, 3, 4, 5];
var result = arr.filter(function(x) {
return x > 2;
});
result.join(',')
`)).toBe("3,4,5");
});
it("should handle reduce", () => {
const interp = new TestInterpreter();
expect(interp.run(`
var arr = [1, 2, 3, 4];
arr.reduce(function(acc, x) {
return acc + x;
}, 0)
`)).toBe(10);
});
});
describe("Security", () => {
it("should block __proto__ access", () => {
const interp = new TestInterpreter();
expect(interp.run("var obj = {}; obj.__proto__")).toBe(undefined);
});
it("should block constructor access", () => {
const interp = new TestInterpreter();
expect(interp.run("var obj = {}; obj.constructor")).toBe(undefined);
});
it("should block prototype access", () => {
const interp = new TestInterpreter();
expect(interp.run("var obj = {}; obj.prototype")).toBe(undefined);
});
it("should block Function constructor", () => {
const interp = new TestInterpreter();
expect(interp.run("Function")).toBe(undefined);
});
it("should re-interpret eval", () => {
const interp = new TestInterpreter();
expect(interp.run('eval("2 + 3")')).toBe(5);
});
it("should timeout on infinite loops", () => {
const interp = new TestInterpreter({}, { maxOps: 1000 });
expect(() => interp.run("while(true) {}")).toThrow("maximum operations exceeded");
});
});
describe("Edge Cases", () => {
it("should handle empty statements", () => {
const interp = new TestInterpreter();
expect(interp.run(";;; 42")).toBe(42);
});
it("should handle multiple statements", () => {
const interp = new TestInterpreter();
expect(interp.run("var x = 1; var y = 2; x + y")).toBe(3);
});
it("should handle empty programs", () => {
const interp = new TestInterpreter();
expect(interp.run("")).toBe(undefined);
});
it("should handle expression as final statement", () => {
const interp = new TestInterpreter();
expect(interp.run("1 + 1")).toBe(2);
});
});
describe("ES6+ Transformation Support", () => {
it("should support constructors with 'new' keyword on interpreted functions", () => {
const interp = new TestInterpreter();
const result = interp.run(`
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return 'Hello, ' + this.name;
};
var p = new Person('Alice');
p.greet();
`);
expect(result).toBe("Hello, Alice");
});
it("should support prototype property on functions", () => {
const interp = new TestInterpreter();
const result = interp.run(`
function MyClass() {}
MyClass.prototype.test = 123;
var instance = new MyClass();
instance.test;
`);
expect(result).toBe(123);
});
it("should support Babel loose mode class transformation", () => {
const interp = new TestInterpreter();
const result = interp.run(`
var Calculator = function() {
function Calculator() {}
var _proto = Calculator.prototype;
_proto.add = function add(a, b) {
return a + b;
};
return Calculator;
}();
new Calculator().add(1, 2);
`);
expect(result).toBe(3);
});
it("should support constructor with return value", () => {
const interp = new TestInterpreter();
const result = interp.run(`
function Factory() {
return { custom: true };
}
var obj = new Factory();
obj.custom;
`);
expect(result).toBe(true);
});
it("should support constructor without return value", () => {
const interp = new TestInterpreter();
const result = interp.run(`
function Person(name) {
this.name = name;
}
var p = new Person('Bob');
p.name;
`);
expect(result).toBe("Bob");
});
it("should handle method calls with correct 'this' context", () => {
const interp = new TestInterpreter();
const result = interp.run(`
function Counter() {
this.count = 0;
}
Counter.prototype.increment = function() {
this.count++;
return this.count;
};
var c = new Counter();
c.increment();
c.increment();
`);
expect(result).toBe(2);
});
it("should support callbacks to native functions (DOM-style)", () => {
// Simulate a DOM-like API that stores and calls callbacks
const mockButton = {
listeners: [],
addEventListener(event, callback) {
this.listeners.push({ event, callback });
},
click() {
this.listeners.forEach((l) => {
if (l.event === "click")
l.callback();
});
},
};
const interp = new TestInterpreter({ button: mockButton });
interp.run(`
var clicked = false;
button.addEventListener('click', function() {
clicked = true;
});
`);
mockButton.click();
const result = interp.run("clicked");
expect(result).toBe(true);
});
it("should support Symbol in default environment", () => {
const interp = new TestInterpreter();
const result = interp.run(`
typeof Symbol;
`);
expect(result).toBe("function");
});
it("should allow setting methods on prototype", () => {
const interp = new TestInterpreter();
const result = interp.run(`
function MyClass() {}
MyClass.prototype.method = function() { return 42; };
var obj = new MyClass();
obj.method();
`);
expect(result).toBe(42);
});
it("should block __proto__ access", () => {
const interp = new TestInterpreter();
const result = interp.run(`
var obj = {};
obj.__proto__;
`);
expect(result).toBe(undefined);
});
it("should support named function expressions", () => {
const interp = new TestInterpreter();
const result = interp.run(`
var factorial = function fact(n) {
return n <= 1 ? 1 : n * fact(n - 1);
};
factorial(5);
`);
expect(result).toBe(120);
});
it("should support Function.prototype.call", () => {
const interp = new TestInterpreter();
const result = interp.run(`
function greet(greeting) {
return greeting + ', ' + this.name;
}
var obj = { name: 'Alice' };
greet.call(obj, 'Hello');
`);
expect(result).toBe("Hello, Alice");
});
it("should support Function.prototype.apply", () => {
const interp = new TestInterpreter();
const result = interp.run(`
function add(a, b) {
return a + b;
}
add.apply(null, [1, 2]);
`);
expect(result).toBe(3);
});
it("should support Function.prototype.bind", () => {
const interp = new TestInterpreter();
const result = interp.run(`
function greet(greeting) {
return greeting + ', ' + this.name;
}
var obj = { name: 'Bob' };
var boundGreet = greet.bind(obj);
boundGreet('Hi');
`);
expect(result).toBe("Hi, Bob");
});
it("should support instanceof with interpreted functions", () => {
const interp = new TestInterpreter();
const result = interp.run(`
function Parent() {}
function Child() {}
Child.prototype = Object.create(Parent.prototype);
var child = new Child();
child instanceof Child && child instanceof Parent;
`);
expect(result).toBe(true);
});
it("should allow mutation of built-in objects (no protection)", () => {
const interp = new TestInterpreter();
// Built-ins are NOT frozen, can be mutated
interp.run(`Math.customProperty = 42;`);
expect(Math.customProperty).toBe(42);
// Clean up
delete Math.customProperty;
});
});
});
//# sourceMappingURL=interpreter.test.js.map