fix: delete 增加 configurable 检查,defineProperty 修复部分更新值覆盖,枚举排序增加整数索引优先和非枚举过滤
This commit is contained in:
@@ -389,6 +389,10 @@ impl Vm {
|
||||
};
|
||||
let obj = unsafe { &mut *obj_ptr };
|
||||
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
|
||||
let configurable = obj.prop_meta_at(pos).map(|m| m.attributes.configurable()).unwrap_or(true);
|
||||
if !configurable {
|
||||
return self.raise_type_error("cannot delete non-configurable property").map(|()| false);
|
||||
}
|
||||
obj.set_prop_at(pos, JsValue::undefined());
|
||||
}
|
||||
self.regs[rd] = JsValue::bool(true);
|
||||
@@ -403,6 +407,10 @@ impl Vm {
|
||||
};
|
||||
let obj = unsafe { &mut *obj_ptr };
|
||||
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
|
||||
let configurable = obj.prop_meta_at(pos).map(|m| m.attributes.configurable()).unwrap_or(true);
|
||||
if !configurable {
|
||||
return self.raise_type_error("cannot delete non-configurable property").map(|()| false);
|
||||
}
|
||||
obj.set_prop_at(pos, JsValue::undefined());
|
||||
}
|
||||
self.regs[rd] = JsValue::bool(true);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
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();
|
||||
let result = vm.run(&module)?;
|
||||
Ok((vm, result))
|
||||
}
|
||||
|
||||
fn eval_str(source: &str) -> Result<JsValue, String> {
|
||||
eval(source).map(|(_vm, v)| v)
|
||||
}
|
||||
|
||||
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
|
||||
match result {
|
||||
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
|
||||
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
|
||||
}
|
||||
}
|
||||
|
||||
// -- Partial writable update preserves existing value --
|
||||
#[test]
|
||||
fn define_property_partial_writable_preserves_value() {
|
||||
let r = eval_str("var obj={}; obj.x=42; Object.defineProperty(obj,'x',{writable:false}); obj.x").unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 42, "value should be preserved at 42, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Mixed descriptor (value+get) throws TypeError --
|
||||
#[test]
|
||||
fn define_property_mixed_value_get_throws() {
|
||||
let result = eval_str("var obj={}; Object.defineProperty(obj,'x',{value:1,get:function(){}});");
|
||||
assert_err_contains(result, "mix");
|
||||
}
|
||||
|
||||
// -- Mixed descriptor (writable+set) throws TypeError --
|
||||
#[test]
|
||||
fn define_property_mixed_writable_set_throws() {
|
||||
let result = eval_str("var obj={}; Object.defineProperty(obj,'x',{writable:true,set:function(){}});");
|
||||
assert_err_contains(result, "mix");
|
||||
}
|
||||
|
||||
// -- Partial enumerable-only update preserves value --
|
||||
#[test]
|
||||
fn define_property_partial_enumerable_only_preserves_value() {
|
||||
let r = eval_str("var obj={}; obj.x=42; Object.defineProperty(obj,'x',{enumerable:true}); obj.x").unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 42, "value should be preserved at 42, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- getOwnPropertyDescriptor returns data fields (no get/set) --
|
||||
#[test]
|
||||
fn get_own_property_descriptor_data_fields() {
|
||||
let r = eval_str(
|
||||
"var obj={x:1}; var d=Object.getOwnPropertyDescriptor(obj,'x'); [d.value,d.writable,d.enumerable,d.configurable,d.get,d.set]",
|
||||
)
|
||||
.unwrap();
|
||||
let obj = unsafe { &*r.as_js_object_ptr() };
|
||||
let v = obj.get_prop_at(0);
|
||||
assert!(v.is_int() && v.as_int() == 1, "value should be 1, got {:?}", v);
|
||||
assert!(obj.get_prop_at(1).is_bool() && obj.get_prop_at(1).as_bool(), "writable should be true");
|
||||
assert!(obj.get_prop_at(2).is_bool() && obj.get_prop_at(2).as_bool(), "enumerable should be true");
|
||||
assert!(
|
||||
obj.get_prop_at(3).is_bool() && obj.get_prop_at(3).as_bool(),
|
||||
"configurable should be true"
|
||||
);
|
||||
assert!(obj.get_prop_at(4).is_undefined(), "get should be undefined");
|
||||
assert!(obj.get_prop_at(5).is_undefined(), "set should be undefined");
|
||||
}
|
||||
|
||||
// -- getOwnPropertyDescriptor returns accessor fields (no value/writable) --
|
||||
#[test]
|
||||
fn get_own_property_descriptor_accessor_fields() {
|
||||
let r = eval_str(
|
||||
"var obj={}; Object.defineProperty(obj,'x',{get:function(){return 1;},enumerable:true,configurable:true}); var d=Object.getOwnPropertyDescriptor(obj,'x'); [d.get!==undefined,d.set===undefined,d.enumerable,d.configurable,d.value===undefined,d.writable===undefined]",
|
||||
)
|
||||
.unwrap();
|
||||
let obj = unsafe { &*r.as_js_object_ptr() };
|
||||
assert!(obj.get_prop_at(0).is_bool() && obj.get_prop_at(0).as_bool(), "get should be defined");
|
||||
assert!(obj.get_prop_at(1).is_bool() && obj.get_prop_at(1).as_bool(), "set should be undefined");
|
||||
assert!(obj.get_prop_at(2).is_bool() && obj.get_prop_at(2).as_bool(), "enumerable should be true");
|
||||
assert!(
|
||||
obj.get_prop_at(3).is_bool() && obj.get_prop_at(3).as_bool(),
|
||||
"configurable should be true"
|
||||
);
|
||||
assert!(obj.get_prop_at(4).is_bool() && obj.get_prop_at(4).as_bool(), "value should be undefined");
|
||||
assert!(
|
||||
obj.get_prop_at(5).is_bool() && obj.get_prop_at(5).as_bool(),
|
||||
"writable should be undefined"
|
||||
);
|
||||
}
|
||||
|
||||
// -- New property without value or accessor throws --
|
||||
#[test]
|
||||
fn define_property_new_without_value_throws() {
|
||||
let result = eval_str("var obj={}; Object.defineProperty(obj,'x',{enumerable:true})");
|
||||
assert_err_contains(result, "Cannot define new property without value or accessor");
|
||||
}
|
||||
|
||||
// -- Partial configurable on existing property preserves value --
|
||||
#[test]
|
||||
fn define_property_partial_configurable_preserves_value() {
|
||||
let r = eval_str("var obj={}; obj.x=1; Object.defineProperty(obj,'x',{configurable:false}); [obj.x]").unwrap();
|
||||
let arr = unsafe { &*r.as_js_object_ptr() };
|
||||
assert!(arr.get_prop_at(0).is_int() && arr.get_prop_at(0).as_int() == 1, "value should be 1");
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_types::value::JsValue;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn eval(source: &str) -> Result<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.run(&module)
|
||||
}
|
||||
|
||||
fn eval_many(lines: &[&str]) -> Result<JsValue, String> {
|
||||
let source = lines.join("; ");
|
||||
eval(&source)
|
||||
}
|
||||
|
||||
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
|
||||
match result {
|
||||
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
|
||||
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
|
||||
}
|
||||
}
|
||||
|
||||
// -- Delete configurable property succeeds --
|
||||
#[test]
|
||||
fn delete_configurable_property_succeeds_and_returns_true() {
|
||||
let r = eval_many(&[
|
||||
"var obj = {}",
|
||||
"Object.defineProperty(obj, 'x', {value: 1, configurable: true})",
|
||||
"delete obj.x",
|
||||
])
|
||||
.unwrap();
|
||||
assert!(r.is_bool() && r.as_bool(), "delete configurable should return true, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Delete non-configurable property throws --
|
||||
#[test]
|
||||
fn delete_non_configurable_throws_type_error() {
|
||||
let result = eval_many(&[
|
||||
"var obj = {}",
|
||||
"Object.defineProperty(obj, 'x', {value: 1, configurable: false})",
|
||||
"delete obj.x",
|
||||
]);
|
||||
assert_err_contains(result, "cannot delete non-configurable property");
|
||||
}
|
||||
|
||||
// -- Delete non-existent property returns true --
|
||||
#[test]
|
||||
fn delete_non_existent_returns_true() {
|
||||
let r = eval_many(&["var obj = {}", "delete obj.x"]).unwrap();
|
||||
assert!(r.is_bool() && r.as_bool(), "delete non-existent should return true, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Delete default (no explicit defineProperty) configurable succeeds --
|
||||
#[test]
|
||||
fn delete_default_configurable_succeeds() {
|
||||
let r = eval_many(&["var obj = {}", "obj.x = 1", "delete obj.x"]).unwrap();
|
||||
assert!(r.is_bool() && r.as_bool(), "delete default configurable should return true, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Delete non-configurable proto property on child: child doesn't own it, so returns true --
|
||||
#[test]
|
||||
fn delete_non_configurable_proto_property_on_child_returns_true() {
|
||||
let r = eval_many(&[
|
||||
"var proto = {}",
|
||||
"proto.x = 1",
|
||||
"Object.defineProperty(proto, 'x', {configurable: false})",
|
||||
"var child = Object.create(proto)",
|
||||
"delete child.x",
|
||||
])
|
||||
.unwrap();
|
||||
assert!(
|
||||
r.is_bool() && r.as_bool(),
|
||||
"delete on child for non-configurable proto prop should return true (not own), got {:?}",
|
||||
r
|
||||
);
|
||||
}
|
||||
|
||||
// -- Delete configurable proto-level property through child --
|
||||
#[test]
|
||||
fn delete_configurable_proto_property_on_child_returns_true() {
|
||||
let r = eval_many(&[
|
||||
"var proto = {}",
|
||||
"Object.defineProperty(proto, 'x', {value: 1, configurable: true, enumerable: true})",
|
||||
"var child = Object.create(proto)",
|
||||
"delete child.x",
|
||||
])
|
||||
.unwrap();
|
||||
assert!(
|
||||
r.is_bool() && r.as_bool(),
|
||||
"delete configurable proto prop from child should return true (not own), got {:?}",
|
||||
r
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
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();
|
||||
let result = vm.run(&module)?;
|
||||
Ok((vm, result))
|
||||
}
|
||||
|
||||
fn eval_str(source: &str) -> Result<JsValue, String> {
|
||||
eval(source).map(|(_vm, v)| v)
|
||||
}
|
||||
|
||||
fn keys_to_strings(result: JsValue) -> Vec<String> {
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
let len = obj.prop_count();
|
||||
let mut keys = Vec::new();
|
||||
for i in 0..len {
|
||||
let v = obj.get_prop_at(i);
|
||||
if v.is_string() {
|
||||
// SAFETY: string value pointing into VM arena (caller keeps vm alive)
|
||||
let s = unsafe { &*v.as_string_ptr() };
|
||||
keys.push(s.data.clone());
|
||||
}
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
// -- Integer keys in ascending order --
|
||||
#[test]
|
||||
fn keys_integer_order_ascending() {
|
||||
let r = eval_str("var obj={}; obj[3]='c'; obj[1]='a'; obj[2]='b'; Object.keys(obj).length").unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 3, "should have 3 keys, got {:?}", r);
|
||||
let (_vm, r2) = eval("var obj={}; obj[3]='c'; obj[1]='a'; obj[2]='b'; Object.keys(obj)").unwrap();
|
||||
let keys = keys_to_strings(r2);
|
||||
assert_eq!(keys, vec!["1", "2", "3"], "integer keys should be ascending, got {:?}", keys);
|
||||
}
|
||||
|
||||
// -- Mixed keys: integers first, then strings in creation order --
|
||||
#[test]
|
||||
fn keys_mixed_order_integers_first() {
|
||||
let (_vm, r) = eval("var obj={}; obj[2]='b'; obj['1']='a'; obj['a']=1; Object.keys(obj)").unwrap();
|
||||
let keys = keys_to_strings(r);
|
||||
assert!(keys[0] == "1" || keys[0] == "2", "first key should be integer, got {:?}", keys);
|
||||
let str_idx = keys.iter().position(|k| k == "a").unwrap();
|
||||
let int_positions: Vec<usize> = keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, k)| k.parse::<u32>().is_ok())
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
for ip in &int_positions {
|
||||
assert!(*ip < str_idx, "integer at {} should come before string 'a' at {}", ip, str_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Object.keys excludes non-enumerable properties --
|
||||
#[test]
|
||||
fn keys_excludes_non_enumerable() {
|
||||
let r = eval_str(
|
||||
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.keys(obj).length",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 1, "keys should exclude non-enumerable, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Object.getOwnPropertyNames includes non-enumerable properties --
|
||||
#[test]
|
||||
fn get_own_property_names_includes_non_enumerable() {
|
||||
let r = eval_str("var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.getOwnPropertyNames(obj).length").unwrap();
|
||||
assert!(
|
||||
r.is_int() && r.as_int() == 2,
|
||||
"getOwnPropertyNames should include non-enumerable, got {:?}",
|
||||
r
|
||||
);
|
||||
}
|
||||
|
||||
// -- Object.entries excludes non-enumerable properties --
|
||||
#[test]
|
||||
fn entries_excludes_non_enumerable() {
|
||||
let r = eval_str(
|
||||
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.entries(obj).length",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 1, "entries should exclude non-enumerable, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Object.values excludes non-enumerable properties --
|
||||
#[test]
|
||||
fn values_excludes_non_enumerable() {
|
||||
let r = eval_str(
|
||||
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.values(obj).length",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 1, "values should exclude non-enumerable, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Leading zero strings are NOT integer indices --
|
||||
#[test]
|
||||
fn keys_with_leading_zero_not_integer_index() {
|
||||
let (_vm, r) = eval("Object.keys({'01':'a','1':'b'})").unwrap();
|
||||
let keys = keys_to_strings(r);
|
||||
assert!(keys.contains(&"01".to_string()), "should contain '01' as string key");
|
||||
assert!(keys.contains(&"1".to_string()), "should contain '1' as integer");
|
||||
let idx_01 = keys.iter().position(|k| k == "01").unwrap();
|
||||
let idx_1 = keys.iter().position(|k| k == "1").unwrap();
|
||||
assert!(idx_1 < idx_01, "integer '1' should come before string '01'");
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
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();
|
||||
let result = vm.run(&module)?;
|
||||
Ok((vm, result))
|
||||
}
|
||||
|
||||
fn eval_str(source: &str) -> Result<JsValue, String> {
|
||||
eval(source).map(|(_vm, v)| v)
|
||||
}
|
||||
|
||||
// Getter on proto sets marker on receiver, then check receiver has marker (not proto)
|
||||
#[test]
|
||||
fn getter_on_proto_this_is_receiver() {
|
||||
let r = eval_str(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._marker=1; return this._marker; },enumerable:true,configurable:true}); var child=Object.create(proto); child.x"
|
||||
).unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 1, "getter should set marker on child (receiver), got {:?}", r);
|
||||
}
|
||||
|
||||
// Getter on deep proto chain: grandchild → child → proto. Getter defined on proto.
|
||||
#[test]
|
||||
fn getter_on_proto_deep_chain_this_is_receiver() {
|
||||
let r = eval_str(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._deep=this; return 99; },enumerable:true,configurable:true}); var child=Object.create(proto); var grandchild=Object.create(child); grandchild.x; grandchild._deep!==undefined"
|
||||
).unwrap();
|
||||
assert!(r.is_bool() && r.as_bool(), "grandchild should have _deep marker, got {:?}", r);
|
||||
}
|
||||
|
||||
// Setter on proto receives receiver as this, sets marker on receiver not proto
|
||||
#[test]
|
||||
fn setter_on_proto_this_is_receiver() {
|
||||
let (_vm, r) = eval(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{set:function(v){ this._set_by_setter=v; },get:function(){ return this._set_by_setter; },enumerable:true,configurable:true}); var child=Object.create(proto); child.x='receiver_test'; child._set_by_setter"
|
||||
).unwrap();
|
||||
assert!(r.is_string(), "child should have _set_by_setter, got {:?}", r);
|
||||
}
|
||||
|
||||
// Dynamic getter (obj[expr]) preserves receiver
|
||||
#[test]
|
||||
fn dynamic_getter_on_proto_this_is_receiver() {
|
||||
let r = eval_str(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._dyn=1; return this._dyn; },enumerable:true,configurable:true}); var child=Object.create(proto); child['x']"
|
||||
).unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 1, "dynamic getter should set marker on child, got {:?}", r);
|
||||
}
|
||||
|
||||
// Dynamic setter (obj[expr]=val) preserves receiver
|
||||
#[test]
|
||||
fn dynamic_setter_on_proto_this_is_receiver() {
|
||||
let (_vm, r) = eval(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{set:function(v){ this._dyn_set=v; },get:function(){ return this._dyn_set; },enumerable:true,configurable:true}); var child=Object.create(proto); child['x']='dyn_test'; child._dyn_set"
|
||||
).unwrap();
|
||||
assert!(r.is_string(), "child should have _dyn_set after dynamic setter, got {:?}", r);
|
||||
}
|
||||
|
||||
// Own getter on object — this is the object itself
|
||||
#[test]
|
||||
fn own_getter_this_is_self() {
|
||||
let r = eval_str(
|
||||
"var obj={}; Object.defineProperty(obj,'x',{get:function(){ this._own=42; return this._own; },enumerable:true,configurable:true}); obj.x"
|
||||
).unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 42, "own getter should set marker on self, got {:?}", r);
|
||||
}
|
||||
|
||||
// Own setter on object — this is the object itself
|
||||
#[test]
|
||||
fn own_setter_this_is_self() {
|
||||
let (_vm, r) = eval(
|
||||
"var obj={}; Object.defineProperty(obj,'x',{set:function(v){ this._own_set=v; },get:function(){ return this._own_set; },enumerable:true,configurable:true}); obj.x='own'; obj._own_set"
|
||||
).unwrap();
|
||||
assert!(r.is_string(), "own setter should set marker on self, got {:?}", r);
|
||||
}
|
||||
|
||||
// Getter returning undefined does not crash
|
||||
#[test]
|
||||
fn getter_returns_undefined_does_not_crash() {
|
||||
let r = eval_str(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{get:function(){},enumerable:true,configurable:true}); var child=Object.create(proto); child.x"
|
||||
).unwrap();
|
||||
assert!(r.is_undefined(), "getter returning nothing should yield undefined, got {:?}", r);
|
||||
}
|
||||
|
||||
// String .length still works after proto getter is defined (primitive auto-boxing not broken)
|
||||
#[test]
|
||||
fn string_length_still_works_after_proto_getter() {
|
||||
let r = eval_str("'hello'.length").unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 5, "string.length should be 5, got {:?}", r);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_types::value::JsValue;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn eval(source: &str) -> Result<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.run(&module)
|
||||
}
|
||||
|
||||
fn eval_many(lines: &[&str]) -> Result<JsValue, String> {
|
||||
let source = lines.join("; ");
|
||||
eval(&source)
|
||||
}
|
||||
|
||||
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
|
||||
match result {
|
||||
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
|
||||
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
|
||||
}
|
||||
}
|
||||
|
||||
// -- Assign to non-writable own property throws TypeError --
|
||||
#[test]
|
||||
fn assign_to_non_writable_throws() {
|
||||
let result = eval_many(&[
|
||||
"var obj = {}",
|
||||
"Object.defineProperty(obj, 'x', {value: 1, writable: false})",
|
||||
"obj.x = 2",
|
||||
]);
|
||||
assert_err_contains(result, "read-only");
|
||||
}
|
||||
|
||||
// -- Assign to writable own property succeeds --
|
||||
#[test]
|
||||
fn assign_to_writable_succeeds() {
|
||||
let r = eval_many(&[
|
||||
"var obj = {}",
|
||||
"Object.defineProperty(obj, 'x', {value: 1, writable: true})",
|
||||
"obj.x = 2",
|
||||
"obj.x",
|
||||
])
|
||||
.unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 2, "assign to writable should set value to 2, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Assign to inherited non-writable property on proto throws --
|
||||
#[test]
|
||||
fn assign_to_inherited_non_writable_throws() {
|
||||
let result = eval_many(&[
|
||||
"var proto = {}",
|
||||
"Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})",
|
||||
"var child = Object.create(proto)",
|
||||
"child.x = 2",
|
||||
]);
|
||||
assert_err_contains(result, "read-only");
|
||||
}
|
||||
|
||||
// -- Assign creates own property on child when proto's is non-writable but child has no own prop
|
||||
// ponytail: spec allows shadowing when proto prop is configurable; engine doesn't yet.
|
||||
// The engine correctly throws for non-writable proto props. Full shadowing is a follow-up.
|
||||
#[test]
|
||||
fn assign_non_writable_proto_configurable_prop_throws() {
|
||||
let result = eval_many(&[
|
||||
"var proto = {}",
|
||||
"Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})",
|
||||
"var child = Object.create(proto)",
|
||||
"child.x = 99",
|
||||
]);
|
||||
assert_err_contains(result, "read-only");
|
||||
}
|
||||
Reference in New Issue
Block a user