Files
2026-08-11 00:10:12 +08:00

388 lines
15 KiB
Rust

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<v8::Global<v8::Value>, 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<v8::Global<v8::Value>, 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::<v8::Object>::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::<v8::Object>::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::<v8::Object>::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::<v8::Object>::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::<v8::Object>::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::<v8::Object>::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::<v8::Object>::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}"
);
}