Files
OxideJS-New/crates/oxide_vm/tests/builtins_object_tests.rs
T

552 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::sync::Arc;
use oxide_compiler::compiler::Compiler;
use oxide_compiler::DefaultCompilerService;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<(Vm, JsValue), String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
let mut vm = Vm::new();
vm.set_compiler_service(Arc::new(DefaultCompilerService));
let result = vm.run(&Arc::new(module))?;
Ok((vm, result))
}
#[test]
fn object_keys_empty() {
let (_vm, result) = eval("Object.keys({})").unwrap();
assert!(result.is_object());
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 0);
}
#[test]
fn object_keys_has_own_property() {
let (_vm, result) = eval("Object.keys({a:1,b:2})").unwrap();
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 2);
}
#[test]
fn object_member_native_call_uses_regular_call_path() {
let (_vm, result) = eval("Object.keys({a:1}).length").unwrap();
assert_eq!(result.as_int(), 1);
}
#[test]
fn object_member_call_allows_user_overwrite() {
let (_vm, result) = eval("Object.keys = function() { return 9; }; Object.keys()").unwrap();
assert_eq!(result.as_int(), 9);
}
#[test]
fn missing_object_member_call_is_not_call_native_target_error() {
let err = match eval("Object.noSuchMethod()") {
Ok(_) => panic!("missing member call should fail"),
Err(err) => err,
};
assert!(err.contains("CALL target is not callable"), "unexpected error: {err}");
assert!(!err.contains("CALL_NATIVE target"), "unexpected CALL_NATIVE path: {err}");
}
#[test]
fn object_create_null_proto() {
let (_vm, _result) = eval("Object.create(null)").unwrap();
}
#[test]
fn object_assign_copies() {
let (_vm, result) = eval("Object.assign({a:1},{b:2})").unwrap();
assert!(result.is_object());
}
#[test]
fn object_assign_boxes_primitive_target() {
let (_vm, result) = eval("var result = Object.assign('a'); result.valueOf()").unwrap();
assert!(result.is_string());
let rendered = _vm.lookup_str(result).unwrap_or_default();
assert_eq!(rendered, "a");
}
#[test]
fn object_is_uses_same_value_semantics() {
let (_vm, result) = eval("Object.is(NaN, NaN)").unwrap();
assert_eq!(result, JsValue::bool(true));
let (_vm, result) = eval("Object.is(1, 1)").unwrap();
assert_eq!(result, JsValue::bool(true));
let (_vm, result) = eval("Object.is(1, '1')").unwrap();
assert_eq!(result, JsValue::bool(false));
}
#[test]
fn object_get_own_property_descriptor_value() {
let (_vm, result) = eval("Object.getOwnPropertyDescriptor({a:1},'a')").unwrap();
assert!(result.is_object());
}
#[test]
fn object_get_own_property_descriptor_missing() {
let (_vm, result) = eval("Object.getOwnPropertyDescriptor({},'x')").unwrap();
assert!(result.is_undefined());
}
#[test]
fn object_define_property_sets_value() {
let (_vm, result) = eval("Object.defineProperty({},'x',{value:42})").unwrap();
assert!(result.is_object());
}
#[test]
fn object_define_property_data_defaults_non_writable() {
let (_vm, result) =
eval("var o={}; Object.defineProperty(o,'x',{value:1}); Object.getOwnPropertyDescriptor(o,'x').writable")
.unwrap();
assert_eq!(result, JsValue::bool(false));
}
#[test]
fn object_define_property_accessor_descriptor_gets() {
let (_vm, result) = eval(
"var o={}; Object.defineProperty(o,'x',{get:function(){return 7}, enumerable:true, configurable:true}); o.x",
)
.unwrap();
assert_eq!(result, JsValue::int(7));
}
#[test]
fn object_define_property_rejects_mixed_descriptor() {
let err = match eval("var o={}; Object.defineProperty(o,'x',{value:1,get:function(){return 2}})") {
Ok(_) => panic!("expected mixed descriptor to fail"),
Err(err) => err,
};
assert!(err.contains("TypeError"));
}
#[test]
fn object_define_property_non_writable_assignment_throws_in_strict() {
let err = match eval("var o={}; Object.defineProperty(o,'x',{value:1}); (function(){'use strict'; o.x=2})()") {
Ok(_) => panic!("expected assignment to fail"),
Err(err) => err,
};
assert!(err.contains("TypeError"));
}
#[test]
fn object_get_own_property_descriptor_accessor_shape() {
let (_vm, result) =
eval("var o={}; Object.defineProperty(o,'x',{get:function(){return 1}, configurable:true}); Object.getOwnPropertyDescriptor(o,'x').value").unwrap();
assert!(result.is_undefined());
}
#[test]
fn object_get_prototype_of() {
let (_vm, result) = eval("Object.getPrototypeOf({})").unwrap();
assert!(result.is_object() || result.is_null());
}
#[test]
fn object_has_own_true() {
let (_vm, result) = eval("Object.hasOwn({a:1}, 'a')").unwrap();
assert_eq!(result, JsValue::bool(true));
}
#[test]
fn object_has_own_false_for_inherited_property() {
let (_vm, result) = eval("Object.hasOwn({}, 'toString')").unwrap();
assert_eq!(result, JsValue::bool(false));
}
#[test]
fn object_proto_has_own_property_call_works() {
let (_vm, result) = eval("Object.prototype.hasOwnProperty.call(Object, 'assign')").unwrap();
assert_eq!(result, JsValue::bool(true));
}
#[test]
fn object_proto_property_is_enumerable_call_works() {
let (_vm, result) = eval("Object.prototype.propertyIsEnumerable.call(Object, 'assign')").unwrap();
// 内置方法按 ES 规范不可枚举。
assert_eq!(result, JsValue::bool(false));
}
#[test]
fn object_entries_returns_array() {
let (_vm, result) = eval("Object.entries({a:1,b:2})").unwrap();
assert!(result.is_object());
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 2);
}
#[test]
fn object_values_returns_array() {
let (_vm, result) = eval("Object.values({a:1,b:2})").unwrap();
assert!(result.is_object());
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.prop_count(), 2);
}
// -- Object.fromEntries --
#[test]
fn from_entries_numeric_key_normalized_to_int_key() {
// 数字键 entry 走 ToPropertyKey 规范化:fromEntries 产出的 "5" 键与 o[5] 同一键。
let (_vm, result) = eval("Object.fromEntries([[5,'a']])[5]").unwrap();
assert!(result.is_string() && unsafe { &*result.as_string_ptr() }.as_str() == "a");
}
#[test]
fn from_entries_string_index_key_reads_via_numeric_access() {
// 字符串 "5" 键是规范数组下标,读 o[5] 命中同一整数键。
let (_vm, result) = eval("Object.fromEntries([['5','a']])[5]").unwrap();
assert!(result.is_string() && unsafe { &*result.as_string_ptr() }.as_str() == "a");
}
#[test]
fn from_entries_numeric_key_write_keeps_single_key() {
// 再写 o[5] 不产生第二个 "5" 键(键唯一)。
let (_vm, result) =
eval("var o = Object.fromEntries([[5,'a']]); o[5]='x'; Object.getOwnPropertyNames(o).join(',')").unwrap();
assert!(result.is_string() && unsafe { &*result.as_string_ptr() }.as_str() == "5");
}
#[test]
fn from_entries_multiple_pairs_roundtrip() {
let (_vm, result) = eval("var o = Object.fromEntries([[1,'a'],[2,'b']]); o[1] + o[2]").unwrap();
assert!(result.is_string() && unsafe { &*result.as_string_ptr() }.as_str() == "ab");
}
// -- Object.keys/values/entries 数组元素区 --
#[test]
fn keys_array_includes_integer_elements() {
let (_vm, result) = eval("Object.keys([1,2]).join(',')").unwrap();
let s = unsafe { &*result.as_string_ptr() }.to_owned_string();
assert_eq!(s, "0,1");
}
#[test]
fn keys_array_skips_holes() {
let (_vm, result) = eval("Object.keys([1,,3]).join(',')").unwrap();
let s = unsafe { &*result.as_string_ptr() }.to_owned_string();
assert_eq!(s, "0,2");
}
#[test]
fn entries_array_reads_element_values() {
let (_vm, result) = eval("var e = Object.entries([1,2]); e[1][1]").unwrap();
assert!(
result.is_int() && result.as_int() == 2,
"second entry value should be 2, got {:?}",
result
);
}
#[test]
fn keys_array_mixed_named_prop_order() {
// 整数下标升序在前,命名属性按插入序随后(无重复键)。
let (_vm, result) = eval("var a = [10]; a.foo = 1; Object.keys(a).join(',')").unwrap();
let s = unsafe { &*result.as_string_ptr() }.to_owned_string();
assert_eq!(s, "0,foo");
}
#[test]
fn keys_array_non_enumerable_element_filtered() {
// defineProperty 把元素改为不可枚举后 keys 应省略(values/entries 同理)。
let (_vm, result) =
eval("var a = [1, 2]; Object.defineProperty(a, '1', {enumerable: false}); Object.keys(a).join(',')").unwrap();
let s = unsafe { &*result.as_string_ptr() }.to_owned_string();
assert_eq!(s, "0");
}
// -- Object.assign --
#[test]
fn assign_filters_non_enumerable_source() {
let (_vm, result) = eval(
"var src = Object.defineProperty({}, 'x', {value: 1, enumerable: false}); JSON.stringify(Object.assign({}, src))",
)
.unwrap();
let s = unsafe { &*result.as_string_ptr() }.to_owned_string();
assert_eq!(s, "{}");
}
#[test]
fn assign_triggers_source_getter() {
let (_vm, result) = eval("var n = 0; var t = Object.assign({}, {get b(){ n++; return 2 }}); [t.b, n]").unwrap();
let obj = unsafe { &*result.as_js_object_ptr() };
assert!(
obj.get_prop_at(0).is_int() && obj.get_prop_at(0).as_int() == 2,
"getter value should be copied"
);
assert!(
obj.get_prop_at(1).is_int() && obj.get_prop_at(1).as_int() == 1,
"getter should run exactly once"
);
}
#[test]
fn assign_from_array_source() {
let (_vm, result) = eval("var t = Object.assign({}, [1, 2]); [t['0'], t['1']]").unwrap();
let obj = unsafe { &*result.as_js_object_ptr() };
assert!(obj.get_prop_at(0).is_int() && obj.get_prop_at(0).as_int() == 1);
assert!(obj.get_prop_at(1).is_int() && obj.get_prop_at(1).as_int() == 2);
}
#[test]
fn assign_target_setter_receives_target() {
// 规范 Set(to, key, value, true):target 同名 setter 的 this 是 target。
let (_vm, result) = eval(
"var recv = null; var t = {}; Object.defineProperty(t, 'x', {set: function(v){ recv = this; }, configurable: true}); Object.assign(t, {x: 1}); recv === t",
)
.unwrap();
assert!(result.is_bool() && result.as_bool(), "target setter should receive target as this");
}
#[test]
fn assign_to_frozen_target_throws() {
let err = match eval("var t = Object.freeze({a: 1}); Object.assign(t, {b: 2});") {
Ok(_) => panic!("assign to frozen target should fail"),
Err(err) => err,
};
assert!(err.contains("extensible"), "unexpected error: {err}");
}
// -- Symbol 键错位回归(shape 槽位计数含 symbol 键) --
// Object.setPrototypeOf:对象目标设置原型并返回原对象;proto 非对象/null 抛 TypeError。
#[test]
fn object_set_prototype_of_sets_prototype_and_validates() {
let (_vm, result) =
eval("var o = {}; (Object.setPrototypeOf(o, null) === o) && (Object.getPrototypeOf(o) === null)").unwrap();
assert!(result.is_bool() && result.as_bool());
let err = match eval("Object.setPrototypeOf({}, 1)") {
Ok(_) => panic!("non-object prototype should fail"),
Err(err) => err,
};
assert!(err.contains("TypeError"), "unexpected error: {err}");
}
// Object.setPrototypeOf 负向分路:不可扩展目标、原型环、原始值目标。
#[test]
fn object_set_prototype_of_negative_paths() {
// 不可扩展且新旧原型不同抛 TypeError。
let err = match eval("var o = {}; Object.preventExtensions(o); Object.setPrototypeOf(o, null);") {
Ok(_) => panic!("non-extensible target with a different prototype should fail"),
Err(err) => err,
};
assert!(err.contains("TypeError"), "unexpected error: {err}");
// 不可扩展但新旧原型相同仍成功,符合规范「SameValue 先于可扩展判定」。
let (_vm, result) = eval(
"var o = {}; var p = Object.getPrototypeOf(o); Object.preventExtensions(o); \
Object.setPrototypeOf(o, p) === o",
)
.unwrap();
assert!(
result.is_bool() && result.as_bool(),
"same prototype on non-extensible target should succeed"
);
// 原型环抛 TypeError。
let err = match eval("var a = {}; var b = {}; Object.setPrototypeOf(a, b); Object.setPrototypeOf(b, a);") {
Ok(_) => panic!("cyclic prototype should fail"),
Err(err) => err,
};
assert!(err.contains("TypeError"), "unexpected error: {err}");
// 原始值目标原样返回,不抛错。
let (_vm, result) = eval("Object.setPrototypeOf(1, null)").unwrap();
assert!(result.is_int() && result.as_int() == 1, "primitive target should be returned unchanged");
}
#[test]
fn entries_values_with_interleaved_symbol_key() {
let (_vm, result) = eval(
"var s = Symbol(); var o = {a: 1, [s]: 2, b: 3}; [JSON.stringify(Object.entries(o)), JSON.stringify(Object.values(o))]",
)
.unwrap();
let obj = unsafe { &*result.as_js_object_ptr() };
let s0 = unsafe { &*obj.get_prop_at(0).as_string_ptr() }.to_owned_string();
let s1 = unsafe { &*obj.get_prop_at(1).as_string_ptr() }.to_owned_string();
assert_eq!(s0, "[[\"a\",1],[\"b\",3]]");
assert_eq!(s1, "[1,3]");
}
// -- Object.getOwnPropertyDescriptors --
#[test]
fn object_get_own_property_descriptors_empty() {
// 结果对象 proto 为 %Object.prototype%,空对象无自身键。
let (_vm, result) = eval(
"(function(){ var d = Object.getOwnPropertyDescriptors({}); \
return Object.getPrototypeOf(d) === Object.prototype && Object.keys(d).length === 0 })()",
)
.unwrap();
assert!(result.is_bool() && result.as_bool());
}
#[test]
fn object_get_own_property_descriptors_data_and_accessor() {
// 数据描述符四字段齐全;访问器描述符有 get 无 value。
let (_vm, result) = eval(
"var o = {a: 1}; Object.defineProperty(o, 'g', {get: function(){ return 2 }, configurable: true}); \
var d = Object.getOwnPropertyDescriptors(o); \
d.a.value === 1 && d.a.writable === true && d.a.enumerable === true && d.a.configurable === true \
&& d.g.get !== undefined && d.g.value === undefined",
)
.unwrap();
assert!(result.is_bool() && result.as_bool());
}
#[test]
fn object_get_own_property_descriptors_integer_keys_ascending() {
// 整数索引键数值升序在前(10 排在 2 后),非整数串键按插入序随后,Symbol 键末段。
let (_vm, result) = eval(
"var o = {10: 'a', 2: 'b', x: 'c'}; var s = Symbol(); o[s] = 1; \
var d = Object.getOwnPropertyDescriptors(o); \
Object.getOwnPropertyNames(d).join(',') + '|' + d[s].value",
)
.unwrap();
let s = unsafe { &*result.as_string_ptr() }.to_owned_string();
assert_eq!(s, "2,10,x|1");
}
#[test]
fn object_get_own_property_descriptors_primitive_receiver() {
// 原始值接收者经 ToObject 装箱,装箱 number 无自身属性。
let (_vm, result) =
eval("(function(){ var d = Object.getOwnPropertyDescriptors(42); return Object.keys(d).length === 0 })()")
.unwrap();
assert!(result.is_bool() && result.as_bool());
}
#[test]
fn object_get_own_property_descriptors_null_receiver_throws() {
let err = match eval("Object.getOwnPropertyDescriptors(null)") {
Ok(_) => panic!("null receiver should throw TypeError"),
Err(err) => err,
};
assert!(err.contains("TypeError"), "unexpected error: {err}");
}
// -- defineProperties / create properties 实参语义 --
#[test]
fn object_define_properties_primitive_props_boxed() {
// properties 实参经 ToObject 装箱(布尔/数字/空串/Symbol/数字盒),
// 包装盒均无可枚举自身键:零定义,目标原样返回且自身键集不变。
for arg in ["false", "-12", "''", "Symbol('s')", "42"] {
let (_vm, result) = eval(&format!(
"(function(){{ var t = {{m: 1}}; return Object.defineProperties(t, {arg}) === t }})()"
))
.unwrap();
assert!(result.is_bool() && result.as_bool(), "arg {arg}");
let (_vm, result) = eval(&format!(
"(function(){{ var t = {{m: 1}}; Object.defineProperties(t, {arg}); return Object.keys(t).length }})()"
))
.unwrap();
assert_eq!(result.as_int(), 1, "arg {arg}");
}
}
#[test]
fn object_create_primitive_props_boxed() {
// Object.create 的 properties 实参同走 ToObject:数字盒零键,仅原型生效。
let (_vm, result) = eval(
"(function(){ var p = {}; var o = Object.create(p, 42); \
return Object.getPrototypeOf(o) === p && Object.keys(o).length === 0 })()",
)
.unwrap();
assert!(result.is_bool() && result.as_bool());
}
#[test]
fn object_create_bigint_props_boxed() {
// BigInt 实参装箱,BigInt 盒零可枚举自身键:仅原型生效。
let (_vm, result) = eval(
"(function(){ var p = {}; var o = Object.create(p, 0n); \
return Object.getPrototypeOf(o) === p && Object.keys(o).length === 0 })()",
)
.unwrap();
assert!(result.is_bool() && result.as_bool());
}
#[test]
fn object_define_properties_null_props_throws() {
// null 是 ToObject 拒绝的值:两入口均抛 TypeError。
let err = match eval("Object.defineProperties({}, null)") {
Ok(_) => panic!("null properties should throw TypeError"),
Err(err) => err,
};
assert!(err.contains("TypeError"), "unexpected error: {err}");
let err = match eval("Object.create(null, null)") {
Ok(_) => panic!("null properties should throw TypeError"),
Err(err) => err,
};
assert!(err.contains("TypeError"), "unexpected error: {err}");
}
#[test]
fn object_define_property_primitive_target_throws() {
// target 不做 ToObject 装箱:原始值直接抛 TypeError。
for src in [
"Object.defineProperty(0, 'a', {value: 1})",
"Object.defineProperty(true, 'a', {value: 1})",
"Object.defineProperty('abc', 'a', {value: 1})",
] {
let err = match eval(src) {
Ok(_) => panic!("{src} should throw TypeError"),
Err(err) => err,
};
assert!(err.contains("TypeError"), "unexpected error: {err}");
}
}
#[test]
fn object_define_properties_primitive_target_throws() {
// target 不做 ToObject 装箱:原始值直接抛 TypeError。
for src in [
"Object.defineProperties(0, {})",
"Object.defineProperties(true, {})",
"Object.defineProperties('abc', {})",
] {
let err = match eval(src) {
Ok(_) => panic!("{src} should throw TypeError"),
Err(err) => err,
};
assert!(err.contains("TypeError"), "unexpected error: {err}");
}
}
#[test]
fn object_create_getter_range_error_propagates() {
// 描述符收集期访问器 getter 抛出的用户异常原类型穿透,不投影为 TypeError。
let err = match eval("Object.create(null, {get p(){ throw new RangeError('boom'); }})") {
Ok(_) => panic!("getter RangeError should propagate"),
Err(err) => err,
};
assert!(err.contains("RangeError"), "unexpected error: {err}");
}
#[test]
fn object_define_properties_two_phase_get_before_define() {
// 两阶段语义:全部描述符 getter 先触发后逐键定义;后键 getter 抛出时
// 前键尚未定义。
let (_vm, result) = eval(
"(function(){ var t = {}; var n = 0; \
Object.defineProperty(t, 'p1', { get: function(){ n++; return {value: 1}; }, enumerable: true }); \
Object.defineProperty(t, 'p2', { get: function(){ if (n === 1) { n++; throw 42; } return {value: 2}; }, enumerable: true }); \
var target = {}; \
try { Object.defineProperties(target, t); } catch (e) {} \
return n === 2 && target.hasOwnProperty('p1') === false && target.hasOwnProperty('p2') === false })()",
)
.unwrap();
assert!(result.is_bool() && result.as_bool());
}