From 172bf4dba5e114e217a203d704a146ab97c38492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 31 Jul 2026 03:01:20 +0200 Subject: [PATCH 1/2] fix(node:test): support symbol keyed mock methods --- .../perry-runtime/src/node_submodules/test.rs | 68 +++++++++++++++++-- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/crates/perry-runtime/src/node_submodules/test.rs b/crates/perry-runtime/src/node_submodules/test.rs index 71eab4fef0..7bd3836ab1 100644 --- a/crates/perry-runtime/src/node_submodules/test.rs +++ b/crates/perry-runtime/src/node_submodules/test.rs @@ -416,6 +416,9 @@ fn parse_mock_timer_options(options: f64) -> (u32, f64) { #[derive(Clone)] enum MockRestoreTarget { None, + // Node 26.5 accepts Symbol method names, but its restore path only + // recognizes string names and falls through to the bare-function case. + ObjectSymbolMethod, ObjectProperty { target: f64, property: String, @@ -743,7 +746,11 @@ fn restore_mock_state(id: i64) { let Some(state) = states.iter_mut().find(|state| state.id == id) else { return None; }; - state.implementation = state.original; + state.implementation = if matches!(state.restore, MockRestoreTarget::ObjectSymbolMethod) { + undefined_value() + } else { + state.original + }; state.once.clear(); reset_mock_state_calls(state); Some(state.restore.clone()) @@ -824,20 +831,42 @@ mod metadata_tests; extern "C" fn mock_function_invoke(closure: *const ClosureHeader, rest: f64) -> f64 { let id = closure_id(closure); let args = array_values(rest).unwrap_or_default(); - let implementation = MOCK_STATES.with(|states| { + let (implementation, is_symbol_method) = MOCK_STATES.with(|states| { let mut states = states.borrow_mut(); let Some(state) = states.iter_mut().find(|state| state.id == id) else { - return undefined_value(); + return (undefined_value(), false); }; - if !state.once.is_empty() { + let implementation = if !state.once.is_empty() { state.once.remove(0) } else { state.implementation - } + }; + ( + implementation, + matches!(state.restore, MockRestoreTarget::ObjectSymbolMethod), + ) }); let this_value = crate::object::js_implicit_this_get(); if JSValue::from_bits(implementation.to_bits()).is_undefined() { + if is_symbol_method { + let scope = crate::gc::RuntimeHandleScope::new(); + let rest_handle = scope.root_nanbox_f64(rest); + let this_handle = scope.root_nanbox_f64(this_value); + let message_handle = scope.root_nanbox_f64(string_value("undefined is not a function")); + let error = crate::error::js_typeerror_new(raw_ptr_from_value( + message_handle.get_nanbox_f64(), + ) as *mut crate::StringHeader); + let error_handle = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(error as i64)); + record_mock_call( + id, + rest_handle.get_nanbox_f64(), + this_handle.get_nanbox_f64(), + undefined_value(), + error_handle.get_nanbox_f64(), + ); + crate::exception::js_throw(error_handle.get_nanbox_f64()); + } record_mock_call(id, rest, this_value, undefined_value(), undefined_value()); return undefined_value(); } @@ -995,6 +1024,35 @@ extern "C" fn mock_method_thunk( implementation.get_nanbox_f64(), ); } + if unsafe { crate::symbol::js_is_symbol(property.get_nanbox_f64()) } != 0 { + object_target_addr(target.get_nanbox_f64()); + let original = unsafe { + crate::symbol::js_object_get_symbol_property( + target.get_nanbox_f64(), + property.get_nanbox_f64(), + ) + }; + let implementation = if is_undefined_value(implementation.get_nanbox_f64()) { + original + } else { + implementation.get_nanbox_f64() + }; + assert_callable_arg("implementation", implementation); + let function = scope.root_nanbox_f64(create_mock_function( + original, + implementation, + MockRestoreTarget::ObjectSymbolMethod, + )); + unsafe { + crate::symbol::js_object_set_symbol_property( + target.get_nanbox_f64(), + property.get_nanbox_f64(), + function.get_nanbox_f64(), + ); + } + return function.get_nanbox_f64(); + } + let property_name = property_name(property.get_nanbox_f64()); let original = get_property_value(target.get_nanbox_f64(), &property_name); let implementation = if is_undefined_value(implementation.get_nanbox_f64()) { From 9ca323fa22feafe482ec54f869ada45fda2de4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 31 Jul 2026 03:01:52 +0200 Subject: [PATCH 2/2] docs(changelog): note symbol mock method parity --- changelog.d/7102-node-test-mock-symbol.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/7102-node-test-mock-symbol.md diff --git a/changelog.d/7102-node-test-mock-symbol.md b/changelog.d/7102-node-test-mock-symbol.md new file mode 100644 index 0000000000..ec3d9e81f5 --- /dev/null +++ b/changelog.d/7102-node-test-mock-symbol.md @@ -0,0 +1,2 @@ +Added `node:test` mock method support for Symbol-keyed object methods, including +Node-compatible call tracking and restore behavior.