Files
rust_browser/tests/js_tests.rs
Zachary D. Rowitsch 75bc30bb8e Add JavaScript arrow function (=>) support
Implement parsing, evaluation, lexical this capture, and new rejection
for arrow functions. Covers expression bodies, block bodies, zero/single/
multi-param forms, and backtracking disambiguation from grouping parens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 22:12:47 -05:00

585 lines
16 KiB
Rust

use js::{CaptureSink, JsEngine, JsValue};
fn engine_with_capture() -> JsEngine {
let sink = CaptureSink::new();
let mut engine = JsEngine::with_output(Box::new(sink));
engine.prime_runtime().unwrap();
engine
}
fn engine() -> JsEngine {
let mut engine = JsEngine::new();
engine.prime_runtime().unwrap();
engine
}
#[test]
fn determinism_100_runs() {
let script = r#"
var x = 0;
function inc(n) { return n + 1; }
x = inc(x);
x = inc(x);
x = inc(x);
x;
"#;
let mut results = Vec::new();
for _ in 0..100 {
let mut eng = engine();
let val = eng.execute(script).unwrap();
results.push(format!("{val}"));
}
let first = &results[0];
assert!(
results.iter().all(|r| r == first),
"non-deterministic: got different results across 100 runs"
);
assert_eq!(first, "3");
}
#[test]
fn multi_script_state_persistence() {
let mut eng = engine();
eng.execute("var counter = 0;").unwrap();
eng.execute("counter = counter + 1;").unwrap();
eng.execute("counter = counter + 1;").unwrap();
let val = eng.execute("counter;").unwrap();
assert!(matches!(val, JsValue::Number(n) if n == 2.0));
}
#[test]
fn error_recovery_after_failure() {
let mut eng = engine();
// First execution fails (undefined variable)
let err = eng.execute("nonexistent;");
assert!(err.is_err());
assert!(err.unwrap_err().is_reference_error());
// Re-prime and try again
eng.prime_runtime().unwrap();
let val = eng.execute("42;").unwrap();
assert!(matches!(val, JsValue::Number(n) if n == 42.0));
}
#[test]
fn large_script_many_statements() {
let mut eng = engine();
let mut script = String::from("var total = 0;\n");
for i in 1..=50 {
script.push_str(&format!("total = total + {i};\n"));
}
script.push_str("total;\n");
let val = eng.execute(&script).unwrap();
// Sum of 1..=50 = 1275
assert!(matches!(val, JsValue::Number(n) if n == 1275.0));
}
#[test]
fn parse_error_classification() {
let mut eng = engine();
let err = eng.execute("var ;").unwrap_err();
assert!(err.is_parse_error());
assert!(!err.is_reference_error());
assert!(!err.is_type_error());
assert!(!err.is_runtime_error());
}
#[test]
fn runtime_error_classification() {
let mut eng = engine();
let err = eng.execute("missing_var;").unwrap_err();
assert!(err.is_reference_error());
assert!(!err.is_parse_error());
}
#[test]
fn console_log_output_captured() {
let mut eng = engine_with_capture();
eng.execute("console.log(\"hello\");").unwrap();
eng.execute("console.log(1 + 2);").unwrap();
eng.execute("console.log(\"a\", \"b\", \"c\");").unwrap();
// We can't directly inspect the sink from here without downcasting,
// but the test verifies no panics/errors occur during console.log execution.
}
#[test]
fn function_hoisting_works() {
let mut eng = engine();
let val = eng
.execute("var r = greet(); function greet() { return \"hi\"; } r;")
.unwrap();
assert!(matches!(val, JsValue::String(ref s) if s == "hi"));
}
#[test]
fn nested_function_calls() {
let mut eng = engine();
let val = eng
.execute(
r#"
function add(a, b) { return a + b; }
function mul(a, b) { return a * b; }
mul(add(2, 3), add(4, 1));
"#,
)
.unwrap();
assert!(matches!(val, JsValue::Number(n) if n == 25.0));
}
#[test]
fn empty_and_comment_scripts() {
let mut eng = engine();
let val = eng.execute("").unwrap();
assert!(matches!(val, JsValue::Undefined));
let val = eng.execute("// just a comment").unwrap();
assert!(matches!(val, JsValue::Undefined));
let val = eng.execute("/* block comment */").unwrap();
assert!(matches!(val, JsValue::Undefined));
}
// --- Recursive call hits statement limit, not stack overflow ---
#[test]
fn finite_recursion_executes_correctly() {
// A bounded recursion that terminates well within the statement limit
// must return the correct value.
let script = r#"
function countdown(n) {
if (n === 0) { return 0; }
return countdown(n - 1);
}
countdown(50);
"#;
let mut eng = engine();
let val = eng.execute(script).unwrap();
assert!(matches!(val, JsValue::Number(n) if n == 0.0));
}
#[test]
fn recursive_function_hits_statement_limit_not_stack_overflow() {
// An infinitely recursive function should be stopped by the statement
// counter (producing a RuntimeError::Generic), never by a Rust stack
// overflow. The unit test in js_vm::interpreter::tests covers the case
// where the limit is small enough (50) to fire before the stack runs out.
// With the default limit (100,000), debug-mode stack frames are large
// enough to exhaust the default 8 MiB stack, so run on a bigger thread.
std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(|| {
let mut eng = engine();
let err = eng
.execute("function inf(n) { return inf(n + 1); } inf(0);")
.unwrap_err();
assert!(
err.is_runtime_error(),
"expected RuntimeError (statement limit) for infinite recursion, got: {err}"
);
})
.unwrap()
.join()
.unwrap();
}
// --- typeof on undeclared variable returns "undefined" (spec-correct) ---
#[test]
fn typeof_undeclared_returns_undefined() {
let mut eng = engine();
let val = eng.execute("typeof totallyMissingVariable;").unwrap();
assert!(
matches!(val, JsValue::String(ref s) if s == "undefined"),
"expected \"undefined\" for typeof on undeclared var, got: {val}"
);
}
// --- var hoisting across un-taken if branches (end-to-end) ---
#[test]
fn var_hoisting_in_untaken_if_branch() {
let mut eng = engine();
// var declared inside a never-executed if block must be hoisted to the
// enclosing scope per the JS spec.
let val = eng.execute("if (false) { var x = 1; } x;").unwrap();
assert!(
matches!(val, JsValue::Undefined),
"expected undefined (hoisted var x), got: {val}"
);
}
#[test]
fn var_hoisting_in_else_branch_when_if_taken() {
let mut eng = engine();
// The else branch is not taken, so `b` is hoisted but never initialised.
let val = eng
.execute("if (true) { var a = 1; } else { var b = 2; } b;")
.unwrap();
assert!(
matches!(val, JsValue::Undefined),
"expected undefined (b hoisted from else), got: {val}"
);
}
// --- Non-ASCII string literals round-trip through parse → execute ---
#[test]
fn non_ascii_string_executes_correctly() {
let mut eng = engine();
let val = eng.execute("\"café\";").unwrap();
assert!(
matches!(val, JsValue::String(ref s) if s == "café"),
"expected café, got: {val}"
);
}
#[test]
fn cjk_string_executes_correctly() {
let mut eng = engine();
let val = eng.execute("\"日本語\";").unwrap();
assert!(
matches!(val, JsValue::String(ref s) if s == "日本語"),
"expected 日本語, got: {val}"
);
}
// --- Chained member access returns TypeError (not crash) ---
#[test]
fn chained_member_access_returns_type_error() {
// a.b.c — accessing a non-console member should produce a TypeError, not panic.
let mut eng = engine();
let err = eng.execute("var a = 1; a.b;").unwrap_err();
assert!(
err.is_type_error(),
"expected TypeError for member access on number, got: {err}"
);
}
// --- Empty function body ---
#[test]
fn empty_function_returns_undefined() {
let mut eng = engine();
let val = eng.execute("function noop() {} noop();").unwrap();
assert!(matches!(val, JsValue::Undefined));
}
// --- Function with fewer args than params ---
#[test]
fn fewer_args_than_params_extra_are_undefined() {
let mut eng = engine();
let val = eng
.execute("function f(a, b, c) { return c; } f(1, 2);")
.unwrap();
assert!(
matches!(val, JsValue::Undefined),
"expected undefined for missing third arg, got: {val}"
);
}
// --- Function with more args than params ---
#[test]
fn more_args_than_params_extras_ignored() {
let mut eng = engine();
let val = eng
.execute("function f(a) { return a; } f(42, 99, 100);")
.unwrap();
assert!(
matches!(val, JsValue::Number(n) if n == 42.0),
"expected 42, got: {val}"
);
}
// --- Multiple return paths ---
#[test]
fn multiple_return_paths_execute_correctly() {
let script = r#"
function classify(n) {
if (n > 0) { return "positive"; }
else { return "non-positive"; }
}
classify(5);
"#;
let mut eng = engine();
let val = eng.execute(script).unwrap();
assert!(matches!(val, JsValue::String(ref s) if s == "positive"));
let val = eng.execute("classify(-1);").unwrap();
assert!(matches!(val, JsValue::String(ref s) if s == "non-positive"));
}
// --- Assignment to undeclared variable ---
#[test]
fn assignment_to_undeclared_variable_is_reference_error() {
let mut eng = engine();
let err = eng.execute("undeclaredVar = 42;").unwrap_err();
assert!(
err.is_reference_error(),
"expected ReferenceError for assignment to undeclared variable, got: {err}"
);
}
// --- Nested blocks with mixed var/let scoping ---
#[test]
fn nested_var_let_mixed_scoping() {
let script = r#"
var outer = 1;
{
var inner_var = 2;
let inner_let = 3;
outer = outer + inner_var + inner_let;
}
outer;
"#;
let mut eng = engine();
let val = eng.execute(script).unwrap();
assert!(matches!(val, JsValue::Number(n) if n == 6.0));
}
#[test]
fn let_not_visible_outside_block_integration() {
let mut eng = engine();
let err = eng
.execute("{ let block_only = 5; } block_only;")
.unwrap_err();
assert!(
err.is_reference_error(),
"expected ReferenceError for let outside its block, got: {err}"
);
}
// --- Function defined in script 1, called in script 2 ---
#[test]
fn function_defined_in_script1_called_in_script2() {
let mut eng = engine();
eng.execute("function greetName(name) { return \"hello_\" + name; }")
.unwrap();
let val = eng.execute("greetName(\"world\");").unwrap();
assert!(
matches!(val, JsValue::String(ref s) if s == "hello_world"),
"expected hello_world, got: {val}"
);
}
// --- Parse error in script 1 doesn't prevent script 2 from running ---
#[test]
fn parse_error_does_not_block_subsequent_script() {
let mut eng = engine();
// First script has a parse error
let err = eng.execute("var ;");
assert!(err.is_err());
// Second script should work (VM never entered Failed state for parse errors)
let val = eng.execute("42;").unwrap();
assert!(matches!(val, JsValue::Number(n) if n == 42.0));
}
// --- Deep function call nesting (10+ levels, under 64 limit) ---
#[test]
fn deep_call_nesting_under_limit() {
let mut eng = engine();
let val = eng
.execute(
r#"
function a(n) { if (n === 0) { return "done"; } return b(n - 1); }
function b(n) { return a(n); }
a(20);
"#,
)
.unwrap();
assert!(
matches!(val, JsValue::String(ref s) if s == "done"),
"expected done, got: {val}"
);
}
// --- typeof in binary expression comparison ---
#[test]
fn typeof_in_equality_comparison() {
let mut eng = engine();
let val = eng.execute("typeof 42 === \"number\";").unwrap();
assert!(
matches!(val, JsValue::Boolean(true)),
"expected true for typeof 42 === \"number\", got: {val}"
);
let val = eng.execute("typeof \"hello\" === \"string\";").unwrap();
assert!(
matches!(val, JsValue::Boolean(true)),
"expected true for typeof \"hello\" === \"string\", got: {val}"
);
}
// --- Multiple console.log calls with varied types ---
#[test]
fn switch_basic_case_match() {
let mut eng = engine();
let val = eng
.execute("var r; switch(2) { case 1: r = 'one'; break; case 2: r = 'two'; break; } r;")
.unwrap();
assert_eq!(val.to_string(), "two");
}
#[test]
fn switch_default_case() {
let mut eng = engine();
let val = eng
.execute("var r; switch(99) { case 1: r = 'one'; break; default: r = 'other'; break; } r;")
.unwrap();
assert_eq!(val.to_string(), "other");
}
#[test]
fn switch_fall_through_behavior() {
let mut eng = engine();
let val = eng
.execute("var r = ''; switch(1) { case 1: r = r + 'a'; case 2: r = r + 'b'; } r;")
.unwrap();
assert_eq!(val.to_string(), "ab");
}
#[test]
fn switch_return_from_function() {
let mut eng = engine();
let val = eng
.execute(
"function f(x) { switch(x) { case 1: return 'one'; default: return 'other'; } } f(1);",
)
.unwrap();
assert_eq!(val.to_string(), "one");
}
#[test]
fn switch_no_match_no_default_is_noop() {
let mut eng = engine();
let val = eng
.execute("var r = 'init'; switch(99) { case 1: r = 'one'; break; } r;")
.unwrap();
assert_eq!(val.to_string(), "init");
}
#[test]
fn console_log_varied_types_no_panic() {
// Verify console.log does not panic or error on any JsValue type.
let mut eng = engine();
eng.execute("console.log(undefined);").unwrap();
eng.execute("console.log(null);").unwrap();
eng.execute("console.log(true);").unwrap();
eng.execute("console.log(false);").unwrap();
eng.execute("console.log(0);").unwrap();
eng.execute("console.log(\"\");").unwrap();
eng.execute("console.log(\"hello\", \"world\");").unwrap();
eng.execute("console.log(1, 2, 3, 4, 5);").unwrap();
}
// --- Loop tests ---
#[test]
fn for_loop_accumulates_sum() {
let mut eng = engine();
let val = eng
.execute("var sum = 0; for (var i = 1; i <= 5; i++) { sum += i; } sum;")
.unwrap();
assert_eq!(val.to_string(), "15");
}
#[test]
fn while_loop_counts_to_five() {
let mut eng = engine();
let val = eng.execute("var n = 0; while (n < 5) { n++; } n;").unwrap();
assert_eq!(val.to_string(), "5");
}
#[test]
fn do_while_runs_at_least_once() {
let mut eng = engine();
let val = eng
.execute("var x = 0; do { x++; } while (false); x;")
.unwrap();
assert_eq!(val.to_string(), "1");
}
#[test]
fn for_loop_with_break() {
let mut eng = engine();
let val = eng
.execute("var r = 0; for (var i = 0; i < 100; i++) { if (i === 7) { break; } r = i; } i;")
.unwrap();
assert_eq!(val.to_string(), "7");
}
#[test]
fn for_loop_with_continue() {
let mut eng = engine();
let val = eng
.execute("var sum = 0; for (var i = 0; i < 10; i++) { if (i % 2 === 0) { continue; } sum += i; } sum;")
.unwrap();
assert_eq!(val.to_string(), "25");
}
#[test]
fn while_loop_with_break() {
let mut eng = engine();
let val = eng
.execute("var n = 0; while (true) { n++; if (n >= 4) { break; } } n;")
.unwrap();
assert_eq!(val.to_string(), "4");
}
#[test]
fn increment_decrement_pre_post() {
let mut eng = engine();
let val = eng
.execute("var a = 1; var b = a++; var c = ++a; b;")
.unwrap();
assert_eq!(val.to_string(), "1");
let val = eng.execute("a;").unwrap();
assert_eq!(val.to_string(), "3");
let val = eng.execute("c;").unwrap();
assert_eq!(val.to_string(), "3");
}
#[test]
fn compound_assignment_operators() {
let mut eng = engine();
let val = eng.execute("var x = 10; x += 5; x;").unwrap();
assert_eq!(val.to_string(), "15");
let val = eng.execute("x -= 3; x;").unwrap();
assert_eq!(val.to_string(), "12");
let val = eng.execute("x *= 2; x;").unwrap();
assert_eq!(val.to_string(), "24");
}
#[test]
fn nested_for_loops() {
let mut eng = engine();
let val = eng
.execute("var total = 0; for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { total++; } } total;")
.unwrap();
assert_eq!(val.to_string(), "9");
}
#[test]
fn do_while_with_continue_integration() {
let mut eng = engine();
let val = eng
.execute("var sum = 0; var i = 0; do { i++; if (i === 3) { continue; } sum += i; } while (i < 5); sum;")
.unwrap();
assert_eq!(val.to_string(), "12");
}