From 4c7ad5fc9bdbfd123710ec0d2333204cd708a46a Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 9 Sep 2026 17:50:18 +0800 Subject: [PATCH] fix(events): preserve native dispatch state and listener lifetime Set and clear child Window event dispatch fields, and clear Worker propagation flags after native delivery. Remove once listeners before invocation and skip listeners removed by an earlier callback, including during synchronous nested dispatch. Add Window, child Window, and Worker regression probes for active event state, redispatch after stopImmediatePropagation, and once/listener removal during nested dispatch. --- moli-core/tests/web_apis.rs | 3 + moli-core/tests/web_apis/event_dispatch.rs | 129 ++++++++++++++++++ .../context_host/child_events.rs | 31 +++-- .../src/worker/thread/dispatch.rs | 2 + 4 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 moli-core/tests/web_apis/event_dispatch.rs diff --git a/moli-core/tests/web_apis.rs b/moli-core/tests/web_apis.rs index 68b2913a22..9144322ecf 100644 --- a/moli-core/tests/web_apis.rs +++ b/moli-core/tests/web_apis.rs @@ -30,6 +30,9 @@ use tokio::time::Duration; #[path = "web_apis/abort_signal.rs"] mod abort_signal; +#[path = "web_apis/event_dispatch.rs"] +mod event_dispatch; + fn diagnostic_global<'a>( page: &'a moli_core::page::Page, name: &str, diff --git a/moli-core/tests/web_apis/event_dispatch.rs b/moli-core/tests/web_apis/event_dispatch.rs new file mode 100644 index 0000000000..4dc61ea1aa --- /dev/null +++ b/moli-core/tests/web_apis/event_dispatch.rs @@ -0,0 +1,129 @@ +use super::*; + +fn markup_url(server: &FixtureServer, markup: &str) -> String { + let mut url = url::Url::parse(&server.url("/compat/child-dynamic-markup-document")).unwrap(); + url.query_pairs_mut().append_pair("markup", markup); + url.into() +} + +pub(super) async fn run_probe( + browser: &Browser, + server: &FixtureServer, + target: &str, + source: &str, +) -> Result { + let markup = if target == "worker" { + let source = serde_json::to_string(&format!( + "self.finish = value => postMessage(value);\n{source}" + ))?; + format!( + r#""# + ) + } else { + format!( + "" + ) + }; + let url = markup_url(server, &markup); + let url = if target == "child" { + markup_url( + server, + &format!( + "", + url.replace('&', "&") + ), + ) + } else { + url + }; + let mut page = browser.fetch(&url).await?; + let expression = if target == "child" { + "document.getElementById('target').contentWindow.done.then(JSON.stringify)" + } else { + "done.then(JSON.stringify)" + }; + let value = page + .evaluate_runtime_expression_with_await_async(expression, true) + .await?; + Ok(serde_json::from_str( + value["value"].as_str().expect("callback probe result"), + )?) +} + +#[tokio::test(flavor = "multi_thread")] +async fn native_message_events_keep_dispatch_state_and_clear_propagation_flags() -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let source = r#" + const log = []; + addEventListener('message', event => { + log.push(['during', event.currentTarget === self, event.eventPhase === 2, + typeof document === 'undefined' || window.event === event]); + event.initEvent('mutated', true, true); + log.push(event.type); + event.stopImmediatePropagation(); + setTimeout(() => { + log.push(['after', event.currentTarget === null, event.eventPhase === 0, + typeof document === 'undefined' || window.event === undefined]); + dispatchEvent(event); + finish(log); + }, 0); + }, {once: true}); + addEventListener('message', () => log.push('second')); + if (typeof document !== 'undefined') postMessage('go', '*'); + "#; + for target in ["window", "child", "worker"] { + let observed = run_probe(&browser, &server, target, source).await?; + assert_eq!( + observed, + serde_json::json!([ + ["during", true, true, true], + "message", + ["after", true, true, true], + "second" + ]), + "target={target}" + ); + } + server.shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn native_message_listeners_observe_once_and_removal_during_nested_dispatch() -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let source = r#" + const log = []; + let calls = 0; + const removed = () => log.push('unexpected removed listener'); + addEventListener('message', () => { + log.push('once'); + if (++calls > 1) return; + removeEventListener('message', removed); + dispatchEvent(new Event('message')); + }, {once: true}); + addEventListener('message', removed); + addEventListener('message', () => { + log.push('retained'); + setTimeout(() => finish(log), 0); + }); + if (typeof document !== 'undefined') postMessage('go', '*'); + "#; + for target in ["window", "child", "worker"] { + let observed = run_probe(&browser, &server, target, source).await?; + assert_eq!( + observed, + serde_json::json!(["once", "retained", "retained"]), + "target={target}" + ); + } + server.shutdown().await; + Ok(()) +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_events.rs b/moli-renderer-v8/src/native_bridge/context_host/child_events.rs index e3049477b1..350ffeb1ff 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_events.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_events.rs @@ -2,7 +2,11 @@ use super::{ JsContextHost, OwnerDispatchScope, child_frame_runtime::WINDOW_EVENT_HANDLER_PROPERTIES, }; use crate::{ - context_bootstrap::{EventHandlerType, apply_event_handler_return_value, event_is_error_event}, + context_bootstrap::{ + EVENT_DISPATCHING_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, EVENT_STOP_PROPAGATION_SLOT, + EventHandlerType, apply_event_handler_return_value, event_is_error_event, + set_event_internal_flag, + }, document_runtime::DomHandle, document_runtime::EventTargetHandle, exception_reporting::invoke_event_handler, @@ -495,16 +499,14 @@ impl JsContextHost { } else { window.into() }; + let _ = crate::context_bootstrap::event_backing(scope, event).set(scope, v8str(scope, "target").into(), target); + let _ = crate::context_bootstrap::event_backing(scope, event).set(scope, v8str(scope, "currentTarget").into(), window.into()); let _ = crate::context_bootstrap::event_backing(scope, event).set( scope, - v8str(scope, "target").into(), - target, - ); - let _ = crate::context_bootstrap::event_backing(scope, event).set( - scope, - v8str(scope, "currentTarget").into(), - window.into(), + v8str(scope, "eventPhase").into(), + v8::Integer::new(scope, 2).into(), ); + set_event_internal_flag(scope, event, EVENT_DISPATCHING_SLOT, true); if event_type == "load" { install_child_body_load_attribute_handler_if_needed(scope, self, handle); @@ -553,6 +555,19 @@ impl JsContextHost { break; } } + let _ = crate::context_bootstrap::event_backing(scope, event).set( + scope, + v8str(scope, "eventPhase").into(), + v8::Integer::new(scope, 0).into(), + ); + let _ = crate::context_bootstrap::event_backing(scope, event).set( + scope, + v8str(scope, "currentTarget").into(), + v8::null(scope).into(), + ); + set_event_internal_flag(scope, event, EVENT_DISPATCHING_SLOT, false); + set_event_internal_flag(scope, event, EVENT_STOP_PROPAGATION_SLOT, false); + set_event_internal_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, false); self.pop_child_subresource_request_scope(); restore_child_window_event_dispatch(scope, previous_active_child_window); } diff --git a/moli-renderer-v8/src/worker/thread/dispatch.rs b/moli-renderer-v8/src/worker/thread/dispatch.rs index 534ec02964..9e87f22381 100644 --- a/moli-renderer-v8/src/worker/thread/dispatch.rs +++ b/moli-renderer-v8/src/worker/thread/dispatch.rs @@ -662,6 +662,8 @@ fn clear_event_dispatch_fields(scope: &mut v8::PinScope<'_, '_>, event: v8::Loca EVENT_DISPATCHING_SLOT, v8::Boolean::new(scope, false).into(), ); + set_event_internal_flag(scope, event, EVENT_STOP_PROPAGATION_SLOT, false); + set_event_internal_flag(scope, event, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, false); } fn worker_event_prevent_default_callback(