diff --git a/moli-core/tests/web_apis.rs b/moli-core/tests/web_apis.rs index f97dad368c..5619670e62 100644 --- a/moli-core/tests/web_apis.rs +++ b/moli-core/tests/web_apis.rs @@ -10,6 +10,9 @@ use moli_fetch::FetchConfig; use support::FixtureServer; use tokio::time::Duration; +#[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/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index ae944cee1f..f3dff1d7cd 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -311,9 +311,9 @@ pub(crate) use self::media_queries::{ mark_simple_event_target_slot, simple_event_target_add_event_listener_callback, simple_event_target_dispatch_event_callback, simple_event_target_inspector_listener_snapshots, simple_event_target_remove_event_listener_callback, simple_event_target_slot_name, - simple_object_event_listeners_snapshot, simple_object_event_remove_listener_value_for_type, - simple_object_event_set_ordered_handler, simple_object_event_target_add_listener, - simple_object_event_target_remove_listener, + simple_object_event_listener_is_registered, simple_object_event_listeners_snapshot, + simple_object_event_remove_listener_value_for_type, simple_object_event_set_ordered_handler, + simple_object_event_target_add_listener, simple_object_event_target_remove_listener, }; pub(crate) use self::message_ports::{ MessagePortDeliveryRunResult, MessagePortEventListenerId, MessagePortEventListenerSnapshot, diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries.rs b/moli-renderer-v8/src/context_bootstrap/media_queries.rs index 68e07773c1..24834eeff0 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries.rs @@ -14,9 +14,9 @@ pub(crate) use self::events::{ mark_simple_event_target_slot, simple_event_target_add_event_listener_callback, simple_event_target_dispatch_event_callback, simple_event_target_inspector_listener_snapshots, simple_event_target_remove_event_listener_callback, simple_event_target_slot_name, - simple_object_event_listeners_snapshot, simple_object_event_remove_listener_value_for_type, - simple_object_event_set_ordered_handler, simple_object_event_target_add_listener, - simple_object_event_target_remove_listener, + simple_object_event_listener_is_registered, simple_object_event_listeners_snapshot, + simple_object_event_remove_listener_value_for_type, simple_object_event_set_ordered_handler, + simple_object_event_target_add_listener, simple_object_event_target_remove_listener, }; pub(super) use self::events::{ media_query_list_add_event_listener_callback, media_query_list_add_listener_callback, diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events.rs index f00a16faa4..60206bd71a 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events.rs @@ -20,8 +20,8 @@ pub(crate) use simple_event_target::{ mark_simple_event_target_slot, simple_event_target_add_event_listener_callback, simple_event_target_dispatch_event_callback, simple_event_target_inspector_listener_snapshots, simple_event_target_remove_event_listener_callback, simple_event_target_slot_name, - simple_object_event_listeners_snapshot, simple_object_event_remove_listener_value_for_type, - simple_object_event_set_ordered_handler, simple_object_event_target_add_listener, - simple_object_event_target_register_webidl_listener, + simple_object_event_listener_is_registered, simple_object_event_listeners_snapshot, + simple_object_event_remove_listener_value_for_type, simple_object_event_set_ordered_handler, + simple_object_event_target_add_listener, simple_object_event_target_register_webidl_listener, simple_object_event_target_remove_listener, }; 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 d6ec3d8948..a52c5acf9f 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, @@ -497,6 +501,12 @@ impl JsContextHost { }; let _ = event.set(scope, v8str(scope, "target").into(), target); let _ = event.set(scope, v8str(scope, "currentTarget").into(), window.into()); + let _ = event.set( + scope, + 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); @@ -545,6 +555,19 @@ impl JsContextHost { break; } } + let _ = event.set( + scope, + v8str(scope, "eventPhase").into(), + v8::Integer::new(scope, 0).into(), + ); + let _ = 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 718a83fb90..4cad04aade 100644 --- a/moli-renderer-v8/src/worker/thread/dispatch.rs +++ b/moli-renderer-v8/src/worker/thread/dispatch.rs @@ -16,8 +16,8 @@ use crate::context_bootstrap::{ 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, - simple_object_event_remove_listener_value_for_type, + set_event_internal_flag, simple_object_event_listener_is_registered, + simple_object_event_listeners_snapshot, simple_object_event_remove_listener_value_for_type, structured_deserialize_value_for_message_event, }; use crate::exception_reporting::{ @@ -695,6 +695,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( @@ -859,6 +861,7 @@ fn dispatch_worker_promise_rejection_event<'s>( listener, global, event, + event_type, "WorkerGlobalScope promise rejection listener", ) { let (report, exception) = *error; @@ -873,16 +876,6 @@ fn dispatch_worker_promise_rejection_event<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - event_type, - listener.original, - listener.capture, - ); - } } let allows_default = !event_bool_property(scope, event, "defaultPrevented"); @@ -997,8 +990,31 @@ fn invoke_worker_listener<'s>( listener: &SimpleObjectEventListenerSnapshot<'s>, target: v8::Local<'s, v8::Object>, event: v8::Local<'s, v8::Object>, + event_type: &str, callback_name: &str, ) -> Result<(), WorkerExceptionError> { + if event_stop_immediate_propagation(scope, event) + || !simple_object_event_listener_is_registered( + scope, + target, + WORKER_GLOBAL_LISTENERS_SLOT, + event_type, + listener.original, + listener.capture, + ) + { + return Ok(()); + } + if listener.once { + simple_object_event_remove_listener_value_for_type( + scope, + target, + WORKER_GLOBAL_LISTENERS_SLOT, + event_type, + listener.original, + listener.capture, + ); + } let arguments = [event.into()]; let invocation = listener.invocation(target.into(), &arguments, Some(event)); match CallbackInvoker::invoke( @@ -1088,6 +1104,7 @@ pub(super) fn dispatch_worker_error_event<'s>( listener, global, event, + "error", "WorkerGlobalScope error listener", ) { let (nested_report, nested_exception) = *nested_report; @@ -1099,16 +1116,6 @@ pub(super) fn dispatch_worker_error_event<'s>( parent_tx, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "error", - listener.original, - listener.capture, - ); - } } let handled = event_bool_property(scope, event, "defaultPrevented"); @@ -1332,6 +1339,7 @@ fn dispatch_service_worker_lifecycle_event_in_context<'s>( listener, global, event_object, + event_type, "ServiceWorkerGlobalScope lifecycle listener", ) { let (report, exception) = *error; @@ -1346,16 +1354,6 @@ fn dispatch_service_worker_lifecycle_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - event_type, - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -1458,6 +1456,7 @@ fn dispatch_service_worker_fetch_event_in_context<'s>( listener, global, event_object, + "fetch", "ServiceWorkerGlobalScope fetch listener", ) { let (report, exception) = *error; @@ -1472,16 +1471,6 @@ fn dispatch_service_worker_fetch_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "fetch", - listener.original, - listener.capture, - ); - } if event_stop_immediate_propagation(scope, event_object) { break; } @@ -1549,6 +1538,7 @@ fn dispatch_service_worker_message_event_in_context<'s>( listener, global, event_object, + event_type, "ServiceWorkerGlobalScope message listener", ) { let (report, exception) = *error; @@ -1563,16 +1553,6 @@ fn dispatch_service_worker_message_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - event_type, - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -1636,6 +1616,7 @@ fn dispatch_service_worker_notification_event_in_context<'s>( listener, global, event_object, + event_type, "ServiceWorkerGlobalScope notification listener", ) { let (report, exception) = *error; @@ -1650,16 +1631,6 @@ fn dispatch_service_worker_notification_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - event_type, - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -1711,6 +1682,7 @@ fn dispatch_service_worker_push_event_in_context<'s>( listener, global, event_object, + "push", "ServiceWorkerGlobalScope push listener", ) { let (report, exception) = *error; @@ -1725,16 +1697,6 @@ fn dispatch_service_worker_push_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "push", - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -1790,6 +1752,7 @@ fn dispatch_service_worker_sync_event_in_context<'s>( listener, global, event_object, + "sync", "ServiceWorkerGlobalScope sync listener", ) { let (report, exception) = *error; @@ -1804,16 +1767,6 @@ fn dispatch_service_worker_sync_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "sync", - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -1873,6 +1826,7 @@ fn dispatch_service_worker_periodic_sync_event_in_context<'s>( listener, global, event_object, + "periodicsync", "ServiceWorkerGlobalScope periodicsync listener", ) { let (report, exception) = *error; @@ -1887,16 +1841,6 @@ fn dispatch_service_worker_periodic_sync_event_in_context<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "periodicsync", - listener.original, - listener.capture, - ); - } } perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); clear_event_dispatch_fields(scope, event_object); @@ -4403,6 +4347,7 @@ fn dispatch_worker_global_message_event<'s>( listener, global, event, + event_type, "WorkerGlobalScope message listener", ) { let (report, exception) = *error; @@ -4417,16 +4362,6 @@ fn dispatch_worker_global_message_event<'s>( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - event_type, - listener.original, - listener.capture, - ); - } } clear_event_dispatch_fields(scope, event); @@ -4520,6 +4455,7 @@ pub(super) fn dispatch_shared_worker_connect_event( listener, global, event, + "connect", "SharedWorkerGlobalScope connect listener", ) { let (report, exception) = *error; @@ -4534,16 +4470,6 @@ pub(super) fn dispatch_shared_worker_connect_event( script_url, ); } - if listener.once { - simple_object_event_remove_listener_value_for_type( - scope, - global, - WORKER_GLOBAL_LISTENERS_SLOT, - "connect", - listener.original, - listener.capture, - ); - } } clear_event_dispatch_fields(scope, event);