fix(events): apply exact handler return cancellation

This commit is contained in:
ldm0
2026-09-22 21:43:33 +08:00
parent de0928e5ce
commit 8bbcfb8df5
15 changed files with 212 additions and 67 deletions
@@ -3236,7 +3236,6 @@ html/webappapis/scripting/events/event-handler-attributes-frameset-alt.html
html/webappapis/scripting/events/event-handler-attributes-frameset-window.html
html/webappapis/scripting/events/event-handler-attributes-window-frameset.html
html/webappapis/scripting/events/event-handler-processing-algorithm-error/frameset-element-synthetic-errorevent.html
html/webappapis/scripting/events/event-handler-processing-algorithm-error/worker.html
html/webappapis/scripting/processing-model-2/compile-error-cross-origin.html
html/webappapis/scripting/processing-model-2/integration-with-the-javascript-job-queue/promise-job-detached-iframe.html
html/webappapis/scripting/processing-model-2/integration-with-the-javascript-job-queue/promise-job-incumbent.html
@@ -7329,6 +7329,7 @@ html/webappapis/scripting/events/event-handler-processing-algorithm-error/synthe
html/webappapis/scripting/events/event-handler-processing-algorithm-error/window-runtime-error.html
html/webappapis/scripting/events/event-handler-processing-algorithm-error/window-synthetic-errorevent.html
html/webappapis/scripting/events/event-handler-processing-algorithm-error/window-synthetic-event.html
html/webappapis/scripting/events/event-handler-processing-algorithm-error/worker.html
html/webappapis/scripting/events/event-handler-processing-algorithm.html
html/webappapis/scripting/events/event-handler-sourcetext.html
html/webappapis/scripting/events/eventhandler-cancellation.html
+4 -4
View File
@@ -232,10 +232,10 @@ pub(crate) use self::css_stylesheet_runtime::{
pub(crate) use self::dom_rect::build_dom_rect_object;
pub(crate) use self::events::{
EVENT_DISPATCHING_SLOT, EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT,
EVENT_STOP_PROPAGATION_SLOT, clear_event_composed_path, event_initialized,
event_internal_bool_flag, event_is_dispatching, event_is_error_event, event_is_mouse_event,
initialize_event_object, mark_event_trusted, set_event_composed_path, set_event_internal_flag,
set_event_trusted,
EVENT_STOP_PROPAGATION_SLOT, EventHandlerType, apply_event_handler_return_value,
clear_event_composed_path, event_initialized, event_internal_bool_flag, event_is_dispatching,
event_is_error_event, event_is_mouse_event, initialize_event_object, mark_event_trusted,
set_event_composed_path, set_event_internal_flag, set_event_trusted,
};
pub(crate) use self::file_api::{
DataTransferStringCallbackTask, DataTransferStringCallbackTaskEffect,
@@ -340,6 +340,7 @@ pub(super) use base::{
set_event_initialized,
};
pub(super) use kind::EventSubclassKind;
pub(crate) use methods::{EventHandlerType, apply_event_handler_return_value};
pub(super) use methods::{
event_cancel_bubble_getter_function, event_cancel_bubble_setter_function,
event_composed_path_callback, event_prevent_default_callback,
@@ -1,5 +1,11 @@
use super::*;
#[derive(Clone, Copy)]
pub(crate) enum EventHandlerType {
EventHandler,
OnErrorEventHandler,
}
pub(super) fn set_event_default_prevented(
scope: &mut v8::PinScope<'_, '_>,
event: v8::Local<'_, v8::Object>,
@@ -9,19 +15,40 @@ pub(super) fn set_event_default_prevented(
let _ = event.define_own_property(scope, key.into(), value, Default::default());
}
fn prevent_event_default_if_allowed<'s>(
scope: &mut v8::PinScope<'s, '_>,
event: v8::Local<'s, v8::Object>,
) {
if !object_bool_property(scope, event, "cancelable").unwrap_or(false)
|| event_internal_bool_flag(scope, event, EVENT_PASSIVE_SLOT)
{
return;
}
set_event_default_prevented(scope, event);
}
pub(crate) fn apply_event_handler_return_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
event: v8::Local<'s, v8::Object>,
returned: v8::Local<'s, v8::Value>,
handler_type: EventHandlerType,
) {
let cancels = match handler_type {
EventHandlerType::EventHandler => returned.is_boolean() && !returned.boolean_value(scope),
EventHandlerType::OnErrorEventHandler => returned.is_true(),
};
if cancels {
prevent_event_default_if_allowed(scope, event);
}
}
pub(in crate::context_bootstrap) fn event_prevent_default_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let event = args.this();
if !object_bool_property(scope, event, "cancelable").unwrap_or(false) {
return;
}
if event_internal_bool_flag(scope, event, EVENT_PASSIVE_SLOT) {
return;
}
set_event_default_prevented(scope, event);
prevent_event_default_if_allowed(scope, event);
}
pub(in crate::context_bootstrap) fn event_return_value_getter_function<'s>(
@@ -6,6 +6,7 @@ use crate::{
clear_event_dispatch_fields, event_internal_bool_flag, set_event_composed_path,
set_event_dispatch_fields, set_event_internal_flag,
},
context_bootstrap::{EventHandlerType, apply_event_handler_return_value},
exception_reporting::CallbackExceptionLogLevel,
host::report_event_callback_exception,
util::{context_host_ptr_from_global_bridge, serialize_v8_array},
@@ -78,7 +79,7 @@ pub(crate) fn dispatch_simple_event_target_event<'s>(
.get_creation_context(scope)
.unwrap_or(current_context);
let incumbent_context = scope.get_incumbent_context().unwrap_or(current_context);
let _ = invoke_simple_event_callback(
let returned = invoke_simple_event_callback(
scope,
event_type,
&format!("simple event target {handler_name}"),
@@ -90,6 +91,14 @@ pub(crate) fn dispatch_simple_event_target_event<'s>(
&[event.into()],
event,
);
if let Some(returned) = returned {
apply_event_handler_return_value(
scope,
event,
v8::Local::new(scope, &returned),
EventHandlerType::EventHandler,
);
}
}
}
@@ -122,7 +131,7 @@ pub(crate) fn dispatch_simple_event_target_event<'s>(
);
}
set_event_internal_flag(scope, event, EVENT_PASSIVE_SLOT, listener.passive);
let _ = invoke_simple_event_listener(
let returned = invoke_simple_event_listener(
scope,
event_type,
&format!("simple event target {event_type} listener"),
@@ -131,6 +140,16 @@ pub(crate) fn dispatch_simple_event_target_event<'s>(
&[event.into()],
event,
);
if listener.handler_slot.is_some()
&& let Some(returned) = returned
{
apply_event_handler_return_value(
scope,
event,
v8::Local::new(scope, &returned),
EventHandlerType::EventHandler,
);
}
set_event_internal_flag(scope, event, EVENT_PASSIVE_SLOT, false);
if event_stop_immediate_propagation(scope, event) {
break 'phases;
@@ -1,6 +1,6 @@
use super::{
CHILD_BROWSING_CONTEXT_HANDLE_SLOT, MessagePortRealmBinding,
ensure_message_port_wrapper_for_id_in_realm,
CHILD_BROWSING_CONTEXT_HANDLE_SLOT, EventHandlerType, MessagePortRealmBinding,
apply_event_handler_return_value, ensure_message_port_wrapper_for_id_in_realm,
events::{clear_event_dispatch_fields, set_event_dispatch_fields},
invoke_simple_event_listener,
navigation_serialize::{
@@ -817,12 +817,12 @@ fn dispatch_shared_worker_error_event<'s>(
);
if listener.handler_slot.as_deref() == Some(SHARED_WORKER_ONERROR_SLOT)
&& let Some(returned) = callback_result
&& v8::Local::new(scope, &returned).boolean_value(scope)
{
let _ = event.set(
apply_event_handler_return_value(
scope,
v8str(scope, "defaultPrevented").into(),
v8::Boolean::new(scope, true).into(),
event,
v8::Local::new(scope, &returned),
EventHandlerType::EventHandler,
);
}
if listener.once {
@@ -5,7 +5,7 @@ use moli_webapi_declare::WebApiObject;
use super::{WORKER_LISTENERS_SLOT, WORKER_ONERROR_SLOT};
use crate::context_bootstrap::{
dispatch_simple_event_target_event,
EventHandlerType, apply_event_handler_return_value, dispatch_simple_event_target_event,
events::{clear_event_dispatch_fields, set_event_dispatch_fields},
invoke_simple_event_listener, simple_object_event_listeners_snapshot,
simple_object_event_remove_listener_value_for_type,
@@ -313,12 +313,12 @@ pub(crate) fn dispatch_worker_error_event_with_kind<'s>(
);
if listener.handler_slot.as_deref() == Some(WORKER_ONERROR_SLOT)
&& let Some(returned) = callback_result
&& v8::Local::new(scope, &returned).boolean_value(scope)
{
let _ = event.set(
apply_event_handler_return_value(
scope,
v8str(scope, "defaultPrevented").into(),
v8::Boolean::new(scope, true).into(),
event,
v8::Local::new(scope, &returned),
EventHandlerType::EventHandler,
);
}
if listener.once {
@@ -1442,6 +1442,72 @@ async fn constructor_onerror_has_event_fields() {
panic!("timed out waiting for error event fields");
}
#[tokio::test]
async fn constructor_onerror_return_true_does_not_cancel_syntax_error_event() {
ensure_v8();
let (mut isolate, ctx) = setup_worker_context();
eval_ok(
&mut isolate,
&ctx,
r#"
var errorReturnObservations = null;
var w = new Worker("function {");
w.onerror = function(e) {
errorReturnObservations = [arguments.length, e.defaultPrevented === true];
return true;
};
w.addEventListener("error", function(e) {
errorReturnObservations.push(e.defaultPrevented === true);
});
"#,
);
for _ in 0..50 {
sleep(Duration::from_millis(20)).await;
let result = eval_ok(
&mut isolate,
&ctx,
"__drainWorkerMessages(w); JSON.stringify(errorReturnObservations)",
);
if result != "null" {
assert_eq!(result, "[1,false,false]");
eval_ok(&mut isolate, &ctx, "w.terminate()");
return;
}
}
panic!("timed out waiting for worker syntax error event");
}
#[test]
fn worker_event_handler_only_boolean_false_cancels_synthetic_event() {
ensure_v8();
let (mut isolate, ctx) = setup_worker_context();
let result = eval_ok(
&mut isolate,
&ctx,
r#"
(() => {
const w = new Worker("");
const run = returned => {
w.onerror = () => returned;
const event = { type: "error", cancelable: true, defaultPrevented: false };
return [w.dispatchEvent(event), event.defaultPrevented];
};
const observations = [run(true), run(false), run(0), run("")];
w.terminate();
return JSON.stringify(observations);
})()
"#,
);
assert_eq!(
result,
"[[true,false],[false,true],[true,false],[true,false]]"
);
}
#[tokio::test]
async fn constructor_error_listener_receives_event() {
ensure_v8();
+10 -16
View File
@@ -1,8 +1,9 @@
use super::*;
use crate::{
context_bootstrap::{
CHILD_BROWSING_CONTEXT_HANDLE_SLOT, clear_event_composed_path, event_is_error_event,
mark_event_trusted, set_event_composed_path,
CHILD_BROWSING_CONTEXT_HANDLE_SLOT, EventHandlerType, apply_event_handler_return_value,
clear_event_composed_path, event_is_error_event, mark_event_trusted,
set_event_composed_path,
},
document_runtime::DocumentRuntime,
dom_parser::DOM_PARSER_FOREIGN_NODE_SLOT,
@@ -452,13 +453,12 @@ fn invoke_registered_event_handler<'s>(
);
if let Some(returned) = returned {
let returned = v8::Local::new(scope, returned);
if returned.boolean_value(scope) {
let _ = event.set(
scope,
v8str(scope, "defaultPrevented").into(),
v8::Boolean::new(scope, true).into(),
);
}
apply_event_handler_return_value(
scope,
event,
returned,
EventHandlerType::OnErrorEventHandler,
);
}
if let Some(timing_started) = timing_started {
tracing::info!(
@@ -486,13 +486,7 @@ fn invoke_registered_event_handler<'s>(
);
if let Some(returned) = returned {
let returned = v8::Local::new(scope, returned);
if returned.is_boolean() && !returned.boolean_value(scope) {
let _ = event.set(
scope,
v8str(scope, "defaultPrevented").into(),
v8::Boolean::new(scope, true).into(),
);
}
apply_event_handler_return_value(scope, event, returned, EventHandlerType::EventHandler);
}
if let Some(timing_started) = timing_started {
tracing::info!(
@@ -6722,7 +6722,7 @@ async fn worker_runtime_error_promise_reaction_can_prevent_window_propagation()
}
#[tokio::test]
async fn worker_runtime_error_return_truthy_suppresses_window_onerror() {
async fn worker_runtime_error_return_non_boolean_truthy_propagates_to_window_onerror() {
run_page_vm_async_test(async move {
let (base_url, server) = spawn_single_response_http_server(
"HTTP/1.1 200 OK",
@@ -6759,8 +6759,8 @@ async fn worker_runtime_error_return_truthy_suppresses_window_onerror() {
)?;
drive_websocket_until_done(
&mut page_vm,
"String(globalThis.__workerDone === true)",
"truthy worker onerror should suppress window.onerror",
"String(globalThis.__workerDone === true && globalThis.__windowErrorCalled === true)",
"non-boolean truthy worker onerror should propagate to window.onerror",
)
.await?;
page_vm.vm_mut().eval(
@@ -6768,14 +6768,14 @@ async fn worker_runtime_error_return_truthy_suppresses_window_onerror() {
)
})
.await
.expect("worker truthy onerror suppression test should run on owner lane");
.expect("worker non-boolean truthy onerror propagation test should run on owner lane");
server
.await
.expect("worker truthy onerror suppression server should finish");
.expect("worker non-boolean truthy onerror propagation server should finish");
assert_eq!(
result,
r#"{"windowErrorCalled":false,"listenerSawDefaultPrevented":true}"#
r#"{"windowErrorCalled":true,"listenerSawDefaultPrevented":false}"#
);
})
.await;
@@ -1719,6 +1719,31 @@ fn synthetic_error_event_uses_normal_window_event_handler_arguments() {
);
}
#[test]
fn window_error_handler_only_boolean_true_cancels() {
let mut vm = new_storage_test_vm("https://window-error-return-value.test/");
let result = vm
.eval(
r#"
(() => {
const run = returned => {
window.onerror = () => returned;
const event = new ErrorEvent("error", { cancelable: true });
return [window.dispatchEvent(event), event.defaultPrevented];
};
return JSON.stringify([run(true), run(1), run(false), run(0)]);
})()
"#,
)
.expect("window error handler return-value probe should evaluate");
assert_eq!(
result,
"[[false,true],[true,false],[true,false],[true,false]]"
);
}
#[test]
fn window_onerror_null_assignment_supersedes_uncompiled_body_attribute() {
let mut vm = new_storage_test_vm("https://window-onerror-null-override.test/");
@@ -12,7 +12,8 @@ use moli_webapi_declare::WebApiObject;
use crate::callback_invocation::{CallbackInvocationOutcome, CallbackInvoker};
use crate::context_bootstrap::{
EVENT_DISPATCHING_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, EVENT_STOP_PROPAGATION_SLOT,
SimpleObjectEventListenerSnapshot, dispatch_message_port_events_for_port_collecting_errors,
EventHandlerType, SimpleObjectEventListenerSnapshot, apply_event_handler_return_value,
dispatch_message_port_events_for_port_collecting_errors,
dispatch_service_worker_controller_change, ensure_message_port_wrapper_for_id,
event_internal_bool_flag, mark_event_trusted, runtime_message_allowed_for_current_target,
set_event_internal_flag, simple_object_event_listeners_snapshot,
@@ -1048,13 +1049,12 @@ pub(super) fn dispatch_worker_error_event<'s>(
],
) {
Ok(returned) => {
if v8::Local::new(scope, &returned).is_true() {
let _ = event.set(
scope,
v8str(scope, "defaultPrevented").into(),
v8::Boolean::new(scope, true).into(),
);
}
apply_event_handler_return_value(
scope,
event,
v8::Local::new(scope, &returned),
EventHandlerType::OnErrorEventHandler,
);
}
Err(nested_report) => {
report_exception_to_parent(
@@ -92,13 +92,16 @@ promise_test(async function () {
});
const defaultPrevented = await wait_for(listenerSeen, "worker error listener");
await wait_for(handlerSeen, "worker.onerror truthy handler");
await wait_for(handlerSeen, "worker.onerror non-boolean truthy handler");
await wait(30);
assert_true(defaultPrevented, "truthy worker.onerror should mark the error as handled");
assert_false(
defaultPrevented,
"a non-boolean truthy worker.onerror return should not mark the error as handled",
);
} finally {
window.onerror = previousOnerror;
worker.terminate();
}
}, "Truthy worker.onerror marks the worker error as handled for later listeners");
}, "A non-boolean truthy Worker.onerror return does not cancel the error event");
</script>
@@ -97,17 +97,20 @@ async function assert_worker_error_prevent_default_suppresses_window() {
}
}
async function assert_truthy_worker_onerror_suppresses_window() {
async function assert_non_boolean_truthy_worker_onerror_propagates_to_window() {
const previousOnerror = window.onerror;
let worker = null;
let windowErrorCalled = false;
let handlerSeen = false;
try {
window.onerror = function () {
windowErrorCalled = true;
return true;
};
const windowSeen = new Promise(function (resolve) {
window.onerror = function () {
windowErrorCalled = true;
resolve();
return true;
};
});
worker = new Worker(worker_error_url());
worker.onerror = function () {
@@ -125,12 +128,19 @@ async function assert_truthy_worker_onerror_suppresses_window() {
{ once: true },
);
}),
"worker error listener after truthy onerror",
"worker error listener after non-boolean truthy onerror",
);
await wait_for(windowSeen, "window.onerror after non-boolean truthy worker.onerror");
assert_true(handlerSeen, "worker.onerror should run before the later listener");
assert_true(listenerDefaultPrevented, "truthy worker.onerror should mark the worker error handled");
assert_false(windowErrorCalled, "truthy worker.onerror should suppress window.onerror");
assert_false(
listenerDefaultPrevented,
"a non-boolean truthy worker.onerror return should not mark the worker error handled",
);
assert_true(
windowErrorCalled,
"a non-boolean truthy worker.onerror return should propagate to window.onerror",
);
} finally {
window.onerror = previousOnerror;
if (worker) {
@@ -142,6 +152,6 @@ async function assert_truthy_worker_onerror_suppresses_window() {
promise_test(async function () {
await assert_unhandled_worker_error_propagates_to_window();
await assert_worker_error_prevent_default_suppresses_window();
await assert_truthy_worker_onerror_suppresses_window();
await assert_non_boolean_truthy_worker_onerror_propagates_to_window();
}, "Dedicated worker runtime errors propagate to page window.onerror unless handled");
</script>