use super::*; fn eval<'s>(scope: &mut v8::PinScope<'s, '_>, source: &str) -> v8::Local<'s, v8::Value> { let source = v8::String::new(scope, source).expect("test source"); let script = v8::Script::compile(scope, source, None).expect("compile test source"); script.run(scope).expect("run test source") } fn invoke_for_test<'s>( scope: &mut v8::PinScope<'s, '_>, invocation: WebIdlCallbackInvocation<'s, '_>, ) -> Result, String> { invoke_webidl_callback( scope, invocation, |scope, callback, receiver, arguments| { callback .call(scope, receiver, arguments) .map(|value| v8::Global::new(scope, value)) .ok_or_else(|| "callback call failed".to_owned()) }, |scope, failure| { failure .exception() .and_then(|exception| exception.to_string(scope)) .map(|message| message.to_rust_string_lossy(scope)) .unwrap_or_else(|| "callback resolution failed".to_owned()) }, ) } fn invoke_function_for_test<'s>( scope: &mut v8::PinScope<'s, '_>, callback: &PreparedWebIdlCallbackFunction, receiver: v8::Local<'s, v8::Value>, arguments: &[v8::Local<'s, v8::Value>], ) -> Result, String> { invoke_webidl_callback_function( scope, callback, receiver, arguments, |scope, callback, receiver, arguments| { callback .call(scope, receiver, arguments) .map(|value| v8::Global::new(scope, value)) .ok_or_else(|| "callback call failed".to_owned()) }, ) } #[test] fn rooted_callback_preparation_preserves_callback_and_contexts() { moli_v8_test_util::ensure_v8(); let mut isolate = v8::Isolate::new(v8::CreateParams::default()); let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let relevant_context = v8::Context::new(scope, Default::default()); let incumbent_context = v8::Context::new(scope, Default::default()); let scope = &mut v8::ContextScope::new(scope, relevant_context); let callback = v8::Local::::try_from(eval(scope, "(function () {})")).expect("callback"); let rooted = WebIdlCallbackInterface::new(scope, callback, relevant_context, incumbent_context); let prepared = rooted.prepare(scope); assert!(rooted.matches(scope, callback)); assert!(prepared.callback(scope).strict_equals(callback.into())); assert_eq!(prepared.relevant_context(scope), relevant_context); assert_eq!(prepared.incumbent_context(scope), incumbent_context); assert!(prepared.callable_at_conversion()); } #[test] fn callback_interface_reads_replacement_operation_and_uses_original_receiver() { moli_v8_test_util::ensure_v8(); let mut isolate = v8::Isolate::new(v8::CreateParams::default()); let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let context = v8::Context::new(scope, Default::default()); let scope = &mut v8::ContextScope::new(scope, context); let callback = v8::Local::::try_from(eval( scope, "({ handleEvent() { throw new Error('old operation'); } })", )) .expect("callback-interface object"); let rooted = WebIdlCallbackInterface::new(scope, callback, context, context); let replacement = eval( scope, "(function (value) { this.seen = value; return 42; })", ); let handle_event = v8::String::new(scope, "handleEvent").expect("handleEvent key"); assert_eq!( callback.set(scope, handle_event.into(), replacement), Some(true) ); let prepared = rooted.prepare(scope); let argument: v8::Local<'_, v8::Value> = v8::Integer::new(scope, 7).into(); let arguments = [argument]; let relevant_context = prepared.relevant_context(scope); let incumbent_context = prepared.incumbent_context(scope); let result = with_webidl_callback_contexts(scope, relevant_context, incumbent_context, |scope| { let callback = prepared.callback(scope); let receiver = v8::undefined(scope).into(); invoke_for_test( scope, WebIdlCallbackInvocation::new( callback, receiver, prepared.callable_at_conversion(), "handleEvent", &arguments, ), ) }) .expect("replacement handleEvent should run"); assert_eq!( v8::Local::new(scope, &result).integer_value(scope), Some(42) ); let seen = callback .get( scope, v8::String::new(scope, "seen").expect("seen key").into(), ) .expect("seen value"); assert_eq!(seen.integer_value(scope), Some(7)); } #[test] fn callback_interface_invocation_enters_relevant_and_incumbent_contexts() { moli_v8_test_util::ensure_v8(); let mut isolate = v8::Isolate::new(v8::CreateParams::default()); let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let caller_context = v8::Context::new(scope, Default::default()); let relevant_context = v8::Context::new(scope, Default::default()); let incumbent_context = v8::Context::new(scope, Default::default()); let callback = { let scope = &mut v8::ContextScope::new(scope, relevant_context); let callback = v8::Local::::try_from(eval(scope, "({ acceptNode() { return 1; } })")) .expect("callback-interface object"); v8::Global::new(scope, callback) }; let scope = &mut v8::ContextScope::new(scope, caller_context); let callback = v8::Local::new(scope, &callback); let rooted = WebIdlCallbackInterface::new(scope, callback, relevant_context, incumbent_context); let prepared = rooted.prepare(scope); let relevant_context = prepared.relevant_context(scope); let incumbent_context = prepared.incumbent_context(scope); let result = with_webidl_callback_contexts(scope, relevant_context, incumbent_context, |scope| { assert_eq!(scope.get_current_context(), relevant_context); assert_eq!(scope.get_incumbent_context(), Some(incumbent_context)); let callback = prepared.callback(scope); let receiver = v8::undefined(scope).into(); invoke_for_test( scope, WebIdlCallbackInvocation::new( callback, receiver, prepared.callable_at_conversion(), "acceptNode", &[], ), ) }) .expect("callback-interface invocation"); assert_eq!(v8::Local::new(scope, &result).integer_value(scope), Some(1)); } #[test] fn callback_function_uses_supplied_receiver() { moli_v8_test_util::ensure_v8(); let mut isolate = v8::Isolate::new(v8::CreateParams::default()); let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let context = v8::Context::new(scope, Default::default()); let scope = &mut v8::ContextScope::new(scope, context); let callback = v8::Local::::try_from(eval(scope, "(function () { this.called = true; })")) .expect("function callback"); let target = v8::Object::new(scope); let arguments = []; invoke_for_test( scope, WebIdlCallbackInvocation::new(callback, target.into(), true, "handleEvent", &arguments), ) .expect("function callback should run"); let called = target .get( scope, v8::String::new(scope, "called").expect("called key").into(), ) .expect("called value"); assert!(called.boolean_value(scope)); } #[test] fn callback_interface_without_callable_operation_reports_type_error() { moli_v8_test_util::ensure_v8(); let mut isolate = v8::Isolate::new(v8::CreateParams::default()); let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let context = v8::Context::new(scope, Default::default()); let scope = &mut v8::ContextScope::new(scope, context); let callback = v8::Object::new(scope); let arguments = []; let receiver = v8::undefined(scope).into(); let error = invoke_for_test( scope, WebIdlCallbackInvocation::new(callback, receiver, false, "handleEvent", &arguments), ) .expect_err("missing handleEvent must fail"); assert!( error.contains("no callable handleEvent property"), "{error}" ); } #[test] fn callback_function_preparation_enters_captured_contexts_and_uses_receiver() { moli_v8_test_util::ensure_v8(); let mut isolate = v8::Isolate::new(v8::CreateParams::default()); let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let caller_context = v8::Context::new(scope, Default::default()); let relevant_context = v8::Context::new(scope, Default::default()); let incumbent_context = v8::Context::new(scope, Default::default()); let callback = { let scope = &mut v8::ContextScope::new(scope, relevant_context); let _ = eval(scope, "globalThis.realmValue = 40"); let callback = v8::Local::::try_from(eval( scope, "(function (argument) { return this.receiverValue + argument + realmValue; })", )) .expect("callback function"); v8::Global::new(scope, callback) }; let scope = &mut v8::ContextScope::new(scope, caller_context); let callback = v8::Local::new(scope, &callback); let rooted = WebIdlCallbackFunction::try_new(scope, callback, relevant_context, incumbent_context) .expect("callable callback"); let prepared = rooted.prepare(scope); assert_eq!(prepared.relevant_context(scope), relevant_context); assert_eq!(prepared.incumbent_context(scope), incumbent_context); let receiver = v8::Object::new(scope); let receiver_value = v8::String::new(scope, "receiverValue").expect("receiverValue"); assert_eq!( receiver.set( scope, receiver_value.into(), v8::Integer::new(scope, 1).into() ), Some(true) ); let arguments = [v8::Integer::new(scope, 1).into()]; let result = invoke_webidl_callback_function( scope, &prepared, receiver.into(), &arguments, |scope, callback, receiver, arguments| { assert_eq!(scope.get_current_context(), relevant_context); callback .call(scope, receiver, arguments) .map(|value| v8::Global::new(scope, value)) .ok_or("callback should return") }, ) .expect("callback function invocation"); assert_eq!( v8::Local::new(scope, &result).integer_value(scope), Some(42) ); assert!(rooted.matches(scope, callback)); assert!(rooted.value(scope).strict_equals(callback.into())); } #[test] fn callback_function_accepts_callable_proxy() { moli_v8_test_util::ensure_v8(); let mut isolate = v8::Isolate::new(v8::CreateParams::default()); let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let context = v8::Context::new(scope, Default::default()); let scope = &mut v8::ContextScope::new(scope, context); let callback = v8::Local::::try_from(eval( scope, "new Proxy(function (value) { return this.base + value; }, { apply(target, receiver, arguments) { return Reflect.apply(target, receiver, arguments) + 1; } })", )) .expect("callable proxy"); let rooted = WebIdlCallbackFunction::try_new(scope, callback, context, context) .expect("callable proxy must convert"); let prepared = rooted.prepare(scope); let receiver = v8::Object::new(scope); let base = v8::String::new(scope, "base").expect("base"); assert_eq!( receiver.set(scope, base.into(), v8::Integer::new(scope, 40).into()), Some(true) ); let arguments = [v8::Integer::new(scope, 1).into()]; let result = invoke_function_for_test(scope, &prepared, receiver.into(), &arguments) .expect("callable proxy invocation"); assert_eq!( v8::Local::new(scope, &result).integer_value(scope), Some(42) ); } #[test] fn callback_function_rejects_non_callable_object() { moli_v8_test_util::ensure_v8(); let mut isolate = v8::Isolate::new(v8::CreateParams::default()); let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let context = v8::Context::new(scope, Default::default()); let scope = &mut v8::ContextScope::new(scope, context); let object = v8::Object::new(scope); assert!(WebIdlCallbackFunction::try_new(scope, object, context, context).is_none()); } #[test] fn revoked_callable_proxy_reaches_host_exception_policy() { moli_v8_test_util::ensure_v8(); let mut isolate = v8::Isolate::new(v8::CreateParams::default()); let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let context = v8::Context::new(scope, Default::default()); let scope = &mut v8::ContextScope::new(scope, context); let callback = v8::Local::::try_from(eval( scope, "globalThis.revocableCallback = Proxy.revocable(function () {}, {}); revocableCallback.proxy", )) .expect("revocable callback proxy"); let rooted = WebIdlCallbackFunction::try_new(scope, callback, context, context) .expect("proxy is callable at conversion"); let prepared = rooted.prepare(scope); let _ = eval(scope, "revocableCallback.revoke()"); let receiver = v8::undefined(scope).into(); let error = invoke_webidl_callback_function( scope, &prepared, receiver, &[], |scope, callback, receiver, arguments| { let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); let scope = try_catch.init(); if callback.call(&scope, receiver, arguments).is_some() { return Ok(()); } Err(scope .exception() .and_then(|exception| exception.to_string(&scope)) .map(|message| message.to_rust_string_lossy(&scope)) .unwrap_or_else(|| "host captured callback exception".to_owned())) }, ) .expect_err("revoked proxy invocation must throw"); assert!( error.contains("revoked") || error.contains("proxy"), "unexpected callback exception: {error}" ); }