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(